From eca8ca21bc6634b07c4a9fbf6b818ccae8ce0f54 Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 13 Dec 2022 19:39:19 +0200 Subject: [PATCH 001/208] WIP: Add Ethereum --- assets/images/ethereum.png | Bin 0 -> 351492 bytes assets/svg/coin_icons/Ethereum.svg | 21 ++ .../add_edit_node_view.dart | 15 +- lib/services/coins/coin_service.dart | 9 + .../coins/ethereum/ethereum_wallet.dart | 241 ++++++++++++++++++ lib/utilities/address_utils.dart | 2 + lib/utilities/assets.dart | 6 + lib/utilities/block_explorers.dart | 2 + lib/utilities/constants.dart | 6 + lib/utilities/default_nodes.dart | 17 ++ lib/utilities/enums/coin_enum.dart | 19 ++ lib/utilities/theme/color_theme.dart | 3 + lib/utilities/theme/stack_colors.dart | 2 + pubspec.lock | 128 ++++++---- pubspec.yaml | 7 + 15 files changed, 426 insertions(+), 52 deletions(-) create mode 100644 assets/images/ethereum.png create mode 100644 assets/svg/coin_icons/Ethereum.svg create mode 100644 lib/services/coins/ethereum/ethereum_wallet.dart diff --git a/assets/images/ethereum.png b/assets/images/ethereum.png new file mode 100644 index 0000000000000000000000000000000000000000..2827ad56e3350a25f1d85d012e5a1f41128db9cb GIT binary patch literal 351492 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¨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

dt!HjOSSY=_?;tPX-y6MVa(@beAhsLSevT!68PR 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Ν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


O|qi3rtjnYTMcUa(qM7f(Mh5= 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 g0)2z5FoUiS(;ZvQR3Qjj(IfSzs56Nq7Yri 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 + + + + + + + + + + + + + + + + + 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 32fa3974a..4f29fc5ab 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 @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:http/http.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; @@ -30,6 +31,7 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:uuid/uuid.dart'; +import 'package:web3dart/web3dart.dart'; enum AddEditNodeViewType { add, edit } @@ -157,11 +159,19 @@ class _AddEditNodeViewState extends ConsumerState { try { testPassed = await client.ping(); - } catch (_) { - testPassed = false; + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Warning); } break; + + case Coin.ethereum: + final client = Web3Client( + "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", + Client()); + try { + await client.getSyncStatus(); + } catch (_) {} } if (showFlushBar) { @@ -695,6 +705,7 @@ class _NodeFormState extends ConsumerState { return false; case Coin.epicCash: + case Coin.ethereum: case Coin.monero: case Coin.wownero: return true; diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 16015ea0c..1793624b4 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart'; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; @@ -172,6 +173,14 @@ abstract class CoinServiceAPI { // tracker: tracker, ); + case Coin.ethereum: + return EthereumWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + secureStore: secureStorageInterface, + ); + case Coin.monero: return MoneroWallet( walletId: walletId, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart new file mode 100644 index 000000000..3919611ae --- /dev/null +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -0,0 +1,241 @@ +import 'dart:math'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:decimal/decimal.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/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:string_to_hex/string_to_hex.dart'; +import 'package:web3dart/credentials.dart'; +import 'package:web3dart/web3dart.dart'; +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'; + +const int MINIMUM_CONFIRMATIONS = 1; +const int DUST_LIMIT = 294; + +const String GENESIS_HASH_MAINNET = + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; +const String GENESIS_HASH_TESTNET = + "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; + +class EthereumWallet extends CoinServiceAPI { + @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 PriceAPI _priceAPI; + + EthereumWallet( + {required String walletId, + required String walletName, + required Coin coin, + PriceAPI? priceAPI, + required SecureStorageInterface secureStore}) { + _walletId = walletId; + _walletName = walletName; + _coin = coin; + + _priceAPI = priceAPI ?? PriceAPI(Client()); + _secureStore = secureStore; + } + + @override + bool shouldAutoSync = false; + + @override + String get walletName => _walletName; + late String _walletName; + + late Coin _coin; + + @override + // TODO: implement allOwnAddresses + Future> get allOwnAddresses => throw UnimplementedError(); + + @override + // TODO: implement availableBalance + Future get availableBalance => throw UnimplementedError(); + + @override + // TODO: implement balanceMinusMaxFee + Future get balanceMinusMaxFee => throw UnimplementedError(); + + @override + Future confirmSend({required Map txData}) { + // TODO: implement confirmSend + throw UnimplementedError(); + } + + @override + // TODO: implement currentReceivingAddress + Future get currentReceivingAddress => throw UnimplementedError(); + + @override + Future estimateFeeFor(int satoshiAmount, int feeRate) { + // TODO: implement estimateFeeFor + throw UnimplementedError(); + } + + @override + Future exit() { + // TODO: implement exit + throw UnimplementedError(); + } + + @override + // TODO: implement fees + Future get fees => throw UnimplementedError(); + + @override + Future fullRescan( + int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { + // TODO: implement fullRescan + throw UnimplementedError(); + } + + @override + Future generateNewAddress() { + // TODO: implement generateNewAddress + throw UnimplementedError(); + } + + @override + // TODO: implement hasCalledExit + bool get hasCalledExit => throw UnimplementedError(); + + @override + Future initializeExisting() { + // TODO: implement initializeExisting + throw UnimplementedError(); + } + + @override + Future initializeNew() { + // TODO: implement initializeNew + throw UnimplementedError(); + } + + @override + // TODO: implement isConnected + bool get isConnected => throw UnimplementedError(); + + @override + // TODO: implement isRefreshing + bool get isRefreshing => throw UnimplementedError(); + + @override + // TODO: implement maxFee + Future get maxFee => throw UnimplementedError(); + + @override + // TODO: implement mnemonic + Future> get mnemonic => throw UnimplementedError(); + + @override + // TODO: implement pendingBalance + Future get pendingBalance => throw UnimplementedError(); + + @override + Future> prepareSend( + {required String address, + required int satoshiAmount, + Map? args}) { + // TODO: implement prepareSend + throw UnimplementedError(); + } + + @override + Future recoverFromMnemonic( + {required String mnemonic, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height}) { + // TODO: implement recoverFromMnemonic + throw UnimplementedError(); + } + + @override + Future refresh() { + // TODO: implement refresh + throw UnimplementedError(); + } + + @override + Future send( + {required String toAddress, + required int amount, + Map args = const {}}) { + // TODO: implement send + throw UnimplementedError(); + } + + @override + Future testNetworkConnection() { + // TODO: implement testNetworkConnection + throw UnimplementedError(); + } + + @override + // TODO: implement totalBalance + Future get totalBalance => throw UnimplementedError(); + + @override + // TODO: implement transactionData + Future get transactionData => throw UnimplementedError(); + + @override + // TODO: implement unspentOutputs + Future> get unspentOutputs => throw UnimplementedError(); + + @override + Future updateNode(bool shouldRefresh) { + // TODO: implement updateNode + throw UnimplementedError(); + } + + @override + Future updateSentCachedTxData(Map txData) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } + + @override + bool validateAddress(String address) { + // TODO: implement validateAddress + throw UnimplementedError(); + } + + @override + String get walletId => _walletId; + late String _walletId; + + @override + @override + set walletName(String newName) => _walletName = newName; +} diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 65303b24d..d1a04d00d 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -51,6 +51,8 @@ class AddressUtils { return Address.validateAddress(address, dogecoin); case Coin.epicCash: return validateSendAddress(address) == "1"; + case Coin.ethereum: + return true; //TODO - validate ETH address case Coin.firo: return Address.validateAddress(address, firoNetwork); case Coin.monero: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 27c8fe3b4..e83a3a7e7 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -176,6 +176,7 @@ class _SVG { String get bitcoincash => "assets/svg/coin_icons/Bitcoincash.svg"; String get dogecoin => "assets/svg/coin_icons/Dogecoin.svg"; String get epicCash => "assets/svg/coin_icons/EpicCash.svg"; + String get ethereum => "assets/svg/coin_icons/Ethereum.svg"; String get firo => "assets/svg/coin_icons/Firo.svg"; String get monero => "assets/svg/coin_icons/Monero.svg"; String get wownero => "assets/svg/coin_icons/Wownero.svg"; @@ -206,6 +207,8 @@ class _SVG { return dogecoin; case Coin.epicCash: return epicCash; + case Coin.ethereum: + return ethereum; case Coin.firo: return firo; case Coin.monero: @@ -239,6 +242,7 @@ class _PNG { String get bitcoin => "assets/images/bitcoin.png"; String get litecoin => "assets/images/litecoin.png"; String get epicCash => "assets/images/epic-cash.png"; + String get ethereum => "assets/images/ethereum.png"; String get bitcoincash => "assets/images/bitcoincash.png"; String get namecoin => "assets/images/namecoin.png"; @@ -261,6 +265,8 @@ class _PNG { return dogecoin; case Coin.epicCash: return epicCash; + case Coin.ethereum: + return ethereum; case Coin.firo: return firo; case Coin.firoTestNet: diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 69afb4a83..27a3d9819 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -20,6 +20,8 @@ Uri getBlockExplorerTransactionUrlFor({ case Coin.epicCash: // TODO: Handle this case. throw UnimplementedError("missing block explorer for epic cash"); + case Coin.ethereum: + return Uri.parse("https://etherscan.io/tx/$txid"); case Coin.monero: return Uri.parse("https://xmrchain.net/tx/$txid"); case Coin.wownero: diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 3263d526e..79916325e 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -53,6 +53,7 @@ abstract class Constants { case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: + case Coin.ethereum: case Coin.namecoin: return _satsPerCoin; @@ -77,6 +78,7 @@ abstract class Constants { case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: + case Coin.ethereum: case Coin.namecoin: return _decimalPlaces; @@ -102,6 +104,7 @@ abstract class Constants { case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: + case Coin.ethereum: case Coin.namecoin: values.addAll([24, 21, 18, 15, 12]); break; @@ -142,6 +145,9 @@ abstract class Constants { case Coin.epicCash: return 60; + case Coin.ethereum: + return 60; + case Coin.monero: return 120; diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index c9e96fbac..c09ebba4f 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:web3dart/browser.dart'; abstract class DefaultNodes { static const String defaultNodeIdPrefix = "default_"; @@ -133,6 +134,19 @@ abstract class DefaultNodes { isDown: false, ); + //TODO - Update with correct node details for ETH + static NodeModel get ethereum => NodeModel( + host: "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", + port: 1234, + name: defaultName, + id: _nodeId(Coin.ethereum), + useSSL: true, + enabled: true, + coinName: Coin.ethereum.name, + isFailover: true, + isDown: false, + ); + static NodeModel get namecoin => NodeModel( host: "namecoin.stackwallet.com", port: 57002, @@ -210,6 +224,9 @@ abstract class DefaultNodes { case Coin.epicCash: return epicCash; + case Coin.ethereum: + return ethereum; + case Coin.firo: return firo; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 543a193ee..a4fd1eea3 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -5,6 +5,8 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' as doge; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart' as epic; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart' + as eth; import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as firo; import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart' as ltc; @@ -19,6 +21,7 @@ enum Coin { bitcoincash, dogecoin, epicCash, + ethereum, firo, litecoin, monero, @@ -52,6 +55,8 @@ extension CoinExt on Coin { return "Dogecoin"; case Coin.epicCash: return "Epic Cash"; + case Coin.ethereum: + return "Ethereum"; case Coin.firo: return "Firo"; case Coin.monero: @@ -85,6 +90,8 @@ extension CoinExt on Coin { return "DOGE"; case Coin.epicCash: return "EPIC"; + case Coin.ethereum: + return "ETH"; case Coin.firo: return "FIRO"; case Coin.monero: @@ -119,6 +126,8 @@ extension CoinExt on Coin { case Coin.epicCash: // TODO: is this actually the right one? return "epic"; + case Coin.ethereum: + return "ethereum"; case Coin.firo: return "firo"; case Coin.monero: @@ -156,6 +165,7 @@ extension CoinExt on Coin { return true; case Coin.epicCash: + case Coin.ethereum: case Coin.monero: case Coin.wownero: return false; @@ -187,6 +197,9 @@ extension CoinExt on Coin { case Coin.epicCash: return epic.MINIMUM_CONFIRMATIONS; + case Coin.ethereum: + return eth.MINIMUM_CONFIRMATIONS; + case Coin.monero: return xmr.MINIMUM_CONFIRMATIONS; @@ -222,6 +235,10 @@ Coin coinFromPrettyName(String name) { case "epicCash": return Coin.epicCash; + case "Ethereum": + case "ethereum": + return Coin.ethereum; + case "Firo": case "firo": return Coin.firo; @@ -287,6 +304,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.dogecoin; case "epic": return Coin.epicCash; + case "eth": + return Coin.ethereum; case "firo": return Coin.firo; case "xmr": diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index e8b491e71..fbb96a5ea 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -217,6 +217,7 @@ class CoinThemeColor { Color get firo => const Color(0xFFFF897A); Color get dogecoin => const Color(0xFFFFE079); Color get epicCash => const Color(0xFFC5C7CB); + Color get ethereum => const Color(0xFFC5C7CB); //TODO - USE CORRECT COLOR FOR ETH Color get monero => const Color(0xFFFF9E6B); Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); @@ -237,6 +238,8 @@ class CoinThemeColor { return dogecoin; case Coin.epicCash: return epicCash; + case Coin.ethereum: + return ethereum; case Coin.firo: case Coin.firoTestNet: return firo; diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 935fa03ae..9326da6d5 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1434,6 +1434,8 @@ class StackColors extends ThemeExtension { return _coin.dogecoin; case Coin.epicCash: return _coin.epicCash; + case Coin.ethereum: + return _coin.ethereum; case Coin.firo: case Coin.firoTestNet: return _coin.firo; diff --git a/pubspec.lock b/pubspec.lock index ffd644d05..81a63d518 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.1.11" args: dependency: transitive description: @@ -56,14 +56,14 @@ packages: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.4.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.9.0" + version: "2.8.2" barcode_scan2: dependency: "direct main" description: @@ -169,7 +169,7 @@ packages: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.2.6" + version: "7.2.7" built_collection: dependency: transitive description: @@ -183,14 +183,21 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.4.1" + version: "8.4.2" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -211,7 +218,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -253,7 +260,7 @@ packages: name: connectivity_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.2.3" connectivity_plus_web: dependency: transitive description: @@ -281,7 +288,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.2.0" cross_file: dependency: transitive description: @@ -372,7 +379,7 @@ packages: name: decimal url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.2" dependency_validator: dependency: "direct dev" description: @@ -435,7 +442,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" ffi: dependency: "direct main" description: @@ -456,7 +463,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "5.2.1" + version: "5.2.4" fixnum: dependency: transitive description: @@ -543,7 +550,7 @@ packages: name: flutter_mobx url: "https://pub.dartlang.org" source: hosted - version: "2.0.6+4" + version: "2.0.6+5" flutter_native_splash: dependency: "direct main" description: @@ -585,35 +592,35 @@ packages: name: flutter_secure_storage_linux url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" flutter_spinkit: dependency: "direct main" description: @@ -627,7 +634,7 @@ packages: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "1.1.5" + version: "1.1.6" flutter_test: dependency: "direct dev" description: flutter @@ -670,7 +677,7 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" hex: dependency: transitive description: @@ -712,7 +719,7 @@ packages: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: "direct main" description: @@ -802,6 +809,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.7.0" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" jsonrpc2: dependency: "direct main" description: @@ -836,7 +850,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" local_auth: dependency: "direct main" description: @@ -864,35 +878,35 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" mobx: dependency: transitive description: name: mobx url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.3" mockingjay: dependency: "direct dev" description: @@ -920,7 +934,7 @@ packages: name: mutex url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" nm: dependency: transitive description: @@ -990,7 +1004,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -1018,7 +1032,7 @@ packages: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.20" + version: "2.0.22" path_provider_ios: dependency: transitive description: @@ -1060,7 +1074,7 @@ packages: name: permission_handler url: "https://pub.dartlang.org" source: hosted - version: "10.1.0" + version: "10.2.0" permission_handler_android: dependency: transitive description: @@ -1144,7 +1158,7 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" pubspec_parse: dependency: transitive description: @@ -1172,7 +1186,7 @@ packages: name: rational url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.2" riverpod: dependency: transitive description: @@ -1200,7 +1214,7 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.5" + version: "0.27.7" share_plus: dependency: "direct main" description: @@ -1228,7 +1242,7 @@ packages: name: share_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "3.2.0" share_plus_web: dependency: transitive description: @@ -1326,7 +1340,7 @@ packages: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" sky_engine: dependency: transitive description: flutter @@ -1366,7 +1380,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -1403,14 +1417,21 @@ packages: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" + 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: @@ -1424,35 +1445,35 @@ packages: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.3.1" + version: "0.3.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.4" + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.12" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.16" + version: "0.4.13" time: dependency: transitive description: @@ -1501,7 +1522,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" universal_io: dependency: transitive description: @@ -1515,14 +1536,14 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.6" + version: "6.1.7" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.19" + version: "6.0.22" url_launcher_ios: dependency: transitive description: @@ -1571,7 +1592,7 @@ packages: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: @@ -1585,7 +1606,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "9.0.0" + version: "8.2.2" wakelock: dependency: "direct main" description: @@ -1628,6 +1649,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + web3dart: + dependency: "direct main" + description: + name: web3dart + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.5" web_socket_channel: dependency: transitive description: @@ -1655,7 +1683,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.1.2" window_size: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d129cec3..232ab10ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,6 +88,11 @@ dependencies: ref: 22279d4bb24ed541b431acd269a1bc50af0f36a0 bs58check: ^1.0.2 + # Eth Plugins + web3dart: + 2.3.5 + string_to_hex: 0.2.2 + # Storage plugins flutter_secure_storage: ^5.0.2 hive: ^2.0.5 @@ -207,6 +212,7 @@ flutter: - assets/images/doge.png - assets/images/bitcoin.png - assets/images/epic-cash.png + - assets/images/ethereum.png - assets/images/bitcoincash.png - assets/images/namecoin.png - assets/images/glasses.png @@ -306,6 +312,7 @@ flutter: - assets/svg/coin_icons/Bitcoincash.svg - assets/svg/coin_icons/Dogecoin.svg - assets/svg/coin_icons/EpicCash.svg + - assets/svg/coin_icons/Ethereum.svg - assets/svg/coin_icons/Firo.svg - assets/svg/coin_icons/Monero.svg - assets/svg/coin_icons/Wownero.svg From 6370e927a22eead22830d950bb93988fe37d50c4 Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 14 Dec 2022 12:15:22 +0200 Subject: [PATCH 002/208] WIP: Add Ethereum --- .../add_edit_node_view.dart | 10 +- .../coins/ethereum/ethereum_wallet.dart | 91 ++++++++++++++++--- lib/services/price.dart | 2 +- lib/utilities/default_nodes.dart | 2 +- pubspec.yaml | 3 +- 5 files changed, 85 insertions(+), 23 deletions(-) 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 4f29fc5ab..8aed2ac67 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 @@ -31,7 +31,7 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:uuid/uuid.dart'; -import 'package:web3dart/web3dart.dart'; +// import 'package:web3dart/web3dart.dart'; enum AddEditNodeViewType { add, edit } @@ -166,11 +166,11 @@ class _AddEditNodeViewState extends ConsumerState { break; case Coin.ethereum: - final client = Web3Client( - "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", - Client()); + // final client = Web3Client( + // "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", + // Client()); try { - await client.getSyncStatus(); + // await client.getSyncStatus(); } catch (_) {} } diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 3919611ae..ce1855f68 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,15 +1,19 @@ import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; import 'package:decimal/decimal.dart'; +import 'package:stack_wallet_backup/generate_password.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/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/prefs.dart'; import 'package:string_to_hex/string_to_hex.dart'; -import 'package:web3dart/credentials.dart'; import 'package:web3dart/web3dart.dart'; +// import 'package:string_to_hex/string_to_hex.dart'; +// import 'package:web3dart/credentials.dart'; +// import 'package:web3dart/web3dart.dart'; import 'package:http/http.dart'; import 'package:stackwallet/hive/db.dart'; @@ -20,9 +24,7 @@ const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = - "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; -const String GENESIS_HASH_TESTNET = - "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"; + "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"; class EthereumWallet extends CoinServiceAPI { @override @@ -50,6 +52,10 @@ class EthereumWallet extends CoinServiceAPI { late SecureStorageInterface _secureStore; late PriceAPI _priceAPI; + final _prefs = Prefs.instance; + final _client = Web3Client( + "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", + Client()); EthereumWallet( {required String walletId, @@ -130,15 +136,56 @@ class EthereumWallet extends CoinServiceAPI { bool get hasCalledExit => throw UnimplementedError(); @override - Future initializeExisting() { - // TODO: implement initializeExisting - throw UnimplementedError(); + Future initializeExisting() async { + Logging.instance.log("Opening existing ${coin.prettyName} wallet.", + level: LogLevel.Info); + + if ((DB.instance.get(boxName: walletId, key: "id")) == 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() { - // TODO: implement initializeNew - throw UnimplementedError(); + Future initializeNew() async { + await _prefs.init(); + final String mnemonic = bip39.generateMnemonic(strength: 256); + final credentials = + EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + + final String password = generatePassword(); + var rng = Random.secure(); + Wallet wallet = Wallet.createNew(credentials, password, rng); + + await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); + + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance.put( + boxName: walletId, key: 'receivingAddresses', value: ["0"]); + 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); } @override @@ -154,8 +201,17 @@ class EthereumWallet extends CoinServiceAPI { Future get maxFee => throw UnimplementedError(); @override - // TODO: implement mnemonic - Future> get mnemonic => throw UnimplementedError(); + Future> get mnemonic => _getMnemonicList(); + + 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 @@ -206,8 +262,11 @@ class EthereumWallet extends CoinServiceAPI { Future get totalBalance => throw UnimplementedError(); @override - // TODO: implement transactionData - Future get transactionData => throw UnimplementedError(); + Future get transactionData => + _transactionData ??= _fetchTransactionData(); + Future? _transactionData; + + TransactionData? cachedTxData; @override // TODO: implement unspentOutputs @@ -231,6 +290,10 @@ class EthereumWallet extends CoinServiceAPI { throw UnimplementedError(); } + Future _fetchTransactionData() async { + throw UnimplementedError(); + } + @override String get walletId => _walletId; late String _walletId; diff --git a/lib/services/price.dart b/lib/services/price.dart index 2514cc12a..2531cb76c 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -87,7 +87,7 @@ class PriceAPI { Map> result = {}; try { final uri = Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero&order=market_cap_desc&per_page=10&page=1&sparkline=false"); + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,ethereum&order=market_cap_desc&per_page=10&page=1&sparkline=false"); // final uri = Uri.parse( // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero%2Cbitcoin%2Cepic-cash%2Czcoin%2Cdogecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index c09ebba4f..2055b9f17 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:web3dart/browser.dart'; +// import 'package:web3dart/browser.dart'; abstract class DefaultNodes { static const String defaultNodeIdPrefix = "default_"; diff --git a/pubspec.yaml b/pubspec.yaml index 232ab10ce..25119a355 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,8 +89,7 @@ dependencies: bs58check: ^1.0.2 # Eth Plugins - web3dart: - 2.3.5 + web3dart: 2.3.5 string_to_hex: 0.2.2 # Storage plugins From c6d5ad598cd586b2fc29261f6c34d725474f58e1 Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 14 Dec 2022 18:09:24 +0200 Subject: [PATCH 003/208] WIP:ADd Ethereum --- .../coins/ethereum/ethereum_wallet.dart | 117 ++++++++++++++---- 1 file changed, 93 insertions(+), 24 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index ce1855f68..513effe8d 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -11,6 +11,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:string_to_hex/string_to_hex.dart'; import 'package:web3dart/web3dart.dart'; +import 'package:web3dart/web3dart.dart' as Transaction; // import 'package:string_to_hex/string_to_hex.dart'; // import 'package:web3dart/credentials.dart'; // import 'package:web3dart/web3dart.dart'; @@ -57,6 +58,8 @@ class EthereumWallet extends CoinServiceAPI { "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", Client()); + late EthPrivateKey _credentials; + EthereumWallet( {required String walletId, required String walletName, @@ -82,25 +85,48 @@ class EthereumWallet extends CoinServiceAPI { @override // TODO: implement allOwnAddresses - Future> get allOwnAddresses => throw UnimplementedError(); + Future> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future>? _allOwnAddresses; + + Future> _fetchAllOwnAddresses() async { + List addresses = []; + final ownAddress = _credentials.address; + addresses.add(ownAddress.toString()); + return addresses; + } @override - // TODO: implement availableBalance - Future get availableBalance => throw UnimplementedError(); + Future get availableBalance async { + EtherAmount ethBalance = await _client.getBalance(_credentials.address); + return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); + } @override // TODO: implement balanceMinusMaxFee Future get balanceMinusMaxFee => throw UnimplementedError(); @override - Future confirmSend({required Map txData}) { - // TODO: implement confirmSend - throw UnimplementedError(); + Future confirmSend({required Map txData}) async { + final transaction = await _client.sendTransaction( + _credentials, + Transaction( + to: EthereumAddress.fromHex( + '0xC914Bb2ba888e3367bcecEb5C2d99DF7C7423706'), + gasPrice: EtherAmount.inWei(BigInt.one), + maxGas: 100000, + value: EtherAmount.fromUnitAndValue(EtherUnit.ether, 1), + ), + ); + + return transaction; } @override - // TODO: implement currentReceivingAddress - Future get currentReceivingAddress => throw UnimplementedError(); + Future get currentReceivingAddress async { + final _currentReceivingAddress = _credentials.address; + return _currentReceivingAddress.toString(); + } @override Future estimateFeeFor(int satoshiAmount, int feeRate) { @@ -115,8 +141,19 @@ class EthereumWallet extends CoinServiceAPI { } @override - // TODO: implement fees - Future get fees => throw UnimplementedError(); + Future get fees => _feeObject ??= _getFees(); + Future? _feeObject; + + Future _getFees() async { + // TODO: implement _getFees + return FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 10, + numberOfBlocksSlow: 10, + fast: 1, + medium: 1, + slow: 1); + } @override Future fullRescan( @@ -157,12 +194,12 @@ class EthereumWallet extends CoinServiceAPI { Future initializeNew() async { await _prefs.init(); final String mnemonic = bip39.generateMnemonic(strength: 256); - final credentials = - EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + print("Mnemonic is $mnemonic"); + _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); final String password = generatePassword(); var rng = Random.secure(); - Wallet wallet = Wallet.createNew(credentials, password, rng); + Wallet wallet = Wallet.createNew(_credentials, password, rng); await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); @@ -188,13 +225,14 @@ class EthereumWallet extends CoinServiceAPI { .put(boxName: walletId, key: "isFavorite", value: false); } - @override - // TODO: implement isConnected - bool get isConnected => throw UnimplementedError(); + bool _isConnected = false; @override - // TODO: implement isRefreshing - bool get isRefreshing => throw UnimplementedError(); + bool get isConnected => _isConnected; + + bool refreshMutex = false; + @override + bool get isRefreshing => refreshMutex; @override // TODO: implement maxFee @@ -231,9 +269,37 @@ class EthereumWallet extends CoinServiceAPI { {required String mnemonic, required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, - required int height}) { - // TODO: implement recoverFromMnemonic - throw UnimplementedError(); + required int height}) async { + await _prefs.init(); + print("Mnemonic is $mnemonic"); + _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + + final String password = generatePassword(); + var rng = Random.secure(); + Wallet wallet = Wallet.createNew(_credentials, password, rng); + + await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); + + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance.put( + boxName: walletId, key: 'receivingAddresses', value: ["0"]); + 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); } @override @@ -258,8 +324,11 @@ class EthereumWallet extends CoinServiceAPI { } @override - // TODO: implement totalBalance - Future get totalBalance => throw UnimplementedError(); + // TODO: Check difference between total and available balance for eth + Future get totalBalance async { + EtherAmount ethBalance = await _client.getBalance(_credentials.address); + return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); + } @override Future get transactionData => @@ -287,7 +356,7 @@ class EthereumWallet extends CoinServiceAPI { @override bool validateAddress(String address) { // TODO: implement validateAddress - throw UnimplementedError(); + return true; } Future _fetchTransactionData() async { From b2c40c014af744ec7255f4b6d861c0a32e0db873 Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 20 Dec 2022 14:54:51 +0200 Subject: [PATCH 004/208] WIP: ADD ETH --- .../coins/ethereum/ethereum_wallet.dart | 129 ++++++++++++++---- 1 file changed, 106 insertions(+), 23 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 513effe8d..679144860 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -12,15 +12,18 @@ import 'package:stackwallet/utilities/prefs.dart'; import 'package:string_to_hex/string_to_hex.dart'; import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart' as Transaction; -// import 'package:string_to_hex/string_to_hex.dart'; -// import 'package:web3dart/credentials.dart'; -// import 'package:web3dart/web3dart.dart'; + 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'; + const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; @@ -55,8 +58,7 @@ class EthereumWallet extends CoinServiceAPI { late PriceAPI _priceAPI; final _prefs = Prefs.instance; final _client = Web3Client( - "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", - Client()); + "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", Client()); late EthPrivateKey _credentials; @@ -108,15 +110,19 @@ class EthereumWallet extends CoinServiceAPI { @override Future confirmSend({required Map txData}) async { + print("CALLING CONFIRM SEND WITH $txData"); + final gasPrice = await _client.getGasPrice(); + print("GAS PRICE IS $gasPrice"); + + // final fee = "21,000" * (gasPrice! + 2 ); + final tx = Transaction.Transaction( + to: EthereumAddress.fromHex(txData['addresss'] as String), + gasPrice: gasPrice, + maxGas: 21000, + value: EtherAmount.fromUnitAndValue(EtherUnit.ether, 1)); final transaction = await _client.sendTransaction( _credentials, - Transaction( - to: EthereumAddress.fromHex( - '0xC914Bb2ba888e3367bcecEb5C2d99DF7C7423706'), - gasPrice: EtherAmount.inWei(BigInt.one), - maxGas: 100000, - value: EtherAmount.fromUnitAndValue(EtherUnit.ether, 1), - ), + tx, ); return transaction; @@ -129,9 +135,11 @@ class EthereumWallet extends CoinServiceAPI { } @override - Future estimateFeeFor(int satoshiAmount, int feeRate) { + Future estimateFeeFor(int satoshiAmount, int feeRate) async { + print("CALLING ESTIMATE FEE"); // TODO: implement estimateFeeFor - throw UnimplementedError(); + // throw UnimplementedError(); + return 1; } @override @@ -145,7 +153,6 @@ class EthereumWallet extends CoinServiceAPI { Future? _feeObject; Future _getFees() async { - // TODO: implement _getFees return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 10, @@ -168,9 +175,10 @@ class EthereumWallet extends CoinServiceAPI { throw UnimplementedError(); } + bool _hasCalledExit = false; + @override - // TODO: implement hasCalledExit - bool get hasCalledExit => throw UnimplementedError(); + bool get hasCalledExit => _hasCalledExit; @override Future initializeExisting() async { @@ -241,6 +249,29 @@ class EthereumWallet extends CoinServiceAPI { @override Future> get mnemonic => _getMnemonicList(); + // Future get chainHeight async { + // try { + // final result = await _client.getSyncStatus(); + // print("HEIGHT IS $result"); + // return 1 as int; + // } catch (e, s) { + // Logging.instance.log("Exception caught in chainHeight: $e\n$s", + // level: LogLevel.Error); + // return -1; + // } + // } + // + // 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'); @@ -255,13 +286,22 @@ class EthereumWallet extends CoinServiceAPI { // TODO: implement pendingBalance Future get pendingBalance => throw UnimplementedError(); + // Future transactionFee(int satoshiAmount) {} + @override Future> prepareSend( {required String address, required int satoshiAmount, - Map? args}) { - // TODO: implement prepareSend - throw UnimplementedError(); + Map? args}) async { + print("CALLING PREPARE SEND ${Decimal.fromInt(satoshiAmount)}"); + + Map txData = { + "fee": 0, + "addresss": address, + "recipientAmt": satoshiAmount, + }; + + return txData; } @override @@ -303,9 +343,50 @@ class EthereumWallet extends CoinServiceAPI { } @override - Future refresh() { - // TODO: implement refresh - throw UnimplementedError(); + Future refresh() async { + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + try { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); + + // final currentHeight = await chainHeight; + const storedHeight = 1; //await storedChainHeight; + + } catch (error, strace) { + refreshMutex = false; + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + coin, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + Logging.instance.log( + "Caught exception in refreshWalletData(): $error\n$strace", + level: LogLevel.Warning); + } } @override @@ -327,6 +408,8 @@ class EthereumWallet extends CoinServiceAPI { // TODO: Check difference between total and available balance for eth Future get totalBalance async { EtherAmount ethBalance = await _client.getBalance(_credentials.address); + print( + "BALANCE NOW IS ${ethBalance.getValueInUnit(EtherUnit.ether).toString()}"); return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); } From 12388ba5ca432822e7cf736a732c380e5ad9cc6b Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 23 Dec 2022 13:51:36 +0200 Subject: [PATCH 005/208] WIP: Add ETH, getting transactions list --- .../coins/ethereum/ethereum_wallet.dart | 77 +++++++++++++------ 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 679144860..0629dde70 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; import 'package:decimal/decimal.dart'; @@ -249,28 +250,28 @@ class EthereumWallet extends CoinServiceAPI { @override Future> get mnemonic => _getMnemonicList(); - // Future get chainHeight async { - // try { - // final result = await _client.getSyncStatus(); - // print("HEIGHT IS $result"); - // return 1 as int; - // } catch (e, s) { - // Logging.instance.log("Exception caught in chainHeight: $e\n$s", - // level: LogLevel.Error); - // return -1; - // } - // } - // - // 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 get chainHeight async { + try { + final result = await _client.getBlockNumber(); + + return result; + } catch (e, s) { + Logging.instance.log("Exception caught in chainHeight: $e\n$s", + level: LogLevel.Error); + return -1; + } + } + + 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 = @@ -344,6 +345,7 @@ class EthereumWallet extends CoinServiceAPI { @override Future refresh() async { + print("CALLING REFRESH"); if (refreshMutex) { Logging.instance.log("$walletId $walletName refreshMutex denied", level: LogLevel.Info); @@ -352,6 +354,10 @@ class EthereumWallet extends CoinServiceAPI { refreshMutex = true; } + print("SYNC STATUS IS "); + final blockNumber = await _client.getBlockNumber(); + print("BLOCK NUMBER IS ::: ${blockNumber}"); + try { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( @@ -364,9 +370,25 @@ class EthereumWallet extends CoinServiceAPI { GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); - // final currentHeight = await chainHeight; + final currentHeight = await chainHeight; const storedHeight = 1; //await storedChainHeight; + Logging.instance + .log("chain height: $currentHeight", level: LogLevel.Info); + Logging.instance + .log("cached height: $storedHeight", level: LogLevel.Info); + + if (currentHeight != storedHeight) { + if (currentHeight != -1) { + // -1 failed to fetch current height + unawaited(updateStoredChainHeight(newHeight: currentHeight)); + } + + final newTxData = _fetchTransactionData(); + print("RETREIVED TX DATA IS $newTxData"); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.50, walletId)); + } } catch (error, strace) { refreshMutex = false; GlobalEventBus.instance.fire( @@ -443,6 +465,15 @@ class EthereumWallet extends CoinServiceAPI { } Future _fetchTransactionData() async { + String thisAddress = await currentReceivingAddress; + int currentBlock = await chainHeight; + var balance = await availableBalance; + var n = _client.getTransactionCount(EthereumAddress.fromHex(thisAddress)); + + print("THIS CURRECT ADDRESS IS $thisAddress"); + print("THIS CURRECT BLOCK IS $currentBlock"); + print("THIS BALANCE IS $balance"); + print("THIS COUNT TRANSACTIONS IS $n"); throw UnimplementedError(); } From f9ec3700705084c5e4d1b16b39f730066cb84ec4 Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 26 Dec 2022 16:12:51 +0200 Subject: [PATCH 006/208] WIP: get transactions data --- .../coins/ethereum/ethereum_wallet.dart | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 0629dde70..029f6d5f8 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/sec_random_native.dart'; import 'package:decimal/decimal.dart'; +import 'package:intl/intl.dart'; import 'package:stack_wallet_backup/generate_password.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; @@ -9,9 +11,11 @@ import 'package:stackwallet/models/paymint/utxo_model.dart'; import 'package:stackwallet/services/price.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:string_to_hex/string_to_hex.dart'; import 'package:web3dart/web3dart.dart'; +import 'package:web3dart/web3dart.dart' as web3; import 'package:web3dart/web3dart.dart' as Transaction; import 'package:http/http.dart'; @@ -59,7 +63,8 @@ class EthereumWallet extends CoinServiceAPI { late PriceAPI _priceAPI; final _prefs = Prefs.instance; final _client = Web3Client( - "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", Client()); + "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", + Client()); late EthPrivateKey _credentials; @@ -111,24 +116,31 @@ class EthereumWallet extends CoinServiceAPI { @override Future confirmSend({required Map txData}) async { - print("CALLING CONFIRM SEND WITH $txData"); final gasPrice = await _client.getGasPrice(); - print("GAS PRICE IS $gasPrice"); - - // final fee = "21,000" * (gasPrice! + 2 ); + final amount = txData['recipientAmt']; + final decimalAmount = + Format.satoshisToAmount(amount as int, coin: Coin.ethereum); + final bigIntAmount = amountToBigInt(decimalAmount.toDouble()); final tx = Transaction.Transaction( to: EthereumAddress.fromHex(txData['addresss'] as String), gasPrice: gasPrice, maxGas: 21000, - value: EtherAmount.fromUnitAndValue(EtherUnit.ether, 1)); + value: EtherAmount.inWei(bigIntAmount)); final transaction = await _client.sendTransaction( _credentials, tx, ); + Logging.instance.log("Generated TX IS $transaction", level: LogLevel.Info); 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 get currentReceivingAddress async { final _currentReceivingAddress = _credentials.address; @@ -294,10 +306,12 @@ class EthereumWallet extends CoinServiceAPI { {required String address, required int satoshiAmount, Map? args}) async { - print("CALLING PREPARE SEND ${Decimal.fromInt(satoshiAmount)}"); + final gasPrice = await _client.getGasPrice(); Map txData = { - "fee": 0, + "fee": Format.decimalAmountToSatoshis( + Decimal.parse(gasPrice.getValueInUnit(EtherUnit.ether).toString()), + coin), "addresss": address, "recipientAmt": satoshiAmount, }; @@ -468,7 +482,27 @@ class EthereumWallet extends CoinServiceAPI { String thisAddress = await currentReceivingAddress; int currentBlock = await chainHeight; var balance = await availableBalance; - var n = _client.getTransactionCount(EthereumAddress.fromHex(thisAddress)); + var n = + await _client.getTransactionCount(EthereumAddress.fromHex(thisAddress)); + + for (var i = currentBlock; i >= 0 && (n > 0); --i) { + try { + // print(StringToHex.toHexString(i.toString())) + print( + "BLOCK IS $i --------->>>>>>> ${StringToHex.toHexString(i.toString())}"); + var block = await _client.getBlockInformation( + blockNumber: StringToHex.toHexString(i.toString()), + isContainFullObj: true); + // var block = await _client.getBlockInformation() + print(block); + // if (block && block.t) { + // + // } + + } catch (e, s) { + print("Error getting transactions ${e.toString()}"); + } + } print("THIS CURRECT ADDRESS IS $thisAddress"); print("THIS CURRECT BLOCK IS $currentBlock"); From 5e67b5734cecca6c30ba210b74c6828386d2edb1 Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 3 Jan 2023 14:50:32 +0200 Subject: [PATCH 007/208] fix balance error when opening existing wallet and add chainId for sending --- .../coins/ethereum/ethereum_wallet.dart | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 029f6d5f8..3b3c21970 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -63,10 +63,10 @@ class EthereumWallet extends CoinServiceAPI { late PriceAPI _priceAPI; final _prefs = Prefs.instance; final _client = Web3Client( - "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", - Client()); + "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", Client()); late EthPrivateKey _credentials; + int _chainId = 5; //5 for testnet and 1 for mainnet EthereumWallet( {required String walletId, @@ -107,6 +107,7 @@ class EthereumWallet extends CoinServiceAPI { @override Future get availableBalance async { EtherAmount ethBalance = await _client.getBalance(_credentials.address); + print("THIS ETH BALANCE IS $ethBalance"); return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); } @@ -126,10 +127,8 @@ class EthereumWallet extends CoinServiceAPI { gasPrice: gasPrice, maxGas: 21000, value: EtherAmount.inWei(bigIntAmount)); - final transaction = await _client.sendTransaction( - _credentials, - tx, - ); + final transaction = + await _client.sendTransaction(_credentials, tx, chainId: _chainId); Logging.instance.log("Generated TX IS $transaction", level: LogLevel.Info); return transaction; @@ -195,6 +194,13 @@ class EthereumWallet extends CoinServiceAPI { @override Future initializeExisting() async { + //First get mnemonic so we can initialize credentials + final mnemonicString = + await _secureStore.read(key: '${_walletId}_mnemonic'); + + _credentials = + EthPrivateKey.fromHex(StringToHex.toHexString(mnemonicString)); + Logging.instance.log("Opening existing ${coin.prettyName} wallet.", level: LogLevel.Info); @@ -215,15 +221,14 @@ class EthereumWallet extends CoinServiceAPI { Future initializeNew() async { await _prefs.init(); final String mnemonic = bip39.generateMnemonic(strength: 256); - print("Mnemonic is $mnemonic"); _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); - final String password = generatePassword(); - var rng = Random.secure(); - Wallet wallet = Wallet.createNew(_credentials, password, rng); - await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); + //Store credentials in secure store + await _secureStore.write( + key: '${_walletId}_credentials', value: _credentials.toString()); + await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance.put( @@ -329,10 +334,6 @@ class EthereumWallet extends CoinServiceAPI { print("Mnemonic is $mnemonic"); _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); - final String password = generatePassword(); - var rng = Random.secure(); - Wallet wallet = Wallet.createNew(_credentials, password, rng); - await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); await DB.instance @@ -360,13 +361,13 @@ class EthereumWallet extends CoinServiceAPI { @override Future refresh() async { print("CALLING REFRESH"); - if (refreshMutex) { - Logging.instance.log("$walletId $walletName refreshMutex denied", - level: LogLevel.Info); - return; - } else { - refreshMutex = true; - } + // if (refreshMutex) { + // Logging.instance.log("$walletId $walletName refreshMutex denied", + // level: LogLevel.Info); + // return; + // } else { + // refreshMutex = true; + // } print("SYNC STATUS IS "); final blockNumber = await _client.getBlockNumber(); @@ -385,6 +386,7 @@ class EthereumWallet extends CoinServiceAPI { GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); final currentHeight = await chainHeight; + print("CURRENT CHAIN HEIGHT IS $currentHeight"); const storedHeight = 1; //await storedChainHeight; Logging.instance @@ -398,7 +400,7 @@ class EthereumWallet extends CoinServiceAPI { unawaited(updateStoredChainHeight(newHeight: currentHeight)); } - final newTxData = _fetchTransactionData(); + final newTxData = await _fetchTransactionData(); print("RETREIVED TX DATA IS $newTxData"); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.50, walletId)); @@ -482,17 +484,18 @@ class EthereumWallet extends CoinServiceAPI { String thisAddress = await currentReceivingAddress; int currentBlock = await chainHeight; var balance = await availableBalance; + print("MY ADDRESS HERE IS $thisAddress"); var n = await _client.getTransactionCount(EthereumAddress.fromHex(thisAddress)); - for (var i = currentBlock; i >= 0 && (n > 0); --i) { + for (int i = currentBlock; + i >= 0 && (n > 0 || balance.toDouble() > 0.0); + --i) { try { // print(StringToHex.toHexString(i.toString())) - print( - "BLOCK IS $i --------->>>>>>> ${StringToHex.toHexString(i.toString())}"); + print("BLOCK IS $i --------->>>>>>>"); var block = await _client.getBlockInformation( - blockNumber: StringToHex.toHexString(i.toString()), - isContainFullObj: true); + blockNumber: i.toString(), isContainFullObj: true); // var block = await _client.getBlockInformation() print(block); // if (block && block.t) { @@ -500,7 +503,7 @@ class EthereumWallet extends CoinServiceAPI { // } } catch (e, s) { - print("Error getting transactions ${e.toString()}"); + print("Error getting transactions ${e.toString()} $s"); } } @@ -508,7 +511,9 @@ class EthereumWallet extends CoinServiceAPI { print("THIS CURRECT BLOCK IS $currentBlock"); print("THIS BALANCE IS $balance"); print("THIS COUNT TRANSACTIONS IS $n"); - throw UnimplementedError(); + + return TransactionData(); + // throw UnimplementedError(); } @override From 6b3d42033e0789b9d24f0bd3e89cb728ddfa3fca Mon Sep 17 00:00:00 2001 From: likho Date: Tue, 3 Jan 2023 17:15:27 +0200 Subject: [PATCH 008/208] WIP: GET address transactions --- .../coins/ethereum/ethereum_wallet.dart | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 3b3c21970..078ee148d 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,10 +1,8 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; -import 'package:cw_core/sec_random_native.dart'; import 'package:decimal/decimal.dart'; -import 'package:intl/intl.dart'; -import 'package:stack_wallet_backup/generate_password.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'; @@ -487,21 +485,33 @@ class EthereumWallet extends CoinServiceAPI { print("MY ADDRESS HERE IS $thisAddress"); var n = await _client.getTransactionCount(EthereumAddress.fromHex(thisAddress)); - + print("TRANSACTION COUNT IS $n"); + String hexHeight = currentBlock.toRadixString(16); + print("HEIGHT TO HEX IS $hexHeight"); + print('0x$hexHeight'); for (int i = currentBlock; i >= 0 && (n > 0 || balance.toDouble() > 0.0); --i) { try { // print(StringToHex.toHexString(i.toString())) - print("BLOCK IS $i --------->>>>>>>"); + print( + "BLOCK IS $i AND HEX IS --------->>>>>>> ${StringToHex.toHexString(i.toString())}"); + String blockHex = i.toRadixString(16); var block = await _client.getBlockInformation( - blockNumber: i.toString(), isContainFullObj: true); - // var block = await _client.getBlockInformation() - print(block); - // if (block && block.t) { - // - // } + blockNumber: '0x$blockHex', isContainFullObj: true); + if (block != null && block.transactions != null) { + block.transactions.forEach((element) { + if (thisAddress == element.from) { + if (element.from != element.to) { + Logging.instance.log( + "TX SENT FROM THIS ACCOUNT ${element.from} ${element.to}", + level: LogLevel.Info); + --n; + } + } + }); + } } catch (e, s) { print("Error getting transactions ${e.toString()} $s"); } From a6c2750f20345087e3de907f4f91191d72c8fa13 Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 5 Jan 2023 13:07:46 +0200 Subject: [PATCH 009/208] WIP: Get wallet transactions --- .../coins/ethereum/ethereum_wallet.dart | 98 ++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 078ee148d..d44d5dc5f 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; import 'package:decimal/decimal.dart'; +import 'package:flutter/foundation.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'; @@ -489,6 +490,14 @@ class EthereumWallet extends CoinServiceAPI { String hexHeight = currentBlock.toRadixString(16); print("HEIGHT TO HEX IS $hexHeight"); print('0x$hexHeight'); + + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + + //Initilaize empty transactions array + final List> midSortedArray = []; + Map midSortedTx = {}; for (int i = currentBlock; i >= 0 && (n > 0 || balance.toDouble() > 0.0); --i) { @@ -502,13 +511,62 @@ class EthereumWallet extends CoinServiceAPI { if (block != null && block.transactions != null) { block.transactions.forEach((element) { - if (thisAddress == element.from) { - if (element.from != element.to) { + // print("TRANSACTION OBJECT IS $element"); + final jsonObject = json.encode(element); + final decodedTransaction = jsonDecode(jsonObject); + // print(somethingElse['from']); + // print(jsonObject.containsKey(other)); + Logging.instance.log(decodedTransaction, + level: LogLevel.Info, printFullLength: true); + + if (thisAddress == decodedTransaction['from']) { + //Ensure this is not a self send + if (decodedTransaction['from'] != decodedTransaction['to']) { + midSortedTx["txType"] = "Sent"; + midSortedTx["txid"] = decodedTransaction["hash"]; + midSortedTx["height"] = i; + midSortedTx["address"] = decodedTransaction['to']; + int confirmations = 0; + try { + confirmations = currentBlock - i; + } catch (e, s) { + debugPrint("$e $s"); + } + midSortedTx["confirmations"] = confirmations; + midSortedTx["inputSize"] = 1; + midSortedTx["outputSize"] = 1; + midSortedTx["aliens"] = []; + midSortedTx["inputs"] = []; + midSortedTx["outputs"] = []; + + midSortedArray.add(midSortedTx); Logging.instance.log( - "TX SENT FROM THIS ACCOUNT ${element.from} ${element.to}", + "TX SENT FROM THIS ACCOUNT ${decodedTransaction['from']} ${decodedTransaction['to']}", level: LogLevel.Info); --n; } + + if (thisAddress == decodedTransaction['to']) { + if (decodedTransaction['from'] != decodedTransaction['to']) { + midSortedTx["txType"] = "Received"; + midSortedTx["txid"] = decodedTransaction["hash"]; + midSortedTx["height"] = i; + midSortedTx["address"] = decodedTransaction['from']; + int confirmations = 0; + try { + confirmations = currentBlock - i; + } catch (e, s) { + debugPrint("$e $s"); + } + midSortedTx["confirmations"] = confirmations; + midSortedTx["inputSize"] = 1; + midSortedTx["outputSize"] = 1; + midSortedTx["aliens"] = []; + midSortedTx["inputs"] = []; + midSortedTx["outputs"] = []; + midSortedArray.add(midSortedTx); + } + } } }); } @@ -517,6 +575,40 @@ class EthereumWallet extends CoinServiceAPI { } } + midSortedArray + .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + final Map result = {"dateTimeChunks": []}; + final dateArray = []; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = extractDateFromTimestamp(txObject["timestamp"] as int); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp(chunk["timestamp"] as int) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = >[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + transactionsMap + .addAll(TransactionData.fromJson(result).getAllTransactions()); + print("THIS CURRECT ADDRESS IS $thisAddress"); print("THIS CURRECT BLOCK IS $currentBlock"); print("THIS BALANCE IS $balance"); From 5d77dcafbed98f3d16374e3026189f46fc236a47 Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 5 Jan 2023 14:39:21 +0200 Subject: [PATCH 010/208] ADd address validation and convert receive addresses as checksum addresses --- .../coins/ethereum/ethereum_wallet.dart | 292 ++++++++++-------- pubspec.lock | 15 +- pubspec.yaml | 1 + 3 files changed, 181 insertions(+), 127 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index d44d5dc5f..b2191da59 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -3,11 +3,14 @@ import 'dart:convert'; import 'dart:math'; 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:flutter/foundation.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/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; @@ -16,6 +19,7 @@ import 'package:string_to_hex/string_to_hex.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; import 'package:http/http.dart'; @@ -142,7 +146,9 @@ class EthereumWallet extends CoinServiceAPI { @override Future get currentReceivingAddress async { final _currentReceivingAddress = _credentials.address; - return _currentReceivingAddress.toString(); + final checkSumAddress = + checksumEthereumAddress(_currentReceivingAddress.toString()); + return checkSumAddress; } @override @@ -360,13 +366,13 @@ class EthereumWallet extends CoinServiceAPI { @override Future refresh() async { print("CALLING REFRESH"); - // if (refreshMutex) { - // Logging.instance.log("$walletId $walletName refreshMutex denied", - // level: LogLevel.Info); - // return; - // } else { - // refreshMutex = true; - // } + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } print("SYNC STATUS IS "); final blockNumber = await _client.getBlockNumber(); @@ -468,15 +474,51 @@ class EthereumWallet extends CoinServiceAPI { } @override - Future updateSentCachedTxData(Map txData) { - // TODO: implement updateSentCachedTxData - throw UnimplementedError(); + Future updateSentCachedTxData(Map txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + 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 - return true; + return isValidEthereumAddress(address); } Future _fetchTransactionData() async { @@ -491,123 +533,127 @@ class EthereumWallet extends CoinServiceAPI { print("HEIGHT TO HEX IS $hexHeight"); print('0x$hexHeight'); + final cachedTransactions = + DB.instance.get(boxName: walletId, key: 'latest_tx_model') + as TransactionData?; + final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; //Initilaize empty transactions array + final List> midSortedArray = []; Map midSortedTx = {}; - for (int i = currentBlock; - i >= 0 && (n > 0 || balance.toDouble() > 0.0); - --i) { - try { - // print(StringToHex.toHexString(i.toString())) - print( - "BLOCK IS $i AND HEX IS --------->>>>>>> ${StringToHex.toHexString(i.toString())}"); - String blockHex = i.toRadixString(16); - var block = await _client.getBlockInformation( - blockNumber: '0x$blockHex', isContainFullObj: true); - - if (block != null && block.transactions != null) { - block.transactions.forEach((element) { - // print("TRANSACTION OBJECT IS $element"); - final jsonObject = json.encode(element); - final decodedTransaction = jsonDecode(jsonObject); - // print(somethingElse['from']); - // print(jsonObject.containsKey(other)); - Logging.instance.log(decodedTransaction, - level: LogLevel.Info, printFullLength: true); - - if (thisAddress == decodedTransaction['from']) { - //Ensure this is not a self send - if (decodedTransaction['from'] != decodedTransaction['to']) { - midSortedTx["txType"] = "Sent"; - midSortedTx["txid"] = decodedTransaction["hash"]; - midSortedTx["height"] = i; - midSortedTx["address"] = decodedTransaction['to']; - int confirmations = 0; - try { - confirmations = currentBlock - i; - } catch (e, s) { - debugPrint("$e $s"); - } - midSortedTx["confirmations"] = confirmations; - midSortedTx["inputSize"] = 1; - midSortedTx["outputSize"] = 1; - midSortedTx["aliens"] = []; - midSortedTx["inputs"] = []; - midSortedTx["outputs"] = []; - - midSortedArray.add(midSortedTx); - Logging.instance.log( - "TX SENT FROM THIS ACCOUNT ${decodedTransaction['from']} ${decodedTransaction['to']}", - level: LogLevel.Info); - --n; - } - - if (thisAddress == decodedTransaction['to']) { - if (decodedTransaction['from'] != decodedTransaction['to']) { - midSortedTx["txType"] = "Received"; - midSortedTx["txid"] = decodedTransaction["hash"]; - midSortedTx["height"] = i; - midSortedTx["address"] = decodedTransaction['from']; - int confirmations = 0; - try { - confirmations = currentBlock - i; - } catch (e, s) { - debugPrint("$e $s"); - } - midSortedTx["confirmations"] = confirmations; - midSortedTx["inputSize"] = 1; - midSortedTx["outputSize"] = 1; - midSortedTx["aliens"] = []; - midSortedTx["inputs"] = []; - midSortedTx["outputs"] = []; - midSortedArray.add(midSortedTx); - } - } - } - }); - } - } catch (e, s) { - print("Error getting transactions ${e.toString()} $s"); - } - } - - midSortedArray - .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); - final Map result = {"dateTimeChunks": []}; - final dateArray = []; - - for (int i = 0; i < midSortedArray.length; i++) { - final txObject = midSortedArray[i]; - final date = extractDateFromTimestamp(txObject["timestamp"] as int); - final txTimeArray = [txObject["timestamp"], date]; - - if (dateArray.contains(txTimeArray[1])) { - result["dateTimeChunks"].forEach((dynamic chunk) { - if (extractDateFromTimestamp(chunk["timestamp"] as int) == - txTimeArray[1]) { - if (chunk["transactions"] == null) { - chunk["transactions"] = >[]; - } - chunk["transactions"].add(txObject); - } - }); - } else { - dateArray.add(txTimeArray[1]); - final chunk = { - "timestamp": txTimeArray[0], - "transactions": [txObject], - }; - result["dateTimeChunks"].add(chunk); - } - } - - final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; - transactionsMap - .addAll(TransactionData.fromJson(result).getAllTransactions()); + // for (int i = currentBlock; + // i >= 0 && (n > 0 || balance.toDouble() > 0.0); + // --i) { + // try { + // // print(StringToHex.toHexString(i.toString())) + // print( + // "BLOCK IS $i AND HEX IS --------->>>>>>> ${StringToHex.toHexString(i.toString())}"); + // String blockHex = i.toRadixString(16); + // var block = await _client.getBlockInformation( + // blockNumber: '0x$blockHex', isContainFullObj: true); + // + // if (block != null && block.transactions != null) { + // block.transactions.forEach((element) { + // // print("TRANSACTION OBJECT IS $element"); + // final jsonObject = json.encode(element); + // final decodedTransaction = jsonDecode(jsonObject); + // // print(somethingElse['from']); + // // print(jsonObject.containsKey(other)); + // Logging.instance.log(decodedTransaction, + // level: LogLevel.Info, printFullLength: true); + // + // if (thisAddress == decodedTransaction['from']) { + // //Ensure this is not a self send + // if (decodedTransaction['from'] != decodedTransaction['to']) { + // midSortedTx["txType"] = "Sent"; + // midSortedTx["txid"] = decodedTransaction["hash"]; + // midSortedTx["height"] = i; + // midSortedTx["address"] = decodedTransaction['to']; + // int confirmations = 0; + // try { + // confirmations = currentBlock - i; + // } catch (e, s) { + // debugPrint("$e $s"); + // } + // midSortedTx["confirmations"] = confirmations; + // midSortedTx["inputSize"] = 1; + // midSortedTx["outputSize"] = 1; + // midSortedTx["aliens"] = []; + // midSortedTx["inputs"] = []; + // midSortedTx["outputs"] = []; + // + // midSortedArray.add(midSortedTx); + // Logging.instance.log( + // "TX SENT FROM THIS ACCOUNT ${decodedTransaction['from']} ${decodedTransaction['to']}", + // level: LogLevel.Info); + // --n; + // } + // + // if (thisAddress == decodedTransaction['to']) { + // if (decodedTransaction['from'] != decodedTransaction['to']) { + // midSortedTx["txType"] = "Received"; + // midSortedTx["txid"] = decodedTransaction["hash"]; + // midSortedTx["height"] = i; + // midSortedTx["address"] = decodedTransaction['from']; + // int confirmations = 0; + // try { + // confirmations = currentBlock - i; + // } catch (e, s) { + // debugPrint("$e $s"); + // } + // midSortedTx["confirmations"] = confirmations; + // midSortedTx["inputSize"] = 1; + // midSortedTx["outputSize"] = 1; + // midSortedTx["aliens"] = []; + // midSortedTx["inputs"] = []; + // midSortedTx["outputs"] = []; + // midSortedArray.add(midSortedTx); + // } + // } + // } + // }); + // } + // } catch (e, s) { + // print("Error getting transactions ${e.toString()} $s"); + // } + // } + // midSortedArray + // .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + // final Map result = {"dateTimeChunks": []}; + // final dateArray = []; + // + // for (int i = 0; i < midSortedArray.length; i++) { + // final txObject = midSortedArray[i]; + // final date = extractDateFromTimestamp(txObject["timestamp"] as int); + // final txTimeArray = [txObject["timestamp"], date]; + // + // if (dateArray.contains(txTimeArray[1])) { + // result["dateTimeChunks"].forEach((dynamic chunk) { + // if (extractDateFromTimestamp(chunk["timestamp"] as int) == + // txTimeArray[1]) { + // if (chunk["transactions"] == null) { + // chunk["transactions"] = >[]; + // } + // chunk["transactions"].add(txObject); + // } + // }); + // } else { + // dateArray.add(txTimeArray[1]); + // final chunk = { + // "timestamp": txTimeArray[0], + // "transactions": [txObject], + // }; + // result["dateTimeChunks"].add(chunk); + // } + // } + // + // final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + // transactionsMap + // .addAll(TransactionData.fromJson(result).getAllTransactions()); print("THIS CURRECT ADDRESS IS $thisAddress"); print("THIS CURRECT BLOCK IS $currentBlock"); diff --git a/pubspec.lock b/pubspec.lock index 81a63d518..e15d53505 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -225,7 +225,7 @@ packages: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "4.3.0" + version: "4.4.0" collection: dependency: transitive description: @@ -429,6 +429,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.0.1" + ethereum_addresses: + dependency: "direct main" + description: + name: ethereum_addresses + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" event_bus: dependency: "direct main" description: @@ -1284,7 +1291,7 @@ packages: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" shared_preferences_macos: dependency: transitive description: @@ -1312,7 +1319,7 @@ packages: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" shelf: dependency: transitive description: @@ -1683,7 +1690,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.1.3" window_size: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 25119a355..1da77f560 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,6 +91,7 @@ dependencies: # Eth Plugins web3dart: 2.3.5 string_to_hex: 0.2.2 + ethereum_addresses: 1.0.2 # Storage plugins flutter_secure_storage: ^5.0.2 From 5fcb65ba0c2b1ae8254f9a17e98af7e258695501 Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 5 Jan 2023 18:04:45 +0200 Subject: [PATCH 011/208] WIP: GET transactions --- .../coins/ethereum/ethereum_wallet.dart | 93 ++++++++++++++----- 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index b2191da59..9b65f283c 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -38,6 +38,26 @@ const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"; +class AddressTransaction { + final String message; + final List result; + final int status; + + const AddressTransaction({ + required this.message, + required this.result, + required this.status, + }); + + factory AddressTransaction.fromJson(Map json) { + return AddressTransaction( + message: json['message'] as String, + result: json['result'] as List, + status: json['status'] as int, + ); + } +} + class EthereumWallet extends CoinServiceAPI { @override set isFavorite(bool markFavorite) { @@ -66,7 +86,10 @@ class EthereumWallet extends CoinServiceAPI { late PriceAPI _priceAPI; final _prefs = Prefs.instance; final _client = Web3Client( - "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", Client()); + "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", + Client()); + + final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; late EthPrivateKey _credentials; int _chainId = 5; //5 for testnet and 1 for mainnet @@ -521,30 +544,52 @@ class EthereumWallet extends CoinServiceAPI { return isValidEthereumAddress(address); } + List parseTransactions(String responseBody) { + final parsed = json.decode(responseBody).cast>(); + return parsed + .map((json) => AddressTransaction.fromJson(json)) + .toList(); + } + Future _fetchTransactionData() async { String thisAddress = await currentReceivingAddress; - int currentBlock = await chainHeight; - var balance = await availableBalance; - print("MY ADDRESS HERE IS $thisAddress"); - var n = - await _client.getTransactionCount(EthereumAddress.fromHex(thisAddress)); - print("TRANSACTION COUNT IS $n"); - String hexHeight = currentBlock.toRadixString(16); - print("HEIGHT TO HEX IS $hexHeight"); - print('0x$hexHeight'); - final cachedTransactions = - DB.instance.get(boxName: walletId, key: 'latest_tx_model') - as TransactionData?; + final response = await get(Uri.parse( + "${_blockExplorer}module=account&action=txlist&address=$thisAddress")); - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + if (response.statusCode == 200) { + // If the server did return a 200 OK response, + // then parse the JSON. - //Initilaize empty transactions array - - final List> midSortedArray = []; - Map midSortedTx = {}; + print(parseTransactions(response.body.toString())); + } else { + // If the server did not return a 200 OK response, + // then throw an exception. + throw Exception('Failed to load album'); + } + print("RETURNED TRANSACTIONS IS $response"); + // int currentBlock = await chainHeight; + // var balance = await availableBalance; + // print("MY ADDRESS HERE IS $thisAddress"); + // var n = + // await _client.getTransactionCount(EthereumAddress.fromHex(thisAddress)); + // print("TRANSACTION COUNT IS $n"); + // String hexHeight = currentBlock.toRadixString(16); + // print("HEIGHT TO HEX IS $hexHeight"); + // print('0x$hexHeight'); + // + // final cachedTransactions = + // DB.instance.get(boxName: walletId, key: 'latest_tx_model') + // as TransactionData?; + // + // final priceData = + // await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + // Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + // + // //Initilaize empty transactions array + // + // final List> midSortedArray = []; + // Map midSortedTx = {}; // for (int i = currentBlock; // i >= 0 && (n > 0 || balance.toDouble() > 0.0); // --i) { @@ -655,10 +700,10 @@ class EthereumWallet extends CoinServiceAPI { // transactionsMap // .addAll(TransactionData.fromJson(result).getAllTransactions()); - print("THIS CURRECT ADDRESS IS $thisAddress"); - print("THIS CURRECT BLOCK IS $currentBlock"); - print("THIS BALANCE IS $balance"); - print("THIS COUNT TRANSACTIONS IS $n"); + // print("THIS CURRECT ADDRESS IS $thisAddress"); + // print("THIS CURRECT BLOCK IS $currentBlock"); + // print("THIS BALANCE IS $balance"); + // print("THIS COUNT TRANSACTIONS IS $n"); return TransactionData(); // throw UnimplementedError(); From b60122fd851cabd060a93a1fd15572bd8e04b472 Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 6 Jan 2023 17:25:28 +0200 Subject: [PATCH 012/208] Transaction listing --- lib/models/paymint/transactions_model.dart | 7 +- .../coins/ethereum/ethereum_wallet.dart | 295 ++++++++---------- 2 files changed, 134 insertions(+), 168 deletions(-) diff --git a/lib/models/paymint/transactions_model.dart b/lib/models/paymint/transactions_model.dart index 382459922..ae6a4fa79 100644 --- a/lib/models/paymint/transactions_model.dart +++ b/lib/models/paymint/transactions_model.dart @@ -96,7 +96,8 @@ class TransactionChunk { .toList(); return TransactionChunk( - timestamp: json['timestamp'] as int, transactions: txList); + timestamp: int.parse(json['timestamp'].toString()), + transactions: txList); } @override @@ -192,13 +193,13 @@ class Transaction { return Transaction( txid: json['txid'] as String, confirmedStatus: json['confirmed_status'] as bool, - timestamp: json['timestamp'] as int, + timestamp: int.parse(json['timestamp'].toString()), txType: json['txType'] as String, amount: json['amount'] as int, aliens: json['aliens'] as List, worthNow: json['worthNow'] as String? ?? "", worthAtBlockTimestamp: json['worthAtBlockTimestamp'] as String? ?? "", - fees: json['fees'] as int, + fees: int.parse(json['fees'].toString()), inputSize: json['inputSize'] as int, outputSize: json['outputSize'] as int, inputs: inputList, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 9b65f283c..7f8388653 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -41,7 +41,7 @@ const String GENESIS_HASH_MAINNET = class AddressTransaction { final String message; final List result; - final int status; + final String status; const AddressTransaction({ required this.message, @@ -53,7 +53,7 @@ class AddressTransaction { return AddressTransaction( message: json['message'] as String, result: json['result'] as List, - status: json['status'] as int, + status: json['status'] as String, ); } } @@ -86,10 +86,9 @@ class EthereumWallet extends CoinServiceAPI { late PriceAPI _priceAPI; final _prefs = Prefs.instance; final _client = Web3Client( - "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", - Client()); + "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", Client()); - final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; + final _blockExplorer = "https://eth-goerli.blockscout.com/api?"; late EthPrivateKey _credentials; int _chainId = 5; //5 for testnet and 1 for mainnet @@ -388,7 +387,6 @@ class EthereumWallet extends CoinServiceAPI { @override Future refresh() async { - print("CALLING REFRESH"); if (refreshMutex) { Logging.instance.log("$walletId $walletName refreshMutex denied", level: LogLevel.Info); @@ -397,9 +395,7 @@ class EthereumWallet extends CoinServiceAPI { refreshMutex = true; } - print("SYNC STATUS IS "); final blockNumber = await _client.getBlockNumber(); - print("BLOCK NUMBER IS ::: ${blockNumber}"); try { GlobalEventBus.instance.fire( @@ -414,7 +410,6 @@ class EthereumWallet extends CoinServiceAPI { GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); final currentHeight = await chainHeight; - print("CURRENT CHAIN HEIGHT IS $currentHeight"); const storedHeight = 1; //await storedChainHeight; Logging.instance @@ -429,7 +424,6 @@ class EthereumWallet extends CoinServiceAPI { } final newTxData = await _fetchTransactionData(); - print("RETREIVED TX DATA IS $newTxData"); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.50, walletId)); } @@ -474,8 +468,6 @@ class EthereumWallet extends CoinServiceAPI { // TODO: Check difference between total and available balance for eth Future get totalBalance async { EtherAmount ethBalance = await _client.getBalance(_credentials.address); - print( - "BALANCE NOW IS ${ethBalance.getValueInUnit(EtherUnit.ether).toString()}"); return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); } @@ -544,169 +536,142 @@ class EthereumWallet extends CoinServiceAPI { return isValidEthereumAddress(address); } - List parseTransactions(String responseBody) { - final parsed = json.decode(responseBody).cast>(); - return parsed - .map((json) => AddressTransaction.fromJson(json)) - .toList(); + Future fetchAddressTransactions(String address) async { + final response = await get(Uri.parse( + "${_blockExplorer}module=account&action=txlist&address=$address")); + + if (response.statusCode == 200) { + return AddressTransaction.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception('Failed to load transactions'); + } } Future _fetchTransactionData() async { String thisAddress = await currentReceivingAddress; + final cachedTransactions = + DB.instance.get(boxName: walletId, key: 'latest_tx_model') + as TransactionData?; + int latestTxnBlockHeight = + DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") + as int? ?? + 0; - final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=txlist&address=$thisAddress")); + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List> midSortedArray = []; - if (response.statusCode == 200) { - // If the server did return a 200 OK response, - // then parse the JSON. + AddressTransaction txs = await fetchAddressTransactions(thisAddress); + if (txs.message == "OK") { + final allTxs = txs.result; + allTxs.forEach((element) { + Map midSortedTx = {}; - print(parseTransactions(response.body.toString())); - } else { - // If the server did not return a 200 OK response, - // then throw an exception. - throw Exception('Failed to load album'); + // create final tx map + midSortedTx["txid"] = element["hash"]; + int confirmations = int.parse(element['confirmations'].toString()); + + 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 + // Decimal.parse(gasPrice.getValueInUnit(EtherUnit.ether).toString()) + final satAmount = Format.decimalAmountToSatoshis( + Decimal.parse(transactionAmountInDecimal.toString()), coin); + + midSortedTx["confirmed_status"] = + (confirmations != 0) && (confirmations >= MINIMUM_CONFIRMATIONS); + midSortedTx["confirmations"] = confirmations; + midSortedTx["timestamp"] = element["timeStamp"]; + + if (checksumEthereumAddress(element["from"].toString()) == + thisAddress) { + midSortedTx["txType"] = "Sent"; + } else { + midSortedTx["txType"] = "Received"; + } + + midSortedTx["amount"] = satAmount; + final String worthNow = ((currentPrice * Decimal.fromInt(satAmount)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + + //Calculate fees (GasLimit * gasPrice) + int txFee = int.parse(element['gasPrice'].toString()) * + int.parse(element['gasUsed'].toString()); + final txFeeDecimal = txFee / (pow(10, decimal)); + + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + midSortedTx["aliens"] = []; + midSortedTx["fees"] = Format.decimalAmountToSatoshis( + Decimal.parse(txFeeDecimal.toString()), coin); + midSortedTx["address"] = element["to"]; + midSortedTx["inputSize"] = 1; + midSortedTx["outputSize"] = 1; + midSortedTx["inputs"] = []; + midSortedTx["outputs"] = []; + midSortedTx["height"] = int.parse(element['blockNumber'].toString()); + + midSortedArray.add(midSortedTx); + }); } - print("RETURNED TRANSACTIONS IS $response"); - // int currentBlock = await chainHeight; - // var balance = await availableBalance; - // print("MY ADDRESS HERE IS $thisAddress"); - // var n = - // await _client.getTransactionCount(EthereumAddress.fromHex(thisAddress)); - // print("TRANSACTION COUNT IS $n"); - // String hexHeight = currentBlock.toRadixString(16); - // print("HEIGHT TO HEX IS $hexHeight"); - // print('0x$hexHeight'); - // - // final cachedTransactions = - // DB.instance.get(boxName: walletId, key: 'latest_tx_model') - // as TransactionData?; - // - // final priceData = - // await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - // Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - // - // //Initilaize empty transactions array - // - // final List> midSortedArray = []; - // Map midSortedTx = {}; - // for (int i = currentBlock; - // i >= 0 && (n > 0 || balance.toDouble() > 0.0); - // --i) { - // try { - // // print(StringToHex.toHexString(i.toString())) - // print( - // "BLOCK IS $i AND HEX IS --------->>>>>>> ${StringToHex.toHexString(i.toString())}"); - // String blockHex = i.toRadixString(16); - // var block = await _client.getBlockInformation( - // blockNumber: '0x$blockHex', isContainFullObj: true); - // - // if (block != null && block.transactions != null) { - // block.transactions.forEach((element) { - // // print("TRANSACTION OBJECT IS $element"); - // final jsonObject = json.encode(element); - // final decodedTransaction = jsonDecode(jsonObject); - // // print(somethingElse['from']); - // // print(jsonObject.containsKey(other)); - // Logging.instance.log(decodedTransaction, - // level: LogLevel.Info, printFullLength: true); - // - // if (thisAddress == decodedTransaction['from']) { - // //Ensure this is not a self send - // if (decodedTransaction['from'] != decodedTransaction['to']) { - // midSortedTx["txType"] = "Sent"; - // midSortedTx["txid"] = decodedTransaction["hash"]; - // midSortedTx["height"] = i; - // midSortedTx["address"] = decodedTransaction['to']; - // int confirmations = 0; - // try { - // confirmations = currentBlock - i; - // } catch (e, s) { - // debugPrint("$e $s"); - // } - // midSortedTx["confirmations"] = confirmations; - // midSortedTx["inputSize"] = 1; - // midSortedTx["outputSize"] = 1; - // midSortedTx["aliens"] = []; - // midSortedTx["inputs"] = []; - // midSortedTx["outputs"] = []; - // - // midSortedArray.add(midSortedTx); - // Logging.instance.log( - // "TX SENT FROM THIS ACCOUNT ${decodedTransaction['from']} ${decodedTransaction['to']}", - // level: LogLevel.Info); - // --n; - // } - // - // if (thisAddress == decodedTransaction['to']) { - // if (decodedTransaction['from'] != decodedTransaction['to']) { - // midSortedTx["txType"] = "Received"; - // midSortedTx["txid"] = decodedTransaction["hash"]; - // midSortedTx["height"] = i; - // midSortedTx["address"] = decodedTransaction['from']; - // int confirmations = 0; - // try { - // confirmations = currentBlock - i; - // } catch (e, s) { - // debugPrint("$e $s"); - // } - // midSortedTx["confirmations"] = confirmations; - // midSortedTx["inputSize"] = 1; - // midSortedTx["outputSize"] = 1; - // midSortedTx["aliens"] = []; - // midSortedTx["inputs"] = []; - // midSortedTx["outputs"] = []; - // midSortedArray.add(midSortedTx); - // } - // } - // } - // }); - // } - // } catch (e, s) { - // print("Error getting transactions ${e.toString()} $s"); - // } - // } - // midSortedArray - // .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); - // final Map result = {"dateTimeChunks": []}; - // final dateArray = []; - // - // for (int i = 0; i < midSortedArray.length; i++) { - // final txObject = midSortedArray[i]; - // final date = extractDateFromTimestamp(txObject["timestamp"] as int); - // final txTimeArray = [txObject["timestamp"], date]; - // - // if (dateArray.contains(txTimeArray[1])) { - // result["dateTimeChunks"].forEach((dynamic chunk) { - // if (extractDateFromTimestamp(chunk["timestamp"] as int) == - // txTimeArray[1]) { - // if (chunk["transactions"] == null) { - // chunk["transactions"] = >[]; - // } - // chunk["transactions"].add(txObject); - // } - // }); - // } else { - // dateArray.add(txTimeArray[1]); - // final chunk = { - // "timestamp": txTimeArray[0], - // "transactions": [txObject], - // }; - // result["dateTimeChunks"].add(chunk); - // } - // } - // - // final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; - // transactionsMap - // .addAll(TransactionData.fromJson(result).getAllTransactions()); - // print("THIS CURRECT ADDRESS IS $thisAddress"); - // print("THIS CURRECT BLOCK IS $currentBlock"); - // print("THIS BALANCE IS $balance"); - // print("THIS COUNT TRANSACTIONS IS $n"); + midSortedArray.sort((a, b) => + (int.parse(b['timestamp'].toString())) - + (int.parse(a['timestamp'].toString()))); - return TransactionData(); - // throw UnimplementedError(); + // buildDateTimeChunks + final Map result = {"dateTimeChunks": []}; + final dateArray = []; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = + extractDateFromTimestamp(int.parse(txObject['timestamp'].toString())); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp( + int.parse(chunk['timestamp'].toString())) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = >[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + final transactionsMap = 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 From 045cf857fdd147cd0792b10e08e57da9d0d0d289 Mon Sep 17 00:00:00 2001 From: likho Date: Sun, 8 Jan 2023 17:19:58 +0200 Subject: [PATCH 013/208] Fix refresh loading forever --- lib/services/coins/coin_service.dart | 1 + .../coins/ethereum/ethereum_wallet.dart | 338 +++++++++++++++--- 2 files changed, 298 insertions(+), 41 deletions(-) diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 1793624b4..3aac57702 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -179,6 +179,7 @@ abstract class CoinServiceAPI { walletName: walletName, coin: coin, secureStore: secureStorageInterface, + tracker: tracker, ); case Coin.monero: diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 7f8388653..5d3ce2aec 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -10,6 +10,7 @@ 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/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; @@ -32,6 +33,11 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha 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'; + const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; @@ -82,23 +88,28 @@ class EthereumWallet extends CoinServiceAPI { Coin get coin => _coin; late SecureStorageInterface _secureStore; - + late final TransactionNotificationTracker txTracker; late PriceAPI _priceAPI; final _prefs = Prefs.instance; + bool longMutex = false; + final _client = Web3Client( "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", Client()); final _blockExplorer = "https://eth-goerli.blockscout.com/api?"; late EthPrivateKey _credentials; - int _chainId = 5; //5 for testnet and 1 for mainnet + final int _chainId = 5; //5 for testnet and 1 for mainnet - EthereumWallet( - {required String walletId, - required String walletName, - required Coin coin, - PriceAPI? priceAPI, - required SecureStorageInterface secureStore}) { + EthereumWallet({ + required String walletId, + required String walletName, + required Coin coin, + PriceAPI? priceAPI, + required SecureStorageInterface secureStore, + required TransactionNotificationTracker tracker, + }) { + txTracker = tracker; _walletId = walletId; _walletName = walletName; _coin = coin; @@ -107,17 +118,35 @@ class EthereumWallet extends CoinServiceAPI { _secureStore = secureStore; } + bool _shouldAutoSync = false; + @override - bool shouldAutoSync = false; + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } else { + startNetworkAlivePinging(); + refresh(); + } + } + } @override String get walletName => _walletName; late String _walletName; late Coin _coin; + Timer? timer; + Timer? _networkAliveTimer; @override - // TODO: implement allOwnAddresses Future> get allOwnAddresses => _allOwnAddresses ??= _fetchAllOwnAddresses(); Future>? _allOwnAddresses; @@ -143,6 +172,11 @@ class EthereumWallet extends CoinServiceAPI { @override Future confirmSend({required Map txData}) async { final gasPrice = await _client.getGasPrice(); + + //Get Gas Limit for current block + final blockInfo = await _client.getBlockInformation(blockNumber: 'latest'); + String gasLimit = blockInfo.gasLimit; + final amount = txData['recipientAmt']; final decimalAmount = Format.satoshisToAmount(amount as int, coin: Coin.ethereum); @@ -150,7 +184,7 @@ class EthereumWallet extends CoinServiceAPI { final tx = Transaction.Transaction( to: EthereumAddress.fromHex(txData['addresss'] as String), gasPrice: gasPrice, - maxGas: 21000, + maxGas: int.parse(gasLimit), value: EtherAmount.inWei(bigIntAmount)); final transaction = await _client.sendTransaction(_credentials, tx, chainId: _chainId); @@ -357,32 +391,168 @@ class EthereumWallet extends CoinServiceAPI { required int maxUnusedAddressGap, required int maxNumberOfIndexesToCheck, required int height}) async { - await _prefs.init(); - print("Mnemonic is $mnemonic"); - _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + longMutex = true; + final start = DateTime.now(); - await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); + try { + if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + print("DUPLICATE MNEMONIC"); + longMutex = false; + throw Exception("Attempted to overwrite mnemonic on restore!"); + } - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - await DB.instance.put( - boxName: walletId, key: 'receivingAddresses', value: ["0"]); - 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); + await _secureStore.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); + + _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + longMutex = false; + rethrow; + } + + longMutex = false; + final end = DateTime.now(); + Logging.instance.log( + "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", + level: LogLevel.Info); + } + + Future refreshIfThereIsNewData() async { + if (longMutex) return false; + if (_hasCalledExit) return false; + Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); + + try { + bool needsRefresh = false; + Set txnsToCheck = {}; + + for (final String txid in txTracker.pendings) { + if (!txTracker.wasNotifiedConfirmed(txid)) { + txnsToCheck.add(txid); + } + } + + // for (String txid in txnsToCheck) { + // final txn = await _client.getTransactionByHash(txid); + // int confirmations = txn["confirmations"] as int? ?? 0; + // bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; + // if (!isUnconfirmed) { + // // unconfirmedTxs = {}; + // needsRefresh = true; + // break; + // } + // } + // if (!needsRefresh) { + // var allOwnAddresses = await _fetchAllOwnAddresses(); + // List> allTxs = + // await _fetchHistory(allOwnAddresses); + // final txData = await transactionData; + // for (Map transaction in allTxs) { + // if (txData.findTransaction(transaction['tx_hash'] as String) == + // null) { + // Logging.instance.log( + // " txid not found in address history already ${transaction['tx_hash']}", + // level: LogLevel.Info); + // needsRefresh = true; + // break; + // } + // } + // } + return needsRefresh; + } catch (e, s) { + Logging.instance.log( + "Exception caught in refreshIfThereIsNewData: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future getAllTxsToWatch( + TransactionData txData, + ) async { + if (_hasCalledExit) return; + List unconfirmedTxnsToNotifyPending = []; + List unconfirmedTxnsToNotifyConfirmed = []; + + for (final chunk in txData.txChunks) { + for (final tx in chunk.transactions) { + if (tx.confirmedStatus) { + // get all transactions that were notified as pending but not as confirmed + if (txTracker.wasNotifiedPending(tx.txid) && + !txTracker.wasNotifiedConfirmed(tx.txid)) { + unconfirmedTxnsToNotifyConfirmed.add(tx); + } + } else { + // get all transactions that were not notified as pending yet + if (!txTracker.wasNotifiedPending(tx.txid)) { + unconfirmedTxnsToNotifyPending.add(tx); + } + } + } + } + + // notify on unconfirmed transactions + for (final tx in unconfirmedTxnsToNotifyPending) { + if (tx.txType == "Received") { + 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, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } else if (tx.txType == "Sent") { + 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, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } + } + + // notify on confirmed + for (final tx in unconfirmedTxnsToNotifyConfirmed) { + if (tx.txType == "Received") { + unawaited(NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } else if (tx.txType == "Sent") { + unawaited(NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } + } } @override @@ -395,7 +565,7 @@ class EthereumWallet extends CoinServiceAPI { refreshMutex = true; } - final blockNumber = await _client.getBlockNumber(); + // final blockNumber = await _client.getBlockNumber(); try { GlobalEventBus.instance.fire( @@ -423,9 +593,60 @@ class EthereumWallet extends CoinServiceAPI { unawaited(updateStoredChainHeight(newHeight: currentHeight)); } - final newTxData = await _fetchTransactionData(); + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); + + final newTxData = _fetchTransactionData(); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.50, walletId)); + + final feeObj = _getFees(); + 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); + await Future.wait([ + newTxData, + feeObj, + + /// TODO - GET fee object + allTxsToWatch, + ]); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.90, walletId)); + } + refreshMutex = false; + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + + if (shouldAutoSync) { + timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { + Logging.instance.log( + "Periodic refresh check for $walletId $walletName in object instance: $hashCode", + level: LogLevel.Info); + // chain height check currently broken + // if ((await chainHeight) != (await storedChainHeight)) { + if (await refreshIfThereIsNewData()) { + await refresh(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + } + // } + }); } } catch (error, strace) { refreshMutex = false; @@ -459,9 +680,27 @@ class EthereumWallet extends CoinServiceAPI { } @override - Future testNetworkConnection() { - // TODO: implement testNetworkConnection - throw UnimplementedError(); + Future testNetworkConnection() async { + //TODO - LOOK for correct implementation of ping + try { + // final result = await _electrumXClient.ping(); + // return result; + return true; + } catch (_) { + return false; + } + } + + void _periodicPingCheck() async { + bool hasNetwork = await testNetworkConnection(); + _isConnected = hasNetwork; + if (_isConnected != hasNetwork) { + NodeConnectionStatus status = hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + GlobalEventBus.instance + .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); + } } @override @@ -678,7 +917,24 @@ class EthereumWallet extends CoinServiceAPI { String get walletId => _walletId; late String _walletId; - @override @override set walletName(String newName) => _walletName = newName; + + void stopNetworkAlivePinging() { + _networkAliveTimer?.cancel(); + _networkAliveTimer = null; + } + + void startNetworkAlivePinging() { + // call once on start right away + _periodicPingCheck(); + + // then periodically check + _networkAliveTimer = Timer.periodic( + Constants.networkAliveTimerDuration, + (_) async { + _periodicPingCheck(); + }, + ); + } } From 357b08d4bf9e0e9d5547bbe577d6b2f074fd780d Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 9 Jan 2023 13:10:34 +0200 Subject: [PATCH 014/208] Error fixes --- .../coins/ethereum/ethereum_wallet.dart | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 5d3ce2aec..8d7942633 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -38,7 +38,7 @@ import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -const int MINIMUM_CONFIRMATIONS = 1; +const int MINIMUM_CONFIRMATIONS = 10; const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = @@ -396,7 +396,6 @@ class EthereumWallet extends CoinServiceAPI { try { if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { - print("DUPLICATE MNEMONIC"); longMutex = false; throw Exception("Attempted to overwrite mnemonic on restore!"); } @@ -405,6 +404,11 @@ class EthereumWallet extends CoinServiceAPI { key: '${_walletId}_mnemonic', value: mnemonic.trim()); _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put(boxName: walletId, key: "isFavorite", value: false); } catch (e, s) { Logging.instance.log( "Exception rethrown from recoverFromMnemonic(): $e\n$s", @@ -424,7 +428,8 @@ class EthereumWallet extends CoinServiceAPI { if (longMutex) return false; if (_hasCalledExit) return false; Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); - + Logging.instance.log("TX Tracker Pendings is ${txTracker.pendings}", + level: LogLevel.Info); try { bool needsRefresh = false; Set txnsToCheck = {}; @@ -435,16 +440,17 @@ class EthereumWallet extends CoinServiceAPI { } } - // for (String txid in txnsToCheck) { - // final txn = await _client.getTransactionByHash(txid); - // int confirmations = txn["confirmations"] as int? ?? 0; - // bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; - // if (!isUnconfirmed) { - // // unconfirmedTxs = {}; - // needsRefresh = true; - // break; - // } - // } + for (String txid in txnsToCheck) { + final txn = await _client.getTransactionByHash(txid); + print("TXS TO CHECK IS $txn"); + // int confirmations = txn["confirmations"] as int? ?? 0; + // bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; + // if (!isUnconfirmed) { + // // unconfirmedTxs = {}; + // needsRefresh = true; + // break; + // } + } // if (!needsRefresh) { // var allOwnAddresses = await _fetchAllOwnAddresses(); // List> allTxs = From ae0a515384fdf0182f7de1414afaed077dcd1717 Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 9 Jan 2023 19:15:40 +0200 Subject: [PATCH 015/208] Error fixes, remove hard coded values --- .../coins/ethereum/ethereum_wallet.dart | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 8d7942633..2d533180e 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -44,6 +44,7 @@ const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"; +//THis is used for mapping transactions per address from the block explorer class AddressTransaction { final String message; final List result; @@ -64,6 +65,26 @@ class AddressTransaction { } } +class GasTracker { + final String status; + final String message; + final List result; + + const GasTracker({ + required this.status, + required this.message, + required this.result, + }); + + factory GasTracker.fromJson(Map json) { + return GasTracker( + status: json['status'] as String, + message: json['message'] as String, + result: json['result'] as List, + ); + } +} + class EthereumWallet extends CoinServiceAPI { @override set isFavorite(bool markFavorite) { @@ -99,7 +120,6 @@ class EthereumWallet extends CoinServiceAPI { final _blockExplorer = "https://eth-goerli.blockscout.com/api?"; late EthPrivateKey _credentials; - final int _chainId = 5; //5 for testnet and 1 for mainnet EthereumWallet({ required String walletId, @@ -172,7 +192,7 @@ class EthereumWallet extends CoinServiceAPI { @override Future confirmSend({required Map txData}) async { final gasPrice = await _client.getGasPrice(); - + final int chainId = await _client.getNetworkId(); //Get Gas Limit for current block final blockInfo = await _client.getBlockInformation(blockNumber: 'latest'); String gasLimit = blockInfo.gasLimit; @@ -187,7 +207,7 @@ class EthereumWallet extends CoinServiceAPI { maxGas: int.parse(gasLimit), value: EtherAmount.inWei(bigIntAmount)); final transaction = - await _client.sendTransaction(_credentials, tx, chainId: _chainId); + await _client.sendTransaction(_credentials, tx, chainId: chainId); Logging.instance.log("Generated TX IS $transaction", level: LogLevel.Info); return transaction; @@ -226,6 +246,8 @@ class EthereumWallet extends CoinServiceAPI { Future? _feeObject; Future _getFees() async { + String feesEndPoint = + "https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=5JJW1UH269SV6ZPC78ZZI7H4QVV1A1TQDH"; return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 10, @@ -687,11 +709,9 @@ class EthereumWallet extends CoinServiceAPI { @override Future testNetworkConnection() async { - //TODO - LOOK for correct implementation of ping try { - // final result = await _electrumXClient.ping(); - // return result; - return true; + final result = await _client.isListeningForNetwork(); + return result; } catch (_) { return false; } From 8b87c7367a49b2045ceab3e146803c6f885220a2 Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 11 Jan 2023 15:35:51 +0200 Subject: [PATCH 016/208] Error fixes --- .../coins/ethereum/ethereum_wallet.dart | 98 ++++++++++--------- lib/utilities/default_nodes.dart | 2 +- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 2d533180e..b5e80d0b7 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -6,6 +6,7 @@ import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:flutter/foundation.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'; @@ -38,7 +39,11 @@ import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -const int MINIMUM_CONFIRMATIONS = 10; +import 'package:stackwallet/services/node_service.dart'; + +import 'package:stackwallet/utilities/default_nodes.dart'; + +const int MINIMUM_CONFIRMATIONS = 5; const int DUST_LIMIT = 294; const String GENESIS_HASH_MAINNET = @@ -86,6 +91,8 @@ class GasTracker { } class EthereumWallet extends CoinServiceAPI { + NodeModel? _ethNode; + @override set isFavorite(bool markFavorite) { DB.instance.put( @@ -181,7 +188,6 @@ class EthereumWallet extends CoinServiceAPI { @override Future get availableBalance async { EtherAmount ethBalance = await _client.getBalance(_credentials.address); - print("THIS ETH BALANCE IS $ethBalance"); return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); } @@ -193,23 +199,19 @@ class EthereumWallet extends CoinServiceAPI { Future confirmSend({required Map txData}) async { final gasPrice = await _client.getGasPrice(); final int chainId = await _client.getNetworkId(); - //Get Gas Limit for current block - final blockInfo = await _client.getBlockInformation(blockNumber: 'latest'); - String gasLimit = blockInfo.gasLimit; final amount = txData['recipientAmt']; final decimalAmount = Format.satoshisToAmount(amount as int, coin: Coin.ethereum); final bigIntAmount = amountToBigInt(decimalAmount.toDouble()); final tx = Transaction.Transaction( - to: EthereumAddress.fromHex(txData['addresss'] as String), + to: EthereumAddress.fromHex(txData['address'] as String), gasPrice: gasPrice, - maxGas: int.parse(gasLimit), + maxGas: 21000, value: EtherAmount.inWei(bigIntAmount)); final transaction = await _client.sendTransaction(_credentials, tx, chainId: chainId); - Logging.instance.log("Generated TX IS $transaction", level: LogLevel.Info); return transaction; } @@ -236,9 +238,11 @@ class EthereumWallet extends CoinServiceAPI { } @override - Future exit() { - // TODO: implement exit - throw UnimplementedError(); + Future exit() async { + _hasCalledExit = true; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); } @override @@ -266,7 +270,7 @@ class EthereumWallet extends CoinServiceAPI { @override Future generateNewAddress() { - // TODO: implement generateNewAddress + // TODO: implement generateNewAddress - might not be needed for ETH throw UnimplementedError(); } @@ -339,10 +343,11 @@ class EthereumWallet extends CoinServiceAPI { @override bool get isConnected => _isConnected; - bool refreshMutex = false; @override bool get isRefreshing => refreshMutex; + bool refreshMutex = false; + @override // TODO: implement maxFee Future get maxFee => throw UnimplementedError(); @@ -400,7 +405,7 @@ class EthereumWallet extends CoinServiceAPI { "fee": Format.decimalAmountToSatoshis( Decimal.parse(gasPrice.getValueInUnit(EtherUnit.ether).toString()), coin), - "addresss": address, + "address": address, "recipientAmt": satoshiAmount, }; @@ -449,9 +454,8 @@ class EthereumWallet extends CoinServiceAPI { Future refreshIfThereIsNewData() async { if (longMutex) return false; if (_hasCalledExit) return false; - Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); - Logging.instance.log("TX Tracker Pendings is ${txTracker.pendings}", - level: LogLevel.Info); + final currentChainHeight = await chainHeight; + try { bool needsRefresh = false; Set txnsToCheck = {}; @@ -464,14 +468,14 @@ class EthereumWallet extends CoinServiceAPI { for (String txid in txnsToCheck) { final txn = await _client.getTransactionByHash(txid); - print("TXS TO CHECK IS $txn"); - // int confirmations = txn["confirmations"] as int? ?? 0; - // bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; - // if (!isUnconfirmed) { - // // unconfirmedTxs = {}; - // needsRefresh = true; - // break; - // } + final int txBlockNumber = txn.blockNumber.blockNum; + + final int txConfirmations = currentChainHeight - txBlockNumber; + bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS; + if (!isUnconfirmed) { + needsRefresh = true; + break; + } } // if (!needsRefresh) { // var allOwnAddresses = await _fetchAllOwnAddresses(); @@ -660,22 +664,22 @@ class EthereumWallet extends CoinServiceAPI { ), ); - if (shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - Logging.instance.log( - "Periodic refresh check for $walletId $walletName in object instance: $hashCode", - level: LogLevel.Info); - // chain height check currently broken - // if ((await chainHeight) != (await storedChainHeight)) { - if (await refreshIfThereIsNewData()) { - await refresh(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - } - // } - }); - } + // if (shouldAutoSync) { + // timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { + // Logging.instance.log( + // "Periodic refresh check for $walletId $walletName in object instance: $hashCode", + // level: LogLevel.Info); + // // chain height check currently broken + // // if ((await chainHeight) != (await storedChainHeight)) { + // if (await refreshIfThereIsNewData()) { + // await refresh(); + // GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + // "New data found in $walletId $walletName in background!", + // walletId)); + // } + // // } + // }); + // } } catch (error, strace) { refreshMutex = false; GlobalEventBus.instance.fire( @@ -748,9 +752,14 @@ class EthereumWallet extends CoinServiceAPI { Future> get unspentOutputs => throw UnimplementedError(); @override - Future updateNode(bool shouldRefresh) { - // TODO: implement updateNode - throw UnimplementedError(); + Future updateNode(bool shouldRefresh) async { + _ethNode = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + + if (shouldRefresh) { + unawaited(refresh()); + } } @override @@ -844,7 +853,6 @@ class EthereumWallet extends CoinServiceAPI { transactionAmount / (pow(10, decimal)); //Convert to satoshi, default display for other coins - // Decimal.parse(gasPrice.getValueInUnit(EtherUnit.ether).toString()) final satAmount = Format.decimalAmountToSatoshis( Decimal.parse(transactionAmountInDecimal.toString()), coin); diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 2055b9f17..30cee1368 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -136,7 +136,7 @@ abstract class DefaultNodes { //TODO - Update with correct node details for ETH static NodeModel get ethereum => NodeModel( - host: "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", + host: "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", port: 1234, name: defaultName, id: _nodeId(Coin.ethereum), From 734a51f5dd7541f70a722e5f60900cb6b9cec78c Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 12 Jan 2023 10:09:11 +0200 Subject: [PATCH 017/208] WIP: Get gas estimator --- .../coins/ethereum/ethereum_wallet.dart | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index b5e80d0b7..a19148bb6 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -71,21 +71,18 @@ class AddressTransaction { } class GasTracker { - final String status; - final String message; - final List result; + final int code; + final String data; const GasTracker({ - required this.status, - required this.message, - required this.result, + required this.code, + required this.data, }); factory GasTracker.fromJson(Map json) { return GasTracker( - status: json['status'] as String, - message: json['message'] as String, - result: json['result'] as List, + code: json['code'] as int, + data: json['data'] as String, ); } } @@ -250,8 +247,10 @@ class EthereumWallet extends CoinServiceAPI { Future? _feeObject; Future _getFees() async { - String feesEndPoint = - "https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=5JJW1UH269SV6ZPC78ZZI7H4QVV1A1TQDH"; + GasTracker fees = await getGasOracle(); + if (fees.code == 200) { + print("FEES IS ${fees.data}"); + } return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 10, @@ -261,6 +260,18 @@ class EthereumWallet extends CoinServiceAPI { slow: 1); } + Future getGasOracle() async { + final response = + await get(Uri.parse("https://beaconcha.in/api/v1/execution/gasnow")); + + if (response.statusCode == 200) { + return GasTracker.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception('Failed to load gas oracle'); + } + } + @override Future fullRescan( int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { From ab4392b0bded092d9eed95f7c756154efd0f609f Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 12 Jan 2023 16:14:49 +0200 Subject: [PATCH 018/208] WIP: Add transaction fees --- .../coins/ethereum/ethereum_wallet.dart | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index a19148bb6..f099cc3d9 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -14,6 +14,7 @@ import 'package:stackwallet/services/price.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -72,7 +73,7 @@ class AddressTransaction { class GasTracker { final int code; - final String data; + final Map data; const GasTracker({ required this.code, @@ -82,13 +83,14 @@ class GasTracker { factory GasTracker.fromJson(Map json) { return GasTracker( code: json['code'] as int, - data: json['data'] as String, + data: json['data'] as Map, ); } } class EthereumWallet extends CoinServiceAPI { NodeModel? _ethNode; + final _gasLimit = 21000; @override set isFavorite(bool markFavorite) { @@ -197,14 +199,17 @@ class EthereumWallet extends CoinServiceAPI { final gasPrice = await _client.getGasPrice(); final int chainId = await _client.getNetworkId(); + print("GAS PRICE IS $gasPrice"); + print("AMOUNT TO SEND IS ${txData['fee']}"); + final amount = txData['recipientAmt']; final decimalAmount = Format.satoshisToAmount(amount as int, coin: Coin.ethereum); final bigIntAmount = amountToBigInt(decimalAmount.toDouble()); final tx = Transaction.Transaction( to: EthereumAddress.fromHex(txData['address'] as String), - gasPrice: gasPrice, - maxGas: 21000, + gasPrice: EtherAmount.fromUnitAndValue(EtherUnit.gwei, txData['fee']), + maxGas: _gasLimit, value: EtherAmount.inWei(bigIntAmount)); final transaction = await _client.sendTransaction(_credentials, tx, chainId: chainId); @@ -228,10 +233,14 @@ class EthereumWallet extends CoinServiceAPI { @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { - print("CALLING ESTIMATE FEE"); - // TODO: implement estimateFeeFor - // throw UnimplementedError(); - return 1; + 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); } @override @@ -248,16 +257,14 @@ class EthereumWallet extends CoinServiceAPI { Future _getFees() async { GasTracker fees = await getGasOracle(); - if (fees.code == 200) { - print("FEES IS ${fees.data}"); - } + final feesMap = fees.data; return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 10, - numberOfBlocksSlow: 10, - fast: 1, - medium: 1, - slow: 1); + numberOfBlocksFast: 3, + numberOfBlocksAverage: 3, + numberOfBlocksSlow: 1, + fast: feesMap['fast'] as int, + medium: feesMap['standard'] as int, + slow: feesMap['slow'] as int); } Future getGasOracle() async { @@ -410,12 +417,29 @@ class EthereumWallet extends CoinServiceAPI { {required String address, required int satoshiAmount, Map? args}) async { + print("CALLING PREPARE SEND"); + print(args); + 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); + print("FEE ESTIMATE IS $feeEstimate"); + final gasPrice = await _client.getGasPrice(); Map txData = { - "fee": Format.decimalAmountToSatoshis( - Decimal.parse(gasPrice.getValueInUnit(EtherUnit.ether).toString()), - coin), + "fee": feeEstimate, "address": address, "recipientAmt": satoshiAmount, }; From 27322061f68c47a0c965ba07deb836f7c9347b01 Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 12 Jan 2023 19:24:26 +0200 Subject: [PATCH 019/208] Implement gas price oracle, Update all fees to use new gas estimate --- .../coins/ethereum/ethereum_wallet.dart | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index f099cc3d9..e30e03aba 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -191,24 +191,24 @@ class EthereumWallet extends CoinServiceAPI { } @override - // TODO: implement balanceMinusMaxFee - Future get balanceMinusMaxFee => throw UnimplementedError(); + Future get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(); @override Future confirmSend({required Map txData}) async { - final gasPrice = await _client.getGasPrice(); final int chainId = await _client.getNetworkId(); - - print("GAS PRICE IS $gasPrice"); - print("AMOUNT TO SEND IS ${txData['fee']}"); - final amount = txData['recipientAmt']; final decimalAmount = Format.satoshisToAmount(amount as int, coin: Coin.ethereum); final bigIntAmount = amountToBigInt(decimalAmount.toDouble()); + final tx = Transaction.Transaction( to: EthereumAddress.fromHex(txData['address'] as String), - gasPrice: EtherAmount.fromUnitAndValue(EtherUnit.gwei, txData['fee']), + gasPrice: + EtherAmount.fromUnitAndValue(EtherUnit.wei, txData['feeInWei']), maxGas: _gasLimit, value: EtherAmount.inWei(bigIntAmount)); final transaction = @@ -259,9 +259,9 @@ class EthereumWallet extends CoinServiceAPI { GasTracker fees = await getGasOracle(); final feesMap = fees.data; return FeeObject( - numberOfBlocksFast: 3, + numberOfBlocksFast: 1, numberOfBlocksAverage: 3, - numberOfBlocksSlow: 1, + numberOfBlocksSlow: 3, fast: feesMap['fast'] as int, medium: feesMap['standard'] as int, slow: feesMap['slow'] as int); @@ -279,6 +279,7 @@ class EthereumWallet extends CoinServiceAPI { } } + //Full rescan is not needed for ETH since we have a balance @override Future fullRescan( int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { @@ -327,7 +328,6 @@ class EthereumWallet extends CoinServiceAPI { await _prefs.init(); final String mnemonic = bip39.generateMnemonic(strength: 256); _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); - await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); //Store credentials in secure store @@ -367,8 +367,11 @@ class EthereumWallet extends CoinServiceAPI { bool refreshMutex = false; @override - // TODO: implement maxFee - Future get maxFee => throw UnimplementedError(); + Future get maxFee async { + final fee = (await fees).fast; + final feeEstimate = await estimateFeeFor(0, fee); + return feeEstimate; + } @override Future> get mnemonic => _getMnemonicList(); @@ -407,18 +410,14 @@ class EthereumWallet extends CoinServiceAPI { } @override - // TODO: implement pendingBalance + // TODO: implement pendingBalance - Not needed since we don't use UTXOs to get a balance Future get pendingBalance => throw UnimplementedError(); - // Future transactionFee(int satoshiAmount) {} - @override Future> prepareSend( {required String address, required int satoshiAmount, Map? args}) async { - print("CALLING PREPARE SEND"); - print(args); final feeRateType = args?["feeRate"]; int fee = 0; final feeObject = await fees; @@ -433,13 +432,12 @@ class EthereumWallet extends CoinServiceAPI { fee = feeObject.slow; break; } - final feeEstimate = await estimateFeeFor(satoshiAmount, fee); - print("FEE ESTIMATE IS $feeEstimate"); - final gasPrice = await _client.getGasPrice(); + final feeEstimate = await estimateFeeFor(satoshiAmount, fee); Map txData = { "fee": feeEstimate, + "feeInWei": fee, "address": address, "recipientAmt": satoshiAmount, }; @@ -682,8 +680,6 @@ class EthereumWallet extends CoinServiceAPI { await Future.wait([ newTxData, feeObj, - - /// TODO - GET fee object allTxsToWatch, ]); GlobalEventBus.instance @@ -769,7 +765,6 @@ class EthereumWallet extends CoinServiceAPI { } @override - // TODO: Check difference between total and available balance for eth Future get totalBalance async { EtherAmount ethBalance = await _client.getBalance(_credentials.address); return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); @@ -783,7 +778,7 @@ class EthereumWallet extends CoinServiceAPI { TransactionData? cachedTxData; @override - // TODO: implement unspentOutputs + // TODO: implement unspentOutputs - NOT NEEDED, ETH DOES NOT USE UTXOs Future> get unspentOutputs => throw UnimplementedError(); @override From 78a64690ed11894260d31d27cb9407733fd04cf7 Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 13 Jan 2023 11:21:10 +0200 Subject: [PATCH 020/208] Fix sendall --- lib/services/coins/ethereum/ethereum_wallet.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index e30e03aba..52967e712 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -434,6 +434,20 @@ class EthereumWallet extends CoinServiceAPI { } final feeEstimate = await estimateFeeFor(satoshiAmount, fee); + print("FEE ESTIMATE IS $feeEstimate"); + print("AMOUNT TO SEND IS $satoshiAmount"); + + 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 txData = { "fee": feeEstimate, @@ -868,6 +882,7 @@ class EthereumWallet extends CoinServiceAPI { final List> midSortedArray = []; AddressTransaction txs = await fetchAddressTransactions(thisAddress); + if (txs.message == "OK") { final allTxs = txs.result; allTxs.forEach((element) { From 5a1569b3fade5dddf1ca52e9f43198d823581038 Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 13 Jan 2023 16:36:50 +0200 Subject: [PATCH 021/208] Clean up and fixes --- .../coins/ethereum/ethereum_wallet.dart | 108 +++++++++--------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 52967e712..cd4c7a92f 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -5,7 +5,6 @@ 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:flutter/foundation.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'; @@ -37,16 +36,11 @@ 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'; const int MINIMUM_CONFIRMATIONS = 5; -const int DUST_LIMIT = 294; - const String GENESIS_HASH_MAINNET = "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"; @@ -91,6 +85,8 @@ class GasTracker { class EthereumWallet extends CoinServiceAPI { NodeModel? _ethNode; final _gasLimit = 21000; + final _blockExplorer = "https://eth-goerli.blockscout.com/api?"; + final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; @override set isFavorite(bool markFavorite) { @@ -120,10 +116,16 @@ class EthereumWallet extends CoinServiceAPI { final _prefs = Prefs.instance; bool longMutex = false; - final _client = Web3Client( - "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", Client()); + Future getCurrentNode() async { + return NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } - final _blockExplorer = "https://eth-goerli.blockscout.com/api?"; + Future getEthClient() async { + final node = await getCurrentNode(); + return Web3Client(node.host, Client()); + } late EthPrivateKey _credentials; @@ -186,7 +188,8 @@ class EthereumWallet extends CoinServiceAPI { @override Future get availableBalance async { - EtherAmount ethBalance = await _client.getBalance(_credentials.address); + Web3Client client = await getEthClient(); + EtherAmount ethBalance = await client.getBalance(_credentials.address); return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); } @@ -199,7 +202,8 @@ class EthereumWallet extends CoinServiceAPI { @override Future confirmSend({required Map txData}) async { - final int chainId = await _client.getNetworkId(); + Web3Client client = await getEthClient(); + final int chainId = await client.getNetworkId(); final amount = txData['recipientAmt']; final decimalAmount = Format.satoshisToAmount(amount as int, coin: Coin.ethereum); @@ -212,7 +216,7 @@ class EthereumWallet extends CoinServiceAPI { maxGas: _gasLimit, value: EtherAmount.inWei(bigIntAmount)); final transaction = - await _client.sendTransaction(_credentials, tx, chainId: chainId); + await client.sendTransaction(_credentials, tx, chainId: chainId); return transaction; } @@ -268,8 +272,7 @@ class EthereumWallet extends CoinServiceAPI { } Future getGasOracle() async { - final response = - await get(Uri.parse("https://beaconcha.in/api/v1/execution/gasnow")); + final response = await get(Uri.parse(_gasTrackerUrl)); if (response.statusCode == 200) { return GasTracker.fromJson( @@ -377,8 +380,9 @@ class EthereumWallet extends CoinServiceAPI { Future> get mnemonic => _getMnemonicList(); Future get chainHeight async { + Web3Client client = await getEthClient(); try { - final result = await _client.getBlockNumber(); + final result = await client.getBlockNumber(); return result; } catch (e, s) { @@ -499,6 +503,7 @@ class EthereumWallet extends CoinServiceAPI { } Future refreshIfThereIsNewData() async { + Web3Client client = await getEthClient(); if (longMutex) return false; if (_hasCalledExit) return false; final currentChainHeight = await chainHeight; @@ -514,7 +519,7 @@ class EthereumWallet extends CoinServiceAPI { } for (String txid in txnsToCheck) { - final txn = await _client.getTransactionByHash(txid); + final txn = await client.getTransactionByHash(txid); final int txBlockNumber = txn.blockNumber.blockNum; final int txConfirmations = currentChainHeight - txBlockNumber; @@ -524,22 +529,23 @@ class EthereumWallet extends CoinServiceAPI { break; } } - // if (!needsRefresh) { - // var allOwnAddresses = await _fetchAllOwnAddresses(); - // List> allTxs = - // await _fetchHistory(allOwnAddresses); - // final txData = await transactionData; - // for (Map transaction in allTxs) { - // if (txData.findTransaction(transaction['tx_hash'] as String) == - // null) { - // Logging.instance.log( - // " txid not found in address history already ${transaction['tx_hash']}", - // level: LogLevel.Info); - // needsRefresh = true; - // break; - // } - // } - // } + if (!needsRefresh) { + var allOwnAddresses = await _fetchAllOwnAddresses(); + AddressTransaction addressTransactions = + await fetchAddressTransactions(allOwnAddresses.elementAt(0)); + final txData = await transactionData; + if (addressTransactions.message == "OK") { + final allTxs = addressTransactions.result; + allTxs.forEach((element) { + if (txData.findTransaction(element["hash"] as String) == null) { + Logging.instance.log( + " txid not found in address history already ${element["hash"]}", + level: LogLevel.Info); + needsRefresh = true; + } + }); + } + } return needsRefresh; } catch (e, s) { Logging.instance.log( @@ -644,8 +650,6 @@ class EthereumWallet extends CoinServiceAPI { refreshMutex = true; } - // final blockNumber = await _client.getBlockNumber(); - try { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( @@ -709,22 +713,19 @@ class EthereumWallet extends CoinServiceAPI { ), ); - // if (shouldAutoSync) { - // timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - // Logging.instance.log( - // "Periodic refresh check for $walletId $walletName in object instance: $hashCode", - // level: LogLevel.Info); - // // chain height check currently broken - // // if ((await chainHeight) != (await storedChainHeight)) { - // if (await refreshIfThereIsNewData()) { - // await refresh(); - // GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - // "New data found in $walletId $walletName in background!", - // walletId)); - // } - // // } - // }); - // } + if (shouldAutoSync) { + timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { + Logging.instance.log( + "Periodic refresh check for $walletId $walletName in object instance: $hashCode", + level: LogLevel.Info); + if (await refreshIfThereIsNewData()) { + await refresh(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + } + }); + } } catch (error, strace) { refreshMutex = false; GlobalEventBus.instance.fire( @@ -758,8 +759,9 @@ class EthereumWallet extends CoinServiceAPI { @override Future testNetworkConnection() async { + Web3Client client = await getEthClient(); try { - final result = await _client.isListeningForNetwork(); + final result = await client.isListeningForNetwork(); return result; } catch (_) { return false; @@ -780,7 +782,8 @@ class EthereumWallet extends CoinServiceAPI { @override Future get totalBalance async { - EtherAmount ethBalance = await _client.getBalance(_credentials.address); + Web3Client client = await getEthClient(); + EtherAmount ethBalance = await client.getBalance(_credentials.address); return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); } @@ -887,7 +890,6 @@ class EthereumWallet extends CoinServiceAPI { final allTxs = txs.result; allTxs.forEach((element) { Map midSortedTx = {}; - // create final tx map midSortedTx["txid"] = element["hash"]; int confirmations = int.parse(element['confirmations'].toString()); From dd8319ee11e4c68c151e972957d2733907200c1a Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 16 Jan 2023 09:42:29 +0200 Subject: [PATCH 022/208] Update ETH color --- lib/utilities/theme/color_theme.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index fbb96a5ea..cef2951f3 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -217,7 +217,7 @@ class CoinThemeColor { Color get firo => const Color(0xFFFF897A); Color get dogecoin => const Color(0xFFFFE079); Color get epicCash => const Color(0xFFC5C7CB); - Color get ethereum => const Color(0xFFC5C7CB); //TODO - USE CORRECT COLOR FOR ETH + Color get ethereum => const Color(0xFFA7ADE9); Color get monero => const Color(0xFFFF9E6B); Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); From b5a19837343e0e4484a7c432a7cd5603541bb1fa Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 16 Jan 2023 15:19:32 +0200 Subject: [PATCH 023/208] WIP: ADD ECR-20 --- assets/svg/circle-plus.svg | 10 + assets/svg/tokens.svg | 11 + lib/pages/token_view/all_tokens_view.dart | 16 ++ lib/pages/token_view/my_tokens_view.dart | 265 ++++++++++++++++++ .../sub_widgets/no_tokens_found.dart | 25 ++ .../sub_widgets/wallet_navigation_bar.dart | 40 +++ lib/pages/wallet_view/wallet_view.dart | 7 + lib/route_generator.dart | 15 + lib/utilities/assets.dart | 2 + pubspec.yaml | 2 + 10 files changed, 393 insertions(+) create mode 100644 assets/svg/circle-plus.svg create mode 100644 assets/svg/tokens.svg create mode 100644 lib/pages/token_view/all_tokens_view.dart create mode 100644 lib/pages/token_view/my_tokens_view.dart create mode 100644 lib/pages/token_view/sub_widgets/no_tokens_found.dart diff --git a/assets/svg/circle-plus.svg b/assets/svg/circle-plus.svg new file mode 100644 index 000000000..a09b12711 --- /dev/null +++ b/assets/svg/circle-plus.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/svg/tokens.svg b/assets/svg/tokens.svg new file mode 100644 index 000000000..be52b614c --- /dev/null +++ b/assets/svg/tokens.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/pages/token_view/all_tokens_view.dart b/lib/pages/token_view/all_tokens_view.dart new file mode 100644 index 000000000..6d80ff2c7 --- /dev/null +++ b/lib/pages/token_view/all_tokens_view.dart @@ -0,0 +1,16 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AllTokensView extends ConsumerStatefulWidget { + const AllTokensView({ + Key? key, + }) : super(key: key); + + static const String routeName = "/allTokens"; + + @override + ConsumerState createState() { + // TODO: implement createState + throw UnimplementedError(); + } +} diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart new file mode 100644 index 000000000..965f2cc54 --- /dev/null +++ b/lib/pages/token_view/my_tokens_view.dart @@ -0,0 +1,265 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; + +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; + +import 'package:stackwallet/pages/token_view/all_tokens_view.dart'; + +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; + +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; + +class MyTokensView extends ConsumerStatefulWidget { + const MyTokensView({ + Key? key, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/myTokens"; + final String walletId; + + @override + ConsumerState createState() => _TokenDetailsViewState(); +} + +class _TokenDetailsViewState extends ConsumerState { + late final String walletId; + + late final TextEditingController _searchController; + final searchFieldFocusNode = FocusNode(); + + @override + void initState() { + walletId = widget.walletId; + _searchController = TextEditingController(); + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + String _searchString = ""; + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + return MasterScaffold( + background: Theme.of(context).extension()!.background, + isDesktop: isDesktop, + appBar: isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox( + width: 12, + ), + Text( + "My ETH Wallet Tokens", + style: STextStyles.desktopH3(context), + ), + ], + ), + ) + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "My ETH Wallet Tokens", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("transactionSearchFilterViewButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension()! + .background, + icon: SvgPicture.asset( + Assets.svg.circlePlusDark, + color: Theme.of(context) + .extension()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + AllTokensView.routeName, + ); + }, + ), + ), + ), + ], + ), + body: Padding( + padding: EdgeInsets.only( + left: isDesktop ? 20 : 12, + top: isDesktop ? 20 : 12, + right: isDesktop ? 20 : 12, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: Row( + children: [ + ConditionalParent( + condition: isDesktop, + builder: (child) => SizedBox( + width: 570, + child: child, + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Search...", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 12 : 10, + vertical: isDesktop ? 18 : 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: isDesktop ? 20 : 16, + height: isDesktop ? 20 : 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ), + if (isDesktop) + const SizedBox( + width: 20, + ), + // const NoTransActionsFound(), + ], + ), + ), + if (isDesktop) + const SizedBox( + height: 8, + ), + const SizedBox( + height: 8, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/no_tokens_found.dart b/lib/pages/token_view/sub_widgets/no_tokens_found.dart new file mode 100644 index 000000000..4fbfff50e --- /dev/null +++ b/lib/pages/token_view/sub_widgets/no_tokens_found.dart @@ -0,0 +1,25 @@ +import 'package:flutter/cupertino.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class NoTokensFound extends StatelessWidget { + const NoTokensFound({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "You do not have any tokens", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + ], + ); + } +} 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 04147c883..8942a3990 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -11,6 +11,7 @@ class WalletNavigationBar extends StatelessWidget { required this.onSendPressed, required this.onExchangePressed, required this.onBuyPressed, + required this.onTokensPressed, required this.height, required this.enableExchange, }) : super(key: key); @@ -19,6 +20,7 @@ class WalletNavigationBar extends StatelessWidget { final VoidCallback onSendPressed; final VoidCallback onExchangePressed; final VoidCallback onBuyPressed; + final VoidCallback onTokensPressed; final double height; final bool enableExchange; @@ -231,6 +233,44 @@ class WalletNavigationBar extends StatelessWidget { // ), // ), // ), + 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 61a1da0ac..12869d6a9 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -14,6 +14,7 @@ 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/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'; @@ -776,6 +777,12 @@ class _WalletViewState extends ConsumerState { ); }, onBuyPressed: () {}, + onTokensPressed: () { + Navigator.of(context).pushNamed( + MyTokensView.routeName, + arguments: walletId, + ); + }, ), ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index ed676c437..3d6eed831 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -78,6 +78,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; +import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; @@ -1292,6 +1293,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case MyTokensView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => MyTokensView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == End of desktop specific routes ===================================== default: diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index e83a3a7e7..dc4dbab5f 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -167,6 +167,8 @@ class _SVG { String get exitDesktop => "assets/svg/exit-desktop.svg"; String get keys => "assets/svg/keys.svg"; String get arrowDown => "assets/svg/arrow-down.svg"; + String get tokens => "assets/svg/tokens.svg"; + String get circlePlusDark => "assets/svg/circle-plus.svg"; String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index 1da77f560..7ffe9871f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -306,6 +306,8 @@ flutter: - assets/svg/arrow-down.svg - assets/svg/plus-circle.svg - assets/svg/configuration.svg + - assets/svg/tokens.svg + - assets/svg/circle-plus.svg # coin icons - assets/svg/coin_icons/Bitcoin.svg - assets/svg/coin_icons/Litecoin.svg From 74ab70df7e92bb854bcd46e5e6d573b366388a4f Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 19 Jan 2023 10:45:09 +0200 Subject: [PATCH 024/208] Use mainnet --- lib/services/coins/ethereum/ethereum_wallet.dart | 2 +- lib/utilities/default_nodes.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index cd4c7a92f..8c9f42455 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -85,7 +85,7 @@ class GasTracker { class EthereumWallet extends CoinServiceAPI { NodeModel? _ethNode; final _gasLimit = 21000; - final _blockExplorer = "https://eth-goerli.blockscout.com/api?"; + final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; @override diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 30cee1368..2055b9f17 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -136,7 +136,7 @@ abstract class DefaultNodes { //TODO - Update with correct node details for ETH static NodeModel get ethereum => NodeModel( - host: "https://goerli.infura.io/v3/22677300bf774e49a458b73313ee56ba", + host: "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", port: 1234, name: defaultName, id: _nodeId(Coin.ethereum), From 706cbbfa39a2adbb35517dfc353e988467ee7be3 Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 20 Jan 2023 19:24:19 +0200 Subject: [PATCH 025/208] Add tokens class --- lib/pages/token_view/my_tokens_view.dart | 28 +- .../sub_widgets/my_token_select_item.dart | 103 ++ .../sub_widgets/my_tokens_list.dart | 35 + lib/pages/token_view/token_view.dart | 823 ++++++++++++++ lib/pages/wallet_view/wallet_view.dart | 20 +- lib/route_generator.dart | 21 +- lib/services/coins/erc_20/erc_wallet.dart | 1007 +++++++++++++++++ .../coins/ethereum/ethereum_wallet.dart | 5 +- lib/utilities/eth_commons.dart | 56 + 9 files changed, 2078 insertions(+), 20 deletions(-) create mode 100644 lib/pages/token_view/sub_widgets/my_token_select_item.dart create mode 100644 lib/pages/token_view/sub_widgets/my_tokens_list.dart create mode 100644 lib/pages/token_view/token_view.dart create mode 100644 lib/services/coins/erc_20/erc_wallet.dart create mode 100644 lib/utilities/eth_commons.dart diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index 965f2cc54..f5a7f8bcd 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/my_tokens_list.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -22,29 +24,33 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; +import 'package:stackwallet/utilities/eth_commons.dart'; class MyTokensView extends ConsumerStatefulWidget { const MyTokensView({ Key? key, - required this.walletId, + required this.walletAddress, + required this.tokens, + required this.walletName, }) : super(key: key); static const String routeName = "/myTokens"; - final String walletId; + final String walletAddress; + final List tokens; + final String walletName; @override ConsumerState createState() => _TokenDetailsViewState(); } class _TokenDetailsViewState extends ConsumerState { - late final String walletId; - + late final String walletAddress; late final TextEditingController _searchController; final searchFieldFocusNode = FocusNode(); @override void initState() { - walletId = widget.walletId; + walletAddress = widget.walletAddress; _searchController = TextEditingController(); super.initState(); @@ -95,7 +101,7 @@ class _TokenDetailsViewState extends ConsumerState { width: 12, ), Text( - "My ETH Wallet Tokens", + "${widget.walletName} Tokens", style: STextStyles.desktopH3(context), ), ], @@ -117,7 +123,7 @@ class _TokenDetailsViewState extends ConsumerState { }, ), title: Text( - "My ETH Wallet Tokens", + "${widget.walletName} Tokens", style: STextStyles.navBarTitle(context), ), actions: [ @@ -250,13 +256,13 @@ class _TokenDetailsViewState extends ConsumerState { ], ), ), - if (isDesktop) - const SizedBox( - height: 8, - ), const SizedBox( height: 8, ), + Expanded( + child: MyTokensList( + tokens: widget.tokens, walletAddress: walletAddress), + ), ], ), ), diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart new file mode 100644 index 000000000..8d62aaeef --- /dev/null +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -0,0 +1,103 @@ +import 'dart:math'; + +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/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; + +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; + +class MyTokenSelectItem extends ConsumerWidget { + const MyTokenSelectItem( + {Key? key, + required this.walletAddress, + required this.tokenData, + required}) + : super(key: key); + + final String walletAddress; + final Map tokenData; + + @override + Widget build(BuildContext context, WidgetRef ref) { + int balance = int.parse(tokenData["balance"] as String); + int tokenDecimals = int.parse(tokenData["decimals"] as String); + final balanceInDecimal = (balance / (pow(10, tokenDecimals))); + + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: MaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + key: Key("walletListItemButtonKey_${tokenData["symbol"]}"), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + ), + onPressed: () { + Navigator.of(context).pushNamed( + TokenView.routeName, + arguments: Tuple2(walletAddress, tokenData["contractAddress"]), + ); + }, + + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: Coin.ethereum), + width: 28, + height: 28, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Consumer( + builder: (_, ref, __) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + tokenData["name"] as String, + style: STextStyles.titleBold12(context), + ), + const Spacer(), + Text( + "$balanceInDecimal ${tokenData["symbol"]}", + style: STextStyles.itemSubtitle(context), + ), + ], + ), + const SizedBox( + height: 1, + ), + Row( + children: [ + Text( + tokenData["symbol"] as String, + style: STextStyles.itemSubtitle(context), + ), + const Spacer(), + const Text("0 USD"), + ], + ), + ], + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart new file mode 100644 index 000000000..f9c5b31b9 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart'; + +class MyTokensList extends StatelessWidget { + const MyTokensList({ + Key? key, + required this.tokens, + required this.walletAddress, + }) : super(key: key); + + final List tokens; + final String walletAddress; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (_, ref, __) { + print("TOKENS LENGTH IS ${tokens.length}"); + return ListView.builder( + itemCount: tokens.length, + itemBuilder: (ctx, index) { + return Padding( + padding: const EdgeInsets.all(4), + child: MyTokenSelectItem( + walletAddress: walletAddress, + tokenData: tokens[index] as Map, + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart new file mode 100644 index 000000000..046a4fb4a --- /dev/null +++ b/lib/pages/token_view/token_view.dart @@ -0,0 +1,823 @@ +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/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/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'; +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/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 { + const TokenView({ + Key? key, + required this.walletId, + required this.contractAddress, + required this.managerProvider, + this.eventBus, + required this.walletAddress, + }) : super(key: key); + + static const String routeName = "/token"; + static const double navBarHeight = 65.0; + + final String walletId; + final String contractAddress; + final String walletAddress; + final ChangeNotifierProvider managerProvider; + + final EventBus? eventBus; + + @override + ConsumerState createState() => _TokenViewState(); +} + +class _TokenViewState extends ConsumerState { + late final EventBus eventBus; + late final String walletId; + late final ChangeNotifierProvider managerProvider; + + late final bool _shouldDisableAutoSyncOnLogOut; + + late WalletSyncStatus _currentSyncStatus; + late NodeConnectionStatus _currentNodeStatus; + + late StreamSubscription _syncStatusSubscription; + late StreamSubscription _nodeStatusSubscription; + + final _cnLoadingService = ExchangeDataLoadingService(); + + @override + void initState() { + walletId = widget.walletId; + managerProvider = widget.managerProvider; + + ref.read(managerProvider).isActiveWallet = true; + if (!ref.read(managerProvider).shouldAutoSync) { + // enable auto sync if it wasn't enabled when loading wallet + ref.read(managerProvider).shouldAutoSync = true; + _shouldDisableAutoSyncOnLogOut = true; + } else { + _shouldDisableAutoSyncOnLogOut = false; + } + + ref.read(managerProvider).refresh(); + + if (ref.read(managerProvider).isRefreshing) { + _currentSyncStatus = WalletSyncStatus.syncing; + _currentNodeStatus = NodeConnectionStatus.connected; + } else { + _currentSyncStatus = WalletSyncStatus.synced; + if (ref.read(managerProvider).isConnected) { + _currentNodeStatus = NodeConnectionStatus.connected; + } else { + _currentNodeStatus = NodeConnectionStatus.disconnected; + _currentSyncStatus = WalletSyncStatus.unableToSync; + } + } + + eventBus = + widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; + + _syncStatusSubscription = + eventBus.on().listen( + (event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case WalletSyncStatus.unableToSync: + // break; + // case WalletSyncStatus.synced: + // break; + // case WalletSyncStatus.syncing: + // break; + // } + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }, + ); + + _nodeStatusSubscription = + eventBus.on().listen( + (event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case NodeConnectionStatus.disconnected: + // break; + // case NodeConnectionStatus.connected: + // break; + // } + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }, + ); + + super.initState(); + } + + @override + void dispose() { + _nodeStatusSubscription.cancel(); + _syncStatusSubscription.cancel(); + super.dispose(); + } + + DateTime? _cachedTime; + + Future _onWillPop() async { + final now = DateTime.now(); + const timeout = Duration(milliseconds: 1500); + if (_cachedTime == null || now.difference(_cachedTime!) > timeout) { + _cachedTime = now; + unawaited(showDialog( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async { + Navigator.of(context).popUntil( + ModalRoute.withName(HomeView.routeName), + ); + _logout(); + return false; + }, + child: const StackDialog(title: "Tap back again to exit wallet"), + ), + ).timeout( + timeout, + onTimeout: () => Navigator.of(context).popUntil( + ModalRoute.withName(TokenView.routeName), + ), + )); + } + return false; + } + + void _logout() async { + if (_shouldDisableAutoSyncOnLogOut) { + // disable auto sync if it was enabled only when loading wallet + ref.read(managerProvider).shouldAutoSync = false; + } + ref.read(managerProvider.notifier).isActiveWallet = false; + ref.read(transactionFilterProvider.state).state = null; + if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && + ref.read(prefsChangeNotifierProvider).backupFrequencyType == + BackupFrequencyType.afterClosingAWallet) { + unawaited(ref.read(autoSWBServiceProvider).doBackup()); + } + } + + Widget _buildNetworkIcon(WalletSyncStatus status) { + switch (status) { + case WalletSyncStatus.unableToSync: + return SvgPicture.asset( + Assets.svg.radioProblem, + color: Theme.of(context).extension()!.accentColorRed, + width: 20, + height: 20, + ); + case WalletSyncStatus.synced: + return SvgPicture.asset( + Assets.svg.radio, + color: Theme.of(context).extension()!.accentColorGreen, + width: 20, + height: 20, + ); + case WalletSyncStatus.syncing: + return SvgPicture.asset( + Assets.svg.radioSyncing, + color: Theme.of(context).extension()!.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( + context: context, + builder: (_) => const StackOkDialog( + title: "Exchange not available for Epic Cash", + ), + ); + } else if (coin.name.endsWith("TestNet")) { + await showDialog( + 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; + 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, + ), + ), + ); + } + } + } + + Future 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( + context: context, + builder: (_) => StackOkDialog( + title: "Anonymize all failed", + message: "Reason: $e", + ), + ); + } + } + } + + 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"); + + final coin = ref.watch(managerProvider.select((value) => value.coin)); + + return WillPopScope( + onWillPop: _onWillPop, + child: Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + _logout(); + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + // color: Theme.of(context).extension()!.accentColorDark + width: 24, + height: 24, + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Text( + ref.watch( + managerProvider.select((value) => value.walletName)), + 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()!.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()!.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()! + .topNavIconPrimary, + ), + onPressed: () { + // reset unread state + ref.refresh(unreadNotificationsStateProvider); + + Navigator.of(context) + .pushNamed( + NotificationsView.routeName, + arguments: walletId, + ) + .then((_) { + final Set unreadNotificationIds = ref + .read(unreadNotificationsStateProvider.state) + .state; + if (unreadNotificationIds.isEmpty) return; + + List> 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()!.background, + icon: SvgPicture.asset( + Assets.svg.bars, + color: Theme.of(context) + .extension()! + .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( + color: Theme.of(context).extension()!.background, + child: Column( + children: [ + const SizedBox( + height: 10, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: WalletSummary( + walletId: walletId, + managerProvider: managerProvider, + initialSyncStatus: ref.watch(managerProvider + .select((value) => value.isRefreshing)) + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + ), + ), + 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()! + .getSecondaryEnabledButtonColor(context), + onPressed: () async { + await showDialog( + 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()! + .accentColorDark, + ), + ), + ), + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(); + + unawaited(attemptAnonymize()); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonColor( + context), + child: Text( + "Continue", + style: STextStyles.button(context), + ), + ), + ), + ); + }, + child: Text( + "Anonymize funds", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + ), + ), + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + BlueTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: walletId, + ); + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Expanded( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Padding( + padding: const EdgeInsets.only(bottom: 14), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottom: Radius.circular( + // TokenView.navBarHeight / 2.0, + Constants.size.circularBorderRadius, + ), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TransactionsList( + managerProvider: managerProvider, + walletId: walletId, + ), + ), + ], + ), + ), + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(), + 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; + + final walletName = ref + .read(managerProvider) + .walletName; + + List tokens = + await getWalletTokens( + walletAddress); + + await Navigator.of(context).pushNamed( + MyTokensView.routeName, + arguments: Tuple3(walletAddress, + tokens, walletName), + ); + }, + ), + ), + ), + ], + ), + ], + ) + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 12869d6a9..b70570b05 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -38,6 +38,7 @@ 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'; @@ -777,10 +778,23 @@ class _WalletViewState extends ConsumerState { ); }, onBuyPressed: () {}, - onTokensPressed: () { - Navigator.of(context).pushNamed( + onTokensPressed: () async { + final walletAddress = await ref + .read(managerProvider) + .currentReceivingAddress; + + final walletName = ref + .read(managerProvider) + .walletName; + + List tokens = + await getWalletTokens( + walletAddress); + + await Navigator.of(context).pushNamed( MyTokensView.routeName, - arguments: walletId, + arguments: Tuple3(walletAddress, + tokens, walletName), ); }, ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 3d6eed831..0f076eade 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -79,6 +79,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; @@ -1294,11 +1295,27 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case MyTokensView.routeName: - if (args is String) { + if (args is Tuple3, String>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => MyTokensView( - walletId: args, + walletAddress: args.item1, + tokens: args.item2, + walletName: args.item3), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case TokenView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => TokenView( + walletAddress: args.item1, + contractAddress: args.item2, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/coins/erc_20/erc_wallet.dart b/lib/services/coins/erc_20/erc_wallet.dart new file mode 100644 index 000000000..e36e2c46b --- /dev/null +++ b/lib/services/coins/erc_20/erc_wallet.dart @@ -0,0 +1,1007 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +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: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/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/constants.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:string_to_hex/string_to_hex.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; + +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'; + +const int MINIMUM_CONFIRMATIONS = 5; + +//THis is used for mapping transactions per address from the block explorer +class AddressTransaction { + final String message; + final List result; + final String status; + + const AddressTransaction({ + required this.message, + required this.result, + required this.status, + }); + + factory AddressTransaction.fromJson(Map json) { + return AddressTransaction( + message: json['message'] as String, + result: json['result'] as List, + status: json['status'] as String, + ); + } +} + +class GasTracker { + final int code; + final Map data; + + const GasTracker({ + required this.code, + required this.data, + }); + + factory GasTracker.fromJson(Map json) { + return GasTracker( + code: json['code'] as int, + data: json['data'] as Map, + ); + } +} + +class TokenWallet extends CoinServiceAPI { + NodeModel? _ethNode; + final _gasLimit = 21000; + final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; + final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; + + @override + set isFavorite(bool markFavorite) {} + + @override + bool get isFavorite { + throw UnimplementedError(); + } + + @override + Coin get coin => Coin.ethereum; + + late SecureStorageInterface _secureStore; + late final TransactionNotificationTracker txTracker; + late PriceAPI _priceAPI; + final _prefs = Prefs.instance; + bool longMutex = false; + + //TODO - move shared logic to eth_commons + 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; + + TokenWallet({ + required String walletId, + required String walletName, + required Coin coin, + PriceAPI? priceAPI, + required SecureStorageInterface secureStore, + required TransactionNotificationTracker tracker, + }) { + txTracker = tracker; + _walletId = walletId; + _walletName = walletName; + // _coin = coin; + + _priceAPI = priceAPI ?? PriceAPI(Client()); + _secureStore = secureStore; + } + + bool _shouldAutoSync = false; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } else { + startNetworkAlivePinging(); + refresh(); + } + } + } + + @override + String get walletName => _walletName; + late String _walletName; + + Timer? timer; + Timer? _networkAliveTimer; + + @override + Future> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future>? _allOwnAddresses; + + Future> _fetchAllOwnAddresses() async { + List addresses = []; + final ownAddress = _credentials.address; + addresses.add(ownAddress.toString()); + return addresses; + } + + @override + Future get availableBalance async { + Web3Client client = await getEthClient(); + EtherAmount ethBalance = await client.getBalance(_credentials.address); + return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); + } + + @override + Future get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(); + + @override + Future confirmSend({required Map txData}) async { + Web3Client client = await getEthClient(); + final int chainId = await client.getNetworkId(); + final amount = txData['recipientAmt']; + final decimalAmount = + Format.satoshisToAmount(amount as int, coin: Coin.ethereum); + final bigIntAmount = amountToBigInt(decimalAmount.toDouble()); + + final tx = Transaction.Transaction( + to: EthereumAddress.fromHex(txData['address'] as String), + gasPrice: + EtherAmount.fromUnitAndValue(EtherUnit.wei, txData['feeInWei']), + maxGas: _gasLimit, + value: EtherAmount.inWei(bigIntAmount)); + final transaction = + await client.sendTransaction(_credentials, tx, chainId: chainId); + + 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 get currentReceivingAddress async { + final _currentReceivingAddress = _credentials.address; + final checkSumAddress = + checksumEthereumAddress(_currentReceivingAddress.toString()); + return checkSumAddress; + } + + @override + Future 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); + } + + @override + Future exit() async { + _hasCalledExit = true; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } + + @override + Future get fees => _feeObject ??= _getFees(); + Future? _feeObject; + + Future _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 getGasOracle() async { + final response = await get(Uri.parse(_gasTrackerUrl)); + + if (response.statusCode == 200) { + return GasTracker.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception('Failed to load gas oracle'); + } + } + + //Full rescan is not needed for ETH since we have a balance + @override + Future fullRescan( + int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { + // TODO: implement fullRescan + throw UnimplementedError(); + } + + @override + Future generateNewAddress() { + // TODO: implement generateNewAddress - might not be needed for ETH + throw UnimplementedError(); + } + + bool _hasCalledExit = false; + + @override + bool get hasCalledExit => _hasCalledExit; + + @override + Future initializeExisting() async { + //First get mnemonic so we can initialize credentials + final mnemonicString = + await _secureStore.read(key: '${_walletId}_mnemonic'); + + _credentials = + EthPrivateKey.fromHex(StringToHex.toHexString(mnemonicString)); + + Logging.instance.log("Opening existing ${coin.prettyName} wallet.", + level: LogLevel.Info); + + if ((DB.instance.get(boxName: walletId, key: "id")) == 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 { + await _prefs.init(); + final String mnemonic = bip39.generateMnemonic(strength: 256); + _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); + + //Store credentials in secure store + await _secureStore.write( + key: '${_walletId}_credentials', value: _credentials.toString()); + + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance.put( + boxName: walletId, key: 'receivingAddresses', value: ["0"]); + 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); + } + + bool _isConnected = false; + + @override + bool get isConnected => _isConnected; + + @override + bool get isRefreshing => refreshMutex; + + bool refreshMutex = false; + + @override + Future get maxFee async { + final fee = (await fees).fast; + final feeEstimate = await estimateFeeFor(0, fee); + return feeEstimate; + } + + @override + Future> get mnemonic => _getMnemonicList(); + + Future get chainHeight async { + Web3Client client = await getEthClient(); + try { + final result = await client.getBlockNumber(); + + return result; + } catch (e, s) { + Logging.instance.log("Exception caught in chainHeight: $e\n$s", + level: LogLevel.Error); + return -1; + } + } + + 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(); + + @override + Future> prepareSend( + {required String address, + required int satoshiAmount, + Map? 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 txData = { + "fee": feeEstimate, + "feeInWei": fee, + "address": address, + "recipientAmt": satoshiAmount, + }; + + return txData; + } + + @override + Future recoverFromMnemonic( + {required String mnemonic, + 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) { + longMutex = false; + throw Exception("Attempted to overwrite mnemonic on restore!"); + } + + await _secureStore.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); + + _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put(boxName: walletId, key: "isFavorite", value: false); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + longMutex = false; + rethrow; + } + + longMutex = false; + final end = DateTime.now(); + Logging.instance.log( + "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", + level: LogLevel.Info); + } + + Future refreshIfThereIsNewData() async { + Web3Client client = await getEthClient(); + if (longMutex) return false; + if (_hasCalledExit) return false; + final currentChainHeight = await chainHeight; + + try { + bool needsRefresh = false; + Set txnsToCheck = {}; + + for (final String txid in txTracker.pendings) { + if (!txTracker.wasNotifiedConfirmed(txid)) { + txnsToCheck.add(txid); + } + } + + for (String txid in txnsToCheck) { + final txn = await client.getTransactionByHash(txid); + final int txBlockNumber = txn.blockNumber.blockNum; + + final int txConfirmations = currentChainHeight - txBlockNumber; + bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS; + if (!isUnconfirmed) { + needsRefresh = true; + break; + } + } + if (!needsRefresh) { + var allOwnAddresses = await _fetchAllOwnAddresses(); + AddressTransaction addressTransactions = + await fetchAddressTransactions(allOwnAddresses.elementAt(0)); + final txData = await transactionData; + if (addressTransactions.message == "OK") { + final allTxs = addressTransactions.result; + allTxs.forEach((element) { + if (txData.findTransaction(element["hash"] as String) == null) { + Logging.instance.log( + " txid not found in address history already ${element["hash"]}", + level: LogLevel.Info); + needsRefresh = true; + } + }); + } + } + return needsRefresh; + } catch (e, s) { + Logging.instance.log( + "Exception caught in refreshIfThereIsNewData: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future getAllTxsToWatch( + TransactionData txData, + ) async { + if (_hasCalledExit) return; + List unconfirmedTxnsToNotifyPending = []; + List unconfirmedTxnsToNotifyConfirmed = []; + + for (final chunk in txData.txChunks) { + for (final tx in chunk.transactions) { + if (tx.confirmedStatus) { + // get all transactions that were notified as pending but not as confirmed + if (txTracker.wasNotifiedPending(tx.txid) && + !txTracker.wasNotifiedConfirmed(tx.txid)) { + unconfirmedTxnsToNotifyConfirmed.add(tx); + } + } else { + // get all transactions that were not notified as pending yet + if (!txTracker.wasNotifiedPending(tx.txid)) { + unconfirmedTxnsToNotifyPending.add(tx); + } + } + } + } + + // notify on unconfirmed transactions + for (final tx in unconfirmedTxnsToNotifyPending) { + if (tx.txType == "Received") { + 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, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } else if (tx.txType == "Sent") { + 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, + coinName: coin.name, + txid: tx.txid, + confirmations: tx.confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } + } + + // notify on confirmed + for (final tx in unconfirmedTxnsToNotifyConfirmed) { + if (tx.txType == "Received") { + unawaited(NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } else if (tx.txType == "Sent") { + unawaited(NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } + } + } + + @override + Future refresh() async { + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + try { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); + + final currentHeight = await chainHeight; + const storedHeight = 1; //await storedChainHeight; + + Logging.instance + .log("chain height: $currentHeight", level: LogLevel.Info); + Logging.instance + .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(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.50, walletId)); + + final feeObj = _getFees(); + 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); + await Future.wait([ + newTxData, + feeObj, + allTxsToWatch, + ]); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.90, walletId)); + } + refreshMutex = false; + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + + if (shouldAutoSync) { + timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { + Logging.instance.log( + "Periodic refresh check for $walletId $walletName in object instance: $hashCode", + level: LogLevel.Info); + if (await refreshIfThereIsNewData()) { + await refresh(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + } + }); + } + } catch (error, strace) { + refreshMutex = false; + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + coin, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + Logging.instance.log( + "Caught exception in refreshWalletData(): $error\n$strace", + level: LogLevel.Warning); + } + } + + @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(); + try { + final result = await client.isListeningForNetwork(); + return result; + } catch (_) { + return false; + } + } + + void _periodicPingCheck() async { + bool hasNetwork = await testNetworkConnection(); + _isConnected = hasNetwork; + if (_isConnected != hasNetwork) { + NodeConnectionStatus status = hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + GlobalEventBus.instance + .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); + } + } + + @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) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + + if (shouldRefresh) { + unawaited(refresh()); + } + } + + @override + Future updateSentCachedTxData(Map txData) async { + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + 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) { + return isValidEthereumAddress(address); + } + + Future fetchAddressTransactions(String address) async { + final response = await get(Uri.parse( + "${_blockExplorer}module=account&action=txlist&address=$address")); + + if (response.statusCode == 200) { + return AddressTransaction.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception('Failed to load transactions'); + } + } + + Future _fetchTransactionData() async { + String thisAddress = await currentReceivingAddress; + final cachedTransactions = + 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); + + if (txs.message == "OK") { + final allTxs = txs.result; + allTxs.forEach((element) { + Map midSortedTx = {}; + // create final tx map + midSortedTx["txid"] = element["hash"]; + int confirmations = int.parse(element['confirmations'].toString()); + + int transactionAmount = int.parse(element['value'].toString()); + 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"]; + + if (checksumEthereumAddress(element["from"].toString()) == + thisAddress) { + midSortedTx["txType"] = "Sent"; + } else { + midSortedTx["txType"] = "Received"; + } + + midSortedTx["amount"] = satAmount; + final String worthNow = ((currentPrice * Decimal.fromInt(satAmount)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + + //Calculate fees (GasLimit * gasPrice) + int txFee = int.parse(element['gasPrice'].toString()) * + int.parse(element['gasUsed'].toString()); + final txFeeDecimal = txFee / (pow(10, decimal)); + + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + midSortedTx["aliens"] = []; + midSortedTx["fees"] = Format.decimalAmountToSatoshis( + Decimal.parse(txFeeDecimal.toString()), coin); + midSortedTx["address"] = element["to"]; + midSortedTx["inputSize"] = 1; + midSortedTx["outputSize"] = 1; + midSortedTx["inputs"] = []; + midSortedTx["outputs"] = []; + midSortedTx["height"] = int.parse(element['blockNumber'].toString()); + + midSortedArray.add(midSortedTx); + }); + } + + midSortedArray.sort((a, b) => + (int.parse(b['timestamp'].toString())) - + (int.parse(a['timestamp'].toString()))); + + // buildDateTimeChunks + final Map result = {"dateTimeChunks": []}; + final dateArray = []; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = + extractDateFromTimestamp(int.parse(txObject['timestamp'].toString())); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp( + int.parse(chunk['timestamp'].toString())) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = >[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + final transactionsMap = 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 + String get walletId => _walletId; + late String _walletId; + + @override + set walletName(String newName) => _walletName = newName; + + void stopNetworkAlivePinging() { + _networkAliveTimer?.cancel(); + _networkAliveTimer = null; + } + + void startNetworkAlivePinging() { + // call once on start right away + _periodicPingCheck(); + + // then periodically check + _networkAliveTimer = Timer.periodic( + Constants.networkAliveTimerDuration, + (_) async { + _periodicPingCheck(); + }, + ); + } +} diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 8c9f42455..0bf1a6373 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -14,6 +14,7 @@ import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/constants.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'; @@ -41,8 +42,6 @@ import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; const int MINIMUM_CONFIRMATIONS = 5; -const String GENESIS_HASH_MAINNET = - "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"; //THis is used for mapping transactions per address from the block explorer class AddressTransaction { @@ -438,8 +437,6 @@ class EthereumWallet extends CoinServiceAPI { } final feeEstimate = await estimateFeeFor(satoshiAmount, fee); - print("FEE ESTIMATE IS $feeEstimate"); - print("AMOUNT TO SEND IS $satoshiAmount"); bool isSendAll = false; final balance = diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart new file mode 100644 index 000000000..a8286993b --- /dev/null +++ b/lib/utilities/eth_commons.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + +class AccountModule { + final String message; + final List result; + final String status; + + const AccountModule({ + required this.message, + required this.result, + required this.status, + }); + + factory AccountModule.fromJson(Map json) { + return AccountModule( + message: json['message'] as String, + result: json['result'] as List, + status: json['status'] as String, + ); + } +} + +const _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; + +Future fetchAccountModule(String action, String address) async { + final response = await get(Uri.parse( + "${_blockExplorer}module=account&action=$action&address=$address")); + if (response.statusCode == 200) { + return AccountModule.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception('Failed to load transactions'); + } +} + +Future> getWalletTokens(String address) async { + AccountModule tokens = await fetchAccountModule("tokenlist", address); + //THIS IS ONLY HARD CODED UNTIL API WORKS AGAIN - TODO REMOVE HARDCODED + return [ + { + "balance": "369039500000000000", + "contractAddress": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "decimals": "18", + "name": "Uniswap", + "symbol": "UNI", + "type": "ERC-20" + } + ]; + + if (tokens.message == "OK") { + return tokens.result as List; + } + return []; +} From 4efd432de672cb8bc4e1d81605878521ffd83ed0 Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 25 Jan 2023 11:29:20 +0200 Subject: [PATCH 026/208] WIP: Add wallet tokens --- lib/pages/token_view/my_tokens_view.dart | 20 +- .../sub_widgets/my_token_select_item.dart | 40 +- .../sub_widgets/my_tokens_list.dart | 7 + lib/pages/token_view/token_view.dart | 1168 ++++++++--------- lib/pages/wallet_view/wallet_view.dart | 13 +- lib/providers/global/tokens_provider.dart | 23 + .../global/tokens_service_provider.dart | 20 + lib/route_generator.dart | 33 +- lib/services/coins/erc_20/erc_wallet.dart | 1007 -------------- .../coins/ethereum/ethereum_wallet.dart | 80 +- lib/services/tokens.dart | 374 ++++++ .../tokens/ethereum/ethereum_token.dart | 127 ++ lib/services/tokens/token_manager.dart | 130 ++ lib/services/tokens/token_service.dart | 73 ++ lib/services/tokens_service.dart | 432 ++++++ 15 files changed, 1874 insertions(+), 1673 deletions(-) create mode 100644 lib/providers/global/tokens_provider.dart create mode 100644 lib/providers/global/tokens_service_provider.dart delete mode 100644 lib/services/coins/erc_20/erc_wallet.dart create mode 100644 lib/services/tokens.dart create mode 100644 lib/services/tokens/ethereum/ethereum_token.dart create mode 100644 lib/services/tokens/token_manager.dart create mode 100644 lib/services/tokens/token_service.dart create mode 100644 lib/services/tokens_service.dart diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index f5a7f8bcd..f6e20d6fe 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -26,18 +26,22 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/services/coins/manager.dart'; + class MyTokensView extends ConsumerStatefulWidget { const MyTokensView({ Key? key, + required this.managerProvider, + required this.walletId, required this.walletAddress, required this.tokens, - required this.walletName, }) : super(key: key); static const String routeName = "/myTokens"; + final ChangeNotifierProvider managerProvider; + final String walletId; final String walletAddress; final List tokens; - final String walletName; @override ConsumerState createState() => _TokenDetailsViewState(); @@ -45,12 +49,13 @@ class MyTokensView extends ConsumerStatefulWidget { class _TokenDetailsViewState extends ConsumerState { late final String walletAddress; + // late final String walletName; late final TextEditingController _searchController; final searchFieldFocusNode = FocusNode(); @override void initState() { - walletAddress = widget.walletAddress; + // walletAddress = widget.walletAddress; _searchController = TextEditingController(); super.initState(); @@ -101,7 +106,7 @@ class _TokenDetailsViewState extends ConsumerState { width: 12, ), Text( - "${widget.walletName} Tokens", + "${ref.read(widget.managerProvider).walletName} Tokens", style: STextStyles.desktopH3(context), ), ], @@ -123,7 +128,7 @@ class _TokenDetailsViewState extends ConsumerState { }, ), title: Text( - "${widget.walletName} Tokens", + "${ref.read(widget.managerProvider).walletName} Tokens", style: STextStyles.navBarTitle(context), ), actions: [ @@ -261,7 +266,10 @@ class _TokenDetailsViewState extends ConsumerState { ), Expanded( child: MyTokensList( - tokens: widget.tokens, walletAddress: walletAddress), + managerProvider: widget.managerProvider, + walletId: widget.walletId, + tokens: widget.tokens, + walletAddress: widget.walletAddress), ), ], ), diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 8d62aaeef..c7a6bf5c9 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/providers/global/tokens_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -13,14 +14,22 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; + +import 'package:stackwallet/services/coins/manager.dart'; + class MyTokenSelectItem extends ConsumerWidget { const MyTokenSelectItem( {Key? key, + required this.managerProvider, + required this.walletId, required this.walletAddress, required this.tokenData, required}) : super(key: key); + final ChangeNotifierProvider managerProvider; + final String walletId; final String walletAddress; final Map tokenData; @@ -42,9 +51,38 @@ class MyTokenSelectItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () { + // ref + // .read(walletsChangeNotifierProvider) + // .getManagerProvider(walletId) + + // final walletId = ref + // .read(managerProvider) + // .walletName; + // final manager = ref + // .read(walletsChangeNotifierProvider) + // .getManagerProvider(walletId) + + // arguments: Tuple2(walletId, managerProvider, walletAddress, + // tokenData["contractAddress"]) + + // arguments: Tuple2( + // walletId, + // ref + // .read(tokensChangeNotifierProvider) + // .getManagerProvider(walletId) + + // arguments: Tuple2( + // walletId, + // ref + // .read(tokensChangeNotifierProvider) + // .getManagerProvider(walletId) + Navigator.of(context).pushNamed( TokenView.routeName, - arguments: Tuple2(walletAddress, tokenData["contractAddress"]), + arguments: Tuple2( + walletId, + ref.read(tokensChangeNotifierProvider).getManagerProvider( + tokenData["contractAddress"] as String)), ); }, diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart index f9c5b31b9..a23614cc7 100644 --- a/lib/pages/token_view/sub_widgets/my_tokens_list.dart +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -1,14 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart'; +import 'package:stackwallet/services/coins/manager.dart'; class MyTokensList extends StatelessWidget { const MyTokensList({ Key? key, + required this.managerProvider, + required this.walletId, required this.tokens, required this.walletAddress, }) : super(key: key); + final ChangeNotifierProvider managerProvider; + final String walletId; final List tokens; final String walletAddress; @@ -23,6 +28,8 @@ class MyTokensList extends StatelessWidget { return Padding( padding: const EdgeInsets.all(4), child: MyTokenSelectItem( + managerProvider: managerProvider, + walletId: walletId, walletAddress: walletAddress, tokenData: tokens[index] as Map, ), diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 046a4fb4a..ee0d3e13d 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -49,35 +49,37 @@ import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; +import '../../services/tokens/token_manager.dart'; + /// [eventBus] should only be set during testing class TokenView extends ConsumerStatefulWidget { const TokenView({ Key? key, - required this.walletId, required this.contractAddress, required this.managerProvider, - this.eventBus, - required this.walletAddress, + + // required this.walletAddress, + // required this.contractAddress, }) : super(key: key); static const String routeName = "/token"; static const double navBarHeight = 65.0; - final String walletId; + final ChangeNotifierProvider managerProvider; final String contractAddress; - final String walletAddress; - final ChangeNotifierProvider managerProvider; + // final String contractAddress; + // final String walletAddress; - final EventBus? eventBus; + // final EventBus? eventBus; @override ConsumerState createState() => _TokenViewState(); } class _TokenViewState extends ConsumerState { - late final EventBus eventBus; - late final String walletId; - late final ChangeNotifierProvider managerProvider; + // late final EventBus eventBus; + late final String contractAddress; + late final ChangeNotifierProvider managerProvider; late final bool _shouldDisableAutoSyncOnLogOut; @@ -91,10 +93,9 @@ class _TokenViewState extends ConsumerState { @override void initState() { - walletId = widget.walletId; + contractAddress = widget.contractAddress; managerProvider = widget.managerProvider; - ref.read(managerProvider).isActiveWallet = true; if (!ref.read(managerProvider).shouldAutoSync) { // enable auto sync if it wasn't enabled when loading wallet ref.read(managerProvider).shouldAutoSync = true; @@ -118,44 +119,44 @@ class _TokenViewState extends ConsumerState { } } - eventBus = - widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; + // eventBus = + // widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - // switch (event.newStatus) { - // case WalletSyncStatus.unableToSync: - // break; - // case WalletSyncStatus.synced: - // break; - // case WalletSyncStatus.syncing: - // break; - // } - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); + // _syncStatusSubscription = + // eventBus.on().listen( + // (event) async { + // if (event.walletId == widget.walletId) { + // // switch (event.newStatus) { + // // case WalletSyncStatus.unableToSync: + // // break; + // // case WalletSyncStatus.synced: + // // break; + // // case WalletSyncStatus.syncing: + // // break; + // // } + // setState(() { + // _currentSyncStatus = event.newStatus; + // }); + // } + // }, + // ); - _nodeStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - // switch (event.newStatus) { - // case NodeConnectionStatus.disconnected: - // break; - // case NodeConnectionStatus.connected: - // break; - // } - setState(() { - _currentNodeStatus = event.newStatus; - }); - } - }, - ); + // _nodeStatusSubscription = + // eventBus.on().listen( + // (event) async { + // if (event.walletId == widget.walletId) { + // // switch (event.newStatus) { + // // case NodeConnectionStatus.disconnected: + // // break; + // // case NodeConnectionStatus.connected: + // // break; + // // } + // setState(() { + // _currentNodeStatus = event.newStatus; + // }); + // } + // }, + // ); super.initState(); } @@ -182,7 +183,6 @@ class _TokenViewState extends ConsumerState { Navigator.of(context).popUntil( ModalRoute.withName(HomeView.routeName), ); - _logout(); return false; }, child: const StackDialog(title: "Tap back again to exit wallet"), @@ -197,19 +197,19 @@ class _TokenViewState extends ConsumerState { return false; } - void _logout() async { - if (_shouldDisableAutoSyncOnLogOut) { - // disable auto sync if it was enabled only when loading wallet - ref.read(managerProvider).shouldAutoSync = false; - } - ref.read(managerProvider.notifier).isActiveWallet = false; - ref.read(transactionFilterProvider.state).state = null; - if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && - ref.read(prefsChangeNotifierProvider).backupFrequencyType == - BackupFrequencyType.afterClosingAWallet) { - unawaited(ref.read(autoSWBServiceProvider).doBackup()); - } - } + // void _logout() async { + // if (_shouldDisableAutoSyncOnLogOut) { + // // disable auto sync if it was enabled only when loading wallet + // ref.read(managerProvider).shouldAutoSync = false; + // } + // ref.read(managerProvider.notifier).isActiveWallet = false; + // ref.read(transactionFilterProvider.state).state = null; + // if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && + // ref.read(prefsChangeNotifierProvider).backupFrequencyType == + // BackupFrequencyType.afterClosingAWallet) { + // unawaited(ref.read(autoSWBServiceProvider).doBackup()); + // } + // } Widget _buildNetworkIcon(WalletSyncStatus status) { switch (status) { @@ -237,141 +237,115 @@ class _TokenViewState extends ConsumerState { } } - void _onExchangePressed(BuildContext context) async { - unawaited(_cnLoadingService.loadAll(ref)); + // void _onExchangePressed(BuildContext context) async { + // unawaited(_cnLoadingService.loadAll(ref)); + // + // final coin = ref.read(managerProvider).coin; + // + // ref.read(currentExchangeNameStateProvider.state).state = + // ChangeNowExchange.exchangeName; + // // final contractAddress = ref.read(managerProvider).contractAddress; + // final contractAddress = "ref.read(managerProvider).contractAddress"; + // 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(), + // ), + // ); + // } + // + // + // } - final coin = ref.read(managerProvider).coin; - - if (coin == Coin.epicCash) { - await showDialog( - context: context, - builder: (_) => const StackOkDialog( - title: "Exchange not available for Epic Cash", - ), - ); - } else if (coin.name.endsWith("TestNet")) { - await showDialog( - 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; - 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, - ), - ), - ); - } - } - } - - Future 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( - context: context, - builder: (_) => StackOkDialog( - title: "Anonymize all failed", - message: "Reason: $e", - ), - ); - } - } - } + // Future 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( + // context: context, + // builder: (_) => StackOkDialog( + // title: "Anonymize all failed", + // message: "Reason: $e", + // ), + // ); + // } + // } + // } void _loadCNData() { // unawaited future if (ref.read(prefsChangeNotifierProvider).externalCalls) { - _cnLoadingService.loadAll(ref, coin: ref.read(managerProvider).coin); + _cnLoadingService.loadAll(ref, coin: Coin.ethereum); } else { Logging.instance.log("User does not want to use external calls", level: LogLevel.Info); @@ -381,443 +355,381 @@ class _TokenViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + return const Scaffold(); - final coin = ref.watch(managerProvider.select((value) => value.coin)); + // final coin = ref.watch(managerProvider.select((value) => value.Co)); - return WillPopScope( - onWillPop: _onWillPop, - child: Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - _logout(); - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - // color: Theme.of(context).extension()!.accentColorDark - width: 24, - height: 24, - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Text( - ref.watch( - managerProvider.select((value) => value.walletName)), - 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()!.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()!.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()! - .topNavIconPrimary, - ), - onPressed: () { - // reset unread state - ref.refresh(unreadNotificationsStateProvider); - - Navigator.of(context) - .pushNamed( - NotificationsView.routeName, - arguments: walletId, - ) - .then((_) { - final Set unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - List> 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()!.background, - icon: SvgPicture.asset( - Assets.svg.bars, - color: Theme.of(context) - .extension()! - .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( - color: Theme.of(context).extension()!.background, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: WalletSummary( - walletId: walletId, - managerProvider: managerProvider, - initialSyncStatus: ref.watch(managerProvider - .select((value) => value.isRefreshing)) - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), - ), - ), - 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()! - .getSecondaryEnabledButtonColor(context), - onPressed: () async { - await showDialog( - 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()! - .accentColorDark, - ), - ), - ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(); - - unawaited(attemptAnonymize()); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor( - context), - child: Text( - "Continue", - style: STextStyles.button(context), - ), - ), - ), - ); - }, - child: Text( - "Anonymize funds", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - ), - ), - ), - ], - ), - ), - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transactions", - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - ), - BlueTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: walletId, - ); - }, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - Expanded( - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Padding( - padding: const EdgeInsets.only(bottom: 14), - child: ClipRRect( - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - bottom: Radius.circular( - // TokenView.navBarHeight / 2.0, - Constants.size.circularBorderRadius, - ), - ), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Expanded( - child: TransactionsList( - managerProvider: managerProvider, - walletId: walletId, - ), - ), - ], - ), - ), - ), - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Spacer(), - 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; - - final walletName = ref - .read(managerProvider) - .walletName; - - List tokens = - await getWalletTokens( - walletAddress); - - await Navigator.of(context).pushNamed( - MyTokensView.routeName, - arguments: Tuple3(walletAddress, - tokens, walletName), - ); - }, - ), - ), - ), - ], - ), - ], - ) - ], - ), - ), - ], - ), - ), - ), - ), - ), - ); + // return WillPopScope( + // onWillPop: _onWillPop, + // child: Background( + // child: Scaffold( + // backgroundColor: + // Theme.of(context).extension()!.background, + // appBar: AppBar( + // leading: AppBarBackButton( + // onPressed: () { + // Navigator.of(context).pop(); + // }, + // ), + // titleSpacing: 0, + // title: Row( + // children: [ + // SvgPicture.asset( + // Assets.svg.iconFor(coin: Coin.ethereum), + // // color: Theme.of(context).extension()!.accentColorDark + // width: 24, + // height: 24, + // ), + // const SizedBox( + // width: 16, + // ), + // Expanded( + // child: Text( + // ref.watch( + // managerProvider.select((value) => "value.walletName")), + // 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()!.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()!.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()! + // // // .topNavIconPrimary, + // // // ), + // // onPressed: () { + // // // reset unread state + // // ref.refresh(unreadNotificationsStateProvider); + // // + // // Navigator.of(context) + // // .pushNamed( + // // NotificationsView.routeName, + // // arguments: walletId, + // // ) + // // .then((_) { + // // final Set unreadNotificationIds = ref + // // .read(unreadNotificationsStateProvider.state) + // // .state; + // // if (unreadNotificationIds.isEmpty) return; + // // + // // List> 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()!.background, + // icon: SvgPicture.asset( + // Assets.svg.bars, + // color: Theme.of(context) + // .extension()! + // .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( + // color: Theme.of(context).extension()!.background, + // child: Column( + // children: [ + // const SizedBox( + // height: 10, + // ), + // Center( + // child: Padding( + // padding: const EdgeInsets.symmetric(horizontal: 16), + // // child: WalletSummary( + // // walletId: walletId, + // // managerProvider: managerProvider, + // // initialSyncStatus: ref.watch(managerProvider + // // .select((value) => value.isRefreshing)) + // // ? WalletSyncStatus.syncing + // // : WalletSyncStatus.synced, + // // ), + // ), + // ), + // // if (coin == Coin.firo) + // // const SizedBox( + // // height: 10, + // // ), + // const SizedBox( + // height: 20, + // ), + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 16), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "Transactions", + // style: STextStyles.itemSubtitle(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .textDark3, + // ), + // ), + // BlueTextButton( + // text: "See all", + // // onTap: () { + // // Navigator.of(context).pushNamed( + // // AllTransactionsView.routeName, + // // arguments: walletId, + // // ); + // // }, + // ), + // ], + // ), + // ), + // const SizedBox( + // height: 12, + // ), + // Expanded( + // child: Stack( + // children: [ + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 16), + // child: Padding( + // padding: const EdgeInsets.only(bottom: 14), + // child: ClipRRect( + // borderRadius: BorderRadius.vertical( + // top: Radius.circular( + // Constants.size.circularBorderRadius, + // ), + // bottom: Radius.circular( + // // TokenView.navBarHeight / 2.0, + // Constants.size.circularBorderRadius, + // ), + // ), + // child: Container( + // decoration: BoxDecoration( + // color: Colors.transparent, + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // ), + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment.stretch, + // children: [ + // // Expanded( + // // child: TransactionsList( + // // managerProvider: managerProvider, + // // walletId: walletId, + // // ), + // // ), + // ], + // ), + // ), + // ), + // ), + // ), + // Column( + // mainAxisAlignment: MainAxisAlignment.end, + // children: [ + // const Spacer(), + // 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; + // + // final walletName = ref + // .read(managerProvider) + // .walletName; + // + // List tokens = + // await getWalletTokens( + // walletAddress); + // + // await Navigator.of(context).pushNamed( + // MyTokensView.routeName, + // arguments: Tuple3(walletAddress, + // tokens, walletName), + // ); + // }, + // ), + // ), + // ), + // ], + // ), + // ], + // ) + // ], + // ), + // ), + // ], + // ), + // ), + // ), + // ), + // ), + // ); } } diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index b70570b05..eef0fa755 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -783,18 +783,17 @@ class _WalletViewState extends ConsumerState { .read(managerProvider) .currentReceivingAddress; - final walletName = ref - .read(managerProvider) - .walletName; + // String walletTokens = await List tokens = - await getWalletTokens( - walletAddress); + await getWalletTokens(await ref + .read(managerProvider) + .currentReceivingAddress); await Navigator.of(context).pushNamed( MyTokensView.routeName, - arguments: Tuple3(walletAddress, - tokens, walletName), + arguments: Tuple4(managerProvider, + walletId, walletAddress, tokens), ); }, ), diff --git a/lib/providers/global/tokens_provider.dart b/lib/providers/global/tokens_provider.dart new file mode 100644 index 000000000..388c45971 --- /dev/null +++ b/lib/providers/global/tokens_provider.dart @@ -0,0 +1,23 @@ +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((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; +}); diff --git a/lib/providers/global/tokens_service_provider.dart b/lib/providers/global/tokens_service_provider.dart new file mode 100644 index 000000000..053bc035e --- /dev/null +++ b/lib/providers/global/tokens_service_provider.dart @@ -0,0 +1,20 @@ +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((ref) { + if (kDebugMode) { + _count++; + debugPrint( + "tokensServiceChangeNotifierProvider instantiation count: $_count"); + } + + return TokensService( + secureStorageInterface: ref.read(secureStoreProvider), + ); +}); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 0f076eade..e86dda8e3 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -117,6 +117,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncin 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/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'; @@ -1295,13 +1296,15 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case MyTokensView.routeName: - if (args is Tuple3, String>) { + if (args is Tuple4, String, String, + List>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => MyTokensView( - walletAddress: args.item1, - tokens: args.item2, - walletName: args.item3), + managerProvider: args.item1, + walletId: args.item2, + walletAddress: args.item3, + tokens: args.item4), settings: RouteSettings( name: settings.name, ), @@ -1309,13 +1312,29 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + // case WalletView.routeName: + // if (args is Tuple2>) { + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletView( + // walletId: args.item1, + // managerProvider: args.item2, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + // } + case TokenView.routeName: - if (args is Tuple2) { + if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => TokenView( - walletAddress: args.item1, - contractAddress: args.item2, + contractAddress: args.item1, + managerProvider: args.item2, + // walletAddress: args.item3, + // contractAddress: args.item4, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/coins/erc_20/erc_wallet.dart b/lib/services/coins/erc_20/erc_wallet.dart deleted file mode 100644 index e36e2c46b..000000000 --- a/lib/services/coins/erc_20/erc_wallet.dart +++ /dev/null @@ -1,1007 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; -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: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/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/constants.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:string_to_hex/string_to_hex.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; - -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'; - -const int MINIMUM_CONFIRMATIONS = 5; - -//THis is used for mapping transactions per address from the block explorer -class AddressTransaction { - final String message; - final List result; - final String status; - - const AddressTransaction({ - required this.message, - required this.result, - required this.status, - }); - - factory AddressTransaction.fromJson(Map json) { - return AddressTransaction( - message: json['message'] as String, - result: json['result'] as List, - status: json['status'] as String, - ); - } -} - -class GasTracker { - final int code; - final Map data; - - const GasTracker({ - required this.code, - required this.data, - }); - - factory GasTracker.fromJson(Map json) { - return GasTracker( - code: json['code'] as int, - data: json['data'] as Map, - ); - } -} - -class TokenWallet extends CoinServiceAPI { - NodeModel? _ethNode; - final _gasLimit = 21000; - final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; - final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; - - @override - set isFavorite(bool markFavorite) {} - - @override - bool get isFavorite { - throw UnimplementedError(); - } - - @override - Coin get coin => Coin.ethereum; - - late SecureStorageInterface _secureStore; - late final TransactionNotificationTracker txTracker; - late PriceAPI _priceAPI; - final _prefs = Prefs.instance; - bool longMutex = false; - - //TODO - move shared logic to eth_commons - 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; - - TokenWallet({ - required String walletId, - required String walletName, - required Coin coin, - PriceAPI? priceAPI, - required SecureStorageInterface secureStore, - required TransactionNotificationTracker tracker, - }) { - txTracker = tracker; - _walletId = walletId; - _walletName = walletName; - // _coin = coin; - - _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = secureStore; - } - - bool _shouldAutoSync = false; - - @override - bool get shouldAutoSync => _shouldAutoSync; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - refresh(); - } - } - } - - @override - String get walletName => _walletName; - late String _walletName; - - Timer? timer; - Timer? _networkAliveTimer; - - @override - Future> get allOwnAddresses => - _allOwnAddresses ??= _fetchAllOwnAddresses(); - Future>? _allOwnAddresses; - - Future> _fetchAllOwnAddresses() async { - List addresses = []; - final ownAddress = _credentials.address; - addresses.add(ownAddress.toString()); - return addresses; - } - - @override - Future get availableBalance async { - Web3Client client = await getEthClient(); - EtherAmount ethBalance = await client.getBalance(_credentials.address); - return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); - } - - @override - Future get balanceMinusMaxFee async => - (await availableBalance) - - (Decimal.fromInt((await maxFee)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(); - - @override - Future confirmSend({required Map txData}) async { - Web3Client client = await getEthClient(); - final int chainId = await client.getNetworkId(); - final amount = txData['recipientAmt']; - final decimalAmount = - Format.satoshisToAmount(amount as int, coin: Coin.ethereum); - final bigIntAmount = amountToBigInt(decimalAmount.toDouble()); - - final tx = Transaction.Transaction( - to: EthereumAddress.fromHex(txData['address'] as String), - gasPrice: - EtherAmount.fromUnitAndValue(EtherUnit.wei, txData['feeInWei']), - maxGas: _gasLimit, - value: EtherAmount.inWei(bigIntAmount)); - final transaction = - await client.sendTransaction(_credentials, tx, chainId: chainId); - - 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 get currentReceivingAddress async { - final _currentReceivingAddress = _credentials.address; - final checkSumAddress = - checksumEthereumAddress(_currentReceivingAddress.toString()); - return checkSumAddress; - } - - @override - Future 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); - } - - @override - Future exit() async { - _hasCalledExit = true; - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } - - @override - Future get fees => _feeObject ??= _getFees(); - Future? _feeObject; - - Future _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 getGasOracle() async { - final response = await get(Uri.parse(_gasTrackerUrl)); - - if (response.statusCode == 200) { - return GasTracker.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception('Failed to load gas oracle'); - } - } - - //Full rescan is not needed for ETH since we have a balance - @override - Future fullRescan( - int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { - // TODO: implement fullRescan - throw UnimplementedError(); - } - - @override - Future generateNewAddress() { - // TODO: implement generateNewAddress - might not be needed for ETH - throw UnimplementedError(); - } - - bool _hasCalledExit = false; - - @override - bool get hasCalledExit => _hasCalledExit; - - @override - Future initializeExisting() async { - //First get mnemonic so we can initialize credentials - final mnemonicString = - await _secureStore.read(key: '${_walletId}_mnemonic'); - - _credentials = - EthPrivateKey.fromHex(StringToHex.toHexString(mnemonicString)); - - Logging.instance.log("Opening existing ${coin.prettyName} wallet.", - level: LogLevel.Info); - - if ((DB.instance.get(boxName: walletId, key: "id")) == 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 { - await _prefs.init(); - final String mnemonic = bip39.generateMnemonic(strength: 256); - _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); - await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); - - //Store credentials in secure store - await _secureStore.write( - key: '${_walletId}_credentials', value: _credentials.toString()); - - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - await DB.instance.put( - boxName: walletId, key: 'receivingAddresses', value: ["0"]); - 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); - } - - bool _isConnected = false; - - @override - bool get isConnected => _isConnected; - - @override - bool get isRefreshing => refreshMutex; - - bool refreshMutex = false; - - @override - Future get maxFee async { - final fee = (await fees).fast; - final feeEstimate = await estimateFeeFor(0, fee); - return feeEstimate; - } - - @override - Future> get mnemonic => _getMnemonicList(); - - Future get chainHeight async { - Web3Client client = await getEthClient(); - try { - final result = await client.getBlockNumber(); - - return result; - } catch (e, s) { - Logging.instance.log("Exception caught in chainHeight: $e\n$s", - level: LogLevel.Error); - return -1; - } - } - - 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(); - - @override - Future> prepareSend( - {required String address, - required int satoshiAmount, - Map? 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 txData = { - "fee": feeEstimate, - "feeInWei": fee, - "address": address, - "recipientAmt": satoshiAmount, - }; - - return txData; - } - - @override - Future recoverFromMnemonic( - {required String mnemonic, - 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) { - longMutex = false; - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - - await _secureStore.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - - _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); - - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - await DB.instance - .put(boxName: walletId, key: "isFavorite", value: false); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from recoverFromMnemonic(): $e\n$s", - level: LogLevel.Error); - longMutex = false; - rethrow; - } - - longMutex = false; - final end = DateTime.now(); - Logging.instance.log( - "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", - level: LogLevel.Info); - } - - Future refreshIfThereIsNewData() async { - Web3Client client = await getEthClient(); - if (longMutex) return false; - if (_hasCalledExit) return false; - final currentChainHeight = await chainHeight; - - try { - bool needsRefresh = false; - Set txnsToCheck = {}; - - for (final String txid in txTracker.pendings) { - if (!txTracker.wasNotifiedConfirmed(txid)) { - txnsToCheck.add(txid); - } - } - - for (String txid in txnsToCheck) { - final txn = await client.getTransactionByHash(txid); - final int txBlockNumber = txn.blockNumber.blockNum; - - final int txConfirmations = currentChainHeight - txBlockNumber; - bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS; - if (!isUnconfirmed) { - needsRefresh = true; - break; - } - } - if (!needsRefresh) { - var allOwnAddresses = await _fetchAllOwnAddresses(); - AddressTransaction addressTransactions = - await fetchAddressTransactions(allOwnAddresses.elementAt(0)); - final txData = await transactionData; - if (addressTransactions.message == "OK") { - final allTxs = addressTransactions.result; - allTxs.forEach((element) { - if (txData.findTransaction(element["hash"] as String) == null) { - Logging.instance.log( - " txid not found in address history already ${element["hash"]}", - level: LogLevel.Info); - needsRefresh = true; - } - }); - } - } - return needsRefresh; - } catch (e, s) { - Logging.instance.log( - "Exception caught in refreshIfThereIsNewData: $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future getAllTxsToWatch( - TransactionData txData, - ) async { - if (_hasCalledExit) return; - List unconfirmedTxnsToNotifyPending = []; - List unconfirmedTxnsToNotifyConfirmed = []; - - for (final chunk in txData.txChunks) { - for (final tx in chunk.transactions) { - if (tx.confirmedStatus) { - // get all transactions that were notified as pending but not as confirmed - if (txTracker.wasNotifiedPending(tx.txid) && - !txTracker.wasNotifiedConfirmed(tx.txid)) { - unconfirmedTxnsToNotifyConfirmed.add(tx); - } - } else { - // get all transactions that were not notified as pending yet - if (!txTracker.wasNotifiedPending(tx.txid)) { - unconfirmedTxnsToNotifyPending.add(tx); - } - } - } - } - - // notify on unconfirmed transactions - for (final tx in unconfirmedTxnsToNotifyPending) { - if (tx.txType == "Received") { - 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, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, - )); - await txTracker.addNotifiedPending(tx.txid); - } else if (tx.txType == "Sent") { - 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, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, - )); - await txTracker.addNotifiedPending(tx.txid); - } - } - - // notify on confirmed - for (final tx in unconfirmedTxnsToNotifyConfirmed) { - if (tx.txType == "Received") { - unawaited(NotificationApi.showNotification( - title: "Incoming transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: false, - coinName: coin.name, - )); - await txTracker.addNotifiedConfirmed(tx.txid); - } else if (tx.txType == "Sent") { - unawaited(NotificationApi.showNotification( - title: "Outgoing transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: false, - coinName: coin.name, - )); - await txTracker.addNotifiedConfirmed(tx.txid); - } - } - } - - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log("$walletId $walletName refreshMutex denied", - level: LogLevel.Info); - return; - } else { - refreshMutex = true; - } - - try { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); - - final currentHeight = await chainHeight; - const storedHeight = 1; //await storedChainHeight; - - Logging.instance - .log("chain height: $currentHeight", level: LogLevel.Info); - Logging.instance - .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(); - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(0.50, walletId)); - - final feeObj = _getFees(); - 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); - await Future.wait([ - newTxData, - feeObj, - allTxsToWatch, - ]); - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(0.90, walletId)); - } - refreshMutex = false; - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - - if (shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - Logging.instance.log( - "Periodic refresh check for $walletId $walletName in object instance: $hashCode", - level: LogLevel.Info); - if (await refreshIfThereIsNewData()) { - await refresh(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - } - }); - } - } catch (error, strace) { - refreshMutex = false; - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - NodeConnectionStatus.disconnected, - walletId, - coin, - ), - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - Logging.instance.log( - "Caught exception in refreshWalletData(): $error\n$strace", - level: LogLevel.Warning); - } - } - - @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(); - try { - final result = await client.isListeningForNetwork(); - return result; - } catch (_) { - return false; - } - } - - void _periodicPingCheck() async { - bool hasNetwork = await testNetworkConnection(); - _isConnected = hasNetwork; - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - GlobalEventBus.instance - .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); - } - } - - @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) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - - if (shouldRefresh) { - unawaited(refresh()); - } - } - - @override - Future updateSentCachedTxData(Map txData) async { - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - 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) { - return isValidEthereumAddress(address); - } - - Future fetchAddressTransactions(String address) async { - final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=txlist&address=$address")); - - if (response.statusCode == 200) { - return AddressTransaction.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception('Failed to load transactions'); - } - } - - Future _fetchTransactionData() async { - String thisAddress = await currentReceivingAddress; - final cachedTransactions = - 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); - - if (txs.message == "OK") { - final allTxs = txs.result; - allTxs.forEach((element) { - Map midSortedTx = {}; - // create final tx map - midSortedTx["txid"] = element["hash"]; - int confirmations = int.parse(element['confirmations'].toString()); - - int transactionAmount = int.parse(element['value'].toString()); - 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"]; - - if (checksumEthereumAddress(element["from"].toString()) == - thisAddress) { - midSortedTx["txType"] = "Sent"; - } else { - midSortedTx["txType"] = "Received"; - } - - midSortedTx["amount"] = satAmount; - final String worthNow = ((currentPrice * Decimal.fromInt(satAmount)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(scaleOnInfinitePrecision: 2) - .toStringAsFixed(2); - - //Calculate fees (GasLimit * gasPrice) - int txFee = int.parse(element['gasPrice'].toString()) * - int.parse(element['gasUsed'].toString()); - final txFeeDecimal = txFee / (pow(10, decimal)); - - midSortedTx["worthNow"] = worthNow; - midSortedTx["worthAtBlockTimestamp"] = worthNow; - midSortedTx["aliens"] = []; - midSortedTx["fees"] = Format.decimalAmountToSatoshis( - Decimal.parse(txFeeDecimal.toString()), coin); - midSortedTx["address"] = element["to"]; - midSortedTx["inputSize"] = 1; - midSortedTx["outputSize"] = 1; - midSortedTx["inputs"] = []; - midSortedTx["outputs"] = []; - midSortedTx["height"] = int.parse(element['blockNumber'].toString()); - - midSortedArray.add(midSortedTx); - }); - } - - midSortedArray.sort((a, b) => - (int.parse(b['timestamp'].toString())) - - (int.parse(a['timestamp'].toString()))); - - // buildDateTimeChunks - final Map result = {"dateTimeChunks": []}; - final dateArray = []; - - for (int i = 0; i < midSortedArray.length; i++) { - final txObject = midSortedArray[i]; - final date = - extractDateFromTimestamp(int.parse(txObject['timestamp'].toString())); - final txTimeArray = [txObject["timestamp"], date]; - - if (dateArray.contains(txTimeArray[1])) { - result["dateTimeChunks"].forEach((dynamic chunk) { - if (extractDateFromTimestamp( - int.parse(chunk['timestamp'].toString())) == - txTimeArray[1]) { - if (chunk["transactions"] == null) { - chunk["transactions"] = >[]; - } - chunk["transactions"].add(txObject); - } - }); - } else { - dateArray.add(txTimeArray[1]); - final chunk = { - "timestamp": txTimeArray[0], - "transactions": [txObject], - }; - result["dateTimeChunks"].add(chunk); - } - } - - final transactionsMap = 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 - String get walletId => _walletId; - late String _walletId; - - @override - set walletName(String newName) => _walletName = newName; - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - void startNetworkAlivePinging() { - // call once on start right away - _periodicPingCheck(); - - // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); - } -} diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 0bf1a6373..ce75fea22 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -84,9 +84,19 @@ class GasTracker { class EthereumWallet extends CoinServiceAPI { NodeModel? _ethNode; final _gasLimit = 21000; - final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; + // final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; + final _blockExplorer = "https://api.etherscan.io/api?"; final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; + @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( @@ -140,7 +150,6 @@ class EthereumWallet extends CoinServiceAPI { _walletId = walletId; _walletName = walletName; _coin = coin; - _priceAPI = priceAPI ?? PriceAPI(Client()); _secureStore = secureStore; } @@ -167,11 +176,6 @@ class EthereumWallet extends CoinServiceAPI { @override String get walletName => _walletName; - late String _walletName; - - late Coin _coin; - Timer? timer; - Timer? _networkAliveTimer; @override Future> get allOwnAddresses => @@ -339,7 +343,9 @@ class EthereumWallet extends CoinServiceAPI { await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance.put( - boxName: walletId, key: 'receivingAddresses', value: ["0"]); + boxName: walletId, + key: 'receivingAddresses', + value: [_credentials.address.toString()]); await DB.instance .put(boxName: walletId, key: "receivingIndex", value: 0); await DB.instance @@ -480,6 +486,46 @@ class EthereumWallet extends CoinServiceAPI { _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + 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"); + var tokenMap = {}; + List> tokensList = []; + if (tokenTransactions.message == "OK") { + final allTxs = tokenTransactions.result; + print("RESULT IS $allTxs"); + allTxs.forEach((element) { + String key = element["tokenSymbol"] as String; + tokenMap[key] = {}; + tokenMap[key]["balance"] = 0; + + if (tokenMap.containsKey(key)) { + tokenMap[key]["contractAddress"] = element["contractAddress"]; + tokenMap[key]["decimals"] = element["tokenDecimal"]; + tokenMap[key]["name"] = element["tokenName"]; + tokenMap[key]["symbol"] = element["tokenSymbol"]; + if (element["to"] == _credentials.address.toString()) { + tokenMap[key]["balance"] += int.parse(element["value"] as String); + } else { + tokenMap[key]["balance"] -= int.parse(element["value"] as String); + } + } + }); + + tokenMap.forEach((key, value) { + //Create New token + + tokensList.add(value as Map); + }); + + await _secureStore.write( + key: '${_walletId}_tokens', value: tokensList.toString()); + } + + print("THIS WALLET TOKENS IS $tokenMap"); + print("ALL TOKENS LIST IS $tokensList"); + await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance @@ -528,8 +574,8 @@ class EthereumWallet extends CoinServiceAPI { } if (!needsRefresh) { var allOwnAddresses = await _fetchAllOwnAddresses(); - AddressTransaction addressTransactions = - await fetchAddressTransactions(allOwnAddresses.elementAt(0)); + AddressTransaction addressTransactions = await fetchAddressTransactions( + allOwnAddresses.elementAt(0), "txlist"); final txData = await transactionData; if (addressTransactions.message == "OK") { final allTxs = addressTransactions.result; @@ -854,9 +900,10 @@ class EthereumWallet extends CoinServiceAPI { return isValidEthereumAddress(address); } - Future fetchAddressTransactions(String address) async { + Future fetchAddressTransactions( + String address, String action) async { final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=txlist&address=$address")); + "${_blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AddressTransaction.fromJson( @@ -881,7 +928,8 @@ class EthereumWallet extends CoinServiceAPI { Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; final List> midSortedArray = []; - AddressTransaction txs = await fetchAddressTransactions(thisAddress); + AddressTransaction txs = + await fetchAddressTransactions(thisAddress, "txlist"); if (txs.message == "OK") { final allTxs = txs.result; @@ -991,13 +1039,11 @@ class EthereumWallet extends CoinServiceAPI { return txModel; } - @override - String get walletId => _walletId; - late String _walletId; - @override set walletName(String newName) => _walletName = newName; + // Future + void stopNetworkAlivePinging() { _networkAliveTimer?.cancel(); _networkAliveTimer = null; diff --git a/lib/services/tokens.dart b/lib/services/tokens.dart new file mode 100644 index 000000000..d7997d9ed --- /dev/null +++ b/lib/services/tokens.dart @@ -0,0 +1,374 @@ +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> _nonFavorites = +// ListenableList(); +// ListenableList> get nonFavorites => +// _nonFavorites; +// +// final ListenableList> _favorites = +// ListenableList(); +// ListenableList> 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> + _managerProviderMap = ListenableMap(); + static final ListenableMap _managerMap = + ListenableMap(); + + // bool get hasWallets => _managerProviderMap.isNotEmpty; + + List> get managerProviders => + _managerProviderMap.values.toList(growable: false); + List get managers => _managerMap.values.toList(growable: false); + + // List getWalletIdsFor({required Coin coin}) { + // final List result = []; + // for (final manager in _managerMap.values) { + // if (manager.coin == coin) { + // result.add(manager.walletId); + // } + // } + // return result; + // } + + // Map>> getManagerProvidersByCoin() { + // print("DOES THIS GET HERE?????"); + // Map>> result = {}; + // for (final manager in _managerMap.values) { + // if (result[manager.coin] == null) { + // result[manager.coin] = []; + // } + // result[manager.coin]!.add(_managerProviderMap[manager.walletId] + // as ChangeNotifierProvider); + // } + // return result; + // } + + // List> getManagerProvidersForCoin(Coin coin) { + // List> result = []; + // for (final manager in _managerMap.values) { + // if (manager.coin == coin) { + // result.add(_managerProviderMap[manager.walletId] + // as ChangeNotifierProvider); + // } + // } + // return result; + // } + + ChangeNotifierProvider getManagerProvider( + String contractAddress) { + print("WALLET ID HERE IS ${_managerProviderMap.length}"); + return _managerProviderMap[contractAddress] + as ChangeNotifierProvider; + } + + 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((_) => 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 _initLinearly( + List> 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 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(boxName: DB.boxNameWalletsToDeleteOnStart)) { + // await DB.instance.deleteBoxFromDisk(boxName: walletId); + // } + // // clear list + // await DB.instance + // .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); + // + // final map = await walletsService.walletNames; + + // List> walletInitFutures = []; + // List> walletsToInitLinearly = []; + + // final favIdList = await walletsService.getFavoriteWalletIds(); + + // List 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); + // _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 loadAfterStackRestore( + // Prefs prefs, List managers) async { + // List> walletInitFutures = []; + // List> walletsToInitLinearly = []; + // + // final favIdList = await walletsService.getFavoriteWalletIds(); + // + // List 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); + // _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(); + // } + // } +} diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/tokens/ethereum/ethereum_token.dart new file mode 100644 index 000000000..ef3bfc17f --- /dev/null +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -0,0 +1,127 @@ +import 'package:decimal/decimal.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/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; + +class EthereumToken extends TokenServiceAPI { + @override + late bool shouldAutoSync; + late String _walletId; + late String _contractAddress; + late SecureStorageInterface _secureStore; + late final TransactionNotificationTracker txTracker; + + EthereumToken({ + required String contractAddress, + required String walletId, + required SecureStorageInterface secureStore, + required TransactionNotificationTracker tracker, + }) { + txTracker = tracker; + _walletId = walletId; + _contractAddress = contractAddress; + _secureStore = secureStore; + } + + @override + // TODO: implement allOwnAddresses + Future> get allOwnAddresses => throw UnimplementedError(); + + @override + // TODO: implement availableBalance + Future get availableBalance => throw UnimplementedError(); + + @override + // TODO: implement balanceMinusMaxFee + Future get balanceMinusMaxFee => throw UnimplementedError(); + + @override + // TODO: implement coin + Coin get coin => throw UnimplementedError(); + + @override + Future confirmSend({required Map txData}) { + // TODO: implement confirmSend + throw UnimplementedError(); + } + + @override + // TODO: implement currentReceivingAddress + Future get currentReceivingAddress => throw UnimplementedError(); + + @override + Future estimateFeeFor(int satoshiAmount, int feeRate) { + // TODO: implement estimateFeeFor + throw UnimplementedError(); + } + + @override + // TODO: implement fees + Future get fees => throw UnimplementedError(); + + @override + Future initializeExisting() { + // TODO: implement initializeExisting + throw UnimplementedError(); + } + + @override + Future initializeNew() async { + throw UnimplementedError(); + } + + @override + // TODO: implement isConnected + bool get isConnected => throw UnimplementedError(); + + @override + // TODO: implement isRefreshing + bool get isRefreshing => throw UnimplementedError(); + + @override + // TODO: implement maxFee + Future get maxFee => throw UnimplementedError(); + + @override + // TODO: implement pendingBalance + Future get pendingBalance => throw UnimplementedError(); + + @override + Future> prepareSend( + {required String address, + required int satoshiAmount, + Map? args}) { + // TODO: implement prepareSend + throw UnimplementedError(); + } + + @override + Future refresh() { + // TODO: implement refresh + throw UnimplementedError(); + } + + @override + // TODO: implement totalBalance + Future get totalBalance => throw UnimplementedError(); + + @override + // TODO: implement transactionData + Future get transactionData => throw UnimplementedError(); + + @override + Future updateSentCachedTxData(Map txData) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } + + @override + bool validateAddress(String address) { + // TODO: implement validateAddress + throw UnimplementedError(); + } +} diff --git a/lib/services/tokens/token_manager.dart b/lib/services/tokens/token_manager.dart new file mode 100644 index 000000000..cceee7e47 --- /dev/null +++ b/lib/services/tokens/token_manager.dart @@ -0,0 +1,130 @@ +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? _backgroundRefreshListener; + + /// optional eventbus parameter for testing only + TokenManager(this._currentToken, [EventBus? globalEventBusForTesting]) { + final bus = globalEventBusForTesting ?? GlobalEventBus.instance; + _backgroundRefreshListener = bus.on().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> prepareSend({ + required String address, + required int satoshiAmount, + Map? 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 confirmSend({required Map 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 get fees => _currentToken.fees; + Future get maxFee => _currentToken.maxFee; + + Future get currentReceivingAddress => + _currentToken.currentReceivingAddress; + // Future get currentLegacyReceivingAddress => + // _currentWallet.currentLegacyReceivingAddress; + + Future get availableBalance async { + _cachedAvailableBalance = await _currentToken.availableBalance; + return _cachedAvailableBalance; + } + + Decimal _cachedAvailableBalance = Decimal.zero; + Decimal get cachedAvailableBalance => _cachedAvailableBalance; + + Future get pendingBalance => _currentToken.pendingBalance; + Future get balanceMinusMaxFee => _currentToken.balanceMinusMaxFee; + + Future get totalBalance async { + _cachedTotalBalance = await _currentToken.totalBalance; + return _cachedTotalBalance; + } + + Decimal _cachedTotalBalance = Decimal.zero; + Decimal get cachedTotalBalance => _cachedTotalBalance; + + Future> get allOwnAddresses => _currentToken.allOwnAddresses; + + Future get transactionData => _currentToken.transactionData; + + Future refresh() async { + await _currentToken.refresh(); + notifyListeners(); + } + + bool validateAddress(String address) => + _currentToken.validateAddress(address); + + Future initializeNew() => _currentToken.initializeNew(); + Future initializeExisting() => _currentToken.initializeExisting(); + + Future isOwnAddress(String address) async { + final allOwnAddresses = await this.allOwnAddresses; + return allOwnAddresses.contains(address); + } + + bool get isConnected => _currentToken.isConnected; + + Future estimateFeeFor(int satoshiAmount, int feeRate) async { + return _currentToken.estimateFeeFor(satoshiAmount, feeRate); + } +} diff --git a/lib/services/tokens/token_service.dart b/lib/services/tokens/token_service.dart new file mode 100644 index 000000000..0f23054f5 --- /dev/null +++ b/lib/services/tokens/token_service.dart @@ -0,0 +1,73 @@ +import 'package:decimal/decimal.dart'; +import 'package:stackwallet/models/models.dart'; +import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +abstract class TokenServiceAPI { + TokenServiceAPI(); + + factory TokenServiceAPI.from( + String contractAddress, + String walletId, + SecureStorageInterface secureStorageInterface, + TransactionNotificationTracker tracker, + Prefs prefs, + ) { + return EthereumToken( + contractAddress: contractAddress, + walletId: walletId, + secureStore: secureStorageInterface, + tracker: tracker, + ); + } + + Coin get coin; + bool get isRefreshing; + bool get shouldAutoSync; + set shouldAutoSync(bool shouldAutoSync); + + Future> prepareSend({ + required String address, + required int satoshiAmount, + Map? args, + }); + + Future confirmSend({required Map txData}); + + Future get fees; + Future get maxFee; + + Future get currentReceivingAddress; + // Future get currentLegacyReceivingAddress; + + Future get availableBalance; + Future get pendingBalance; + Future get totalBalance; + Future get balanceMinusMaxFee; + + Future> get allOwnAddresses; + + Future get transactionData; + + Future refresh(); + + // String get walletName; + // String get walletId; + + bool validateAddress(String address); + + Future initializeNew(); + Future initializeExisting(); + + // void Function(bool isActive)? onIsActiveWalletChanged; + + bool get isConnected; + + Future estimateFeeFor(int satoshiAmount, int feeRate); + + // used for electrumx coins + Future updateSentCachedTxData(Map txData); +} diff --git a/lib/services/tokens_service.dart b/lib/services/tokens_service.dart new file mode 100644 index 000000000..3c21fe3ea --- /dev/null +++ b/lib/services/tokens_service.dart @@ -0,0 +1,432 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_libmonero/monero/monero.dart'; +import 'package:flutter_libmonero/wownero/wownero.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; +import 'package:stackwallet/services/notifications_service.dart'; +import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:uuid/uuid.dart'; + +class TokenInfo { + final Coin coin; + final String walletId; + final String contractAddress; + + const TokenInfo( + {required this.coin, + required this.walletId, + required this.contractAddress}); + + factory TokenInfo.fromJson(Map jsonObject) { + return TokenInfo( + coin: Coin.values.byName(jsonObject["coin"] as String), + walletId: jsonObject["id"] as String, + contractAddress: jsonObject["contractAddress"] as String, + ); + } + + Map toMap() { + return { + "contractAddress": contractAddress, + "walletId": walletId, + "coin": coin.name, + }; + } + + String toJsonString() { + return jsonEncode(toMap()); + } + + @override + String toString() { + return "TokenInfo: ${toJsonString()}"; + } +} + +class TokensService extends ChangeNotifier { + late final SecureStorageInterface _secureStore; + + // Future>? _walletNames; + // Future> get walletNames => + // _walletNames ??= _fetchWalletNames(); + + TokensService({ + required SecureStorageInterface secureStorageInterface, + }) { + _secureStore = secureStorageInterface; + } + + // Future getWalletCryptoCurrency({required String walletName}) async { + // final id = await getWalletId(walletName); + // final currency = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, key: "${id}_cryptoCurrency"); + // return Coin.values.byName(currency as String); + // } + + // Future renameWallet({ + // required String from, + // required String to, + // required bool shouldNotifyListeners, + // }) async { + // if (from == to) { + // return true; + // } + // + // final walletInfo = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map; + // + // final info = walletInfo.values.firstWhere( + // (element) => element['name'] == from, + // orElse: () => {}) as Map; + // + // if (info.isEmpty) { + // // tried to rename a non existing wallet + // Logging.instance + // .log("Tried to rename a non existing wallet!", level: LogLevel.Error); + // return false; + // } + // + // if (from != to && + // (walletInfo.values.firstWhere((element) => element['name'] == to, + // orElse: () => {}) as Map) + // .isNotEmpty) { + // // name already exists + // Logging.instance.log("wallet with name \"$to\" already exists!", + // level: LogLevel.Error); + // return false; + // } + // + // info["name"] = to; + // walletInfo[info['id']] = info; + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: walletInfo); + // await refreshWallets(shouldNotifyListeners); + // return true; + // } + + // Future> _fetchWalletNames() async { + // final names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // if (names == null) { + // Logging.instance.log( + // "Fetched wallet 'names' returned null. Setting initializing 'names'", + // level: LogLevel.Info); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: 'names', + // value: {}); + // return {}; + // } + // Logging.instance.log("Fetched wallet names: $names", level: LogLevel.Info); + // final mapped = Map.from(names); + // mapped.removeWhere((name, dyn) { + // final jsonObject = Map.from(dyn as Map); + // try { + // Coin.values.byName(jsonObject["coin"] as String); + // return false; + // } catch (e, s) { + // Logging.instance.log("Error, ${jsonObject["coin"]} does not exist", + // level: LogLevel.Error); + // return true; + // } + // }); + // + // return mapped.map((name, dyn) => MapEntry( + // name, WalletInfo.fromJson(Map.from(dyn as Map)))); + // } + + // Future addExistingStackWallet({ + // required String name, + // required String walletId, + // required Coin coin, + // required bool shouldNotifyListeners, + // }) async { + // final _names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // + // Map names; + // if (_names == null) { + // names = {}; + // } else { + // names = Map.from(_names); + // } + // + // if (names.keys.contains(walletId)) { + // throw Exception("Wallet with walletId \"$walletId\" already exists!"); + // } + // if (names.values.where((element) => element['name'] == name).isNotEmpty) { + // throw Exception("Wallet with name \"$name\" already exists!"); + // } + // + // names[walletId] = { + // "id": walletId, + // "coin": coin.name, + // "name": name, + // }; + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: names); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_cryptoCurrency", + // value: coin.name); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified", + // value: false); + // await DB.instance.addWalletBox(walletId: walletId); + // await refreshWallets(shouldNotifyListeners); + // } + + // /// returns the new walletId if successful, otherwise null + // Future addNewWallet({ + // required String name, + // required Coin coin, + // required bool shouldNotifyListeners, + // }) async { + // final _names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // + // Map names; + // if (_names == null) { + // names = {}; + // } else { + // names = Map.from(_names); + // } + // + // // Prevent overwriting or storing empty names + // if (name.isEmpty || + // names.values.where((element) => element['name'] == name).isNotEmpty) { + // return null; + // } + // + // final id = const Uuid().v1(); + // names[id] = { + // "id": id, + // "coin": coin.name, + // "name": name, + // }; + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: names); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${id}_cryptoCurrency", + // value: coin.name); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${id}_mnemonicHasBeenVerified", + // value: false); + // await DB.instance.addWalletBox(walletId: id); + // await refreshWallets(shouldNotifyListeners); + // return id; + // } + + // Future> getFavoriteWalletIds() async { + // return DB.instance + // .values(boxName: DB.boxNameFavoriteWallets) + // .toList(); + // } + + // Future saveFavoriteWalletIds(List walletIds) async { + // await DB.instance.deleteAll(boxName: DB.boxNameFavoriteWallets); + // await DB.instance + // .addAll(boxName: DB.boxNameFavoriteWallets, values: walletIds); + // debugPrint("saveFavoriteWalletIds list: $walletIds"); + // } + // + // Future addFavorite(String walletId) async { + // final list = await getFavoriteWalletIds(); + // if (!list.contains(walletId)) { + // list.add(walletId); + // } + // await saveFavoriteWalletIds(list); + // } + // + // Future removeFavorite(String walletId) async { + // final list = await getFavoriteWalletIds(); + // list.remove(walletId); + // await saveFavoriteWalletIds(list); + // } + // + // Future moveFavorite({ + // required int fromIndex, + // required int toIndex, + // }) async { + // final list = await getFavoriteWalletIds(); + // if (fromIndex < toIndex) { + // toIndex -= 1; + // } + // final walletId = list.removeAt(fromIndex); + // list.insert(toIndex, walletId); + // await saveFavoriteWalletIds(list); + // } + // + // Future checkForDuplicate(String name) async { + // final names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // if (names == null) return false; + // + // return names.values.where((element) => element['name'] == name).isNotEmpty; + // } + + Future getWalletId(String walletName) async { + final names = DB.instance + .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map; + final shells = + names.values.where((element) => element['name'] == walletName); + if (shells.isEmpty) { + return null; + } + return shells.first["id"] as String; + } + + // Future isMnemonicVerified({required String walletId}) async { + // final isVerified = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified") as bool?; + // + // if (isVerified == null) { + // Logging.instance.log( + // "isMnemonicVerified(walletId: $walletId) returned null which should never happen!", + // level: LogLevel.Error, + // ); + // throw Exception( + // "isMnemonicVerified(walletId: $walletId) returned null which should never happen!"); + // } else { + // return isVerified; + // } + // } + // + // Future setMnemonicVerified({required String walletId}) async { + // final isVerified = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified") as bool?; + // + // if (isVerified == null) { + // Logging.instance.log( + // "setMnemonicVerified(walletId: $walletId) tried running on non existent wallet!", + // level: LogLevel.Error, + // ); + // throw Exception( + // "setMnemonicVerified(walletId: $walletId) tried running on non existent wallet!"); + // } else if (isVerified) { + // Logging.instance.log( + // "setMnemonicVerified(walletId: $walletId) tried running on already verified wallet!", + // level: LogLevel.Error, + // ); + // throw Exception( + // "setMnemonicVerified(walletId: $walletId) tried running on already verified wallet!"); + // } else { + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified", + // value: true); + // Logging.instance.log( + // "setMnemonicVerified(walletId: $walletId) successful", + // level: LogLevel.Error, + // ); + // } + // } + // + // // pin + mnemonic as well as anything else in secureStore + // Future deleteWallet(String name, bool shouldNotifyListeners) async { + // final names = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, key: 'names') as Map? ?? + // {}; + // + // final walletId = await getWalletId(name); + // if (walletId == null) { + // return 3; + // } + // + // Logging.instance.log( + // "deleteWallet called with name=$name and id=$walletId", + // level: LogLevel.Warning, + // ); + // + // final shell = names.remove(walletId); + // + // if (shell == null) { + // return 0; + // } + // + // // TODO delete derivations!!! + // await _secureStore.delete(key: "${walletId}_pin"); + // await _secureStore.delete(key: "${walletId}_mnemonic"); + // + // await DB.instance.delete( + // boxName: DB.boxNameAllWalletsData, key: "${walletId}_cryptoCurrency"); + // await DB.instance.delete( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified"); + // if (coinFromPrettyName(shell['coin'] as String) == Coin.wownero) { + // final wowService = + // wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + // await wowService.remove(walletId); + // Logging.instance + // .log("monero wallet: $walletId deleted", level: LogLevel.Info); + // } else if (coinFromPrettyName(shell['coin'] as String) == Coin.monero) { + // final xmrService = + // monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); + // await xmrService.remove(walletId); + // Logging.instance + // .log("monero wallet: $walletId deleted", level: LogLevel.Info); + // } else if (coinFromPrettyName(shell['coin'] as String) == Coin.epicCash) { + // final deleteResult = + // await deleteEpicWallet(walletId: walletId, secureStore: _secureStore); + // Logging.instance.log( + // "epic wallet: $walletId deleted with result: $deleteResult", + // level: LogLevel.Info); + // } + // + // // box data may currently still be read/written to if wallet was refreshing + // // when delete was requested so instead of deleting now we mark the wallet + // // as needs delete by adding it's id to a list which gets checked on app start + // await DB.instance.add( + // boxName: DB.boxNameWalletsToDeleteOnStart, value: walletId); + // + // final lookupService = TradeSentFromStackService(); + // for (final lookup in lookupService.all) { + // if (lookup.walletIds.contains(walletId)) { + // // update lookup data to reflect deleted wallet + // await lookupService.save( + // tradeWalletLookup: lookup.copyWith( + // walletIds: lookup.walletIds.where((id) => id != walletId).toList(), + // ), + // ); + // } + // } + // + // // delete notifications tied to deleted wallet + // for (final notification in NotificationsService.instance.notifications) { + // if (notification.walletId == walletId) { + // await NotificationsService.instance.delete(notification, false); + // } + // } + // + // if (names.isEmpty) { + // await DB.instance.deleteAll(boxName: DB.boxNameAllWalletsData); + // _walletNames = Future(() => {}); + // notifyListeners(); + // return 2; // error code no wallets on device + // } + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: names); + // await refreshWallets(shouldNotifyListeners); + // return 0; + // } + // + // Future refreshWallets(bool shouldNotifyListeners) async { + // final newNames = await _fetchWalletNames(); + // _walletNames = Future(() => newNames); + // if (shouldNotifyListeners) notifyListeners(); + // } +} From abf9f02f8ea43814218c4f3ecf9e6de042d1af26 Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 25 Jan 2023 14:09:07 +0200 Subject: [PATCH 027/208] ADdress fix --- .../coins/ethereum/ethereum_wallet.dart | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index ce75fea22..1a9e54a66 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,7 +1,11 @@ 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:bitcoindart/bitcoindart.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; @@ -87,6 +91,7 @@ class EthereumWallet extends CoinServiceAPI { // 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"; @override String get walletId => _walletId; @@ -309,9 +314,8 @@ class EthereumWallet extends CoinServiceAPI { //First get mnemonic so we can initialize credentials final mnemonicString = await _secureStore.read(key: '${_walletId}_mnemonic'); - - _credentials = - EthPrivateKey.fromHex(StringToHex.toHexString(mnemonicString)); + String privateKey = getPrivateKey(mnemonicString!); + _credentials = EthPrivateKey.fromHex(privateKey); Logging.instance.log("Opening existing ${coin.prettyName} wallet.", level: LogLevel.Info); @@ -329,13 +333,27 @@ 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); + } + @override Future initializeNew() async { await _prefs.init(); final String mnemonic = bip39.generateMnemonic(strength: 256); - _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); 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()); @@ -484,9 +502,12 @@ class EthereumWallet extends CoinServiceAPI { await _secureStore.write( key: '${_walletId}_mnemonic', value: mnemonic.trim()); - _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + String privateKey = getPrivateKey(mnemonic); + _credentials = EthPrivateKey.fromHex(privateKey); - print(_credentials.address); + // _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); + + // 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"); From d4653ea794fdd3b261f2d7287697765ceef7957e Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 25 Jan 2023 18:08:27 +0200 Subject: [PATCH 028/208] WIP: Add token functionality --- .../sub_widgets/my_token_select_item.dart | 43 +- .../sub_widgets/my_tokens_list.dart | 2 +- lib/pages/token_view/token_view.dart | 1169 +++++++++-------- lib/pages/wallet_view/wallet_view.dart | 2 - lib/route_generator.dart | 9 +- .../coins/ethereum/ethereum_wallet.dart | 12 +- .../tokens/ethereum/ethereum_token.dart | 66 +- lib/utilities/eth_commons.dart | 68 +- 8 files changed, 760 insertions(+), 611 deletions(-) diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index c7a6bf5c9..40fc7a154 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -4,7 +4,9 @@ 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'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -31,11 +33,12 @@ class MyTokenSelectItem extends ConsumerWidget { final ChangeNotifierProvider managerProvider; final String walletId; final String walletAddress; - final Map tokenData; + final Map tokenData; @override Widget build(BuildContext context, WidgetRef ref) { - int balance = int.parse(tokenData["balance"] as String); + print("TOKEN DATA IS $tokenData"); + int balance = tokenData["balance"] as int; int tokenDecimals = int.parse(tokenData["decimals"] as String); final balanceInDecimal = (balance / (pow(10, tokenDecimals))); @@ -51,38 +54,20 @@ class MyTokenSelectItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () { - // ref - // .read(walletsChangeNotifierProvider) - // .getManagerProvider(walletId) + final mnemonicList = ref.read(managerProvider).mnemonic; - // final walletId = ref - // .read(managerProvider) - // .walletName; - // final manager = ref - // .read(walletsChangeNotifierProvider) - // .getManagerProvider(walletId) - - // arguments: Tuple2(walletId, managerProvider, walletAddress, - // tokenData["contractAddress"]) - - // arguments: Tuple2( - // walletId, - // ref - // .read(tokensChangeNotifierProvider) - // .getManagerProvider(walletId) - - // arguments: Tuple2( - // walletId, - // ref - // .read(tokensChangeNotifierProvider) - // .getManagerProvider(walletId) + final token = EthereumToken( + contractAddress: tokenData["contractAddress"] as String, + walletMnemonic: mnemonicList); Navigator.of(context).pushNamed( TokenView.routeName, - arguments: Tuple2( + arguments: Tuple3( walletId, - ref.read(tokensChangeNotifierProvider).getManagerProvider( - tokenData["contractAddress"] as String)), + ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(walletId), + token), ); }, diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart index a23614cc7..03731c89a 100644 --- a/lib/pages/token_view/sub_widgets/my_tokens_list.dart +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -31,7 +31,7 @@ class MyTokensList extends StatelessWidget { managerProvider: managerProvider, walletId: walletId, walletAddress: walletAddress, - tokenData: tokens[index] as Map, + tokenData: tokens[index] as Map, ), ); }, diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index ee0d3e13d..10b478606 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -32,6 +32,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ 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'; @@ -49,37 +50,32 @@ import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; -import '../../services/tokens/token_manager.dart'; - /// [eventBus] should only be set during testing class TokenView extends ConsumerStatefulWidget { const TokenView({ Key? key, - required this.contractAddress, + required this.walletId, required this.managerProvider, - - // required this.walletAddress, - // required this.contractAddress, + required this.token, + this.eventBus, }) : super(key: key); static const String routeName = "/token"; static const double navBarHeight = 65.0; - final ChangeNotifierProvider managerProvider; - final String contractAddress; - // final String contractAddress; - // final String walletAddress; - - // final EventBus? eventBus; + final String walletId; + final ChangeNotifierProvider managerProvider; + final EthereumToken token; + final EventBus? eventBus; @override ConsumerState createState() => _TokenViewState(); } class _TokenViewState extends ConsumerState { - // late final EventBus eventBus; - late final String contractAddress; - late final ChangeNotifierProvider managerProvider; + late final EventBus eventBus; + late final String walletId; + late final ChangeNotifierProvider managerProvider; late final bool _shouldDisableAutoSyncOnLogOut; @@ -93,9 +89,10 @@ class _TokenViewState extends ConsumerState { @override void initState() { - contractAddress = widget.contractAddress; + walletId = widget.walletId; managerProvider = widget.managerProvider; + ref.read(managerProvider).isActiveWallet = true; if (!ref.read(managerProvider).shouldAutoSync) { // enable auto sync if it wasn't enabled when loading wallet ref.read(managerProvider).shouldAutoSync = true; @@ -119,44 +116,44 @@ class _TokenViewState extends ConsumerState { } } - // eventBus = - // widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; + eventBus = + widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - // _syncStatusSubscription = - // eventBus.on().listen( - // (event) async { - // if (event.walletId == widget.walletId) { - // // switch (event.newStatus) { - // // case WalletSyncStatus.unableToSync: - // // break; - // // case WalletSyncStatus.synced: - // // break; - // // case WalletSyncStatus.syncing: - // // break; - // // } - // setState(() { - // _currentSyncStatus = event.newStatus; - // }); - // } - // }, - // ); + _syncStatusSubscription = + eventBus.on().listen( + (event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case WalletSyncStatus.unableToSync: + // break; + // case WalletSyncStatus.synced: + // break; + // case WalletSyncStatus.syncing: + // break; + // } + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }, + ); - // _nodeStatusSubscription = - // eventBus.on().listen( - // (event) async { - // if (event.walletId == widget.walletId) { - // // switch (event.newStatus) { - // // case NodeConnectionStatus.disconnected: - // // break; - // // case NodeConnectionStatus.connected: - // // break; - // // } - // setState(() { - // _currentNodeStatus = event.newStatus; - // }); - // } - // }, - // ); + _nodeStatusSubscription = + eventBus.on().listen( + (event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case NodeConnectionStatus.disconnected: + // break; + // case NodeConnectionStatus.connected: + // break; + // } + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }, + ); super.initState(); } @@ -183,6 +180,7 @@ class _TokenViewState extends ConsumerState { Navigator.of(context).popUntil( ModalRoute.withName(HomeView.routeName), ); + _logout(); return false; }, child: const StackDialog(title: "Tap back again to exit wallet"), @@ -197,19 +195,19 @@ class _TokenViewState extends ConsumerState { return false; } - // void _logout() async { - // if (_shouldDisableAutoSyncOnLogOut) { - // // disable auto sync if it was enabled only when loading wallet - // ref.read(managerProvider).shouldAutoSync = false; - // } - // ref.read(managerProvider.notifier).isActiveWallet = false; - // ref.read(transactionFilterProvider.state).state = null; - // if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && - // ref.read(prefsChangeNotifierProvider).backupFrequencyType == - // BackupFrequencyType.afterClosingAWallet) { - // unawaited(ref.read(autoSWBServiceProvider).doBackup()); - // } - // } + void _logout() async { + if (_shouldDisableAutoSyncOnLogOut) { + // disable auto sync if it was enabled only when loading wallet + ref.read(managerProvider).shouldAutoSync = false; + } + ref.read(managerProvider.notifier).isActiveWallet = false; + ref.read(transactionFilterProvider.state).state = null; + if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && + ref.read(prefsChangeNotifierProvider).backupFrequencyType == + BackupFrequencyType.afterClosingAWallet) { + unawaited(ref.read(autoSWBServiceProvider).doBackup()); + } + } Widget _buildNetworkIcon(WalletSyncStatus status) { switch (status) { @@ -237,115 +235,141 @@ 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 contractAddress = ref.read(managerProvider).contractAddress; - // final contractAddress = "ref.read(managerProvider).contractAddress"; - // 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(), - // ), - // ); - // } - // - // - // } + void _onExchangePressed(BuildContext context) async { + unawaited(_cnLoadingService.loadAll(ref)); - // Future 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( - // context: context, - // builder: (_) => StackOkDialog( - // title: "Anonymize all failed", - // message: "Reason: $e", - // ), - // ); - // } - // } - // } + final coin = ref.read(managerProvider).coin; + + if (coin == Coin.epicCash) { + await showDialog( + context: context, + builder: (_) => const StackOkDialog( + title: "Exchange not available for Epic Cash", + ), + ); + } else if (coin.name.endsWith("TestNet")) { + await showDialog( + 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; + 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, + ), + ), + ); + } + } + } + + Future 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( + context: context, + builder: (_) => StackOkDialog( + title: "Anonymize all failed", + message: "Reason: $e", + ), + ); + } + } + } void _loadCNData() { // unawaited future if (ref.read(prefsChangeNotifierProvider).externalCalls) { - _cnLoadingService.loadAll(ref, coin: Coin.ethereum); + _cnLoadingService.loadAll(ref, coin: ref.read(managerProvider).coin); } else { Logging.instance.log("User does not want to use external calls", level: LogLevel.Info); @@ -355,381 +379,440 @@ class _TokenViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return const Scaffold(); - // final coin = ref.watch(managerProvider.select((value) => value.Co)); + final coin = ref.watch(managerProvider.select((value) => value.coin)); - // return WillPopScope( - // onWillPop: _onWillPop, - // child: Background( - // child: Scaffold( - // backgroundColor: - // Theme.of(context).extension()!.background, - // appBar: AppBar( - // leading: AppBarBackButton( - // onPressed: () { - // Navigator.of(context).pop(); - // }, - // ), - // titleSpacing: 0, - // title: Row( - // children: [ - // SvgPicture.asset( - // Assets.svg.iconFor(coin: Coin.ethereum), - // // color: Theme.of(context).extension()!.accentColorDark - // width: 24, - // height: 24, - // ), - // const SizedBox( - // width: 16, - // ), - // Expanded( - // child: Text( - // ref.watch( - // managerProvider.select((value) => "value.walletName")), - // 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()!.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()!.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()! - // // // .topNavIconPrimary, - // // // ), - // // onPressed: () { - // // // reset unread state - // // ref.refresh(unreadNotificationsStateProvider); - // // - // // Navigator.of(context) - // // .pushNamed( - // // NotificationsView.routeName, - // // arguments: walletId, - // // ) - // // .then((_) { - // // final Set unreadNotificationIds = ref - // // .read(unreadNotificationsStateProvider.state) - // // .state; - // // if (unreadNotificationIds.isEmpty) return; - // // - // // List> 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()!.background, - // icon: SvgPicture.asset( - // Assets.svg.bars, - // color: Theme.of(context) - // .extension()! - // .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( - // color: Theme.of(context).extension()!.background, - // child: Column( - // children: [ - // const SizedBox( - // height: 10, - // ), - // Center( - // child: Padding( - // padding: const EdgeInsets.symmetric(horizontal: 16), - // // child: WalletSummary( - // // walletId: walletId, - // // managerProvider: managerProvider, - // // initialSyncStatus: ref.watch(managerProvider - // // .select((value) => value.isRefreshing)) - // // ? WalletSyncStatus.syncing - // // : WalletSyncStatus.synced, - // // ), - // ), - // ), - // // if (coin == Coin.firo) - // // const SizedBox( - // // height: 10, - // // ), - // const SizedBox( - // height: 20, - // ), - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 16), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "Transactions", - // style: STextStyles.itemSubtitle(context).copyWith( - // color: Theme.of(context) - // .extension()! - // .textDark3, - // ), - // ), - // BlueTextButton( - // text: "See all", - // // onTap: () { - // // Navigator.of(context).pushNamed( - // // AllTransactionsView.routeName, - // // arguments: walletId, - // // ); - // // }, - // ), - // ], - // ), - // ), - // const SizedBox( - // height: 12, - // ), - // Expanded( - // child: Stack( - // children: [ - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 16), - // child: Padding( - // padding: const EdgeInsets.only(bottom: 14), - // child: ClipRRect( - // borderRadius: BorderRadius.vertical( - // top: Radius.circular( - // Constants.size.circularBorderRadius, - // ), - // bottom: Radius.circular( - // // TokenView.navBarHeight / 2.0, - // Constants.size.circularBorderRadius, - // ), - // ), - // child: Container( - // decoration: BoxDecoration( - // color: Colors.transparent, - // borderRadius: BorderRadius.circular( - // Constants.size.circularBorderRadius, - // ), - // ), - // child: Column( - // crossAxisAlignment: - // CrossAxisAlignment.stretch, - // children: [ - // // Expanded( - // // child: TransactionsList( - // // managerProvider: managerProvider, - // // walletId: walletId, - // // ), - // // ), - // ], - // ), - // ), - // ), - // ), - // ), - // Column( - // mainAxisAlignment: MainAxisAlignment.end, - // children: [ - // const Spacer(), - // 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; - // - // final walletName = ref - // .read(managerProvider) - // .walletName; - // - // List tokens = - // await getWalletTokens( - // walletAddress); - // - // await Navigator.of(context).pushNamed( - // MyTokensView.routeName, - // arguments: Tuple3(walletAddress, - // tokens, walletName), - // ); - // }, - // ), - // ), - // ), - // ], - // ), - // ], - // ) - // ], - // ), - // ), - // ], - // ), - // ), - // ), - // ), - // ), - // ); + return WillPopScope( + onWillPop: _onWillPop, + child: Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + _logout(); + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + // color: Theme.of(context).extension()!.accentColorDark + width: 24, + height: 24, + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Text( + ref.watch( + managerProvider.select((value) => value.walletName)), + 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()!.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()!.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()! + .topNavIconPrimary, + ), + onPressed: () { + // reset unread state + ref.refresh(unreadNotificationsStateProvider); + + Navigator.of(context) + .pushNamed( + NotificationsView.routeName, + arguments: walletId, + ) + .then((_) { + final Set unreadNotificationIds = ref + .read(unreadNotificationsStateProvider.state) + .state; + if (unreadNotificationIds.isEmpty) return; + + List> 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()!.background, + icon: SvgPicture.asset( + Assets.svg.bars, + color: Theme.of(context) + .extension()! + .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( + color: Theme.of(context).extension()!.background, + child: Column( + children: [ + const SizedBox( + height: 10, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: WalletSummary( + walletId: walletId, + managerProvider: managerProvider, + initialSyncStatus: ref.watch(managerProvider + .select((value) => value.isRefreshing)) + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + ), + ), + 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()! + .getSecondaryEnabledButtonColor(context), + onPressed: () async { + await showDialog( + 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()! + .accentColorDark, + ), + ), + ), + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(); + + unawaited(attemptAnonymize()); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonColor( + context), + child: Text( + "Continue", + style: STextStyles.button(context), + ), + ), + ), + ); + }, + child: Text( + "Anonymize funds", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + ), + ), + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + BlueTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: walletId, + ); + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Expanded( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Padding( + padding: const EdgeInsets.only(bottom: 14), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottom: Radius.circular( + // TokenView.navBarHeight / 2.0, + Constants.size.circularBorderRadius, + ), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TransactionsList( + managerProvider: managerProvider, + walletId: walletId, + ), + ), + ], + ), + ), + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(), + 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 tokens = + await getWalletTokens(await ref + .read(managerProvider) + .currentReceivingAddress); + + await Navigator.of(context).pushNamed( + MyTokensView.routeName, + arguments: Tuple4(managerProvider, + walletId, walletAddress, tokens), + ); + }, + ), + ), + ), + ], + ), + ], + ) + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); } } diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index eef0fa755..03bc2941c 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -783,8 +783,6 @@ class _WalletViewState extends ConsumerState { .read(managerProvider) .currentReceivingAddress; - // String walletTokens = await - List tokens = await getWalletTokens(await ref .read(managerProvider) diff --git a/lib/route_generator.dart b/lib/route_generator.dart index e86dda8e3..04be917a3 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -117,6 +117,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncin 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'; @@ -1327,14 +1328,14 @@ class RouteGenerator { // } case TokenView.routeName: - if (args is Tuple2>) { + if (args + is Tuple3, EthereumToken>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => TokenView( - contractAddress: args.item1, + walletId: args.item1, managerProvider: args.item2, - // walletAddress: args.item3, - // contractAddress: args.item4, + token: args.item3, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 1a9e54a66..520186643 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -5,7 +5,6 @@ import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import "package:hex/hex.dart"; -import 'package:bitcoindart/bitcoindart.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; @@ -22,7 +21,6 @@ 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:string_to_hex/string_to_hex.dart'; import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart' as web3; import 'package:web3dart/web3dart.dart' as Transaction; @@ -45,7 +43,7 @@ import 'package:stackwallet/services/event_bus/events/global/updated_in_backgrou import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; -const int MINIMUM_CONFIRMATIONS = 5; +const int MINIMUM_CONFIRMATIONS = 3; //THis is used for mapping transactions per address from the block explorer class AddressTransaction { @@ -469,11 +467,15 @@ 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 txData = { "fee": feeEstimate, "feeInWei": fee, @@ -505,8 +507,6 @@ class EthereumWallet extends CoinServiceAPI { String privateKey = getPrivateKey(mnemonic); _credentials = EthPrivateKey.fromHex(privateKey); - // _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); - // 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( @@ -1063,8 +1063,6 @@ class EthereumWallet extends CoinServiceAPI { @override set walletName(String newName) => _walletName = newName; - // Future - 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 ef3bfc17f..5ddef7259 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -1,30 +1,69 @@ +import 'dart:convert'; + import 'package:decimal/decimal.dart'; +import 'package:http/http.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:web3dart/web3dart.dart'; + +class AbiRequestResponse { + final String message; + final String result; + final String status; + + const AbiRequestResponse({ + required this.message, + required this.result, + required this.status, + }); + + factory AbiRequestResponse.fromJson(Map json) { + return AbiRequestResponse( + message: json['message'] as String, + result: json['result'] as String, + status: json['status'] as String, + ); + } +} class EthereumToken extends TokenServiceAPI { @override late bool shouldAutoSync; - late String _walletId; late String _contractAddress; + late EthPrivateKey _credentials; + late Future> _walletMnemonic; late SecureStorageInterface _secureStore; + late String _tokenAbi; late final TransactionNotificationTracker txTracker; + String rpcUrl = + 'https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba'; + EthereumToken({ required String contractAddress, - required String walletId, - required SecureStorageInterface secureStore, - required TransactionNotificationTracker tracker, + required Future> walletMnemonic, + // required SecureStorageInterface secureStore, }) { - txTracker = tracker; - _walletId = walletId; _contractAddress = contractAddress; - _secureStore = secureStore; + _walletMnemonic = walletMnemonic; + // _secureStore = secureStore; + } + + Future fetchTokenAbi() async { + final response = await get(Uri.parse( + "https://api.etherscan.io/api?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + if (response.statusCode == 200) { + return AbiRequestResponse.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception('Failed to load transactions'); + } } @override @@ -64,7 +103,18 @@ class EthereumToken extends TokenServiceAPI { Future get fees => throw UnimplementedError(); @override - Future initializeExisting() { + Future initializeExisting() async { + 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(); } diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index a8286993b..c3c95f8a6 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -1,6 +1,11 @@ import 'dart:convert'; import 'package:http/http.dart'; +import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'flutter_secure_storage_interface.dart'; +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; +import "package:hex/hex.dart"; class AccountModule { final String message; @@ -22,11 +27,13 @@ class AccountModule { } } -const _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; +const _blockExplorer = "https://api.etherscan.io/api?"; +late SecureStorageInterface _secureStore; +const _hdPath = "m/44'/60'/0'/0"; Future fetchAccountModule(String action, String address) async { final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=$action&address=$address")); + "${_blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AccountModule.fromJson( json.decode(response.body) as Map); @@ -36,21 +43,48 @@ Future fetchAccountModule(String action, String address) async { } Future> getWalletTokens(String address) async { - AccountModule tokens = await fetchAccountModule("tokenlist", address); - //THIS IS ONLY HARD CODED UNTIL API WORKS AGAIN - TODO REMOVE HARDCODED - return [ - { - "balance": "369039500000000000", - "contractAddress": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", - "decimals": "18", - "name": "Uniswap", - "symbol": "UNI", - "type": "ERC-20" - } - ]; - + AccountModule tokens = await fetchAccountModule("tokentx", address); + List tokensList = []; + var tokenMap = {}; if (tokens.message == "OK") { - return tokens.result as List; + final allTxs = tokens.result; + print("RESULT IS $allTxs"); + allTxs.forEach((element) { + String key = element["tokenSymbol"] as String; + tokenMap[key] = {}; + tokenMap[key]["balance"] = 0; + + if (tokenMap.containsKey(key)) { + tokenMap[key]["contractAddress"] = element["contractAddress"] as String; + tokenMap[key]["decimals"] = element["tokenDecimal"]; + tokenMap[key]["name"] = element["tokenName"]; + tokenMap[key]["symbol"] = element["tokenSymbol"]; + if (checksumEthereumAddress(address) == address) { + tokenMap[key]["balance"] += int.parse(element["value"] as String); + } else { + tokenMap[key]["balance"] -= int.parse(element["value"] as String); + } + } + }); + + tokenMap.forEach((key, value) { + tokensList.add(value as Map); + }); + return tokensList; } - return []; + return []; +} + +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); } From dbcbfe342c5a52e1f7a30a7ad8d15099f20fb90e Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 26 Jan 2023 20:08:12 +0200 Subject: [PATCH 029/208] WIP: Add test ETH Token functionality in stack --- .../sub_widgets/my_token_select_item.dart | 8 +- .../token_view/sub_widgets/token_summary.dart | 128 ++++ .../sub_widgets/token_summary_info.dart | 298 ++++++++++ lib/pages/token_view/token_view.dart | 562 +++++------------- lib/providers/global/tokens_provider.dart | 23 - .../global/tokens_service_provider.dart | 20 - lib/route_generator.dart | 10 +- .../coins/ethereum/ethereum_wallet.dart | 93 +-- lib/services/tokens.dart | 374 ------------ .../tokens/ethereum/ethereum_token.dart | 277 +++++++-- lib/services/tokens/token_manager.dart | 130 ---- lib/services/tokens/token_service.dart | 17 +- lib/utilities/eth_commons.dart | 65 +- 13 files changed, 861 insertions(+), 1144 deletions(-) create mode 100644 lib/pages/token_view/sub_widgets/token_summary.dart create mode 100644 lib/pages/token_view/sub_widgets/token_summary_info.dart delete mode 100644 lib/providers/global/tokens_provider.dart delete mode 100644 lib/providers/global/tokens_service_provider.dart delete mode 100644 lib/services/tokens.dart delete mode 100644 lib/services/tokens/token_manager.dart diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 40fc7a154..064949cd7 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -4,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), diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart new file mode 100644 index 000000000..bac284bb0 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -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 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()! + .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, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/token_summary_info.dart b/lib/pages/token_view/sub_widgets/token_summary_info.dart new file mode 100644 index 000000000..40abaebe0 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/token_summary_info.dart @@ -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 managerProvider; + final WalletSyncStatus initialSyncStatus; + + @override + State createState() => _TokenSummaryInfoState(); +} + +class _TokenSummaryInfoState extends State { + late final String walletId; + late final ChangeNotifierProvider managerProvider; + + void showSheet() { + showModalBottomSheet( + 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? 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 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 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()! + .textFavoriteCard, + ), + ), + Text( + "${_showAvailable ? "Available" : "Full"} Balance", + style: + STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + const SizedBox( + width: 4, + ), + SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension()! + .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()! + .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()! + .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()! + .textFavoriteCard, + ), + ), + if (coin != Coin.firo && coin != Coin.firoTestNet) + Text( + "${_showAvailable ? "Available" : "Full"} Balance", + style: + STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + const SizedBox( + width: 4, + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension()! + .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()! + .textFavoriteCard, + ), + ), + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .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, + ), + ], + ) + ], + ); + } +} diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 10b478606..1645cdf78 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -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 tokenData; final ChangeNotifierProvider managerProvider; final EthereumToken token; final EventBus? eventBus; @@ -209,160 +211,51 @@ class _TokenViewState extends ConsumerState { } } - Widget _buildNetworkIcon(WalletSyncStatus status) { - switch (status) { - case WalletSyncStatus.unableToSync: - return SvgPicture.asset( - Assets.svg.radioProblem, - color: Theme.of(context).extension()!.accentColorRed, - width: 20, - height: 20, - ); - case WalletSyncStatus.synced: - return SvgPicture.asset( - Assets.svg.radio, - color: Theme.of(context).extension()!.accentColorGreen, - width: 20, - height: 20, - ); - case WalletSyncStatus.syncing: - return SvgPicture.asset( - Assets.svg.radioSyncing, - color: Theme.of(context).extension()!.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( - context: context, - builder: (_) => const StackOkDialog( - title: "Exchange not available for Epic Cash", - ), - ); - } else if (coin.name.endsWith("TestNet")) { - await showDialog( - 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; - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.estimated; + 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; + 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()); + 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, - ), - ), - ); - } - } - } - - Future 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; + if (currencies.isNotEmpty) { + ref.read(exchangeFormStateProvider).setCurrencies( + currencies.first, + ref + .read(availableChangeNowCurrenciesProvider) + .currencies + .firstWhere( + (element) => + element.ticker.toLowerCase() != coin.ticker.toLowerCase(), + ), + ); } - 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, + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + WalletInitiatedExchangeView.routeName, + arguments: Tuple3( + walletId, + coin, + _loadCNData, ), - ); - } - } catch (e) { - shouldPop = true; - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(TokenView.routeName), - ); - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Anonymize all failed", - message: "Reason: $e", - ), - ); - } + ), + ); } } @@ -379,6 +272,8 @@ class _TokenViewState extends ConsumerState { @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 { 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()!.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()!.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()! - .topNavIconPrimary, - ), - onPressed: () { - // reset unread state - ref.refresh(unreadNotificationsStateProvider); - - Navigator.of(context) - .pushNamed( - NotificationsView.routeName, - arguments: walletId, - ) - .then((_) { - final Set unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - List> 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()!.background, - icon: SvgPicture.asset( - Assets.svg.bars, - color: Theme.of(context) - .extension()! - .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 { 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 { ), ), ), - 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()! - .getSecondaryEnabledButtonColor(context), - onPressed: () async { - await showDialog( - 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()! - .accentColorDark, - ), - ), - ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(); - - unawaited(attemptAnonymize()); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor( - context), - child: Text( - "Continue", - style: STextStyles.button(context), - ), - ), - ), - ); - }, - child: Text( - "Anonymize funds", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - ), - ), - ), - ], - ), - ), const SizedBox( height: 20, ), @@ -715,91 +419,91 @@ class _TokenViewState extends ConsumerState { 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 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 tokens = + // await getWalletTokens(await ref + // .read(managerProvider) + // .currentReceivingAddress); + // + // await Navigator.of(context).pushNamed( + // MyTokensView.routeName, + // arguments: Tuple4(managerProvider, + // walletId, walletAddress, tokens), + // ); + // }, + // ), + // ), + // ), ], ), ], diff --git a/lib/providers/global/tokens_provider.dart b/lib/providers/global/tokens_provider.dart deleted file mode 100644 index 388c45971..000000000 --- a/lib/providers/global/tokens_provider.dart +++ /dev/null @@ -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((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; -}); diff --git a/lib/providers/global/tokens_service_provider.dart b/lib/providers/global/tokens_service_provider.dart deleted file mode 100644 index 053bc035e..000000000 --- a/lib/providers/global/tokens_service_provider.dart +++ /dev/null @@ -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((ref) { - if (kDebugMode) { - _count++; - debugPrint( - "tokensServiceChangeNotifierProvider instantiation count: $_count"); - } - - return TokensService( - secureStorageInterface: ref.read(secureStoreProvider), - ); -}); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 04be917a3..21ee18aa3 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -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, EthereumToken>) { + if (args is Tuple4, + ChangeNotifierProvider, 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, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 520186643..e056e334b 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -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 data; - - const GasTracker({ - required this.code, - required this.data, - }); - - factory GasTracker.fromJson(Map json) { - return GasTracker( - code: json['code'] as int, - data: json['data'] as Map, - ); - } -} - 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 get currentReceivingAddress async { final _currentReceivingAddress = _credentials.address; @@ -243,14 +215,8 @@ class EthereumWallet extends CoinServiceAPI { @override Future 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; Future _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 getGasOracle() async { - final response = await get(Uri.parse(_gasTrackerUrl)); - - if (response.statusCode == 200) { - return GasTracker.fromJson( - json.decode(response.body) as Map); - } 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); - } - @override Future 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 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> 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(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"; } diff --git a/lib/services/tokens.dart b/lib/services/tokens.dart deleted file mode 100644 index d7997d9ed..000000000 --- a/lib/services/tokens.dart +++ /dev/null @@ -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> _nonFavorites = -// ListenableList(); -// ListenableList> get nonFavorites => -// _nonFavorites; -// -// final ListenableList> _favorites = -// ListenableList(); -// ListenableList> 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> - _managerProviderMap = ListenableMap(); - static final ListenableMap _managerMap = - ListenableMap(); - - // bool get hasWallets => _managerProviderMap.isNotEmpty; - - List> get managerProviders => - _managerProviderMap.values.toList(growable: false); - List get managers => _managerMap.values.toList(growable: false); - - // List getWalletIdsFor({required Coin coin}) { - // final List result = []; - // for (final manager in _managerMap.values) { - // if (manager.coin == coin) { - // result.add(manager.walletId); - // } - // } - // return result; - // } - - // Map>> getManagerProvidersByCoin() { - // print("DOES THIS GET HERE?????"); - // Map>> result = {}; - // for (final manager in _managerMap.values) { - // if (result[manager.coin] == null) { - // result[manager.coin] = []; - // } - // result[manager.coin]!.add(_managerProviderMap[manager.walletId] - // as ChangeNotifierProvider); - // } - // return result; - // } - - // List> getManagerProvidersForCoin(Coin coin) { - // List> result = []; - // for (final manager in _managerMap.values) { - // if (manager.coin == coin) { - // result.add(_managerProviderMap[manager.walletId] - // as ChangeNotifierProvider); - // } - // } - // return result; - // } - - ChangeNotifierProvider getManagerProvider( - String contractAddress) { - print("WALLET ID HERE IS ${_managerProviderMap.length}"); - return _managerProviderMap[contractAddress] - as ChangeNotifierProvider; - } - - 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((_) => 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 _initLinearly( - List> 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 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(boxName: DB.boxNameWalletsToDeleteOnStart)) { - // await DB.instance.deleteBoxFromDisk(boxName: walletId); - // } - // // clear list - // await DB.instance - // .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); - // - // final map = await walletsService.walletNames; - - // List> walletInitFutures = []; - // List> walletsToInitLinearly = []; - - // final favIdList = await walletsService.getFavoriteWalletIds(); - - // List 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); - // _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 loadAfterStackRestore( - // Prefs prefs, List managers) async { - // List> walletInitFutures = []; - // List> walletsToInitLinearly = []; - // - // final favIdList = await walletsService.getFavoriteWalletIds(); - // - // List 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); - // _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(); - // } - // } -} diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/tokens/ethereum/ethereum_token.dart index 5ddef7259..01407aa60 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -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 _tokenData; + late ContractFunction _balanceFunction; + late ContractFunction _sendFunction; late Future> _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 tokenData, required Future> walletMnemonic, // required SecureStorageInterface secureStore, }) { - _contractAddress = contractAddress; + _contractAddress = + EthereumAddress.fromHex(tokenData["contractAddress"] as String); _walletMnemonic = walletMnemonic; + _tokenData = tokenData; // _secureStore = secureStore; } Future 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); } else { - throw Exception('Failed to load transactions'); + throw Exception('Failed to load token abi'); } } @override - // TODO: implement allOwnAddresses - Future> get allOwnAddresses => throw UnimplementedError(); + Future> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future>? _allOwnAddresses; - @override - // TODO: implement availableBalance - Future get availableBalance => throw UnimplementedError(); - - @override - // TODO: implement balanceMinusMaxFee - Future get balanceMinusMaxFee => throw UnimplementedError(); - - @override - // TODO: implement coin - Coin get coin => throw UnimplementedError(); - - @override - Future confirmSend({required Map txData}) { - // TODO: implement confirmSend - throw UnimplementedError(); + Future> _fetchAllOwnAddresses() async { + List addresses = []; + final ownAddress = _credentials.address; + addresses.add(ownAddress.toString()); + return addresses; } @override - // TODO: implement currentReceivingAddress - Future get currentReceivingAddress => throw UnimplementedError(); - - @override - Future estimateFeeFor(int satoshiAmount, int feeRate) { - // TODO: implement estimateFeeFor - throw UnimplementedError(); + Future get availableBalance async { + return await totalBalance; } @override - // TODO: implement fees - Future get fees => throw UnimplementedError(); + Future get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(); + + @override + Coin get coin => Coin.ethereum; + + @override + Future confirmSend({required Map 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 get currentReceivingAddress async { + final _currentReceivingAddress = await _credentials.extractAddress(); + final checkSumAddress = + checksumEthereumAddress(_currentReceivingAddress.toString()); + return checkSumAddress; + } + + @override + Future 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 get fees => _feeObject ??= _getFees(); + Future? _feeObject; + + Future _getFees() async { + return await getFees(); + } @override Future initializeExisting() async { + //TODO - GET abi FROM secure store AbiRequestResponse abi = await fetchTokenAbi(); //Fetch token ABI so we can call token functions if (abi.message == "OK") { _tokenAbi = abi.result; } - 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 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 get maxFee => throw UnimplementedError(); - - @override - // TODO: implement pendingBalance - Future get pendingBalance => throw UnimplementedError(); + Future get maxFee async { + final fee = (await fees).fast; + final feeEstimate = await estimateFeeFor(0, fee); + return feeEstimate; + } @override Future> prepareSend( {required String address, required int satoshiAmount, - Map? args}) { - // TODO: implement prepareSend - throw UnimplementedError(); + Map? 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 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 get totalBalance => throw UnimplementedError(); + Future 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 get transactionData => throw UnimplementedError(); @override - Future updateSentCachedTxData(Map txData) { - // TODO: implement updateSentCachedTxData - throw UnimplementedError(); + Future updateSentCachedTxData(Map 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 getEthClient() async { + return Web3Client(rpcUrl, Client()); } } diff --git a/lib/services/tokens/token_manager.dart b/lib/services/tokens/token_manager.dart deleted file mode 100644 index cceee7e47..000000000 --- a/lib/services/tokens/token_manager.dart +++ /dev/null @@ -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? _backgroundRefreshListener; - - /// optional eventbus parameter for testing only - TokenManager(this._currentToken, [EventBus? globalEventBusForTesting]) { - final bus = globalEventBusForTesting ?? GlobalEventBus.instance; - _backgroundRefreshListener = bus.on().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> prepareSend({ - required String address, - required int satoshiAmount, - Map? 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 confirmSend({required Map 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 get fees => _currentToken.fees; - Future get maxFee => _currentToken.maxFee; - - Future get currentReceivingAddress => - _currentToken.currentReceivingAddress; - // Future get currentLegacyReceivingAddress => - // _currentWallet.currentLegacyReceivingAddress; - - Future get availableBalance async { - _cachedAvailableBalance = await _currentToken.availableBalance; - return _cachedAvailableBalance; - } - - Decimal _cachedAvailableBalance = Decimal.zero; - Decimal get cachedAvailableBalance => _cachedAvailableBalance; - - Future get pendingBalance => _currentToken.pendingBalance; - Future get balanceMinusMaxFee => _currentToken.balanceMinusMaxFee; - - Future get totalBalance async { - _cachedTotalBalance = await _currentToken.totalBalance; - return _cachedTotalBalance; - } - - Decimal _cachedTotalBalance = Decimal.zero; - Decimal get cachedTotalBalance => _cachedTotalBalance; - - Future> get allOwnAddresses => _currentToken.allOwnAddresses; - - Future get transactionData => _currentToken.transactionData; - - Future refresh() async { - await _currentToken.refresh(); - notifyListeners(); - } - - bool validateAddress(String address) => - _currentToken.validateAddress(address); - - Future initializeNew() => _currentToken.initializeNew(); - Future initializeExisting() => _currentToken.initializeExisting(); - - Future isOwnAddress(String address) async { - final allOwnAddresses = await this.allOwnAddresses; - return allOwnAddresses.contains(address); - } - - bool get isConnected => _currentToken.isConnected; - - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - return _currentToken.estimateFeeFor(satoshiAmount, feeRate); - } -} diff --git a/lib/services/tokens/token_service.dart b/lib/services/tokens/token_service.dart index 0f23054f5..c19897b08 100644 --- a/lib/services/tokens/token_service.dart +++ b/lib/services/tokens/token_service.dart @@ -10,17 +10,17 @@ abstract class TokenServiceAPI { TokenServiceAPI(); factory TokenServiceAPI.from( - String contractAddress, - String walletId, + Map tokenData, + Future> 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 get currentLegacyReceivingAddress; Future get availableBalance; - Future get pendingBalance; Future get totalBalance; Future get balanceMinusMaxFee; @@ -62,10 +61,6 @@ abstract class TokenServiceAPI { Future initializeNew(); Future initializeExisting(); - // void Function(bool isActive)? onIsActiveWalletChanged; - - bool get isConnected; - Future estimateFeeFor(int satoshiAmount, int feeRate); // used for electrumx coins diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index c3c95f8a6..5a221f828 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -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 data; + + const GasTracker({ + required this.code, + required this.data, + }); + + factory GasTracker.fromJson(Map json) { + return GasTracker( + code: json['code'] as int, + data: json['data'] as Map, + ); + } +} + +// 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 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); @@ -48,7 +68,6 @@ Future> 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); } + +Future getGasOracle() async { + final response = await get(Uri.parse(_gasTrackerUrl)); + + if (response.statusCode == 200) { + return GasTracker.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception('Failed to load gas oracle'); + } +} + +Future 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); +} From fd0b20d66102b777d7da25114a5fe15d03d80f9a Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 27 Jan 2023 14:32:05 +0200 Subject: [PATCH 030/208] Complete adding ERC-20 functionality --- .../sub_widgets/my_token_select_item.dart | 7 +- .../coins/ethereum/ethereum_wallet.dart | 72 ------- .../tokens/ethereum/ethereum_token.dart | 181 ++++++++++++++++-- lib/services/tokens/token_service.dart | 2 +- lib/utilities/eth_commons.dart | 56 +++--- 5 files changed, 199 insertions(+), 119 deletions(-) diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 064949cd7..3fcc75619 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -4,12 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; @@ -35,7 +35,6 @@ class MyTokenSelectItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - print("TOKEN DATA IS $tokenData"); int balance = tokenData["balance"] as int; int tokenDecimals = int.parse(tokenData["decimals"] as String); final balanceInDecimal = (balance / (pow(10, tokenDecimals))); @@ -55,9 +54,9 @@ class MyTokenSelectItem extends ConsumerWidget { final mnemonicList = ref.read(managerProvider).mnemonic; final token = EthereumToken( - // contractAddress: tokenData["contractAddress"] as String, tokenData: tokenData, - walletMnemonic: mnemonicList); + walletMnemonic: mnemonicList, + secureStore: ref.read(secureStoreProvider)); Navigator.of(context).pushNamed( TokenView.routeName, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index e056e334b..ff37014d5 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; @@ -42,31 +41,9 @@ import 'package:stackwallet/utilities/default_nodes.dart'; const int MINIMUM_CONFIRMATIONS = 3; -//THis is used for mapping transactions per address from the block explorer -class AddressTransaction { - final String message; - final List result; - final String status; - - const AddressTransaction({ - required this.message, - required this.result, - required this.status, - }); - - factory AddressTransaction.fromJson(Map json) { - return AddressTransaction( - message: json['message'] as String, - result: json['result'] as List, - status: json['status'] as String, - ); - } -} - class EthereumWallet extends CoinServiceAPI { NodeModel? _ethNode; final _gasLimit = 21000; - final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; @override String get walletId => _walletId; @@ -436,42 +413,6 @@ class EthereumWallet extends CoinServiceAPI { String privateKey = getPrivateKey(mnemonic); _credentials = EthPrivateKey.fromHex(privateKey); - //Get ERC-20 transactions for wallet (So we can get the and save wallet's ERC-20 TOKENS - AddressTransaction tokenTransactions = await fetchAddressTransactions( - _credentials.address.toString(), "tokentx"); - var tokenMap = {}; - List> tokensList = []; - if (tokenTransactions.message == "OK") { - final allTxs = tokenTransactions.result; - - allTxs.forEach((element) { - String key = element["tokenSymbol"] as String; - tokenMap[key] = {}; - tokenMap[key]["balance"] = 0; - - if (tokenMap.containsKey(key)) { - tokenMap[key]["contractAddress"] = element["contractAddress"]; - tokenMap[key]["decimals"] = element["tokenDecimal"]; - tokenMap[key]["name"] = element["tokenName"]; - tokenMap[key]["symbol"] = element["tokenSymbol"]; - if (element["to"] == _credentials.address.toString()) { - tokenMap[key]["balance"] += int.parse(element["value"] as String); - } else { - tokenMap[key]["balance"] -= int.parse(element["value"] as String); - } - } - }); - - tokenMap.forEach((key, value) { - //Create New token - - tokensList.add(value as Map); - }); - - await _secureStore.write( - key: '${_walletId}_tokens', value: tokensList.toString()); - } - await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance @@ -846,19 +787,6 @@ class EthereumWallet extends CoinServiceAPI { return isValidEthereumAddress(address); } - Future fetchAddressTransactions( - String address, String action) async { - final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); - - if (response.statusCode == 200) { - return AddressTransaction.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception('Failed to load transactions'); - } - } - Future _fetchTransactionData() async { String thisAddress = await currentReceivingAddress; final cachedTransactions = diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/tokens/ethereum/ethereum_token.dart index 01407aa60..8998acaea 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:math'; +import 'package:devicelocale/devicelocale.dart'; import 'package:http/http.dart'; import 'package:decimal/decimal.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; @@ -13,9 +14,14 @@ import 'package:stackwallet/services/tokens/token_service.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/models/paymint/transactions_model.dart' as models; import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart' as transaction; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/services/node_service.dart'; + class AbiRequestResponse { final String message; final String result; @@ -36,6 +42,8 @@ class AbiRequestResponse { } } +const int MINIMUM_CONFIRMATIONS = 3; + class EthereumToken extends TokenServiceAPI { @override late bool shouldAutoSync; @@ -50,28 +58,25 @@ class EthereumToken extends TokenServiceAPI { late String _tokenAbi; late Web3Client _client; late final TransactionNotificationTracker txTracker; + TransactionData? cachedTxData; - String rpcUrl = - 'https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba'; final _gasLimit = 200000; EthereumToken({ required Map tokenData, required Future> walletMnemonic, - // required SecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) { _contractAddress = EthereumAddress.fromHex(tokenData["contractAddress"] as String); _walletMnemonic = walletMnemonic; _tokenData = tokenData; - // _secureStore = secureStore; + _secureStore = secureStore; } Future fetchTokenAbi() async { - print( - "$blockExplorer?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"); final response = await get(Uri.parse( - "$blockExplorer?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + "$abiUrl?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AbiRequestResponse.fromJson( json.decode(response.body) as Map); @@ -156,12 +161,24 @@ class EthereumToken extends TokenServiceAPI { @override Future initializeExisting() async { - //TODO - GET abi FROM secure store - AbiRequestResponse abi = await fetchTokenAbi(); - //Fetch token ABI so we can call token functions - if (abi.message == "OK") { - _tokenAbi = abi.result; + if ((await _secureStore.read( + key: '${_contractAddress.toString()}_tokenAbi')) != + null) { + _tokenAbi = (await _secureStore.read( + key: '${_contractAddress.toString()}_tokenAbi'))!; + } else { + AbiRequestResponse abi = await fetchTokenAbi(); + //Fetch token ABI so we can call token functions + if (abi.message == "OK") { + _tokenAbi = abi.result; + //Store abi in secure store + await _secureStore.write( + key: '${_contractAddress.toString()}_tokenAbi', value: _tokenAbi); + } else { + throw Exception('Failed to load token abi'); + } } + final mnemonic = await _walletMnemonic; String mnemonicString = mnemonic.join(' '); @@ -175,17 +192,21 @@ class EthereumToken extends TokenServiceAPI { _balanceFunction = _contract.function('balanceOf'); _sendFunction = _contract.function('transfer'); _client = await getEthClient(); - print("${await totalBalance}"); } @override Future initializeNew() async { - //TODO - Save abi in secure store AbiRequestResponse abi = await fetchTokenAbi(); //Fetch token ABI so we can call token functions if (abi.message == "OK") { _tokenAbi = abi.result; + //Store abi in secure store + await _secureStore.write( + key: '${_contractAddress.toString()}_tokenAbi', value: _tokenAbi); + } else { + throw Exception('Failed to load token abi'); } + final mnemonic = await _walletMnemonic; String mnemonicString = mnemonic.join(' '); @@ -253,7 +274,6 @@ class EthereumToken extends TokenServiceAPI { "recipientAmt": satoshiAmount, }; - print("TX DATA TO BE SENT IS $txData"); return txData; } @@ -277,12 +297,13 @@ class EthereumToken extends TokenServiceAPI { } @override - // TODO: implement transactionData - Future get transactionData => throw UnimplementedError(); + Future get transactionData => + _transactionData ??= _fetchTransactionData(); + Future? _transactionData; @override Future updateSentCachedTxData(Map txData) async { - Decimal currentPrice = Decimal.parse(0.0 as String); + Decimal currentPrice = Decimal.zero; final locale = await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: @@ -321,12 +342,134 @@ class EthereumToken extends TokenServiceAPI { } } + Future _fetchTransactionData() async { + String thisAddress = await currentReceivingAddress; + // final cachedTransactions = {} as TransactionData?; + int latestTxnBlockHeight = 0; + + // final priceData = + // await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = Decimal.zero; + final List> midSortedArray = []; + + AddressTransaction txs = + await fetchAddressTransactions(thisAddress, "tokentx"); + + if (txs.message == "OK") { + final allTxs = txs.result; + allTxs.forEach((element) { + Map midSortedTx = {}; + // create final tx map + midSortedTx["txid"] = element["hash"]; + int confirmations = int.parse(element['confirmations'].toString()); + + int transactionAmount = int.parse(element['value'].toString()); + int decimal = int.parse( + _tokenData["decimals"] as String); //Eth has up to 18 decimal places + final transactionAmountInDecimal = + transactionAmount / (pow(10, decimal)); + + //Convert to satoshi, default display for other coins + final satAmount = Format.decimalAmountToSatoshis( + Decimal.parse(transactionAmountInDecimal.toString()), coin); + + midSortedTx["confirmed_status"] = + (confirmations != 0) && (confirmations >= MINIMUM_CONFIRMATIONS); + midSortedTx["confirmations"] = confirmations; + midSortedTx["timestamp"] = element["timeStamp"]; + + if (checksumEthereumAddress(element["from"].toString()) == + thisAddress) { + midSortedTx["txType"] = "Sent"; + } else { + midSortedTx["txType"] = "Received"; + } + + midSortedTx["amount"] = satAmount; + final String worthNow = ((currentPrice * Decimal.fromInt(satAmount)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + + //Calculate fees (GasLimit * gasPrice) + int txFee = int.parse(element['gasPrice'].toString()) * + int.parse(element['gasUsed'].toString()); + final txFeeDecimal = txFee / (pow(10, decimal)); + + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + midSortedTx["aliens"] = []; + midSortedTx["fees"] = Format.decimalAmountToSatoshis( + Decimal.parse(txFeeDecimal.toString()), coin); + midSortedTx["address"] = element["to"]; + midSortedTx["inputSize"] = 1; + midSortedTx["outputSize"] = 1; + midSortedTx["inputs"] = []; + midSortedTx["outputs"] = []; + midSortedTx["height"] = int.parse(element['blockNumber'].toString()); + + midSortedArray.add(midSortedTx); + }); + } + + midSortedArray.sort((a, b) => + (int.parse(b['timestamp'].toString())) - + (int.parse(a['timestamp'].toString()))); + + // buildDateTimeChunks + final Map result = {"dateTimeChunks": []}; + final dateArray = []; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = + extractDateFromTimestamp(int.parse(txObject['timestamp'].toString())); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp( + int.parse(chunk['timestamp'].toString())) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = >[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + // final transactionsMap = {} as Map; + // transactionsMap + // .addAll(TransactionData.fromJson(result).getAllTransactions()); + final txModel = TransactionData.fromMap( + TransactionData.fromJson(result).getAllTransactions()); + + cachedTxData = txModel; + return txModel; + } + @override bool validateAddress(String address) { return isValidEthereumAddress(address); } + Future getCurrentNode() async { + return NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } + Future getEthClient() async { - return Web3Client(rpcUrl, Client()); + final node = await getCurrentNode(); + return Web3Client(node.host, Client()); } } diff --git a/lib/services/tokens/token_service.dart b/lib/services/tokens/token_service.dart index c19897b08..ca515f43c 100644 --- a/lib/services/tokens/token_service.dart +++ b/lib/services/tokens/token_service.dart @@ -19,7 +19,7 @@ abstract class TokenServiceAPI { return EthereumToken( tokenData: tokenData, walletMnemonic: walletMnemonic, - // secureStore: secureStorageInterface, + secureStore: secureStorageInterface, // tracker: tracker, ); } diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index 5a221f828..627c8f96b 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -4,24 +4,23 @@ import 'dart:math'; import 'package:http/http.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'flutter_secure_storage_interface.dart'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import "package:hex/hex.dart"; -class AccountModule { +class AddressTransaction { final String message; final List result; final String status; - const AccountModule({ + const AddressTransaction({ required this.message, required this.result, required this.status, }); - factory AccountModule.fromJson(Map json) { - return AccountModule( + factory AddressTransaction.fromJson(Map json) { + return AddressTransaction( message: json['message'] as String, result: json['result'] as List, status: json['status'] as String, @@ -30,32 +29,40 @@ class AccountModule { } class GasTracker { - final int code; - final Map data; + final double average; + final double fast; + final double slow; + // final Map data; const GasTracker({ - required this.code, - required this.data, + required this.average, + required this.fast, + required this.slow, }); factory GasTracker.fromJson(Map json) { return GasTracker( - code: json['code'] as int, - data: json['data'] as Map, + average: json['average'] as double, + fast: json['fast'] as double, + slow: json['slow'] as double, ); } } -// const blockExplorer = "https://blockscout.com/eth/mainnet/api"; -const blockExplorer = "https://api.etherscan.io/api"; +const blockExplorer = "https://blockscout.com/eth/mainnet/api"; +const abiUrl = + "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update const _hdPath = "m/44'/60'/0'/0"; -const _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; +const _gasTrackerUrl = + "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; -Future fetchAccountModule(String action, String address) async { +Future fetchAddressTransactions( + String address, String action) async { final response = await get(Uri.parse( - "${blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + "$blockExplorer?module=account&action=$action&address=$address")); + if (response.statusCode == 200) { - return AccountModule.fromJson( + return AddressTransaction.fromJson( json.decode(response.body) as Map); } else { throw Exception('Failed to load transactions'); @@ -63,7 +70,8 @@ Future fetchAccountModule(String action, String address) async { } Future> getWalletTokens(String address) async { - AccountModule tokens = await fetchAccountModule("tokentx", address); + AddressTransaction tokens = + await fetchAddressTransactions(address, "tokentx"); List tokensList = []; var tokenMap = {}; if (tokens.message == "OK") { @@ -110,7 +118,6 @@ String getPrivateKey(String mnemonic) { Future getGasOracle() async { final response = await get(Uri.parse(_gasTrackerUrl)); - if (response.statusCode == 200) { return GasTracker.fromJson( json.decode(response.body) as Map); @@ -121,14 +128,17 @@ Future getGasOracle() async { Future getFees() async { GasTracker fees = await getGasOracle(); - final feesMap = fees.data; + final feesFast = fees.fast * (pow(10, 9)); + final feesStandard = fees.average * (pow(10, 9)); + final feesSlow = fees.slow * (pow(10, 9)); + return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 3, numberOfBlocksSlow: 3, - fast: feesMap['fast'] as int, - medium: feesMap['standard'] as int, - slow: feesMap['slow'] as int); + fast: feesFast.toInt(), + medium: feesStandard.toInt(), + slow: feesSlow.toInt()); } double estimateFee(int feeRate, int gasLimit, int decimals) { From d53709c7b0bfbf5c974226c23c75e1fbf5377485 Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 27 Jan 2023 16:32:03 +0200 Subject: [PATCH 031/208] Remove balanceMinusMaxFee for token since fees paid in ETH and send full balance on sendAll --- lib/services/tokens/ethereum/ethereum_token.dart | 11 ++--------- lib/services/tokens/token_service.dart | 1 - 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/tokens/ethereum/ethereum_token.dart index 8998acaea..c3ee0f3cc 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -102,13 +102,6 @@ class EthereumToken extends TokenServiceAPI { return await totalBalance; } - @override - Future get balanceMinusMaxFee async => - (await availableBalance) - - (Decimal.fromInt((await maxFee)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(); - @override Coin get coin => Coin.ethereum; @@ -263,8 +256,8 @@ class EthereumToken extends TokenServiceAPI { } if (isSendAll) { - //Subtract fee amount from send amount - satoshiAmount -= feeEstimate; + //Send the full balance + satoshiAmount = balance; } Map txData = { diff --git a/lib/services/tokens/token_service.dart b/lib/services/tokens/token_service.dart index ca515f43c..b2cf4c2aa 100644 --- a/lib/services/tokens/token_service.dart +++ b/lib/services/tokens/token_service.dart @@ -45,7 +45,6 @@ abstract class TokenServiceAPI { Future get availableBalance; Future get totalBalance; - Future get balanceMinusMaxFee; Future> get allOwnAddresses; From f7e2568e6aeb6766ca28eb288f5e96f7e813387a Mon Sep 17 00:00:00 2001 From: likho Date: Mon, 30 Jan 2023 15:44:30 +0200 Subject: [PATCH 032/208] Add validation for custom addresses and clean up --- .../coins/ethereum/ethereum_wallet.dart | 40 +------- .../tokens/ethereum/ethereum_token.dart | 97 ++++++++++--------- lib/services/tokens/token_service.dart | 7 +- lib/utilities/eth_commons.dart | 20 ++-- 4 files changed, 65 insertions(+), 99 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index ff37014d5..9adf47246 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -741,45 +741,7 @@ class EthereumWallet extends CoinServiceAPI { @override Future updateSentCachedTxData(Map txData) async { - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - 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!); - } + //Only used for Electrumx coins } @override diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/tokens/ethereum/ethereum_token.dart index c3ee0f3cc..c19ac7bbb 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -1,6 +1,5 @@ import 'dart:convert'; import 'dart:math'; -import 'package:devicelocale/devicelocale.dart'; import 'package:http/http.dart'; import 'package:decimal/decimal.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; @@ -42,6 +41,26 @@ class AbiRequestResponse { } } +class TokenData { + final String message; + final Map result; + final String status; + + const TokenData({ + required this.message, + required this.result, + required this.status, + }); + + factory TokenData.fromJson(Map json) { + return TokenData( + message: json['message'] as String, + result: json['result'] as Map, + status: json['status'] as String, + ); + } +} + const int MINIMUM_CONFIRMATIONS = 3; class EthereumToken extends TokenServiceAPI { @@ -81,7 +100,7 @@ class EthereumToken extends TokenServiceAPI { return AbiRequestResponse.fromJson( json.decode(response.body) as Map); } else { - throw Exception('Failed to load token abi'); + throw Exception("ERROR GETTING TOKENABI ${response.reasonPhrase}"); } } @@ -185,6 +204,8 @@ class EthereumToken extends TokenServiceAPI { _balanceFunction = _contract.function('balanceOf'); _sendFunction = _contract.function('transfer'); _client = await getEthClient(); + + // print(_credentials.p) } @override @@ -294,47 +315,6 @@ class EthereumToken extends TokenServiceAPI { _transactionData ??= _fetchTransactionData(); Future? _transactionData; - @override - Future updateSentCachedTxData(Map txData) async { - Decimal currentPrice = Decimal.zero; - 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!); - } - } - Future _fetchTransactionData() async { String thisAddress = await currentReceivingAddress; // final cachedTransactions = {} as TransactionData?; @@ -440,9 +420,6 @@ class EthereumToken extends TokenServiceAPI { } } - // final transactionsMap = {} as Map; - // transactionsMap - // .addAll(TransactionData.fromJson(result).getAllTransactions()); final txModel = TransactionData.fromMap( TransactionData.fromJson(result).getAllTransactions()); @@ -455,6 +432,34 @@ class EthereumToken extends TokenServiceAPI { return isValidEthereumAddress(address); } + //Validate that a custom token is valid and is ERC-20, a token will be valid + @override + Future getTokenByContractAddress(String contractAddress) async { + final response = await get(Uri.parse( + "$blockExplorer?module=token&action=getToken&contractaddress=$contractAddress")); + if (response.statusCode == 200) { + return TokenData.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception("ERROR GETTING TOKEN ${response.reasonPhrase}"); + } + } + + //Validate that a custom token is valid and is ERC-20 + @override + Future isValidToken(String contractAddress) async { + TokenData tokenData = await getTokenByContractAddress(contractAddress); + + if (tokenData.message == "OK") { + final result = tokenData.result; + if (result["type"] == "ERC-20") { + return true; + } + return false; + } + return false; + } + Future getCurrentNode() async { return NodeService(secureStorageInterface: _secureStore) .getPrimaryNodeFor(coin: coin) ?? diff --git a/lib/services/tokens/token_service.dart b/lib/services/tokens/token_service.dart index b2cf4c2aa..f243a769c 100644 --- a/lib/services/tokens/token_service.dart +++ b/lib/services/tokens/token_service.dart @@ -41,7 +41,6 @@ abstract class TokenServiceAPI { Future get maxFee; Future get currentReceivingAddress; - // Future get currentLegacyReceivingAddress; Future get availableBalance; Future get totalBalance; @@ -52,9 +51,6 @@ abstract class TokenServiceAPI { Future refresh(); - // String get walletName; - // String get walletId; - bool validateAddress(String address); Future initializeNew(); @@ -62,6 +58,5 @@ abstract class TokenServiceAPI { Future estimateFeeFor(int satoshiAmount, int feeRate); - // used for electrumx coins - Future updateSentCachedTxData(Map txData); + Future isValidToken(String contractAddress); } diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index 627c8f96b..d31fbbccb 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -58,14 +58,18 @@ const _gasTrackerUrl = Future fetchAddressTransactions( String address, String action) async { - final response = await get(Uri.parse( - "$blockExplorer?module=account&action=$action&address=$address")); - - if (response.statusCode == 200) { - return AddressTransaction.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception('Failed to load transactions'); + try { + final response = await get(Uri.parse( + "$blockExplorer?module=account&action=$action&address=$address")); + if (response.statusCode == 200) { + return AddressTransaction.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception( + 'ERROR GETTING TRANSACTIONS WITH STATUS ${response.statusCode}'); + } + } catch (e, s) { + throw Exception('ERROR GETTING TRANSACTIONS ${e.toString()}'); } } From 15b37b6f5c4505d974ae1ea7908144d5843c1d4a Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 14 Feb 2023 11:43:48 -0600 Subject: [PATCH 033/208] merge clean up and update eth to use isar for tx history and addresses --- assets/images/{ => dark}/ethereum.png | Bin assets/images/forest/ethereum.png | Bin 0 -> 351492 bytes assets/images/fruitSorbet/ethereum.png | Bin 0 -> 351492 bytes assets/images/light/ethereum.png | Bin 0 -> 351492 bytes assets/images/oceanBreeze/ethereum.png | Bin 0 -> 351492 bytes assets/images/oledBlack/ethereum.png | Bin 0 -> 351492 bytes .../isar/models/blockchain_data/address.dart | 1 + .../add_edit_node_view.dart | 4 +- .../sub_widgets/token_summary_info.dart | 18 +- lib/pages/token_view/token_view.dart | 85 +- .../sub_widgets/wallet_navigation_bar.dart | 122 +-- lib/pages/wallet_view/wallet_view.dart | 45 +- .../coins/bitcoincash/bitcoincash_wallet.dart | 13 +- .../coins/ethereum/ethereum_wallet.dart | 774 +++++++++--------- .../tokens/ethereum/ethereum_token.dart | 27 +- lib/utilities/assets.dart | 4 +- lib/utilities/constants.dart | 16 +- lib/utilities/enums/coin_enum.dart | 1 + .../enums/derive_path_type_enum.dart | 4 + lib/utilities/eth_commons.dart | 23 +- pubspec.lock | 7 + 21 files changed, 527 insertions(+), 617 deletions(-) rename assets/images/{ => dark}/ethereum.png (100%) create mode 100644 assets/images/forest/ethereum.png create mode 100644 assets/images/fruitSorbet/ethereum.png create mode 100644 assets/images/light/ethereum.png create mode 100644 assets/images/oceanBreeze/ethereum.png create mode 100644 assets/images/oledBlack/ethereum.png 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 0000000000000000000000000000000000000000..2827ad56e3350a25f1d85d012e5a1f41128db9cb GIT binary patch literal 351492 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¨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

dt!HjOSSY=_?;tPX-y6MVa(@beAhsLSevT!68PR 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Ν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


O|qi3rtjnYTMcUa(qM7f(Mh5= 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 g0)2z5FoUiS(;ZvQR3Qjj(IfSzs56Nq7Yri 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¨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

dt!HjOSSY=_?;tPX-y6MVa(@beAhsLSevT!68PR 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Ν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


O|qi3rtjnYTMcUa(qM7f(Mh5= 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 g0)2z5FoUiS(;ZvQR3Qjj(IfSzs56Nq7Yri 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¨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

dt!HjOSSY=_?;tPX-y6MVa(@beAhsLSevT!68PR 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Ν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


O|qi3rtjnYTMcUa(qM7f(Mh5= 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 g0)2z5FoUiS(;ZvQR3Qjj(IfSzs56Nq7Yri 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¨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

dt!HjOSSY=_?;tPX-y6MVa(@beAhsLSevT!68PR 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Ν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


O|qi3rtjnYTMcUa(qM7f(Mh5= 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 g0)2z5FoUiS(;ZvQR3Qjj(IfSzs56Nq7Yri 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¨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

dt!HjOSSY=_?;tPX-y6MVa(@beAhsLSevT!68PR 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Ν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


O|qi3rtjnYTMcUa(qM7f(Mh5= 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 g0)2z5FoUiS(;ZvQR3Qjj(IfSzs56Nq7Yri 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 { 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: From da2607cbc6aeedb795d4f302c1457226c9a7f220 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Tue, 21 Feb 2023 15:04:52 -0600 Subject: [PATCH 034/208] update ios deployment target --- ios/Podfile | 2 +- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index 9411102b1..f17bddc91 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '10.0' +platform :ios, '15.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c935c514e..17990457d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -439,7 +439,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -574,7 +574,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -624,7 +624,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; From 580467a302c2fdbdb3dd44ae06b580ec20df5a2b Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Wed, 22 Feb 2023 16:09:34 -0600 Subject: [PATCH 035/208] framework info min ios version --- ios/Flutter/AppFrameworkInfo.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 3158eba31..7b1095736 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 10.0 + 15.0 From 2ef56ac66b748552d61a6ee91c9729be09bfcd63 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Feb 2023 14:36:23 -0600 Subject: [PATCH 036/208] temp eth asset fix --- lib/utilities/assets.dart | 2 ++ pubspec.lock | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 8530dea80..6be741e01 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -327,6 +327,8 @@ class _SVG { return firoImage(context); case Coin.dogecoinTestNet: return dogecoinImage(context); + case Coin.ethereum: + return ethereum; } } } diff --git a/pubspec.lock b/pubspec.lock index 349ab50df..0f83b1eca 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -459,6 +459,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + ethereum_addresses: + dependency: "direct main" + description: + name: ethereum_addresses + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" event_bus: dependency: "direct main" description: @@ -839,6 +846,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.8.0" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" jsonrpc2: dependency: "direct main" description: @@ -1399,6 +1413,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: @@ -1616,6 +1637,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + web3dart: + dependency: "direct main" + description: + name: web3dart + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.5" web_socket_channel: dependency: transitive description: From 5aed55235c19ceedf323f0cd383c8a33c24555df Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Feb 2023 16:59:58 -0600 Subject: [PATCH 037/208] WIP eth refactor --- lib/models/ethereum/erc20_token.dart | 11 + lib/models/ethereum/erc721_token.dart | 11 + lib/models/ethereum/eth_token.dart | 15 + .../models/blockchain_data/transaction.dart | 3 +- lib/pages/token_view/my_tokens_view.dart | 26 +- .../sub_widgets/my_token_select_item.dart | 40 ++- .../sub_widgets/my_tokens_list.dart | 5 +- lib/pages/token_view/token_view.dart | 15 +- lib/pages/wallet_view/wallet_view.dart | 52 +++- lib/route_generator.dart | 16 +- .../coins/ethereum/ethereum_wallet.dart | 12 +- lib/services/ethereum/ethereum_api.dart | 263 ++++++++++++++++++ .../ethereum_token_service.dart} | 179 ++---------- lib/services/tokens/token_service.dart | 62 ----- lib/utilities/eth_commons.dart | 84 ------ lib/utilities/format.dart | 7 + lib/utilities/show_loading.dart | 43 +++ 17 files changed, 470 insertions(+), 374 deletions(-) create mode 100644 lib/models/ethereum/erc20_token.dart create mode 100644 lib/models/ethereum/erc721_token.dart create mode 100644 lib/models/ethereum/eth_token.dart create mode 100644 lib/services/ethereum/ethereum_api.dart rename lib/services/{tokens/ethereum/ethereum_token.dart => ethereum/ethereum_token_service.dart} (67%) delete mode 100644 lib/services/tokens/token_service.dart create mode 100644 lib/utilities/show_loading.dart diff --git a/lib/models/ethereum/erc20_token.dart b/lib/models/ethereum/erc20_token.dart new file mode 100644 index 000000000..a7c4508ee --- /dev/null +++ b/lib/models/ethereum/erc20_token.dart @@ -0,0 +1,11 @@ +import 'package:stackwallet/models/ethereum/eth_token.dart'; + +class Erc20Token extends EthToken { + Erc20Token({ + required super.contractAddress, + required super.name, + required super.symbol, + required super.decimals, + required super.balance, + }); +} diff --git a/lib/models/ethereum/erc721_token.dart b/lib/models/ethereum/erc721_token.dart new file mode 100644 index 000000000..f06d1572d --- /dev/null +++ b/lib/models/ethereum/erc721_token.dart @@ -0,0 +1,11 @@ +import 'package:stackwallet/models/ethereum/eth_token.dart'; + +class Erc721Token extends EthToken { + Erc721Token({ + required super.contractAddress, + required super.name, + required super.symbol, + required super.decimals, + required super.balance, + }); +} diff --git a/lib/models/ethereum/eth_token.dart b/lib/models/ethereum/eth_token.dart new file mode 100644 index 000000000..748fac307 --- /dev/null +++ b/lib/models/ethereum/eth_token.dart @@ -0,0 +1,15 @@ +class EthToken { + EthToken({ + required this.contractAddress, + required this.name, + required this.symbol, + required this.decimals, + required this.balance, + }); + + final String contractAddress; + final String name; + final String symbol; + final int decimals; + final int balance; +} diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index 191f62b46..538504748 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -207,5 +207,6 @@ enum TransactionSubType { none, bip47Notification, // bip47 payment code notification transaction flag mint, // firo specific - join; // firo specific + join, // firo specific + ethToken; // eth token } diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index f6e20d6fe..bd54a5283 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -3,30 +3,22 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/pages/token_view/all_tokens_view.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_tokens_list.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; - +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; - -import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; - -import 'package:stackwallet/pages/token_view/all_tokens_view.dart'; - -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/widgets/conditional_parent.dart'; - +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; -import 'package:stackwallet/utilities/eth_commons.dart'; - -import 'package:stackwallet/services/coins/manager.dart'; class MyTokensView extends ConsumerStatefulWidget { const MyTokensView({ @@ -41,7 +33,7 @@ class MyTokensView extends ConsumerStatefulWidget { final ChangeNotifierProvider managerProvider; final String walletId; final String walletAddress; - final List tokens; + final List tokens; @override ConsumerState createState() => _TokenDetailsViewState(); diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 3fcc75619..aee7cacc4 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -1,49 +1,47 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; -import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; - import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; - -import 'package:stackwallet/services/coins/manager.dart'; - class MyTokenSelectItem extends ConsumerWidget { const MyTokenSelectItem( {Key? key, required this.managerProvider, required this.walletId, required this.walletAddress, - required this.tokenData, + required this.token, required}) : super(key: key); final ChangeNotifierProvider managerProvider; final String walletId; final String walletAddress; - final Map tokenData; + final EthToken token; @override Widget build(BuildContext context, WidgetRef ref) { - int balance = tokenData["balance"] as int; - int tokenDecimals = int.parse(tokenData["decimals"] as String); - final balanceInDecimal = (balance / (pow(10, tokenDecimals))); + final balanceInDecimal = Format.satoshisToEthTokenAmount( + token.balance, + token.decimals, + ); return RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: MaterialButton( // splashColor: Theme.of(context).extension()!.highlight, - key: Key("walletListItemButtonKey_${tokenData["symbol"]}"), + key: Key("walletListItemButtonKey_${token.symbol}"), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( @@ -53,8 +51,8 @@ class MyTokenSelectItem extends ConsumerWidget { onPressed: () { final mnemonicList = ref.read(managerProvider).mnemonic; - final token = EthereumToken( - tokenData: tokenData, + final tokenService = EthereumTokenService( + token: token, walletMnemonic: mnemonicList, secureStore: ref.read(secureStoreProvider)); @@ -62,11 +60,11 @@ class MyTokenSelectItem extends ConsumerWidget { TokenView.routeName, arguments: Tuple4( walletId, - tokenData, + token, ref .read(walletsChangeNotifierProvider) .getManagerProvider(walletId), - token), + tokenService), ); }, @@ -89,12 +87,12 @@ class MyTokenSelectItem extends ConsumerWidget { Row( children: [ Text( - tokenData["name"] as String, + token.name, style: STextStyles.titleBold12(context), ), const Spacer(), Text( - "$balanceInDecimal ${tokenData["symbol"]}", + "$balanceInDecimal ${token.symbol}", style: STextStyles.itemSubtitle(context), ), ], @@ -105,7 +103,7 @@ class MyTokenSelectItem extends ConsumerWidget { Row( children: [ Text( - tokenData["symbol"] as String, + token.symbol, style: STextStyles.itemSubtitle(context), ), const Spacer(), diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart index 03731c89a..a1a2a22ca 100644 --- a/lib/pages/token_view/sub_widgets/my_tokens_list.dart +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -14,7 +15,7 @@ class MyTokensList extends StatelessWidget { final ChangeNotifierProvider managerProvider; final String walletId; - final List tokens; + final List tokens; final String walletAddress; @override @@ -31,7 +32,7 @@ class MyTokensList extends StatelessWidget { managerProvider: managerProvider, walletId: walletId, walletAddress: walletAddress, - tokenData: tokens[index] as Map, + token: tokens[index], ), ); }, diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index cb0cc8bc3..29d18bafc 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -4,6 +4,7 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; @@ -12,10 +13,10 @@ import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; @@ -32,9 +33,9 @@ class TokenView extends ConsumerStatefulWidget { const TokenView({ Key? key, required this.walletId, - required this.tokenData, - required this.managerProvider, required this.token, + required this.managerProvider, + required this.tokenService, this.eventBus, }) : super(key: key); @@ -42,9 +43,9 @@ class TokenView extends ConsumerStatefulWidget { static const double navBarHeight = 65.0; final String walletId; - final Map tokenData; + final EthToken token; final ChangeNotifierProvider managerProvider; - final EthereumToken token; + final EthereumTokenService tokenService; final EventBus? eventBus; @override @@ -189,7 +190,7 @@ class _TokenViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - widget.token.initializeExisting(); + widget.tokenService.initializeExisting(); // print("MY TOTAL BALANCE IS ${widget.token.totalBalance}"); final coin = ref.watch(managerProvider.select((value) => value.coin)); @@ -221,7 +222,7 @@ class _TokenViewState extends ConsumerState { ), Expanded( child: Text( - widget.tokenData["name"] as String, + widget.token.name, style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 58e07bc5f..c8b832875 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -5,6 +5,7 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; @@ -27,6 +28,7 @@ import 'package:stackwallet/providers/wallet/public_private_balance_state_provid import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; @@ -35,7 +37,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; -import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -783,21 +785,43 @@ class _WalletViewState extends ConsumerState { .read(managerProvider) .currentReceivingAddress; - List tokens = - await getWalletTokens(await ref - .read(managerProvider) - .currentReceivingAddress); + final response = await showLoading< + EthereumResponse>>( + whileFuture: + EthereumAPI.getWalletTokens( + address: await ref + .read(managerProvider) + .currentReceivingAddress), + message: "Loading tokens", + isDesktop: false, + context: context, + ); if (mounted) { - await Navigator.of(context).pushNamed( - MyTokensView.routeName, - arguments: Tuple4( - managerProvider, - walletId, - walletAddress, - tokens, - ), - ); + if (response.value != null) { + await Navigator.of(context) + .pushNamed( + MyTokensView.routeName, + arguments: Tuple4( + managerProvider, + walletId, + walletAddress, + response.value!, + ), + ); + } else { + await showDialog( + context: context, + builder: (context) { + return StackOkDialog( + title: + "Failed to fetch tokens", + message: response.exception + .toString(), + ); + }, + ); + } } }, ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index b1d05b357..60375e881 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -4,8 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; @@ -126,15 +128,13 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/nodes_ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:tuple/tuple.dart'; -import 'models/isar/models/blockchain_data/transaction.dart'; - class RouteGenerator { static const bool useMaterialPageRoute = true; @@ -1431,7 +1431,7 @@ class RouteGenerator { case MyTokensView.routeName: if (args is Tuple4, String, String, - List>) { + List>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => MyTokensView( @@ -1461,15 +1461,15 @@ class RouteGenerator { // } case TokenView.routeName: - if (args is Tuple4, - ChangeNotifierProvider, EthereumToken>) { + if (args is Tuple4, + EthereumTokenService>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => TokenView( walletId: args.item1, - tokenData: args.item2, + token: args.item2, managerProvider: args.item3, - token: args.item4, + tokenService: args.item4, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index e5cb6f5de..d7fb3616e 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -11,6 +11,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; @@ -206,9 +207,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { Future get fees => _feeObject ??= _getFees(); Future? _feeObject; - Future _getFees() async { - return await getFees(); - } + Future _getFees() => EthereumAPI.getFees(); //Full rescan is not needed for ETH since we have a balance @override @@ -536,8 +535,9 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { } if (!needsRefresh) { var allOwnAddresses = await _fetchAllOwnAddresses(); - AddressTransaction addressTransactions = await fetchAddressTransactions( - allOwnAddresses.elementAt(0).value, "txlist"); + AddressTransaction addressTransactions = + await EthereumAPI.fetchAddressTransactions( + allOwnAddresses.elementAt(0).value, "txlist"); if (addressTransactions.message == "OK") { final allTxs = addressTransactions.result; for (final element in allTxs) { @@ -812,7 +812,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { String thisAddress = await currentReceivingAddress; AddressTransaction txs = - await fetchAddressTransactions(thisAddress, "txlist"); + await EthereumAPI.fetchAddressTransactions(thisAddress, "txlist"); if (txs.message == "OK") { final allTxs = txs.result; diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart new file mode 100644 index 000000000..e1ee0784d --- /dev/null +++ b/lib/services/ethereum/ethereum_api.dart @@ -0,0 +1,263 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'package:http/http.dart'; +import 'package:stackwallet/models/ethereum/erc721_token.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/utilities/logger.dart'; + +import '../../models/ethereum/erc20_token.dart'; +import '../../models/ethereum/eth_token.dart'; + +class AbiRequestResponse { + final String message; + final String result; + final String status; + + const AbiRequestResponse({ + required this.message, + required this.result, + required this.status, + }); + + factory AbiRequestResponse.fromJson(Map json) { + return AbiRequestResponse( + message: json['message'] as String, + result: json['result'] as String, + status: json['status'] as String, + ); + } +} + +class EthereumResponse { + final T? value; + final Exception? exception; + + EthereumResponse(this.value, this.exception); +} + +abstract class EthereumAPI { + static const blockExplorer = "https://blockscout.com/eth/mainnet/api"; + static const abiUrl = + "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update + + static const gasTrackerUrl = + "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; + + static Future fetchAddressTransactions( + String address, String action) async { + try { + final response = await get(Uri.parse( + "$blockExplorer?module=account&action=$action&address=$address")); + if (response.statusCode == 200) { + return AddressTransaction.fromJson( + jsonDecode(response.body) as Map); + } else { + throw Exception( + 'ERROR GETTING TRANSACTIONS WITH STATUS ${response.statusCode}'); + } + } catch (e, s) { + throw Exception('ERROR GETTING TRANSACTIONS ${e.toString()}'); + } + } + + static Future>> getWalletTokens({ + required String address, + }) async { + try { + final uri = Uri.parse( + "$blockExplorer?module=account&action=tokenlist&address=$address", + ); + final response = await get(uri); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json["message"] == "OK") { + final result = + List>.from(json["result"] as List); + final List tokens = []; + for (final map in result) { + if (map["type"] == "ERC-20") { + tokens.add( + Erc20Token( + balance: int.parse(map["balance"] as String), + contractAddress: map["contractAddress"] as String, + decimals: int.parse(map["decimals"] as String), + name: map["name"] as String, + symbol: map["symbol"] as String, + ), + ); + } else if (map["type"] == "ERC-721") { + tokens.add( + Erc721Token( + balance: int.parse(map["balance"] as String), + contractAddress: map["contractAddress"] as String, + decimals: int.parse(map["decimals"] as String), + name: map["name"] as String, + symbol: map["symbol"] as String, + ), + ); + } else { + throw Exception("Unsupported token type found: ${map["type"]}"); + } + } + + return EthereumResponse( + tokens, + null, + ); + } else { + throw Exception(json["message"] as String); + } + } else { + throw Exception( + "getWalletTokens($address) failed with status code: " + "${response.statusCode}", + ); + } + } catch (e, s) { + Logging.instance.log( + "getWalletTokens(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + Exception(e.toString()), + ); + } + } + + static Future> getWalletTokenTransactions( + String address) async { + AddressTransaction tokens = + await fetchAddressTransactions(address, "tokentx"); + List tokensList = []; + var tokenMap = {}; + if (tokens.message == "OK") { + final allTxs = tokens.result; + allTxs.forEach((element) { + print("========================================================="); + print("THING: $element"); + print("========================================================="); + + String key = element["tokenSymbol"] as String; + tokenMap[key] = {}; + tokenMap[key]["balance"] = 0; + + if (tokenMap.containsKey(key)) { + tokenMap[key]["contractAddress"] = + element["contractAddress"] as String; + tokenMap[key]["decimals"] = element["tokenDecimal"]; + tokenMap[key]["name"] = element["tokenName"]; + tokenMap[key]["symbol"] = element["tokenSymbol"]; + if (checksumEthereumAddress(address) == address) { + tokenMap[key]["balance"] += int.parse(element["value"] as String); + } else { + tokenMap[key]["balance"] -= int.parse(element["value"] as String); + } + } + }); + + tokenMap.forEach((key, value) { + tokensList.add(value as Map); + }); + return tokensList; + } + return []; + } + + static Future getGasOracle() async { + final response = await get(Uri.parse(gasTrackerUrl)); + if (response.statusCode == 200) { + return GasTracker.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception('Failed to load gas oracle'); + } + } + + static Future getFees() async { + GasTracker fees = await getGasOracle(); + final feesFast = fees.fast * (pow(10, 9)); + final feesStandard = fees.average * (pow(10, 9)); + final feesSlow = fees.slow * (pow(10, 9)); + + return FeeObject( + numberOfBlocksFast: 1, + numberOfBlocksAverage: 3, + numberOfBlocksSlow: 3, + fast: feesFast.toInt(), + medium: feesStandard.toInt(), + slow: feesSlow.toInt()); + } + + //Validate that a custom token is valid and is ERC-20, a token will be valid + static Future> getTokenByContractAddress( + String contractAddress) async { + try { + final response = await get(Uri.parse( + "$blockExplorer?module=token&action=getToken&contractaddress=$contractAddress")); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json["message"] == "OK") { + final map = Map.from(json["result"] as Map); + EthToken? token; + if (map["type"] == "ERC-20") { + token = Erc20Token( + balance: int.parse(map["balance"] as String), + contractAddress: map["contractAddress"] as String, + decimals: int.parse(map["decimals"] as String), + name: map["name"] as String, + symbol: map["symbol"] as String, + ); + } else if (map["type"] == "ERC-721") { + token = Erc721Token( + balance: int.parse(map["balance"] as String), + contractAddress: map["contractAddress"] as String, + decimals: int.parse(map["decimals"] as String), + name: map["name"] as String, + symbol: map["symbol"] as String, + ); + } else { + throw Exception("Unsupported token type found: ${map["type"]}"); + } + + return EthereumResponse( + token, + null, + ); + } else { + throw Exception(json["message"] as String); + } + } else { + throw Exception( + "getTokenByContractAddress($contractAddress) failed with status code: " + "${response.statusCode}", + ); + } + } catch (e, s) { + Logging.instance.log( + "getWalletTokens(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + Exception(e.toString()), + ); + } + } + + static Future fetchTokenAbi( + String contractAddress) async { + final response = await get(Uri.parse( + "$abiUrl?module=contract&action=getabi&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + if (response.statusCode == 200) { + return AbiRequestResponse.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception("ERROR GETTING TOKENABI ${response.reasonPhrase}"); + } + } +} diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/ethereum/ethereum_token_service.dart similarity index 67% rename from lib/services/tokens/ethereum/ethereum_token.dart rename to lib/services/ethereum/ethereum_token_service.dart index 5bcb2870e..ebc77f5f8 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -1,16 +1,15 @@ -import 'dart:convert'; import 'dart:math'; import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:http/http.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/services/tokens/token_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; @@ -20,55 +19,15 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart' as transaction; -class AbiRequestResponse { - final String message; - final String result; - final String status; - - const AbiRequestResponse({ - required this.message, - required this.result, - required this.status, - }); - - factory AbiRequestResponse.fromJson(Map json) { - return AbiRequestResponse( - message: json['message'] as String, - result: json['result'] as String, - status: json['status'] as String, - ); - } -} - -class TokenData { - final String message; - final Map result; - final String status; - - const TokenData({ - required this.message, - required this.result, - required this.status, - }); - - factory TokenData.fromJson(Map json) { - return TokenData( - message: json['message'] as String, - result: json['result'] as Map, - status: json['status'] as String, - ); - } -} - const int MINIMUM_CONFIRMATIONS = 3; -class EthereumToken extends TokenServiceAPI { - @override +class EthereumTokenService { + final EthToken token; + late bool shouldAutoSync; late EthereumAddress _contractAddress; late EthPrivateKey _credentials; late DeployedContract _contract; - late Map _tokenData; late ContractFunction _balanceFunction; late ContractFunction _sendFunction; late Future> _walletMnemonic; @@ -80,30 +39,16 @@ class EthereumToken extends TokenServiceAPI { final _gasLimit = 200000; - EthereumToken({ - required Map tokenData, + EthereumTokenService({ + required this.token, required Future> walletMnemonic, required SecureStorageInterface secureStore, }) { - _contractAddress = - EthereumAddress.fromHex(tokenData["contractAddress"] as String); + _contractAddress = EthereumAddress.fromHex(token.contractAddress); _walletMnemonic = walletMnemonic; - _tokenData = tokenData; _secureStore = secureStore; } - Future fetchTokenAbi() async { - final response = await get(Uri.parse( - "$abiUrl?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); - if (response.statusCode == 200) { - return AbiRequestResponse.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception("ERROR GETTING TOKENABI ${response.reasonPhrase}"); - } - } - - @override Future> get allOwnAddresses => _allOwnAddresses ??= _fetchAllOwnAddresses(); Future>? _allOwnAddresses; @@ -115,21 +60,18 @@ class EthereumToken extends TokenServiceAPI { return addresses; } - @override Future get availableBalance async { return await totalBalance; } - @override Coin get coin => Coin.ethereum; - @override Future confirmSend({required Map txData}) async { final amount = txData['recipientAmt']; final decimalAmount = Format.satoshisToAmount(amount as int, coin: Coin.ethereum); - final bigIntAmount = amountToBigInt( - decimalAmount.toDouble(), int.parse(_tokenData["decimals"] as String)); + final bigIntAmount = + amountToBigInt(decimalAmount.toDouble(), token.decimals); final sentTx = await _client.sendTransaction( _credentials, @@ -147,7 +89,6 @@ class EthereumToken extends TokenServiceAPI { return sentTx; } - @override Future get currentReceivingAddress async { final _currentReceivingAddress = await _credentials.extractAddress(); final checkSumAddress = @@ -155,40 +96,21 @@ class EthereumToken extends TokenServiceAPI { return checkSumAddress; } - @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { - final fee = estimateFee( - feeRate, _gasLimit, int.parse(_tokenData["decimals"] as String)); + final fee = estimateFee(feeRate, _gasLimit, token.decimals); return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin); } - @override Future get fees => _feeObject ??= _getFees(); Future? _feeObject; Future _getFees() async { - return await getFees(); + return await EthereumAPI.getFees(); } - @override Future initializeExisting() async { - if ((await _secureStore.read( - key: '${_contractAddress.toString()}_tokenAbi')) != - null) { - _tokenAbi = (await _secureStore.read( - key: '${_contractAddress.toString()}_tokenAbi'))!; - } else { - AbiRequestResponse abi = await fetchTokenAbi(); - //Fetch token ABI so we can call token functions - if (abi.message == "OK") { - _tokenAbi = abi.result; - //Store abi in secure store - await _secureStore.write( - key: '${_contractAddress.toString()}_tokenAbi', value: _tokenAbi); - } else { - throw Exception('Failed to load token abi'); - } - } + _tokenAbi = (await _secureStore.read( + key: '${_contractAddress.toString()}_tokenAbi'))!; final mnemonic = await _walletMnemonic; String mnemonicString = mnemonic.join(' '); @@ -199,8 +121,7 @@ class EthereumToken extends TokenServiceAPI { _credentials = EthPrivateKey.fromHex(privateKey); _contract = DeployedContract( - ContractAbi.fromJson(_tokenAbi, _tokenData["name"] as String), - _contractAddress); + ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); _balanceFunction = _contract.function('balanceOf'); _sendFunction = _contract.function('transfer'); _client = await getEthClient(); @@ -208,15 +129,15 @@ class EthereumToken extends TokenServiceAPI { // print(_credentials.p) } - @override Future initializeNew() async { - AbiRequestResponse abi = await fetchTokenAbi(); + AbiRequestResponse abi = + await EthereumAPI.fetchTokenAbi(_contractAddress.hex); //Fetch token ABI so we can call token functions if (abi.message == "OK") { _tokenAbi = abi.result; //Store abi in secure store await _secureStore.write( - key: '${_contractAddress.toString()}_tokenAbi', value: _tokenAbi); + key: '${_contractAddress.hex}_tokenAbi', value: _tokenAbi); } else { throw Exception('Failed to load token abi'); } @@ -230,25 +151,21 @@ class EthereumToken extends TokenServiceAPI { _credentials = EthPrivateKey.fromHex(privateKey); _contract = DeployedContract( - ContractAbi.fromJson(_tokenAbi, _tokenData["name"] as String), - _contractAddress); + ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); _balanceFunction = _contract.function('balanceOf'); _sendFunction = _contract.function('transfer'); _client = await getEthClient(); } - @override // TODO: implement isRefreshing bool get isRefreshing => throw UnimplementedError(); - @override Future get maxFee async { final fee = (await fees).fast; final feeEstimate = await estimateFeeFor(0, fee); return feeEstimate; } - @override Future> prepareSend( {required String address, required int satoshiAmount, @@ -292,13 +209,11 @@ class EthereumToken extends TokenServiceAPI { return txData; } - @override Future refresh() { // TODO: implement refresh throw UnimplementedError(); } - @override Future get totalBalance async { final balanceRequest = await _client.call( contract: _contract, @@ -306,40 +221,35 @@ class EthereumToken extends TokenServiceAPI { params: [_credentials.address]); String balance = balanceRequest.first.toString(); - int tokenDecimals = int.parse(_tokenData["decimals"] as String); - final balanceInDecimal = (int.parse(balance) / (pow(10, tokenDecimals))); + final balanceInDecimal = Format.satoshisToEthTokenAmount( + int.parse(balance), + token.decimals, + ); return Decimal.parse(balanceInDecimal.toString()); } - @override Future get transactionData => _transactionData ??= _fetchTransactionData(); Future? _transactionData; Future _fetchTransactionData() async { String thisAddress = await currentReceivingAddress; - // final cachedTransactions = {} as TransactionData?; - int latestTxnBlockHeight = 0; - // final priceData = - // await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = Decimal.zero; final List> midSortedArray = []; AddressTransaction txs = - await fetchAddressTransactions(thisAddress, "tokentx"); + await EthereumAPI.fetchAddressTransactions(thisAddress, "tokentx"); if (txs.message == "OK") { final allTxs = txs.result; - allTxs.forEach((element) { + for (var element in allTxs) { Map midSortedTx = {}; // create final tx map midSortedTx["txid"] = element["hash"]; int confirmations = int.parse(element['confirmations'].toString()); int transactionAmount = int.parse(element['value'].toString()); - int decimal = int.parse( - _tokenData["decimals"] as String); //Eth has up to 18 decimal places + int decimal = token.decimals; //Eth has up to 18 decimal places final transactionAmountInDecimal = transactionAmount / (pow(10, decimal)); @@ -360,18 +270,12 @@ class EthereumToken extends TokenServiceAPI { } midSortedTx["amount"] = satAmount; - final String worthNow = ((currentPrice * Decimal.fromInt(satAmount)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(scaleOnInfinitePrecision: 2) - .toStringAsFixed(2); //Calculate fees (GasLimit * gasPrice) int txFee = int.parse(element['gasPrice'].toString()) * int.parse(element['gasUsed'].toString()); final txFeeDecimal = txFee / (pow(10, decimal)); - midSortedTx["worthNow"] = worthNow; - midSortedTx["worthAtBlockTimestamp"] = worthNow; midSortedTx["aliens"] = []; midSortedTx["fees"] = Format.decimalAmountToSatoshis( Decimal.parse(txFeeDecimal.toString()), coin); @@ -383,7 +287,7 @@ class EthereumToken extends TokenServiceAPI { midSortedTx["height"] = int.parse(element['blockNumber'].toString()); midSortedArray.add(midSortedTx); - }); + } } midSortedArray.sort((a, b) => @@ -428,39 +332,10 @@ class EthereumToken extends TokenServiceAPI { return txModel; } - @override bool validateAddress(String address) { return isValidEthereumAddress(address); } - //Validate that a custom token is valid and is ERC-20, a token will be valid - @override - Future getTokenByContractAddress(String contractAddress) async { - final response = await get(Uri.parse( - "$blockExplorer?module=token&action=getToken&contractaddress=$contractAddress")); - if (response.statusCode == 200) { - return TokenData.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception("ERROR GETTING TOKEN ${response.reasonPhrase}"); - } - } - - //Validate that a custom token is valid and is ERC-20 - @override - Future isValidToken(String contractAddress) async { - TokenData tokenData = await getTokenByContractAddress(contractAddress); - - if (tokenData.message == "OK") { - final result = tokenData.result; - if (result["type"] == "ERC-20") { - return true; - } - return false; - } - return false; - } - Future getCurrentNode() async { return NodeService(secureStorageInterface: _secureStore) .getPrimaryNodeFor(coin: coin) ?? diff --git a/lib/services/tokens/token_service.dart b/lib/services/tokens/token_service.dart deleted file mode 100644 index f243a769c..000000000 --- a/lib/services/tokens/token_service.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:decimal/decimal.dart'; -import 'package:stackwallet/models/models.dart'; -import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/prefs.dart'; - -abstract class TokenServiceAPI { - TokenServiceAPI(); - - factory TokenServiceAPI.from( - Map tokenData, - Future> walletMnemonic, - SecureStorageInterface secureStorageInterface, - TransactionNotificationTracker tracker, - Prefs prefs, - ) { - return EthereumToken( - tokenData: tokenData, - walletMnemonic: walletMnemonic, - secureStore: secureStorageInterface, - // tracker: tracker, - ); - } - - Coin get coin; - bool get isRefreshing; - bool get shouldAutoSync; - set shouldAutoSync(bool shouldAutoSync); - - Future> prepareSend({ - required String address, - required int satoshiAmount, - Map? args, - }); - - Future confirmSend({required Map txData}); - - Future get fees; - Future get maxFee; - - Future get currentReceivingAddress; - - Future get availableBalance; - Future get totalBalance; - - Future> get allOwnAddresses; - - Future get transactionData; - - Future refresh(); - - bool validateAddress(String address); - - Future initializeNew(); - Future initializeExisting(); - - Future estimateFeeFor(int satoshiAmount, int feeRate); - - Future isValidToken(String contractAddress); -} diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index 78dff426b..9eeededd6 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -1,12 +1,8 @@ -import 'dart:convert'; import 'dart:math'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; -import 'package:ethereum_addresses/ethereum_addresses.dart'; import "package:hex/hex.dart"; -import 'package:http/http.dart'; -import 'package:stackwallet/models/paymint/fee_object_model.dart'; class AddressTransaction { final String message; @@ -58,62 +54,7 @@ class GasTracker { } } -const blockExplorer = "https://blockscout.com/eth/mainnet/api"; -const abiUrl = - "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update const hdPathEthereum = "m/44'/60'/0'/0"; -const _gasTrackerUrl = - "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; - -Future fetchAddressTransactions( - String address, String action) async { - try { - final response = await get(Uri.parse( - "$blockExplorer?module=account&action=$action&address=$address")); - if (response.statusCode == 200) { - return AddressTransaction.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception( - 'ERROR GETTING TRANSACTIONS WITH STATUS ${response.statusCode}'); - } - } catch (e, s) { - throw Exception('ERROR GETTING TRANSACTIONS ${e.toString()}'); - } -} - -Future> getWalletTokens(String address) async { - AddressTransaction tokens = - await fetchAddressTransactions(address, "tokentx"); - List tokensList = []; - var tokenMap = {}; - if (tokens.message == "OK") { - final allTxs = tokens.result; - allTxs.forEach((element) { - String key = element["tokenSymbol"] as String; - tokenMap[key] = {}; - tokenMap[key]["balance"] = 0; - - if (tokenMap.containsKey(key)) { - tokenMap[key]["contractAddress"] = element["contractAddress"] as String; - tokenMap[key]["decimals"] = element["tokenDecimal"]; - tokenMap[key]["name"] = element["tokenName"]; - tokenMap[key]["symbol"] = element["tokenSymbol"]; - if (checksumEthereumAddress(address) == address) { - tokenMap[key]["balance"] += int.parse(element["value"] as String); - } else { - tokenMap[key]["balance"] -= int.parse(element["value"] as String); - } - } - }); - - tokenMap.forEach((key, value) { - tokensList.add(value as Map); - }); - return tokensList; - } - return []; -} String getPrivateKey(String mnemonic, String mnemonicPassphrase) { final isValidMnemonic = bip39.validateMnemonic(mnemonic); @@ -129,31 +70,6 @@ String getPrivateKey(String mnemonic, String mnemonicPassphrase) { return HEX.encode(addressAtIndex.privateKey as List); } -Future getGasOracle() async { - final response = await get(Uri.parse(_gasTrackerUrl)); - if (response.statusCode == 200) { - return GasTracker.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception('Failed to load gas oracle'); - } -} - -Future getFees() async { - GasTracker fees = await getGasOracle(); - final feesFast = fees.fast * (pow(10, 9)); - final feesStandard = fees.average * (pow(10, 9)); - final feesSlow = fees.slow * (pow(10, 9)); - - return FeeObject( - numberOfBlocksFast: 1, - numberOfBlocksAverage: 3, - numberOfBlocksSlow: 3, - fast: feesFast.toInt(), - medium: feesStandard.toInt(), - slow: feesSlow.toInt()); -} - double estimateFee(int feeRate, int gasLimit, int decimals) { final gweiAmount = feeRate / (pow(10, 9)); final fee = gasLimit * gweiAmount; diff --git a/lib/utilities/format.dart b/lib/utilities/format.dart index a0a6613a7..8aa0c1162 100644 --- a/lib/utilities/format.dart +++ b/lib/utilities/format.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'dart:typed_data'; import 'package:decimal/decimal.dart'; @@ -19,6 +20,12 @@ abstract class Format { scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); } + static Decimal satoshisToEthTokenAmount(int sats, int decimalPlaces) { + return (Decimal.fromInt(sats) / + Decimal.fromInt(pow(10, decimalPlaces).toInt())) + .toDecimal(scaleOnInfinitePrecision: decimalPlaces); + } + /// static String satoshiAmountToPrettyString( int sats, String locale, Coin coin) { diff --git a/lib/utilities/show_loading.dart b/lib/utilities/show_loading.dart new file mode 100644 index 000000000..55a537780 --- /dev/null +++ b/lib/utilities/show_loading.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_loading_overlay.dart'; + +Future showLoading({ + required Future whileFuture, + required BuildContext context, + required String message, + String? subMessage, + bool isDesktop = false, +}) async { + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.6), + child: CustomLoadingOverlay( + message: message, + subMessage: subMessage, + eventBus: null, + ), + ), + ), + ), + ); + + final result = await whileFuture; + + // TODO: update to flutter 3.7.x to take advantage of context.mounted + // if (mounted) { + Navigator.of(context, rootNavigator: isDesktop).pop(); + // } + + return result; +} From 58280010f83652eac980182742133a34df073d92 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Feb 2023 08:45:34 -0600 Subject: [PATCH 038/208] refactor import --- .../ethereum/ethereum_token_service.dart | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index ebc77f5f8..3dcccc780 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -16,8 +16,7 @@ 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:web3dart/web3dart.dart'; -import 'package:web3dart/web3dart.dart' as transaction; +import 'package:web3dart/web3dart.dart' as web3dart; const int MINIMUM_CONFIRMATIONS = 3; @@ -25,15 +24,15 @@ class EthereumTokenService { final EthToken token; late bool shouldAutoSync; - late EthereumAddress _contractAddress; - late EthPrivateKey _credentials; - late DeployedContract _contract; - late ContractFunction _balanceFunction; - late ContractFunction _sendFunction; + late web3dart.EthereumAddress _contractAddress; + late web3dart.EthPrivateKey _credentials; + late web3dart.DeployedContract _contract; + late web3dart.ContractFunction _balanceFunction; + late web3dart.ContractFunction _sendFunction; late Future> _walletMnemonic; late SecureStorageInterface _secureStore; late String _tokenAbi; - late Web3Client _client; + late web3dart.Web3Client _client; late final TransactionNotificationTracker txTracker; TransactionData? cachedTxData; @@ -44,7 +43,7 @@ class EthereumTokenService { required Future> walletMnemonic, required SecureStorageInterface secureStore, }) { - _contractAddress = EthereumAddress.fromHex(token.contractAddress); + _contractAddress = web3dart.EthereumAddress.fromHex(token.contractAddress); _walletMnemonic = walletMnemonic; _secureStore = secureStore; } @@ -75,16 +74,16 @@ class EthereumTokenService { final sentTx = await _client.sendTransaction( _credentials, - transaction.Transaction.callContract( + web3dart.Transaction.callContract( contract: _contract, function: _sendFunction, parameters: [ - EthereumAddress.fromHex(txData['address'] as String), + web3dart.EthereumAddress.fromHex(txData['address'] as String), bigIntAmount ], maxGas: _gasLimit, - gasPrice: EtherAmount.fromUnitAndValue( - EtherUnit.wei, txData['feeInWei']))); + gasPrice: web3dart.EtherAmount.fromUnitAndValue( + web3dart.EtherUnit.wei, txData['feeInWei']))); return sentTx; } @@ -118,10 +117,10 @@ class EthereumTokenService { //Get private key for given mnemonic // TODO: replace empty string with actual passphrase String privateKey = getPrivateKey(mnemonicString, ""); - _credentials = EthPrivateKey.fromHex(privateKey); + _credentials = web3dart.EthPrivateKey.fromHex(privateKey); - _contract = DeployedContract( - ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); + _contract = web3dart.DeployedContract( + web3dart.ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); _balanceFunction = _contract.function('balanceOf'); _sendFunction = _contract.function('transfer'); _client = await getEthClient(); @@ -148,10 +147,10 @@ class EthereumTokenService { //Get private key for given mnemonic // TODO: replace empty string with actual passphrase String privateKey = getPrivateKey(mnemonicString, ""); - _credentials = EthPrivateKey.fromHex(privateKey); + _credentials = web3dart.EthPrivateKey.fromHex(privateKey); - _contract = DeployedContract( - ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); + _contract = web3dart.DeployedContract( + web3dart.ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); _balanceFunction = _contract.function('balanceOf'); _sendFunction = _contract.function('transfer'); _client = await getEthClient(); @@ -342,8 +341,8 @@ class EthereumTokenService { DefaultNodes.getNodeFor(coin); } - Future getEthClient() async { + Future getEthClient() async { final node = await getCurrentNode(); - return Web3Client(node.host, Client()); + return web3dart.Web3Client(node.host, Client()); } } From 057066950ee65027741d48e205ac5d942dd08e61 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Feb 2023 10:22:25 -0600 Subject: [PATCH 039/208] get token transactions refactor --- lib/services/ethereum/ethereum_api.dart | 173 ++++++++++++++++++------ 1 file changed, 130 insertions(+), 43 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index e1ee0784d..926038399 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -1,16 +1,14 @@ import 'dart:convert'; import 'dart:math'; -import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:http/http.dart'; +import 'package:stackwallet/models/ethereum/erc20_token.dart'; import 'package:stackwallet/models/ethereum/erc721_token.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/logger.dart'; -import '../../models/ethereum/erc20_token.dart'; -import '../../models/ethereum/eth_token.dart'; - class AbiRequestResponse { final String message; final String result; @@ -31,6 +29,87 @@ class AbiRequestResponse { } } +class EthTokenTx { + final String blockHash; + final int blockNumber; + final int confirmations; + final String contractAddress; + final int cumulativeGasUsed; + final String from; + final int gas; + final BigInt gasPrice; + final int gasUsed; + final String hash; + final String input; + final int logIndex; + final int nonce; + final int timeStamp; + final String to; + final int tokenDecimal; + final String tokenName; + final String tokenSymbol; + final int transactionIndex; + final BigInt value; + + EthTokenTx({ + required this.blockHash, + required this.blockNumber, + required this.confirmations, + required this.contractAddress, + required this.cumulativeGasUsed, + required this.from, + required this.gas, + required this.gasPrice, + required this.gasUsed, + required this.hash, + required this.input, + required this.logIndex, + required this.nonce, + required this.timeStamp, + required this.to, + required this.tokenDecimal, + required this.tokenName, + required this.tokenSymbol, + required this.transactionIndex, + required this.value, + }); + + factory EthTokenTx.fromMap({ + required Map map, + }) { + try { + return EthTokenTx( + blockHash: map["blockHash"] as String, + blockNumber: int.parse(map["blockNumber"] as String), + confirmations: int.parse(map["confirmations"] as String), + contractAddress: map["contractAddress"] as String, + cumulativeGasUsed: int.parse(map["cumulativeGasUsed"] as String), + from: map["from"] as String, + gas: int.parse(map["gas"] as String), + gasPrice: BigInt.parse(map["gasPrice"] as String), + gasUsed: int.parse(map["gasUsed"] as String), + hash: map["hash"] as String, + input: map["input"] as String, + logIndex: int.parse(map["logIndex"] as String), + nonce: int.parse(map["nonce"] as String), + timeStamp: int.parse(map["timeStamp"] as String), + to: map["to"] as String, + tokenDecimal: int.parse(map["tokenDecimal"] as String), + tokenName: map["tokenName"] as String, + tokenSymbol: map["tokenSymbol"] as String, + transactionIndex: int.parse(map["transactionIndex"] as String), + value: BigInt.parse(map["value"] as String), + ); + } catch (e, s) { + Logging.instance.log( + "EthTokenTx.fromMap() failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } +} + class EthereumResponse { final T? value; final Exception? exception; @@ -63,6 +142,53 @@ abstract class EthereumAPI { } } + static Future>> getTokenTransactions({ + required String address, + int? startBlock, + int? endBlock, + // todo add more params? + }) async { + try { + final uri = Uri.parse( + "$blockExplorer?module=account&action=tokentx&address=$address", + ); + final response = await get(uri); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json["message"] == "OK") { + final result = + List>.from(json["result"] as List); + final List tokenTxns = []; + for (final map in result) { + tokenTxns.add(EthTokenTx.fromMap(map: map)); + } + + return EthereumResponse( + tokenTxns, + null, + ); + } else { + throw Exception(json["message"] as String); + } + } else { + throw Exception( + "getWalletTokens($address) failed with status code: " + "${response.statusCode}", + ); + } + } catch (e, s) { + Logging.instance.log( + "getWalletTokens(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + Exception(e.toString()), + ); + } + } + static Future>> getWalletTokens({ required String address, }) async { @@ -129,45 +255,6 @@ abstract class EthereumAPI { } } - static Future> getWalletTokenTransactions( - String address) async { - AddressTransaction tokens = - await fetchAddressTransactions(address, "tokentx"); - List tokensList = []; - var tokenMap = {}; - if (tokens.message == "OK") { - final allTxs = tokens.result; - allTxs.forEach((element) { - print("========================================================="); - print("THING: $element"); - print("========================================================="); - - String key = element["tokenSymbol"] as String; - tokenMap[key] = {}; - tokenMap[key]["balance"] = 0; - - if (tokenMap.containsKey(key)) { - tokenMap[key]["contractAddress"] = - element["contractAddress"] as String; - tokenMap[key]["decimals"] = element["tokenDecimal"]; - tokenMap[key]["name"] = element["tokenName"]; - tokenMap[key]["symbol"] = element["tokenSymbol"]; - if (checksumEthereumAddress(address) == address) { - tokenMap[key]["balance"] += int.parse(element["value"] as String); - } else { - tokenMap[key]["balance"] -= int.parse(element["value"] as String); - } - } - }); - - tokenMap.forEach((key, value) { - tokensList.add(value as Map); - }); - return tokensList; - } - return []; - } - static Future getGasOracle() async { final response = await get(Uri.parse(gasTrackerUrl)); if (response.statusCode == 200) { From 67fbb6ec5e100fc98f924c8d427dbc9708a19023 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Feb 2023 10:23:39 -0600 Subject: [PATCH 040/208] WIP token view --- .../sub_widgets/my_token_select_item.dart | 31 +- .../token_transaction_list_widget.dart | 298 +++++++++++ lib/pages/token_view/token_view.dart | 494 +++++------------- lib/route_generator.dart | 5 +- 4 files changed, 438 insertions(+), 390 deletions(-) create mode 100644 lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index aee7cacc4..b85997ca8 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -4,13 +4,13 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; @@ -48,23 +48,28 @@ class MyTokenSelectItem extends ConsumerWidget { borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ), - onPressed: () { + onPressed: () async { final mnemonicList = ref.read(managerProvider).mnemonic; final tokenService = EthereumTokenService( - token: token, - walletMnemonic: mnemonicList, - secureStore: ref.read(secureStoreProvider)); + token: token, + walletMnemonic: mnemonicList, + secureStore: ref.read(secureStoreProvider), + ); - Navigator.of(context).pushNamed( + await showLoading( + whileFuture: tokenService.initializeExisting(), + context: context, + message: "Loading ${token.name}", + ); + + await Navigator.of(context).pushNamed( TokenView.routeName, - arguments: Tuple4( - walletId, - token, - ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(walletId), - tokenService), + arguments: Tuple3( + walletId, + token, + tokenService, + ), ); }, diff --git a/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart b/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart new file mode 100644 index 000000000..e0c6a8304 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart @@ -0,0 +1,298 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; +import 'package:stackwallet/providers/global/trades_service_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/trade_card.dart'; +import 'package:stackwallet/widgets/transaction_card.dart'; +import 'package:tuple/tuple.dart'; + +class TokenTransactionsList extends ConsumerStatefulWidget { + const TokenTransactionsList({ + Key? key, + required this.walletId, + required this.tokenService, + }) : super(key: key); + + final String walletId; + final EthereumTokenService tokenService; + + @override + ConsumerState createState() => + _TransactionsListState(); +} + +class _TransactionsListState extends ConsumerState { + // + bool _hasLoaded = false; + List _transactions2 = []; + + BorderRadius get _borderRadiusFirst { + return BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + BorderRadius get _borderRadiusLast { + return BorderRadius.only( + bottomLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottomRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + Widget itemBuilder( + BuildContext context, + Transaction tx, + BorderRadius? radius, + Coin coin, + ) { + final matchingTrades = ref + .read(tradesServiceProvider) + .trades + .where((e) => e.payInTxid == tx.txid || e.payOutTxid == tx.txid); + + if (tx.type == TransactionType.outgoing && matchingTrades.isNotEmpty) { + final trade = matchingTrades.first; + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: radius, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TransactionCard( + // this may mess with combined firo transactions + key: tx.isConfirmed( + ref.watch(walletsChangeNotifierProvider.select((value) => + value.getManager(widget.walletId).currentHeight)), + coin.requiredConfirmations) + ? Key(tx.txid + tx.type.name + tx.address.value.toString()) + : UniqueKey(), // + transaction: tx, + walletId: widget.walletId, + ), + TradeCard( + // this may mess with combined firo transactions + key: Key(tx.txid + + tx.type.name + + tx.address.value.toString() + + trade.uuid), // + trade: trade, + onTap: () async { + final walletName = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .walletName; + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: trade.tradeId, + transactionIfSentFromStack: tx, + walletName: walletName, + walletId: widget.walletId, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4( + trade.tradeId, + tx, + widget.walletId, + walletName, + ), + ), + ); + } + }, + ) + ], + ), + ); + } else { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: radius, + ), + child: TransactionCard( + // this may mess with combined firo transactions + key: tx.isConfirmed( + ref.watch(walletsChangeNotifierProvider.select((value) => + value.getManager(widget.walletId).currentHeight)), + coin.requiredConfirmations) + ? Key(tx.txid + tx.type.name + tx.address.value.toString()) + : UniqueKey(), + transaction: tx, + walletId: widget.walletId, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + + return FutureBuilder( + future: widget.tokenService.transaction, + builder: (fbContext, AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _transactions2 = snapshot.data!; + _hasLoaded = true; + } + if (!_hasLoaded) { + return Column( + children: const [ + Spacer(), + Center( + child: LoadingIndicator( + height: 50, + width: 50, + ), + ), + Spacer( + flex: 4, + ), + ], + ); + } + if (_transactions2.isEmpty) { + return const NoTransActionsFound(); + } else { + _transactions2.sort((a, b) => b.timestamp - a.timestamp); + return RefreshIndicator( + onRefresh: () async { + //todo: check if print needed + // debugPrint("pulled down to refresh on transaction list"); + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(widget.walletId); + if (!ref.read(managerProvider).isRefreshing) { + unawaited(ref.read(managerProvider).refresh()); + } + }, + child: Util.isDesktop + ? ListView.separated( + itemBuilder: (context, index) { + BorderRadius? radius; + if (_transactions2.length == 1) { + radius = BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == _transactions2.length - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + final tx = _transactions2[index]; + return itemBuilder(context, tx, radius, manager.coin); + }, + separatorBuilder: (context, index) { + return Container( + width: double.infinity, + height: 2, + color: Theme.of(context) + .extension()! + .background, + ); + }, + itemCount: _transactions2.length, + ) + : ListView.builder( + itemCount: _transactions2.length, + itemBuilder: (context, index) { + BorderRadius? radius; + if (_transactions2.length == 1) { + radius = BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == _transactions2.length - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + final tx = _transactions2[index]; + return itemBuilder(context, tx, radius, manager.coin); + }, + ), + ); + } + }, + ); + } +} diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 29d18bafc..71ec06edf 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -1,32 +1,19 @@ -import 'dart:async'; - import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; 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/services/coins/manager.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; -import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/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/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/stack_dialog.dart'; /// [eventBus] should only be set during testing class TokenView extends ConsumerStatefulWidget { @@ -34,7 +21,6 @@ class TokenView extends ConsumerStatefulWidget { Key? key, required this.walletId, required this.token, - required this.managerProvider, required this.tokenService, this.eventBus, }) : super(key: key); @@ -44,7 +30,6 @@ class TokenView extends ConsumerStatefulWidget { final String walletId; final EthToken token; - final ChangeNotifierProvider managerProvider; final EthereumTokenService tokenService; final EventBus? eventBus; @@ -53,385 +38,146 @@ class TokenView extends ConsumerStatefulWidget { } class _TokenViewState extends ConsumerState { - late final EventBus eventBus; - late final String walletId; - late final ChangeNotifierProvider managerProvider; - - late final bool _shouldDisableAutoSyncOnLogOut; - - late WalletSyncStatus _currentSyncStatus; - late NodeConnectionStatus _currentNodeStatus; - - late StreamSubscription _syncStatusSubscription; - late StreamSubscription _nodeStatusSubscription; - @override void initState() { - walletId = widget.walletId; - managerProvider = widget.managerProvider; - - ref.read(managerProvider).isActiveWallet = true; - if (!ref.read(managerProvider).shouldAutoSync) { - // enable auto sync if it wasn't enabled when loading wallet - ref.read(managerProvider).shouldAutoSync = true; - _shouldDisableAutoSyncOnLogOut = true; - } else { - _shouldDisableAutoSyncOnLogOut = false; - } - - ref.read(managerProvider).refresh(); - - if (ref.read(managerProvider).isRefreshing) { - _currentSyncStatus = WalletSyncStatus.syncing; - _currentNodeStatus = NodeConnectionStatus.connected; - } else { - _currentSyncStatus = WalletSyncStatus.synced; - if (ref.read(managerProvider).isConnected) { - _currentNodeStatus = NodeConnectionStatus.connected; - } else { - _currentNodeStatus = NodeConnectionStatus.disconnected; - _currentSyncStatus = WalletSyncStatus.unableToSync; - } - } - - eventBus = - widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - // switch (event.newStatus) { - // case WalletSyncStatus.unableToSync: - // break; - // case WalletSyncStatus.synced: - // break; - // case WalletSyncStatus.syncing: - // break; - // } - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); - - _nodeStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - // switch (event.newStatus) { - // case NodeConnectionStatus.disconnected: - // break; - // case NodeConnectionStatus.connected: - // break; - // } - setState(() { - _currentNodeStatus = event.newStatus; - }); - } - }, - ); - super.initState(); } @override void dispose() { - _nodeStatusSubscription.cancel(); - _syncStatusSubscription.cancel(); super.dispose(); } - DateTime? _cachedTime; - - Future _onWillPop() async { - final now = DateTime.now(); - const timeout = Duration(milliseconds: 1500); - if (_cachedTime == null || now.difference(_cachedTime!) > timeout) { - _cachedTime = now; - unawaited(showDialog( - context: context, - barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async { - Navigator.of(context).popUntil( - ModalRoute.withName(HomeView.routeName), - ); - _logout(); - return false; - }, - child: const StackDialog(title: "Tap back again to exit wallet"), - ), - ).timeout( - timeout, - onTimeout: () => Navigator.of(context).popUntil( - ModalRoute.withName(TokenView.routeName), - ), - )); - } - return false; - } - - void _logout() async { - if (_shouldDisableAutoSyncOnLogOut) { - // disable auto sync if it was enabled only when loading wallet - ref.read(managerProvider).shouldAutoSync = false; - } - ref.read(managerProvider.notifier).isActiveWallet = false; - ref.read(transactionFilterProvider.state).state = null; - if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && - ref.read(prefsChangeNotifierProvider).backupFrequencyType == - BackupFrequencyType.afterClosingAWallet) { - unawaited(ref.read(autoSWBServiceProvider).doBackup()); - } - } - @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - widget.tokenService.initializeExisting(); // print("MY TOTAL BALANCE IS ${widget.token.totalBalance}"); - final coin = ref.watch(managerProvider.select((value) => value.coin)); - - return WillPopScope( - onWillPop: _onWillPop, - child: Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - _logout(); - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - // color: Theme.of(context).extension()!.accentColorDark - width: 24, - height: 24, - ), - const SizedBox( - width: 16, - ), - Expanded( - child: Text( - widget.token.name, - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, - ), - ), - Expanded( - child: Text( - ref.watch( - managerProvider.select((value) => value.coin.ticker)), - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - body: SafeArea( - child: Container( - color: Theme.of(context).extension()!.background, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: TokenSummary( - walletId: walletId, - managerProvider: managerProvider, - initialSyncStatus: ref.watch(managerProvider - .select((value) => value.isRefreshing)) - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, + titleSpacing: 0, + title: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: Coin.ethereum), + // color: Theme.of(context).extension()!.accentColorDark + width: 24, + height: 24, + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Text( + widget.token.name, + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + Expanded( + child: Text( + widget.token.symbol, + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + body: Container( + color: Theme.of(context).extension()!.background, + child: Column( + children: [ + const SizedBox( + height: 10, + ), + // Center( + // child: Padding( + // padding: const EdgeInsets.symmetric(horizontal: 16), + // child: TokenSummary( + // walletId: widget.walletId, + // managerProvider: managerProvider, + // initialSyncStatus: ref.watch(managerProvider + // .select((value) => value.isRefreshing)) + // ? WalletSyncStatus.syncing + // : WalletSyncStatus.synced, + // ), + // ), + // ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + CustomTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottom: Radius.circular( + // TokenView.navBarHeight / 2.0, + Constants.size.circularBorderRadius, + ), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TransactionsList( + managerProvider: managerProvider, + walletId: walletId, + ), + ), + ], ), ), ), - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transactions", - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - ), - CustomTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: walletId, - ); - }, - ), - ], - ), - ), - const SizedBox( - height: 12, - ), - Expanded( - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Padding( - padding: const EdgeInsets.only(bottom: 14), - child: ClipRRect( - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - bottom: Radius.circular( - // TokenView.navBarHeight / 2.0, - Constants.size.circularBorderRadius, - ), - ), - child: Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - Expanded( - child: TransactionsList( - managerProvider: managerProvider, - walletId: walletId, - ), - ), - ], - ), - ), - ), - ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Spacer(), - 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 tokens = - // await getWalletTokens(await ref - // .read(managerProvider) - // .currentReceivingAddress); - // - // await Navigator.of(context).pushNamed( - // MyTokensView.routeName, - // arguments: Tuple4(managerProvider, - // walletId, walletAddress, tokens), - // ); - // }, - // ), - // ), - // ), - ], - ), - ], - ) - ], - ), - ), - ], + ), ), - ), + ], ), ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 60375e881..37d7ccefa 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1461,15 +1461,14 @@ class RouteGenerator { // } case TokenView.routeName: - if (args is Tuple4, + if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => TokenView( walletId: args.item1, token: args.item2, - managerProvider: args.item3, - tokenService: args.item4, + tokenService: args.item3, ), settings: RouteSettings( name: settings.name, From 9c8fd22bfb707d8c835867373443e01164b2206e Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Feb 2023 14:07:59 -0600 Subject: [PATCH 041/208] WIP load and display token transactions --- .../sub_widgets/my_token_select_item.dart | 8 +- .../token_transaction_list_widget.dart | 11 +- lib/pages/token_view/token_view.dart | 8 +- .../coins/ethereum/ethereum_wallet.dart | 17 +- lib/services/ethereum/ethereum_api.dart | 10 +- .../ethereum/ethereum_token_service.dart | 281 +++++++++--------- 6 files changed, 170 insertions(+), 165 deletions(-) diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index b85997ca8..c7bfeb25a 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -4,8 +4,10 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.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/enums/coin_enum.dart'; @@ -49,12 +51,12 @@ class MyTokenSelectItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () async { - final mnemonicList = ref.read(managerProvider).mnemonic; - final tokenService = EthereumTokenService( token: token, - walletMnemonic: mnemonicList, secureStore: ref.read(secureStoreProvider), + ethWallet: ref.read(managerProvider).wallet as EthereumWallet, + tracker: TransactionNotificationTracker( + walletId: ref.read(managerProvider).walletId), ); await showLoading( diff --git a/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart b/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart index e0c6a8304..dbe3050c0 100644 --- a/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart +++ b/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart @@ -208,7 +208,7 @@ class _TransactionsListState extends ConsumerState { .select((value) => value.getManager(widget.walletId))); return FutureBuilder( - future: widget.tokenService.transaction, + future: widget.tokenService.transactions, builder: (fbContext, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -237,13 +237,8 @@ class _TransactionsListState extends ConsumerState { _transactions2.sort((a, b) => b.timestamp - a.timestamp); return RefreshIndicator( onRefresh: () async { - //todo: check if print needed - // debugPrint("pulled down to refresh on transaction list"); - final managerProvider = ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(widget.walletId); - if (!ref.read(managerProvider).isRefreshing) { - unawaited(ref.read(managerProvider).refresh()); + if (!widget.tokenService.isRefreshing) { + unawaited(widget.tokenService.refresh()); } }, child: Util.isDesktop diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 71ec06edf..41357ff37 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -166,9 +166,9 @@ class _TokenViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( - child: TransactionsList( - managerProvider: managerProvider, - walletId: walletId, + child: TokenTransactionsList( + tokenService: widget.tokenService, + walletId: widget.walletId, ), ), ], diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index d7fb3616e..37c79dc5c 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -130,8 +130,12 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { Future> get utxos => db.getUTXOs(walletId).findAll(); @override - Future> get transactions => - db.getTransactions(walletId).sortByTimestampDesc().findAll(); + Future> get transactions => db + .getTransactions(walletId) + .filter() + .otherDataEqualTo(null) + .sortByTimestampDesc() + .findAll(); @override Future get currentReceivingAddress async { @@ -143,7 +147,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { Future get _currentReceivingAddress => db .getAddresses(walletId) .filter() - .typeEqualTo(AddressType.p2wpkh) + .typeEqualTo(AddressType.ethereum) .subTypeEqualTo(AddressSubType.receiving) .sortByDerivationIndexDesc() .findFirst(); @@ -759,8 +763,9 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { ), ); Logging.instance.log( - "Caught exception in refreshWalletData(): $error\n$strace", - level: LogLevel.Warning); + "Caught exception in $walletName $walletId refresh(): $error\n$strace", + level: LogLevel.Warning, + ); } } @@ -910,7 +915,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { } } else { Logging.instance.log( - "Failed to refresh transactions for ${coin.prettyName} $walletName $walletId: $txs", + "Failed to refresh transactions for ${coin.prettyName} $walletName $walletId", level: LogLevel.Warning, ); } diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 926038399..0f1a96308 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -144,14 +144,18 @@ abstract class EthereumAPI { static Future>> getTokenTransactions({ required String address, + String? contractAddress, int? startBlock, int? endBlock, // todo add more params? }) async { try { - final uri = Uri.parse( - "$blockExplorer?module=account&action=tokentx&address=$address", - ); + String uriString = + "$blockExplorer?module=account&action=tokentx&address=$address"; + if (contractAddress != null) { + uriString += "&contractAddress=$contractAddress"; + } + final uri = Uri.parse(uriString); final response = await get(uri); if (response.statusCode == 200) { diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 3dcccc780..bae28dba9 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -1,13 +1,18 @@ -import 'dart:math'; +import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:http/http.dart'; +import 'package:isar/isar.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/models/paymint/transactions_model.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.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/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; @@ -16,47 +21,33 @@ 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/logger.dart'; +import 'package:tuple/tuple.dart'; import 'package:web3dart/web3dart.dart' as web3dart; -const int MINIMUM_CONFIRMATIONS = 3; - class EthereumTokenService { final EthToken token; + final EthereumWallet ethWallet; + final TransactionNotificationTracker tracker; + final SecureStorageInterface _secureStore; - late bool shouldAutoSync; late web3dart.EthereumAddress _contractAddress; late web3dart.EthPrivateKey _credentials; late web3dart.DeployedContract _contract; late web3dart.ContractFunction _balanceFunction; late web3dart.ContractFunction _sendFunction; - late Future> _walletMnemonic; - late SecureStorageInterface _secureStore; late String _tokenAbi; late web3dart.Web3Client _client; - late final TransactionNotificationTracker txTracker; - TransactionData? cachedTxData; - final _gasLimit = 200000; + static const _gasLimit = 200000; EthereumTokenService({ required this.token, - required Future> walletMnemonic, + required this.ethWallet, required SecureStorageInterface secureStore, - }) { + required this.tracker, + }) : _secureStore = secureStore { _contractAddress = web3dart.EthereumAddress.fromHex(token.contractAddress); - _walletMnemonic = walletMnemonic; - _secureStore = secureStore; - } - - Future> get allOwnAddresses => - _allOwnAddresses ??= _fetchAllOwnAddresses(); - Future>? _allOwnAddresses; - - Future> _fetchAllOwnAddresses() async { - List addresses = []; - final ownAddress = _credentials.address; - addresses.add(ownAddress.toString()); - return addresses; } Future get availableBalance async { @@ -89,12 +80,19 @@ class EthereumTokenService { } Future get currentReceivingAddress async { - final _currentReceivingAddress = await _credentials.extractAddress(); - final checkSumAddress = - checksumEthereumAddress(_currentReceivingAddress.toString()); - return checkSumAddress; + final address = await _currentReceivingAddress; + return address?.value ?? + checksumEthereumAddress(_credentials.address.toString()); } + Future get _currentReceivingAddress => ethWallet.db + .getAddresses(ethWallet.walletId) + .filter() + .typeEqualTo(AddressType.ethereum) + .subTypeEqualTo(AddressSubType.receiving) + .sortByDerivationIndexDesc() + .findFirst(); + Future estimateFeeFor(int satoshiAmount, int feeRate) async { final fee = estimateFee(feeRate, _gasLimit, token.decimals); return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin); @@ -111,12 +109,11 @@ class EthereumTokenService { _tokenAbi = (await _secureStore.read( key: '${_contractAddress.toString()}_tokenAbi'))!; - final mnemonic = await _walletMnemonic; - String mnemonicString = mnemonic.join(' '); + String? mnemonicString = await ethWallet.mnemonicString; //Get private key for given mnemonic - // TODO: replace empty string with actual passphrase - String privateKey = getPrivateKey(mnemonicString, ""); + String privateKey = getPrivateKey( + mnemonicString!, (await ethWallet.mnemonicPassphrase) ?? ""); _credentials = web3dart.EthPrivateKey.fromHex(privateKey); _contract = web3dart.DeployedContract( @@ -125,7 +122,7 @@ class EthereumTokenService { _sendFunction = _contract.function('transfer'); _client = await getEthClient(); - // print(_credentials.p) + unawaited(refresh()); } Future initializeNew() async { @@ -141,12 +138,11 @@ class EthereumTokenService { throw Exception('Failed to load token abi'); } - final mnemonic = await _walletMnemonic; - String mnemonicString = mnemonic.join(' '); + String? mnemonicString = await ethWallet.mnemonicString; //Get private key for given mnemonic - // TODO: replace empty string with actual passphrase - String privateKey = getPrivateKey(mnemonicString, ""); + String privateKey = getPrivateKey( + mnemonicString!, (await ethWallet.mnemonicPassphrase) ?? ""); _credentials = web3dart.EthPrivateKey.fromHex(privateKey); _contract = web3dart.DeployedContract( @@ -154,16 +150,11 @@ class EthereumTokenService { _balanceFunction = _contract.function('balanceOf'); _sendFunction = _contract.function('transfer'); _client = await getEthClient(); + + unawaited(refresh()); } - // TODO: implement isRefreshing - bool get isRefreshing => throw UnimplementedError(); - - Future get maxFee async { - final fee = (await fees).fast; - final feeEstimate = await estimateFeeFor(0, fee); - return feeEstimate; - } + bool get isRefreshing => _refreshLock; Future> prepareSend( {required String address, @@ -208,9 +199,22 @@ class EthereumTokenService { return txData; } - Future refresh() { - // TODO: implement refresh - throw UnimplementedError(); + bool _refreshLock = false; + + Future refresh() async { + if (!_refreshLock) { + _refreshLock = true; + try { + await _refreshTransactions(); + } catch (e, s) { + Logging.instance.log( + "Caught exception in ${token.name} ${ethWallet.walletName} ${ethWallet.walletId} refresh(): $e\n$s", + level: LogLevel.Warning, + ); + } finally { + _refreshLock = false; + } + } } Future get totalBalance async { @@ -227,108 +231,103 @@ class EthereumTokenService { return Decimal.parse(balanceInDecimal.toString()); } - Future get transactionData => - _transactionData ??= _fetchTransactionData(); - Future? _transactionData; + Future> get transactions => ethWallet.db + .getTransactions(ethWallet.walletId) + .filter() + .otherDataEqualTo(token.contractAddress) + .sortByTimestampDesc() + .findAll(); - Future _fetchTransactionData() async { - String thisAddress = await currentReceivingAddress; + Future _refreshTransactions() async { + String addressString = await currentReceivingAddress; - final List> midSortedArray = []; + final response = await EthereumAPI.getTokenTransactions( + address: addressString, + contractAddress: token.contractAddress, + ); - AddressTransaction txs = - await EthereumAPI.fetchAddressTransactions(thisAddress, "tokentx"); - - if (txs.message == "OK") { - final allTxs = txs.result; - for (var element in allTxs) { - Map midSortedTx = {}; - // create final tx map - midSortedTx["txid"] = element["hash"]; - int confirmations = int.parse(element['confirmations'].toString()); - - int transactionAmount = int.parse(element['value'].toString()); - int decimal = token.decimals; //Eth has up to 18 decimal places - final transactionAmountInDecimal = - transactionAmount / (pow(10, decimal)); - - //Convert to satoshi, default display for other coins - final satAmount = Format.decimalAmountToSatoshis( - Decimal.parse(transactionAmountInDecimal.toString()), coin); - - midSortedTx["confirmed_status"] = - (confirmations != 0) && (confirmations >= MINIMUM_CONFIRMATIONS); - midSortedTx["confirmations"] = confirmations; - midSortedTx["timestamp"] = element["timeStamp"]; - - if (checksumEthereumAddress(element["from"].toString()) == - thisAddress) { - midSortedTx["txType"] = "Sent"; - } else { - midSortedTx["txType"] = "Received"; - } - - midSortedTx["amount"] = satAmount; - - //Calculate fees (GasLimit * gasPrice) - int txFee = int.parse(element['gasPrice'].toString()) * - int.parse(element['gasUsed'].toString()); - final txFeeDecimal = txFee / (pow(10, decimal)); - - midSortedTx["aliens"] = []; - midSortedTx["fees"] = Format.decimalAmountToSatoshis( - Decimal.parse(txFeeDecimal.toString()), coin); - midSortedTx["address"] = element["to"]; - midSortedTx["inputSize"] = 1; - midSortedTx["outputSize"] = 1; - midSortedTx["inputs"] = []; - midSortedTx["outputs"] = []; - midSortedTx["height"] = int.parse(element['blockNumber'].toString()); - - midSortedArray.add(midSortedTx); - } + if (response.value == null) { + throw Exception("Failed to fetch token transactions"); } - midSortedArray.sort((a, b) => - (int.parse(b['timestamp'].toString())) - - (int.parse(a['timestamp'].toString()))); + final List> txnsData = []; - // buildDateTimeChunks - final Map result = {"dateTimeChunks": []}; - final dateArray = []; - - for (int i = 0; i < midSortedArray.length; i++) { - final txObject = midSortedArray[i]; - final date = - extractDateFromTimestamp(int.parse(txObject['timestamp'].toString())); - final txTimeArray = [txObject["timestamp"], date]; - - if (dateArray.contains(txTimeArray[1])) { - result["dateTimeChunks"].forEach((dynamic chunk) { - if (extractDateFromTimestamp( - int.parse(chunk['timestamp'].toString())) == - txTimeArray[1]) { - if (chunk["transactions"] == null) { - chunk["transactions"] = >[]; - } - chunk["transactions"].add(txObject); - } - }); + for (final tx in response.value!) { + bool isIncoming; + if (checksumEthereumAddress(tx.from) == addressString) { + isIncoming = false; } else { - dateArray.add(txTimeArray[1]); - final chunk = { - "timestamp": txTimeArray[0], - "transactions": [txObject], - }; - result["dateTimeChunks"].add(chunk); + isIncoming = true; } + + final txn = Transaction( + walletId: ethWallet.walletId, + txid: tx.hash, + timestamp: tx.timeStamp, + type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, + subType: TransactionSubType.ethToken, + amount: tx.value.toInt(), + fee: tx.gasUsed * tx.gasPrice.toInt(), + height: tx.blockNumber, + isCancelled: false, + isLelantus: false, + slateId: null, + otherData: tx.contractAddress, + inputs: [], + outputs: [], + ); + + Address? transactionAddress = await ethWallet.db + .getAddresses(ethWallet.walletId) + .filter() + .valueEqualTo(addressString) + .findFirst(); + + if (transactionAddress == null) { + if (isIncoming) { + transactionAddress = Address( + walletId: ethWallet.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; + + transactionAddress = Address( + walletId: ethWallet.walletId, + value: addressString, + publicKey: [], + derivationIndex: isSentToSelf ? 0 : -1, + derivationPath: isSentToSelf + ? (DerivationPath()..value = "$hdPathEthereum/0") + : null, + type: AddressType.ethereum, + subType: isSentToSelf + ? AddressSubType.receiving + : AddressSubType.nonWallet, + ); + } + } + + txnsData.add(Tuple2(txn, transactionAddress)); } + await ethWallet.db.addNewTransactionData(txnsData, ethWallet.walletId); - final txModel = TransactionData.fromMap( - TransactionData.fromJson(result).getAllTransactions()); - - cachedTxData = txModel; - return txModel; + // quick hack to notify manager to call notifyListeners if + // transactions changed + if (txnsData.isNotEmpty) { + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "${token.name} transactions updated/added for: ${ethWallet.walletId} ${ethWallet.walletName}", + ethWallet.walletId, + ), + ); + } } bool validateAddress(String address) { From fcd8f01d9304144f1539bc3b0773327577264a51 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 10:01:06 -0600 Subject: [PATCH 042/208] convert token service to change notifier, add token cache per eth wallet, token balances, and fix routing issues --- lib/models/token_balance.dart | 71 +++++++++ .../sub_widgets/my_token_select_item.dart | 15 +- .../token_view/sub_widgets/token_summary.dart | 141 ++++-------------- lib/pages/token_view/token_view.dart | 21 +-- lib/route_generator.dart | 8 +- .../ethereum/ethereum_token_service.dart | 47 +++--- lib/services/mixins/eth_token_cache.dart | 50 +++++++ 7 files changed, 197 insertions(+), 156 deletions(-) create mode 100644 lib/models/token_balance.dart create mode 100644 lib/services/mixins/eth_token_cache.dart diff --git a/lib/models/token_balance.dart b/lib/models/token_balance.dart new file mode 100644 index 000000000..632a5ade5 --- /dev/null +++ b/lib/models/token_balance.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +import 'package:decimal/decimal.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; + +class TokenBalance extends Balance { + TokenBalance({ + required this.contractAddress, + required this.decimalPlaces, + required super.total, + required super.spendable, + required super.blockedTotal, + required super.pendingSpendable, + super.coin = Coin.ethereum, + }); + + final String contractAddress; + final int decimalPlaces; + + @override + Decimal getTotal({bool includeBlocked = false}) => + Format.satoshisToEthTokenAmount( + includeBlocked ? total : total - blockedTotal, + decimalPlaces, + ); + + @override + Decimal getSpendable() => Format.satoshisToEthTokenAmount( + spendable, + decimalPlaces, + ); + + @override + Decimal getPending() => Format.satoshisToEthTokenAmount( + pendingSpendable, + decimalPlaces, + ); + + @override + Decimal getBlocked() => Format.satoshisToEthTokenAmount( + blockedTotal, + decimalPlaces, + ); + + @override + String toJsonIgnoreCoin() => jsonEncode({ + "decimalPlaces": decimalPlaces, + "total": total, + "spendable": spendable, + "blockedTotal": blockedTotal, + "pendingSpendable": pendingSpendable, + }); + + factory TokenBalance.fromJson( + String json, + String contractAddress, + int decimalPlaces, + ) { + final decoded = jsonDecode(json); + return TokenBalance( + contractAddress: contractAddress, + decimalPlaces: decoded["decimalPlaces"] as int, + total: decoded["total"] as int, + spendable: decoded["spendable"] as int, + blockedTotal: decoded["blockedTotal"] as int, + pendingSpendable: decoded["pendingSpendable"] as int, + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index c7bfeb25a..3d021f30e 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -15,7 +15,6 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:tuple/tuple.dart'; class MyTokenSelectItem extends ConsumerWidget { const MyTokenSelectItem( @@ -51,27 +50,25 @@ class MyTokenSelectItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () async { - final tokenService = EthereumTokenService( + ref.read(tokenServiceStateProvider.state).state = + EthereumTokenService( token: token, secureStore: ref.read(secureStoreProvider), ethWallet: ref.read(managerProvider).wallet as EthereumWallet, tracker: TransactionNotificationTracker( - walletId: ref.read(managerProvider).walletId), + walletId: ref.read(managerProvider).walletId, + ), ); await showLoading( - whileFuture: tokenService.initializeExisting(), + whileFuture: ref.read(tokenServiceProvider)!.initializeExisting(), context: context, message: "Loading ${token.name}", ); await Navigator.of(context).pushNamed( TokenView.routeName, - arguments: Tuple3( - walletId, - token, - tokenService, - ), + arguments: walletId, ); }, diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index bac284bb0..28abdda8a 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -1,127 +1,50 @@ 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'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; -class TokenSummary extends StatelessWidget { +class TokenSummary extends ConsumerWidget { 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 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()! - .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, - ), - ], + Widget build(BuildContext context, WidgetRef ref) { + return RoundedContainer( + color: const Color(0xFFE9EAFF), // todo: fix color + // color: Theme.of(context).extension()!., + + child: Column( + children: [ + Text( + ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).walletName, ), ), - // 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, - ), - ], + style: STextStyles.label(context), + ), + Text( + ref.watch(tokenServiceProvider.select((value) => value!.balance + .getTotal() + .toStringAsFixed(ref.watch(tokenServiceProvider + .select((value) => value!.token.decimals))))), + style: STextStyles.label(context), + ), + Text( + ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).walletName, ), ), - // ], - // ), - // ), - Positioned.fill( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: TokenSummaryInfo( - walletId: walletId, - managerProvider: managerProvider, - initialSyncStatus: initialSyncStatus, - ), - ), - ), - ], - ), + style: STextStyles.label(context), + ), + ], ), ); } diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 41357ff37..4160650bf 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -2,7 +2,6 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; @@ -15,13 +14,16 @@ 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'; +final tokenServiceStateProvider = + StateProvider((ref) => null); +final tokenServiceProvider = ChangeNotifierProvider( + (ref) => ref.watch(tokenServiceStateProvider)); + /// [eventBus] should only be set during testing class TokenView extends ConsumerStatefulWidget { const TokenView({ Key? key, required this.walletId, - required this.token, - required this.tokenService, this.eventBus, }) : super(key: key); @@ -29,8 +31,6 @@ class TokenView extends ConsumerStatefulWidget { static const double navBarHeight = 65.0; final String walletId; - final EthToken token; - final EthereumTokenService tokenService; final EventBus? eventBus; @override @@ -51,7 +51,6 @@ class _TokenViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - // print("MY TOTAL BALANCE IS ${widget.token.totalBalance}"); return Background( child: Scaffold( @@ -67,7 +66,6 @@ class _TokenViewState extends ConsumerState { children: [ SvgPicture.asset( Assets.svg.iconFor(coin: Coin.ethereum), - // color: Theme.of(context).extension()!.accentColorDark width: 24, height: 24, ), @@ -76,14 +74,16 @@ class _TokenViewState extends ConsumerState { ), Expanded( child: Text( - widget.token.name, + ref.watch(tokenServiceProvider + .select((value) => value!.token.name)), style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), ), Expanded( child: Text( - widget.token.symbol, + ref.watch(tokenServiceProvider + .select((value) => value!.token.symbol)), style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), @@ -167,7 +167,8 @@ class _TokenViewState extends ConsumerState { children: [ Expanded( child: TokenTransactionsList( - tokenService: widget.tokenService, + tokenService: ref.watch(tokenServiceProvider + .select((value) => value!)), walletId: widget.walletId, ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 37d7ccefa..1b3c5b9ab 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -128,7 +128,6 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/nodes_ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart'; import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; @@ -1461,14 +1460,11 @@ class RouteGenerator { // } case TokenView.routeName: - if (args is Tuple3) { + if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => TokenView( - walletId: args.item1, - token: args.item2, - tokenService: args.item3, + walletId: args, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index bae28dba9..ca26fbecd 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; @@ -9,10 +10,12 @@ import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/token_balance.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.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/mixins/eth_token_cache.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; @@ -25,7 +28,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:tuple/tuple.dart'; import 'package:web3dart/web3dart.dart' as web3dart; -class EthereumTokenService { +class EthereumTokenService extends ChangeNotifier with EthTokenCache { final EthToken token; final EthereumWallet ethWallet; final TransactionNotificationTracker tracker; @@ -48,11 +51,11 @@ class EthereumTokenService { required this.tracker, }) : _secureStore = secureStore { _contractAddress = web3dart.EthereumAddress.fromHex(token.contractAddress); + initCache(ethWallet.walletId, token); } - Future get availableBalance async { - return await totalBalance; - } + TokenBalance get balance => _balance ??= getCachedBalance(); + TokenBalance? _balance; Coin get coin => Coin.ethereum; @@ -177,18 +180,6 @@ class EthereumTokenService { final feeEstimate = await estimateFeeFor(satoshiAmount, fee); - bool isSendAll = false; - final balance = - Format.decimalAmountToSatoshis(await availableBalance, coin); - if (satoshiAmount == balance) { - isSendAll = true; - } - - if (isSendAll) { - //Send the full balance - satoshiAmount = balance; - } - Map txData = { "fee": feeEstimate, "feeInWei": fee, @@ -205,6 +196,7 @@ class EthereumTokenService { if (!_refreshLock) { _refreshLock = true; try { + await refreshCachedBalance(); await _refreshTransactions(); } catch (e, s) { Logging.instance.log( @@ -213,22 +205,33 @@ class EthereumTokenService { ); } finally { _refreshLock = false; + notifyListeners(); } } } - Future get totalBalance async { + Future refreshCachedBalance() async { final balanceRequest = await _client.call( contract: _contract, function: _balanceFunction, params: [_credentials.address]); - String balance = balanceRequest.first.toString(); - final balanceInDecimal = Format.satoshisToEthTokenAmount( - int.parse(balance), - token.decimals, + print("=========================================="); + print("balanceRequest: $balanceRequest"); + print("=========================================="); + + String _balance = balanceRequest.first.toString(); + + final newBalance = TokenBalance( + contractAddress: token.contractAddress, + total: int.parse(_balance), + spendable: int.parse(_balance), + blockedTotal: 0, + pendingSpendable: 0, + decimalPlaces: token.decimals, ); - return Decimal.parse(balanceInDecimal.toString()); + await updateCachedBalance(newBalance); + notifyListeners(); } Future> get transactions => ethWallet.db diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart new file mode 100644 index 000000000..b490e849c --- /dev/null +++ b/lib/services/mixins/eth_token_cache.dart @@ -0,0 +1,50 @@ +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/token_balance.dart'; + +abstract class _Keys { + static String tokenBalance(String contractAddress) { + return "tokenBalanceCache_$contractAddress"; + } +} + +mixin EthTokenCache { + late final String _walletId; + late final EthToken _token; + + void initCache(String walletId, EthToken token) { + _walletId = walletId; + _token = token; + } + + // token balance cache + TokenBalance getCachedBalance() { + final jsonString = DB.instance.get( + boxName: _walletId, + key: _Keys.tokenBalance(_token.contractAddress), + ) as String?; + if (jsonString == null) { + return TokenBalance( + contractAddress: _token.contractAddress, + decimalPlaces: _token.decimals, + total: 0, + spendable: 0, + blockedTotal: 0, + pendingSpendable: 0, + ); + } + return TokenBalance.fromJson( + jsonString, + _token.contractAddress, + _token.decimals, + ); + } + + Future updateCachedBalance(TokenBalance balance) async { + await DB.instance.put( + boxName: _walletId, + key: _Keys.tokenBalance(_token.contractAddress), + value: balance.toJsonIgnoreCoin(), + ); + } +} From 8dbefd87fe58fd56aa4ac81a24fab5a9a8605302 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 10:37:41 -0600 Subject: [PATCH 043/208] eth token summary layout and style --- assets/svg/cc.svg | 11 ++ .../token_view/sub_widgets/token_summary.dart | 166 +++++++++++++++--- lib/pages/token_view/token_view.dart | 20 +-- lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 5 files changed, 165 insertions(+), 34 deletions(-) create mode 100644 assets/svg/cc.svg diff --git a/assets/svg/cc.svg b/assets/svg/cc.svg new file mode 100644 index 000000000..646ae64ce --- /dev/null +++ b/assets/svg/cc.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 28abdda8a..e080c7d67 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; class TokenSummary extends ConsumerWidget { @@ -15,37 +19,157 @@ class TokenSummary extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final token = + ref.watch(tokenServiceProvider.select((value) => value!.token)); + final balance = + ref.watch(tokenServiceProvider.select((value) => value!.balance)); + return RoundedContainer( color: const Color(0xFFE9EAFF), // todo: fix color // color: Theme.of(context).extension()!., - + padding: const EdgeInsets.all(24), child: Column( children: [ - Text( - ref.watch( - walletsChangeNotifierProvider.select( - (value) => value.getManager(walletId).walletName, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.walletDesktop, + color: const Color(0xFF8488AB), // todo: fix color + width: 12, + height: 12, ), - ), - style: STextStyles.label(context), - ), - Text( - ref.watch(tokenServiceProvider.select((value) => value!.balance - .getTotal() - .toStringAsFixed(ref.watch(tokenServiceProvider - .select((value) => value!.token.decimals))))), - style: STextStyles.label(context), - ), - Text( - ref.watch( - walletsChangeNotifierProvider.select( - (value) => value.getManager(walletId).walletName, + const SizedBox( + width: 6, ), - ), - style: STextStyles.label(context), + Text( + ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).walletName, + ), + ), + style: STextStyles.w500_12(context).copyWith( + color: const Color(0xFF8488AB), // todo: fix color + ), + ), + ], ), + const SizedBox( + height: 6, + ), + Text( + "${balance.getTotal()}" + " ${token.symbol}", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 6, + ), + Text( + "FIXME: price", + style: STextStyles.subtitle500(context), + ), + const SizedBox( + height: 20, + ), + const TokenWalletOptions(), ], ), ); } } + +class TokenWalletOptions extends StatelessWidget { + const TokenWalletOptions({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TokenOptionsButton( + onPressed: () {}, + subLabel: "Receive", + iconAssetSVG: Assets.svg.receive(context), + ), + const SizedBox( + width: 16, + ), + TokenOptionsButton( + onPressed: () {}, + subLabel: "Receive", + iconAssetSVG: Assets.svg.send(context), + ), + const SizedBox( + width: 16, + ), + TokenOptionsButton( + onPressed: () {}, + subLabel: "Exchange", + iconAssetSVG: Assets.svg.exchange(context), + ), + const SizedBox( + width: 16, + ), + TokenOptionsButton( + onPressed: () {}, + subLabel: "Buy", + iconAssetSVG: Assets.svg.creditCard, + ), + ], + ); + } +} + +class TokenOptionsButton extends StatelessWidget { + const TokenOptionsButton({ + Key? key, + required this.onPressed, + required this.subLabel, + required this.iconAssetSVG, + }) : super(key: key); + + final VoidCallback onPressed; + final String subLabel; + final String iconAssetSVG; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + RawMaterialButton( + fillColor: Theme.of(context).extension()!.popupBG, + elevation: 0, + focusElevation: 0, + hoverElevation: 0, + highlightElevation: 0, + constraints: const BoxConstraints(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: onPressed, + child: Padding( + padding: const EdgeInsets.all(10), + child: SvgPicture.asset( + iconAssetSVG, + color: const Color(0xFF424A97), // todo: fix color + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + height: 6, + ), + Text( + subLabel, + style: STextStyles.w500_12(context), + ) + ], + ); + } +} diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 4160650bf..5d7396e88 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -2,6 +2,7 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; @@ -98,19 +99,12 @@ class _TokenViewState extends ConsumerState { const SizedBox( height: 10, ), - // Center( - // child: Padding( - // padding: const EdgeInsets.symmetric(horizontal: 16), - // child: TokenSummary( - // walletId: widget.walletId, - // managerProvider: managerProvider, - // initialSyncStatus: ref.watch(managerProvider - // .select((value) => value.isRefreshing)) - // ? WalletSyncStatus.syncing - // : WalletSyncStatus.synced, - // ), - // ), - // ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TokenSummary( + walletId: widget.walletId, + ), + ), const SizedBox( height: 20, ), diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 38d09c131..262875cee 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -210,6 +210,7 @@ class _SVG { String get faceId => "assets/svg/faceid.svg"; String get tokens => "assets/svg/tokens.svg"; String get circlePlusDark => "assets/svg/circle-plus.svg"; + String get creditCard => "assets/svg/cc.svg"; String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index bf92ed314..e6b7ec266 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -310,6 +310,7 @@ flutter: - assets/svg/whirlpool.svg - assets/svg/fingerprint.svg - assets/svg/faceid.svg + - assets/svg/cc.svg # light theme coin - assets/images/light/ From 82842f1aa07a06b1f43cd2cd2d2ce976f0f32ab9 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 11:42:22 -0600 Subject: [PATCH 044/208] token view refresh, and WIP token icon assets and other small fixes --- .../token_transaction_list_widget.dart | 11 ++-- lib/pages/token_view/token_view.dart | 66 ++++++++++++++----- .../sub_widgets/wallet_refresh_button.dart | 33 ++++++---- lib/services/ethereum/ethereum_api.dart | 5 +- .../ethereum/ethereum_token_service.dart | 18 ++++- lib/utilities/assets.dart | 7 ++ 6 files changed, 103 insertions(+), 37 deletions(-) diff --git a/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart b/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart index dbe3050c0..1db04f592 100644 --- a/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart +++ b/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/route_generator.dart'; -import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -25,11 +25,9 @@ class TokenTransactionsList extends ConsumerStatefulWidget { const TokenTransactionsList({ Key? key, required this.walletId, - required this.tokenService, }) : super(key: key); final String walletId; - final EthereumTokenService tokenService; @override ConsumerState createState() => @@ -208,7 +206,8 @@ class _TransactionsListState extends ConsumerState { .select((value) => value.getManager(widget.walletId))); return FutureBuilder( - future: widget.tokenService.transactions, + future: ref + .watch(tokenServiceProvider.select((value) => value!.transactions)), builder: (fbContext, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -237,8 +236,8 @@ class _TransactionsListState extends ConsumerState { _transactions2.sort((a, b) => b.timestamp - a.timestamp); return RefreshIndicator( onRefresh: () async { - if (!widget.tokenService.isRefreshing) { - unawaited(widget.tokenService.refresh()); + if (!ref.read(tokenServiceProvider)!.isRefreshing) { + unawaited(ref.read(tokenServiceProvider)!.refresh()); } }, child: Util.isDesktop diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 5d7396e88..2d5c41738 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -4,8 +4,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.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/enums/coin_enum.dart'; @@ -14,6 +17,7 @@ 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/rounded_container.dart'; final tokenServiceStateProvider = StateProvider((ref) => null); @@ -29,7 +33,6 @@ class TokenView extends ConsumerStatefulWidget { }) : super(key: key); static const String routeName = "/token"; - static const double navBarHeight = 65.0; final String walletId; final EventBus? eventBus; @@ -39,8 +42,13 @@ class TokenView extends ConsumerStatefulWidget { } class _TokenViewState extends ConsumerState { + late final WalletSyncStatus initialSyncStatus; + @override void initState() { + initialSyncStatus = ref.read(tokenServiceProvider)!.isRefreshing + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced; super.initState(); } @@ -62,35 +70,59 @@ class _TokenViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - titleSpacing: 0, + centerTitle: true, title: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( - Assets.svg.iconFor(coin: Coin.ethereum), + Assets.svg.iconForToken( + contractAddress: ref.watch(tokenServiceProvider + .select((value) => value!.token.contractAddress))), width: 24, height: 24, ), const SizedBox( - width: 16, + width: 10, ), - Expanded( - child: Text( - ref.watch(tokenServiceProvider - .select((value) => value!.token.name)), - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, - ), + Text( + ref.watch( + tokenServiceProvider.select((value) => value!.token.name)), + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, ), - Expanded( + const SizedBox( + width: 6, + ), + RoundedContainer( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + radiusMultiplier: 0.25, + color: const Color( + 0xFF4D5798), // TODO: color theme for multi themes child: Text( - ref.watch(tokenServiceProvider - .select((value) => value!.token.symbol)), - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, + ref.watch(walletsChangeNotifierProvider.select((value) => + value.getManager(widget.walletId).coin.ticker)), + style: STextStyles.w600_12(context).copyWith( + color: Colors.white, // TODO: design is wrong? + ), ), ) ], ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: WalletRefreshButton( + walletId: widget.walletId, + initialSyncStatus: initialSyncStatus, + tokenContractAddress: ref.watch(tokenServiceProvider + .select((value) => value!.token.contractAddress)), + ), + ), + ), + ], ), body: Container( color: Theme.of(context).extension()!.background, @@ -161,8 +193,6 @@ class _TokenViewState extends ConsumerState { children: [ Expanded( child: TokenTransactionsList( - tokenService: ref.watch(tokenServiceProvider - .select((value) => value!)), walletId: widget.walletId, ), ), diff --git a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart index 603b72338..3811fdb7b 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart @@ -4,6 +4,7 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.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'; @@ -18,12 +19,14 @@ class WalletRefreshButton extends ConsumerStatefulWidget { Key? key, required this.walletId, required this.initialSyncStatus, + this.tokenContractAddress, this.onPressed, this.eventBus, }) : super(key: key); final String walletId; final WalletSyncStatus initialSyncStatus; + final String? tokenContractAddress; final VoidCallback? onPressed; final EventBus? eventBus; @@ -62,7 +65,9 @@ class _RefreshButtonState extends ConsumerState _syncStatusSubscription = eventBus.on().listen( (event) async { - if (event.walletId == widget.walletId) { + if (event.walletId == widget.walletId && + widget.tokenContractAddress == null || + event.walletId == widget.walletId + widget.tokenContractAddress!) { switch (event.newStatus) { case WalletSyncStatus.unableToSync: _spinController?.stop(); @@ -104,16 +109,22 @@ class _RefreshButtonState extends ConsumerState : null, splashColor: Theme.of(context).extension()!.highlight, onPressed: () { - final managerProvider = ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(widget.walletId); - final isRefreshing = ref.read(managerProvider).isRefreshing; - if (!isRefreshing) { - _spinController?.repeat(); - ref - .read(managerProvider) - .refresh() - .then((_) => _spinController?.stop()); + if (widget.tokenContractAddress == null) { + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(widget.walletId); + final isRefreshing = ref.read(managerProvider).isRefreshing; + if (!isRefreshing) { + _spinController?.repeat(); + ref + .read(managerProvider) + .refresh() + .then((_) => _spinController?.stop()); + } + } else { + if (!ref.read(tokenServiceProvider)!.isRefreshing) { + ref.read(tokenServiceProvider)!.refresh(); + } } }, elevation: 0, diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 0f1a96308..80acee0ad 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -111,10 +111,13 @@ class EthTokenTx { } class EthereumResponse { + EthereumResponse(this.value, this.exception); + final T? value; final Exception? exception; - EthereumResponse(this.value, this.exception); + @override + toString() => "EthereumResponse{ value: $value, exception: $exception"; } abstract class EthereumAPI { diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index ca26fbecd..933975c42 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -14,6 +14,7 @@ import 'package:stackwallet/models/token_balance.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.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/eth_token_cache.dart'; import 'package:stackwallet/services/node_service.dart'; @@ -196,6 +197,14 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { if (!_refreshLock) { _refreshLock = true; try { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + ethWallet.walletId + token.contractAddress, + coin, + ), + ); + await refreshCachedBalance(); await _refreshTransactions(); } catch (e, s) { @@ -205,6 +214,13 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { ); } finally { _refreshLock = false; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + ethWallet.walletId + token.contractAddress, + coin, + ), + ); notifyListeners(); } } @@ -217,7 +233,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { params: [_credentials.address]); print("=========================================="); - print("balanceRequest: $balanceRequest"); + print("${token.name} balanceRequest: $balanceRequest"); print("=========================================="); String _balance = balanceRequest.first.toString(); diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 262875cee..310848543 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -278,6 +278,13 @@ class _SVG { } } + String iconForToken({required String contractAddress}) { + switch (contractAddress) { + default: + return ethereum; + } + } + // big icons String bitcoinImage(BuildContext context) => "assets/images/${Theme.of(context).extension()!.themeType.name}/bitcoin.svg"; From 6771e39e8a6ca99c0129911d72192cce38d9bf85 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 11:59:18 -0600 Subject: [PATCH 045/208] forest theme icon color fixes --- .../wallet_view/sub_widgets/wallet_navigation_bar.dart | 3 +++ lib/utilities/theme/forest_colors.dart | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) 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 18ed2c5cb..173bee009 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -357,6 +357,9 @@ class _WalletNavigationBarState extends ConsumerState { const Spacer(), SvgPicture.asset( Assets.svg.tokens, + color: Theme.of(context) + .extension()! + .accentColorDark, width: 24, height: 24, ), diff --git a/lib/utilities/theme/forest_colors.dart b/lib/utilities/theme/forest_colors.dart index 62e7f029a..e4be1cfd2 100644 --- a/lib/utilities/theme/forest_colors.dart +++ b/lib/utilities/theme/forest_colors.dart @@ -175,12 +175,12 @@ class ForestColors extends StackColorTheme { @override Color get bottomNavIconBack => const Color(0xFFA7C7CF); @override - Color get bottomNavIconIcon => const Color(0xFF227386); + Color get bottomNavIconIcon => accentColorDark; //const Color(0xFF227386); @override - Color get topNavIconPrimary => const Color(0xFF227386); + Color get topNavIconPrimary => accentColorDark; //const Color(0xFF227386); @override - Color get topNavIconGreen => const Color(0xFF00A591); + Color get topNavIconGreen => accentColorDark; //const Color(0xFF00A591); @override Color get topNavIconYellow => const Color(0xFFFDD33A); @override From b6f58e4ce15f93caf546c65587ef65b089f51549 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 11:59:31 -0600 Subject: [PATCH 046/208] refresh button null error fix --- .../sub_widgets/wallet_refresh_button.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart index 3811fdb7b..453762b80 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart @@ -66,7 +66,19 @@ class _RefreshButtonState extends ConsumerState eventBus.on().listen( (event) async { if (event.walletId == widget.walletId && - widget.tokenContractAddress == null || + widget.tokenContractAddress == null) { + switch (event.newStatus) { + case WalletSyncStatus.unableToSync: + _spinController?.stop(); + break; + case WalletSyncStatus.synced: + _spinController?.stop(); + break; + case WalletSyncStatus.syncing: + unawaited(_spinController?.repeat()); + break; + } + } else if (widget.tokenContractAddress != null && event.walletId == widget.walletId + widget.tokenContractAddress!) { switch (event.newStatus) { case WalletSyncStatus.unableToSync: From 0956ee31377c8deaf064c66fb04eccf26ee48428 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 16:01:54 -0600 Subject: [PATCH 047/208] update eth icon --- assets/svg/coin_icons/Ethereum.svg | 40 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/assets/svg/coin_icons/Ethereum.svg b/assets/svg/coin_icons/Ethereum.svg index 684e96873..77020df7b 100644 --- a/assets/svg/coin_icons/Ethereum.svg +++ b/assets/svg/coin_icons/Ethereum.svg @@ -1,21 +1,21 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + From 24e66f3d5f32035d3e156953c11205dfd1b9ff17 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 16:43:09 -0600 Subject: [PATCH 048/208] WIP add wallet coin options lists --- .../add_wallet_view/add_wallet_view.dart | 142 ++++++++++++++++-- .../sub_widgets/coin_select_item.dart | 29 ++-- .../sub_widgets/mobile_coin_list.dart | 77 +++++++--- .../sub_widgets/next_button.dart | 6 +- .../sub_widgets/searchable_coin_list.dart | 38 +++-- .../create_or_restore_wallet_view.dart | 18 +-- .../ui/add_wallet_selected_coin_provider.dart | 15 +- lib/route_generator.dart | 5 +- lib/utilities/default_eth_tokens.dart | 49 ++++++ 9 files changed, 286 insertions(+), 93 deletions(-) create mode 100644 lib/utilities/default_eth_tokens.dart diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index a0190cfef..fd698679a 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -1,14 +1,17 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_text.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_eth_tokens.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -17,27 +20,35 @@ import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -class AddWalletView extends StatefulWidget { +class AddWalletView extends ConsumerStatefulWidget { const AddWalletView({Key? key}) : super(key: key); static const routeName = "/addWallet"; @override - State createState() => _AddWalletViewState(); + ConsumerState createState() => _AddWalletViewState(); } -class _AddWalletViewState extends State { +class _AddWalletViewState extends ConsumerState { late final TextEditingController _searchFieldController; late final FocusNode _searchFocusNode; String _searchTerm = ""; - final List coins = [...Coin.values]; + final List _coinsTestnet = [ + ...Coin.values.sublist(Coin.values.length - kTestNetCoinCount - 1), + ]; + final List _coins = [ + ...Coin.values.sublist(0, Coin.values.length - kTestNetCoinCount - 1) + ]; + final List coinEntities = []; + final List tokenEntities = []; final bool isDesktop = Util.isDesktop; @@ -45,13 +56,23 @@ class _AddWalletViewState extends State { void initState() { _searchFieldController = TextEditingController(); _searchFocusNode = FocusNode(); - coins.remove(Coin.firoTestNet); + _coinsTestnet.remove(Coin.firoTestNet); if (isDesktop) { - coins.remove(Coin.wownero); + _coins.remove(Coin.wownero); if (Platform.isWindows) { - coins.remove(Coin.monero); + _coins.remove(Coin.monero); } } + + coinEntities.addAll(_coins.map((e) => CoinEntity(e))); + + if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) { + coinEntities.addAll(_coinsTestnet.map((e) => CoinEntity(e))); + } + + tokenEntities.addAll(DefaultTokens.list.map((e) => EthTokenEntity(e))); + tokenEntities.sort((a, b) => a.name.compareTo(b.name)); + super.initState(); } @@ -166,7 +187,7 @@ class _AddWalletViewState extends State { ), Expanded( child: SearchableCoinList( - coins: coins, + entities: coinEntities, isDesktop: true, searchTerm: _searchTerm, ), @@ -218,8 +239,21 @@ class _AddWalletViewState extends State { height: 16, ), Expanded( - child: MobileCoinList( - coins: coins, + child: SingleChildScrollView( + child: Column( + children: [ + ExpandingSubListItem( + title: "Coins", + entities: coinEntities, + initialState: ExpandableState.expanded, + ), + ExpandingSubListItem( + title: "Tokens", + entities: tokenEntities, + initialState: ExpandableState.collapsed, + ), + ], + ), ), ), const SizedBox( @@ -237,3 +271,91 @@ class _AddWalletViewState extends State { } } } + +class ExpandingSubListItem extends StatefulWidget { + const ExpandingSubListItem({ + Key? key, + required this.title, + required this.entities, + required this.initialState, + }) : super(key: key); + + final String title; + final List entities; + final ExpandableState initialState; + + @override + State createState() => _ExpandingSubListItemState(); +} + +class _ExpandingSubListItemState extends State { + final isDesktop = Util.isDesktop; + + late final ExpandableController _controller; + + late bool _expandedState; + + @override + void initState() { + _expandedState = widget.initialState == ExpandableState.expanded; + _controller = ExpandableController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_expandedState) { + _controller.toggle?.call(); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Expandable( + controller: _controller, + onExpandChanged: (state) { + setState(() { + _expandedState = state == ExpandableState.expanded; + }); + }, + header: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8.0, + bottom: 8.0, + right: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + SvgPicture.asset( + _expandedState ? Assets.svg.chevronUp : Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + ], + ), + ), + ), + body: SingleChildScrollView( + primary: false, + child: MobileCoinList( + entities: widget.entities, + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart index 87ba95e64..0ccd713db 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -12,30 +12,28 @@ import 'package:stackwallet/utilities/util.dart'; class CoinSelectItem extends ConsumerWidget { const CoinSelectItem({ Key? key, - required this.coin, + required this.entity, }) : super(key: key); - final Coin coin; + final AddWalletListEntity entity; @override Widget build(BuildContext context, WidgetRef ref) { - debugPrint("BUILD: CoinSelectItem for ${coin.name}"); - final selectedCoin = ref.watch(addWalletSelectedCoinStateProvider); + debugPrint("BUILD: CoinSelectItem for ${entity.name}"); + final selectedEntity = ref.watch(addWalletSelectedEntityStateProvider); final isDesktop = Util.isDesktop; return Container( decoration: BoxDecoration( - // color: selectedCoin == coin ? CFColors.selection : CFColors.white, - color: selectedCoin == coin + color: selectedEntity == entity ? Theme.of(context).extension()!.textFieldActiveBG : Theme.of(context).extension()!.popupBG, borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ), child: MaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - key: Key("coinSelectItemButtonKey_${coin.name}"), + key: Key("coinSelectItemButtonKey_${entity.name}${entity.ticker}"), padding: isDesktop ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(12), @@ -51,7 +49,7 @@ class CoinSelectItem extends ConsumerWidget { child: Row( children: [ SvgPicture.asset( - Assets.svg.iconFor(coin: coin), + Assets.svg.iconFor(coin: entity.coin), width: 26, height: 26, ), @@ -59,15 +57,15 @@ class CoinSelectItem extends ConsumerWidget { width: isDesktop ? 12 : 10, ), Text( - coin.prettyName, + "${entity.name} (${entity.ticker})", style: isDesktop ? STextStyles.desktopTextMedium(context) : STextStyles.subtitle600(context).copyWith( fontSize: 14, ), ), - if (isDesktop && selectedCoin == coin) const Spacer(), - if (isDesktop && selectedCoin == coin) + if (isDesktop && selectedEntity == entity) const Spacer(), + if (isDesktop && selectedEntity == entity) Padding( padding: const EdgeInsets.only( right: 18, @@ -86,8 +84,9 @@ class CoinSelectItem extends ConsumerWidget { ], ), ), - onPressed: () => - ref.read(addWalletSelectedCoinStateProvider.state).state = coin, + onPressed: () { + ref.read(addWalletSelectedEntityStateProvider.state).state = entity; + }, ), ); } diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart index fd950963c..573d9d75f 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart @@ -1,38 +1,73 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; class MobileCoinList extends StatelessWidget { const MobileCoinList({ Key? key, - required this.coins, + required this.entities, }) : super(key: key); - final List coins; + final List entities; @override Widget build(BuildContext context) { - return Consumer( - builder: (_, ref, __) { - bool showTestNet = ref.watch( - prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), - ); - - return ListView.builder( - itemCount: - showTestNet ? coins.length : coins.length - (kTestNetCoinCount), - itemBuilder: (ctx, index) { - return Padding( - padding: const EdgeInsets.all(4), - child: CoinSelectItem( - coin: coins[index], - ), - ); - }, + return ListView.builder( + shrinkWrap: true, + itemCount: entities.length, + primary: false, + itemBuilder: (ctx, index) { + return Padding( + padding: const EdgeInsets.all(4), + child: CoinSelectItem( + entity: entities[index], + ), ); }, ); } } + +abstract class AddWalletListEntity extends Equatable { + Coin get coin; + String get name; + String get ticker; +} + +class EthTokenEntity extends AddWalletListEntity { + EthTokenEntity(this.token); + + final EthToken token; + + @override + Coin get coin => Coin.ethereum; + + @override + String get name => token.name; + + @override + String get ticker => token.symbol; + + @override + List get props => [coin, name, ticker, token.contractAddress]; +} + +class CoinEntity extends AddWalletListEntity { + CoinEntity(this._coin); + + final Coin _coin; + + @override + Coin get coin => _coin; + + @override + String get name => coin.prettyName; + + @override + String get ticker => coin.ticker; + + @override + List get props => [coin, name, ticker]; +} diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart index 939f16a31..40081474c 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart @@ -17,7 +17,7 @@ class AddWalletNextButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: NextButton"); final selectedCoin = - ref.watch(addWalletSelectedCoinStateProvider.state).state; + ref.watch(addWalletSelectedEntityStateProvider.state).state; final enabled = selectedCoin != null; @@ -26,10 +26,8 @@ class AddWalletNextButton extends ConsumerWidget { ? null : () { final selectedCoin = - ref.read(addWalletSelectedCoinStateProvider.state).state; + ref.read(addWalletSelectedEntityStateProvider.state).state; - //todo: check if print needed - // debugPrint("Next pressed with ${selectedCoin!.name} selected!"); Navigator.of(context).pushNamed( CreateOrRestoreWalletView.routeName, arguments: selectedCoin, diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart index 935b5f231..0bf73f9f3 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart @@ -1,43 +1,41 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; class SearchableCoinList extends ConsumerWidget { const SearchableCoinList({ Key? key, - required this.coins, + required this.entities, required this.isDesktop, required this.searchTerm, }) : super(key: key); - final List coins; + final List entities; final bool isDesktop; final String searchTerm; - List filterCoins(String text, bool showTestNetCoins) { - final _coins = [...coins]; + List filterCoins(String text, bool showTestNetCoins) { + final _entities = [...entities]; if (text.isNotEmpty) { final lowercaseTerm = text.toLowerCase(); - _coins.retainWhere((e) => - e.ticker.toLowerCase().contains(lowercaseTerm) || - e.prettyName.toLowerCase().contains(lowercaseTerm) || - e.name.toLowerCase().contains(lowercaseTerm)); + _entities.retainWhere( + (e) => + e.ticker.toLowerCase().contains(lowercaseTerm) || + e.name.toLowerCase().contains(lowercaseTerm) || + e.coin.name.toLowerCase().contains(lowercaseTerm) || + (e is EthTokenEntity && + e.token.contractAddress.toLowerCase().contains(lowercaseTerm)), + ); } if (!showTestNetCoins) { - _coins.removeWhere( + _entities.removeWhere( (e) => e.name.endsWith("TestNet") || e == Coin.bitcoincashTestnet); } - // remove firo testnet regardless - _coins.remove(Coin.firoTestNet); - // Kidgloves for Wownero on desktop - // if(isDesktop) { - // _coins.remove(Coin.wownero); - // } - - return _coins; + return _entities; } @override @@ -46,15 +44,15 @@ class SearchableCoinList extends ConsumerWidget { prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), ); - final _coins = filterCoins(searchTerm, showTestNet); + final _entities = filterCoins(searchTerm, showTestNet); return ListView.builder( - itemCount: _coins.length, + itemCount: _entities.length, itemBuilder: (ctx, index) { return Padding( padding: const EdgeInsets.all(4), child: CoinSelectItem( - coin: _coins[index], + entity: _entities[index], ), ); }, diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart index 206e5e787..bf1db760c 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_or_restore_wallet_subtitle.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_or_restore_wallet_title.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_wallet_button_group.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -15,12 +15,12 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; class CreateOrRestoreWalletView extends StatelessWidget { const CreateOrRestoreWalletView({ Key? key, - required this.coin, + required this.entity, }) : super(key: key); static const routeName = "/createOrRestoreWallet"; - final Coin coin; + final AddWalletListEntity entity; @override Widget build(BuildContext context) { @@ -44,7 +44,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { flex: 10, ), CreateRestoreWalletTitle( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), const SizedBox( @@ -60,14 +60,14 @@ class CreateOrRestoreWalletView extends StatelessWidget { height: 32, ), CoinImage( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), const SizedBox( height: 32, ), CreateWalletButtonGroup( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), const Spacer( @@ -99,7 +99,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { Padding( padding: const EdgeInsets.all(31), child: CoinImage( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), ), @@ -107,7 +107,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { flex: 2, ), CreateRestoreWalletTitle( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), const SizedBox( @@ -120,7 +120,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { flex: 5, ), CreateWalletButtonGroup( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), ], diff --git a/lib/providers/ui/add_wallet_selected_coin_provider.dart b/lib/providers/ui/add_wallet_selected_coin_provider.dart index 4e2f77cdd..acbad9c7b 100644 --- a/lib/providers/ui/add_wallet_selected_coin_provider.dart +++ b/lib/providers/ui/add_wallet_selected_coin_provider.dart @@ -1,14 +1,5 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; -int _count = 0; - -final addWalletSelectedCoinStateProvider = - StateProvider.autoDispose((_) { - if (kDebugMode) { - _count++; - } - - return null; -}); +final addWalletSelectedEntityStateProvider = + StateProvider.autoDispose((_) => null); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 1b3c5b9ab..ea2cd76c1 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; @@ -654,11 +655,11 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case CreateOrRestoreWalletView.routeName: - if (args is Coin) { + if (args is AddWalletListEntity) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => CreateOrRestoreWalletView( - coin: args, + entity: args, ), settings: RouteSettings( name: settings.name, diff --git a/lib/utilities/default_eth_tokens.dart b/lib/utilities/default_eth_tokens.dart new file mode 100644 index 000000000..2a253fb56 --- /dev/null +++ b/lib/utilities/default_eth_tokens.dart @@ -0,0 +1,49 @@ +import 'package:stackwallet/models/ethereum/erc20_token.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; + +abstract class DefaultTokens { + static List list = [ + Erc20Token( + contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + name: "USD Coin", + symbol: "USDC", + decimals: 18, + balance: 0, + ), + Erc20Token( + contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", + name: "Tether", + symbol: "USDT", + decimals: 18, + balance: 0, + ), + Erc20Token( + contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + name: "Shiba Inu", + symbol: "SHIB", + decimals: 18, + balance: 0, + ), + Erc20Token( + contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + name: "BNB Token", + symbol: "BNB", + decimals: 18, + balance: 0, + ), + Erc20Token( + contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", + name: "BUSD", + symbol: "BUSD", + decimals: 18, + balance: 0, + ), + Erc20Token( + contractAddress: "0x514910771af9ca656af840dff83e8264ecf986ca", + name: "Chainlink", + symbol: "LINK", + decimals: 18, + balance: 0, + ), + ]; +} From 305b2525737305db6f6f8294154dd756c5548c1b Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 16:49:20 -0600 Subject: [PATCH 049/208] replace 'net_*' calls with 'eth_*' alternatives --- lib/services/coins/ethereum/ethereum_wallet.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 37c79dc5c..ff8a47bba 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -173,9 +173,9 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future confirmSend({required Map txData}) async { web3.Web3Client client = getEthClient(); - final int chainId = await client.getNetworkId(); - final amount = txData['recipientAmt']; - final decimalAmount = Format.satoshisToAmount(amount as int, coin: coin); + final chainId = await client.getChainId(); + final amount = txData['recipientAmt'] as int; + final decimalAmount = Format.satoshisToAmount(amount, coin: coin); final bigIntAmount = amountToBigInt( decimalAmount.toDouble(), Constants.decimalPlacesForCoin(coin), @@ -187,8 +187,8 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { web3.EtherUnit.wei, txData['feeInWei']), maxGas: _gasLimit, value: web3.EtherAmount.inWei(bigIntAmount)); - final transaction = - await client.sendTransaction(_credentials, tx, chainId: chainId); + final transaction = await client.sendTransaction(_credentials, tx, + chainId: chainId.toInt()); return transaction; } @@ -773,8 +773,8 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { Future testNetworkConnection() async { web3.Web3Client client = getEthClient(); try { - final result = await client.isListeningForNetwork(); - return result; + await client.getBlockNumber(); + return true; } catch (_) { return false; } From d07906c4a713aa6b541a5c40c12b734f7518aaf3 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 17:15:05 -0600 Subject: [PATCH 050/208] add wallet entity clean up --- .../add_wallet_list_entity.dart | 8 + .../sub_classes/coin_entity.dart | 20 ++ .../sub_classes/eth_token_entity.dart | 21 ++ .../add_wallet_view/add_wallet_view.dart | 192 +++++++++--------- .../sub_widgets/add_wallet_entity_list.dart | 29 +++ .../sub_widgets/coin_select_item.dart | 2 +- .../sub_widgets/expanding_sub_list_item.dart | 97 +++++++++ .../sub_widgets/mobile_coin_list.dart | 73 ------- .../sub_widgets/searchable_coin_list.dart | 61 ------ .../create_or_restore_wallet_view.dart | 2 +- .../ui/add_wallet_selected_coin_provider.dart | 2 +- lib/route_generator.dart | 2 +- 12 files changed, 275 insertions(+), 234 deletions(-) create mode 100644 lib/models/add_wallet_list_entity/add_wallet_list_entity.dart create mode 100644 lib/models/add_wallet_list_entity/sub_classes/coin_entity.dart create mode 100644 lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart create mode 100644 lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart create mode 100644 lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart delete mode 100644 lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart delete mode 100644 lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart diff --git a/lib/models/add_wallet_list_entity/add_wallet_list_entity.dart b/lib/models/add_wallet_list_entity/add_wallet_list_entity.dart new file mode 100644 index 000000000..3dd24d7b1 --- /dev/null +++ b/lib/models/add_wallet_list_entity/add_wallet_list_entity.dart @@ -0,0 +1,8 @@ +import 'package:equatable/equatable.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +abstract class AddWalletListEntity extends Equatable { + Coin get coin; + String get name; + String get ticker; +} diff --git a/lib/models/add_wallet_list_entity/sub_classes/coin_entity.dart b/lib/models/add_wallet_list_entity/sub_classes/coin_entity.dart new file mode 100644 index 000000000..770a9d1cf --- /dev/null +++ b/lib/models/add_wallet_list_entity/sub_classes/coin_entity.dart @@ -0,0 +1,20 @@ +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class CoinEntity extends AddWalletListEntity { + CoinEntity(this._coin); + + final Coin _coin; + + @override + Coin get coin => _coin; + + @override + String get name => coin.prettyName; + + @override + String get ticker => coin.ticker; + + @override + List get props => [coin, name, ticker]; +} diff --git a/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart b/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart new file mode 100644 index 000000000..8392d5f82 --- /dev/null +++ b/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart @@ -0,0 +1,21 @@ +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class EthTokenEntity extends AddWalletListEntity { + EthTokenEntity(this.token); + + final EthToken token; + + @override + Coin get coin => Coin.ethereum; + + @override + String get name => token.name; + + @override + String get ticker => token.symbol; + + @override + List get props => [coin, name, ticker, token.contractAddress]; +} diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index fd698679a..1047417a0 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -3,10 +3,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_text.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -52,6 +54,26 @@ class _AddWalletViewState extends ConsumerState { final bool isDesktop = Util.isDesktop; + List filter( + String text, + List entities, + ) { + final _entities = [...entities]; + if (text.isNotEmpty) { + final lowercaseTerm = text.toLowerCase(); + _entities.retainWhere( + (e) => + e.ticker.toLowerCase().contains(lowercaseTerm) || + e.name.toLowerCase().contains(lowercaseTerm) || + e.coin.name.toLowerCase().contains(lowercaseTerm) || + (e is EthTokenEntity && + e.token.contractAddress.toLowerCase().contains(lowercaseTerm)), + ); + } + + return _entities; + } + @override void initState() { _searchFieldController = TextEditingController(); @@ -186,10 +208,21 @@ class _AddWalletViewState extends ConsumerState { ), ), Expanded( - child: SearchableCoinList( - entities: coinEntities, - isDesktop: true, - searchTerm: _searchTerm, + child: SingleChildScrollView( + child: Column( + children: [ + ExpandingSubListItem( + title: "Coins", + entities: filter(_searchTerm, coinEntities), + initialState: ExpandableState.expanded, + ), + ExpandingSubListItem( + title: "Tokens", + entities: filter(_searchTerm, tokenEntities), + initialState: ExpandableState.collapsed, + ), + ], + ), ), ), ], @@ -238,18 +271,73 @@ class _AddWalletViewState extends ConsumerState { const SizedBox( height: 16, ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: (value) => setState(() => _searchTerm = value), + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchFieldController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 10, + ), Expanded( child: SingleChildScrollView( child: Column( children: [ ExpandingSubListItem( title: "Coins", - entities: coinEntities, + entities: filter(_searchTerm, coinEntities), initialState: ExpandableState.expanded, ), ExpandingSubListItem( title: "Tokens", - entities: tokenEntities, + entities: filter(_searchTerm, tokenEntities), initialState: ExpandableState.collapsed, ), ], @@ -271,91 +359,3 @@ class _AddWalletViewState extends ConsumerState { } } } - -class ExpandingSubListItem extends StatefulWidget { - const ExpandingSubListItem({ - Key? key, - required this.title, - required this.entities, - required this.initialState, - }) : super(key: key); - - final String title; - final List entities; - final ExpandableState initialState; - - @override - State createState() => _ExpandingSubListItemState(); -} - -class _ExpandingSubListItemState extends State { - final isDesktop = Util.isDesktop; - - late final ExpandableController _controller; - - late bool _expandedState; - - @override - void initState() { - _expandedState = widget.initialState == ExpandableState.expanded; - _controller = ExpandableController(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_expandedState) { - _controller.toggle?.call(); - } - }); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Expandable( - controller: _controller, - onExpandChanged: (state) { - setState(() { - _expandedState = state == ExpandableState.expanded; - }); - }, - header: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 8.0, - bottom: 8.0, - right: 10, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.title, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ) - : STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - SvgPicture.asset( - _expandedState ? Assets.svg.chevronUp : Assets.svg.chevronDown, - width: 12, - height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - ], - ), - ), - ), - body: SingleChildScrollView( - primary: false, - child: MobileCoinList( - entities: widget.entities, - ), - ), - ); - } -} diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart new file mode 100644 index 000000000..590a8c8ce --- /dev/null +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart'; + +class AddWalletEntityList extends StatelessWidget { + const AddWalletEntityList({ + Key? key, + required this.entities, + }) : super(key: key); + + final List entities; + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: entities.length, + itemBuilder: (ctx, index) { + return Padding( + padding: const EdgeInsets.all(4), + child: CoinSelectItem( + entity: entities[index], + ), + ); + }, + ); + } +} diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart index 0ccd713db..1a7ca829f 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart new file mode 100644 index 000000000..3097f1e75 --- /dev/null +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/expandable.dart'; + +class ExpandingSubListItem extends StatefulWidget { + const ExpandingSubListItem({ + Key? key, + required this.title, + required this.entities, + required this.initialState, + }) : super(key: key); + + final String title; + final List entities; + final ExpandableState initialState; + + @override + State createState() => _ExpandingSubListItemState(); +} + +class _ExpandingSubListItemState extends State { + final isDesktop = Util.isDesktop; + + late final ExpandableController _controller; + + late bool _expandedState; + + @override + void initState() { + _expandedState = widget.initialState == ExpandableState.expanded; + _controller = ExpandableController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_expandedState) { + _controller.toggle?.call(); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Expandable( + controller: _controller, + onExpandChanged: (state) { + setState(() { + _expandedState = state == ExpandableState.expanded; + }); + }, + header: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8.0, + bottom: 8.0, + right: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + SvgPicture.asset( + _expandedState ? Assets.svg.chevronUp : Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + ], + ), + ), + ), + body: SingleChildScrollView( + primary: false, + child: AddWalletEntityList( + entities: widget.entities, + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart deleted file mode 100644 index 573d9d75f..000000000 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; - -class MobileCoinList extends StatelessWidget { - const MobileCoinList({ - Key? key, - required this.entities, - }) : super(key: key); - - final List entities; - - @override - Widget build(BuildContext context) { - return ListView.builder( - shrinkWrap: true, - itemCount: entities.length, - primary: false, - itemBuilder: (ctx, index) { - return Padding( - padding: const EdgeInsets.all(4), - child: CoinSelectItem( - entity: entities[index], - ), - ); - }, - ); - } -} - -abstract class AddWalletListEntity extends Equatable { - Coin get coin; - String get name; - String get ticker; -} - -class EthTokenEntity extends AddWalletListEntity { - EthTokenEntity(this.token); - - final EthToken token; - - @override - Coin get coin => Coin.ethereum; - - @override - String get name => token.name; - - @override - String get ticker => token.symbol; - - @override - List get props => [coin, name, ticker, token.contractAddress]; -} - -class CoinEntity extends AddWalletListEntity { - CoinEntity(this._coin); - - final Coin _coin; - - @override - Coin get coin => _coin; - - @override - String get name => coin.prettyName; - - @override - String get ticker => coin.ticker; - - @override - List get props => [coin, name, ticker]; -} diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart deleted file mode 100644 index 0bf73f9f3..000000000 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; - -class SearchableCoinList extends ConsumerWidget { - const SearchableCoinList({ - Key? key, - required this.entities, - required this.isDesktop, - required this.searchTerm, - }) : super(key: key); - - final List entities; - final bool isDesktop; - final String searchTerm; - - List filterCoins(String text, bool showTestNetCoins) { - final _entities = [...entities]; - if (text.isNotEmpty) { - final lowercaseTerm = text.toLowerCase(); - _entities.retainWhere( - (e) => - e.ticker.toLowerCase().contains(lowercaseTerm) || - e.name.toLowerCase().contains(lowercaseTerm) || - e.coin.name.toLowerCase().contains(lowercaseTerm) || - (e is EthTokenEntity && - e.token.contractAddress.toLowerCase().contains(lowercaseTerm)), - ); - } - if (!showTestNetCoins) { - _entities.removeWhere( - (e) => e.name.endsWith("TestNet") || e == Coin.bitcoincashTestnet); - } - - return _entities; - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - bool showTestNet = ref.watch( - prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), - ); - - final _entities = filterCoins(searchTerm, showTestNet); - - return ListView.builder( - itemCount: _entities.length, - itemBuilder: (ctx, index) { - return Padding( - padding: const EdgeInsets.all(4), - child: CoinSelectItem( - entity: _entities[index], - ), - ); - }, - ); - } -} diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart index bf1db760c..32c4b239e 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_or_restore_wallet_subtitle.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_or_restore_wallet_title.dart'; diff --git a/lib/providers/ui/add_wallet_selected_coin_provider.dart b/lib/providers/ui/add_wallet_selected_coin_provider.dart index acbad9c7b..6acf51db8 100644 --- a/lib/providers/ui/add_wallet_selected_coin_provider.dart +++ b/lib/providers/ui/add_wallet_selected_coin_provider.dart @@ -1,5 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; final addWalletSelectedEntityStateProvider = StateProvider.autoDispose((_) => null); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index ea2cd76c1..140147f9c 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -2,6 +2,7 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; @@ -12,7 +13,6 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; From e3dbc64f17862019ebab48411254b4d49a56ac39 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 17:25:37 -0600 Subject: [PATCH 051/208] add custom animation curve param to expandable.dart --- .../add_wallet_view/sub_widgets/expanding_sub_list_item.dart | 2 ++ lib/widgets/expandable.dart | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart index 3097f1e75..39f45d589 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart @@ -46,6 +46,8 @@ class _ExpandingSubListItemState extends State { @override Widget build(BuildContext context) { return Expandable( + animationDurationMultiplier: 0.1 * widget.entities.length, + curve: Curves.easeInOutCubicEmphasized, controller: _controller, onExpandChanged: (state) { setState(() { diff --git a/lib/widgets/expandable.dart b/lib/widgets/expandable.dart index b60226a50..c5f669d4e 100644 --- a/lib/widgets/expandable.dart +++ b/lib/widgets/expandable.dart @@ -21,6 +21,7 @@ class Expandable extends StatefulWidget { this.onExpandChanged, this.controller, this.expandOverride, + this.curve = Curves.easeInOut, }) : super(key: key); final Widget header; @@ -31,6 +32,7 @@ class Expandable extends StatefulWidget { final void Function(ExpandableState)? onExpandChanged; final ExpandableController? controller; final VoidCallback? expandOverride; + final Curve curve; @override State createState() => _ExpandableState(); @@ -73,7 +75,7 @@ class _ExpandableState extends State with TickerProviderStateMixin { animation = widget.animation ?? Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( - curve: Curves.easeInOut, + curve: widget.curve, parent: animationController, ), ); From 658708da95761dd389518ed2a11d30139c29de4f Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Feb 2023 17:51:22 -0600 Subject: [PATCH 052/208] add custom icon rotate widget --- .../sub_widgets/expanding_sub_list_item.dart | 57 ++++++++++----- lib/widgets/animated_widgets/rotate_icon.dart | 72 +++++++++++++++++++ lib/widgets/expandable.dart | 4 ++ 3 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 lib/widgets/animated_widgets/rotate_icon.dart diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart index 39f45d589..f2cb410d3 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; import 'package:stackwallet/widgets/expandable.dart'; class ExpandingSubListItem extends StatefulWidget { @@ -14,11 +15,17 @@ class ExpandingSubListItem extends StatefulWidget { required this.title, required this.entities, required this.initialState, - }) : super(key: key); + double? animationDurationMultiplier, + this.curve = Curves.easeInOutCubicEmphasized, + }) : animationDurationMultiplier = + animationDurationMultiplier ?? entities.length * 0.11, + super(key: key); final String title; final List entities; final ExpandableState initialState; + final double animationDurationMultiplier; + final Curve curve; @override State createState() => _ExpandingSubListItemState(); @@ -28,31 +35,40 @@ class _ExpandingSubListItemState extends State { final isDesktop = Util.isDesktop; late final ExpandableController _controller; - - late bool _expandedState; + late final RotateIconController _rotateIconController; @override void initState() { - _expandedState = widget.initialState == ExpandableState.expanded; _controller = ExpandableController(); + _rotateIconController = RotateIconController(); WidgetsBinding.instance.addPostFrameCallback((_) { - if (_expandedState) { + if (widget.initialState == ExpandableState.expanded) { _controller.toggle?.call(); } }); super.initState(); } + @override + void dispose() { + _controller.toggle = null; + _rotateIconController.forward = null; + _rotateIconController.reverse = null; + super.dispose(); + } + @override Widget build(BuildContext context) { return Expandable( - animationDurationMultiplier: 0.1 * widget.entities.length, - curve: Curves.easeInOutCubicEmphasized, + animationDurationMultiplier: widget.animationDurationMultiplier, + curve: widget.curve, controller: _controller, - onExpandChanged: (state) { - setState(() { - _expandedState = state == ExpandableState.expanded; - }); + onExpandWillChange: (state) { + if (state == ExpandableState.expanded) { + _rotateIconController.forward?.call(); + } else { + _rotateIconController.reverse?.call(); + } }, header: Container( color: Colors.transparent, @@ -76,13 +92,18 @@ class _ExpandingSubListItemState extends State { : STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - SvgPicture.asset( - _expandedState ? Assets.svg.chevronUp : Assets.svg.chevronDown, - width: 12, - height: 6, - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + RotateIcon( + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + curve: widget.curve, + animationDurationMultiplier: widget.animationDurationMultiplier, + controller: _rotateIconController, ), ], ), diff --git a/lib/widgets/animated_widgets/rotate_icon.dart b/lib/widgets/animated_widgets/rotate_icon.dart new file mode 100644 index 000000000..c818628d0 --- /dev/null +++ b/lib/widgets/animated_widgets/rotate_icon.dart @@ -0,0 +1,72 @@ +import 'package:flutter/widgets.dart'; + +class RotateIconController { + VoidCallback? forward; + VoidCallback? reverse; +} + +class RotateIcon extends StatefulWidget { + const RotateIcon({ + Key? key, + required this.icon, + required this.curve, + this.controller, + this.animationDurationMultiplier = 1.0, + this.rotationPercent = 0.5, + }) : super(key: key); + + final Widget icon; + final Curve curve; + final RotateIconController? controller; + final double animationDurationMultiplier; + final double rotationPercent; + + @override + State createState() => _RotateIconState(); +} + +class _RotateIconState extends State + with SingleTickerProviderStateMixin { + late final AnimationController animationController; + late final Animation animation; + late final Duration duration; + + @override + void initState() { + duration = Duration( + milliseconds: (500 * widget.animationDurationMultiplier).toInt(), + ); + animationController = AnimationController( + vsync: this, + duration: duration, + ); + animation = Tween( + begin: 0.0, + end: widget.rotationPercent, + ).animate( + CurvedAnimation( + curve: widget.curve, + parent: animationController, + ), + ); + + widget.controller?.forward = animationController.forward; + widget.controller?.reverse = animationController.reverse; + + super.initState(); + } + + @override + void dispose() { + animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RotationTransition( + turns: animation, + child: widget.icon, + ); + } +} diff --git a/lib/widgets/expandable.dart b/lib/widgets/expandable.dart index c5f669d4e..aa438e34f 100644 --- a/lib/widgets/expandable.dart +++ b/lib/widgets/expandable.dart @@ -19,6 +19,7 @@ class Expandable extends StatefulWidget { this.animation, this.animationDurationMultiplier = 1.0, this.onExpandChanged, + this.onExpandWillChange, this.controller, this.expandOverride, this.curve = Curves.easeInOut, @@ -30,6 +31,7 @@ class Expandable extends StatefulWidget { final Animation? animation; final double animationDurationMultiplier; final void Function(ExpandableState)? onExpandChanged; + final void Function(ExpandableState)? onExpandWillChange; final ExpandableController? controller; final VoidCallback? expandOverride; final Curve curve; @@ -48,10 +50,12 @@ class _ExpandableState extends State with TickerProviderStateMixin { Future toggle() async { if (animation.isDismissed) { + widget.onExpandWillChange?.call(ExpandableState.expanded); await animationController.forward(); _toggleState = ExpandableState.expanded; widget.onExpandChanged?.call(_toggleState); } else if (animation.isCompleted) { + widget.onExpandWillChange?.call(ExpandableState.collapsed); await animationController.reverse(); _toggleState = ExpandableState.collapsed; widget.onExpandChanged?.call(_toggleState); From 1cdd3338f3c4d0d401386741fab50309e391a2c0 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 09:07:41 -0600 Subject: [PATCH 053/208] fade bottom of transaction list on mobile --- .../sub_widgets/transactions_list.dart | 19 ++++++++-- lib/pages/wallet_view/wallet_view.dart | 38 +++++++++++++------ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/lib/pages/wallet_view/sub_widgets/transactions_list.dart b/lib/pages/wallet_view/sub_widgets/transactions_list.dart index 0c48e69b6..3d9b9c95e 100644 --- a/lib/pages/wallet_view/sub_widgets/transactions_list.dart +++ b/lib/pages/wallet_view/sub_widgets/transactions_list.dart @@ -5,11 +5,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -20,8 +22,6 @@ import 'package:stackwallet/widgets/trade_card.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; import 'package:tuple/tuple.dart'; -import '../../../utilities/enums/coin_enum.dart'; - class TransactionsList extends ConsumerStatefulWidget { const TransactionsList({ Key? key, @@ -281,17 +281,30 @@ class _TransactionsListState extends ConsumerState { itemCount: _transactions2.length, itemBuilder: (context, index) { BorderRadius? radius; + bool shouldWrap = false; if (_transactions2.length == 1) { radius = BorderRadius.circular( Constants.size.circularBorderRadius, ); } else if (index == _transactions2.length - 1) { radius = _borderRadiusLast; + shouldWrap = true; } else if (index == 0) { radius = _borderRadiusFirst; } final tx = _transactions2[index]; - return itemBuilder(context, tx, radius, manager.coin); + if (shouldWrap) { + return Column( + children: [ + itemBuilder(context, tx, radius, manager.coin), + const SizedBox( + height: WalletView.navBarHeight + 14, + ), + ], + ); + } else { + return itemBuilder(context, tx, radius, manager.coin); + } }, ), ); diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index c8b832875..5937930d1 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -666,18 +666,34 @@ class _WalletViewState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Padding( - padding: const EdgeInsets.only(bottom: 14), - child: ClipRRect( - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - bottom: Radius.circular( - // WalletView.navBarHeight / 2.0, - Constants.size.circularBorderRadius, - ), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, ), + bottom: Radius.circular( + // WalletView.navBarHeight / 2.0, + Constants.size.circularBorderRadius, + ), + ), + child: ShaderMask( + blendMode: BlendMode.dstOut, + shaderCallback: (Rect bounds) { + return const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.transparent, + Colors.white, + ], + stops: [ + 0.0, + 0.8, + 1.0, + ], // 10% purple, 80% transparent, 10% purple + ).createShader(bounds); + }, child: Container( decoration: BoxDecoration( color: Colors.transparent, From 4239187602d9c9cb1243f821db90ba4ab775a95b Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 09:33:56 -0600 Subject: [PATCH 054/208] eth images --- assets/images/dark/ethereum.svg | 174 +++++++++++++++++++++++++ assets/images/fruitSorbet/ethereum.svg | 174 +++++++++++++++++++++++++ assets/images/light/ethereum.svg | 174 +++++++++++++++++++++++++ assets/images/oceanBreeze/ethereum.svg | 174 +++++++++++++++++++++++++ assets/images/oledBlack/ethereum.svg | 174 +++++++++++++++++++++++++ lib/utilities/assets.dart | 4 +- 6 files changed, 873 insertions(+), 1 deletion(-) create mode 100644 assets/images/dark/ethereum.svg create mode 100644 assets/images/fruitSorbet/ethereum.svg create mode 100644 assets/images/light/ethereum.svg create mode 100644 assets/images/oceanBreeze/ethereum.svg create mode 100644 assets/images/oledBlack/ethereum.svg diff --git a/assets/images/dark/ethereum.svg b/assets/images/dark/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/images/dark/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/fruitSorbet/ethereum.svg b/assets/images/fruitSorbet/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/images/fruitSorbet/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/light/ethereum.svg b/assets/images/light/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/images/light/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/oceanBreeze/ethereum.svg b/assets/images/oceanBreeze/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/images/oceanBreeze/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/oledBlack/ethereum.svg b/assets/images/oledBlack/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/images/oledBlack/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 310848543..20d70cee9 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -294,6 +294,8 @@ class _SVG { "assets/images/${Theme.of(context).extension()!.themeType.name}/doge.svg"; String epicCashImage(BuildContext context) => "assets/images/${Theme.of(context).extension()!.themeType.name}/epic-cash.svg"; + String ethereumImage(BuildContext context) => + "assets/images/${Theme.of(context).extension()!.themeType.name}/ethereum.svg"; String firoImage(BuildContext context) => "assets/images/${Theme.of(context).extension()!.themeType.name}/firo.svg"; String litecoinImage(BuildContext context) => @@ -339,7 +341,7 @@ class _SVG { case Coin.dogecoinTestNet: return dogecoinImage(context); case Coin.ethereum: - return ethereum; + return ethereumImage(context); } } } From f1bfe72b73dd84b93355bfe791ddfd89d6431cb1 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 10:36:24 -0600 Subject: [PATCH 055/208] eth token price service updates --- .../global_settings_view/hidden_settings.dart | 20 ++++-- lib/services/price.dart | 65 +++++++++++++++++-- lib/services/price_service.dart | 22 +++++++ 3 files changed, 99 insertions(+), 8 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 3d16e20b4..1f8d80c81 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_api.dart'; +import 'package:stackwallet/services/price_service.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -156,9 +156,21 @@ class HiddenSettings extends StatelessWidget { Consumer(builder: (_, ref, __) { return GestureDetector( onTap: () async { - final x = await MajesticBankAPI.instance - .getLimit(fromCurrency: 'btc'); - print(x); + PriceService.tokenContractAddressesToCheck.add( + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + PriceService.tokenContractAddressesToCheck.add( + "0xdAC17F958D2ee523a2206206994597C13D831ec7"); + await ref + .read(priceAnd24hChangeNotifierProvider) + .updatePrice(); + + final x = ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice( + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + + print( + "PRICE 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48: $x"); }, child: RoundedWhiteContainer( child: Text( diff --git a/lib/services/price.dart b/lib/services/price.dart index 7219b1b96..1863c3b74 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -86,10 +86,12 @@ class PriceAPI { } Map> result = {}; try { - final uri = Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,ethereum,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"); - // final uri = Uri.parse( - // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero%2Cbitcoin%2Cepic-cash%2Czcoin%2Cdogecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); + final uri = + Uri.parse("https://api.coingecko.com/api/v3/coins/markets?vs_currency" + "=${baseCurrency.toLowerCase()}" + "&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false"); final coinGeckoResponse = await client.get( uri, @@ -146,4 +148,59 @@ class PriceAPI { return null; } } + + Future>> + getPricesAnd24hChangeForEthTokens({ + required Set contractAddresses, + required String baseCurrency, + }) async { + final Map> tokenPrices = {}; + + if (contractAddresses.isEmpty) return tokenPrices; + + final externalCalls = Prefs.instance.externalCalls; + if ((!Logger.isTestEnv && !externalCalls) || + !(await Prefs.instance.isExternalCallsSet())) { + Logging.instance.log("User does not want to use external calls", + level: LogLevel.Info); + return tokenPrices; + } + + try { + final contractAddressesString = + contractAddresses.reduce((value, element) => "$value,$element"); + final uri = Uri.parse( + "https://api.coingecko.com/api/v3/simple/token_price/ethereum" + "?vs_currencies=${baseCurrency.toLowerCase()}&contract_addresses" + "=$contractAddressesString&include_24hr_change=true"); + + final coinGeckoResponse = await client.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map; + + for (final key in coinGeckoData.keys) { + final contractAddress = key as String; + + final map = coinGeckoData[contractAddress] as Map; + + final price = Decimal.parse(map[baseCurrency.toLowerCase()].toString()); + final change24h = double.parse( + map["${baseCurrency.toLowerCase()}_24h_change"].toString()); + + tokenPrices[contractAddress] = Tuple2(price, change24h); + } + + return tokenPrices; + } catch (e, s) { + Logging.instance.log( + "getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddresses): $e\n$s", + level: LogLevel.Error, + ); + // return previous cached values + return tokenPrices; + } + } } diff --git a/lib/services/price_service.dart b/lib/services/price_service.dart index eb2b1eba4..ee5b5a9d7 100644 --- a/lib/services/price_service.dart +++ b/lib/services/price_service.dart @@ -9,16 +9,24 @@ import 'package:tuple/tuple.dart'; class PriceService extends ChangeNotifier { late final String baseTicker; + static Set tokenContractAddressesToCheck = {}; final Duration updateInterval = const Duration(seconds: 60); Timer? _timer; final Map> _cachedPrices = { for (final coin in Coin.values) coin: Tuple2(Decimal.zero, 0.0) }; + + final Map> _cachedTokenPrices = {}; + final _priceAPI = PriceAPI(Client()); Tuple2 getPrice(Coin coin) => _cachedPrices[coin]!; + Tuple2 getTokenPrice(String contractAddress) => + _cachedTokenPrices[contractAddress.toLowerCase()] ?? + Tuple2(Decimal.zero, 0); + PriceService(this.baseTicker); Future updatePrice() async { @@ -33,6 +41,20 @@ class PriceService extends ChangeNotifier { } } + if (tokenContractAddressesToCheck.isNotEmpty) { + final tokenPriceMap = await _priceAPI.getPricesAnd24hChangeForEthTokens( + contractAddresses: tokenContractAddressesToCheck, + baseCurrency: baseTicker, + ); + + for (final map in tokenPriceMap.entries) { + if (_cachedTokenPrices[map.key] != map.value) { + _cachedTokenPrices[map.key] = map.value; + shouldNotify = true; + } + } + } + if (shouldNotify) { notifyListeners(); } From 6a734e28f0c5a1a9f91a9aab1cae7b174302ab1f Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 11:02:38 -0600 Subject: [PATCH 056/208] basic add token view with correct navigation --- .../add_token_view/add_token_view.dart | 310 ++++++++++++++++++ .../verify_recovery_phrase_view.dart | 9 + lib/pages/token_view/all_tokens_view.dart | 16 - lib/pages/token_view/my_tokens_view.dart | 17 +- .../sub_widgets/token_summary_info.dart | 290 ---------------- lib/route_generator.dart | 7 + 6 files changed, 334 insertions(+), 315 deletions(-) create mode 100644 lib/pages/add_wallet_views/add_token_view/add_token_view.dart delete mode 100644 lib/pages/token_view/all_tokens_view.dart delete mode 100644 lib/pages/token_view/sub_widgets/token_summary_info.dart diff --git a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart new file mode 100644 index 000000000..85df2a3b6 --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart @@ -0,0 +1,310 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_text.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_eth_tokens.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class AddTokenView extends ConsumerStatefulWidget { + const AddTokenView({ + Key? key, + }) : super(key: key); + + static const routeName = "/addToken"; + + @override + ConsumerState createState() => _AddTokenViewState(); +} + +class _AddTokenViewState extends ConsumerState { + late final TextEditingController _searchFieldController; + late final FocusNode _searchFocusNode; + + String _searchTerm = ""; + + final List tokenEntities = []; + + final bool isDesktop = Util.isDesktop; + + List filter( + String text, + List entities, + ) { + final _entities = [...entities]; + if (text.isNotEmpty) { + final lowercaseTerm = text.toLowerCase(); + _entities.retainWhere( + (e) => + e.ticker.toLowerCase().contains(lowercaseTerm) || + e.name.toLowerCase().contains(lowercaseTerm) || + (e is EthTokenEntity && + e.token.contractAddress.toLowerCase().contains(lowercaseTerm)), + ); + } + + return _entities; + } + + @override + void initState() { + _searchFieldController = TextEditingController(); + _searchFocusNode = FocusNode(); + + tokenEntities.addAll(DefaultTokens.list.map((e) => EthTokenEntity(e))); + tokenEntities.sort((a, b) => a.name.compareTo(b.name)); + + super.initState(); + } + + @override + void dispose() { + _searchFieldController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + if (isDesktop) { + return DesktopScaffold( + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + trailing: ExitToMyStackButton(), + ), + body: Column( + children: [ + const AddWalletText( + isDesktop: true, + ), + const SizedBox( + height: 16, + ), + Expanded( + child: SizedBox( + width: 480, + child: RoundedWhiteContainer( + radiusMultiplier: 2, + padding: const EdgeInsets.only( + left: 16, + top: 16, + right: 16, + bottom: 0, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: + STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.symmetric( + vertical: 10, + ), + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + // vertical: 20, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, + ), + ), + suffixIcon: _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 10), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon( + width: 24, + height: 24, + ), + onTap: () async { + setState(() { + _searchFieldController.text = + ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + Expanded( + child: AddWalletEntityList( + entities: filter(_searchTerm, tokenEntities), + ), + ), + ], + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + const SizedBox( + height: 70, + width: 480, + child: AddWalletNextButton( + isDesktop: true, + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ); + } else { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: Container( + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const AddWalletText( + isDesktop: false, + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: (value) => setState(() => _searchTerm = value), + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchFieldController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 10, + ), + Expanded( + child: AddWalletEntityList( + entities: filter(_searchTerm, tokenEntities), + ), + ), + const SizedBox( + height: 16, + ), + const AddWalletNextButton( + isDesktop: false, + ), + ], + ), + ), + ), + ), + ); + } + } +} diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 643783369..cc1dc5d20 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; @@ -14,6 +15,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -107,6 +109,13 @@ class _VerifyRecoveryPhraseViewState (route) => false, ), ); + if (widget.manager.coin == Coin.ethereum) { + unawaited( + Navigator.of(context).pushNamed( + AddTokenView.routeName, + ), + ); + } } } diff --git a/lib/pages/token_view/all_tokens_view.dart b/lib/pages/token_view/all_tokens_view.dart deleted file mode 100644 index 6d80ff2c7..000000000 --- a/lib/pages/token_view/all_tokens_view.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class AllTokensView extends ConsumerStatefulWidget { - const AllTokensView({ - Key? key, - }) : super(key: key); - - static const String routeName = "/allTokens"; - - @override - ConsumerState createState() { - // TODO: implement createState - throw UnimplementedError(); - } -} diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index bd54a5283..e8c489043 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; -import 'package:stackwallet/pages/token_view/all_tokens_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_tokens_list.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -41,13 +41,11 @@ class MyTokensView extends ConsumerStatefulWidget { class _TokenDetailsViewState extends ConsumerState { late final String walletAddress; - // late final String walletName; late final TextEditingController _searchController; final searchFieldFocusNode = FocusNode(); @override void initState() { - // walletAddress = widget.walletAddress; _searchController = TextEditingController(); super.initState(); @@ -133,7 +131,7 @@ class _TokenDetailsViewState extends ConsumerState { child: AspectRatio( aspectRatio: 1, child: AppBarIconButton( - key: const Key("transactionSearchFilterViewButton"), + key: const Key("addTokenAppBarIconButtonKey"), size: 36, shadows: const [], color: Theme.of(context) @@ -149,7 +147,7 @@ class _TokenDetailsViewState extends ConsumerState { ), onPressed: () { Navigator.of(context).pushNamed( - AllTokensView.routeName, + AddTokenView.routeName, ); }, ), @@ -258,10 +256,11 @@ class _TokenDetailsViewState extends ConsumerState { ), Expanded( child: MyTokensList( - managerProvider: widget.managerProvider, - walletId: widget.walletId, - tokens: widget.tokens, - walletAddress: widget.walletAddress), + managerProvider: widget.managerProvider, + walletId: widget.walletId, + tokens: widget.tokens, + walletAddress: widget.walletAddress, + ), ), ], ), diff --git a/lib/pages/token_view/sub_widgets/token_summary_info.dart b/lib/pages/token_view/sub_widgets/token_summary_info.dart deleted file mode 100644 index 1c3c23dda..000000000 --- a/lib/pages/token_view/sub_widgets/token_summary_info.dart +++ /dev/null @@ -1,290 +0,0 @@ -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/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 managerProvider; - final WalletSyncStatus initialSyncStatus; - - @override - State createState() => _TokenSummaryInfoState(); -} - -class _TokenSummaryInfoState extends State { - late final String walletId; - late final ChangeNotifierProvider managerProvider; - - void showSheet() { - showModalBottomSheet( - 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? totalBalanceFuture; - Future? availableBalanceFuture; - - 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)); - - 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 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()! - .textFavoriteCard, - ), - ), - Text( - "${_showAvailable ? "Available" : "Full"} Balance", - style: - STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - const SizedBox( - width: 4, - ), - SvgPicture.asset( - Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .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()! - .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()! - .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()! - .textFavoriteCard, - ), - ), - if (coin != Coin.firo && coin != Coin.firoTestNet) - Text( - "${_showAvailable ? "Available" : "Full"} Balance", - style: - STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), - const SizedBox( - width: 4, - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension()! - .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()! - .textFavoriteCard, - ), - ), - AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .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, - ), - ], - ) - ], - ); - } -} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 140147f9c..ff9018ce9 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; @@ -203,6 +204,12 @@ class RouteGenerator { builder: (_) => const AddWalletView(), settings: RouteSettings(name: settings.name)); + case AddTokenView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const AddTokenView(), + settings: RouteSettings(name: settings.name)); + case PaynymClaimView.routeName: if (args is String) { return getRoute( From 695d43bbd5116d182bb90c739b38d133e0ca35e2 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 13:26:17 -0600 Subject: [PATCH 057/208] add token list ui --- lib/models/ethereum/eth_token.dart | 11 +++ .../add_token_view/add_token_view.dart | 84 ++++++++++++----- .../sub_widgets/add_token_list.dart | 85 +++++++++++++++++ .../sub_widgets/add_token_list_element.dart | 94 +++++++++++++++++++ .../sub_widgets/add_token_text.dart | 49 ++++++++++ .../verify_recovery_phrase_view.dart | 1 + lib/pages/token_view/my_tokens_view.dart | 1 + lib/route_generator.dart | 14 ++- 8 files changed, 311 insertions(+), 28 deletions(-) create mode 100644 lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart create mode 100644 lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart create mode 100644 lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart diff --git a/lib/models/ethereum/eth_token.dart b/lib/models/ethereum/eth_token.dart index 748fac307..00351a77b 100644 --- a/lib/models/ethereum/eth_token.dart +++ b/lib/models/ethereum/eth_token.dart @@ -12,4 +12,15 @@ class EthToken { final String symbol; final int decimals; final int balance; + + @override + String toString() { + return "$runtimeType: { " + "name: $name, " + "symbol: $symbol, " + "contractAddress: $contractAddress, " + "decimals: $decimals, " + "balance: $balance" + " }"; + } } diff --git a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart index 85df2a3b6..131a37f82 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; -import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_text.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_eth_tokens.dart'; @@ -17,6 +16,7 @@ import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -25,8 +25,11 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class AddTokenView extends ConsumerStatefulWidget { const AddTokenView({ Key? key, + required this.walletId, }) : super(key: key); + final String walletId; + static const routeName = "/addToken"; @override @@ -39,36 +42,42 @@ class _AddTokenViewState extends ConsumerState { String _searchTerm = ""; - final List tokenEntities = []; + final List tokenEntities = []; final bool isDesktop = Util.isDesktop; - List filter( + List filter( String text, - List entities, + List entities, ) { final _entities = [...entities]; if (text.isNotEmpty) { final lowercaseTerm = text.toLowerCase(); _entities.retainWhere( (e) => - e.ticker.toLowerCase().contains(lowercaseTerm) || - e.name.toLowerCase().contains(lowercaseTerm) || - (e is EthTokenEntity && - e.token.contractAddress.toLowerCase().contains(lowercaseTerm)), + e.token.name.toLowerCase().contains(lowercaseTerm) || + e.token.symbol.toLowerCase().contains(lowercaseTerm) || + e.token.contractAddress.toLowerCase().contains(lowercaseTerm), ); } return _entities; } + void onNextPressed() { + final selectedTokens = + tokenEntities.where((e) => e.selected).map((e) => e.token); + print("SELECTED TOKENS: $selectedTokens"); + } + @override void initState() { _searchFieldController = TextEditingController(); _searchFocusNode = FocusNode(); - tokenEntities.addAll(DefaultTokens.list.map((e) => EthTokenEntity(e))); - tokenEntities.sort((a, b) => a.name.compareTo(b.name)); + tokenEntities + .addAll(DefaultTokens.list.map((e) => AddTokenListElementData(e))); + tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); super.initState(); } @@ -83,6 +92,8 @@ class _AddTokenViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + final walletName = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).walletName)); if (isDesktop) { return DesktopScaffold( @@ -93,8 +104,9 @@ class _AddTokenViewState extends ConsumerState { ), body: Column( children: [ - const AddWalletText( + AddTokenText( isDesktop: true, + walletName: walletName, ), const SizedBox( height: 16, @@ -183,8 +195,8 @@ class _AddTokenViewState extends ConsumerState { ), ), Expanded( - child: AddWalletEntityList( - entities: filter(_searchTerm, tokenEntities), + child: AddTokenList( + items: filter(_searchTerm, tokenEntities), ), ), ], @@ -195,11 +207,12 @@ class _AddTokenViewState extends ConsumerState { const SizedBox( height: 16, ), - const SizedBox( + SizedBox( height: 70, width: 480, - child: AddWalletNextButton( - isDesktop: true, + child: PrimaryButton( + label: "Next", + onPressed: onNextPressed, ), ), const SizedBox( @@ -219,6 +232,25 @@ class _AddTokenViewState extends ConsumerState { Navigator.of(context).pop(); }, ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: AppBarIconButton( + icon: SvgPicture.asset( + Assets.svg.circlePlusFilled, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + // todo add custom token + }, + ), + ), + ), + ], ), body: Container( color: Theme.of(context).extension()!.background, @@ -227,8 +259,9 @@ class _AddTokenViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const AddWalletText( + AddTokenText( isDesktop: false, + walletName: walletName, ), const SizedBox( height: 16, @@ -289,15 +322,16 @@ class _AddTokenViewState extends ConsumerState { height: 10, ), Expanded( - child: AddWalletEntityList( - entities: filter(_searchTerm, tokenEntities), + child: AddTokenList( + items: filter(_searchTerm, tokenEntities), ), ), const SizedBox( height: 16, ), - const AddWalletNextButton( - isDesktop: false, + PrimaryButton( + label: "Next", + onPressed: onNextPressed, ), ], ), diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart new file mode 100644 index 000000000..1fd0c13d2 --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; + +class AddTokenList extends StatelessWidget { + const AddTokenList({ + Key? key, + required this.items, + }) : super(key: key); + + final List items; + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: items.length, + itemBuilder: (ctx, index) { + return ConditionalParent( + condition: index == items.length - 1, + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + Padding( + padding: const EdgeInsets.all(4), + child: RawMaterialButton( + fillColor: + Theme.of(context).extension()!.popupBG, + elevation: 0, + focusElevation: 0, + hoverElevation: 0, + highlightElevation: 0, + constraints: const BoxConstraints(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + // todo add custom token + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circlePlusFilled, + color: Theme.of(context) + .extension()! + .textDark, + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Text( + "Add custom token", + style: STextStyles.w600_14(context), + ), + ], + ), + ), + ), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: AddTokenListElement( + data: items[index], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart new file mode 100644 index 000000000..ecf19fa85 --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.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/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class AddTokenListElementData { + AddTokenListElementData(this.token); + + final EthToken token; + bool selected = false; +} + +class AddTokenListElement extends StatefulWidget { + const AddTokenListElement({Key? key, required this.data}) : super(key: key); + + final AddTokenListElementData data; + + @override + State createState() => _AddTokenListElementState(); +} + +class _AddTokenListElementState extends State { + @override + Widget build(BuildContext context) { + final currency = ExchangeDataLoadingService.instance.isar.currencies + .where() + .tickerExchangeNameEqualToAnyName( + "${widget.data.token.symbol}ERC20", + ChangeNowExchange.exchangeName, + ) + .or() + .tickerExchangeNameEqualToAnyName( + widget.data.token.symbol, + ChangeNowExchange.exchangeName, + ) + .or() + .tickerExchangeNameEqualToAnyName( + "${widget.data.token.symbol}BSC", + ChangeNowExchange.exchangeName, + ) + .filter() + .imageIsNotEmpty() + .findFirstSync(); + return RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + currency != null + ? SvgPicture.network( + currency.image, + width: 24, + height: 24, + ) + : Container( + width: 24, + height: 24, + color: Colors.red, + ), + const SizedBox( + width: 12, + ), + Text( + "${widget.data.token.name} (${widget.data.token.symbol})", + style: STextStyles.w600_14(context), + overflow: TextOverflow.ellipsis, + ), + ], + ), + const SizedBox( + width: 4, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: widget.data.selected, + onValueChanged: (newValue) { + widget.data.selected = newValue; + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart new file mode 100644 index 000000000..c11e6295a --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; + +class AddTokenText extends StatelessWidget { + const AddTokenText({ + Key? key, + required this.isDesktop, + required this.walletName, + }) : super(key: key); + + final String walletName; + final bool isDesktop; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + walletName, + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.sectionLabelMedium12(context) // todo: fixme + : STextStyles.sectionLabelMedium12(context), + ), + const SizedBox( + height: 4, + ), + Text( + "Add Tokens", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "You can also do it later in your wallet", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), + ), + ], + ); + } +} diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index cc1dc5d20..64324fb20 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -113,6 +113,7 @@ class _VerifyRecoveryPhraseViewState unawaited( Navigator.of(context).pushNamed( AddTokenView.routeName, + arguments: widget.manager.walletId, ), ); } diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index e8c489043..fc89b5037 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -148,6 +148,7 @@ class _TokenDetailsViewState extends ConsumerState { onPressed: () { Navigator.of(context).pushNamed( AddTokenView.routeName, + arguments: widget.walletId, ); }, ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index ff9018ce9..9f7dece46 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -205,10 +205,18 @@ class RouteGenerator { settings: RouteSettings(name: settings.name)); case AddTokenView.routeName: - return getRoute( + if (args is String) { + return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => const AddTokenView(), - settings: RouteSettings(name: settings.name)); + builder: (_) => AddTokenView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); case PaynymClaimView.routeName: if (args is String) { From 1d97be9f73bf906f437e0b712ee21dd74c47f553 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 14:26:13 -0600 Subject: [PATCH 058/208] hard coded bnb icon --- assets/svg/coin_icons/bnb_icon.svg | 161 +++++++++++++++++++++++++++++ lib/utilities/assets.dart | 2 + 2 files changed, 163 insertions(+) create mode 100644 assets/svg/coin_icons/bnb_icon.svg diff --git a/assets/svg/coin_icons/bnb_icon.svg b/assets/svg/coin_icons/bnb_icon.svg new file mode 100644 index 000000000..d32653823 --- /dev/null +++ b/assets/svg/coin_icons/bnb_icon.svg @@ -0,0 +1,161 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 20d70cee9..308c7b49a 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -228,6 +228,8 @@ class _SVG { String get namecoin => "assets/svg/coin_icons/Namecoin.svg"; String get particl => "assets/svg/coin_icons/Particl.svg"; + String get bnbIcon => "assets/svg/coin_icons/bnb_icon.svg"; + String get chevronRight => "assets/svg/chevron-right.svg"; String get minimize => "assets/svg/minimize.svg"; String get walletFa => "assets/svg/wallet-fa.svg"; From 0c11e859aa97f7b96954ce99821db35ca6ab4358 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 14:27:42 -0600 Subject: [PATCH 059/208] changenow v2 currencies call to access tokenContract param --- lib/models/isar/exchange_cache/currency.dart | 16 +- .../isar/exchange_cache/currency.g.dart | 205 ++++++++++++++++++ .../models/blockchain_data/address.g.dart | 2 + .../models/blockchain_data/transaction.g.dart | 2 + .../sub_widgets/add_token_list_element.dart | 28 +-- .../exchange/change_now/change_now_api.dart | 83 +++++++ .../change_now/change_now_exchange.dart | 9 +- .../majestic_bank/majestic_bank_exchange.dart | 1 + .../simpleswap/simpleswap_exchange.dart | 1 + .../pages/send_view/send_view_test.mocks.dart | 16 ++ .../exchange/exchange_view_test.mocks.dart | 17 ++ .../managed_favorite_test.mocks.dart | 16 ++ .../table_view/table_view_row_test.mocks.dart | 16 ++ .../transaction_card_test.mocks.dart | 15 ++ test/widget_tests/wallet_card_test.mocks.dart | 16 ++ ...et_info_row_balance_future_test.mocks.dart | 16 ++ .../wallet_info_row_test.mocks.dart | 16 ++ 17 files changed, 452 insertions(+), 23 deletions(-) diff --git a/lib/models/isar/exchange_cache/currency.dart b/lib/models/isar/exchange_cache/currency.dart index 81471a3d1..1744f9350 100644 --- a/lib/models/isar/exchange_cache/currency.dart +++ b/lib/models/isar/exchange_cache/currency.dart @@ -44,11 +44,16 @@ class Currency { @Index() final bool isStackCoin; - @ignore - bool get supportsFixedRate => rateType == SupportedRateType.fixed || rateType == SupportedRateType.both; + final String? tokenContract; @ignore - bool get supportsEstimatedRate => rateType == SupportedRateType.estimated || rateType == SupportedRateType.both; + bool get supportsFixedRate => + rateType == SupportedRateType.fixed || rateType == SupportedRateType.both; + + @ignore + bool get supportsEstimatedRate => + rateType == SupportedRateType.estimated || + rateType == SupportedRateType.both; Currency({ required this.exchangeName, @@ -61,6 +66,7 @@ class Currency { required this.rateType, this.isAvailable, required this.isStackCoin, + required this.tokenContract, }); factory Currency.fromJson( @@ -83,6 +89,7 @@ class Currency { isAvailable: json["isAvailable"] as bool?, isStackCoin: json["isStackCoin"] as bool? ?? Currency.checkIsStackCoin(ticker), + tokenContract: json["tokenContract"] as String?, )..id = json["id"] as int?; } catch (e) { rethrow; @@ -102,6 +109,7 @@ class Currency { "rateType": rateType, "isAvailable": isAvailable, "isStackCoin": isStackCoin, + "tokenContract": tokenContract, }; return map; @@ -119,6 +127,7 @@ class Currency { SupportedRateType? rateType, bool? isAvailable, bool? isStackCoin, + String? tokenContract, }) { return Currency( exchangeName: exchangeName ?? this.exchangeName, @@ -131,6 +140,7 @@ class Currency { rateType: rateType ?? this.rateType, isAvailable: isAvailable ?? this.isAvailable, isStackCoin: isStackCoin ?? this.isStackCoin, + tokenContract: tokenContract ?? this.tokenContract, )..id = id ?? this.id; } diff --git a/lib/models/isar/exchange_cache/currency.g.dart b/lib/models/isar/exchange_cache/currency.g.dart index 02c58cadd..8f81bfdc0 100644 --- a/lib/models/isar/exchange_cache/currency.g.dart +++ b/lib/models/isar/exchange_cache/currency.g.dart @@ -67,6 +67,11 @@ const CurrencySchema = CollectionSchema( id: 9, name: r'ticker', type: IsarType.string, + ), + r'tokenContract': PropertySchema( + id: 10, + name: r'tokenContract', + type: IsarType.string, ) }, estimateSize: _currencyEstimateSize, @@ -150,6 +155,12 @@ int _currencyEstimateSize( bytesCount += 3 + object.name.length * 3; bytesCount += 3 + object.network.length * 3; bytesCount += 3 + object.ticker.length * 3; + { + final value = object.tokenContract; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } return bytesCount; } @@ -169,6 +180,7 @@ void _currencySerialize( writer.writeString(offsets[7], object.network); writer.writeByte(offsets[8], object.rateType.index); writer.writeString(offsets[9], object.ticker); + writer.writeString(offsets[10], object.tokenContract); } Currency _currencyDeserialize( @@ -190,6 +202,7 @@ Currency _currencyDeserialize( _CurrencyrateTypeValueEnumMap[reader.readByteOrNull(offsets[8])] ?? SupportedRateType.fixed, ticker: reader.readString(offsets[9]), + tokenContract: reader.readStringOrNull(offsets[10]), ); object.id = id; return object; @@ -223,6 +236,8 @@ P _currencyDeserializeProp

( SupportedRateType.fixed) as P; case 9: return (reader.readString(offset)) as P; + case 10: + return (reader.readStringOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -1533,6 +1548,158 @@ extension CurrencyQueryFilter )); }); } + + QueryBuilder + tokenContractIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'tokenContract', + )); + }); + } + + QueryBuilder + tokenContractIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'tokenContract', + )); + }); + } + + QueryBuilder tokenContractEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenContractGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'tokenContract', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenContractStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'tokenContract', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenContractIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'tokenContract', + value: '', + )); + }); + } + + QueryBuilder + tokenContractIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'tokenContract', + value: '', + )); + }); + } } extension CurrencyQueryObject @@ -1661,6 +1828,18 @@ extension CurrencyQuerySortBy on QueryBuilder { return query.addSortBy(r'ticker', Sort.desc); }); } + + QueryBuilder sortByTokenContract() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tokenContract', Sort.asc); + }); + } + + QueryBuilder sortByTokenContractDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tokenContract', Sort.desc); + }); + } } extension CurrencyQuerySortThenBy @@ -1796,6 +1975,18 @@ extension CurrencyQuerySortThenBy return query.addSortBy(r'ticker', Sort.desc); }); } + + QueryBuilder thenByTokenContract() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tokenContract', Sort.asc); + }); + } + + QueryBuilder thenByTokenContractDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tokenContract', Sort.desc); + }); + } } extension CurrencyQueryWhereDistinct @@ -1865,6 +2056,14 @@ extension CurrencyQueryWhereDistinct return query.addDistinctBy(r'ticker', caseSensitive: caseSensitive); }); } + + QueryBuilder distinctByTokenContract( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'tokenContract', + caseSensitive: caseSensitive); + }); + } } extension CurrencyQueryProperty @@ -1935,4 +2134,10 @@ extension CurrencyQueryProperty return query.addPropertyName(r'ticker'); }); } + + QueryBuilder tokenContractProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'tokenContract'); + }); + } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index 37186584b..49188b395 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -260,6 +260,7 @@ const _AddresstypeEnumValueMap = { 'mimbleWimble': 4, 'unknown': 5, 'nonWallet': 6, + 'ethereum': 7, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -269,6 +270,7 @@ const _AddresstypeValueEnumMap = { 4: AddressType.mimbleWimble, 5: AddressType.unknown, 6: AddressType.nonWallet, + 7: AddressType.ethereum, }; Id _addressGetId(Address object) { diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index 53acce84f..b70e0eec7 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -330,12 +330,14 @@ const _TransactionsubTypeEnumValueMap = { 'bip47Notification': 1, 'mint': 2, 'join': 3, + 'ethToken': 4, }; const _TransactionsubTypeValueEnumMap = { 0: TransactionSubType.none, 1: TransactionSubType.bip47Notification, 2: TransactionSubType.mint, 3: TransactionSubType.join, + 4: TransactionSubType.ethToken, }; const _TransactiontypeEnumValueMap = { 'outgoing': 0, diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart index ecf19fa85..2dbdd7ce5 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.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/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -30,23 +31,16 @@ class _AddTokenListElementState extends State { Widget build(BuildContext context) { final currency = ExchangeDataLoadingService.instance.isar.currencies .where() - .tickerExchangeNameEqualToAnyName( - "${widget.data.token.symbol}ERC20", - ChangeNowExchange.exchangeName, - ) - .or() - .tickerExchangeNameEqualToAnyName( - widget.data.token.symbol, - ChangeNowExchange.exchangeName, - ) - .or() - .tickerExchangeNameEqualToAnyName( - "${widget.data.token.symbol}BSC", - ChangeNowExchange.exchangeName, - ) + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) .filter() + .tokenContractEqualTo( + widget.data.token.contractAddress, + caseSensitive: false, + ) + .and() .imageIsNotEmpty() .findFirstSync(); + return RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -59,10 +53,12 @@ class _AddTokenListElementState extends State { width: 24, height: 24, ) - : Container( + : SvgPicture.asset( + widget.data.token.symbol == "BNB" + ? Assets.svg.bnbIcon + : Assets.svg.ethereum, width: 24, height: 24, - color: Colors.red, ), const SizedBox( width: 12, diff --git a/lib/services/exchange/change_now/change_now_api.dart b/lib/services/exchange/change_now/change_now_api.dart index cc9a14182..57823a813 100644 --- a/lib/services/exchange/change_now/change_now_api.dart +++ b/lib/services/exchange/change_now/change_now_api.dart @@ -195,6 +195,89 @@ class ChangeNowAPI { } } + Future>> getCurrenciesV2( + // { + // bool? fixedRate, + // bool? active, + // } + ) async { + Map? params; + + // if (active != null || fixedRate != null) { + // params = {}; + // if (fixedRate != null) { + // params.addAll({"fixedRate": fixedRate.toString()}); + // } + // if (active != null) { + // params.addAll({"active": active.toString()}); + // } + // } + + final uri = _buildUriV2("/exchange/currencies", params); + + try { + // json array is expected here + final jsonArray = await _makeGetRequest(uri); + + try { + final result = await compute( + _parseV2CurrenciesJson, + jsonArray as List, + ); + return result; + } catch (e, s) { + Logging.instance.log("getAvailableCurrencies exception: $e\n$s", + level: LogLevel.Error); + return ExchangeResponse( + exception: ExchangeException( + "Error: $jsonArray", + ExchangeExceptionType.serializeResponseError, + ), + ); + } + } catch (e, s) { + Logging.instance.log("getAvailableCurrencies exception: $e\n$s", + level: LogLevel.Error); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + ExchangeResponse> _parseV2CurrenciesJson( + List args, + ) { + try { + List currencies = []; + + for (final json in args) { + try { + final map = Map.from(json as Map); + currencies.add( + Currency.fromJson( + map, + rateType: (map["supportsFixedRate"] as bool) + ? SupportedRateType.both + : SupportedRateType.estimated, + exchangeName: ChangeNowExchange.exchangeName, + ), + ); + } catch (_) { + return ExchangeResponse( + exception: ExchangeException("Failed to serialize $json", + ExchangeExceptionType.serializeResponseError)); + } + } + + return ExchangeResponse(value: currencies); + } catch (_) { + rethrow; + } + } + /// This API endpoint returns the array of markets available for the specified currency be default. /// The availability of a particular pair is determined by the 'isAvailable' field. /// diff --git a/lib/services/exchange/change_now/change_now_exchange.dart b/lib/services/exchange/change_now/change_now_exchange.dart index dd5baec94..3189ff84e 100644 --- a/lib/services/exchange/change_now/change_now_exchange.dart +++ b/lib/services/exchange/change_now/change_now_exchange.dart @@ -82,10 +82,11 @@ class ChangeNowExchange extends Exchange { Future>> getAllCurrencies( bool fixedRate, ) async { - return await ChangeNowAPI.instance.getAvailableCurrencies( - fixedRate: fixedRate ? true : null, - active: true, - ); + return await ChangeNowAPI.instance.getCurrenciesV2(); + // return await ChangeNowAPI.instance.getAvailableCurrencies( + // fixedRate: fixedRate ? true : null, + // active: true, + // ); } @override diff --git a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart index acb881c31..24403b722 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart @@ -131,6 +131,7 @@ class MajesticBankExchange extends Exchange { rateType: SupportedRateType.both, isAvailable: true, isStackCoin: Currency.checkIsStackCoin(limit.currency), + tokenContract: null, ); currencies.add(currency); } diff --git a/lib/services/exchange/simpleswap/simpleswap_exchange.dart b/lib/services/exchange/simpleswap/simpleswap_exchange.dart index 791ccb17c..1157bd09e 100644 --- a/lib/services/exchange/simpleswap/simpleswap_exchange.dart +++ b/lib/services/exchange/simpleswap/simpleswap_exchange.dart @@ -67,6 +67,7 @@ class SimpleSwapExchange extends Exchange { : SupportedRateType.estimated, isAvailable: true, isStackCoin: Currency.checkIsStackCoin(e.symbol), + tokenContract: null, ), ) .toList(); diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 0bd33fe78..50e97f600 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -1988,6 +1988,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { returnValue: _i22.Future<_i18.PaymentCode?>.value(), ) as _i22.Future<_i18.PaymentCode?>); @override + _i22.Future<_i18.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({ + required _i16.Transaction? transaction, + required _i16.Address? myNotificationAddress, + }) => + (super.noSuchMethod( + Invocation.method( + #unBlindedPaymentCodeFromTransactionBad, + [], + { + #transaction: transaction, + #myNotificationAddress: myNotificationAddress, + }, + ), + returnValue: _i22.Future<_i18.PaymentCode?>.value(), + ) as _i22.Future<_i18.PaymentCode?>); + @override _i22.Future> getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 767b18bb6..5ef51904d 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -662,6 +662,23 @@ class MockChangeNowAPI extends _i1.Mock implements _i11.ChangeNowAPI { )), ) as _i6.Future<_i2.ExchangeResponse>>); @override + _i6.Future<_i2.ExchangeResponse>> getCurrenciesV2() => + (super.noSuchMethod( + Invocation.method( + #getCurrenciesV2, + [], + ), + returnValue: + _i6.Future<_i2.ExchangeResponse>>.value( + _FakeExchangeResponse_0>( + this, + Invocation.method( + #getCurrenciesV2, + [], + ), + )), + ) as _i6.Future<_i2.ExchangeResponse>>); + @override _i6.Future<_i2.ExchangeResponse>> getPairedCurrencies({ required String? ticker, bool? fixedRate, diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 95a80031f..c3237983f 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -1780,6 +1780,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValue: _i22.Future<_i17.PaymentCode?>.value(), ) as _i22.Future<_i17.PaymentCode?>); @override + _i22.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({ + required _i15.Transaction? transaction, + required _i15.Address? myNotificationAddress, + }) => + (super.noSuchMethod( + Invocation.method( + #unBlindedPaymentCodeFromTransactionBad, + [], + { + #transaction: transaction, + #myNotificationAddress: myNotificationAddress, + }, + ), + returnValue: _i22.Future<_i17.PaymentCode?>.value(), + ) as _i22.Future<_i17.PaymentCode?>); + @override _i22.Future> getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index e4076b175..26b0bdfcc 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -1767,6 +1767,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { returnValue: _i21.Future<_i17.PaymentCode?>.value(), ) as _i21.Future<_i17.PaymentCode?>); @override + _i21.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({ + required _i15.Transaction? transaction, + required _i15.Address? myNotificationAddress, + }) => + (super.noSuchMethod( + Invocation.method( + #unBlindedPaymentCodeFromTransactionBad, + [], + { + #transaction: transaction, + #myNotificationAddress: myNotificationAddress, + }, + ), + returnValue: _i21.Future<_i17.PaymentCode?>.value(), + ) as _i21.Future<_i17.PaymentCode?>); + @override _i21.Future> getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index b26e5779d..5d0011b14 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -2454,6 +2454,21 @@ class MockPriceService extends _i1.Mock implements _i26.PriceService { ), ) as _i15.Tuple2<_i14.Decimal, double>); @override + _i15.Tuple2<_i14.Decimal, double> getTokenPrice(String? contractAddress) => + (super.noSuchMethod( + Invocation.method( + #getTokenPrice, + [contractAddress], + ), + returnValue: _FakeTuple2_13<_i14.Decimal, double>( + this, + Invocation.method( + #getTokenPrice, + [contractAddress], + ), + ), + ) as _i15.Tuple2<_i14.Decimal, double>); + @override _i18.Future updatePrice() => (super.noSuchMethod( Invocation.method( #updatePrice, diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index 696d8a0fd..e3a64d7da 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -1530,6 +1530,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { returnValue: _i20.Future<_i17.PaymentCode?>.value(), ) as _i20.Future<_i17.PaymentCode?>); @override + _i20.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({ + required _i15.Transaction? transaction, + required _i15.Address? myNotificationAddress, + }) => + (super.noSuchMethod( + Invocation.method( + #unBlindedPaymentCodeFromTransactionBad, + [], + { + #transaction: transaction, + #myNotificationAddress: myNotificationAddress, + }, + ), + returnValue: _i20.Future<_i17.PaymentCode?>.value(), + ) as _i20.Future<_i17.PaymentCode?>); + @override _i20.Future> getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index cf667bf2d..18da705aa 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -1779,6 +1779,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValue: _i22.Future<_i17.PaymentCode?>.value(), ) as _i22.Future<_i17.PaymentCode?>); @override + _i22.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({ + required _i15.Transaction? transaction, + required _i15.Address? myNotificationAddress, + }) => + (super.noSuchMethod( + Invocation.method( + #unBlindedPaymentCodeFromTransactionBad, + [], + { + #transaction: transaction, + #myNotificationAddress: myNotificationAddress, + }, + ), + returnValue: _i22.Future<_i17.PaymentCode?>.value(), + ) as _i22.Future<_i17.PaymentCode?>); + @override _i22.Future> getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 019047b9a..0f8c36d4a 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -1779,6 +1779,22 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValue: _i22.Future<_i17.PaymentCode?>.value(), ) as _i22.Future<_i17.PaymentCode?>); @override + _i22.Future<_i17.PaymentCode?> unBlindedPaymentCodeFromTransactionBad({ + required _i15.Transaction? transaction, + required _i15.Address? myNotificationAddress, + }) => + (super.noSuchMethod( + Invocation.method( + #unBlindedPaymentCodeFromTransactionBad, + [], + { + #transaction: transaction, + #myNotificationAddress: myNotificationAddress, + }, + ), + returnValue: _i22.Future<_i17.PaymentCode?>.value(), + ) as _i22.Future<_i17.PaymentCode?>); + @override _i22.Future> getAllPaymentCodesFromNotificationTransactions() => (super.noSuchMethod( Invocation.method( From 6b7eb8f463200d3448bf029ecc7e42f48f086d49 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 14:42:43 -0600 Subject: [PATCH 060/208] fix price tests to include ethereum --- test/price_test.dart | 68 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/test/price_test.dart b/test/price_test.dart index 6b98b67d1..42cc28c2c 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -26,7 +26,10 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids" + "=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash" + ",namecoin,wownero,ethereum,particl&order=market_cap_desc&per_page=50" + "&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -38,11 +41,26 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); - expect(price.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + expect( + price.toString(), + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], ' + 'Coin.dogecoin: [0.00000315, -2.68533], ' + 'Coin.epicCash: [0.00002803, 7.27524], ' + 'Coin.ethereum: [0, 0.0], ' + 'Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], ' + 'Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], ' + 'Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], ' + 'Coin.bitcoinTestNet: [0, 0.0],' + ' Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], ' + 'Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}', + ); verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" + "&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false", + ), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -53,7 +71,10 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&" + "ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -70,13 +91,21 @@ void main() { final cachedPrice = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); - expect(cachedPrice.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + expect( + cachedPrice.toString(), + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0],' + ' Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524],' + ' Coin.ethereum: [0, 0.0], Coin.firo: [0.0001096, -0.89304], ' + 'Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], ' + 'Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], ' + 'Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], ' + 'Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], ' + 'Coin.firoTestNet: [0, 0.0]}'); // verify only called once during filling of cache verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,ethereum,particl&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -87,7 +116,10 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" + "&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -100,7 +132,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.ethereum: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); test("no internet available", () async { @@ -108,7 +140,10 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" + "&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenThrow(const SocketException( @@ -119,8 +154,15 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); - expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + expect( + price.toString(), + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], ' + 'Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.ethereum: [0, 0.0],' + ' Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0],' + ' Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0],' + ' Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], ' + 'Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], ' + 'Coin.firoTestNet: [0, 0.0]}'); }); tearDown(() async { From 70335286be2fb66f1f9d639b10009d5641d79f82 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 16:03:36 -0600 Subject: [PATCH 061/208] custom eth api exception --- lib/services/ethereum/ethereum_api.dart | 54 ++++++++++++++++++------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 80acee0ad..e5d83fa2f 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -110,11 +110,20 @@ class EthTokenTx { } } +class EthApiException with Exception { + EthApiException(this.message); + + final String message; + + @override + String toString() => "$runtimeType: $message"; +} + class EthereumResponse { EthereumResponse(this.value, this.exception); final T? value; - final Exception? exception; + final EthApiException? exception; @override toString() => "EthereumResponse{ value: $value, exception: $exception"; @@ -176,14 +185,19 @@ abstract class EthereumAPI { null, ); } else { - throw Exception(json["message"] as String); + throw EthApiException(json["message"] as String); } } else { - throw Exception( + throw EthApiException( "getWalletTokens($address) failed with status code: " "${response.statusCode}", ); } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); } catch (e, s) { Logging.instance.log( "getWalletTokens(): $e\n$s", @@ -191,7 +205,7 @@ abstract class EthereumAPI { ); return EthereumResponse( null, - Exception(e.toString()), + EthApiException(e.toString()), ); } } @@ -233,7 +247,8 @@ abstract class EthereumAPI { ), ); } else { - throw Exception("Unsupported token type found: ${map["type"]}"); + throw EthApiException( + "Unsupported token type found: ${map["type"]}"); } } @@ -242,14 +257,19 @@ abstract class EthereumAPI { null, ); } else { - throw Exception(json["message"] as String); + throw EthApiException(json["message"] as String); } } else { - throw Exception( + throw EthApiException( "getWalletTokens($address) failed with status code: " "${response.statusCode}", ); } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); } catch (e, s) { Logging.instance.log( "getWalletTokens(): $e\n$s", @@ -257,7 +277,7 @@ abstract class EthereumAPI { ); return EthereumResponse( null, - Exception(e.toString()), + EthApiException(e.toString()), ); } } @@ -300,7 +320,7 @@ abstract class EthereumAPI { EthToken? token; if (map["type"] == "ERC-20") { token = Erc20Token( - balance: int.parse(map["balance"] as String), + balance: 0, //int.parse(map["balance"] as String), contractAddress: map["contractAddress"] as String, decimals: int.parse(map["decimals"] as String), name: map["name"] as String, @@ -308,14 +328,15 @@ abstract class EthereumAPI { ); } else if (map["type"] == "ERC-721") { token = Erc721Token( - balance: int.parse(map["balance"] as String), + balance: 0, //int.parse(map["balance"] as String), contractAddress: map["contractAddress"] as String, decimals: int.parse(map["decimals"] as String), name: map["name"] as String, symbol: map["symbol"] as String, ); } else { - throw Exception("Unsupported token type found: ${map["type"]}"); + throw EthApiException( + "Unsupported token type found: ${map["type"]}"); } return EthereumResponse( @@ -323,14 +344,19 @@ abstract class EthereumAPI { null, ); } else { - throw Exception(json["message"] as String); + throw EthApiException(json["message"] as String); } } else { - throw Exception( + throw EthApiException( "getTokenByContractAddress($contractAddress) failed with status code: " "${response.statusCode}", ); } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); } catch (e, s) { Logging.instance.log( "getWalletTokens(): $e\n$s", @@ -338,7 +364,7 @@ abstract class EthereumAPI { ); return EthereumResponse( null, - Exception(e.toString()), + EthApiException(e.toString()), ); } } From 0e4b664e63f57228ce1ffd2cdcdf0a2276e0b4c7 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 16:03:56 -0600 Subject: [PATCH 062/208] WIP custom token addition --- .../add_token_view/add_custom_token_view.dart | 189 ++++++++++++++++++ .../add_token_view/add_token_view.dart | 14 +- .../sub_widgets/add_token_list.dart | 12 +- lib/route_generator.dart | 15 ++ 4 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart new file mode 100644 index 000000000..f10c11d41 --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -0,0 +1,189 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class AddCustomTokenView extends ConsumerStatefulWidget { + const AddCustomTokenView({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + static const routeName = "/addCustomToken"; + + @override + ConsumerState createState() => _AddCustomTokenViewState(); +} + +class _AddCustomTokenViewState extends ConsumerState { + final isDesktop = Util.isDesktop; + + final contractController = TextEditingController(); + final nameController = TextEditingController(); + final symbolController = TextEditingController(); + final decimalsController = TextEditingController(); + + bool enableSubFields = false; + bool addTokenButtonEnabled = false; + + EthToken? currentToken; + + @override + Widget build(BuildContext context) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: Padding( + padding: const EdgeInsets.only( + top: 10, + left: 16, + right: 16, + bottom: 16, + ), + child: Column( + children: [ + Text( + "Add custom ETH token", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + Text("Add custom ETH token"), + const SizedBox( + height: 16, + ), + TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: contractController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Contract address", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 8, + ), + PrimaryButton( + label: "Search", + onPressed: () async { + final response = await showLoading( + whileFuture: EthereumAPI.getTokenByContractAddress( + contractController.text), + context: context, + message: "Looking up contract", + ); + currentToken = response.value; + if (currentToken != null) { + nameController.text = currentToken!.name; + symbolController.text = currentToken!.symbol; + decimalsController.text = currentToken!.decimals.toString(); + } else { + nameController.text = ""; + symbolController.text = ""; + decimalsController.text = ""; + unawaited( + showDialog( + context: context, + builder: (context) => StackOkDialog( + title: "Failed to look up token", + message: response.exception?.message, + ), + ), + ); + } + setState(() { + addTokenButtonEnabled = currentToken != null; + }); + }, + ), + const SizedBox( + height: 8, + ), + TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: nameController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Token name", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 8, + ), + TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: symbolController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Ticker", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 8, + ), + TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: decimalsController, + style: STextStyles.field(context), + inputFormatters: [ + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*)$').hasMatch(newValue.text) + ? newValue + : oldValue), + ], + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: false, + ), + decoration: InputDecoration( + hintText: "Decimals", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 16, + ), + const Spacer(), + PrimaryButton( + label: "Add token", + enabled: addTokenButtonEnabled, + onPressed: () {}, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart index 131a37f82..1fa47ba4d 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart'; @@ -70,6 +71,13 @@ class _AddTokenViewState extends ConsumerState { print("SELECTED TOKENS: $selectedTokens"); } + void onAddCustomTokenPressed() { + Navigator.of(context).pushNamed( + AddCustomTokenView.routeName, + arguments: widget.walletId, + ); + } + @override void initState() { _searchFieldController = TextEditingController(); @@ -196,6 +204,7 @@ class _AddTokenViewState extends ConsumerState { ), Expanded( child: AddTokenList( + walletId: widget.walletId, items: filter(_searchTerm, tokenEntities), ), ), @@ -244,9 +253,7 @@ class _AddTokenViewState extends ConsumerState { .extension()! .topNavIconPrimary, ), - onPressed: () { - // todo add custom token - }, + onPressed: onAddCustomTokenPressed, ), ), ), @@ -323,6 +330,7 @@ class _AddTokenViewState extends ConsumerState { ), Expanded( child: AddTokenList( + walletId: widget.walletId, items: filter(_searchTerm, tokenEntities), ), ), diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart index 1fd0c13d2..56516eaf0 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -10,9 +11,11 @@ import 'package:stackwallet/widgets/conditional_parent.dart'; class AddTokenList extends StatelessWidget { const AddTokenList({ Key? key, + required this.walletId, required this.items, }) : super(key: key); + final String walletId; final List items; @override @@ -29,7 +32,7 @@ class AddTokenList extends StatelessWidget { children: [ child, Padding( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.symmetric(vertical: 4), child: RawMaterialButton( fillColor: Theme.of(context).extension()!.popupBG, @@ -44,7 +47,10 @@ class AddTokenList extends StatelessWidget { ), ), onPressed: () { - // todo add custom token + Navigator.of(context).pushNamed( + AddCustomTokenView.routeName, + arguments: walletId, + ); }, child: Padding( padding: const EdgeInsets.all(12), @@ -73,7 +79,7 @@ class AddTokenList extends StatelessWidget { ], ), child: Padding( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.symmetric(vertical: 4), child: AddTokenListElement( data: items[index], ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 9f7dece46..01e6ab48f 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; @@ -218,6 +219,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case AddCustomTokenView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => AddCustomTokenView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case PaynymClaimView.routeName: if (args is String) { return getRoute( From e3548c5ecc529a0934c9d02a2c20815d67130036 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Feb 2023 18:36:54 -0600 Subject: [PATCH 063/208] WIP wallet token management --- .../sub_classes/eth_token_entity.dart | 2 +- lib/models/ethereum/erc20_token.dart | 5 +- lib/models/ethereum/erc721_token.dart | 5 +- lib/models/ethereum/eth_token.dart | 62 +++++-- .../add_token_view/add_custom_token_view.dart | 10 +- .../add_token_view/add_token_view.dart | 35 +++- .../sub_widgets/add_token_list.dart | 9 +- .../sub_widgets/add_token_list_element.dart | 2 +- .../global_settings_view/hidden_settings.dart | 35 ++++ lib/pages/token_view/my_tokens_view.dart | 37 +++-- .../sub_widgets/my_token_select_item.dart | 22 ++- .../sub_widgets/my_tokens_list.dart | 14 +- lib/pages/wallet_view/wallet_view.dart | 49 +----- lib/route_generator.dart | 10 +- .../coins/ethereum/ethereum_wallet.dart | 30 +++- lib/services/ethereum/ethereum_api.dart | 153 +++++++++--------- .../ethereum/ethereum_token_service.dart | 2 +- .../mixins/eth_extras_wallet_cache.dart | 29 ++++ lib/services/mixins/eth_token_cache.dart | 4 +- lib/utilities/default_eth_tokens.dart | 20 +-- 20 files changed, 315 insertions(+), 220 deletions(-) create mode 100644 lib/services/mixins/eth_extras_wallet_cache.dart diff --git a/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart b/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart index 8392d5f82..f38d80850 100644 --- a/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart +++ b/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart @@ -5,7 +5,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; class EthTokenEntity extends AddWalletListEntity { EthTokenEntity(this.token); - final EthToken token; + final EthContractInfo token; @override Coin get coin => Coin.ethereum; diff --git a/lib/models/ethereum/erc20_token.dart b/lib/models/ethereum/erc20_token.dart index a7c4508ee..04d443862 100644 --- a/lib/models/ethereum/erc20_token.dart +++ b/lib/models/ethereum/erc20_token.dart @@ -1,11 +1,10 @@ import 'package:stackwallet/models/ethereum/eth_token.dart'; -class Erc20Token extends EthToken { - Erc20Token({ +class Erc20ContractInfo extends EthContractInfo { + const Erc20ContractInfo({ required super.contractAddress, required super.name, required super.symbol, required super.decimals, - required super.balance, }); } diff --git a/lib/models/ethereum/erc721_token.dart b/lib/models/ethereum/erc721_token.dart index f06d1572d..79fef8361 100644 --- a/lib/models/ethereum/erc721_token.dart +++ b/lib/models/ethereum/erc721_token.dart @@ -1,11 +1,10 @@ import 'package:stackwallet/models/ethereum/eth_token.dart'; -class Erc721Token extends EthToken { - Erc721Token({ +class Erc721ContractInfo extends EthContractInfo { + const Erc721ContractInfo({ required super.contractAddress, required super.name, required super.symbol, required super.decimals, - required super.balance, }); } diff --git a/lib/models/ethereum/eth_token.dart b/lib/models/ethereum/eth_token.dart index 00351a77b..a1bee7471 100644 --- a/lib/models/ethereum/eth_token.dart +++ b/lib/models/ethereum/eth_token.dart @@ -1,26 +1,62 @@ -class EthToken { - EthToken({ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; +import 'package:stackwallet/models/ethereum/erc20_token.dart'; +import 'package:stackwallet/models/ethereum/erc721_token.dart'; + +abstract class EthContractInfo extends Equatable { + const EthContractInfo({ required this.contractAddress, required this.name, required this.symbol, required this.decimals, - required this.balance, }); final String contractAddress; final String name; final String symbol; final int decimals; - final int balance; + + static EthContractInfo? fromMap(Map map) { + switch (map["runtimeType"]) { + case "Erc20ContractInfo": + return Erc20ContractInfo( + contractAddress: map["contractAddress"] as String, + name: map["name"] as String, + symbol: map["symbol"] as String, + decimals: map["decimals"] as int, + ); + case "Erc721ContractInfo": + return Erc721ContractInfo( + contractAddress: map["contractAddress"] as String, + name: map["name"] as String, + symbol: map["symbol"] as String, + decimals: map["decimals"] as int, + ); + default: + return null; + } + } + + static EthContractInfo? fromJson(String json) => fromMap( + Map.from( + jsonDecode(json) as Map, + ), + ); + + Map toMap() => { + "runtimeType": "$runtimeType", + "contractAddress": contractAddress, + "name": name, + "symbol": symbol, + "decimals": decimals, + }; + + String toJson() => jsonEncode(toMap()); @override - String toString() { - return "$runtimeType: { " - "name: $name, " - "symbol: $symbol, " - "contractAddress: $contractAddress, " - "decimals: $decimals, " - "balance: $balance" - " }"; - } + String toString() => toMap().toString(); + + @override + List get props => [contractAddress]; } diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart index f10c11d41..40201fbd0 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -39,7 +39,7 @@ class _AddCustomTokenViewState extends ConsumerState { bool enableSubFields = false; bool addTokenButtonEnabled = false; - EthToken? currentToken; + EthContractInfo? currentToken; @override Widget build(BuildContext context) { @@ -69,10 +69,6 @@ class _AddCustomTokenViewState extends ConsumerState { const SizedBox( height: 16, ), - Text("Add custom ETH token"), - const SizedBox( - height: 16, - ), TextField( autocorrect: !isDesktop, enableSuggestions: !isDesktop, @@ -178,7 +174,9 @@ class _AddCustomTokenViewState extends ConsumerState { PrimaryButton( label: "Add token", enabled: addTokenButtonEnabled, - onPressed: () {}, + onPressed: () { + Navigator.of(context).pop(currentToken!); + }, ), ], ), diff --git a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart index 1fa47ba4d..9b61a1cf4 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_eth_tokens.dart'; @@ -65,17 +67,36 @@ class _AddTokenViewState extends ConsumerState { return _entities; } - void onNextPressed() { + Future onNextPressed() async { final selectedTokens = - tokenEntities.where((e) => e.selected).map((e) => e.token); - print("SELECTED TOKENS: $selectedTokens"); + tokenEntities.where((e) => e.selected).map((e) => e.token).toSet(); + + final ethWallet = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as EthereumWallet; + + await ethWallet.addTokenContract(selectedTokens); + if (mounted) { + Navigator.of(context).pop(); + } } - void onAddCustomTokenPressed() { - Navigator.of(context).pushNamed( + Future _addToken() async { + final token = await Navigator.of(context).pushNamed( AddCustomTokenView.routeName, arguments: widget.walletId, ); + if (token is EthContractInfo) { + setState(() { + if (tokenEntities + .where((e) => e.token.contractAddress == token.contractAddress) + .isEmpty) { + tokenEntities.add(AddTokenListElementData(token)..selected = true); + tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); + } + }); + } } @override @@ -206,6 +227,7 @@ class _AddTokenViewState extends ConsumerState { child: AddTokenList( walletId: widget.walletId, items: filter(_searchTerm, tokenEntities), + addFunction: _addToken, ), ), ], @@ -253,7 +275,7 @@ class _AddTokenViewState extends ConsumerState { .extension()! .topNavIconPrimary, ), - onPressed: onAddCustomTokenPressed, + onPressed: _addToken, ), ), ), @@ -332,6 +354,7 @@ class _AddTokenViewState extends ConsumerState { child: AddTokenList( walletId: widget.walletId, items: filter(_searchTerm, tokenEntities), + addFunction: _addToken, ), ), const SizedBox( diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart index 56516eaf0..5b532536e 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart @@ -13,10 +13,12 @@ class AddTokenList extends StatelessWidget { Key? key, required this.walletId, required this.items, + required this.addFunction, }) : super(key: key); final String walletId; final List items; + final VoidCallback addFunction; @override Widget build(BuildContext context) { @@ -46,12 +48,7 @@ class AddTokenList extends StatelessWidget { Constants.size.circularBorderRadius, ), ), - onPressed: () { - Navigator.of(context).pushNamed( - AddCustomTokenView.routeName, - arguments: walletId, - ); - }, + onPressed: addFunction, child: Padding( padding: const EdgeInsets.all(12), child: Row( diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart index 2dbdd7ce5..da78836aa 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -13,7 +13,7 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddTokenListElementData { AddTokenListElementData(this.token); - final EthToken token; + final EthContractInfo token; bool selected = false; } diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 1f8d80c81..52a00ed61 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -12,6 +13,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import '../../../models/ethereum/erc20_token.dart'; + class HiddenSettings extends StatelessWidget { const HiddenSettings({Key? key}) : super(key: key); @@ -186,6 +189,38 @@ class HiddenSettings extends StatelessWidget { const SizedBox( height: 12, ), + Consumer(builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + final erc20 = Erc20ContractInfo( + contractAddress: 'some con', + name: "loonamsn", + symbol: "DD", + decimals: 19, + ); + + final json = erc20.toJson(); + + print(json); + + final ee = EthContractInfo.fromJson(json); + + print(ee); + }, + child: RoundedWhiteContainer( + child: Text( + "Click me", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }), + const SizedBox( + height: 12, + ), Consumer( builder: (_, ref, __) { if (ref.watch(prefsChangeNotifierProvider diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index fc89b5037..499bcb2f5 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -6,7 +6,8 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_tokens_list.dart'; -import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -23,17 +24,11 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class MyTokensView extends ConsumerStatefulWidget { const MyTokensView({ Key? key, - required this.managerProvider, required this.walletId, - required this.walletAddress, - required this.tokens, }) : super(key: key); static const String routeName = "/myTokens"; - final ChangeNotifierProvider managerProvider; final String walletId; - final String walletAddress; - final List tokens; @override ConsumerState createState() => _TokenDetailsViewState(); @@ -60,10 +55,24 @@ class _TokenDetailsViewState extends ConsumerState { String _searchString = ""; + List _filter( + Set tokens, String searchTerm) { + if (searchTerm.isEmpty) { + return tokens.toList(); + } + //implement search/filter + return tokens.toList(); + } + @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; + final tokens = (ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).wallet)) + as EthereumWallet) + .contracts; + return MasterScaffold( background: Theme.of(context).extension()!.background, isDesktop: isDesktop, @@ -96,7 +105,10 @@ class _TokenDetailsViewState extends ConsumerState { width: 12, ), Text( - "${ref.read(widget.managerProvider).walletName} Tokens", + "${ref.watch( + walletsChangeNotifierProvider.select((value) => + value.getManager(widget.walletId).walletName), + )} Tokens", style: STextStyles.desktopH3(context), ), ], @@ -118,7 +130,10 @@ class _TokenDetailsViewState extends ConsumerState { }, ), title: Text( - "${ref.read(widget.managerProvider).walletName} Tokens", + "${ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).walletName), + )} Tokens", style: STextStyles.navBarTitle(context), ), actions: [ @@ -257,10 +272,8 @@ class _TokenDetailsViewState extends ConsumerState { ), Expanded( child: MyTokensList( - managerProvider: widget.managerProvider, walletId: widget.walletId, - tokens: widget.tokens, - walletAddress: widget.walletAddress, + tokens: _filter(tokens, _searchString), ), ), ], diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 3d021f30e..ef4303df1 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -4,8 +4,8 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; -import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -18,23 +18,16 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; class MyTokenSelectItem extends ConsumerWidget { const MyTokenSelectItem( - {Key? key, - required this.managerProvider, - required this.walletId, - required this.walletAddress, - required this.token, - required}) + {Key? key, required this.walletId, required this.token, required}) : super(key: key); - final ChangeNotifierProvider managerProvider; final String walletId; - final String walletAddress; - final EthToken token; + final EthContractInfo token; @override Widget build(BuildContext context, WidgetRef ref) { final balanceInDecimal = Format.satoshisToEthTokenAmount( - token.balance, + 1111111111111, //token.balance, token.decimals, ); @@ -54,9 +47,12 @@ class MyTokenSelectItem extends ConsumerWidget { EthereumTokenService( token: token, secureStore: ref.read(secureStoreProvider), - ethWallet: ref.read(managerProvider).wallet as EthereumWallet, + ethWallet: ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .wallet as EthereumWallet, tracker: TransactionNotificationTracker( - walletId: ref.read(managerProvider).walletId, + walletId: walletId, ), ); diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart index a1a2a22ca..5de2490aa 100644 --- a/lib/pages/token_view/sub_widgets/my_tokens_list.dart +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -2,37 +2,31 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart'; -import 'package:stackwallet/services/coins/manager.dart'; class MyTokensList extends StatelessWidget { const MyTokensList({ Key? key, - required this.managerProvider, required this.walletId, required this.tokens, - required this.walletAddress, }) : super(key: key); - final ChangeNotifierProvider managerProvider; final String walletId; - final List tokens; - final String walletAddress; + final List tokens; @override Widget build(BuildContext context) { return Consumer( builder: (_, ref, __) { - print("TOKENS LENGTH IS ${tokens.length}"); return ListView.builder( itemCount: tokens.length, itemBuilder: (ctx, index) { + final token = tokens[index]; return Padding( + key: Key(token.contractAddress), padding: const EdgeInsets.all(4), child: MyTokenSelectItem( - managerProvider: managerProvider, walletId: walletId, - walletAddress: walletAddress, - token: tokens[index], + token: token, ), ); }, diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 5937930d1..f222550ff 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -5,7 +5,6 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; @@ -28,7 +27,6 @@ import 'package:stackwallet/providers/wallet/public_private_balance_state_provid import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/services/ethereum/ethereum_api.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; @@ -37,7 +35,6 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; -import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -796,49 +793,11 @@ class _WalletViewState extends ConsumerState { arguments: coin, )); }, - onTokensPressed: () async { - final walletAddress = await ref - .read(managerProvider) - .currentReceivingAddress; - - final response = await showLoading< - EthereumResponse>>( - whileFuture: - EthereumAPI.getWalletTokens( - address: await ref - .read(managerProvider) - .currentReceivingAddress), - message: "Loading tokens", - isDesktop: false, - context: context, + onTokensPressed: () { + Navigator.of(context).pushNamed( + MyTokensView.routeName, + arguments: walletId, ); - - if (mounted) { - if (response.value != null) { - await Navigator.of(context) - .pushNamed( - MyTokensView.routeName, - arguments: Tuple4( - managerProvider, - walletId, - walletAddress, - response.value!, - ), - ); - } else { - await showDialog( - context: context, - builder: (context) { - return StackOkDialog( - title: - "Failed to fetch tokens", - message: response.exception - .toString(), - ); - }, - ); - } - } }, ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 01e6ab48f..81426cec5 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; @@ -1460,15 +1459,12 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case MyTokensView.routeName: - if (args is Tuple4, String, String, - List>) { + if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => MyTokensView( - managerProvider: args.item1, - walletId: args.item2, - walletAddress: args.item3, - tokens: args.item4), + walletId: args, + ), settings: RouteSettings( name: settings.name, ), diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index ff8a47bba..a5af59607 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -7,6 +7,7 @@ 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/ethereum/eth_token.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'; @@ -17,6 +18,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha 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/eth_extras_wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; @@ -37,7 +39,8 @@ import 'package:web3dart/web3dart.dart' as web3; const int MINIMUM_CONFIRMATIONS = 3; -class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { +class EthereumWallet extends CoinServiceAPI + with WalletCache, WalletDB, EthExtrasWalletCache { EthereumWallet({ required String walletId, required String walletName, @@ -52,6 +55,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { _coin = coin; _secureStore = secureStore; initCache(walletId, coin); + initEthExtrasCache(walletId); initWalletDB(mockableOverride: mockableOverride); } @@ -62,6 +66,30 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { Timer? timer; Timer? _networkAliveTimer; + Set get contracts => getCachedTokenContracts(); + + Future addTokenContract(Set contractInfo) => + updateCachedTokenContracts(contracts..addAll(contractInfo)).then( + (value) => GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "$contractInfo updated/added for: $walletId $walletName", + walletId, + ), + ), + ); + + Future removeTokenContract(String contractAddress) => + updateCachedTokenContracts(contracts + ..removeWhere((e) => e.contractAddress == contractAddress)) + .then( + (value) => GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "$contractAddress removed for: $walletId $walletName", + walletId, + ), + ), + ); + @override String get walletId => _walletId; late String _walletId; diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index e5d83fa2f..2a6009cfc 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -210,77 +210,78 @@ abstract class EthereumAPI { } } - static Future>> getWalletTokens({ - required String address, - }) async { - try { - final uri = Uri.parse( - "$blockExplorer?module=account&action=tokenlist&address=$address", - ); - final response = await get(uri); - - if (response.statusCode == 200) { - final json = jsonDecode(response.body); - if (json["message"] == "OK") { - final result = - List>.from(json["result"] as List); - final List tokens = []; - for (final map in result) { - if (map["type"] == "ERC-20") { - tokens.add( - Erc20Token( - balance: int.parse(map["balance"] as String), - contractAddress: map["contractAddress"] as String, - decimals: int.parse(map["decimals"] as String), - name: map["name"] as String, - symbol: map["symbol"] as String, - ), - ); - } else if (map["type"] == "ERC-721") { - tokens.add( - Erc721Token( - balance: int.parse(map["balance"] as String), - contractAddress: map["contractAddress"] as String, - decimals: int.parse(map["decimals"] as String), - name: map["name"] as String, - symbol: map["symbol"] as String, - ), - ); - } else { - throw EthApiException( - "Unsupported token type found: ${map["type"]}"); - } - } - - return EthereumResponse( - tokens, - null, - ); - } else { - throw EthApiException(json["message"] as String); - } - } else { - throw EthApiException( - "getWalletTokens($address) failed with status code: " - "${response.statusCode}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.log( - "getWalletTokens(): $e\n$s", - level: LogLevel.Error, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } +// ONLY FETCHES WALLET TOKENS WITH A NON ZERO BALANCE + // static Future>> getWalletTokens({ + // required String address, + // }) async { + // try { + // final uri = Uri.parse( + // "$blockExplorer?module=account&action=tokenlist&address=$address", + // ); + // final response = await get(uri); + // + // if (response.statusCode == 200) { + // final json = jsonDecode(response.body); + // if (json["message"] == "OK") { + // final result = + // List>.from(json["result"] as List); + // final List tokens = []; + // for (final map in result) { + // if (map["type"] == "ERC-20") { + // tokens.add( + // Erc20Token( + // balance: int.parse(map["balance"] as String), + // contractAddress: map["contractAddress"] as String, + // decimals: int.parse(map["decimals"] as String), + // name: map["name"] as String, + // symbol: map["symbol"] as String, + // ), + // ); + // } else if (map["type"] == "ERC-721") { + // tokens.add( + // Erc721Token( + // balance: int.parse(map["balance"] as String), + // contractAddress: map["contractAddress"] as String, + // decimals: int.parse(map["decimals"] as String), + // name: map["name"] as String, + // symbol: map["symbol"] as String, + // ), + // ); + // } else { + // throw EthApiException( + // "Unsupported token type found: ${map["type"]}"); + // } + // } + // + // return EthereumResponse( + // tokens, + // null, + // ); + // } else { + // throw EthApiException(json["message"] as String); + // } + // } else { + // throw EthApiException( + // "getWalletTokens($address) failed with status code: " + // "${response.statusCode}", + // ); + // } + // } on EthApiException catch (e) { + // return EthereumResponse( + // null, + // e, + // ); + // } catch (e, s) { + // Logging.instance.log( + // "getWalletTokens(): $e\n$s", + // level: LogLevel.Error, + // ); + // return EthereumResponse( + // null, + // EthApiException(e.toString()), + // ); + // } + // } static Future getGasOracle() async { final response = await get(Uri.parse(gasTrackerUrl)); @@ -308,7 +309,7 @@ abstract class EthereumAPI { } //Validate that a custom token is valid and is ERC-20, a token will be valid - static Future> getTokenByContractAddress( + static Future> getTokenByContractAddress( String contractAddress) async { try { final response = await get(Uri.parse( @@ -317,18 +318,16 @@ abstract class EthereumAPI { final json = jsonDecode(response.body); if (json["message"] == "OK") { final map = Map.from(json["result"] as Map); - EthToken? token; + EthContractInfo? token; if (map["type"] == "ERC-20") { - token = Erc20Token( - balance: 0, //int.parse(map["balance"] as String), + token = Erc20ContractInfo( contractAddress: map["contractAddress"] as String, decimals: int.parse(map["decimals"] as String), name: map["name"] as String, symbol: map["symbol"] as String, ); } else if (map["type"] == "ERC-721") { - token = Erc721Token( - balance: 0, //int.parse(map["balance"] as String), + token = Erc721ContractInfo( contractAddress: map["contractAddress"] as String, decimals: int.parse(map["decimals"] as String), name: map["name"] as String, diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 933975c42..feda6be0d 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -30,7 +30,7 @@ import 'package:tuple/tuple.dart'; import 'package:web3dart/web3dart.dart' as web3dart; class EthereumTokenService extends ChangeNotifier with EthTokenCache { - final EthToken token; + final EthContractInfo token; final EthereumWallet ethWallet; final TransactionNotificationTracker tracker; final SecureStorageInterface _secureStore; diff --git a/lib/services/mixins/eth_extras_wallet_cache.dart b/lib/services/mixins/eth_extras_wallet_cache.dart new file mode 100644 index 000000000..e0ef75ee0 --- /dev/null +++ b/lib/services/mixins/eth_extras_wallet_cache.dart @@ -0,0 +1,29 @@ +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; + +mixin EthExtrasWalletCache { + late final String _walletId; + + void initEthExtrasCache(String walletId) { + _walletId = walletId; + } + + // cached list of user added token contracts + Set getCachedTokenContracts() { + final list = DB.instance.get( + boxName: _walletId, + key: "ethTokenContracts", + ) as List? ?? + []; + return list.map((e) => EthContractInfo.fromJson(e)!).toSet(); + } + + Future updateCachedTokenContracts( + Set contracts) async { + await DB.instance.put( + boxName: _walletId, + key: "ethTokenContracts", + value: contracts.map((e) => e.toJson()).toList(), + ); + } +} diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart index b490e849c..614688dee 100644 --- a/lib/services/mixins/eth_token_cache.dart +++ b/lib/services/mixins/eth_token_cache.dart @@ -10,9 +10,9 @@ abstract class _Keys { mixin EthTokenCache { late final String _walletId; - late final EthToken _token; + late final EthContractInfo _token; - void initCache(String walletId, EthToken token) { + void initCache(String walletId, EthContractInfo token) { _walletId = walletId; _token = token; } diff --git a/lib/utilities/default_eth_tokens.dart b/lib/utilities/default_eth_tokens.dart index 2a253fb56..a07daf40a 100644 --- a/lib/utilities/default_eth_tokens.dart +++ b/lib/utilities/default_eth_tokens.dart @@ -2,48 +2,42 @@ import 'package:stackwallet/models/ethereum/erc20_token.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; abstract class DefaultTokens { - static List list = [ - Erc20Token( + static List list = [ + Erc20ContractInfo( contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", name: "USD Coin", symbol: "USDC", decimals: 18, - balance: 0, ), - Erc20Token( + Erc20ContractInfo( contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", name: "Tether", symbol: "USDT", decimals: 18, - balance: 0, ), - Erc20Token( + Erc20ContractInfo( contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", name: "Shiba Inu", symbol: "SHIB", decimals: 18, - balance: 0, ), - Erc20Token( + Erc20ContractInfo( contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", name: "BNB Token", symbol: "BNB", decimals: 18, - balance: 0, ), - Erc20Token( + Erc20ContractInfo( contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", name: "BUSD", symbol: "BUSD", decimals: 18, - balance: 0, ), - Erc20Token( + Erc20ContractInfo( contractAddress: "0x514910771af9ca656af840dff83e8264ecf986ca", name: "Chainlink", symbol: "LINK", decimals: 18, - balance: 0, ), ]; } From 508fc484621ab663f97acc928cd1798849cf19b4 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 14:03:23 -0600 Subject: [PATCH 064/208] dirty proxy contract function injection hack --- .../ethereum/ethereum_token_service.dart | 119 ++++++++++++------ 1 file changed, 80 insertions(+), 39 deletions(-) diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index feda6be0d..c6567973e 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; @@ -109,37 +110,24 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { return await EthereumAPI.getFees(); } - Future initializeExisting() async { - _tokenAbi = (await _secureStore.read( - key: '${_contractAddress.toString()}_tokenAbi'))!; + Future initialize() async { + final storedABI = + await _secureStore.read(key: '${_contractAddress.toString()}_tokenAbi'); - String? mnemonicString = await ethWallet.mnemonicString; - - //Get private key for given mnemonic - String privateKey = getPrivateKey( - mnemonicString!, (await ethWallet.mnemonicPassphrase) ?? ""); - _credentials = web3dart.EthPrivateKey.fromHex(privateKey); - - _contract = web3dart.DeployedContract( - web3dart.ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); - _balanceFunction = _contract.function('balanceOf'); - _sendFunction = _contract.function('transfer'); - _client = await getEthClient(); - - unawaited(refresh()); - } - - Future initializeNew() async { - AbiRequestResponse abi = - await EthereumAPI.fetchTokenAbi(_contractAddress.hex); - //Fetch token ABI so we can call token functions - if (abi.message == "OK") { - _tokenAbi = abi.result; - //Store abi in secure store - await _secureStore.write( - key: '${_contractAddress.hex}_tokenAbi', value: _tokenAbi); + if (storedABI == null) { + AbiRequestResponse abi = + await EthereumAPI.fetchTokenAbi(_contractAddress.hex); + //Fetch token ABI so we can call token functions + if (abi.message == "OK") { + _tokenAbi = abi.result; + //Store abi in secure store + await _secureStore.write( + key: '${_contractAddress.hex}_tokenAbi', value: _tokenAbi); + } else { + throw Exception('Failed to load token abi'); + } } else { - throw Exception('Failed to load token abi'); + _tokenAbi = storedABI; } String? mnemonicString = await ethWallet.mnemonicString; @@ -151,8 +139,63 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { _contract = web3dart.DeployedContract( web3dart.ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); - _balanceFunction = _contract.function('balanceOf'); - _sendFunction = _contract.function('transfer'); + + bool hackInBalanceOf = false, hackInTransfer = false; + try { + _balanceFunction = _contract.function('balanceOf'); + } catch (_) { + // function not found so likely a proxy so we need to hack the function in + hackInBalanceOf = true; + } + + try { + _sendFunction = _contract.function('transfer'); + } catch (_) { + // function not found so likely a proxy so we need to hack the function in + hackInTransfer = true; + } + + if (hackInBalanceOf || hackInTransfer) { + final json = jsonDecode(_tokenAbi) as List; + if (hackInBalanceOf) { + json.add({ + "constant": true, + "inputs": [ + {"name": "", "type": "address"} + ], + "name": "balanceOf", + "outputs": [ + {"name": "", "type": "uint256"} + ], + "payable": false, + "type": "function" + }); + } + if (hackInTransfer) { + json.add({ + "constant": false, + "inputs": [ + {"name": "_to", "type": "address"}, + {"name": "_value", "type": "uint256"} + ], + "name": "transfer", + "outputs": [], + "payable": false, + "type": "function" + }); + } + _tokenAbi = jsonEncode(json); + await _secureStore.write( + key: '${_contractAddress.hex}_tokenAbi', value: _tokenAbi); + + _contract = web3dart.DeployedContract( + web3dart.ContractAbi.fromJson(_tokenAbi, token.name), + _contractAddress); + + _balanceFunction = _contract.function('balanceOf'); + _sendFunction = _contract.function('transfer'); + } + _client = await getEthClient(); unawaited(refresh()); @@ -228,13 +271,10 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { Future refreshCachedBalance() async { final balanceRequest = await _client.call( - contract: _contract, - function: _balanceFunction, - params: [_credentials.address]); - - print("=========================================="); - print("${token.name} balanceRequest: $balanceRequest"); - print("=========================================="); + contract: _contract, + function: _balanceFunction, + params: [_credentials.address], + ); String _balance = balanceRequest.first.toString(); @@ -266,7 +306,8 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { ); if (response.value == null) { - throw Exception("Failed to fetch token transactions"); + throw response.exception ?? + Exception("Failed to fetch token transactions"); } final List> txnsData = []; From 5bf18a541a4fcf5335c367bb8ef478d70af3e586 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 14:03:44 -0600 Subject: [PATCH 065/208] json serialize fix --- lib/models/token_balance.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/models/token_balance.dart b/lib/models/token_balance.dart index 632a5ade5..8984aa135 100644 --- a/lib/models/token_balance.dart +++ b/lib/models/token_balance.dart @@ -46,6 +46,7 @@ class TokenBalance extends Balance { @override String toJsonIgnoreCoin() => jsonEncode({ + "contractAddress": contractAddress, "decimalPlaces": decimalPlaces, "total": total, "spendable": spendable, @@ -55,12 +56,10 @@ class TokenBalance extends Balance { factory TokenBalance.fromJson( String json, - String contractAddress, - int decimalPlaces, ) { final decoded = jsonDecode(json); return TokenBalance( - contractAddress: contractAddress, + contractAddress: decoded["contractAddress"] as String, decimalPlaces: decoded["decimalPlaces"] as int, total: decoded["total"] as int, spendable: decoded["spendable"] as int, From 100ab3984703e8f02435cac28077c908c2c3c0f9 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 14:04:25 -0600 Subject: [PATCH 066/208] add get token balance for address function --- lib/services/ethereum/ethereum_api.dart | 58 ++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 2a6009cfc..4f7d96933 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -130,8 +130,8 @@ class EthereumResponse { } abstract class EthereumAPI { - static const blockExplorer = "https://blockscout.com/eth/mainnet/api"; - static const abiUrl = + static const blockScout = "https://blockscout.com/eth/mainnet/api"; + static const etherscanApi = "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update static const gasTrackerUrl = @@ -141,7 +141,7 @@ abstract class EthereumAPI { String address, String action) async { try { final response = await get(Uri.parse( - "$blockExplorer?module=account&action=$action&address=$address")); + "$blockScout?module=account&action=$action&address=$address")); if (response.statusCode == 200) { return AddressTransaction.fromJson( jsonDecode(response.body) as Map); @@ -163,7 +163,7 @@ abstract class EthereumAPI { }) async { try { String uriString = - "$blockExplorer?module=account&action=tokentx&address=$address"; + "$blockScout?module=account&action=tokentx&address=$address"; if (contractAddress != null) { uriString += "&contractAddress=$contractAddress"; } @@ -283,6 +283,52 @@ abstract class EthereumAPI { // } // } + static Future> getWalletTokenBalance({ + required String address, + required String contractAddress, + }) async { + try { + final uri = Uri.parse( + "$blockScout?module=account&action=tokenbalance" + "&contractaddress=$contractAddress&address=$address", + ); + final response = await get(uri); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json["message"] == "OK") { + final result = json["result"] as String; + + return EthereumResponse( + int.parse(result), + null, + ); + } else { + throw EthApiException(json["message"] as String); + } + } else { + throw EthApiException( + "getWalletTokens($address) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getWalletTokens(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + static Future getGasOracle() async { final response = await get(Uri.parse(gasTrackerUrl)); if (response.statusCode == 200) { @@ -313,7 +359,7 @@ abstract class EthereumAPI { String contractAddress) async { try { final response = await get(Uri.parse( - "$blockExplorer?module=token&action=getToken&contractaddress=$contractAddress")); + "$blockScout?module=token&action=getToken&contractaddress=$contractAddress")); if (response.statusCode == 200) { final json = jsonDecode(response.body); if (json["message"] == "OK") { @@ -371,7 +417,7 @@ abstract class EthereumAPI { static Future fetchTokenAbi( String contractAddress) async { final response = await get(Uri.parse( - "$abiUrl?module=contract&action=getabi&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + "$etherscanApi?module=contract&action=getabi&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AbiRequestResponse.fromJson( json.decode(response.body) as Map); From 8812efaefb7e3a6bb0c4b4d86b7cfd0cd71e0451 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 14:50:14 -0600 Subject: [PATCH 067/208] temp disable broken blockscout api --- lib/services/ethereum/ethereum_api.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 4f7d96933..af8e5a5ca 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -90,7 +90,7 @@ class EthTokenTx { gasUsed: int.parse(map["gasUsed"] as String), hash: map["hash"] as String, input: map["input"] as String, - logIndex: int.parse(map["logIndex"] as String), + logIndex: int.parse(map["logIndex"] as String? ?? "-1"), nonce: int.parse(map["nonce"] as String), timeStamp: int.parse(map["timeStamp"] as String), to: map["to"] as String, @@ -130,7 +130,7 @@ class EthereumResponse { } abstract class EthereumAPI { - static const blockScout = "https://blockscout.com/eth/mainnet/api"; + // static const blockScout = "https://blockscout.com/eth/mainnet/api"; static const etherscanApi = "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update @@ -138,10 +138,11 @@ abstract class EthereumAPI { "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; static Future fetchAddressTransactions( - String address, String action) async { + String address) async { try { final response = await get(Uri.parse( - "$blockScout?module=account&action=$action&address=$address")); + // "$blockScout?module=account&action=txlist&address=$address")); + "$etherscanApi?module=account&action=txlist&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AddressTransaction.fromJson( jsonDecode(response.body) as Map); @@ -163,7 +164,8 @@ abstract class EthereumAPI { }) async { try { String uriString = - "$blockScout?module=account&action=tokentx&address=$address"; + // "$blockScout?module=account&action=tokentx&address=$address"; + "$etherscanApi?module=account&action=tokentx&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"; if (contractAddress != null) { uriString += "&contractAddress=$contractAddress"; } @@ -289,8 +291,8 @@ abstract class EthereumAPI { }) async { try { final uri = Uri.parse( - "$blockScout?module=account&action=tokenbalance" - "&contractaddress=$contractAddress&address=$address", + "$etherscanApi?module=account&action=tokenbalance" + "&contractaddress=$contractAddress&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP", ); final response = await get(uri); @@ -359,7 +361,8 @@ abstract class EthereumAPI { String contractAddress) async { try { final response = await get(Uri.parse( - "$blockScout?module=token&action=getToken&contractaddress=$contractAddress")); + "$etherscanApi?module=token&action=getToken&contractaddress=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + // "$blockScout?module=token&action=getToken&contractaddress=$contractAddress")); if (response.statusCode == 200) { final json = jsonDecode(response.body); if (json["message"] == "OK") { From bfbf175f449087219c408373a7a5806e1f9d5bdb Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 14:50:40 -0600 Subject: [PATCH 068/208] hide generate address button for eth --- lib/pages/receive_view/receive_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 52283111c..b9cc227ab 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -288,11 +288,11 @@ class _ReceiveViewState extends ConsumerState { ), ), ), - if (coin != Coin.epicCash) + if (coin != Coin.epicCash || coin != Coin.ethereum) const SizedBox( height: 12, ), - if (coin != Coin.epicCash) + if (coin != Coin.epicCash || coin != Coin.ethereum) TextButton( onPressed: generateNewAddress, style: Theme.of(context) From 11177b50c342ddf909a4ec857feb971d2fe16bc2 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 15:19:53 -0600 Subject: [PATCH 069/208] tx parsing fix --- lib/services/coins/ethereum/ethereum_wallet.dart | 11 ++++++----- lib/services/ethereum/ethereum_token_service.dart | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index a5af59607..526d141a8 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -168,8 +168,8 @@ class EthereumWallet extends CoinServiceAPI @override Future get currentReceivingAddress async { final address = await _currentReceivingAddress; - return address?.value ?? - checksumEthereumAddress(_credentials.address.toString()); + return checksumEthereumAddress( + address?.value ?? _credentials.address.toString()); } Future get _currentReceivingAddress => db @@ -569,7 +569,8 @@ class EthereumWallet extends CoinServiceAPI var allOwnAddresses = await _fetchAllOwnAddresses(); AddressTransaction addressTransactions = await EthereumAPI.fetchAddressTransactions( - allOwnAddresses.elementAt(0).value, "txlist"); + allOwnAddresses.elementAt(0).value, + ); if (addressTransactions.message == "OK") { final allTxs = addressTransactions.result; for (final element in allTxs) { @@ -845,7 +846,7 @@ class EthereumWallet extends CoinServiceAPI String thisAddress = await currentReceivingAddress; AddressTransaction txs = - await EthereumAPI.fetchAddressTransactions(thisAddress, "txlist"); + await EthereumAPI.fetchAddressTransactions(thisAddress); if (txs.message == "OK") { final allTxs = txs.result; @@ -875,7 +876,7 @@ class EthereumWallet extends CoinServiceAPI final txn = Transaction( walletId: walletId, txid: element["hash"] as String, - timestamp: element["timeStamp"] as int, + timestamp: int.parse(element["timeStamp"].toString()), type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, subType: TransactionSubType.none, diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index c6567973e..dcf8acc46 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -86,8 +86,8 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { Future get currentReceivingAddress async { final address = await _currentReceivingAddress; - return address?.value ?? - checksumEthereumAddress(_credentials.address.toString()); + return checksumEthereumAddress( + address?.value ?? _credentials.address.toString()); } Future get _currentReceivingAddress => ethWallet.db From eb6eb4d4d5988377be3e24b06d70a3378b4eda36 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 15:20:40 -0600 Subject: [PATCH 070/208] update eth node details --- lib/utilities/default_nodes.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index ac3c02d7c..5da140bd5 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -137,10 +137,9 @@ abstract class DefaultNodes { isDown: false, ); - //TODO - Update with correct node details for ETH static NodeModel get ethereum => NodeModel( - host: "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", - port: 1234, + host: "https://eth.stackwallet.com", + port: 443, name: defaultName, id: _nodeId(Coin.ethereum), useSSL: true, From 7a9acb6649012f669cc09d0362858c3681c80298 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 15:21:09 -0600 Subject: [PATCH 071/208] add eth default node item to `all` list --- lib/utilities/default_nodes.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 5da140bd5..02865d372 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -16,6 +16,7 @@ abstract class DefaultNodes { firo, monero, epicCash, + ethereum, bitcoincash, namecoin, wownero, From c89ae56135869e8893cb0e782c5e2055ccefafc6 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 15:21:31 -0600 Subject: [PATCH 072/208] fic default eth tokens list --- lib/utilities/default_eth_tokens.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utilities/default_eth_tokens.dart b/lib/utilities/default_eth_tokens.dart index a07daf40a..b5bcde9ba 100644 --- a/lib/utilities/default_eth_tokens.dart +++ b/lib/utilities/default_eth_tokens.dart @@ -2,18 +2,18 @@ import 'package:stackwallet/models/ethereum/erc20_token.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; abstract class DefaultTokens { - static List list = [ + static const List list = [ Erc20ContractInfo( contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", name: "USD Coin", symbol: "USDC", - decimals: 18, + decimals: 6, ), Erc20ContractInfo( contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", name: "Tether", symbol: "USDT", - decimals: 18, + decimals: 6, ), Erc20ContractInfo( contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", From e732c4f1b728c57fb14fcc24bf337e252a3a388e Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 15:27:10 -0600 Subject: [PATCH 073/208] eth wallet token list fixes --- .../sub_widgets/my_token_select_item.dart | 87 +++++++++++++------ .../ethereum/cached_eth_token_balance.dart | 39 +++++++++ lib/services/mixins/eth_token_cache.dart | 2 - 3 files changed, 100 insertions(+), 28 deletions(-) create mode 100644 lib/services/ethereum/cached_eth_token_balance.dart diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index ef4303df1..508ab7e53 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -6,36 +6,57 @@ import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/services/ethereum/cached_eth_token_balance.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.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/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -class MyTokenSelectItem extends ConsumerWidget { - const MyTokenSelectItem( - {Key? key, required this.walletId, required this.token, required}) - : super(key: key); +class MyTokenSelectItem extends ConsumerStatefulWidget { + const MyTokenSelectItem({ + Key? key, + required this.walletId, + required this.token, + }) : super(key: key); final String walletId; final EthContractInfo token; @override - Widget build(BuildContext context, WidgetRef ref) { - final balanceInDecimal = Format.satoshisToEthTokenAmount( - 1111111111111, //token.balance, - token.decimals, - ); + ConsumerState createState() => _MyTokenSelectItemState(); +} +class _MyTokenSelectItemState extends ConsumerState { + late final CachedEthTokenBalance cachedBalance; + + @override + void initState() { + cachedBalance = CachedEthTokenBalance(widget.walletId, widget.token); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + final address = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .currentReceivingAddress; + await cachedBalance.fetchAndUpdateCachedBalance(address); + if (mounted) { + setState(() {}); + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { return RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: MaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - key: Key("walletListItemButtonKey_${token.symbol}"), + key: Key("walletListItemButtonKey_${widget.token.symbol}"), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( @@ -45,29 +66,30 @@ class MyTokenSelectItem extends ConsumerWidget { onPressed: () async { ref.read(tokenServiceStateProvider.state).state = EthereumTokenService( - token: token, + token: widget.token, secureStore: ref.read(secureStoreProvider), ethWallet: ref .read(walletsChangeNotifierProvider) - .getManager(walletId) + .getManager(widget.walletId) .wallet as EthereumWallet, tracker: TransactionNotificationTracker( - walletId: walletId, + walletId: widget.walletId, ), ); await showLoading( - whileFuture: ref.read(tokenServiceProvider)!.initializeExisting(), + whileFuture: ref.read(tokenServiceProvider)!.initialize(), context: context, - message: "Loading ${token.name}", + message: "Loading ${widget.token.name}", ); - await Navigator.of(context).pushNamed( - TokenView.routeName, - arguments: walletId, - ); + if (mounted) { + await Navigator.of(context).pushNamed( + TokenView.routeName, + arguments: widget.walletId, + ); + } }, - child: Row( children: [ SvgPicture.asset( @@ -87,12 +109,13 @@ class MyTokenSelectItem extends ConsumerWidget { Row( children: [ Text( - token.name, + widget.token.name, style: STextStyles.titleBold12(context), ), const Spacer(), Text( - "$balanceInDecimal ${token.symbol}", + "${cachedBalance.getCachedBalance().getTotal()} " + "${widget.token.symbol}", style: STextStyles.itemSubtitle(context), ), ], @@ -103,11 +126,23 @@ class MyTokenSelectItem extends ConsumerWidget { Row( children: [ Text( - token.symbol, + widget.token.symbol, style: STextStyles.itemSubtitle(context), ), const Spacer(), - const Text("0 USD"), + Text("${ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value + .getTokenPrice(widget.token.contractAddress) + .item1 + .toStringAsFixed(2), + ), + )} " + "${ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + )}"), ], ), ], diff --git a/lib/services/ethereum/cached_eth_token_balance.dart b/lib/services/ethereum/cached_eth_token_balance.dart new file mode 100644 index 000000000..1867d57b5 --- /dev/null +++ b/lib/services/ethereum/cached_eth_token_balance.dart @@ -0,0 +1,39 @@ +import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/token_balance.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; +import 'package:stackwallet/services/mixins/eth_token_cache.dart'; +import 'package:stackwallet/utilities/logger.dart'; + +class CachedEthTokenBalance with EthTokenCache { + final String walletId; + final EthContractInfo token; + + CachedEthTokenBalance(this.walletId, this.token) { + initCache(walletId, token); + } + + Future fetchAndUpdateCachedBalance(String address) async { + final response = await EthereumAPI.getWalletTokenBalance( + address: address, + contractAddress: token.contractAddress, + ); + + if (response.value != null) { + await updateCachedBalance( + TokenBalance( + contractAddress: token.contractAddress, + decimalPlaces: token.decimals, + total: response.value!, + spendable: response.value!, + blockedTotal: 0, + pendingSpendable: 0, + ), + ); + } else { + Logging.instance.log( + "CachedEthTokenBalance.fetchAndUpdateCachedBalance failed: ${response.exception}", + level: LogLevel.Warning, + ); + } + } +} diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart index 614688dee..b182da682 100644 --- a/lib/services/mixins/eth_token_cache.dart +++ b/lib/services/mixins/eth_token_cache.dart @@ -35,8 +35,6 @@ mixin EthTokenCache { } return TokenBalance.fromJson( jsonString, - _token.contractAddress, - _token.decimals, ); } From 16efeea1dbc82faf4b997b02a4654aadc3a889cd Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 15:52:13 -0600 Subject: [PATCH 074/208] project structure --- lib/{ => db}/hive/db.dart | 0 lib/db/{ => isar}/main_db.dart | 0 lib/electrumx_rpc/cached_electrumx.dart | 2 +- lib/main.dart | 4 ++-- .../subviews/contact_details_view.dart | 2 +- lib/pages/exchange_view/exchange_view.dart | 2 +- lib/pages/receive_view/addresses/address_card.dart | 2 +- .../receive_view/addresses/edit_address_label_view.dart | 2 +- .../receive_view/addresses/receiving_addresses_view.dart | 2 +- .../global_settings_view/appearance_settings_view.dart | 2 +- .../helpers/restore_create_backup.dart | 2 +- lib/pages/stack_privacy_calls.dart | 2 +- .../subwidgets/desktop_contact_details.dart | 2 +- .../desktop_exchange/desktop_all_trades_view.dart | 2 +- .../subwidgets/desktop_trade_history.dart | 2 +- .../password/delete_password_warning_view.dart | 2 +- .../password/desktop_login_view.dart | 2 +- .../password/forgotten_passphrase_restore_from_swb.dart | 2 +- .../advanced_settings/stack_privacy_dialog.dart | 2 +- .../settings/settings_menu/appearance_settings.dart | 2 +- lib/services/address_book_service.dart | 2 +- lib/services/coins/bitcoin/bitcoin_wallet.dart | 2 +- lib/services/coins/bitcoincash/bitcoincash_wallet.dart | 2 +- lib/services/coins/dogecoin/dogecoin_wallet.dart | 2 +- lib/services/coins/epiccash/epiccash_wallet.dart | 2 +- lib/services/coins/ethereum/ethereum_wallet.dart | 2 +- lib/services/coins/firo/firo_wallet.dart | 2 +- lib/services/coins/litecoin/litecoin_wallet.dart | 2 +- lib/services/coins/manager.dart | 2 +- lib/services/coins/monero/monero_wallet.dart | 4 ++-- lib/services/coins/namecoin/namecoin_wallet.dart | 2 +- lib/services/coins/particl/particl_wallet.dart | 2 +- lib/services/coins/wownero/wownero_wallet.dart | 4 ++-- lib/services/exchange/exchange_data_loading_service.dart | 2 +- lib/services/mixins/epic_cash_hive.dart | 2 +- lib/services/mixins/eth_extras_wallet_cache.dart | 2 +- lib/services/mixins/eth_token_cache.dart | 2 +- lib/services/mixins/firo_hive.dart | 2 +- lib/services/mixins/paynym_wallet_interface.dart | 2 +- lib/services/mixins/wallet_cache.dart | 2 +- lib/services/mixins/wallet_db.dart | 2 +- lib/services/node_service.dart | 2 +- lib/services/notes_service.dart | 2 +- lib/services/notifications_service.dart | 2 +- lib/services/price.dart | 2 +- lib/services/tokens_service.dart | 9 +-------- lib/services/trade_notes_service.dart | 2 +- lib/services/trade_sent_from_stack_service.dart | 2 +- lib/services/trade_service.dart | 2 +- lib/services/transaction_notification_tracker.dart | 2 +- lib/services/wallets.dart | 2 +- lib/services/wallets_service.dart | 4 ++-- lib/utilities/db_version_migration.dart | 4 ++-- lib/utilities/delete_everything.dart | 2 +- lib/utilities/desktop_password_service.dart | 2 +- lib/utilities/prefs.dart | 2 +- test/address_book_service_test.dart | 2 +- test/cached_electrumx_test.dart | 2 +- test/hive/db_test.dart | 2 +- test/pages/send_view/send_view_test.mocks.dart | 2 +- test/price_test.dart | 2 +- .../coins/bitcoincash/bitcoincash_wallet_test.dart | 2 +- test/services/coins/dogecoin/dogecoin_wallet_test.dart | 3 +-- test/services/coins/manager_test.mocks.dart | 2 +- test/services/node_service_test.dart | 2 +- test/services/wallets_service_test.dart | 2 +- test/widget_tests/managed_favorite_test.mocks.dart | 2 +- .../table_view/table_view_row_test.mocks.dart | 2 +- test/widget_tests/transaction_card_test.mocks.dart | 2 +- test/widget_tests/wallet_card_test.mocks.dart | 2 +- .../wallet_info_row_balance_future_test.mocks.dart | 2 +- .../wallet_info_row/wallet_info_row_test.mocks.dart | 2 +- 72 files changed, 75 insertions(+), 83 deletions(-) rename lib/{ => db}/hive/db.dart (100%) rename lib/db/{ => isar}/main_db.dart (100%) diff --git a/lib/hive/db.dart b/lib/db/hive/db.dart similarity index 100% rename from lib/hive/db.dart rename to lib/db/hive/db.dart diff --git a/lib/db/main_db.dart b/lib/db/isar/main_db.dart similarity index 100% rename from lib/db/main_db.dart rename to lib/db/isar/main_db.dart diff --git a/lib/electrumx_rpc/cached_electrumx.dart b/lib/electrumx_rpc/cached_electrumx.dart index e7d815eab..935e8605e 100644 --- a/lib/electrumx_rpc/cached_electrumx.dart +++ b/lib/electrumx_rpc/cached_electrumx.dart @@ -1,7 +1,7 @@ import 'dart:convert'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; diff --git a/lib/main.dart b/lib/main.dart index 8612d2f7e..f7548a850 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,8 +17,8 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:isar/isar.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:stackwallet/db/main_db.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; diff --git a/lib/pages/address_book_views/subviews/contact_details_view.dart b/lib/pages/address_book_views/subviews/contact_details_view.dart index 4f417c9a5..b1ed4f2da 100644 --- a/lib/pages/address_book_views/subviews/contact_details_view.dart +++ b/lib/pages/address_book_views/subviews/contact_details_view.dart @@ -27,7 +27,7 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; import 'package:tuple/tuple.dart'; -import '../../../db/main_db.dart'; +import '../../../db/isar/main_db.dart'; class ContactDetailsView extends ConsumerStatefulWidget { const ContactDetailsView({ diff --git a/lib/pages/exchange_view/exchange_view.dart b/lib/pages/exchange_view/exchange_view.dart index f96ed39c3..d7a988574 100644 --- a/lib/pages/exchange_view/exchange_view.dart +++ b/lib/pages/exchange_view/exchange_view.dart @@ -17,7 +17,7 @@ import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/trade_card.dart'; import 'package:tuple/tuple.dart'; -import '../../db/main_db.dart'; +import '../../db/isar/main_db.dart'; class ExchangeView extends ConsumerStatefulWidget { const ExchangeView({Key? key}) : super(key: key); diff --git a/lib/pages/receive_view/addresses/address_card.dart b/lib/pages/receive_view/addresses/address_card.dart index c297060d2..72aa5d03d 100644 --- a/lib/pages/receive_view/addresses/address_card.dart +++ b/lib/pages/receive_view/addresses/address_card.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_qr_popup.dart'; diff --git a/lib/pages/receive_view/addresses/edit_address_label_view.dart b/lib/pages/receive_view/addresses/edit_address_label_view.dart index 5230e2295..ed6a610fe 100644 --- a/lib/pages/receive_view/addresses/edit_address_label_view.dart +++ b/lib/pages/receive_view/addresses/edit_address_label_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/address_label.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/pages/receive_view/addresses/receiving_addresses_view.dart b/lib/pages/receive_view/addresses/receiving_addresses_view.dart index ac817bedf..d2b513a46 100644 --- a/lib/pages/receive_view/addresses/receiving_addresses_view.dart +++ b/lib/pages/receive_view/addresses/receiving_addresses_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart index d25a650e3..f855cf46f 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 280077819..b337364b2 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index b2c77dc5e..05b885eff 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; import 'package:stackwallet/pages_desktop_specific/password/create_password_view.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; diff --git a/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart index 4b2eb33a8..6544e1acd 100644 --- a/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart @@ -24,7 +24,7 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; import 'package:tuple/tuple.dart'; -import '../../../db/main_db.dart'; +import '../../../db/isar/main_db.dart'; class DesktopContactDetails extends ConsumerStatefulWidget { const DesktopContactDetails({ diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart index 7dd6578c3..aaeb7e854 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart @@ -29,7 +29,7 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:tuple/tuple.dart'; -import '../../db/main_db.dart'; +import '../../db/isar/main_db.dart'; class DesktopAllTradesView extends ConsumerStatefulWidget { const DesktopAllTradesView({Key? key}) : super(key: key); diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart index b87ca6c6b..a7dcca99f 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -19,7 +19,7 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/trade_card.dart'; -import '../../../db/main_db.dart'; +import '../../../db/isar/main_db.dart'; class DesktopTradeHistory extends ConsumerStatefulWidget { const DesktopTradeHistory({Key? key}) : super(key: key); diff --git a/lib/pages_desktop_specific/password/delete_password_warning_view.dart b/lib/pages_desktop_specific/password/delete_password_warning_view.dart index 54db989fd..700754efd 100644 --- a/lib/pages_desktop_specific/password/delete_password_warning_view.dart +++ b/lib/pages_desktop_specific/password/delete_password_warning_view.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hive/hive.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/utilities/assets.dart'; diff --git a/lib/pages_desktop_specific/password/desktop_login_view.dart b/lib/pages_desktop_specific/password/desktop_login_view.dart index 05ad2d2ad..641d70fa5 100644 --- a/lib/pages_desktop_specific/password/desktop_login_view.dart +++ b/lib/pages_desktop_specific/password/desktop_login_view.dart @@ -20,7 +20,7 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; -import '../../hive/db.dart'; +import '../../db/hive/db.dart'; import '../../utilities/db_version_migration.dart'; import '../../utilities/logger.dart'; diff --git a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart index 60dad82f1..735253ada 100644 --- a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart +++ b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart index 2f8d09e77..8b119b369 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/price_provider.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; diff --git a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart index ebe94ed12..15ae4b7b1 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index 80c60e4b8..e92c0b00b 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index 25a74e7b9..53adc9892 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -11,7 +11,7 @@ import 'package:bs58check/bs58check.dart' as bs58check; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 82bfda2e1..b20f34309 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -11,7 +11,7 @@ import 'package:bs58check/bs58check.dart' as bs58check; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index f1eb0ab20..86e3e8be1 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -11,7 +11,7 @@ import 'package:bs58check/bs58check.dart' as bs58check; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 8785661c9..980784ecf 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -9,7 +9,7 @@ import 'package:flutter_libepiccash/epic_cash.dart'; import 'package:isar/isar.dart'; import 'package:mutex/mutex.dart'; import 'package:stack_wallet_backup/generate_password.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/node_model.dart'; diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 526d141a8..699f43d34 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -5,7 +5,7 @@ import 'package:decimal/decimal.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/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 1ea8ddf13..d5eb20de2 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -10,7 +10,7 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:lelantus/lelantus.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 46aa56f46..762a39cb2 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -11,7 +11,7 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index a15d227c6..51fd1121e 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/models.dart'; diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index d8aaee0c1..11a0aacd2 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -23,8 +23,8 @@ import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output; import 'package:isar/isar.dart'; import 'package:mutex/mutex.dart'; -import 'package:stackwallet/db/main_db.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/node_model.dart'; diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 4277a2974..6ce0581e0 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -11,7 +11,7 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 7b32ac54f..4142053c4 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -11,7 +11,7 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index c10a37728..7dec040e1 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -25,8 +25,8 @@ import 'package:flutter_libmonero/view_model/send/output.dart' import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:isar/isar.dart'; import 'package:mutex/mutex.dart'; -import 'package:stackwallet/db/main_db.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/node_model.dart'; diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart index ad754cb56..8e73e46a9 100644 --- a/lib/services/exchange/exchange_data_loading_service.dart +++ b/lib/services/exchange/exchange_data_loading_service.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/exchange/aggregate_currency.dart'; import 'package:stackwallet/models/exchange/exchange_form_state.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; diff --git a/lib/services/mixins/epic_cash_hive.dart b/lib/services/mixins/epic_cash_hive.dart index 09a3563b8..3c5c91b07 100644 --- a/lib/services/mixins/epic_cash_hive.dart +++ b/lib/services/mixins/epic_cash_hive.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; mixin EpicCashHive { late final String _walletId; diff --git a/lib/services/mixins/eth_extras_wallet_cache.dart b/lib/services/mixins/eth_extras_wallet_cache.dart index e0ef75ee0..b7184a916 100644 --- a/lib/services/mixins/eth_extras_wallet_cache.dart +++ b/lib/services/mixins/eth_extras_wallet_cache.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; mixin EthExtrasWalletCache { diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart index b182da682..63501e501 100644 --- a/lib/services/mixins/eth_token_cache.dart +++ b/lib/services/mixins/eth_token_cache.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/models/token_balance.dart'; diff --git a/lib/services/mixins/firo_hive.dart b/lib/services/mixins/firo_hive.dart index 43f0dd622..321724ad1 100644 --- a/lib/services/mixins/firo_hive.dart +++ b/lib/services/mixins/firo_hive.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; mixin FiroHive { late final String _walletId; diff --git a/lib/services/mixins/paynym_wallet_interface.dart b/lib/services/mixins/paynym_wallet_interface.dart index 4192e7f42..730dfd2bf 100644 --- a/lib/services/mixins/paynym_wallet_interface.dart +++ b/lib/services/mixins/paynym_wallet_interface.dart @@ -10,7 +10,7 @@ import 'package:bitcoindart/src/utils/constants/op.dart' as op; import 'package:bitcoindart/src/utils/script.dart' as bscript; import 'package:isar/isar.dart'; import 'package:pointycastle/digests/sha256.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart'; import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart'; diff --git a/lib/services/mixins/wallet_cache.dart b/lib/services/mixins/wallet_cache.dart index ec0cbfe67..7d859e74c 100644 --- a/lib/services/mixins/wallet_cache.dart +++ b/lib/services/mixins/wallet_cache.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/services/mixins/wallet_db.dart b/lib/services/mixins/wallet_db.dart index cf62cf6da..bb46be7d1 100644 --- a/lib/services/mixins/wallet_db.dart +++ b/lib/services/mixins/wallet_db.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; mixin WalletDB { MainDB? _db; diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index 8a0e17ad7..fc588fa6a 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/services/notes_service.dart b/lib/services/notes_service.dart index 013600625..eac8b6c22 100644 --- a/lib/services/notes_service.dart +++ b/lib/services/notes_service.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/logger.dart'; class NotesService extends ChangeNotifier { diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index 51d2fc7ef..c21eab41b 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/notification_model.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; diff --git a/lib/services/price.dart b/lib/services/price.dart index 1863c3b74..2a50f0cf1 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; diff --git a/lib/services/tokens_service.dart b/lib/services/tokens_service.dart index 3c21fe3ea..166a97cda 100644 --- a/lib/services/tokens_service.dart +++ b/lib/services/tokens_service.dart @@ -1,16 +1,9 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_libmonero/monero/monero.dart'; -import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:stackwallet/hive/db.dart'; -import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; -import 'package:stackwallet/services/notifications_service.dart'; -import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:uuid/uuid.dart'; class TokenInfo { final Coin coin; diff --git a/lib/services/trade_notes_service.dart b/lib/services/trade_notes_service.dart index 763566736..ba5226001 100644 --- a/lib/services/trade_notes_service.dart +++ b/lib/services/trade_notes_service.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; class TradeNotesService extends ChangeNotifier { Map get all { diff --git a/lib/services/trade_sent_from_stack_service.dart b/lib/services/trade_sent_from_stack_service.dart index 88c7d7602..20d2b0ffb 100644 --- a/lib/services/trade_sent_from_stack_service.dart +++ b/lib/services/trade_sent_from_stack_service.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/trade_wallet_lookup.dart'; class TradeSentFromStackService extends ChangeNotifier { diff --git a/lib/services/trade_service.dart b/lib/services/trade_service.dart index abdcebb4b..7d1a3e00c 100644 --- a/lib/services/trade_service.dart +++ b/lib/services/trade_service.dart @@ -1,5 +1,5 @@ import 'package:flutter/cupertino.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; class TradesService extends ChangeNotifier { diff --git a/lib/services/transaction_notification_tracker.dart b/lib/services/transaction_notification_tracker.dart index 6a2cce2c7..732c4a110 100644 --- a/lib/services/transaction_notification_tracker.dart +++ b/lib/services/transaction_notification_tracker.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; class TransactionNotificationTracker { final String walletId; diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 4d5c07fb9..ab3886015 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/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'; diff --git a/lib/services/wallets_service.dart b/lib/services/wallets_service.dart index 3a9c99346..085937ca6 100644 --- a/lib/services/wallets_service.dart +++ b/lib/services/wallets_service.dart @@ -3,8 +3,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:stackwallet/db/main_db.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index 67e76ef29..97b3630df 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -1,7 +1,7 @@ import 'package:hive/hive.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; diff --git a/lib/utilities/delete_everything.dart b/lib/utilities/delete_everything.dart index 497cd71f4..446f43b3a 100644 --- a/lib/utilities/delete_everything.dart +++ b/lib/utilities/delete_everything.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/logger.dart'; Future deleteEverything() async { diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart index 6263eb33b..dc32b8254 100644 --- a/lib/utilities/desktop_password_service.dart +++ b/lib/utilities/desktop_password_service.dart @@ -1,6 +1,6 @@ import 'package:hive/hive.dart'; import 'package:stack_wallet_backup/secure_storage.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/logger.dart'; const String _kKeyBlobKey = "swbKeyBlobKeyStringID"; diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index f803ff9b2..f6e247312 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -1,5 +1,5 @@ import 'package:flutter/cupertino.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/languages_enum.dart'; diff --git a/test/address_book_service_test.dart b/test/address_book_service_test.dart index c5effd223..adc036d5a 100644 --- a/test/address_book_service_test.dart +++ b/test/address_book_service_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/services/address_book_service.dart'; diff --git a/test/cached_electrumx_test.dart b/test/cached_electrumx_test.dart index e0f7fd6ca..329de9daf 100644 --- a/test/cached_electrumx_test.dart +++ b/test/cached_electrumx_test.dart @@ -3,9 +3,9 @@ import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; diff --git a/test/hive/db_test.dart b/test/hive/db_test.dart index a87f568bd..2ea60bd52 100644 --- a/test/hive/db_test.dart +++ b/test/hive/db_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive_test/hive_test.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; void main() { diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 50e97f600..2f18d2d02 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i14; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i13; +import 'package:stackwallet/db/isar/main_db.dart' as _i13; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i11; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i10; import 'package:stackwallet/models/balance.dart' as _i12; diff --git a/test/price_test.dart b/test/price_test.dart index 42cc28c2c..b93255976 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -6,7 +6,7 @@ import 'package:hive_test/hive_test.dart'; import 'package:http/http.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/services/price.dart'; import 'price_test.mocks.dart'; diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart index 20ab88a53..d6a3f7454 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -4,9 +4,9 @@ import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.dart index 697d1c98c..9e117d2ca 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.dart @@ -4,9 +4,9 @@ import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; @@ -40,7 +40,6 @@ void main() { }); }); - group("validate mainnet dogecoin addresses", () { MockElectrumX? client; MockCachedElectrumX? cachedClient; diff --git a/test/services/coins/manager_test.mocks.dart b/test/services/coins/manager_test.mocks.dart index 273b4a01c..fdc1e7e2b 100644 --- a/test/services/coins/manager_test.mocks.dart +++ b/test/services/coins/manager_test.mocks.dart @@ -7,7 +7,7 @@ import 'dart:async' as _i10; import 'package:decimal/decimal.dart' as _i8; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i7; +import 'package:stackwallet/db/isar/main_db.dart' as _i7; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i5; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i4; import 'package:stackwallet/models/balance.dart' as _i6; diff --git a/test/services/node_service_test.dart b/test/services/node_service_test.dart index cea30be2d..2bd889b6e 100644 --- a/test/services/node_service_test.dart +++ b/test/services/node_service_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; diff --git a/test/services/wallets_service_test.dart b/test/services/wallets_service_test.dart index fc1984923..86659206a 100644 --- a/test/services/wallets_service_test.dart +++ b/test/services/wallets_service_test.dart @@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/services/wallets_service.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index c3237983f..921091fb4 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index 26b0bdfcc..3ec849e3e 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 5d0011b14..7077d7db5 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -10,7 +10,7 @@ import 'package:decimal/decimal.dart' as _i14; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i13; +import 'package:stackwallet/db/isar/main_db.dart' as _i13; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i12; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i11; import 'package:stackwallet/models/balance.dart' as _i9; diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index e3a64d7da..0fd68c3bb 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index 18da705aa..eb6bc6dac 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 0f8c36d4a..f34be63e9 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; From 8466180b47a2937928757b6e0512791b208ce7ed Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 1 Mar 2023 18:02:53 -0600 Subject: [PATCH 075/208] get token abi fixes --- lib/services/ethereum/ethereum_api.dart | 114 ++++++++++++------ .../ethereum/ethereum_token_service.dart | 87 +++++-------- 2 files changed, 112 insertions(+), 89 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index af8e5a5ca..9236ebb71 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -9,26 +9,6 @@ import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/logger.dart'; -class AbiRequestResponse { - final String message; - final String result; - final String status; - - const AbiRequestResponse({ - required this.message, - required this.result, - required this.status, - }); - - factory AbiRequestResponse.fromJson(Map json) { - return AbiRequestResponse( - message: json['message'] as String, - result: json['result'] as String, - status: json['status'] as String, - ); - } -} - class EthTokenTx { final String blockHash; final int blockNumber; @@ -191,7 +171,7 @@ abstract class EthereumAPI { } } else { throw EthApiException( - "getWalletTokens($address) failed with status code: " + "getTokenTransactions($address) failed with status code: " "${response.statusCode}", ); } @@ -202,7 +182,7 @@ abstract class EthereumAPI { ); } catch (e, s) { Logging.instance.log( - "getWalletTokens(): $e\n$s", + "getTokenTransactions(): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( @@ -310,7 +290,7 @@ abstract class EthereumAPI { } } else { throw EthApiException( - "getWalletTokens($address) failed with status code: " + "getWalletTokenBalance($address) failed with status code: " "${response.statusCode}", ); } @@ -321,7 +301,7 @@ abstract class EthereumAPI { ); } catch (e, s) { Logging.instance.log( - "getWalletTokens(): $e\n$s", + "getWalletTokenBalance(): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( @@ -356,7 +336,6 @@ abstract class EthereumAPI { slow: feesSlow.toInt()); } - //Validate that a custom token is valid and is ERC-20, a token will be valid static Future> getTokenByContractAddress( String contractAddress) async { try { @@ -407,7 +386,7 @@ abstract class EthereumAPI { ); } catch (e, s) { Logging.instance.log( - "getWalletTokens(): $e\n$s", + "getTokenByContractAddress(): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( @@ -417,15 +396,82 @@ abstract class EthereumAPI { } } - static Future fetchTokenAbi( + static Future> getTokenAbi( String contractAddress) async { - final response = await get(Uri.parse( - "$etherscanApi?module=contract&action=getabi&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); - if (response.statusCode == 200) { - return AbiRequestResponse.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception("ERROR GETTING TOKENABI ${response.reasonPhrase}"); + try { + final response = await get(Uri.parse( + "$etherscanApi?module=contract&action=getabi&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json["message"] == "OK") { + return EthereumResponse( + json["result"] as String, + null, + ); + } else { + throw EthApiException(json["message"] as String); + } + } else { + throw EthApiException( + "getTokenAbi($contractAddress) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getTokenAbi(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future> getProxyTokenImplementation( + String contractAddress) async { + try { + final response = await get(Uri.parse( + "$etherscanApi?module=contract&action=getsourcecode&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json["message"] == "OK") { + final list = json["result"] as List; + final map = Map.from(list.first as Map); + + return EthereumResponse( + map["Implementation"] as String, + null, + ); + } else { + throw EthApiException(json["message"] as String); + } + } else { + throw EthApiException( + "fetchProxyTokenImplementation($contractAddress) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "fetchProxyTokenImplementation(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); } } } diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index dcf8acc46..104d379ce 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; @@ -115,16 +114,17 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { await _secureStore.read(key: '${_contractAddress.toString()}_tokenAbi'); if (storedABI == null) { - AbiRequestResponse abi = - await EthereumAPI.fetchTokenAbi(_contractAddress.hex); + final abiResponse = await EthereumAPI.getTokenAbi(_contractAddress.hex); //Fetch token ABI so we can call token functions - if (abi.message == "OK") { - _tokenAbi = abi.result; + if (abiResponse.value != null) { + _tokenAbi = abiResponse.value!; //Store abi in secure store await _secureStore.write( - key: '${_contractAddress.hex}_tokenAbi', value: _tokenAbi); + key: '${_contractAddress.hex}_tokenAbi', + value: _tokenAbi, + ); } else { - throw Exception('Failed to load token abi'); + throw abiResponse.exception!; } } else { _tokenAbi = storedABI; @@ -140,61 +140,38 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { _contract = web3dart.DeployedContract( web3dart.ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); - bool hackInBalanceOf = false, hackInTransfer = false; try { _balanceFunction = _contract.function('balanceOf'); - } catch (_) { - // function not found so likely a proxy so we need to hack the function in - hackInBalanceOf = true; - } - - try { _sendFunction = _contract.function('transfer'); } catch (_) { - // function not found so likely a proxy so we need to hack the function in - hackInTransfer = true; + // function not found so likely a proxy so we need to fetch the impl + final response = + await EthereumAPI.getProxyTokenImplementation(_contractAddress.hex); + + if (response.value != null) { + final abiResponse = await EthereumAPI.getTokenAbi(response.value!); + if (abiResponse.value != null) { + _tokenAbi = abiResponse.value!; + await _secureStore.write( + key: '${_contractAddress.hex}_tokenAbi', value: _tokenAbi); + } else { + throw abiResponse.exception!; + } + } else { + throw response.exception!; + } } - if (hackInBalanceOf || hackInTransfer) { - final json = jsonDecode(_tokenAbi) as List; - if (hackInBalanceOf) { - json.add({ - "constant": true, - "inputs": [ - {"name": "", "type": "address"} - ], - "name": "balanceOf", - "outputs": [ - {"name": "", "type": "uint256"} - ], - "payable": false, - "type": "function" - }); - } - if (hackInTransfer) { - json.add({ - "constant": false, - "inputs": [ - {"name": "_to", "type": "address"}, - {"name": "_value", "type": "uint256"} - ], - "name": "transfer", - "outputs": [], - "payable": false, - "type": "function" - }); - } - _tokenAbi = jsonEncode(json); - await _secureStore.write( - key: '${_contractAddress.hex}_tokenAbi', value: _tokenAbi); + _contract = web3dart.DeployedContract( + web3dart.ContractAbi.fromJson( + _tokenAbi, + token.name, + ), + _contractAddress, + ); - _contract = web3dart.DeployedContract( - web3dart.ContractAbi.fromJson(_tokenAbi, token.name), - _contractAddress); - - _balanceFunction = _contract.function('balanceOf'); - _sendFunction = _contract.function('transfer'); - } + _balanceFunction = _contract.function('balanceOf'); + _sendFunction = _contract.function('transfer'); _client = await getEthClient(); From b4b4c5e69650eb76a7ed500ccacf4df412c5e36a Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 Mar 2023 13:28:51 -0600 Subject: [PATCH 076/208] WIP eth api refactor to use chifra based api --- lib/services/ethereum/ethereum_api.dart | 73 ++++++++++++++++++------- lib/utilities/eth_commons.dart | 8 +-- 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 9236ebb71..15ba969d3 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -1,11 +1,13 @@ import 'dart:convert'; import 'dart:math'; +import 'package:decimal/decimal.dart'; import 'package:http/http.dart'; import 'package:stackwallet/models/ethereum/erc20_token.dart'; import 'package:stackwallet/models/ethereum/erc721_token.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -106,10 +108,14 @@ class EthereumResponse { final EthApiException? exception; @override - toString() => "EthereumResponse{ value: $value, exception: $exception"; + toString() => "EthereumResponse: { value: $value, exception: $exception }"; } abstract class EthereumAPI { + static String get stackBaseServer => DefaultNodes.ethereum.host; + + static String stackURI = "$stackBaseServer/eth/mainnet/api"; + // static const blockScout = "https://blockscout.com/eth/mainnet/api"; static const etherscanApi = "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update @@ -120,12 +126,18 @@ abstract class EthereumAPI { static Future fetchAddressTransactions( String address) async { try { - final response = await get(Uri.parse( + final response = await get( + Uri.parse( // "$blockScout?module=account&action=txlist&address=$address")); - "$etherscanApi?module=account&action=txlist&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + // "stackURI?module=account&action=txlist&address=$address")); + "$stackBaseServer/export?addrs=$address", + ), + ); + + // "$etherscanApi?module=account&action=txlist&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AddressTransaction.fromJson( - jsonDecode(response.body) as Map); + jsonDecode(response.body)["data"] as List); } else { throw Exception( 'ERROR GETTING TRANSACTIONS WITH STATUS ${response.statusCode}'); @@ -145,6 +157,7 @@ abstract class EthereumAPI { try { String uriString = // "$blockScout?module=account&action=tokentx&address=$address"; + // "stackURI?module=account&action=tokentx&address=$address"; "$etherscanApi?module=account&action=tokentx&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"; if (contractAddress != null) { uriString += "&contractAddress=$contractAddress"; @@ -271,18 +284,28 @@ abstract class EthereumAPI { }) async { try { final uri = Uri.parse( - "$etherscanApi?module=account&action=tokenbalance" - "&contractaddress=$contractAddress&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP", + "$stackBaseServer/tokens?addrs=$contractAddress $address", ); final response = await get(uri); if (response.statusCode == 200) { final json = jsonDecode(response.body); - if (json["message"] == "OK") { - final result = json["result"] as String; + if (json["data"] is List) { + final map = json["data"].first as Map; + + final bal = Decimal.tryParse(map["balance"].toString()); + final int balance; + if (bal == null) { + balance = 0; + } else { + final int decimals = map["decimals"] as int; + balance = (bal * Decimal.fromInt(pow(10, decimals).truncate())) + .toBigInt() + .toInt(); + } return EthereumResponse( - int.parse(result), + balance, null, ); } else { @@ -341,6 +364,7 @@ abstract class EthereumAPI { try { final response = await get(Uri.parse( "$etherscanApi?module=token&action=getToken&contractaddress=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + // "stackURI?module=token&action=getToken&contractaddress=$contractAddress")); // "$blockScout?module=token&action=getToken&contractaddress=$contractAddress")); if (response.statusCode == 200) { final json = jsonDecode(response.body); @@ -399,18 +423,28 @@ abstract class EthereumAPI { static Future> getTokenAbi( String contractAddress) async { try { - final response = await get(Uri.parse( - "$etherscanApi?module=contract&action=getabi&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + final response = await get( + Uri.parse( + "$stackBaseServer/abis?addrs=$contractAddress", + ), + ); + if (response.statusCode == 200) { - final json = jsonDecode(response.body); - if (json["message"] == "OK") { - return EthereumResponse( - json["result"] as String, - null, - ); - } else { - throw EthApiException(json["message"] as String); + final json = jsonDecode(response.body)["data"] as List; + + // trueblocks api does not contain the `anonymous` value + // web3dart expects it so hack it in + // TODO: fix this if we ever actually need to use contract ABI events + for (final map in json) { + if (map["type"] == "event") { + map["anonymous"] = false; + } } + + return EthereumResponse( + jsonEncode(json), + null, + ); } else { throw EthApiException( "getTokenAbi($contractAddress) failed with status code: " @@ -438,6 +472,7 @@ abstract class EthereumAPI { String contractAddress) async { try { final response = await get(Uri.parse( + // "$stackURI?module=contract&action=getsourcecode&address=$contractAddress")); "$etherscanApi?module=contract&action=getsourcecode&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { final json = jsonDecode(response.body); diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index 9eeededd6..c250d68c5 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -15,11 +15,11 @@ class AddressTransaction { required this.status, }); - factory AddressTransaction.fromJson(Map json) { + factory AddressTransaction.fromJson(List json) { return AddressTransaction( - message: json['message'] as String, - result: json['result'] as List, - status: json['status'] as String, + message: "", + result: json, + status: "", ); } From 4cec54620a5d2a5c3650de2ed6213c9fae8a4fad Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 Mar 2023 14:08:29 -0600 Subject: [PATCH 077/208] eth contract isar schema --- lib/db/isar/main_db.dart | 1 + lib/models/isar/models/contract.dart | 3 + .../isar/models/ethereum/eth_contract.dart | 42 + .../isar/models/ethereum/eth_contract.g.dart | 1526 +++++++++++++++++ lib/models/isar/models/isar_models.dart | 1 + 5 files changed, 1573 insertions(+) create mode 100644 lib/models/isar/models/contract.dart create mode 100644 lib/models/isar/models/ethereum/eth_contract.dart create mode 100644 lib/models/isar/models/ethereum/eth_contract.g.dart diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 56d802473..2e5828eb6 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -27,6 +27,7 @@ class MainDB { UTXOSchema, AddressSchema, AddressLabelSchema, + EthContractSchema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, diff --git a/lib/models/isar/models/contract.dart b/lib/models/isar/models/contract.dart new file mode 100644 index 000000000..88e2a454c --- /dev/null +++ b/lib/models/isar/models/contract.dart @@ -0,0 +1,3 @@ +abstract class Contract { + // for possible future use +} diff --git a/lib/models/isar/models/ethereum/eth_contract.dart b/lib/models/isar/models/ethereum/eth_contract.dart new file mode 100644 index 000000000..ac1b07209 --- /dev/null +++ b/lib/models/isar/models/ethereum/eth_contract.dart @@ -0,0 +1,42 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/contract.dart'; + +part 'eth_contract.g.dart'; + +@collection +class EthContract extends Contract { + EthContract({ + required this.address, + required this.name, + required this.symbol, + required this.decimals, + required this.type, + this.abi, + this.otherData, + }); + + Id id = Isar.autoIncrement; + + @Index(unique: true, replace: true) + late final String address; + + late final String name; + + late final String symbol; + + late final int decimals; + + late final String? abi; + + @enumerated + late final EthContractType type; + + late final String? otherData; +} + +// Used in Isar db and stored there as int indexes so adding/removing values +// in this definition should be done extremely carefully in production +enum EthContractType { + erc20, + erc721; +} diff --git a/lib/models/isar/models/ethereum/eth_contract.g.dart b/lib/models/isar/models/ethereum/eth_contract.g.dart new file mode 100644 index 000000000..d79052df2 --- /dev/null +++ b/lib/models/isar/models/ethereum/eth_contract.g.dart @@ -0,0 +1,1526 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'eth_contract.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters + +extension GetEthContractCollection on Isar { + IsarCollection get ethContracts => this.collection(); +} + +const EthContractSchema = CollectionSchema( + name: r'EthContract', + id: 3784021410994159165, + properties: { + r'abi': PropertySchema( + id: 0, + name: r'abi', + type: IsarType.string, + ), + r'address': PropertySchema( + id: 1, + name: r'address', + type: IsarType.string, + ), + r'decimals': PropertySchema( + id: 2, + name: r'decimals', + type: IsarType.long, + ), + r'name': PropertySchema( + id: 3, + name: r'name', + type: IsarType.string, + ), + r'otherData': PropertySchema( + id: 4, + name: r'otherData', + type: IsarType.string, + ), + r'symbol': PropertySchema( + id: 5, + name: r'symbol', + type: IsarType.string, + ), + r'type': PropertySchema( + id: 6, + name: r'type', + type: IsarType.byte, + enumMap: _EthContracttypeEnumValueMap, + ) + }, + estimateSize: _ethContractEstimateSize, + serialize: _ethContractSerialize, + deserialize: _ethContractDeserialize, + deserializeProp: _ethContractDeserializeProp, + idName: r'id', + indexes: { + r'address': IndexSchema( + id: -259407546592846288, + name: r'address', + unique: true, + replace: true, + properties: [ + IndexPropertySchema( + name: r'address', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _ethContractGetId, + getLinks: _ethContractGetLinks, + attach: _ethContractAttach, + version: '3.0.5', +); + +int _ethContractEstimateSize( + EthContract object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.abi; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + bytesCount += 3 + object.address.length * 3; + bytesCount += 3 + object.name.length * 3; + { + final value = object.otherData; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + bytesCount += 3 + object.symbol.length * 3; + return bytesCount; +} + +void _ethContractSerialize( + EthContract object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.abi); + writer.writeString(offsets[1], object.address); + writer.writeLong(offsets[2], object.decimals); + writer.writeString(offsets[3], object.name); + writer.writeString(offsets[4], object.otherData); + writer.writeString(offsets[5], object.symbol); + writer.writeByte(offsets[6], object.type.index); +} + +EthContract _ethContractDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = EthContract( + abi: reader.readStringOrNull(offsets[0]), + address: reader.readString(offsets[1]), + decimals: reader.readLong(offsets[2]), + name: reader.readString(offsets[3]), + otherData: reader.readStringOrNull(offsets[4]), + symbol: reader.readString(offsets[5]), + type: _EthContracttypeValueEnumMap[reader.readByteOrNull(offsets[6])] ?? + EthContractType.erc20, + ); + object.id = id; + return object; +} + +P _ethContractDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringOrNull(offset)) as P; + case 1: + return (reader.readString(offset)) as P; + case 2: + return (reader.readLong(offset)) as P; + case 3: + return (reader.readString(offset)) as P; + case 4: + return (reader.readStringOrNull(offset)) as P; + case 5: + return (reader.readString(offset)) as P; + case 6: + return (_EthContracttypeValueEnumMap[reader.readByteOrNull(offset)] ?? + EthContractType.erc20) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +const _EthContracttypeEnumValueMap = { + 'erc20': 0, + 'erc721': 1, +}; +const _EthContracttypeValueEnumMap = { + 0: EthContractType.erc20, + 1: EthContractType.erc721, +}; + +Id _ethContractGetId(EthContract object) { + return object.id; +} + +List> _ethContractGetLinks(EthContract object) { + return []; +} + +void _ethContractAttach( + IsarCollection col, Id id, EthContract object) { + object.id = id; +} + +extension EthContractByIndex on IsarCollection { + Future getByAddress(String address) { + return getByIndex(r'address', [address]); + } + + EthContract? getByAddressSync(String address) { + return getByIndexSync(r'address', [address]); + } + + Future deleteByAddress(String address) { + return deleteByIndex(r'address', [address]); + } + + bool deleteByAddressSync(String address) { + return deleteByIndexSync(r'address', [address]); + } + + Future> getAllByAddress(List addressValues) { + final values = addressValues.map((e) => [e]).toList(); + return getAllByIndex(r'address', values); + } + + List getAllByAddressSync(List addressValues) { + final values = addressValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'address', values); + } + + Future deleteAllByAddress(List addressValues) { + final values = addressValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'address', values); + } + + int deleteAllByAddressSync(List addressValues) { + final values = addressValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'address', values); + } + + Future putByAddress(EthContract object) { + return putByIndex(r'address', object); + } + + Id putByAddressSync(EthContract object, {bool saveLinks = true}) { + return putByIndexSync(r'address', object, saveLinks: saveLinks); + } + + Future> putAllByAddress(List objects) { + return putAllByIndex(r'address', objects); + } + + List putAllByAddressSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'address', objects, saveLinks: saveLinks); + } +} + +extension EthContractQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension EthContractQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder idNotEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder idGreaterThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder addressEqualTo( + String address) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'address', + value: [address], + )); + }); + } + + QueryBuilder addressNotEqualTo( + String address) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'address', + lower: [], + upper: [address], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'address', + lower: [address], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'address', + lower: [address], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'address', + lower: [], + upper: [address], + includeUpper: false, + )); + } + }); + } +} + +extension EthContractQueryFilter + on QueryBuilder { + QueryBuilder abiIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'abi', + )); + }); + } + + QueryBuilder abiIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'abi', + )); + }); + } + + QueryBuilder abiEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'abi', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'abi', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'abi', + value: '', + )); + }); + } + + QueryBuilder + abiIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'abi', + value: '', + )); + }); + } + + QueryBuilder addressEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + addressGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'address', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + addressStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'address', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + addressIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'address', + value: '', + )); + }); + } + + QueryBuilder + addressIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'address', + value: '', + )); + }); + } + + QueryBuilder decimalsEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'decimals', + value: value, + )); + }); + } + + QueryBuilder + decimalsGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'decimals', + value: value, + )); + }); + } + + QueryBuilder + decimalsLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'decimals', + value: value, + )); + }); + } + + QueryBuilder decimalsBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'decimals', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder idEqualTo( + Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder nameEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'name', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'name', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'name', + value: '', + )); + }); + } + + QueryBuilder + nameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'name', + value: '', + )); + }); + } + + QueryBuilder + otherDataIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'otherData', + )); + }); + } + + QueryBuilder + otherDataIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'otherData', + )); + }); + } + + QueryBuilder + otherDataEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'otherData', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'otherData', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'otherData', + value: '', + )); + }); + } + + QueryBuilder + otherDataIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'otherData', + value: '', + )); + }); + } + + QueryBuilder symbolEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + symbolGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'symbol', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + symbolStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'symbol', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + symbolIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'symbol', + value: '', + )); + }); + } + + QueryBuilder + symbolIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'symbol', + value: '', + )); + }); + } + + QueryBuilder typeEqualTo( + EthContractType value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeGreaterThan( + EthContractType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeLessThan( + EthContractType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeBetween( + EthContractType lower, + EthContractType upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'type', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } +} + +extension EthContractQueryObject + on QueryBuilder {} + +extension EthContractQueryLinks + on QueryBuilder {} + +extension EthContractQuerySortBy + on QueryBuilder { + QueryBuilder sortByAbi() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'abi', Sort.asc); + }); + } + + QueryBuilder sortByAbiDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'abi', Sort.desc); + }); + } + + QueryBuilder sortByAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.asc); + }); + } + + QueryBuilder sortByAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.desc); + }); + } + + QueryBuilder sortByDecimals() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'decimals', Sort.asc); + }); + } + + QueryBuilder sortByDecimalsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'decimals', Sort.desc); + }); + } + + QueryBuilder sortByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder sortByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder sortByOtherData() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.asc); + }); + } + + QueryBuilder sortByOtherDataDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.desc); + }); + } + + QueryBuilder sortBySymbol() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'symbol', Sort.asc); + }); + } + + QueryBuilder sortBySymbolDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'symbol', Sort.desc); + }); + } + + QueryBuilder sortByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder sortByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } +} + +extension EthContractQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByAbi() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'abi', Sort.asc); + }); + } + + QueryBuilder thenByAbiDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'abi', Sort.desc); + }); + } + + QueryBuilder thenByAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.asc); + }); + } + + QueryBuilder thenByAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.desc); + }); + } + + QueryBuilder thenByDecimals() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'decimals', Sort.asc); + }); + } + + QueryBuilder thenByDecimalsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'decimals', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder thenByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder thenByOtherData() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.asc); + }); + } + + QueryBuilder thenByOtherDataDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.desc); + }); + } + + QueryBuilder thenBySymbol() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'symbol', Sort.asc); + }); + } + + QueryBuilder thenBySymbolDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'symbol', Sort.desc); + }); + } + + QueryBuilder thenByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder thenByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } +} + +extension EthContractQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByAbi( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'abi', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByAddress( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'address', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByDecimals() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'decimals'); + }); + } + + QueryBuilder distinctByName( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'name', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByOtherData( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'otherData', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctBySymbol( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'symbol', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByType() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'type'); + }); + } +} + +extension EthContractQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder abiProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'abi'); + }); + } + + QueryBuilder addressProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'address'); + }); + } + + QueryBuilder decimalsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'decimals'); + }); + } + + QueryBuilder nameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'name'); + }); + } + + QueryBuilder otherDataProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'otherData'); + }); + } + + QueryBuilder symbolProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'symbol'); + }); + } + + QueryBuilder typeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'type'); + }); + } +} diff --git a/lib/models/isar/models/isar_models.dart b/lib/models/isar/models/isar_models.dart index 4fb7c8b24..6b244ee48 100644 --- a/lib/models/isar/models/isar_models.dart +++ b/lib/models/isar/models/isar_models.dart @@ -4,5 +4,6 @@ export 'blockchain_data/input.dart'; export 'blockchain_data/output.dart'; export 'blockchain_data/transaction.dart'; export 'blockchain_data/utxo.dart'; +export 'ethereum/eth_contract.dart'; export 'log.dart'; export 'transaction_note.dart'; From a5d8fdde79b9737bace80880574af9c141a52287 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 Mar 2023 15:07:25 -0600 Subject: [PATCH 078/208] parse eth tx json to data transfer objects --- lib/dto/ethereum/eth_token_tx_dto.dart | 82 +++++++++++ lib/dto/ethereum/eth_tx_dto.dart | 131 +++++++++++++++++ .../coins/ethereum/ethereum_wallet.dart | 37 +++-- lib/services/ethereum/ethereum_api.dart | 132 ++++++------------ 4 files changed, 272 insertions(+), 110 deletions(-) create mode 100644 lib/dto/ethereum/eth_token_tx_dto.dart create mode 100644 lib/dto/ethereum/eth_tx_dto.dart diff --git a/lib/dto/ethereum/eth_token_tx_dto.dart b/lib/dto/ethereum/eth_token_tx_dto.dart new file mode 100644 index 000000000..797fe2f7e --- /dev/null +++ b/lib/dto/ethereum/eth_token_tx_dto.dart @@ -0,0 +1,82 @@ +import 'package:stackwallet/utilities/logger.dart'; + +class EthTokenTxDTO { + final String blockHash; + final int blockNumber; + final int confirmations; + final String contractAddress; + final int cumulativeGasUsed; + final String from; + final int gas; + final BigInt gasPrice; + final int gasUsed; + final String hash; + final String input; + final int logIndex; + final int nonce; + final int timeStamp; + final String to; + final int tokenDecimal; + final String tokenName; + final String tokenSymbol; + final int transactionIndex; + final BigInt value; + + EthTokenTxDTO({ + required this.blockHash, + required this.blockNumber, + required this.confirmations, + required this.contractAddress, + required this.cumulativeGasUsed, + required this.from, + required this.gas, + required this.gasPrice, + required this.gasUsed, + required this.hash, + required this.input, + required this.logIndex, + required this.nonce, + required this.timeStamp, + required this.to, + required this.tokenDecimal, + required this.tokenName, + required this.tokenSymbol, + required this.transactionIndex, + required this.value, + }); + + factory EthTokenTxDTO.fromMap({ + required Map map, + }) { + try { + return EthTokenTxDTO( + blockHash: map["blockHash"] as String, + blockNumber: int.parse(map["blockNumber"] as String), + confirmations: int.parse(map["confirmations"] as String), + contractAddress: map["contractAddress"] as String, + cumulativeGasUsed: int.parse(map["cumulativeGasUsed"] as String), + from: map["from"] as String, + gas: int.parse(map["gas"] as String), + gasPrice: BigInt.parse(map["gasPrice"] as String), + gasUsed: int.parse(map["gasUsed"] as String), + hash: map["hash"] as String, + input: map["input"] as String, + logIndex: int.parse(map["logIndex"] as String? ?? "-1"), + nonce: int.parse(map["nonce"] as String), + timeStamp: int.parse(map["timeStamp"] as String), + to: map["to"] as String, + tokenDecimal: int.parse(map["tokenDecimal"] as String), + tokenName: map["tokenName"] as String, + tokenSymbol: map["tokenSymbol"] as String, + transactionIndex: int.parse(map["transactionIndex"] as String), + value: BigInt.parse(map["value"] as String), + ); + } catch (e, s) { + Logging.instance.log( + "EthTokenTxDTO.fromMap() failed: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + } +} diff --git a/lib/dto/ethereum/eth_tx_dto.dart b/lib/dto/ethereum/eth_tx_dto.dart new file mode 100644 index 000000000..f839711bb --- /dev/null +++ b/lib/dto/ethereum/eth_tx_dto.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; + +class EthTxDTO { + EthTxDTO({ + required this.hash, + required this.blockHash, + required this.blockNumber, + required this.transactionIndex, + required this.timestamp, + required this.from, + required this.to, + required this.value, + required this.gas, + required this.gasPrice, + required this.maxFeePerGas, + required this.maxPriorityFeePerGas, + required this.isError, + required this.hasToken, + required this.compressedTx, + required this.gasCost, + required this.gasUsed, + }); + + factory EthTxDTO.fromMap(Map map) => EthTxDTO( + hash: map['hash'] as String, + blockHash: map['blockHash'] as String, + blockNumber: map['blockNumber'] as int, + transactionIndex: map['transactionIndex'] as int, + timestamp: map['timestamp'] as int, + from: map['from'] as String, + to: map['to'] as String, + value: map['value'] as int, + gas: map['gas'] as int, + gasPrice: map['gasPrice'] as int, + maxFeePerGas: map['maxFeePerGas'] as int, + maxPriorityFeePerGas: map['maxPriorityFeePerGas'] as int, + isError: map['isError'] as int, + hasToken: map['hasToken'] as int, + compressedTx: map['compressedTx'] as String, + gasCost: map['gasCost'] as int, + gasUsed: map['gasUsed'] as int, + ); + + factory EthTxDTO.fromJsonString(String jsonString) => EthTxDTO.fromMap( + Map.from( + jsonDecode(jsonString) as Map, + ), + ); + + final String hash; + final String blockHash; + final int blockNumber; + final int transactionIndex; + final int timestamp; + final String from; + final String to; + final int value; + final int gas; + final int gasPrice; + final int maxFeePerGas; + final int maxPriorityFeePerGas; + final int isError; + final int hasToken; + final String compressedTx; + final int gasCost; + final int gasUsed; + + EthTxDTO copyWith({ + String? hash, + String? blockHash, + int? blockNumber, + int? transactionIndex, + int? timestamp, + String? from, + String? to, + int? value, + int? gas, + int? gasPrice, + int? maxFeePerGas, + int? maxPriorityFeePerGas, + int? isError, + int? hasToken, + String? compressedTx, + int? gasCost, + int? gasUsed, + }) => + EthTxDTO( + hash: hash ?? this.hash, + blockHash: blockHash ?? this.blockHash, + blockNumber: blockNumber ?? this.blockNumber, + transactionIndex: transactionIndex ?? this.transactionIndex, + timestamp: timestamp ?? this.timestamp, + from: from ?? this.from, + to: to ?? this.to, + value: value ?? this.value, + gas: gas ?? this.gas, + gasPrice: gasPrice ?? this.gasPrice, + maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, + isError: isError ?? this.isError, + hasToken: hasToken ?? this.hasToken, + compressedTx: compressedTx ?? this.compressedTx, + gasCost: gasCost ?? this.gasCost, + gasUsed: gasUsed ?? this.gasUsed, + ); + + Map toMap() { + final map = {}; + map['hash'] = hash; + map['blockHash'] = blockHash; + map['blockNumber'] = blockNumber; + map['transactionIndex'] = transactionIndex; + map['timestamp'] = timestamp; + map['from'] = from; + map['to'] = to; + map['value'] = value; + map['gas'] = gas; + map['gasPrice'] = gasPrice; + map['maxFeePerGas'] = maxFeePerGas; + map['maxPriorityFeePerGas'] = maxPriorityFeePerGas; + map['isError'] = isError; + map['hasToken'] = hasToken; + map['compressedTx'] = compressedTx; + map['gasCost'] = gasCost; + map['gasUsed'] = gasUsed; + return map; + } + + @override + String toString() => jsonEncode(toMap()); +} diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 699f43d34..d7fb2cb48 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -567,14 +567,13 @@ class EthereumWallet extends CoinServiceAPI } if (!needsRefresh) { var allOwnAddresses = await _fetchAllOwnAddresses(); - AddressTransaction addressTransactions = - await EthereumAPI.fetchAddressTransactions( + final response = await EthereumAPI.getEthTransactions( allOwnAddresses.elementAt(0).value, ); - if (addressTransactions.message == "OK") { - final allTxs = addressTransactions.result; + if (response.value != null) { + final allTxs = response.value!; for (final element in allTxs) { - final txid = element["hash"] as String; + final txid = element.hash; if ((await db .getTransactions(walletId) .filter() @@ -582,7 +581,7 @@ class EthereumWallet extends CoinServiceAPI .findFirst()) == null) { Logging.instance.log( - " txid not found in address history already ${element['hash']}", + " txid not found in address history already $txid", level: LogLevel.Info); needsRefresh = true; break; @@ -845,20 +844,18 @@ class EthereumWallet extends CoinServiceAPI Future _refreshTransactions() async { String thisAddress = await currentReceivingAddress; - AddressTransaction txs = - await EthereumAPI.fetchAddressTransactions(thisAddress); + final txsResponse = await EthereumAPI.getEthTransactions(thisAddress); - if (txs.message == "OK") { - final allTxs = txs.result; + if (txsResponse.value != null) { + final allTxs = txsResponse.value!; final List> txnsData = []; for (final element in allTxs) { - int transactionAmount = int.parse(element['value'].toString()); + int transactionAmount = element.value; bool isIncoming; bool txFailed = false; - if (checksumEthereumAddress(element["from"].toString()) == - thisAddress) { - if (!(int.parse(element["isError"] as String) == 0)) { + if (checksumEthereumAddress(element.from) == thisAddress) { + if (element.isError != 0) { txFailed = true; } isIncoming = false; @@ -867,16 +864,16 @@ class EthereumWallet extends CoinServiceAPI } //Calculate fees (GasLimit * gasPrice) - int txFee = int.parse(element['gasPrice'].toString()) * - int.parse(element['gasUsed'].toString()); + // int txFee = element.gasPrice * element.gasUsed; + int txFee = element.gasCost; - final String addressString = element["to"] as String; - final int height = int.parse(element['blockNumber'].toString()); + final String addressString = checksumEthereumAddress(element.to); + final int height = element.blockNumber; final txn = Transaction( walletId: walletId, - txid: element["hash"] as String, - timestamp: int.parse(element["timeStamp"].toString()), + txid: element.hash, + timestamp: element.timestamp, type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, subType: TransactionSubType.none, diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 15ba969d3..b73b2f1f4 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -3,6 +3,8 @@ import 'dart:math'; import 'package:decimal/decimal.dart'; import 'package:http/http.dart'; +import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; +import 'package:stackwallet/dto/ethereum/eth_tx_dto.dart'; import 'package:stackwallet/models/ethereum/erc20_token.dart'; import 'package:stackwallet/models/ethereum/erc721_token.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; @@ -11,87 +13,6 @@ import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/logger.dart'; -class EthTokenTx { - final String blockHash; - final int blockNumber; - final int confirmations; - final String contractAddress; - final int cumulativeGasUsed; - final String from; - final int gas; - final BigInt gasPrice; - final int gasUsed; - final String hash; - final String input; - final int logIndex; - final int nonce; - final int timeStamp; - final String to; - final int tokenDecimal; - final String tokenName; - final String tokenSymbol; - final int transactionIndex; - final BigInt value; - - EthTokenTx({ - required this.blockHash, - required this.blockNumber, - required this.confirmations, - required this.contractAddress, - required this.cumulativeGasUsed, - required this.from, - required this.gas, - required this.gasPrice, - required this.gasUsed, - required this.hash, - required this.input, - required this.logIndex, - required this.nonce, - required this.timeStamp, - required this.to, - required this.tokenDecimal, - required this.tokenName, - required this.tokenSymbol, - required this.transactionIndex, - required this.value, - }); - - factory EthTokenTx.fromMap({ - required Map map, - }) { - try { - return EthTokenTx( - blockHash: map["blockHash"] as String, - blockNumber: int.parse(map["blockNumber"] as String), - confirmations: int.parse(map["confirmations"] as String), - contractAddress: map["contractAddress"] as String, - cumulativeGasUsed: int.parse(map["cumulativeGasUsed"] as String), - from: map["from"] as String, - gas: int.parse(map["gas"] as String), - gasPrice: BigInt.parse(map["gasPrice"] as String), - gasUsed: int.parse(map["gasUsed"] as String), - hash: map["hash"] as String, - input: map["input"] as String, - logIndex: int.parse(map["logIndex"] as String? ?? "-1"), - nonce: int.parse(map["nonce"] as String), - timeStamp: int.parse(map["timeStamp"] as String), - to: map["to"] as String, - tokenDecimal: int.parse(map["tokenDecimal"] as String), - tokenName: map["tokenName"] as String, - tokenSymbol: map["tokenSymbol"] as String, - transactionIndex: int.parse(map["transactionIndex"] as String), - value: BigInt.parse(map["value"] as String), - ); - } catch (e, s) { - Logging.instance.log( - "EthTokenTx.fromMap() failed: $e\n$s", - level: LogLevel.Fatal, - ); - rethrow; - } - } -} - class EthApiException with Exception { EthApiException(this.message); @@ -123,7 +44,7 @@ abstract class EthereumAPI { static const gasTrackerUrl = "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; - static Future fetchAddressTransactions( + static Future>> getEthTransactions( String address) async { try { final response = await get( @@ -135,19 +56,50 @@ abstract class EthereumAPI { ); // "$etherscanApi?module=account&action=txlist&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + if (response.statusCode == 200) { - return AddressTransaction.fromJson( - jsonDecode(response.body)["data"] as List); + if (response.body.isNotEmpty) { + final json = jsonDecode(response.body) as Map; + final list = json["data"] as List?; + + final List txns = []; + for (final map in list!) { + txns.add(EthTxDTO.fromMap(Map.from(map as Map))); + } + return EthereumResponse( + txns, + null, + ); + } else { + throw EthApiException( + "getEthTransactions($address) response is empty but status code is " + "${response.statusCode}", + ); + } } else { - throw Exception( - 'ERROR GETTING TRANSACTIONS WITH STATUS ${response.statusCode}'); + throw EthApiException( + "getEthTransactions($address) failed with status code: " + "${response.statusCode}", + ); } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); } catch (e, s) { - throw Exception('ERROR GETTING TRANSACTIONS ${e.toString()}'); + Logging.instance.log( + "getEthTransactions(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); } } - static Future>> getTokenTransactions({ + static Future>> getTokenTransactions({ required String address, String? contractAddress, int? startBlock, @@ -170,9 +122,9 @@ abstract class EthereumAPI { if (json["message"] == "OK") { final result = List>.from(json["result"] as List); - final List tokenTxns = []; + final List tokenTxns = []; for (final map in result) { - tokenTxns.add(EthTokenTx.fromMap(map: map)); + tokenTxns.add(EthTokenTxDTO.fromMap(map: map)); } return EthereumResponse( From babbd75da3c4530a357bca746a863da398c67d33 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 Mar 2023 18:40:12 -0600 Subject: [PATCH 079/208] use database contract data and contract management updates --- lib/db/isar/main_db.dart | 16 ++ .../sub_classes/eth_token_entity.dart | 6 +- lib/models/ethereum/erc20_token.dart | 10 - lib/models/ethereum/erc721_token.dart | 10 - lib/models/ethereum/eth_token.dart | 62 ----- .../isar/models/ethereum/eth_contract.dart | 25 ++ .../isar/models/ethereum/eth_contract.g.dart | 254 ++++++++++++++++++ .../add_token_view/add_custom_token_view.dart | 5 +- .../add_token_view/add_token_view.dart | 45 ++-- .../sub_widgets/add_token_list_element.dart | 6 +- .../add_wallet_view/add_wallet_view.dart | 16 +- .../global_settings_view/hidden_settings.dart | 31 +-- lib/pages/token_view/my_tokens_view.dart | 59 ++-- .../sub_widgets/my_token_select_item.dart | 6 +- .../sub_widgets/my_tokens_list.dart | 6 +- lib/pages/token_view/token_view.dart | 4 +- .../coins/ethereum/ethereum_wallet.dart | 66 +++-- .../ethereum/cached_eth_token_balance.dart | 8 +- lib/services/ethereum/ethereum_api.dart | 25 +- .../ethereum/ethereum_token_service.dart | 18 +- .../mixins/eth_extras_wallet_cache.dart | 29 -- lib/services/mixins/eth_token_cache.dart | 12 +- lib/utilities/default_eth_tokens.dart | 41 +-- 23 files changed, 504 insertions(+), 256 deletions(-) delete mode 100644 lib/models/ethereum/erc20_token.dart delete mode 100644 lib/models/ethereum/erc721_token.dart delete mode 100644 lib/models/ethereum/eth_token.dart delete mode 100644 lib/services/mixins/eth_extras_wallet_cache.dart diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 2e5828eb6..9ada30578 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -358,4 +358,20 @@ class MainDB { throw MainDBException("failed addNewTransactionData", e); } } + + // ========== Ethereum ======================================================= + + // eth contracts + + QueryBuilder getEthContracts() => + isar.ethContracts.where(); + + Future putEthContract(EthContract contract) => isar.writeTxn(() async { + await isar.ethContracts.put(contract); + }); + + Future putEthContracts(List contracts) => + isar.writeTxn(() async { + await isar.ethContracts.putAll(contracts); + }); } diff --git a/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart b/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart index f38d80850..ccc0da239 100644 --- a/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart +++ b/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart @@ -1,11 +1,11 @@ import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; class EthTokenEntity extends AddWalletListEntity { EthTokenEntity(this.token); - final EthContractInfo token; + final EthContract token; @override Coin get coin => Coin.ethereum; @@ -17,5 +17,5 @@ class EthTokenEntity extends AddWalletListEntity { String get ticker => token.symbol; @override - List get props => [coin, name, ticker, token.contractAddress]; + List get props => [coin, name, ticker, token.address]; } diff --git a/lib/models/ethereum/erc20_token.dart b/lib/models/ethereum/erc20_token.dart deleted file mode 100644 index 04d443862..000000000 --- a/lib/models/ethereum/erc20_token.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:stackwallet/models/ethereum/eth_token.dart'; - -class Erc20ContractInfo extends EthContractInfo { - const Erc20ContractInfo({ - required super.contractAddress, - required super.name, - required super.symbol, - required super.decimals, - }); -} diff --git a/lib/models/ethereum/erc721_token.dart b/lib/models/ethereum/erc721_token.dart deleted file mode 100644 index 79fef8361..000000000 --- a/lib/models/ethereum/erc721_token.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:stackwallet/models/ethereum/eth_token.dart'; - -class Erc721ContractInfo extends EthContractInfo { - const Erc721ContractInfo({ - required super.contractAddress, - required super.name, - required super.symbol, - required super.decimals, - }); -} diff --git a/lib/models/ethereum/eth_token.dart b/lib/models/ethereum/eth_token.dart deleted file mode 100644 index a1bee7471..000000000 --- a/lib/models/ethereum/eth_token.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; -import 'package:stackwallet/models/ethereum/erc20_token.dart'; -import 'package:stackwallet/models/ethereum/erc721_token.dart'; - -abstract class EthContractInfo extends Equatable { - const EthContractInfo({ - required this.contractAddress, - required this.name, - required this.symbol, - required this.decimals, - }); - - final String contractAddress; - final String name; - final String symbol; - final int decimals; - - static EthContractInfo? fromMap(Map map) { - switch (map["runtimeType"]) { - case "Erc20ContractInfo": - return Erc20ContractInfo( - contractAddress: map["contractAddress"] as String, - name: map["name"] as String, - symbol: map["symbol"] as String, - decimals: map["decimals"] as int, - ); - case "Erc721ContractInfo": - return Erc721ContractInfo( - contractAddress: map["contractAddress"] as String, - name: map["name"] as String, - symbol: map["symbol"] as String, - decimals: map["decimals"] as int, - ); - default: - return null; - } - } - - static EthContractInfo? fromJson(String json) => fromMap( - Map.from( - jsonDecode(json) as Map, - ), - ); - - Map toMap() => { - "runtimeType": "$runtimeType", - "contractAddress": contractAddress, - "name": name, - "symbol": symbol, - "decimals": decimals, - }; - - String toJson() => jsonEncode(toMap()); - - @override - String toString() => toMap().toString(); - - @override - List get props => [contractAddress]; -} diff --git a/lib/models/isar/models/ethereum/eth_contract.dart b/lib/models/isar/models/ethereum/eth_contract.dart index ac1b07209..391c2364a 100644 --- a/lib/models/isar/models/ethereum/eth_contract.dart +++ b/lib/models/isar/models/ethereum/eth_contract.dart @@ -11,6 +11,7 @@ class EthContract extends Contract { required this.symbol, required this.decimals, required this.type, + required this.walletIds, this.abi, this.otherData, }); @@ -28,10 +29,34 @@ class EthContract extends Contract { late final String? abi; + late final List walletIds; + @enumerated late final EthContractType type; late final String? otherData; + + EthContract copyWith({ + Id? id, + String? address, + String? name, + String? symbol, + int? decimals, + EthContractType? type, + List? walletIds, + String? abi, + String? otherData, + }) => + EthContract( + address: address ?? this.address, + name: name ?? this.name, + symbol: symbol ?? this.symbol, + decimals: decimals ?? this.decimals, + type: type ?? this.type, + walletIds: walletIds ?? this.walletIds, + abi: abi ?? this.abi, + otherData: otherData ?? this.otherData, + )..id = id ?? this.id; } // Used in Isar db and stored there as int indexes so adding/removing values diff --git a/lib/models/isar/models/ethereum/eth_contract.g.dart b/lib/models/isar/models/ethereum/eth_contract.g.dart index d79052df2..d5bf8f0fe 100644 --- a/lib/models/isar/models/ethereum/eth_contract.g.dart +++ b/lib/models/isar/models/ethereum/eth_contract.g.dart @@ -52,6 +52,11 @@ const EthContractSchema = CollectionSchema( name: r'type', type: IsarType.byte, enumMap: _EthContracttypeEnumValueMap, + ), + r'walletIds': PropertySchema( + id: 7, + name: r'walletIds', + type: IsarType.stringList, ) }, estimateSize: _ethContractEstimateSize, @@ -103,6 +108,13 @@ int _ethContractEstimateSize( } } bytesCount += 3 + object.symbol.length * 3; + bytesCount += 3 + object.walletIds.length * 3; + { + for (var i = 0; i < object.walletIds.length; i++) { + final value = object.walletIds[i]; + bytesCount += value.length * 3; + } + } return bytesCount; } @@ -119,6 +131,7 @@ void _ethContractSerialize( writer.writeString(offsets[4], object.otherData); writer.writeString(offsets[5], object.symbol); writer.writeByte(offsets[6], object.type.index); + writer.writeStringList(offsets[7], object.walletIds); } EthContract _ethContractDeserialize( @@ -136,6 +149,7 @@ EthContract _ethContractDeserialize( symbol: reader.readString(offsets[5]), type: _EthContracttypeValueEnumMap[reader.readByteOrNull(offsets[6])] ?? EthContractType.erc20, + walletIds: reader.readStringList(offsets[7]) ?? [], ); object.id = id; return object; @@ -163,6 +177,8 @@ P _ethContractDeserializeProp

( case 6: return (_EthContracttypeValueEnumMap[reader.readByteOrNull(offset)] ?? EthContractType.erc20) as P; + case 7: + return (reader.readStringList(offset) ?? []) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -1230,6 +1246,231 @@ extension EthContractQueryFilter )); }); } + + QueryBuilder + walletIdsElementEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletIds', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdsElementGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'walletIds', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdsElementLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'walletIds', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdsElementBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'walletIds', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdsElementStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'walletIds', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdsElementEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'walletIds', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdsElementContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'walletIds', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdsElementMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'walletIds', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdsElementIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletIds', + value: '', + )); + }); + } + + QueryBuilder + walletIdsElementIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'walletIds', + value: '', + )); + }); + } + + QueryBuilder + walletIdsLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'walletIds', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + walletIdsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'walletIds', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + walletIdsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'walletIds', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + walletIdsLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'walletIds', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + walletIdsLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'walletIds', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + walletIdsLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'walletIds', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } } extension EthContractQueryObject @@ -1472,6 +1713,12 @@ extension EthContractQueryWhereDistinct return query.addDistinctBy(r'type'); }); } + + QueryBuilder distinctByWalletIds() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'walletIds'); + }); + } } extension EthContractQueryProperty @@ -1523,4 +1770,11 @@ extension EthContractQueryProperty return query.addPropertyName(r'type'); }); } + + QueryBuilder, QQueryOperations> + walletIdsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'walletIds'); + }); + } } diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart index 40201fbd0..395e8fa52 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -39,7 +39,7 @@ class _AddCustomTokenViewState extends ConsumerState { bool enableSubFields = false; bool addTokenButtonEnabled = false; - EthContractInfo? currentToken; + EthContract? currentToken; @override Widget build(BuildContext context) { @@ -96,6 +96,7 @@ class _AddCustomTokenViewState extends ConsumerState { nameController.text = currentToken!.name; symbolController.text = currentToken!.symbol; decimalsController.text = currentToken!.decimals.toString(); + currentToken!.walletIds.add(widget.walletId); } else { nameController.text = ""; symbolController.text = ""; diff --git a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart index 9b61a1cf4..2e4e3b03b 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; @@ -60,7 +62,7 @@ class _AddTokenViewState extends ConsumerState { (e) => e.token.name.toLowerCase().contains(lowercaseTerm) || e.token.symbol.toLowerCase().contains(lowercaseTerm) || - e.token.contractAddress.toLowerCase().contains(lowercaseTerm), + e.token.address.toLowerCase().contains(lowercaseTerm), ); } @@ -69,16 +71,16 @@ class _AddTokenViewState extends ConsumerState { Future onNextPressed() async { final selectedTokens = - tokenEntities.where((e) => e.selected).map((e) => e.token).toSet(); + tokenEntities.where((e) => e.selected).map((e) => e.token).toList(); final ethWallet = ref .read(walletsChangeNotifierProvider) .getManager(widget.walletId) .wallet as EthereumWallet; - await ethWallet.addTokenContract(selectedTokens); + await ethWallet.addTokenContracts(selectedTokens); if (mounted) { - Navigator.of(context).pop(); + Navigator.of(context).pop(42); } } @@ -87,15 +89,18 @@ class _AddTokenViewState extends ConsumerState { AddCustomTokenView.routeName, arguments: widget.walletId, ); - if (token is EthContractInfo) { - setState(() { - if (tokenEntities - .where((e) => e.token.contractAddress == token.contractAddress) - .isEmpty) { - tokenEntities.add(AddTokenListElementData(token)..selected = true); - tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); - } - }); + if (token is EthContract) { + await MainDB.instance.putEthContract(token); + if (mounted) { + setState(() { + if (tokenEntities + .where((e) => e.token.address == token.address) + .isEmpty) { + tokenEntities.add(AddTokenListElementData(token)..selected = true); + tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); + } + }); + } } } @@ -104,9 +109,15 @@ class _AddTokenViewState extends ConsumerState { _searchFieldController = TextEditingController(); _searchFocusNode = FocusNode(); - tokenEntities - .addAll(DefaultTokens.list.map((e) => AddTokenListElementData(e))); - tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); + final contracts = + MainDB.instance.getEthContracts().sortByName().findAllSync(); + + if (contracts.isEmpty) { + contracts.addAll(DefaultTokens.list); + MainDB.instance.putEthContracts(contracts); + } + + tokenEntities.addAll(contracts.map((e) => AddTokenListElementData(e))); super.initState(); } diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart index da78836aa..efe0c4b3a 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.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/utilities/assets.dart'; @@ -13,7 +13,7 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; class AddTokenListElementData { AddTokenListElementData(this.token); - final EthContractInfo token; + final EthContract token; bool selected = false; } @@ -34,7 +34,7 @@ class _AddTokenListElementState extends State { .exchangeNameEqualTo(ChangeNowExchange.exchangeName) .filter() .tokenContractEqualTo( - widget.data.token.contractAddress, + widget.data.token.address, caseSensitive: false, ) .and() diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 1047417a0..32d7b9c9c 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -3,9 +3,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_text.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart'; @@ -67,7 +70,7 @@ class _AddWalletViewState extends ConsumerState { e.name.toLowerCase().contains(lowercaseTerm) || e.coin.name.toLowerCase().contains(lowercaseTerm) || (e is EthTokenEntity && - e.token.contractAddress.toLowerCase().contains(lowercaseTerm)), + e.token.address.toLowerCase().contains(lowercaseTerm)), ); } @@ -92,8 +95,15 @@ class _AddWalletViewState extends ConsumerState { coinEntities.addAll(_coinsTestnet.map((e) => CoinEntity(e))); } - tokenEntities.addAll(DefaultTokens.list.map((e) => EthTokenEntity(e))); - tokenEntities.sort((a, b) => a.name.compareTo(b.name)); + final contracts = + MainDB.instance.getEthContracts().sortByName().findAllSync(); + + if (contracts.isEmpty) { + contracts.addAll(DefaultTokens.list); + MainDB.instance.putEthContracts(contracts); + } + + tokenEntities.addAll(contracts.map((e) => EthTokenEntity(e))); super.initState(); } diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 52a00ed61..3bc3e9a17 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -13,8 +12,6 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import '../../../models/ethereum/erc20_token.dart'; - class HiddenSettings extends StatelessWidget { const HiddenSettings({Key? key}) : super(key: key); @@ -192,20 +189,20 @@ class HiddenSettings extends StatelessWidget { Consumer(builder: (_, ref, __) { return GestureDetector( onTap: () async { - final erc20 = Erc20ContractInfo( - contractAddress: 'some con', - name: "loonamsn", - symbol: "DD", - decimals: 19, - ); - - final json = erc20.toJson(); - - print(json); - - final ee = EthContractInfo.fromJson(json); - - print(ee); + // final erc20 = Erc20ContractInfo( + // contractAddress: 'some con', + // name: "loonamsn", + // symbol: "DD", + // decimals: 19, + // ); + // + // final json = erc20.toJson(); + // + // print(json); + // + // final ee = EthContractInfo.fromJson(json); + // + // print(ee); }, child: RoundedWhiteContainer( child: Text( diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index 499bcb2f5..2801040fd 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -3,11 +3,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_tokens_list.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -38,6 +39,34 @@ class _TokenDetailsViewState extends ConsumerState { late final String walletAddress; late final TextEditingController _searchController; final searchFieldFocusNode = FocusNode(); + String _searchString = ""; + + List _filter(String searchTerm) { + if (searchTerm.isNotEmpty) { + final term = searchTerm.toLowerCase(); + return MainDB.instance + .getEthContracts() + .filter() + .walletIdsElementEqualTo(widget.walletId) + .and() + .group( + (q) => q + .nameContains(term, caseSensitive: false) + .or() + .symbolContains(term, caseSensitive: false) + .or() + .addressContains(term, caseSensitive: false), + ) + .findAllSync(); + // return tokens.toList(); + } + //implement search/filter + return MainDB.instance + .getEthContracts() + .filter() + .walletIdsElementEqualTo(widget.walletId) + .findAllSync(); + } @override void initState() { @@ -53,26 +82,10 @@ class _TokenDetailsViewState extends ConsumerState { super.dispose(); } - String _searchString = ""; - - List _filter( - Set tokens, String searchTerm) { - if (searchTerm.isEmpty) { - return tokens.toList(); - } - //implement search/filter - return tokens.toList(); - } - @override Widget build(BuildContext context) { final isDesktop = Util.isDesktop; - final tokens = (ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(widget.walletId).wallet)) - as EthereumWallet) - .contracts; - return MasterScaffold( background: Theme.of(context).extension()!.background, isDesktop: isDesktop, @@ -160,11 +173,15 @@ class _TokenDetailsViewState extends ConsumerState { width: 20, height: 20, ), - onPressed: () { - Navigator.of(context).pushNamed( + onPressed: () async { + final result = await Navigator.of(context).pushNamed( AddTokenView.routeName, arguments: widget.walletId, ); + + if (mounted && result == 42) { + setState(() {}); + } }, ), ), @@ -273,7 +290,7 @@ class _TokenDetailsViewState extends ConsumerState { Expanded( child: MyTokensList( walletId: widget.walletId, - tokens: _filter(tokens, _searchString), + tokens: _filter(_searchString), ), ), ], diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 508ab7e53..a95f7f71a 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -24,7 +24,7 @@ class MyTokenSelectItem extends ConsumerStatefulWidget { }) : super(key: key); final String walletId; - final EthContractInfo token; + final EthContract token; @override ConsumerState createState() => _MyTokenSelectItemState(); @@ -133,7 +133,7 @@ class _MyTokenSelectItemState extends ConsumerState { Text("${ref.watch( priceAnd24hChangeNotifierProvider.select( (value) => value - .getTokenPrice(widget.token.contractAddress) + .getTokenPrice(widget.token.address) .item1 .toStringAsFixed(2), ), diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart index 5de2490aa..473165944 100644 --- a/lib/pages/token_view/sub_widgets/my_tokens_list.dart +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart'; class MyTokensList extends StatelessWidget { @@ -11,7 +11,7 @@ class MyTokensList extends StatelessWidget { }) : super(key: key); final String walletId; - final List tokens; + final List tokens; @override Widget build(BuildContext context) { @@ -22,7 +22,7 @@ class MyTokensList extends StatelessWidget { itemBuilder: (ctx, index) { final token = tokens[index]; return Padding( - key: Key(token.contractAddress), + key: Key(token.address), padding: const EdgeInsets.all(4), child: MyTokenSelectItem( walletId: walletId, diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 2d5c41738..7747c961f 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -78,7 +78,7 @@ class _TokenViewState extends ConsumerState { SvgPicture.asset( Assets.svg.iconForToken( contractAddress: ref.watch(tokenServiceProvider - .select((value) => value!.token.contractAddress))), + .select((value) => value!.token.address))), width: 24, height: 24, ), @@ -118,7 +118,7 @@ class _TokenViewState extends ConsumerState { walletId: widget.walletId, initialSyncStatus: initialSyncStatus, tokenContractAddress: ref.watch(tokenServiceProvider - .select((value) => value!.token.contractAddress)), + .select((value) => value!.token.address)), ), ), ), diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index d7fb2cb48..e3c7028a0 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -7,7 +7,6 @@ import 'package:http/http.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; -import 'package:stackwallet/models/ethereum/eth_token.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'; @@ -18,7 +17,6 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha 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/eth_extras_wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; @@ -39,8 +37,7 @@ import 'package:web3dart/web3dart.dart' as web3; const int MINIMUM_CONFIRMATIONS = 3; -class EthereumWallet extends CoinServiceAPI - with WalletCache, WalletDB, EthExtrasWalletCache { +class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { EthereumWallet({ required String walletId, required String walletName, @@ -55,7 +52,6 @@ class EthereumWallet extends CoinServiceAPI _coin = coin; _secureStore = secureStore; initCache(walletId, coin); - initEthExtrasCache(walletId); initWalletDB(mockableOverride: mockableOverride); } @@ -66,29 +62,47 @@ class EthereumWallet extends CoinServiceAPI Timer? timer; Timer? _networkAliveTimer; - Set get contracts => getCachedTokenContracts(); + Future addTokenContracts(List contracts) async { + List updatedContracts = []; + for (final contract in contracts) { + final updatedWalletIds = contract.walletIds.toList(); + if (!updatedWalletIds.contains(walletId)) { + updatedWalletIds.add(walletId); + updatedContracts.add(contract.copyWith(walletIds: updatedWalletIds)); + } else { + updatedContracts.add(contract); + } + } + await db.putEthContracts(updatedContracts); - Future addTokenContract(Set contractInfo) => - updateCachedTokenContracts(contracts..addAll(contractInfo)).then( - (value) => GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "$contractInfo updated/added for: $walletId $walletName", - walletId, - ), - ), - ); + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "$contracts updated/added for: $walletId $walletName", + walletId, + ), + ); + } - Future removeTokenContract(String contractAddress) => - updateCachedTokenContracts(contracts - ..removeWhere((e) => e.contractAddress == contractAddress)) - .then( - (value) => GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "$contractAddress removed for: $walletId $walletName", - walletId, - ), - ), - ); + Future removeTokenContract(String contractAddress) async { + final contract = + await db.getEthContracts().addressEqualTo(contractAddress).findFirst(); + + if (contract == null) { + return; // todo some error? + } + + final updatedWalletIds = contract.walletIds.toList(); + updatedWalletIds.removeWhere((e) => e == contractAddress); + + await db.putEthContract(contract.copyWith(walletIds: updatedWalletIds)); + + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "$contractAddress removed for: $walletId $walletName", + walletId, + ), + ); + } @override String get walletId => _walletId; diff --git a/lib/services/ethereum/cached_eth_token_balance.dart b/lib/services/ethereum/cached_eth_token_balance.dart index 1867d57b5..d5295070e 100644 --- a/lib/services/ethereum/cached_eth_token_balance.dart +++ b/lib/services/ethereum/cached_eth_token_balance.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/token_balance.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.dart'; import 'package:stackwallet/services/mixins/eth_token_cache.dart'; @@ -6,7 +6,7 @@ import 'package:stackwallet/utilities/logger.dart'; class CachedEthTokenBalance with EthTokenCache { final String walletId; - final EthContractInfo token; + final EthContract token; CachedEthTokenBalance(this.walletId, this.token) { initCache(walletId, token); @@ -15,13 +15,13 @@ class CachedEthTokenBalance with EthTokenCache { Future fetchAndUpdateCachedBalance(String address) async { final response = await EthereumAPI.getWalletTokenBalance( address: address, - contractAddress: token.contractAddress, + contractAddress: token.address, ); if (response.value != null) { await updateCachedBalance( TokenBalance( - contractAddress: token.contractAddress, + contractAddress: token.address, decimalPlaces: token.decimals, total: response.value!, spendable: response.value!, diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index b73b2f1f4..e73ec5c3b 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -5,9 +5,7 @@ import 'package:decimal/decimal.dart'; import 'package:http/http.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; import 'package:stackwallet/dto/ethereum/eth_tx_dto.dart'; -import 'package:stackwallet/models/ethereum/erc20_token.dart'; -import 'package:stackwallet/models/ethereum/erc721_token.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; @@ -64,7 +62,10 @@ abstract class EthereumAPI { final List txns = []; for (final map in list!) { - txns.add(EthTxDTO.fromMap(Map.from(map as Map))); + final txn = EthTxDTO.fromMap(Map.from(map as Map)); + if (txn.hasToken == 0) { + txns.add(txn); + } } return EthereumResponse( txns, @@ -311,7 +312,7 @@ abstract class EthereumAPI { slow: feesSlow.toInt()); } - static Future> getTokenByContractAddress( + static Future> getTokenByContractAddress( String contractAddress) async { try { final response = await get(Uri.parse( @@ -322,20 +323,24 @@ abstract class EthereumAPI { final json = jsonDecode(response.body); if (json["message"] == "OK") { final map = Map.from(json["result"] as Map); - EthContractInfo? token; + EthContract? token; if (map["type"] == "ERC-20") { - token = Erc20ContractInfo( - contractAddress: map["contractAddress"] as String, + token = EthContract( + address: map["contractAddress"] as String, decimals: int.parse(map["decimals"] as String), name: map["name"] as String, symbol: map["symbol"] as String, + type: EthContractType.erc20, + walletIds: [], ); } else if (map["type"] == "ERC-721") { - token = Erc721ContractInfo( - contractAddress: map["contractAddress"] as String, + token = EthContract( + address: map["contractAddress"] as String, decimals: int.parse(map["decimals"] as String), name: map["name"] as String, symbol: map["symbol"] as String, + type: EthContractType.erc721, + walletIds: [], ); } else { throw EthApiException( diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 104d379ce..1069e964b 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -5,9 +5,7 @@ import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/transaction.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/token_balance.dart'; @@ -30,7 +28,7 @@ import 'package:tuple/tuple.dart'; import 'package:web3dart/web3dart.dart' as web3dart; class EthereumTokenService extends ChangeNotifier with EthTokenCache { - final EthContractInfo token; + final EthContract token; final EthereumWallet ethWallet; final TransactionNotificationTracker tracker; final SecureStorageInterface _secureStore; @@ -51,7 +49,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { required SecureStorageInterface secureStore, required this.tracker, }) : _secureStore = secureStore { - _contractAddress = web3dart.EthereumAddress.fromHex(token.contractAddress); + _contractAddress = web3dart.EthereumAddress.fromHex(token.address); initCache(ethWallet.walletId, token); } @@ -220,7 +218,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.syncing, - ethWallet.walletId + token.contractAddress, + ethWallet.walletId + token.address, coin, ), ); @@ -237,7 +235,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.synced, - ethWallet.walletId + token.contractAddress, + ethWallet.walletId + token.address, coin, ), ); @@ -256,7 +254,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { String _balance = balanceRequest.first.toString(); final newBalance = TokenBalance( - contractAddress: token.contractAddress, + contractAddress: token.address, total: int.parse(_balance), spendable: int.parse(_balance), blockedTotal: 0, @@ -270,7 +268,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { Future> get transactions => ethWallet.db .getTransactions(ethWallet.walletId) .filter() - .otherDataEqualTo(token.contractAddress) + .otherDataEqualTo(token.address) .sortByTimestampDesc() .findAll(); @@ -279,7 +277,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { final response = await EthereumAPI.getTokenTransactions( address: addressString, - contractAddress: token.contractAddress, + contractAddress: token.address, ); if (response.value == null) { diff --git a/lib/services/mixins/eth_extras_wallet_cache.dart b/lib/services/mixins/eth_extras_wallet_cache.dart deleted file mode 100644 index b7184a916..000000000 --- a/lib/services/mixins/eth_extras_wallet_cache.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:stackwallet/db/hive/db.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; - -mixin EthExtrasWalletCache { - late final String _walletId; - - void initEthExtrasCache(String walletId) { - _walletId = walletId; - } - - // cached list of user added token contracts - Set getCachedTokenContracts() { - final list = DB.instance.get( - boxName: _walletId, - key: "ethTokenContracts", - ) as List? ?? - []; - return list.map((e) => EthContractInfo.fromJson(e)!).toSet(); - } - - Future updateCachedTokenContracts( - Set contracts) async { - await DB.instance.put( - boxName: _walletId, - key: "ethTokenContracts", - value: contracts.map((e) => e.toJson()).toList(), - ); - } -} diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart index 63501e501..87ec3c189 100644 --- a/lib/services/mixins/eth_token_cache.dart +++ b/lib/services/mixins/eth_token_cache.dart @@ -1,5 +1,5 @@ import 'package:stackwallet/db/hive/db.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/token_balance.dart'; abstract class _Keys { @@ -10,9 +10,9 @@ abstract class _Keys { mixin EthTokenCache { late final String _walletId; - late final EthContractInfo _token; + late final EthContract _token; - void initCache(String walletId, EthContractInfo token) { + void initCache(String walletId, EthContract token) { _walletId = walletId; _token = token; } @@ -21,11 +21,11 @@ mixin EthTokenCache { TokenBalance getCachedBalance() { final jsonString = DB.instance.get( boxName: _walletId, - key: _Keys.tokenBalance(_token.contractAddress), + key: _Keys.tokenBalance(_token.address), ) as String?; if (jsonString == null) { return TokenBalance( - contractAddress: _token.contractAddress, + contractAddress: _token.address, decimalPlaces: _token.decimals, total: 0, spendable: 0, @@ -41,7 +41,7 @@ mixin EthTokenCache { Future updateCachedBalance(TokenBalance balance) async { await DB.instance.put( boxName: _walletId, - key: _Keys.tokenBalance(_token.contractAddress), + key: _Keys.tokenBalance(_token.address), value: balance.toJsonIgnoreCoin(), ); } diff --git a/lib/utilities/default_eth_tokens.dart b/lib/utilities/default_eth_tokens.dart index b5bcde9ba..11363752d 100644 --- a/lib/utilities/default_eth_tokens.dart +++ b/lib/utilities/default_eth_tokens.dart @@ -1,43 +1,54 @@ -import 'package:stackwallet/models/ethereum/erc20_token.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; abstract class DefaultTokens { - static const List list = [ - Erc20ContractInfo( - contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + static List list = [ + EthContract( + address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", name: "USD Coin", symbol: "USDC", decimals: 6, + type: EthContractType.erc20, + walletIds: [], ), - Erc20ContractInfo( - contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", + EthContract( + address: "0xdac17f958d2ee523a2206206994597c13d831ec7", name: "Tether", symbol: "USDT", decimals: 6, + type: EthContractType.erc20, + walletIds: [], ), - Erc20ContractInfo( - contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + EthContract( + address: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", name: "Shiba Inu", symbol: "SHIB", decimals: 18, + type: EthContractType.erc20, + walletIds: [], ), - Erc20ContractInfo( - contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + EthContract( + address: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", name: "BNB Token", symbol: "BNB", decimals: 18, + type: EthContractType.erc20, + walletIds: [], ), - Erc20ContractInfo( - contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", + EthContract( + address: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", name: "BUSD", symbol: "BUSD", decimals: 18, + type: EthContractType.erc20, + walletIds: [], ), - Erc20ContractInfo( - contractAddress: "0x514910771af9ca656af840dff83e8264ecf986ca", + EthContract( + address: "0x514910771af9ca656af840dff83e8264ecf986ca", name: "Chainlink", symbol: "LINK", decimals: 18, + type: EthContractType.erc20, + walletIds: [], ), ]; } From f26fb1945378821e98c7e516df486a64402becec Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 Mar 2023 08:35:43 -0600 Subject: [PATCH 080/208] do not use secure storage for token ABIs --- lib/db/isar/main_db.dart | 4 +- .../token_view/sub_widgets/token_summary.dart | 2 +- lib/pages/token_view/token_view.dart | 8 +- lib/services/ethereum/ethereum_api.dart | 1 + .../ethereum/ethereum_token_service.dart | 129 ++++++++++-------- 5 files changed, 77 insertions(+), 67 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 9ada30578..c9577f3e3 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -366,8 +366,8 @@ class MainDB { QueryBuilder getEthContracts() => isar.ethContracts.where(); - Future putEthContract(EthContract contract) => isar.writeTxn(() async { - await isar.ethContracts.put(contract); + Future putEthContract(EthContract contract) => isar.writeTxn(() async { + return await isar.ethContracts.put(contract); }); Future putEthContracts(List contracts) => diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index e080c7d67..51d8fb91c 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -20,7 +20,7 @@ class TokenSummary extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final token = - ref.watch(tokenServiceProvider.select((value) => value!.token)); + ref.watch(tokenServiceProvider.select((value) => value!.tokenContract)); final balance = ref.watch(tokenServiceProvider.select((value) => value!.balance)); diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 7747c961f..0126d913c 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -78,7 +78,7 @@ class _TokenViewState extends ConsumerState { SvgPicture.asset( Assets.svg.iconForToken( contractAddress: ref.watch(tokenServiceProvider - .select((value) => value!.token.address))), + .select((value) => value!.tokenContract.address))), width: 24, height: 24, ), @@ -86,8 +86,8 @@ class _TokenViewState extends ConsumerState { width: 10, ), Text( - ref.watch( - tokenServiceProvider.select((value) => value!.token.name)), + ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.name)), style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), @@ -118,7 +118,7 @@ class _TokenViewState extends ConsumerState { walletId: widget.walletId, initialSyncStatus: initialSyncStatus, tokenContractAddress: ref.watch(tokenServiceProvider - .select((value) => value!.token.address)), + .select((value) => value!.tokenContract.address)), ), ), ), diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index e73ec5c3b..03f003ffe 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -425,6 +425,7 @@ abstract class EthereumAPI { } } + /// Fetch the underlying contract address that a proxy contract points to static Future> getProxyTokenImplementation( String contractAddress) async { try { diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 1069e964b..fc26a9466 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -5,6 +5,7 @@ import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.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'; @@ -28,31 +29,33 @@ import 'package:tuple/tuple.dart'; import 'package:web3dart/web3dart.dart' as web3dart; class EthereumTokenService extends ChangeNotifier with EthTokenCache { - final EthContract token; final EthereumWallet ethWallet; final TransactionNotificationTracker tracker; final SecureStorageInterface _secureStore; - late web3dart.EthereumAddress _contractAddress; + // late web3dart.EthereumAddress _contractAddress; late web3dart.EthPrivateKey _credentials; - late web3dart.DeployedContract _contract; + late web3dart.DeployedContract _deployedContract; late web3dart.ContractFunction _balanceFunction; late web3dart.ContractFunction _sendFunction; - late String _tokenAbi; late web3dart.Web3Client _client; static const _gasLimit = 200000; EthereumTokenService({ - required this.token, + required EthContract token, required this.ethWallet, required SecureStorageInterface secureStore, required this.tracker, - }) : _secureStore = secureStore { - _contractAddress = web3dart.EthereumAddress.fromHex(token.address); + }) : _secureStore = secureStore, + _tokenContract = token { + // _contractAddress = web3dart.EthereumAddress.fromHex(token.address); initCache(ethWallet.walletId, token); } + EthContract get tokenContract => _tokenContract; + EthContract _tokenContract; + TokenBalance get balance => _balance ??= getCachedBalance(); TokenBalance? _balance; @@ -63,12 +66,12 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { final decimalAmount = Format.satoshisToAmount(amount as int, coin: Coin.ethereum); final bigIntAmount = - amountToBigInt(decimalAmount.toDouble(), token.decimals); + amountToBigInt(decimalAmount.toDouble(), tokenContract.decimals); final sentTx = await _client.sendTransaction( _credentials, web3dart.Transaction.callContract( - contract: _contract, + contract: _deployedContract, function: _sendFunction, parameters: [ web3dart.EthereumAddress.fromHex(txData['address'] as String), @@ -96,7 +99,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { .findFirst(); Future estimateFeeFor(int satoshiAmount, int feeRate) async { - final fee = estimateFee(feeRate, _gasLimit, token.decimals); + final fee = estimateFee(feeRate, _gasLimit, tokenContract.decimals); return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin); } @@ -107,69 +110,75 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { return await EthereumAPI.getFees(); } - Future initialize() async { - final storedABI = - await _secureStore.read(key: '${_contractAddress.toString()}_tokenAbi'); - - if (storedABI == null) { - final abiResponse = await EthereumAPI.getTokenAbi(_contractAddress.hex); - //Fetch token ABI so we can call token functions - if (abiResponse.value != null) { - _tokenAbi = abiResponse.value!; - //Store abi in secure store - await _secureStore.write( - key: '${_contractAddress.hex}_tokenAbi', - value: _tokenAbi, - ); - } else { - throw abiResponse.exception!; - } + Future _updateTokenABI({ + required EthContract forContract, + required String usingContractAddress, + }) async { + final abiResponse = await EthereumAPI.getTokenAbi(usingContractAddress); + // Fetch token ABI so we can call token functions + if (abiResponse.value != null) { + final updatedToken = forContract.copyWith(abi: abiResponse.value!); + // Store updated contract + final id = await MainDB.instance.putEthContract(updatedToken); + return updatedToken..id = id; } else { - _tokenAbi = storedABI; + throw abiResponse.exception!; + } + } + + Future initialize() async { + final contractAddress = + web3dart.EthereumAddress.fromHex(tokenContract.address); + + if (tokenContract.abi == null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddress.hex, + ); } String? mnemonicString = await ethWallet.mnemonicString; //Get private key for given mnemonic String privateKey = getPrivateKey( - mnemonicString!, (await ethWallet.mnemonicPassphrase) ?? ""); + mnemonicString!, + (await ethWallet.mnemonicPassphrase) ?? "", + ); _credentials = web3dart.EthPrivateKey.fromHex(privateKey); - _contract = web3dart.DeployedContract( - web3dart.ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress); + _deployedContract = web3dart.DeployedContract( + web3dart.ContractAbi.fromJson(tokenContract.abi!, tokenContract.name), + contractAddress, + ); try { - _balanceFunction = _contract.function('balanceOf'); - _sendFunction = _contract.function('transfer'); + _balanceFunction = _deployedContract.function('balanceOf'); + _sendFunction = _deployedContract.function('transfer'); } catch (_) { // function not found so likely a proxy so we need to fetch the impl - final response = - await EthereumAPI.getProxyTokenImplementation(_contractAddress.hex); + final contractAddressResponse = + await EthereumAPI.getProxyTokenImplementation(contractAddress.hex); - if (response.value != null) { - final abiResponse = await EthereumAPI.getTokenAbi(response.value!); - if (abiResponse.value != null) { - _tokenAbi = abiResponse.value!; - await _secureStore.write( - key: '${_contractAddress.hex}_tokenAbi', value: _tokenAbi); - } else { - throw abiResponse.exception!; - } + if (contractAddressResponse.value != null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddressResponse.value!, + ); } else { - throw response.exception!; + throw contractAddressResponse.exception!; } } - _contract = web3dart.DeployedContract( + _deployedContract = web3dart.DeployedContract( web3dart.ContractAbi.fromJson( - _tokenAbi, - token.name, + tokenContract.abi!, + tokenContract.name, ), - _contractAddress, + contractAddress, ); - _balanceFunction = _contract.function('balanceOf'); - _sendFunction = _contract.function('transfer'); + _balanceFunction = _deployedContract.function('balanceOf'); + _sendFunction = _deployedContract.function('transfer'); _client = await getEthClient(); @@ -218,7 +227,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.syncing, - ethWallet.walletId + token.address, + ethWallet.walletId + tokenContract.address, coin, ), ); @@ -227,7 +236,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { await _refreshTransactions(); } catch (e, s) { Logging.instance.log( - "Caught exception in ${token.name} ${ethWallet.walletName} ${ethWallet.walletId} refresh(): $e\n$s", + "Caught exception in ${tokenContract.name} ${ethWallet.walletName} ${ethWallet.walletId} refresh(): $e\n$s", level: LogLevel.Warning, ); } finally { @@ -235,7 +244,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { GlobalEventBus.instance.fire( WalletSyncStatusChangedEvent( WalletSyncStatus.synced, - ethWallet.walletId + token.address, + ethWallet.walletId + tokenContract.address, coin, ), ); @@ -246,7 +255,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { Future refreshCachedBalance() async { final balanceRequest = await _client.call( - contract: _contract, + contract: _deployedContract, function: _balanceFunction, params: [_credentials.address], ); @@ -254,12 +263,12 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { String _balance = balanceRequest.first.toString(); final newBalance = TokenBalance( - contractAddress: token.address, + contractAddress: tokenContract.address, total: int.parse(_balance), spendable: int.parse(_balance), blockedTotal: 0, pendingSpendable: 0, - decimalPlaces: token.decimals, + decimalPlaces: tokenContract.decimals, ); await updateCachedBalance(newBalance); notifyListeners(); @@ -268,7 +277,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { Future> get transactions => ethWallet.db .getTransactions(ethWallet.walletId) .filter() - .otherDataEqualTo(token.address) + .otherDataEqualTo(tokenContract.address) .sortByTimestampDesc() .findAll(); @@ -277,7 +286,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { final response = await EthereumAPI.getTokenTransactions( address: addressString, - contractAddress: token.address, + contractAddress: tokenContract.address, ); if (response.value == null) { @@ -358,7 +367,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { if (txnsData.isNotEmpty) { GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( - "${token.name} transactions updated/added for: ${ethWallet.walletId} ${ethWallet.walletName}", + "${tokenContract.name} transactions updated/added for: ${ethWallet.walletId} ${ethWallet.walletName}", ethWallet.walletId, ), ); From 5ba44d5b8e56ed6faf99e38c7bec65069fb425c5 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 Mar 2023 08:36:56 -0600 Subject: [PATCH 081/208] rename eth token wallet class --- lib/pages/token_view/sub_widgets/my_token_select_item.dart | 3 +-- lib/pages/token_view/token_view.dart | 5 ++--- lib/services/ethereum/ethereum_token_service.dart | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index a95f7f71a..d918adb88 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -64,8 +64,7 @@ class _MyTokenSelectItemState extends ConsumerState { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () async { - ref.read(tokenServiceStateProvider.state).state = - EthereumTokenService( + ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( token: widget.token, secureStore: ref.read(secureStoreProvider), ethWallet: ref diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 0126d913c..8552e8ba3 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -19,9 +19,8 @@ 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/rounded_container.dart'; -final tokenServiceStateProvider = - StateProvider((ref) => null); -final tokenServiceProvider = ChangeNotifierProvider( +final tokenServiceStateProvider = StateProvider((ref) => null); +final tokenServiceProvider = ChangeNotifierProvider( (ref) => ref.watch(tokenServiceStateProvider)); /// [eventBus] should only be set during testing diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index fc26a9466..c59ce2657 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -28,7 +28,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:tuple/tuple.dart'; import 'package:web3dart/web3dart.dart' as web3dart; -class EthereumTokenService extends ChangeNotifier with EthTokenCache { +class EthTokenWallet extends ChangeNotifier with EthTokenCache { final EthereumWallet ethWallet; final TransactionNotificationTracker tracker; final SecureStorageInterface _secureStore; @@ -42,7 +42,7 @@ class EthereumTokenService extends ChangeNotifier with EthTokenCache { static const _gasLimit = 200000; - EthereumTokenService({ + EthTokenWallet({ required EthContract token, required this.ethWallet, required SecureStorageInterface secureStore, From 1814fb3752e1bd420af998ff696a9d4bc2b2e866 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 Mar 2023 10:50:17 -0600 Subject: [PATCH 082/208] logic fix --- lib/pages/receive_view/receive_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index b9cc227ab..073b2dfb8 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -288,11 +288,11 @@ class _ReceiveViewState extends ConsumerState { ), ), ), - if (coin != Coin.epicCash || coin != Coin.ethereum) + if (coin != Coin.epicCash && coin != Coin.ethereum) const SizedBox( height: 12, ), - if (coin != Coin.epicCash || coin != Coin.ethereum) + if (coin != Coin.epicCash && coin != Coin.ethereum) TextButton( onPressed: generateNewAddress, style: Theme.of(context) From 23d38ef4d9ff9a73e759bb8769060f16c6a6f6d5 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 Mar 2023 11:35:43 -0600 Subject: [PATCH 083/208] added some utility extensions --- lib/utilities/extensions/extensions.dart | 3 ++ lib/utilities/extensions/impl/big_int.dart | 33 +++++++++++++++++ lib/utilities/extensions/impl/string.dart | 17 +++++++++ lib/utilities/extensions/impl/uint8_list.dart | 36 +++++++++++++++++++ pubspec.lock | 6 ++-- pubspec.yaml | 3 ++ 6 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 lib/utilities/extensions/extensions.dart create mode 100644 lib/utilities/extensions/impl/big_int.dart create mode 100644 lib/utilities/extensions/impl/string.dart create mode 100644 lib/utilities/extensions/impl/uint8_list.dart diff --git a/lib/utilities/extensions/extensions.dart b/lib/utilities/extensions/extensions.dart new file mode 100644 index 000000000..792ebe0b3 --- /dev/null +++ b/lib/utilities/extensions/extensions.dart @@ -0,0 +1,3 @@ +export 'impl/big_int.dart'; +export 'impl/string.dart'; +export 'impl/uint8_list.dart'; diff --git a/lib/utilities/extensions/impl/big_int.dart b/lib/utilities/extensions/impl/big_int.dart new file mode 100644 index 000000000..c9b78ab55 --- /dev/null +++ b/lib/utilities/extensions/impl/big_int.dart @@ -0,0 +1,33 @@ +import 'dart:typed_data'; + +extension BigIntExtensions on BigInt { + String get toHex { + if (this < BigInt.zero) { + throw Exception("BigInt value is negative"); + } + + final String hex = toRadixString(16); + if (hex.length % 2 == 0) { + return hex; + } else { + return "0$hex"; + } + } + + String get toHexUppercase => toHex.toUpperCase(); + + Uint8List get toBytes { + if (this < BigInt.zero) { + throw Exception("BigInt value is negative"); + } + BigInt number = this; + int bytes = (number.bitLength + 7) >> 3; + final b256 = BigInt.from(256); + final result = Uint8List(bytes); + for (int i = 0; i < bytes; i++) { + result[bytes - 1 - i] = number.remainder(b256).toInt(); + number = number >> 8; + } + return result; + } +} diff --git a/lib/utilities/extensions/impl/string.dart b/lib/utilities/extensions/impl/string.dart new file mode 100644 index 000000000..4e996e05b --- /dev/null +++ b/lib/utilities/extensions/impl/string.dart @@ -0,0 +1,17 @@ +import 'dart:typed_data'; + +import 'package:dart_bs58/dart_bs58.dart'; +import 'package:dart_bs58check/dart_bs58check.dart'; +import 'package:hex/hex.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; + +extension StringExtensions on String { + Uint8List get toUint8ListFromHex => + Uint8List.fromList(HEX.decode(startsWith("0x") ? substring(2) : this)); + + Uint8List get toUint8ListFromBase58Encoded => bs58.decode(this); + + Uint8List get toUint8ListFromBase58CheckEncoded => bs58check.decode(this); + + BigInt get toBigIntFromHex => toUint8ListFromHex.toBigInt; +} diff --git a/lib/utilities/extensions/impl/uint8_list.dart b/lib/utilities/extensions/impl/uint8_list.dart new file mode 100644 index 000000000..5dcf0b435 --- /dev/null +++ b/lib/utilities/extensions/impl/uint8_list.dart @@ -0,0 +1,36 @@ +import 'dart:typed_data'; + +import 'package:dart_bs58/dart_bs58.dart'; +import 'package:dart_bs58check/dart_bs58check.dart'; +import 'package:hex/hex.dart'; + +extension Uint8ListExtensions on Uint8List { + String get toHex { + return HEX.encode(this); + } + + String get toBase58Encoded { + return bs58.encode(this); + } + + String get toBase58CheckEncoded { + return bs58check.encode(this); + } + + /// returns copy of byte list in reverse order + Uint8List get reversed { + final reversed = Uint8List(length); + for (final byte in this) { + reversed.insert(0, byte); + } + return reversed; + } + + BigInt get toBigInt { + BigInt number = BigInt.zero; + for (final byte in this) { + number = (number << 8) | BigInt.from(byte & 0xff); + } + return number; + } +} diff --git a/pubspec.lock b/pubspec.lock index 0f83b1eca..a7948d64a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -355,14 +355,14 @@ packages: source: hosted version: "1.0.0" dart_bs58: - dependency: transitive + dependency: "direct main" description: name: dart_bs58 url: "https://pub.dartlang.org" source: hosted version: "1.0.1" dart_bs58check: - dependency: transitive + dependency: "direct main" description: name: dart_bs58check url: "https://pub.dartlang.org" @@ -716,7 +716,7 @@ packages: source: hosted version: "2.2.0" hex: - dependency: transitive + dependency: "direct main" description: name: hex url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index f102ebf55..b0b362827 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -146,6 +146,9 @@ dependencies: dropdown_button2: 1.7.2 string_validator: ^0.3.0 equatable: ^2.0.5 + dart_bs58: ^1.0.1 + dart_bs58check: ^3.0.2 + hex: ^0.2.0 dev_dependencies: flutter_test: From b617b3e5e01454a44a6ecaa5ff591c067395b48f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 Mar 2023 11:36:28 -0600 Subject: [PATCH 084/208] added more logging --- lib/services/coins/ethereum/ethereum_wallet.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index e3c7028a0..523148c4c 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -601,6 +601,11 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { break; } } + } else { + Logging.instance.log( + " refreshIfThereIsNewData get eth transactions failed: ${response.exception}", + level: LogLevel.Error, + ); } } return needsRefresh; From 22b7da2206432f9593dcb2ca2cc0619437d15e21 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 3 Mar 2023 11:36:40 -0600 Subject: [PATCH 085/208] WIP eth tx parsing --- lib/services/ethereum/ethereum_api.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 03f003ffe..41d37ba0c 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; class EthApiException with Exception { @@ -55,6 +56,9 @@ abstract class EthereumAPI { // "$etherscanApi?module=account&action=txlist&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + final BigInt myReceivingAddressInt = address.toBigIntFromHex; + + // print(response.body); if (response.statusCode == 200) { if (response.body.isNotEmpty) { final json = jsonDecode(response.body) as Map; @@ -63,6 +67,24 @@ abstract class EthereumAPI { final List txns = []; for (final map in list!) { final txn = EthTxDTO.fromMap(Map.from(map as Map)); + + final logs = map["receipt"]["logs"] as List; + + for (final log in logs) { + final map = log as Map; + + final contractAddress = map["address"] as String; + final topics = List.from(map["topics"] as List); + + for (int i = 0; i < topics.length; i++) { + if (topics[i].toBigIntFromHex == myReceivingAddressInt) { + print("================================================"); + print("Contract: $contractAddress"); + Logger.print((log).toString(), normalLength: false); + } + } + } + if (txn.hasToken == 0) { txns.add(txn); } From d97a994c54c15173b28890175c1765099ec1037c Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Sun, 5 Mar 2023 12:35:26 -0600 Subject: [PATCH 086/208] desktop sizing fixes --- .../add_wallet_view/add_wallet_view.dart | 6 ++++ .../sub_widgets/expanding_sub_list_item.dart | 6 ++-- lib/pages_desktop_specific/desktop_menu.dart | 29 ++++++++++--------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 32d7b9c9c..a3ca0aef8 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -217,6 +217,9 @@ class _AddWalletViewState extends ConsumerState { ), ), ), + const SizedBox( + height: 8, + ), Expanded( child: SingleChildScrollView( child: Column( @@ -235,6 +238,9 @@ class _AddWalletViewState extends ConsumerState { ), ), ), + const SizedBox( + height: 20, + ), ], ), ), diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart index f2cb410d3..d3a4b543a 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart @@ -84,7 +84,7 @@ class _ExpandingSubListItemState extends State { Text( widget.title, style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + ? STextStyles.desktopTextMedium(context).copyWith( color: Theme.of(context) .extension()! .textDark3, @@ -95,8 +95,8 @@ class _ExpandingSubListItemState extends State { RotateIcon( icon: SvgPicture.asset( Assets.svg.chevronDown, - width: 12, - height: 6, + width: isDesktop ? 20 : 12, + height: isDesktop ? 10 : 6, color: Theme.of(context) .extension()! .textFieldActiveSearchIconRight, diff --git a/lib/pages_desktop_specific/desktop_menu.dart b/lib/pages_desktop_specific/desktop_menu.dart index b705a33be..d8ad67720 100644 --- a/lib/pages_desktop_specific/desktop_menu.dart +++ b/lib/pages_desktop_specific/desktop_menu.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -225,19 +227,20 @@ class _DesktopMenuState extends ConsumerState { controller: controllers[7], ), const Spacer(), - DesktopMenuItem( - duration: duration, - labelLength: 123, - icon: const DesktopExitIcon(), - label: "Exit", - value: 7, - onChanged: (_) { - // todo: save stuff/ notify before exit? - // exit(0); - SystemNavigator.pop(); - }, - controller: controllers[8], - ), + if (!Platform.isIOS) + DesktopMenuItem( + duration: duration, + labelLength: 123, + icon: const DesktopExitIcon(), + label: "Exit", + value: 7, + onChanged: (_) { + // todo: save stuff/ notify before exit? + // exit(0); + SystemNavigator.pop(); + }, + controller: controllers[8], + ), ], ), ), From 55bac49481673d3877ffdd5217ffe991052aa4fa Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Sun, 5 Mar 2023 12:36:38 -0600 Subject: [PATCH 087/208] build files --- ios/Podfile.lock | 36 +- ios/Runner.xcodeproj/project.pbxproj | 134 ++--- pubspec.lock | 711 ++++++++++++++++++--------- 3 files changed, 557 insertions(+), 324 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bb92ea24c..8cca5a81c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -61,6 +61,8 @@ PODS: - cw_wownero/Wownero (0.0.2): - cw_shared_external - Flutter + - device_info_plus (0.0.1): + - Flutter - devicelocale (0.0.1): - Flutter - DKImagePickerController/Core (4.3.4): @@ -119,8 +121,9 @@ PODS: - MTBBarcodeScanner (5.0.11) - package_info_plus (0.4.5): - Flutter - - path_provider_ios (0.0.1): + - path_provider_foundation (0.0.1): - Flutter + - FlutterMacOS - permission_handler_apple (9.0.4): - Flutter - ReachabilitySwift (5.0.0) @@ -129,8 +132,9 @@ PODS: - SDWebImage/Core (5.13.2) - share_plus (0.0.1): - Flutter - - shared_preferences_ios (0.0.1): + - shared_preferences_foundation (0.0.1): - Flutter + - FlutterMacOS - stack_wallet_backup (0.0.1): - Flutter - SwiftProtobuf (1.19.0) @@ -147,6 +151,7 @@ DEPENDENCIES: - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - cw_wownero (from `.symlinks/plugins/cw_wownero/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) @@ -160,10 +165,10 @@ DEPENDENCIES: - lelantus (from `.symlinks/plugins/lelantus/ios`) - local_auth (from `.symlinks/plugins/local_auth/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) - stack_wallet_backup (from `.symlinks/plugins/stack_wallet_backup/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) @@ -191,6 +196,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cw_shared_external/ios" cw_wownero: :path: ".symlinks/plugins/cw_wownero/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" devicelocale: :path: ".symlinks/plugins/devicelocale/ios" file_picker: @@ -217,14 +224,14 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/local_auth/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" - shared_preferences_ios: - :path: ".symlinks/plugins/shared_preferences_ios/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/ios" stack_wallet_backup: :path: ".symlinks/plugins/stack_wallet_backup/ios" url_launcher_ios: @@ -239,10 +246,11 @@ SPEC CHECKSUMS: cw_monero: 9816991daff0e3ad0a8be140e31933b5526babd4 cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 cw_wownero: 08e5713fe311a3be95efd7f3c1bf9d47d9cfafde + device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_libepiccash: 36241aa7d3126f6521529985ccb3dc5eaf7bb317 flutter_libmonero: da68a616b73dd0374a8419c684fa6b6df2c44ffe @@ -250,23 +258,23 @@ SPEC CHECKSUMS: flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 - isar_flutter_libs: bfb66f35a1fa9db9ec96b93539a03329ce147738 + isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 lelantus: 97ab4ecc648423278f807e499b3a717dea9268f8 local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 - shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca stack_wallet_backup: 5b8563aba5d8ffbf2ce1944331ff7294a0ec7c03 SwiftProtobuf: 6ef3f0e422ef90d6605ca20b21a94f6c1324d6b3 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993 wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f -PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea +PODFILE CHECKSUM: 57c8aed26fba39d3ec9424816221f294a07c58eb COCOAPODS: 1.11.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 17990457d..0279825a9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - C64D72F92051288D5CB5033D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08E84808BC00DEE3447AF47E /* Pods_Runner.framework */; }; + B49D91439948369648AB0603 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51604430FD0FD1FA5C4767A0 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -35,11 +35,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 08E84808BC00DEE3447AF47E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 15168938F13F6113519C963B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1B82E12F9C5D326CBB2ADF7E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 689E9A74C0452C94E3479BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 51604430FD0FD1FA5C4767A0 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -53,7 +54,6 @@ 7E8A4F15288D645200F18717 /* flutter_libepiccash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = flutter_libepiccash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7E8A4F19288D721300F18717 /* flutter_libepiccash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = flutter_libepiccash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7E8A4F1D288D72D100F18717 /* flutter_libepiccash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = flutter_libepiccash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7F0C23A93667326FB8E95604 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -61,7 +61,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F69E2FD9CB433963DAA9B09E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + E6F536731AC506735EB76340 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,7 +72,7 @@ 7E1603B5288D73EA002F7A6F /* libepic_cash_wallet.a in Frameworks */, 7E729AE82893C1B1009BBD65 /* flutter_libepiccash.framework in Frameworks */, 7E569F992798D47200056D51 /* mobileliblelantus.framework in Frameworks */, - C64D72F92051288D5CB5033D /* Pods_Runner.framework in Frameworks */, + B49D91439948369648AB0603 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -82,9 +82,9 @@ 4D9BC5822E8E05B80CC958A0 /* Pods */ = { isa = PBXGroup; children = ( - 7F0C23A93667326FB8E95604 /* Pods-Runner.debug.xcconfig */, - F69E2FD9CB433963DAA9B09E /* Pods-Runner.release.xcconfig */, - 689E9A74C0452C94E3479BEA /* Pods-Runner.profile.xcconfig */, + 1B82E12F9C5D326CBB2ADF7E /* Pods-Runner.debug.xcconfig */, + E6F536731AC506735EB76340 /* Pods-Runner.release.xcconfig */, + 15168938F13F6113519C963B /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -102,7 +102,7 @@ 7E8A4F06288D5A9300F18717 /* libepic_cash_wallet.a */, 7E8A4F02288D57DE00F18717 /* flutter_libepiccash.framework */, 7E569F982798D47200056D51 /* mobileliblelantus.framework */, - 08E84808BC00DEE3447AF47E /* Pods_Runner.framework */, + 51604430FD0FD1FA5C4767A0 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -167,14 +167,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - D7C3965259DE109272702285 /* [CP] Check Pods Manifest.lock */, + B108E043921CDEDDCB9E1E86 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 30277110A528C730AF372175 /* [CP] Embed Pods Frameworks */, + FD1CA371131604E6658D4146 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -235,7 +235,57 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 30277110A528C730AF372175 /* [CP] Embed Pods Frameworks */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; + B108E043921CDEDDCB9E1E86 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FD1CA371131604E6658D4146 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -267,9 +317,9 @@ "${BUILT_PRODUCTS_DIR}/lelantus/lelantus.framework", "${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", - "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework", + "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", - "${BUILT_PRODUCTS_DIR}/shared_preferences_ios/shared_preferences_ios.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", "${BUILT_PRODUCTS_DIR}/stack_wallet_backup/stack_wallet_backup.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", "${BUILT_PRODUCTS_DIR}/wakelock/wakelock.framework", @@ -301,9 +351,9 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/lelantus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/stack_wallet_backup.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock.framework", @@ -313,56 +363,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; - }; - D7C3965259DE109272702285 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/pubspec.lock b/pubspec.lock index a7948d64a..ec3dde2f7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,70 +5,80 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + url: "https://pub.dev" source: hosted version: "47.0.0" analyzer: dependency: "direct dev" description: name: analyzer - url: "https://pub.dartlang.org" + sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + url: "https://pub.dev" source: hosted version: "4.7.0" animations: dependency: "direct main" description: name: animations - url: "https://pub.dartlang.org" + sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 + url: "https://pub.dev" source: hosted version: "2.0.7" another_flushbar: dependency: "direct main" description: name: another_flushbar - url: "https://pub.dartlang.org" + sha256: fa09f8a4ca582c417669b7b1d0e85ce65bd074d80bb0dcbb1302ad1b22bdc3ef + url: "https://pub.dev" source: hosted version: "1.12.29" app_settings: dependency: "direct main" description: name: app_settings - url: "https://pub.dartlang.org" + sha256: "66715a323ac36d6c8201035ba678777c0d2ea869e4d7064300d95af10c3bb8cb" + url: "https://pub.dev" source: hosted version: "4.2.0" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: "80e5141fafcb3361653ce308776cfd7d45e6e9fbb429e14eec571382c0c5fecb" + url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.2" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" source: hosted version: "2.4.0" asn1lib: dependency: transitive description: name: asn1lib - url: "https://pub.dartlang.org" + sha256: ab96a1cb3beeccf8145c52e449233fe68364c9641623acd3adad66f8184f1039 + url: "https://pub.dev" source: hosted version: "1.4.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" barcode_scan2: dependency: "direct main" description: name: barcode_scan2 - url: "https://pub.dartlang.org" + sha256: f9af9252b8f3f5fa446f5456fd45f8871d09f883d8389a1d608b39231bfbc3fa + url: "https://pub.dev" source: hosted version: "4.2.3" bech32: @@ -84,7 +94,8 @@ packages: dependency: "direct main" description: name: bip32 - url: "https://pub.dartlang.org" + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" source: hosted version: "2.0.0" bip39: @@ -127,196 +138,224 @@ packages: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" bs58check: dependency: "direct main" description: name: bs58check - url: "https://pub.dartlang.org" + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" source: hosted version: "1.0.2" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + url: "https://pub.dev" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + url: "https://pub.dev" source: hosted version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: "93f05c041932674be039b0a2323d6cf57e5f2bbf884a3c0382f9e53fc45ebace" + url: "https://pub.dev" source: hosted version: "2.3.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + url: "https://pub.dev" source: hosted version: "8.4.3" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted version: "2.0.2" cli_util: dependency: transitive description: name: cli_util - url: "https://pub.dartlang.org" + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" source: hosted version: "0.3.5" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" source: hosted version: "4.4.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - url: "https://pub.dartlang.org" + sha256: e18bbd0243342ceca6fb33718fd2737c468c1a881ad7fed4b0a47258b5838d9d + url: "https://pub.dev" source: hosted version: "2.3.6+1" connectivity_plus_linux: dependency: transitive description: name: connectivity_plus_linux - url: "https://pub.dartlang.org" + sha256: "3caf859d001f10407b8e48134c761483e4495ae38094ffcca97193f6c271f5e2" + url: "https://pub.dev" source: hosted version: "1.3.1" connectivity_plus_macos: dependency: transitive description: name: connectivity_plus_macos - url: "https://pub.dartlang.org" + sha256: "488d2de1e47e1224ad486e501b20b088686ba1f4ee9c4420ecbc3b9824f0b920" + url: "https://pub.dev" source: hosted version: "1.2.6" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" source: hosted version: "1.2.4" connectivity_plus_web: dependency: transitive description: name: connectivity_plus_web - url: "https://pub.dartlang.org" + sha256: "81332be1b4baf8898fed17bb4fdef27abb7c6fd990bf98c54fd978478adf2f1a" + url: "https://pub.dev" source: hosted version: "1.2.5" connectivity_plus_windows: dependency: transitive description: name: connectivity_plus_windows - url: "https://pub.dartlang.org" + sha256: "535b0404b4d5605c4dd8453d67e5d6d2ea0dd36e3b477f50f31af51b0aeab9dd" + url: "https://pub.dev" source: hosted version: "1.2.2" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "196284f26f69444b7f5c50692b55ec25da86d9e500451dc09333bf2e3ad69259" + url: "https://pub.dev" source: hosted version: "3.0.2" coverage: dependency: transitive description: name: coverage - url: "https://pub.dartlang.org" + sha256: "525ac94733f9ce82507a050bfd62ad89eb1dcbc56308e1e2e17ab11abeee4a75" + url: "https://pub.dev" source: hosted version: "1.5.0" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" source: hosted version: "0.3.3+4" crypto: dependency: "direct main" description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" cryptography: dependency: transitive description: name: cryptography - url: "https://pub.dartlang.org" + sha256: ffd770340e5a48f57e473c42d9036a773c43b396e80b41a2dd164ffaf53f57a4 + url: "https://pub.dev" source: hosted version: "2.1.1" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted version: "0.17.2" cw_core: @@ -351,168 +390,192 @@ packages: dependency: transitive description: name: dart_base_x - url: "https://pub.dartlang.org" + sha256: c8af4f6a6518daab4aa85bb27ee148221644e80446bb44117052b6f4674cdb23 + url: "https://pub.dev" source: hosted version: "1.0.0" dart_bs58: dependency: "direct main" description: name: dart_bs58 - url: "https://pub.dartlang.org" + sha256: e2fff08fca810d5215f6fca3ea713d8a4a9728aaf1b1658472863b2de7377234 + url: "https://pub.dev" source: hosted version: "1.0.1" dart_bs58check: dependency: "direct main" description: name: dart_bs58check - url: "https://pub.dartlang.org" + sha256: "4284e606795a18c1df5a955928bdc4e1b6f908da7ab0e87f49db51b3774e9e6c" + url: "https://pub.dev" source: hosted version: "3.0.2" dart_numerics: dependency: "direct main" description: name: dart_numerics - url: "https://pub.dartlang.org" + sha256: "47408d4890551636204851325e5649bf1a1616ebc325184c36722a1716cbaba4" + url: "https://pub.dev" source: hosted version: "0.0.6" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" source: hosted version: "2.2.4" dartx: dependency: transitive description: name: dartx - url: "https://pub.dartlang.org" + sha256: "45d7176701f16c5a5e00a4798791c1964bc231491b879369c818dd9a9c764871" + url: "https://pub.dev" source: hosted version: "1.1.0" dbus: dependency: transitive description: name: dbus - url: "https://pub.dartlang.org" + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" source: hosted version: "0.7.8" decimal: dependency: "direct main" description: name: decimal - url: "https://pub.dartlang.org" + sha256: eece91944f523657c75a3a008a90ec7f7eb3986191153a78570c7d0ac8ef3d01 + url: "https://pub.dev" source: hosted version: "2.3.2" dependency_validator: dependency: "direct dev" description: name: dependency_validator - url: "https://pub.dartlang.org" + sha256: "08349175533ed0bd06eb9b6043cde66c45b2bfc7ebc222a7542cdb1324f1bf03" + url: "https://pub.dev" source: hosted version: "3.2.2" device_info_plus: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + sha256: "8d99246809e63d93e4e68fade79495d81f445ad735bde2b129b19c0adddcaf1a" + url: "https://pub.dev" source: hosted version: "7.0.1" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: "8b8b65e598b84fdb82c26cf9b3f05a6c4978636e99b0c070bae5905a24728199" + url: "https://pub.dev" source: hosted version: "6.0.1" devicelocale: dependency: "direct main" description: name: devicelocale - url: "https://pub.dartlang.org" + sha256: "5fd46b8c2a987b1c14b1551433be1974dbca5ec6ab00e7d53288b5197a21863e" + url: "https://pub.dev" source: hosted version: "0.5.5" dropdown_button2: dependency: "direct main" description: name: dropdown_button2 - url: "https://pub.dartlang.org" + sha256: "1a00301a1ec87666b1f1938f373865e3f5fb3056e4e36e1b3c2c0dc0c11f4737" + url: "https://pub.dev" source: hosted version: "1.7.2" emojis: dependency: "direct main" description: name: emojis - url: "https://pub.dartlang.org" + sha256: "2e4d847c3f1e2670f30dc355909ce6fa7808b4e626c34a4dd503a360995a38bf" + url: "https://pub.dev" source: hosted version: "0.9.9" encrypt: dependency: transitive description: name: encrypt - url: "https://pub.dartlang.org" + sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" + url: "https://pub.dev" source: hosted version: "5.0.1" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted version: "2.0.5" ethereum_addresses: dependency: "direct main" description: name: ethereum_addresses - url: "https://pub.dartlang.org" + sha256: e6ba01d44ecb9c5634367b017d6e94598fc937be8b28fc406d0e51ed6e9513dd + url: "https://pub.dev" source: hosted version: "1.0.2" event_bus: dependency: "direct main" description: name: event_bus - url: "https://pub.dartlang.org" + sha256: "44baa799834f4c803921873e7446a2add0f3efa45e101a054b1f0ab9b95f8edc" + url: "https://pub.dev" source: hosted version: "2.0.0" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.dartlang.org" + sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 + url: "https://pub.dev" source: hosted version: "5.2.5" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + url: "https://pub.dev" source: hosted version: "1.0.1" flare_flutter: dependency: "direct main" description: name: flare_flutter - url: "https://pub.dartlang.org" + sha256: "99d63c60f00fac81249ce6410ee015d7b125c63d8278a30da81edf3317a1f6a0" + url: "https://pub.dev" source: hosted version: "3.0.2" flutter: @@ -529,14 +592,16 @@ packages: dependency: "direct main" description: name: flutter_feather_icons - url: "https://pub.dartlang.org" + sha256: b33b9c276fc8108254632da6644cf01f71af6c17fbfb26e136a86945f5ff9b67 + url: "https://pub.dev" source: hosted version: "2.0.0+1" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + url: "https://pub.dev" source: hosted version: "0.11.0" flutter_libepiccash: @@ -557,119 +622,136 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - url: "https://pub.dartlang.org" + sha256: "57d0012730780fe137260dd180e072c18a73fbeeb924cdc029c18aaa0f338d64" + url: "https://pub.dev" source: hosted version: "9.9.1" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - url: "https://pub.dartlang.org" + sha256: b472bfc173791b59ede323661eae20f7fff0b6908fea33dd720a6ef5d576bae8 + url: "https://pub.dev" source: hosted version: "0.5.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - url: "https://pub.dartlang.org" + sha256: "21bceee103a66a53b30ea9daf677f990e5b9e89b62f222e60dd241cd08d63d3a" + url: "https://pub.dev" source: hosted version: "5.0.0" flutter_mobx: dependency: transitive description: name: flutter_mobx - url: "https://pub.dartlang.org" + sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" + url: "https://pub.dev" source: hosted version: "2.0.6+5" flutter_native_splash: dependency: "direct main" description: name: flutter_native_splash - url: "https://pub.dartlang.org" + sha256: "6777a3abb974021a39b5fdd2d46a03ca390e03903b6351f21d10e7ecc969f12d" + url: "https://pub.dev" source: hosted version: "2.2.16" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + url: "https://pub.dev" source: hosted version: "2.0.7" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - url: "https://pub.dartlang.org" + sha256: d84e180f039a6b963e610d2e4435641fdfe8f12437e8770e963632e05af16d80 + url: "https://pub.dev" source: hosted version: "1.0.4" flutter_rounded_date_picker: dependency: "direct main" description: name: flutter_rounded_date_picker - url: "https://pub.dartlang.org" + sha256: "369f7c63c1518d24c63f5da889ea9a6fb9a0a6f105ba9d22ccbba7665475784f" + url: "https://pub.dev" source: hosted version: "3.0.2" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - url: "https://pub.dartlang.org" + sha256: "5abe3d5c25ab435e48c47fb61bac25606062a305fac637c2f020e25abd30014a" + url: "https://pub.dev" source: hosted version: "5.1.2" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - url: "https://pub.dartlang.org" + sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" + url: "https://pub.dev" source: hosted version: "1.1.3" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - url: "https://pub.dartlang.org" + sha256: "388f76fd0f093e7415a39ec4c169ae7cceeee6d9f9ba529d788a13f2be4de7bd" + url: "https://pub.dev" source: hosted version: "1.1.2" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - url: "https://pub.dartlang.org" + sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + url: "https://pub.dev" source: hosted version: "1.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - url: "https://pub.dartlang.org" + sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + url: "https://pub.dev" source: hosted version: "1.1.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - url: "https://pub.dartlang.org" + sha256: ca89c8059cf439985aa83c59619b3674c7ef6cc2e86943d169a7369d6a69cab5 + url: "https://pub.dev" source: hosted version: "1.1.3" flutter_spinkit: dependency: "direct main" description: name: flutter_spinkit - url: "https://pub.dartlang.org" + sha256: "77a2117c0517ff909221f3160b8eb20052ab5216107581168af574ac1f05dff8" + url: "https://pub.dev" source: hosted version: "5.1.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: "6ff9fa12892ae074092de2fa6a9938fb21dbabfdaa2ff57dc697ff912fc8d4b2" + url: "https://pub.dev" source: hosted version: "1.1.6" flutter_test: @@ -686,7 +768,8 @@ packages: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "4f4a162323c86ffc1245765cfe138872b8f069deb42f7dbb36115fa27f31469b" + url: "https://pub.dev" source: hosted version: "2.1.3" fuchsia_remote_debug_protocol: @@ -698,98 +781,112 @@ packages: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: c51b4fdfee4d281f49b8c957f1add91b815473597f76bcf07377987f66a55729 + url: "https://pub.dev" source: hosted version: "2.1.0" google_fonts: dependency: "direct main" description: name: google_fonts - url: "https://pub.dartlang.org" + sha256: e70521755a6b08c6bde14ddae27dff5bf21010033888fc61da6c595f8a9f58c1 + url: "https://pub.dev" source: hosted version: "2.3.3" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted version: "2.2.0" hex: dependency: "direct main" description: name: hex - url: "https://pub.dartlang.org" + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" source: hosted version: "0.2.0" hive: dependency: "direct main" description: name: hive - url: "https://pub.dartlang.org" + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" source: hosted version: "2.2.3" hive_flutter: dependency: "direct main" description: name: hive_flutter - url: "https://pub.dartlang.org" + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" source: hosted version: "1.1.0" hive_generator: dependency: "direct dev" description: name: hive_generator - url: "https://pub.dartlang.org" + sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + url: "https://pub.dev" source: hosted version: "1.1.3" hive_test: dependency: "direct dev" description: name: hive_test - url: "https://pub.dartlang.org" + sha256: dd7a5cf0be7af288566a96180b5d07574023777aa947ef252b69046ec36d8eb2 + url: "https://pub.dev" source: hosted version: "1.0.1" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + url: "https://pub.dev" source: hosted version: "0.15.1" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" image: dependency: transitive description: name: image - url: "https://pub.dartlang.org" + sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + url: "https://pub.dev" source: hosted version: "3.3.0" import_sorter: dependency: "direct dev" description: name: import_sorter - url: "https://pub.dartlang.org" + sha256: eb15738ccead84e62c31e0208ea4e3104415efcd4972b86906ca64a1187d0836 + url: "https://pub.dev" source: hosted version: "4.6.0" integration_test: @@ -801,70 +898,80 @@ packages: dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted version: "1.0.4" isar: dependency: "direct main" description: name: isar - url: "https://pub.dartlang.org" + sha256: "5be35dbc489880fccc535da3d1c4b3f5fdeee6ebfcacd4b149e39e803c4029cd" + url: "https://pub.dev" source: hosted version: "3.0.5" isar_flutter_libs: dependency: "direct main" description: name: isar_flutter_libs - url: "https://pub.dartlang.org" + sha256: "9794524734856a8a3629652f9f359b66e3fea3cebeec4dbdeb3e3a8fb253073e" + url: "https://pub.dev" source: hosted version: "3.0.5" isar_generator: dependency: "direct dev" description: name: isar_generator - url: "https://pub.dartlang.org" + sha256: ee4ab5d5b251bc7e86e1257793b57af100065831f00f3a12404b177ae53c2d69 + url: "https://pub.dev" source: hosted version: "3.0.5" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted version: "4.8.0" json_rpc_2: dependency: transitive description: name: json_rpc_2 - url: "https://pub.dartlang.org" + sha256: "5e469bffa23899edacb7b22787780068d650b106a21c76db3c49218ab7ca447e" + url: "https://pub.dev" source: hosted version: "3.0.2" jsonrpc2: dependency: "direct main" description: name: jsonrpc2 - url: "https://pub.dartlang.org" + sha256: "98a71b834240ca6d003499ab8f28d1c35aa8ca90235b51e972e0f70596b86bd3" + url: "https://pub.dev" source: hosted version: "3.0.1" keyboard_dismisser: dependency: "direct main" description: name: keyboard_dismisser - url: "https://pub.dartlang.org" + sha256: f67e032581fc3dd1f77e1cb54c421b089e015d122aeba2490ba001cfcc42a181 + url: "https://pub.dev" source: hosted version: "3.0.0" lelantus: @@ -878,455 +985,520 @@ packages: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" local_auth: dependency: "direct main" description: name: local_auth - url: "https://pub.dartlang.org" + sha256: d3fece0749101725b03206f84a7dab7aaafb702dbbd09131ff8d8173259a9b19 + url: "https://pub.dev" source: hosted version: "1.1.11" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted version: "1.1.1" lottie: dependency: "direct main" description: name: lottie - url: "https://pub.dartlang.org" + sha256: "893da7a0022ec2fcaa616f34529a081f617e86cc501105b856e5a3184c58c7c2" + url: "https://pub.dev" source: hosted version: "1.4.3" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted version: "1.0.4" mobx: dependency: transitive description: name: mobx - url: "https://pub.dartlang.org" + sha256: "6738620307a424d2c9ad8b873f4dce391c44e9135eb4e75668ac8202fec7a9b8" + url: "https://pub.dev" source: hosted version: "2.1.4" mockingjay: dependency: "direct dev" description: name: mockingjay - url: "https://pub.dartlang.org" + sha256: b05c786d68da95286274470ad53d9ca98198d168300005500bdd348fbf6a503a + url: "https://pub.dev" source: hosted version: "0.2.0" mockito: dependency: "direct dev" description: name: mockito - url: "https://pub.dartlang.org" + sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe" + url: "https://pub.dev" source: hosted version: "5.3.2" mocktail: dependency: transitive description: name: mocktail - url: "https://pub.dartlang.org" + sha256: dd85ca5229cf677079fd9ac740aebfc34d9287cdf294e6b2ba9fae25c39e4dc2 + url: "https://pub.dev" source: hosted version: "0.2.0" mutex: dependency: "direct main" description: name: mutex - url: "https://pub.dartlang.org" + sha256: "03116a4e46282a671b46c12de649d72c0ed18188ffe12a8d0fc63e83f4ad88f4" + url: "https://pub.dev" source: hosted version: "3.0.1" nm: dependency: transitive description: name: nm - url: "https://pub.dartlang.org" + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" source: hosted version: "0.5.0" node_preamble: dependency: transitive description: name: node_preamble - url: "https://pub.dartlang.org" + sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + url: "https://pub.dev" source: hosted version: "2.0.1" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + sha256: f62d7253edc197fe3c88d7c2ddab82d68f555e778d55390ccc3537eca8e8d637 + url: "https://pub.dev" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + sha256: "04b575f44233d30edbb80a94e57cad9107aada334fc02aabb42b6becd13c43fc" + url: "https://pub.dev" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + sha256: a2ad8b4acf4cd479d4a0afa5a74ea3f5b1c7563b77e52cc32b3ee6956d5482a6 + url: "https://pub.dev" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: f7a0c8f1e7e981bc65f8b64137a53fd3c195b18d429fba960babc59a5a1c7ae8 + url: "https://pub.dev" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + sha256: f0829327eb534789e0a16ccac8936a80beed4e2401c4d3a74f3f39094a822d3b + url: "https://pub.dev" source: hosted version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + sha256: "79524f11c42dd9078b96d797b3cf79c0a2883a50c4920dc43da8562c115089bc" + url: "https://pub.dev" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.dartlang.org" + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + url: "https://pub.dev" source: hosted version: "2.0.12" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + url: "https://pub.dev" source: hosted version: "2.0.22" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - url: "https://pub.dartlang.org" + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + url: "https://pub.dev" source: hosted version: "2.1.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" + url: "https://pub.dev" source: hosted version: "2.1.8" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + url: "https://pub.dev" source: hosted version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + url: "https://pub.dev" source: hosted version: "2.1.3" permission_handler: dependency: "direct main" description: name: permission_handler - url: "https://pub.dartlang.org" + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + url: "https://pub.dev" source: hosted version: "10.2.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - url: "https://pub.dartlang.org" + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + url: "https://pub.dev" source: hosted version: "10.2.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - url: "https://pub.dartlang.org" + sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163" + url: "https://pub.dev" source: hosted version: "9.0.7" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - url: "https://pub.dartlang.org" + sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + url: "https://pub.dev" source: hosted version: "3.9.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - url: "https://pub.dartlang.org" + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" source: hosted version: "0.1.2" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted version: "5.1.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" pointycastle: dependency: "direct main" description: name: pointycastle - url: "https://pub.dartlang.org" + sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + url: "https://pub.dev" source: hosted version: "3.6.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" protobuf: dependency: transitive description: name: protobuf - url: "https://pub.dartlang.org" + sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08" + url: "https://pub.dev" source: hosted version: "2.1.0" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + url: "https://pub.dev" source: hosted version: "1.2.1" qr: dependency: transitive description: name: qr - url: "https://pub.dartlang.org" + sha256: "5c4208b4dc0d55c3184d10d83ee0ded6212dc2b5e2ba17c5a0c0aab279128d21" + url: "https://pub.dev" source: hosted version: "2.1.0" qr_flutter: dependency: "direct main" description: name: qr_flutter - url: "https://pub.dartlang.org" + sha256: c5c121c54cb6dd837b9b9d57eb7bc7ec6df4aee741032060c8833a678c80b87e + url: "https://pub.dev" source: hosted version: "4.0.0" rational: dependency: transitive description: name: rational - url: "https://pub.dartlang.org" + sha256: ba58e9e18df9abde280e8b10051e4bce85091e41e8e7e411b6cde2e738d357cf + url: "https://pub.dev" source: hosted version: "2.2.2" riverpod: dependency: transitive description: name: riverpod - url: "https://pub.dartlang.org" + sha256: e7f097159b9512f5953ff544164c19057f45ce28fd0cb971fc4cad1f7b28217d + url: "https://pub.dev" source: hosted version: "1.0.3" rpc_dispatcher: dependency: transitive description: name: rpc_dispatcher - url: "https://pub.dartlang.org" + sha256: b6ddcae58b3fbc1172a7c2dc8ab30d2f1090db8c7a728e4405bd10142dc48a47 + url: "https://pub.dev" source: hosted version: "1.0.1" rpc_exceptions: dependency: transitive description: name: rpc_exceptions - url: "https://pub.dartlang.org" + sha256: "09b2e5f3f805b65a262b40e3410d79fb916ff5be2a65e2f6b8b0eeef7aa965b7" + url: "https://pub.dev" source: hosted version: "1.0.1" rxdart: dependency: "direct main" description: name: rxdart - url: "https://pub.dartlang.org" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" source: hosted version: "0.27.7" share_plus: dependency: "direct main" description: name: share_plus - url: "https://pub.dartlang.org" + sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625" + url: "https://pub.dev" source: hosted version: "6.3.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: "82ddd4ab9260c295e6e39612d4ff00390b9a7a21f1bb1da771e2f232d80ab8a1" + url: "https://pub.dev" source: hosted version: "3.2.0" shared_preferences: dependency: transitive description: name: shared_preferences - url: "https://pub.dartlang.org" + sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" + url: "https://pub.dev" source: hosted version: "2.0.17" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" + url: "https://pub.dev" source: hosted version: "2.0.15" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - url: "https://pub.dartlang.org" + sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" + url: "https://pub.dev" source: hosted version: "2.1.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 + url: "https://pub.dev" source: hosted version: "2.1.3" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + url: "https://pub.dev" source: hosted version: "2.1.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + url: "https://pub.dev" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" + url: "https://pub.dev" source: hosted version: "2.1.3" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler - url: "https://pub.dartlang.org" + sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306 + url: "https://pub.dev" source: hosted version: "3.0.1" shelf_static: dependency: transitive description: name: shelf_static - url: "https://pub.dartlang.org" + sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + url: "https://pub.dev" source: hosted version: "1.1.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted version: "1.0.3" sky_engine: @@ -1338,44 +1510,50 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" source: hosted version: "1.2.6" source_helper: dependency: transitive description: name: source_helper - url: "https://pub.dartlang.org" + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" source: hosted version: "1.3.3" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace - url: "https://pub.dartlang.org" + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" source: hosted version: "2.1.1" source_maps: dependency: transitive description: name: source_maps - url: "https://pub.dartlang.org" + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" source: hosted version: "0.10.12" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stack_wallet_backup: dependency: "direct main" description: @@ -1389,294 +1567,336 @@ packages: dependency: transitive description: name: state_notifier - url: "https://pub.dartlang.org" + sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + url: "https://pub.dev" source: hosted version: "0.7.2+1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" string_to_hex: dependency: "direct main" description: name: string_to_hex - url: "https://pub.dartlang.org" + sha256: "63e5dc1f4821a2449d505033fbd4569f7020ebf30ddffb54d00ebaba8e144a49" + url: "https://pub.dev" source: hosted version: "0.2.2" string_validator: dependency: "direct main" description: name: string_validator - url: "https://pub.dartlang.org" + sha256: "50dd8ecf91db6a732f4a851eeae81ee12406eedc62d0da72f2d91a04a2d10dd8" + url: "https://pub.dev" source: hosted version: "0.3.0" sync_http: dependency: transitive description: name: sync_http - url: "https://pub.dartlang.org" + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" source: hosted version: "0.3.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test: dependency: transitive description: name: test - url: "https://pub.dartlang.org" + sha256: a5fcd2d25eeadbb6589e80198a47d6a464ba3e2049da473943b8af9797900c2d + url: "https://pub.dev" source: hosted - version: "1.21.4" + version: "1.22.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" test_core: dependency: transitive description: name: test_core - url: "https://pub.dartlang.org" + sha256: "0ef9755ec6d746951ba0aabe62f874b707690b5ede0fecc818b138fcc9b14888" + url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.4.20" time: dependency: transitive description: name: time - url: "https://pub.dartlang.org" + sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + url: "https://pub.dev" source: hosted version: "2.1.3" timezone: dependency: transitive description: name: timezone - url: "https://pub.dartlang.org" + sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e" + url: "https://pub.dev" source: hosted version: "0.8.0" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted version: "1.0.1" tint: dependency: transitive description: name: tint - url: "https://pub.dartlang.org" + sha256: "9652d9a589f4536d5e392cf790263d120474f15da3cf1bee7f1fdb31b4de5f46" + url: "https://pub.dev" source: hosted version: "2.0.1" toast: dependency: "direct main" description: name: toast - url: "https://pub.dartlang.org" + sha256: bedb96d37030acf9c4c06a7ac2ffd1f1f365e780cda9458c9e24e6a1e1ab6fd9 + url: "https://pub.dev" source: hosted version: "0.1.5" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + url: "https://pub.dev" source: hosted version: "2.2.0" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b + url: "https://pub.dev" source: hosted version: "6.1.9" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + url: "https://pub.dev" source: hosted version: "6.0.23" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" + url: "https://pub.dev" source: hosted version: "6.1.0" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + url: "https://pub.dev" source: hosted version: "3.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + url: "https://pub.dev" source: hosted version: "3.0.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + url: "https://pub.dev" source: hosted version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + url: "https://pub.dev" source: hosted version: "2.0.14" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + url: "https://pub.dev" source: hosted version: "3.0.3" uuid: dependency: "direct main" description: name: uuid - url: "https://pub.dartlang.org" + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" source: hosted version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - url: "https://pub.dartlang.org" + sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.4.0" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + sha256: "769ecf42eb2d07128407b50cb93d7c10bd2ee48f0276ef0119db1d25cc2f87db" + url: "https://pub.dev" source: hosted version: "0.6.2" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621" + url: "https://pub.dev" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567" + url: "https://pub.dev" source: hosted version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted version: "1.0.2" web3dart: dependency: "direct main" description: name: web3dart - url: "https://pub.dartlang.org" + sha256: "48b89a5fac0029770a18d1a8bd05ce8431722bacf76184e4301dae05781565e5" + url: "https://pub.dev" source: hosted version: "2.3.5" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted version: "2.3.0" webdriver: dependency: transitive description: name: webdriver - url: "https://pub.dartlang.org" + sha256: ef67178f0cc7e32c1494645b11639dd1335f1d18814aa8435113a92e9ef9d841 + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol - url: "https://pub.dartlang.org" + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" source: hosted version: "1.2.0" websocket_universal: dependency: "direct main" description: name: websocket_universal - url: "https://pub.dartlang.org" + sha256: c12656930ceb52dc22505bcb0705f93418c97ab70b8bbc31e6cc7e79a3717fd6 + url: "https://pub.dev" source: hosted version: "0.5.1" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" source: hosted version: "3.1.3" window_size: @@ -1692,35 +1912,40 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" source: hosted version: "0.2.0+3" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb + url: "https://pub.dev" source: hosted version: "6.1.0" xxh3: dependency: transitive description: name: xxh3 - url: "https://pub.dartlang.org" + sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7 + url: "https://pub.dev" source: hosted version: "1.0.1" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" zxcvbn: dependency: "direct main" description: name: zxcvbn - url: "https://pub.dartlang.org" + sha256: "5d860ab87c0e7f295902697afd364aa722d89d4e5839e8800ad1b0faf3d63b08" + url: "https://pub.dev" source: hosted version: "1.0.0" sdks: From e3bd3d3bf34d67f5fea352b441d2f66054192072 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Sun, 5 Mar 2023 13:01:12 -0600 Subject: [PATCH 088/208] coin select item token icon --- .../sub_widgets/coin_select_item.dart | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart index 1a7ca829f..4f2a84a9a 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/providers/providers.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/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -24,6 +29,22 @@ class CoinSelectItem extends ConsumerWidget { final isDesktop = Util.isDesktop; + String? tokenImageUri; + if (entity is EthTokenEntity) { + final currency = ExchangeDataLoadingService.instance.isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + (entity as EthTokenEntity).token.address, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirstSync(); + tokenImageUri = currency?.image; + } + return Container( decoration: BoxDecoration( color: selectedEntity == entity @@ -48,11 +69,17 @@ class CoinSelectItem extends ConsumerWidget { ), child: Row( children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: entity.coin), - width: 26, - height: 26, - ), + tokenImageUri != null + ? SvgPicture.network( + tokenImageUri, + width: 26, + height: 26, + ) + : SvgPicture.asset( + Assets.svg.iconFor(coin: entity.coin), + width: 26, + height: 26, + ), SizedBox( width: isDesktop ? 12 : 10, ), From 99a941485191a297edf225359b7fe079afa9b621 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Sun, 5 Mar 2023 13:59:44 -0600 Subject: [PATCH 089/208] add custom token selector to global list for desktop --- .../add_token_view/add_custom_token_view.dart | 7 +- .../add_custom_token_selector.dart | 66 ++++++++++++++ .../sub_widgets/add_token_list.dart | 88 +++++++++---------- .../add_wallet_view/add_wallet_view.dart | 27 +++++- .../sub_widgets/add_wallet_entity_list.dart | 23 +++-- .../sub_widgets/expanding_sub_list_item.dart | 3 + lib/route_generator.dart | 3 +- 7 files changed, 160 insertions(+), 57 deletions(-) create mode 100644 lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart index 395e8fa52..760efb00f 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -20,7 +20,7 @@ class AddCustomTokenView extends ConsumerStatefulWidget { required this.walletId, }) : super(key: key); - final String walletId; + final String? walletId; static const routeName = "/addCustomToken"; @@ -96,7 +96,10 @@ class _AddCustomTokenViewState extends ConsumerState { nameController.text = currentToken!.name; symbolController.text = currentToken!.symbol; decimalsController.text = currentToken!.decimals.toString(); - currentToken!.walletIds.add(widget.walletId); + if (widget.walletId != null) { + // TODO: this needs to be changed? + currentToken!.walletIds.add(widget.walletId!); + } } else { nameController.text = ""; symbolController.text = ""; diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart new file mode 100644 index 000000000..6c12107b0 --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; + +class AddCustomTokenSelector extends StatelessWidget { + const AddCustomTokenSelector({ + Key? key, + required this.addFunction, + }) : super(key: key); + + final VoidCallback addFunction; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: RawMaterialButton( + fillColor: Theme.of(context).extension()!.popupBG, + elevation: 0, + focusElevation: 0, + hoverElevation: 0, + highlightElevation: 0, + constraints: const BoxConstraints(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: addFunction, + child: Padding( + padding: Util.isDesktop + ? const EdgeInsets.only(left: 24) + : const EdgeInsets.all(12), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: Util.isDesktop ? 70 : 0, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circlePlusFilled, + color: Theme.of(context).extension()!.textDark, + width: 26, + height: 26, + ), + const SizedBox( + width: 12, + ), + Text( + "Add custom token", + style: Util.isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.w600_14(context), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart index 5b532536e..b472f1f63 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart @@ -1,11 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; -import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; class AddTokenList extends StatelessWidget { @@ -33,46 +28,49 @@ class AddTokenList extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ child, - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: RawMaterialButton( - fillColor: - Theme.of(context).extension()!.popupBG, - elevation: 0, - focusElevation: 0, - hoverElevation: 0, - highlightElevation: 0, - constraints: const BoxConstraints(), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: addFunction, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.circlePlusFilled, - color: Theme.of(context) - .extension()! - .textDark, - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Text( - "Add custom token", - style: STextStyles.w600_14(context), - ), - ], - ), - ), - ), + AddCustomTokenSelector( + addFunction: addFunction, ), + // Padding( + // padding: const EdgeInsets.symmetric(vertical: 4), + // child: RawMaterialButton( + // fillColor: + // Theme.of(context).extension()!.popupBG, + // elevation: 0, + // focusElevation: 0, + // hoverElevation: 0, + // highlightElevation: 0, + // constraints: const BoxConstraints(), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // ), + // onPressed: addFunction, + // child: Padding( + // padding: const EdgeInsets.all(12), + // child: Row( + // children: [ + // SvgPicture.asset( + // Assets.svg.circlePlusFilled, + // color: Theme.of(context) + // .extension()! + // .textDark, + // width: 24, + // height: 24, + // ), + // const SizedBox( + // width: 12, + // ), + // Text( + // "Add custom token", + // style: STextStyles.w600_14(context), + // ), + // ], + // ), + // ), + // ), + // ), ], ), child: Padding( diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index a3ca0aef8..6e74f9e81 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -9,6 +9,8 @@ import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_text.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart'; @@ -53,7 +55,7 @@ class _AddWalletViewState extends ConsumerState { ...Coin.values.sublist(0, Coin.values.length - kTestNetCoinCount - 1) ]; final List coinEntities = []; - final List tokenEntities = []; + final List tokenEntities = []; final bool isDesktop = Util.isDesktop; @@ -77,6 +79,26 @@ class _AddWalletViewState extends ConsumerState { return _entities; } + Future _addToken() async { + final token = await Navigator.of(context).pushNamed( + AddCustomTokenView.routeName, + arguments: null, // no walletId as no wallet has been selected/created yet + ); + if (token is EthContract) { + await MainDB.instance.putEthContract(token); + if (mounted) { + setState(() { + if (tokenEntities + .where((e) => e.token.address == token.address) + .isEmpty) { + tokenEntities.add(EthTokenEntity(token)); + tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); + } + }); + } + } + } + @override void initState() { _searchFieldController = TextEditingController(); @@ -233,6 +255,9 @@ class _AddWalletViewState extends ConsumerState { title: "Tokens", entities: filter(_searchTerm, tokenEntities), initialState: ExpandableState.collapsed, + trailing: AddCustomTokenSelector( + addFunction: _addToken, + ), ), ], ), diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart index 590a8c8ce..c503daa88 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart @@ -6,23 +6,32 @@ class AddWalletEntityList extends StatelessWidget { const AddWalletEntityList({ Key? key, required this.entities, + this.trailing, }) : super(key: key); final List entities; + final Widget? trailing; @override Widget build(BuildContext context) { return ListView.builder( shrinkWrap: true, primary: false, - itemCount: entities.length, + itemCount: trailing != null ? entities.length + 1 : entities.length, itemBuilder: (ctx, index) { - return Padding( - padding: const EdgeInsets.all(4), - child: CoinSelectItem( - entity: entities[index], - ), - ); + if (trailing != null && index == entities.length) { + return Padding( + padding: const EdgeInsets.all(4), + child: trailing, + ); + } else { + return Padding( + padding: const EdgeInsets.all(4), + child: CoinSelectItem( + entity: entities[index], + ), + ); + } }, ); } diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart index d3a4b543a..67c748b99 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart @@ -14,6 +14,7 @@ class ExpandingSubListItem extends StatefulWidget { Key? key, required this.title, required this.entities, + this.trailing, required this.initialState, double? animationDurationMultiplier, this.curve = Curves.easeInOutCubicEmphasized, @@ -23,6 +24,7 @@ class ExpandingSubListItem extends StatefulWidget { final String title; final List entities; + final Widget? trailing; final ExpandableState initialState; final double animationDurationMultiplier; final Curve curve; @@ -113,6 +115,7 @@ class _ExpandingSubListItemState extends State { primary: false, child: AddWalletEntityList( entities: widget.entities, + trailing: widget.trailing, ), ), ); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 81426cec5..75a5dc429 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -7,7 +7,6 @@ import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; @@ -219,7 +218,7 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case AddCustomTokenView.routeName: - if (args is String) { + if (args is String?) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => AddCustomTokenView( From ac5155e5f4ebfd6fab0c3381cf0baa0ec78b884a Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 6 Mar 2023 07:51:40 -0600 Subject: [PATCH 090/208] padding fix --- .../add_custom_token_selector.dart | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart index 6c12107b0..d84db9b6c 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart @@ -18,46 +18,40 @@ class AddCustomTokenSelector extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: RawMaterialButton( - fillColor: Theme.of(context).extension()!.popupBG, - elevation: 0, - focusElevation: 0, - hoverElevation: 0, - highlightElevation: 0, - constraints: const BoxConstraints(), + child: MaterialButton( + key: const Key("coinSelectItemButtonKey_add_custom"), + padding: Util.isDesktop + ? const EdgeInsets.only(left: 24) + : const EdgeInsets.all(12), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), onPressed: addFunction, - child: Padding( - padding: Util.isDesktop - ? const EdgeInsets.only(left: 24) - : const EdgeInsets.all(12), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: Util.isDesktop ? 70 : 0, - ), - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.circlePlusFilled, - color: Theme.of(context).extension()!.textDark, - width: 26, - height: 26, - ), - const SizedBox( - width: 12, - ), - Text( - "Add custom token", - style: Util.isDesktop - ? STextStyles.desktopTextMedium(context) - : STextStyles.w600_14(context), - ), - ], - ), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: Util.isDesktop ? 70 : 0, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circlePlusFilled, + color: Theme.of(context).extension()!.textDark, + width: 26, + height: 26, + ), + const SizedBox( + width: 12, + ), + Text( + "Add custom token", + style: Util.isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.w600_14(context), + ), + ], ), ), ), From 9fa2d4535d4f3f7f282de6ebaa3760e6a35ffa9e Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 22 Mar 2023 09:25:21 -0600 Subject: [PATCH 091/208] merge fixes --- assets/svg/themed/dark/ethereum.svg | 174 ++++++++++++++++++ assets/svg/themed/fruitSorbet/ethereum.svg | 174 ++++++++++++++++++ assets/svg/themed/light/ethereum.svg | 174 ++++++++++++++++++ assets/svg/themed/oceanBreeze/ethereum.svg | 174 ++++++++++++++++++ assets/svg/themed/oledBlack/ethereum.svg | 174 ++++++++++++++++++ lib/db/isar/main_db.dart | 2 +- lib/db/queries/queries.dart | 2 +- lib/pages/coin_control/coin_control_view.dart | 7 +- lib/pages/coin_control/utxo_card.dart | 2 +- lib/pages/coin_control/utxo_details_view.dart | 2 +- .../addresses/edit_address_label_view.dart | 2 +- .../addresses/receiving_addresses_view.dart | 96 ---------- .../addresses/wallet_addresses_view.dart | 2 +- .../sub_widgets/wallet_navigation_bar.dart | 1 + lib/pages/wallet_view/wallet_view.dart | 10 +- .../desktop_coin_control_use_dialog.dart | 2 +- .../desktop_coin_control_view.dart | 2 +- .../coin_control/freeze_button.dart | 2 +- .../coin_control/utxo_row.dart | 2 +- lib/route_generator.dart | 2 +- .../mixins/coin_control_interface.dart | 2 +- lib/utilities/assets.dart | 14 -- lib/utilities/enums/coin_enum.dart | 2 + pubspec.lock | 38 +++- 24 files changed, 927 insertions(+), 135 deletions(-) create mode 100644 assets/svg/themed/dark/ethereum.svg create mode 100644 assets/svg/themed/fruitSorbet/ethereum.svg create mode 100644 assets/svg/themed/light/ethereum.svg create mode 100644 assets/svg/themed/oceanBreeze/ethereum.svg create mode 100644 assets/svg/themed/oledBlack/ethereum.svg delete mode 100644 lib/pages/receive_view/addresses/receiving_addresses_view.dart diff --git a/assets/svg/themed/dark/ethereum.svg b/assets/svg/themed/dark/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/dark/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/fruitSorbet/ethereum.svg b/assets/svg/themed/fruitSorbet/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/fruitSorbet/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/light/ethereum.svg b/assets/svg/themed/light/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/light/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/ethereum.svg b/assets/svg/themed/oceanBreeze/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/oceanBreeze/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oledBlack/ethereum.svg b/assets/svg/themed/oledBlack/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/oledBlack/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 57efc4c87..efe2fdd6b 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -8,7 +8,7 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:tuple/tuple.dart'; -part 'queries/queries.dart'; +part '../queries/queries.dart'; class MainDB { MainDB._(); diff --git a/lib/db/queries/queries.dart b/lib/db/queries/queries.dart index 2995de06d..bc0307572 100644 --- a/lib/db/queries/queries.dart +++ b/lib/db/queries/queries.dart @@ -1,4 +1,4 @@ -part of 'package:stackwallet/db/main_db.dart'; +part of 'package:stackwallet/db/isar/main_db.dart'; enum CCFilter { all, diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 0eeff1089..de12dba39 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/coin_control/utxo_card.dart'; import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; @@ -16,6 +16,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.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_widgets/rotate_icon.dart'; import 'package:stackwallet/widgets/app_bar_field.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -24,13 +25,11 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/expandable2.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/toggle.dart'; import 'package:tuple/tuple.dart'; -import '../../widgets/animated_widgets/rotate_icon.dart'; -import '../../widgets/rounded_container.dart'; - enum CoinControlViewType { manage, use; diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index e2900a48b..685f94272 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index e4b31c63d..e5bcba57e 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; diff --git a/lib/pages/receive_view/addresses/edit_address_label_view.dart b/lib/pages/receive_view/addresses/edit_address_label_view.dart index 35102e7f9..7d9aa6635 100644 --- a/lib/pages/receive_view/addresses/edit_address_label_view.dart +++ b/lib/pages/receive_view/addresses/edit_address_label_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/db/isar/main_db.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/address_label.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/pages/receive_view/addresses/receiving_addresses_view.dart b/lib/pages/receive_view/addresses/receiving_addresses_view.dart deleted file mode 100644 index d2b513a46..000000000 --- a/lib/pages/receive_view/addresses/receiving_addresses_view.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:isar/isar.dart'; -import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/utilities/clipboard_interface.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/conditional_parent.dart'; -import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; - -class ReceivingAddressesView extends ConsumerWidget { - const ReceivingAddressesView({ - Key? key, - required this.walletId, - required this.isDesktop, - this.clipboard = const ClipboardWrapper(), - }) : super(key: key); - - static const String routeName = "/receivingAddressesView"; - - final String walletId; - final bool isDesktop; - final ClipboardInterface clipboard; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final coin = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId).coin)); - return ConditionalParent( - condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.backgroundAppBar, - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Receiving addresses", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ), - child: FutureBuilder( - future: MainDB.instance - .getAddresses(walletId) - .filter() - .subTypeEqualTo(AddressSubType.receiving) - .and() - .not() - .typeEqualTo(AddressType.nonWallet) - .sortByDerivationIndex() - .findAll(), - builder: (context, AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.data != null) { - // listview - return ListView.separated( - itemCount: snapshot.data!.length, - separatorBuilder: (_, __) => Container( - height: 10, - ), - itemBuilder: (_, index) => AddressCard( - walletId: walletId, - address: snapshot.data![index], - coin: coin, - ), - ); - } else { - return const Center( - child: LoadingIndicator( - height: 200, - width: 200, - ), - ); - } - }, - ), - ); - } -} diff --git a/lib/pages/receive_view/addresses/wallet_addresses_view.dart b/lib/pages/receive_view/addresses/wallet_addresses_view.dart index aec8f24c3..dad379988 100644 --- a/lib/pages/receive_view/addresses/wallet_addresses_view.dart +++ b/lib/pages/receive_view/addresses/wallet_addresses_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; 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 e69de29bb..8b1378917 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -0,0 +1 @@ + diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 083b79990..eaea818e9 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -7,8 +7,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; -import 'package:isar/isar.dart'; -import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; @@ -20,10 +18,10 @@ import 'package:stackwallet/pages/paynym/paynym_home_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_settingew/transaction_views/all_transactions_view.dart'; -import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; -import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; -import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.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/global/paynym_api_provider.dart'; import 'package:stackwallet/providers/providers.dart'; diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart index d0ba9ecbd..bf65a2923 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart index cf27cc80e..593666c3c 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/freeze_button.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; diff --git a/lib/pages_desktop_specific/coin_control/freeze_button.dart b/lib/pages_desktop_specific/coin_control/freeze_button.dart index f50e4661e..b33bc89fa 100644 --- a/lib/pages_desktop_specific/coin_control/freeze_button.dart +++ b/lib/pages_desktop_specific/coin_control/freeze_button.dart @@ -1,7 +1,7 @@ import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; import 'package:stackwallet/utilities/logger.dart'; diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart index e5ddf2f06..10348f409 100644 --- a/lib/pages_desktop_specific/coin_control/utxo_row.dart +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 2cc28d52d..c94867a00 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -2,8 +2,8 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; diff --git a/lib/services/mixins/coin_control_interface.dart b/lib/services/mixins/coin_control_interface.dart index ceebb6416..f41f5214a 100644 --- a/lib/services/mixins/coin_control_interface.dart +++ b/lib/services/mixins/coin_control_interface.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 189a34c3b..5d5f97feb 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -298,20 +298,6 @@ class _SVG { String get bnbIcon => "assets/svg/coin_icons/bnb_icon.svg"; - String get chevronRight => "assets/svg/chevron-right.svg"; - String get minimize => "assets/svg/minimize.svg"; - String get walletFa => "assets/svg/wallet-fa.svg"; - String get exchange3 => "assets/svg/exchange-3.svg"; - String get messageQuestion => "assets/svg/message-question-1.svg"; - -// TODO provide proper assets - String get bitcoinTestnet => "assets/svg/coin_icons/Bitcoin.svg"; - String get bitcoincashTestnet => "assets/svg/coin_icons/Bitcoincash.svg"; - String get firoTestnet => "assets/svg/coin_icons/Firo.svg"; - String get dogecoinTestnet => "assets/svg/coin_icons/Dogecoin.svg"; - String get particlTestnet => - "assets/svg/coin_icons/Dogecoin.svg"; //TODO - Update icon to particl - String iconFor({required Coin coin}) { switch (coin) { case Coin.bitcoin: diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 0463b4fa4..b4c994f8c 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -216,6 +216,7 @@ extension CoinExt on Coin { case Coin.namecoin: case Coin.particl: case Coin.epicCash: + case Coin.ethereum: case Coin.monero: case Coin.wownero: return false; @@ -239,6 +240,7 @@ extension CoinExt on Coin { case Coin.namecoin: case Coin.particl: case Coin.epicCash: + case Coin.ethereum: case Coin.monero: case Coin.wownero: return this; diff --git a/pubspec.lock b/pubspec.lock index fdf6de547..6aed3f88c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -395,7 +395,7 @@ packages: source: hosted version: "1.0.0" dart_bs58: - dependency: transitive + dependency: "direct main" description: name: dart_bs58 sha256: e2fff08fca810d5215f6fca3ea713d8a4a9728aaf1b1658472863b2de7377234 @@ -403,7 +403,7 @@ packages: source: hosted version: "1.0.1" dart_bs58check: - dependency: transitive + dependency: "direct main" description: name: dart_bs58check sha256: "4284e606795a18c1df5a955928bdc4e1b6f908da7ab0e87f49db51b3774e9e6c" @@ -514,6 +514,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.5" + ethereum_addresses: + dependency: "direct main" + description: + name: ethereum_addresses + sha256: e6ba01d44ecb9c5634367b017d6e94598fc937be8b28fc406d0e51ed6e9513dd + url: "https://pub.dev" + source: hosted + version: "1.0.2" event_bus: dependency: "direct main" description: @@ -794,7 +802,7 @@ packages: source: hosted version: "2.2.0" hex: - dependency: transitive + dependency: "direct main" description: name: hex sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" @@ -942,6 +950,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + sha256: "5e469bffa23899edacb7b22787780068d650b106a21c76db3c49218ab7ca447e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" jsonrpc2: dependency: "direct main" description: @@ -1579,6 +1595,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + string_to_hex: + dependency: "direct main" + description: + name: string_to_hex + sha256: "63e5dc1f4821a2449d505033fbd4569f7020ebf30ddffb54d00ebaba8e144a49" + url: "https://pub.dev" + source: hosted + version: "0.2.2" string_validator: dependency: "direct main" description: @@ -1827,6 +1851,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + web3dart: + dependency: "direct main" + description: + name: web3dart + sha256: "48b89a5fac0029770a18d1a8bd05ce8431722bacf76184e4301dae05781565e5" + url: "https://pub.dev" + source: hosted + version: "2.3.5" web_socket_channel: dependency: transitive description: From d2bddcdd8d44c008926a6a006bae9aee1d89e4b4 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 22 Mar 2023 09:39:28 -0600 Subject: [PATCH 092/208] add token view button to wallet nav bar --- lib/pages/wallet_view/wallet_view.dart | 17 +++++++++++++++++ lib/services/coins/manager.dart | 2 ++ 2 files changed, 19 insertions(+) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index eaea818e9..ce813c650 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -19,6 +19,7 @@ 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/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; @@ -824,6 +825,22 @@ class _WalletViewState extends ConsumerState { ), ], moreItems: [ + if (ref.watch( + walletsChangeNotifierProvider.select( + (value) => + value.getManager(widget.walletId).hasTokenSupport, + ), + )) + WalletNavigationBarItemData( + label: "Tokens", + icon: const CoinControlNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + MyTokensView.routeName, + arguments: walletId, + ); + }, + ), if (ref.watch( walletsChangeNotifierProvider.select( (value) => value diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index 5f232ec4b..408d2707a 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -232,6 +232,8 @@ class Manager with ChangeNotifier { bool get hasCoinControlSupport => _currentWallet is CoinControlInterface; + bool get hasTokenSupport => _currentWallet.coin == Coin.ethereum; + bool get hasWhirlpoolSupport => false; int get rescanOnOpenVersion => From b9b7ed3733086d46c9438cc8285b696dea8e418a Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 22 Mar 2023 14:00:01 -0600 Subject: [PATCH 093/208] missing asset --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 4403ea462..570b85cdf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -324,6 +324,7 @@ flutter: - assets/svg/framed-address-book.svg - assets/svg/framed-gear.svg - assets/svg/list-ul.svg + - assets/svg/cc.svg # coin icons From bb75a8c57cafcfb6e64e246e48c55d2f59d949bf Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 09:37:49 -0600 Subject: [PATCH 094/208] generated mocks update --- .../pages/send_view/send_view_test.mocks.dart | 29 +++++++++++++++++++ ...d_address_book_view_screen_test.mocks.dart | 10 +++++++ ..._entry_details_view_screen_test.mocks.dart | 10 +++++++ ...ess_book_entry_view_screen_test.mocks.dart | 10 +++++++ .../lockscreen_view_screen_test.mocks.dart | 10 +++++++ .../main_view_screen_testA_test.mocks.dart | 10 +++++++ .../main_view_screen_testB_test.mocks.dart | 10 +++++++ .../main_view_screen_testC_test.mocks.dart | 10 +++++++ .../backup_key_view_screen_test.mocks.dart | 10 +++++++ ...up_key_warning_view_screen_test.mocks.dart | 10 +++++++ .../create_pin_view_screen_test.mocks.dart | 10 +++++++ ...restore_wallet_view_screen_test.mocks.dart | 10 +++++++ ...ify_backup_key_view_screen_test.mocks.dart | 10 +++++++ .../currency_view_screen_test.mocks.dart | 10 +++++++ ...dd_custom_node_view_screen_test.mocks.dart | 10 +++++++ .../node_details_view_screen_test.mocks.dart | 10 +++++++ .../wallet_backup_view_screen_test.mocks.dart | 10 +++++++ ...rescan_warning_view_screen_test.mocks.dart | 10 +++++++ ...elete_mnemonic_view_screen_test.mocks.dart | 10 +++++++ ...allet_settings_view_screen_test.mocks.dart | 10 +++++++ .../settings_view_screen_test.mocks.dart | 10 +++++++ ...search_results_view_screen_test.mocks.dart | 10 +++++++ .../confirm_send_view_screen_test.mocks.dart | 10 +++++++ .../receive_view_screen_test.mocks.dart | 10 +++++++ .../send_view_screen_test.mocks.dart | 10 +++++++ .../wallet_view_screen_test.mocks.dart | 10 +++++++ test/services/coins/manager_test.mocks.dart | 19 ++++++++++++ .../managed_favorite_test.mocks.dart | 29 +++++++++++++++++++ .../table_view/table_view_row_test.mocks.dart | 29 +++++++++++++++++++ .../transaction_card_test.mocks.dart | 29 +++++++++++++++++++ test/widget_tests/wallet_card_test.mocks.dart | 19 ++++++++++++ ...et_info_row_balance_future_test.mocks.dart | 29 +++++++++++++++++++ .../wallet_info_row_test.mocks.dart | 29 +++++++++++++++++++ 33 files changed, 462 insertions(+) diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 9c2b16e64..77f714322 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -1640,6 +1640,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i13.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2781,6 +2800,16 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart index 010eb83a3..7d01dfbba 100644 --- a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart @@ -378,6 +378,16 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart index addfaf6ee..e82f51d64 100644 --- a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart @@ -339,6 +339,16 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart index ed28d7edc..d150ff23d 100644 --- a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart @@ -337,6 +337,16 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index 09c20af69..1a535475b 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -646,6 +646,16 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart index 83f17a9da..4833c56ee 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart @@ -433,6 +433,16 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart index 092a08b8d..33c356d74 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart @@ -433,6 +433,16 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart index 8ac5c53a2..b19d16114 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart @@ -433,6 +433,16 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart index e11d6e753..34a6a36fe 100644 --- a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart @@ -208,6 +208,16 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart index 3070b5d69..f3967ade5 100644 --- a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart @@ -431,6 +431,16 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 80b636a27..f1d3b0c4b 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -646,6 +646,16 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index 208bdfab9..20ac25812 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -487,6 +487,16 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart index f7f149b62..93d4f4874 100644 --- a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart @@ -208,6 +208,16 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart index 2c8902e82..cfe9cd50e 100644 --- a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart @@ -208,6 +208,16 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index 4894da3fe..8e5c07700 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -423,6 +423,16 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index 55a581fbd..c3d4a7279 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -423,6 +423,16 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart index 1ddfc95bc..8efc17d42 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart @@ -208,6 +208,16 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart index dd8109ad4..5f5c9ee97 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart @@ -208,6 +208,16 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart index f25778670..c39df921e 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart @@ -431,6 +431,16 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index 373541050..6f3828684 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -688,6 +688,16 @@ class MockManager extends _i1.Mock implements _i15.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart index 5a96f3a7b..ea371aad5 100644 --- a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart @@ -431,6 +431,16 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart index 031b57e12..9b4056348 100644 --- a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart +++ b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart @@ -210,6 +210,16 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart index 8df9045fd..b0e4bcc7d 100644 --- a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart @@ -209,6 +209,16 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart index c8f0c873a..6c02b46e1 100644 --- a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart @@ -208,6 +208,16 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart index 0f6a3d551..b3fcf528f 100644 --- a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart @@ -250,6 +250,16 @@ class MockManager extends _i1.Mock implements _i8.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart index 4656dacc4..5cbb93bc1 100644 --- a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart @@ -210,6 +210,16 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/services/coins/manager_test.mocks.dart b/test/services/coins/manager_test.mocks.dart index 2cbe98c5c..afcf7d743 100644 --- a/test/services/coins/manager_test.mocks.dart +++ b/test/services/coins/manager_test.mocks.dart @@ -992,6 +992,25 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i10.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); + @override void initWalletDB({_i7.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 351704fa0..5abf517b7 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -1432,6 +1432,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2411,6 +2430,16 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index 5eacbe02a..cc022b12e 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -1419,6 +1419,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { returnValueForMissingStub: _i21.Future.value(), ) as _i21.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i21.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i21.Future.value(), + returnValueForMissingStub: _i21.Future.value(), + ) as _i21.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2136,6 +2155,16 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 3774b9124..ba9b854b5 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -555,6 +555,16 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, @@ -1950,6 +1960,25 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { returnValueForMissingStub: _i18.Future.value(), ) as _i18.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i18.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i18.Future.value(), + returnValueForMissingStub: _i18.Future.value(), + ) as _i18.Future); + @override void initWalletDB({_i13.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index 93e6b562e..cce60be42 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -1182,6 +1182,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { returnValueForMissingStub: _i20.Future.value(), ) as _i20.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i20.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i20.Future.value(), + returnValueForMissingStub: _i20.Future.value(), + ) as _i20.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index f75530def..5c2641dbb 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -1431,6 +1431,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2348,6 +2367,16 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index a9730c670..1f786dbab 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -1431,6 +1431,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2348,6 +2367,16 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override + bool get hasWhirlpoolSupport => (super.noSuchMethod( + Invocation.getter(#hasWhirlpoolSupport), + returnValue: false, + ) as bool); + @override int get rescanOnOpenVersion => (super.noSuchMethod( Invocation.getter(#rescanOnOpenVersion), returnValue: 0, From e99ef7497ccace909a8ee9da158ed2eb679a4702 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 09:38:17 -0600 Subject: [PATCH 095/208] update eth contract isar model class --- .../isar/models/ethereum/eth_contract.dart | 9 +- .../isar/models/ethereum/eth_contract.g.dart | 488 +----------------- 2 files changed, 16 insertions(+), 481 deletions(-) diff --git a/lib/models/isar/models/ethereum/eth_contract.dart b/lib/models/isar/models/ethereum/eth_contract.dart index 391c2364a..d969cd503 100644 --- a/lib/models/isar/models/ethereum/eth_contract.dart +++ b/lib/models/isar/models/ethereum/eth_contract.dart @@ -11,9 +11,7 @@ class EthContract extends Contract { required this.symbol, required this.decimals, required this.type, - required this.walletIds, this.abi, - this.otherData, }); Id id = Isar.autoIncrement; @@ -29,13 +27,9 @@ class EthContract extends Contract { late final String? abi; - late final List walletIds; - @enumerated late final EthContractType type; - late final String? otherData; - EthContract copyWith({ Id? id, String? address, @@ -53,15 +47,14 @@ class EthContract extends Contract { symbol: symbol ?? this.symbol, decimals: decimals ?? this.decimals, type: type ?? this.type, - walletIds: walletIds ?? this.walletIds, abi: abi ?? this.abi, - otherData: otherData ?? this.otherData, )..id = id ?? this.id; } // Used in Isar db and stored there as int indexes so adding/removing values // in this definition should be done extremely carefully in production enum EthContractType { + unknown, erc20, erc721; } diff --git a/lib/models/isar/models/ethereum/eth_contract.g.dart b/lib/models/isar/models/ethereum/eth_contract.g.dart index d5bf8f0fe..bc9548e8d 100644 --- a/lib/models/isar/models/ethereum/eth_contract.g.dart +++ b/lib/models/isar/models/ethereum/eth_contract.g.dart @@ -37,26 +37,16 @@ const EthContractSchema = CollectionSchema( name: r'name', type: IsarType.string, ), - r'otherData': PropertySchema( - id: 4, - name: r'otherData', - type: IsarType.string, - ), r'symbol': PropertySchema( - id: 5, + id: 4, name: r'symbol', type: IsarType.string, ), r'type': PropertySchema( - id: 6, + id: 5, name: r'type', type: IsarType.byte, enumMap: _EthContracttypeEnumValueMap, - ), - r'walletIds': PropertySchema( - id: 7, - name: r'walletIds', - type: IsarType.stringList, ) }, estimateSize: _ethContractEstimateSize, @@ -101,20 +91,7 @@ int _ethContractEstimateSize( } bytesCount += 3 + object.address.length * 3; bytesCount += 3 + object.name.length * 3; - { - final value = object.otherData; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } bytesCount += 3 + object.symbol.length * 3; - bytesCount += 3 + object.walletIds.length * 3; - { - for (var i = 0; i < object.walletIds.length; i++) { - final value = object.walletIds[i]; - bytesCount += value.length * 3; - } - } return bytesCount; } @@ -128,10 +105,8 @@ void _ethContractSerialize( writer.writeString(offsets[1], object.address); writer.writeLong(offsets[2], object.decimals); writer.writeString(offsets[3], object.name); - writer.writeString(offsets[4], object.otherData); - writer.writeString(offsets[5], object.symbol); - writer.writeByte(offsets[6], object.type.index); - writer.writeStringList(offsets[7], object.walletIds); + writer.writeString(offsets[4], object.symbol); + writer.writeByte(offsets[5], object.type.index); } EthContract _ethContractDeserialize( @@ -145,11 +120,9 @@ EthContract _ethContractDeserialize( address: reader.readString(offsets[1]), decimals: reader.readLong(offsets[2]), name: reader.readString(offsets[3]), - otherData: reader.readStringOrNull(offsets[4]), - symbol: reader.readString(offsets[5]), - type: _EthContracttypeValueEnumMap[reader.readByteOrNull(offsets[6])] ?? - EthContractType.erc20, - walletIds: reader.readStringList(offsets[7]) ?? [], + symbol: reader.readString(offsets[4]), + type: _EthContracttypeValueEnumMap[reader.readByteOrNull(offsets[5])] ?? + EthContractType.unknown, ); object.id = id; return object; @@ -171,26 +144,24 @@ P _ethContractDeserializeProp

( case 3: return (reader.readString(offset)) as P; case 4: - return (reader.readStringOrNull(offset)) as P; - case 5: return (reader.readString(offset)) as P; - case 6: + case 5: return (_EthContracttypeValueEnumMap[reader.readByteOrNull(offset)] ?? - EthContractType.erc20) as P; - case 7: - return (reader.readStringList(offset) ?? []) as P; + EthContractType.unknown) as P; default: throw IsarError('Unknown property with id $propertyId'); } } const _EthContracttypeEnumValueMap = { - 'erc20': 0, - 'erc721': 1, + 'unknown': 0, + 'erc20': 1, + 'erc721': 2, }; const _EthContracttypeValueEnumMap = { - 0: EthContractType.erc20, - 1: EthContractType.erc721, + 0: EthContractType.unknown, + 1: EthContractType.erc20, + 2: EthContractType.erc721, }; Id _ethContractGetId(EthContract object) { @@ -906,160 +877,6 @@ extension EthContractQueryFilter }); } - QueryBuilder - otherDataIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'otherData', - )); - }); - } - - QueryBuilder - otherDataIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'otherData', - )); - }); - } - - QueryBuilder - otherDataEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'otherData', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'otherData', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'otherData', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - otherDataIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'otherData', - value: '', - )); - }); - } - - QueryBuilder - otherDataIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'otherData', - value: '', - )); - }); - } - QueryBuilder symbolEqualTo( String value, { bool caseSensitive = true, @@ -1246,231 +1063,6 @@ extension EthContractQueryFilter )); }); } - - QueryBuilder - walletIdsElementEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'walletIds', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - walletIdsElementGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'walletIds', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - walletIdsElementLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'walletIds', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - walletIdsElementBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'walletIds', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - walletIdsElementStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'walletIds', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - walletIdsElementEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'walletIds', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - walletIdsElementContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'walletIds', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - walletIdsElementMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'walletIds', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - walletIdsElementIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'walletIds', - value: '', - )); - }); - } - - QueryBuilder - walletIdsElementIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'walletIds', - value: '', - )); - }); - } - - QueryBuilder - walletIdsLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'walletIds', - length, - true, - length, - true, - ); - }); - } - - QueryBuilder - walletIdsIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'walletIds', - 0, - true, - 0, - true, - ); - }); - } - - QueryBuilder - walletIdsIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'walletIds', - 0, - false, - 999999, - true, - ); - }); - } - - QueryBuilder - walletIdsLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'walletIds', - 0, - true, - length, - include, - ); - }); - } - - QueryBuilder - walletIdsLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'walletIds', - length, - include, - 999999, - true, - ); - }); - } - - QueryBuilder - walletIdsLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'walletIds', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } } extension EthContractQueryObject @@ -1529,18 +1121,6 @@ extension EthContractQuerySortBy }); } - QueryBuilder sortByOtherData() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'otherData', Sort.asc); - }); - } - - QueryBuilder sortByOtherDataDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'otherData', Sort.desc); - }); - } - QueryBuilder sortBySymbol() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'symbol', Sort.asc); @@ -1628,18 +1208,6 @@ extension EthContractQuerySortThenBy }); } - QueryBuilder thenByOtherData() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'otherData', Sort.asc); - }); - } - - QueryBuilder thenByOtherDataDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'otherData', Sort.desc); - }); - } - QueryBuilder thenBySymbol() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'symbol', Sort.asc); @@ -1694,13 +1262,6 @@ extension EthContractQueryWhereDistinct }); } - QueryBuilder distinctByOtherData( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'otherData', caseSensitive: caseSensitive); - }); - } - QueryBuilder distinctBySymbol( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1713,12 +1274,6 @@ extension EthContractQueryWhereDistinct return query.addDistinctBy(r'type'); }); } - - QueryBuilder distinctByWalletIds() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'walletIds'); - }); - } } extension EthContractQueryProperty @@ -1753,12 +1308,6 @@ extension EthContractQueryProperty }); } - QueryBuilder otherDataProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'otherData'); - }); - } - QueryBuilder symbolProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'symbol'); @@ -1770,11 +1319,4 @@ extension EthContractQueryProperty return query.addPropertyName(r'type'); }); } - - QueryBuilder, QQueryOperations> - walletIdsProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'walletIds'); - }); - } } From b1d5b7f6eadb65617faeb4b4bd72fb0b63b91959 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 09:39:06 -0600 Subject: [PATCH 096/208] store user added token contracts in wallet hive box for now --- lib/db/hive/db.dart | 1 + lib/services/mixins/wallet_cache.dart | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 5c4f252e6..9f73a4962 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -253,4 +253,5 @@ abstract class DBKeys { static const String isFavorite = "isFavorite"; static const String id = "id"; static const String storedChainHeight = "storedChainHeight"; + static const String ethTokenContracts = "ethTokenContracts"; } diff --git a/lib/services/mixins/wallet_cache.dart b/lib/services/mixins/wallet_cache.dart index 7d859e74c..b5c4e2e75 100644 --- a/lib/services/mixins/wallet_cache.dart +++ b/lib/services/mixins/wallet_cache.dart @@ -112,4 +112,22 @@ mixin WalletCache { value: balance.toJsonIgnoreCoin(), ); } + + // Ethereum specific + List getWalletTokenContractAddresses() { + return DB.instance.get( + boxName: _walletId, + key: DBKeys.ethTokenContracts, + ) as List? ?? + []; + } + + Future updateWalletTokenContractAddresses( + List contractAddresses) async { + await DB.instance.put( + boxName: _walletId, + key: DBKeys.ethTokenContracts, + value: contractAddresses, + ); + } } From 51c00372e4e970413be342fa019ea869e6e6dce4 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 09:40:00 -0600 Subject: [PATCH 097/208] mounted check --- .../add_token_view/add_custom_token_view.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart index 760efb00f..26b417279 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -104,15 +104,17 @@ class _AddCustomTokenViewState extends ConsumerState { nameController.text = ""; symbolController.text = ""; decimalsController.text = ""; - unawaited( - showDialog( - context: context, - builder: (context) => StackOkDialog( - title: "Failed to look up token", - message: response.exception?.message, + if (mounted) { + unawaited( + showDialog( + context: context, + builder: (context) => StackOkDialog( + title: "Failed to look up token", + message: response.exception?.message, + ), ), - ), - ); + ); + } } setState(() { addTokenButtonEnabled = currentToken != null; From b7497f8dfed497c4c8327ae245b0517a32ad855e Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 10:22:05 -0600 Subject: [PATCH 098/208] wallet tokens list ui updates --- lib/pages/token_view/my_tokens_view.dart | 45 +++++-------------- .../sub_widgets/my_tokens_list.dart | 42 ++++++++++++++++- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index 2801040fd..2d0782574 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -3,12 +3,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:isar/isar.dart'; -import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_tokens_list.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -32,46 +30,18 @@ class MyTokensView extends ConsumerStatefulWidget { final String walletId; @override - ConsumerState createState() => _TokenDetailsViewState(); + ConsumerState createState() => _MyTokensViewState(); } -class _TokenDetailsViewState extends ConsumerState { +class _MyTokensViewState extends ConsumerState { late final String walletAddress; late final TextEditingController _searchController; final searchFieldFocusNode = FocusNode(); String _searchString = ""; - List _filter(String searchTerm) { - if (searchTerm.isNotEmpty) { - final term = searchTerm.toLowerCase(); - return MainDB.instance - .getEthContracts() - .filter() - .walletIdsElementEqualTo(widget.walletId) - .and() - .group( - (q) => q - .nameContains(term, caseSensitive: false) - .or() - .symbolContains(term, caseSensitive: false) - .or() - .addressContains(term, caseSensitive: false), - ) - .findAllSync(); - // return tokens.toList(); - } - //implement search/filter - return MainDB.instance - .getEthContracts() - .filter() - .walletIdsElementEqualTo(widget.walletId) - .findAllSync(); - } - @override void initState() { _searchController = TextEditingController(); - super.initState(); } @@ -84,6 +54,8 @@ class _TokenDetailsViewState extends ConsumerState { @override Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; return MasterScaffold( @@ -290,7 +262,12 @@ class _TokenDetailsViewState extends ConsumerState { Expanded( child: MyTokensList( walletId: widget.walletId, - tokens: _filter(_searchString), + searchTerm: _searchString, + tokenContracts: ref + .watch(walletsChangeNotifierProvider.select((value) => value + .getManager(widget.walletId) + .wallet as EthereumWallet)) + .getWalletTokenContractAddresses(), ), ), ], diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart index 473165944..fb8c8ef13 100644 --- a/lib/pages/token_view/sub_widgets/my_tokens_list.dart +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart'; @@ -7,16 +9,52 @@ class MyTokensList extends StatelessWidget { const MyTokensList({ Key? key, required this.walletId, - required this.tokens, + required this.searchTerm, + required this.tokenContracts, }) : super(key: key); final String walletId; - final List tokens; + final String searchTerm; + final List tokenContracts; + + List _filter(String searchTerm) { + if (tokenContracts.isEmpty) { + return []; + } + + if (searchTerm.isNotEmpty) { + final term = searchTerm.toLowerCase(); + return MainDB.instance + .getEthContracts() + .filter() + .anyOf( + tokenContracts, (q, e) => q.addressEqualTo(e)) + .and() + .group( + (q) => q + .nameContains(term, caseSensitive: false) + .or() + .symbolContains(term, caseSensitive: false) + .or() + .addressContains(term, caseSensitive: false), + ) + .findAllSync(); + // return tokens.toList(); + } + //implement search/filter + return MainDB.instance + .getEthContracts() + .filter() + .anyOf( + tokenContracts, (q, e) => q.addressEqualTo(e)) + .findAllSync(); + } @override Widget build(BuildContext context) { return Consumer( builder: (_, ref, __) { + final tokens = _filter(searchTerm); return ListView.builder( itemCount: tokens.length, itemBuilder: (ctx, index) { From a2ae9c62449f378f31266be1a26cd8f1eae74ce2 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 10:22:27 -0600 Subject: [PATCH 099/208] update defaults --- lib/utilities/default_eth_tokens.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/utilities/default_eth_tokens.dart b/lib/utilities/default_eth_tokens.dart index 11363752d..93f783697 100644 --- a/lib/utilities/default_eth_tokens.dart +++ b/lib/utilities/default_eth_tokens.dart @@ -8,7 +8,6 @@ abstract class DefaultTokens { symbol: "USDC", decimals: 6, type: EthContractType.erc20, - walletIds: [], ), EthContract( address: "0xdac17f958d2ee523a2206206994597c13d831ec7", @@ -16,7 +15,6 @@ abstract class DefaultTokens { symbol: "USDT", decimals: 6, type: EthContractType.erc20, - walletIds: [], ), EthContract( address: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", @@ -24,7 +22,6 @@ abstract class DefaultTokens { symbol: "SHIB", decimals: 18, type: EthContractType.erc20, - walletIds: [], ), EthContract( address: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", @@ -32,7 +29,6 @@ abstract class DefaultTokens { symbol: "BNB", decimals: 18, type: EthContractType.erc20, - walletIds: [], ), EthContract( address: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", @@ -40,7 +36,6 @@ abstract class DefaultTokens { symbol: "BUSD", decimals: 18, type: EthContractType.erc20, - walletIds: [], ), EthContract( address: "0x514910771af9ca656af840dff83e8264ecf986ca", @@ -48,7 +43,6 @@ abstract class DefaultTokens { symbol: "LINK", decimals: 18, type: EthContractType.erc20, - walletIds: [], ), ]; } From be11b18eb8d193475c74c3000f291ec2949fa8fb Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 12:16:28 -0600 Subject: [PATCH 100/208] handle user added wallet contracts --- .../add_token_view/add_token_view.dart | 6 ++-- .../coins/ethereum/ethereum_wallet.dart | 32 +++++-------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart index 2e4e3b03b..fc542d513 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart @@ -70,8 +70,10 @@ class _AddTokenViewState extends ConsumerState { } Future onNextPressed() async { - final selectedTokens = - tokenEntities.where((e) => e.selected).map((e) => e.token).toList(); + final selectedTokens = tokenEntities + .where((e) => e.selected) + .map((e) => e.token.address) + .toList(); final ethWallet = ref .read(walletsChangeNotifierProvider) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 523148c4c..3ea83be65 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -62,39 +62,23 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { Timer? timer; Timer? _networkAliveTimer; - Future addTokenContracts(List contracts) async { - List updatedContracts = []; - for (final contract in contracts) { - final updatedWalletIds = contract.walletIds.toList(); - if (!updatedWalletIds.contains(walletId)) { - updatedWalletIds.add(walletId); - updatedContracts.add(contract.copyWith(walletIds: updatedWalletIds)); - } else { - updatedContracts.add(contract); - } - } - await db.putEthContracts(updatedContracts); + Future addTokenContracts(List contractAddresses) async { + final set = getWalletTokenContractAddresses().toSet(); + set.addAll(contractAddresses); + await updateWalletTokenContractAddresses(set.toList()); GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( - "$contracts updated/added for: $walletId $walletName", + "$contractAddresses updated/added for: $walletId $walletName", walletId, ), ); } Future removeTokenContract(String contractAddress) async { - final contract = - await db.getEthContracts().addressEqualTo(contractAddress).findFirst(); - - if (contract == null) { - return; // todo some error? - } - - final updatedWalletIds = contract.walletIds.toList(); - updatedWalletIds.removeWhere((e) => e == contractAddress); - - await db.putEthContract(contract.copyWith(walletIds: updatedWalletIds)); + final set = getWalletTokenContractAddresses().toSet(); + set.removeWhere((e) => e == contractAddress); + await updateWalletTokenContractAddresses(set.toList()); GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( From 40cceed8e6148984be11234fccc1e5ba82390c1f Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 12:19:51 -0600 Subject: [PATCH 101/208] use trueblocks api to grab transactions --- lib/dto/ethereum/eth_token_tx_dto.dart | 216 ++++++++++++------ lib/dto/ethereum/eth_token_tx_extra_dto.dart | 115 ++++++++++ lib/services/ethereum/ethereum_api.dart | 142 +++++++----- .../ethereum/ethereum_token_service.dart | 43 +++- 4 files changed, 376 insertions(+), 140 deletions(-) create mode 100644 lib/dto/ethereum/eth_token_tx_extra_dto.dart diff --git a/lib/dto/ethereum/eth_token_tx_dto.dart b/lib/dto/ethereum/eth_token_tx_dto.dart index 797fe2f7e..2d2ddfd1f 100644 --- a/lib/dto/ethereum/eth_token_tx_dto.dart +++ b/lib/dto/ethereum/eth_token_tx_dto.dart @@ -1,82 +1,152 @@ -import 'package:stackwallet/utilities/logger.dart'; +/// address : "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984" +/// blockNumber : 16484149 +/// logIndex : 61 +/// topics : ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000003a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","0x000000000000000000000000c5e81fc2401b8104966637d5334cbce92f01dbf7"] +/// data : "0x0000000000000000000000000000000000000000000000002dac1c4be587d800" +/// articulatedLog : {"name":"Transfer","inputs":{"_amount":"3291036540000000000","_from":"0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","_to":"0xc5e81fc2401b8104966637d5334cbce92f01dbf7"}} +/// compressedLog : "{name:Transfer|inputs:{_amount:3291036540000000000|_from:0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597|_to:0xc5e81fc2401b8104966637d5334cbce92f01dbf7}}" +/// transactionHash : "0x5b59559a77fa5f1c70528d41f4fa2e5fa5a00b21fc2f3bc26b208b3062e46333" +/// transactionIndex : 25 -class EthTokenTxDTO { - final String blockHash; - final int blockNumber; - final int confirmations; - final String contractAddress; - final int cumulativeGasUsed; - final String from; - final int gas; - final BigInt gasPrice; - final int gasUsed; - final String hash; - final String input; - final int logIndex; - final int nonce; - final int timeStamp; - final String to; - final int tokenDecimal; - final String tokenName; - final String tokenSymbol; - final int transactionIndex; - final BigInt value; - - EthTokenTxDTO({ - required this.blockHash, +class EthTokenTxDto { + EthTokenTxDto({ + required this.address, required this.blockNumber, - required this.confirmations, - required this.contractAddress, - required this.cumulativeGasUsed, - required this.from, - required this.gas, - required this.gasPrice, - required this.gasUsed, - required this.hash, - required this.input, required this.logIndex, - required this.nonce, - required this.timeStamp, - required this.to, - required this.tokenDecimal, - required this.tokenName, - required this.tokenSymbol, + required this.topics, + required this.data, + required this.articulatedLog, + required this.compressedLog, + required this.transactionHash, required this.transactionIndex, - required this.value, }); - factory EthTokenTxDTO.fromMap({ - required Map map, - }) { - try { - return EthTokenTxDTO( - blockHash: map["blockHash"] as String, - blockNumber: int.parse(map["blockNumber"] as String), - confirmations: int.parse(map["confirmations"] as String), - contractAddress: map["contractAddress"] as String, - cumulativeGasUsed: int.parse(map["cumulativeGasUsed"] as String), - from: map["from"] as String, - gas: int.parse(map["gas"] as String), - gasPrice: BigInt.parse(map["gasPrice"] as String), - gasUsed: int.parse(map["gasUsed"] as String), - hash: map["hash"] as String, - input: map["input"] as String, - logIndex: int.parse(map["logIndex"] as String? ?? "-1"), - nonce: int.parse(map["nonce"] as String), - timeStamp: int.parse(map["timeStamp"] as String), - to: map["to"] as String, - tokenDecimal: int.parse(map["tokenDecimal"] as String), - tokenName: map["tokenName"] as String, - tokenSymbol: map["tokenSymbol"] as String, - transactionIndex: int.parse(map["transactionIndex"] as String), - value: BigInt.parse(map["value"] as String), + EthTokenTxDto.fromMap(Map json) + : address = json['address'] as String, + blockNumber = json['blockNumber'] as int, + logIndex = json['logIndex'] as int, + topics = List.from(json['topics'] as List), + data = json['data'] as String, + articulatedLog = ArticulatedLog.fromJson(json['articulatedLog']), + compressedLog = json['compressedLog'] as String, + transactionHash = json['transactionHash'] as String, + transactionIndex = json['transactionIndex'] as int; + + final String address; + final int blockNumber; + final int logIndex; + final List topics; + final String data; + final ArticulatedLog articulatedLog; + final String compressedLog; + final String transactionHash; + final int transactionIndex; + EthTokenTxDto copyWith({ + String? address, + int? blockNumber, + int? logIndex, + List? topics, + String? data, + ArticulatedLog? articulatedLog, + String? compressedLog, + String? transactionHash, + int? transactionIndex, + }) => + EthTokenTxDto( + address: address ?? this.address, + blockNumber: blockNumber ?? this.blockNumber, + logIndex: logIndex ?? this.logIndex, + topics: topics ?? this.topics, + data: data ?? this.data, + articulatedLog: articulatedLog ?? this.articulatedLog, + compressedLog: compressedLog ?? this.compressedLog, + transactionHash: transactionHash ?? this.transactionHash, + transactionIndex: transactionIndex ?? this.transactionIndex, ); - } catch (e, s) { - Logging.instance.log( - "EthTokenTxDTO.fromMap() failed: $e\n$s", - level: LogLevel.Fatal, - ); - rethrow; - } + Map toJson() { + final map = {}; + map['address'] = address; + map['blockNumber'] = blockNumber; + map['logIndex'] = logIndex; + map['topics'] = topics; + map['data'] = data; + map['articulatedLog'] = articulatedLog.toJson(); + map['compressedLog'] = compressedLog; + map['transactionHash'] = transactionHash; + map['transactionIndex'] = transactionIndex; + return map; + } +} + +/// name : "Transfer" +/// inputs : {"_amount":"3291036540000000000","_from":"0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","_to":"0xc5e81fc2401b8104966637d5334cbce92f01dbf7"} + +class ArticulatedLog { + ArticulatedLog({ + required this.name, + required this.inputs, + }); + + ArticulatedLog.fromJson(dynamic json) + : name = json['name'] as String, + inputs = Inputs.fromJson(json['inputs']); + + final String name; + final Inputs inputs; + + ArticulatedLog copyWith({ + String? name, + Inputs? inputs, + }) => + ArticulatedLog( + name: name ?? this.name, + inputs: inputs ?? this.inputs, + ); + + Map toJson() { + final map = {}; + map['name'] = name; + map['inputs'] = inputs.toJson(); + return map; + } +} + +/// _amount : "3291036540000000000" +/// _from : "0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597" +/// _to : "0xc5e81fc2401b8104966637d5334cbce92f01dbf7" +/// +class Inputs { + Inputs({ + required this.amount, + required this.from, + required this.to, + }); + + Inputs.fromJson(dynamic json) + : amount = json['_amount'] as String, + from = json['_from'] as String, + to = json['_to'] as String; + + final String amount; + final String from; + final String to; + + Inputs copyWith({ + String? amount, + String? from, + String? to, + }) => + Inputs( + amount: amount ?? this.amount, + from: from ?? this.from, + to: to ?? this.to, + ); + + Map toJson() { + final map = {}; + map['_amount'] = amount; + map['_from'] = from; + map['_to'] = to; + return map; } } diff --git a/lib/dto/ethereum/eth_token_tx_extra_dto.dart b/lib/dto/ethereum/eth_token_tx_extra_dto.dart new file mode 100644 index 000000000..60a032255 --- /dev/null +++ b/lib/dto/ethereum/eth_token_tx_extra_dto.dart @@ -0,0 +1,115 @@ +import 'dart:convert'; + +class EthTokenTxExtraDTO { + EthTokenTxExtraDTO({ + required this.blockHash, + required this.blockNumber, + required this.from, + required this.gas, + required this.gasCost, + required this.gasPrice, + required this.gasUsed, + required this.hash, + required this.input, + required this.nonce, + required this.timestamp, + required this.to, + required this.transactionIndex, + required this.value, + }); + + factory EthTokenTxExtraDTO.fromMap(Map map) => + EthTokenTxExtraDTO( + hash: map['hash'] as String, + blockHash: map['blockHash'] as String, + blockNumber: map['blockNumber'] as int, + transactionIndex: map['transactionIndex'] as int, + timestamp: map['timestamp'] as int, + from: map['from'] as String, + to: map['to'] as String, + value: int.parse(map['value'] as String), + gas: map['gas'] as int, + gasPrice: map['gasPrice'] as int, + nonce: map['nonce'] as int, + input: map['input'] as String, + gasCost: map['gasCost'] as int, + gasUsed: map['gasUsed'] as int, + ); + + factory EthTokenTxExtraDTO.fromJsonString(String jsonString) => + EthTokenTxExtraDTO.fromMap( + Map.from( + jsonDecode(jsonString) as Map, + ), + ); + + final String hash; + final String blockHash; + final int blockNumber; + final int transactionIndex; + final int timestamp; + final String from; + final String to; + final int value; + final int gas; + final int gasPrice; + final String input; + final int nonce; + final int gasCost; + final int gasUsed; + + EthTokenTxExtraDTO copyWith({ + String? hash, + String? blockHash, + int? blockNumber, + int? transactionIndex, + int? timestamp, + String? from, + String? to, + int? value, + int? gas, + int? gasPrice, + int? nonce, + String? input, + int? gasCost, + int? gasUsed, + }) => + EthTokenTxExtraDTO( + hash: hash ?? this.hash, + blockHash: blockHash ?? this.blockHash, + blockNumber: blockNumber ?? this.blockNumber, + transactionIndex: transactionIndex ?? this.transactionIndex, + timestamp: timestamp ?? this.timestamp, + from: from ?? this.from, + to: to ?? this.to, + value: value ?? this.value, + gas: gas ?? this.gas, + gasPrice: gasPrice ?? this.gasPrice, + nonce: nonce ?? this.nonce, + input: input ?? this.input, + gasCost: gasCost ?? this.gasCost, + gasUsed: gasUsed ?? this.gasUsed, + ); + + Map toMap() { + final map = {}; + map['hash'] = hash; + map['blockHash'] = blockHash; + map['blockNumber'] = blockNumber; + map['transactionIndex'] = transactionIndex; + map['timestamp'] = timestamp; + map['from'] = from; + map['to'] = to; + map['value'] = value; + map['gas'] = gas; + map['gasPrice'] = gasPrice; + map['input'] = input; + map['nonce'] = nonce; + map['gasCost'] = gasCost; + map['gasUsed'] = gasUsed; + return map; + } + + @override + String toString() => jsonEncode(toMap()); +} diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 41d37ba0c..ec8ebb7fe 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -4,12 +4,12 @@ import 'dart:math'; import 'package:decimal/decimal.dart'; import 'package:http/http.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; +import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart'; import 'package:stackwallet/dto/ethereum/eth_tx_dto.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; -import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; class EthApiException with Exception { @@ -36,9 +36,8 @@ abstract class EthereumAPI { static String stackURI = "$stackBaseServer/eth/mainnet/api"; - // static const blockScout = "https://blockscout.com/eth/mainnet/api"; - static const etherscanApi = - "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update + // static const etherscanApi = + // "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update static const gasTrackerUrl = "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; @@ -48,17 +47,10 @@ abstract class EthereumAPI { try { final response = await get( Uri.parse( - // "$blockScout?module=account&action=txlist&address=$address")); - // "stackURI?module=account&action=txlist&address=$address")); "$stackBaseServer/export?addrs=$address", ), ); - // "$etherscanApi?module=account&action=txlist&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); - - final BigInt myReceivingAddressInt = address.toBigIntFromHex; - - // print(response.body); if (response.statusCode == 200) { if (response.body.isNotEmpty) { final json = jsonDecode(response.body) as Map; @@ -68,23 +60,6 @@ abstract class EthereumAPI { for (final map in list!) { final txn = EthTxDTO.fromMap(Map.from(map as Map)); - final logs = map["receipt"]["logs"] as List; - - for (final log in logs) { - final map = log as Map; - - final contractAddress = map["address"] as String; - final topics = List.from(map["topics"] as List); - - for (int i = 0; i < topics.length; i++) { - if (topics[i].toBigIntFromHex == myReceivingAddressInt) { - print("================================================"); - print("Contract: $contractAddress"); - Logger.print((log).toString(), normalLength: false); - } - } - } - if (txn.hasToken == 0) { txns.add(txn); } @@ -112,7 +87,7 @@ abstract class EthereumAPI { ); } catch (e, s) { Logging.instance.log( - "getEthTransactions(): $e\n$s", + "getEthTransactions($address): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( @@ -122,44 +97,41 @@ abstract class EthereumAPI { } } - static Future>> getTokenTransactions({ - required String address, - String? contractAddress, - int? startBlock, - int? endBlock, - // todo add more params? - }) async { + static Future>> + getEthTokenTransactionsByTxids(List txids) async { try { - String uriString = - // "$blockScout?module=account&action=tokentx&address=$address"; - // "stackURI?module=account&action=tokentx&address=$address"; - "$etherscanApi?module=account&action=tokentx&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"; - if (contractAddress != null) { - uriString += "&contractAddress=$contractAddress"; - } - final uri = Uri.parse(uriString); - final response = await get(uri); + final response = await get( + Uri.parse( + "$stackBaseServer/transactions?transactions=${txids.join(" ")}", + ), + ); if (response.statusCode == 200) { - final json = jsonDecode(response.body); - if (json["message"] == "OK") { - final result = - List>.from(json["result"] as List); - final List tokenTxns = []; - for (final map in result) { - tokenTxns.add(EthTokenTxDTO.fromMap(map: map)); - } + if (response.body.isNotEmpty) { + final json = jsonDecode(response.body) as Map; + final list = json["data"] as List?; + final List txns = []; + for (final map in list!) { + final txn = EthTokenTxExtraDTO.fromMap( + Map.from(map as Map), + ); + + txns.add(txn); + } return EthereumResponse( - tokenTxns, + txns, null, ); } else { - throw EthApiException(json["message"] as String); + throw EthApiException( + "getEthTransaction($txids) response is empty but status code is " + "${response.statusCode}", + ); } } else { throw EthApiException( - "getTokenTransactions($address) failed with status code: " + "getEthTransaction($txids) failed with status code: " "${response.statusCode}", ); } @@ -170,7 +142,63 @@ abstract class EthereumAPI { ); } catch (e, s) { Logging.instance.log( - "getTokenTransactions(): $e\n$s", + "getEthTransaction($txids): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future>> getTokenTransactions({ + required String address, + required String tokenContractAddress, + }) async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/export?addrs=$address&emitter=$tokenContractAddress&logs=true", + ), + ); + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + final json = jsonDecode(response.body) as Map; + final list = json["data"] as List?; + + final List txns = []; + for (final map in list!) { + final txn = + EthTokenTxDto.fromMap(Map.from(map as Map)); + + txns.add(txn); + } + return EthereumResponse( + txns, + null, + ); + } else { + throw EthApiException( + "getTokenTransactions($address, $tokenContractAddress) response is empty but status code is " + "${response.statusCode}", + ); + } + } else { + throw EthApiException( + "getTokenTransactions($address, $tokenContractAddress) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getTokenTransactions($address, $tokenContractAddress): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index c59ce2657..ef6f25794 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -6,6 +6,8 @@ import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; +import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.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'; @@ -282,23 +284,44 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { .findAll(); Future _refreshTransactions() async { - String addressString = await currentReceivingAddress; + String addressString = + checksumEthereumAddress(await currentReceivingAddress); final response = await EthereumAPI.getTokenTransactions( address: addressString, - contractAddress: tokenContract.address, + tokenContractAddress: tokenContract.address, ); if (response.value == null) { throw response.exception ?? + Exception("Failed to fetch token transaction data"); + } + final response2 = await EthereumAPI.getEthTokenTransactionsByTxids( + response.value!.map((e) => e.transactionHash).toList(), + ); + + if (response2.value == null) { + throw response2.exception ?? Exception("Failed to fetch token transactions"); } + final List> data = []; + for (final tokenDto in response.value!) { + data.add( + Tuple2( + tokenDto, + response2.value!.firstWhere( + (e) => e.hash == tokenDto.transactionHash, + ), + ), + ); + } final List> txnsData = []; - for (final tx in response.value!) { + for (final tuple in data) { bool isIncoming; - if (checksumEthereumAddress(tx.from) == addressString) { + if (checksumEthereumAddress(tuple.item1.articulatedLog.inputs.from) == + addressString) { isIncoming = false; } else { isIncoming = true; @@ -306,17 +329,17 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { final txn = Transaction( walletId: ethWallet.walletId, - txid: tx.hash, - timestamp: tx.timeStamp, + txid: tuple.item1.transactionHash, + timestamp: tuple.item2.timestamp, type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, subType: TransactionSubType.ethToken, - amount: tx.value.toInt(), - fee: tx.gasUsed * tx.gasPrice.toInt(), - height: tx.blockNumber, + amount: int.parse(tuple.item1.articulatedLog.inputs.amount), + fee: tuple.item2.gasUsed * tuple.item2.gasPrice.toInt(), + height: tuple.item1.blockNumber, isCancelled: false, isLelantus: false, slateId: null, - otherData: tx.contractAddress, + otherData: tuple.item1.address, inputs: [], outputs: [], ); From 09b2c68cd5becb766a2f08b5406336b4aeb1eb8c Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 13:48:38 -0600 Subject: [PATCH 102/208] get token contract info updates and fixes --- .../add_token_view/add_custom_token_view.dart | 9 +------ .../add_wallet_view/add_wallet_view.dart | 1 - lib/route_generator.dart | 19 +++++--------- lib/services/ethereum/ethereum_api.dart | 26 +++++++++---------- 4 files changed, 21 insertions(+), 34 deletions(-) diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart index 26b417279..01f7b6f49 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -17,11 +17,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; class AddCustomTokenView extends ConsumerStatefulWidget { const AddCustomTokenView({ Key? key, - required this.walletId, }) : super(key: key); - final String? walletId; - static const routeName = "/addCustomToken"; @override @@ -86,7 +83,7 @@ class _AddCustomTokenViewState extends ConsumerState { label: "Search", onPressed: () async { final response = await showLoading( - whileFuture: EthereumAPI.getTokenByContractAddress( + whileFuture: EthereumAPI.getTokenContractInfoByAddress( contractController.text), context: context, message: "Looking up contract", @@ -96,10 +93,6 @@ class _AddCustomTokenViewState extends ConsumerState { nameController.text = currentToken!.name; symbolController.text = currentToken!.symbol; decimalsController.text = currentToken!.decimals.toString(); - if (widget.walletId != null) { - // TODO: this needs to be changed? - currentToken!.walletIds.add(widget.walletId!); - } } else { nameController.text = ""; symbolController.text = ""; diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 6e74f9e81..9e9d8a6fc 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -82,7 +82,6 @@ class _AddWalletViewState extends ConsumerState { Future _addToken() async { final token = await Navigator.of(context).pushNamed( AddCustomTokenView.routeName, - arguments: null, // no walletId as no wallet has been selected/created yet ); if (token is EthContract) { await MainDB.instance.putEthContract(token); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index c94867a00..7103d5f6b 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -223,18 +223,13 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case AddCustomTokenView.routeName: - if (args is String?) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddCustomTokenView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const AddCustomTokenView(), + settings: RouteSettings( + name: settings.name, + ), + ); case SingleFieldEditView.routeName: if (args is Tuple2) { diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index ec8ebb7fe..0f9c42c6c 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -362,35 +362,35 @@ abstract class EthereumAPI { slow: feesSlow.toInt()); } - static Future> getTokenByContractAddress( + static Future> getTokenContractInfoByAddress( String contractAddress) async { try { - final response = await get(Uri.parse( - "$etherscanApi?module=token&action=getToken&contractaddress=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); - // "stackURI?module=token&action=getToken&contractaddress=$contractAddress")); - // "$blockScout?module=token&action=getToken&contractaddress=$contractAddress")); + final response = await get( + Uri.parse( + "$stackBaseServer/tokens?addrs=$contractAddress&parts=all", + ), + ); + if (response.statusCode == 200) { final json = jsonDecode(response.body); if (json["message"] == "OK") { final map = Map.from(json["result"] as Map); EthContract? token; - if (map["type"] == "ERC-20") { + if (map["isErc20"] == true) { token = EthContract( - address: map["contractAddress"] as String, - decimals: int.parse(map["decimals"] as String), + address: map["address"] as String, + decimals: map["decimals"] as int, name: map["name"] as String, symbol: map["symbol"] as String, type: EthContractType.erc20, - walletIds: [], ); - } else if (map["type"] == "ERC-721") { + } else if (map["isErc721"] == true) { token = EthContract( - address: map["contractAddress"] as String, - decimals: int.parse(map["decimals"] as String), + address: map["address"] as String, + decimals: map["decimals"] as int, name: map["name"] as String, symbol: map["symbol"] as String, type: EthContractType.erc721, - walletIds: [], ); } else { throw EthApiException( From 1fd7e129e25f531cc6bf8962bea5aeed4e5dd37a Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 13:51:56 -0600 Subject: [PATCH 103/208] empty token tx check --- lib/services/ethereum/ethereum_token_service.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index ef6f25794..4aa431f3a 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -296,6 +296,12 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { throw response.exception ?? Exception("Failed to fetch token transaction data"); } + + // no need to continue if no transactions found + if (response.value!.isEmpty) { + return; + } + final response2 = await EthereumAPI.getEthTokenTransactionsByTxids( response.value!.map((e) => e.transactionHash).toList(), ); From c51ef8ee268e00ed8945672ea2f158aa5080c41d Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 16:28:20 -0600 Subject: [PATCH 104/208] edit wallet token list --- .../add_token_view/add_token_view.dart | 12 ++++++- .../sub_widgets/add_token_text.dart | 2 +- .../coins/ethereum/ethereum_wallet.dart | 32 +++++++++---------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart index fc542d513..0582440b2 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_token_view.dart @@ -80,7 +80,7 @@ class _AddTokenViewState extends ConsumerState { .getManager(widget.walletId) .wallet as EthereumWallet; - await ethWallet.addTokenContracts(selectedTokens); + await ethWallet.updateTokenContracts(selectedTokens); if (mounted) { Navigator.of(context).pop(42); } @@ -121,6 +121,16 @@ class _AddTokenViewState extends ConsumerState { tokenEntities.addAll(contracts.map((e) => AddTokenListElementData(e))); + final walletContracts = (ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as EthereumWallet) + .getWalletTokenContractAddresses(); + + for (var e in tokenEntities) { + e.selected = walletContracts.contains(e.token.address); + } + super.initState(); } diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart index c11e6295a..c1edfc2eb 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart @@ -27,7 +27,7 @@ class AddTokenText extends StatelessWidget { height: 4, ), Text( - "Add Tokens", + "Edit Tokens", textAlign: TextAlign.center, style: isDesktop ? STextStyles.desktopH2(context) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 3ea83be65..636c12c9c 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -62,10 +62,10 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { Timer? timer; Timer? _networkAliveTimer; - Future addTokenContracts(List contractAddresses) async { - final set = getWalletTokenContractAddresses().toSet(); - set.addAll(contractAddresses); - await updateWalletTokenContractAddresses(set.toList()); + Future updateTokenContracts(List contractAddresses) async { + // final set = getWalletTokenContractAddresses().toSet(); + // set.addAll(contractAddresses); + await updateWalletTokenContractAddresses(contractAddresses); GlobalEventBus.instance.fire( UpdatedInBackgroundEvent( @@ -75,18 +75,18 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { ); } - Future removeTokenContract(String contractAddress) async { - final set = getWalletTokenContractAddresses().toSet(); - set.removeWhere((e) => e == contractAddress); - await updateWalletTokenContractAddresses(set.toList()); - - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "$contractAddress removed for: $walletId $walletName", - walletId, - ), - ); - } + // Future removeTokenContract(String contractAddress) async { + // final set = getWalletTokenContractAddresses().toSet(); + // set.removeWhere((e) => e == contractAddress); + // await updateWalletTokenContractAddresses(set.toList()); + // + // GlobalEventBus.instance.fire( + // UpdatedInBackgroundEvent( + // "$contractAddress removed for: $walletId $walletName", + // walletId, + // ), + // ); + // } @override String get walletId => _walletId; From 0527497ce6ca6523c5cdb682709a55902becdda4 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 16:28:56 -0600 Subject: [PATCH 105/208] token view ui tweak --- .../token_view/sub_widgets/token_summary.dart | 30 ++++++- lib/pages/token_view/token_view.dart | 85 ++++++++++--------- 2 files changed, 69 insertions(+), 46 deletions(-) diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 51d8fb91c..eece604e5 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; @@ -57,10 +58,31 @@ class TokenSummary extends ConsumerWidget { const SizedBox( height: 6, ), - Text( - "${balance.getTotal()}" - " ${token.symbol}", - style: STextStyles.pageTitleH1(context), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "${balance.getTotal()}" + " ${token.symbol}", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + width: 10, + ), + RoundedContainer( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + radiusMultiplier: 0.25, + color: const Color( + 0xFF4D5798), // TODO: color theme for multi themes + child: Text( + ref.watch(walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).coin.ticker)), + style: STextStyles.w600_12(context).copyWith( + color: Colors.white, // TODO: design is wrong? + ), + ), + ), + ], ), const SizedBox( height: 6, diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 8552e8ba3..54c8e920c 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -6,18 +6,15 @@ import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.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/enums/coin_enum.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/rounded_container.dart'; final tokenServiceStateProvider = StateProvider((ref) => null); final tokenServiceProvider = ChangeNotifierProvider( @@ -69,59 +66,63 @@ class _TokenViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - centerTitle: true, + // centerTitle: true title: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset( - Assets.svg.iconForToken( - contractAddress: ref.watch(tokenServiceProvider - .select((value) => value!.tokenContract.address))), - width: 24, - height: 24, + const SizedBox( + width: 5, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.iconForToken( + contractAddress: ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.address))), + width: 24, + height: 24, + ), + const SizedBox( + width: 10, + ), + Flexible( + child: Text( + ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.name)), + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ], + ), ), const SizedBox( - width: 10, + width: 28, ), - Text( - ref.watch(tokenServiceProvider - .select((value) => value!.tokenContract.name)), - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, + WalletRefreshButton( + walletId: widget.walletId, + initialSyncStatus: initialSyncStatus, + tokenContractAddress: ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.address)), ), const SizedBox( width: 6, ), - RoundedContainer( - padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), - radiusMultiplier: 0.25, - color: const Color( - 0xFF4D5798), // TODO: color theme for multi themes - child: Text( - ref.watch(walletsChangeNotifierProvider.select((value) => - value.getManager(widget.walletId).coin.ticker)), - style: STextStyles.w600_12(context).copyWith( - color: Colors.white, // TODO: design is wrong? - ), + AppBarIconButton( + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, ), - ) + onPressed: () { + // todo: contract details + }, + ), ], ), - actions: [ - Padding( - padding: const EdgeInsets.all(10), - child: AspectRatio( - aspectRatio: 1, - child: WalletRefreshButton( - walletId: widget.walletId, - initialSyncStatus: initialSyncStatus, - tokenContractAddress: ref.watch(tokenServiceProvider - .select((value) => value!.tokenContract.address)), - ), - ), - ), - ], ), body: Container( color: Theme.of(context).extension()!.background, From 60225c93d3c6f7fb61a722b87d4b3d0c9015330d Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 16:53:40 -0600 Subject: [PATCH 106/208] basic contract details view --- .../token_contract_details_view.dart | 193 ++++++++++++++++++ lib/pages/token_view/token_view.dart | 12 +- lib/route_generator.dart | 16 ++ 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 lib/pages/token_view/token_contract_details_view.dart diff --git a/lib/pages/token_view/token_contract_details_view.dart b/lib/pages/token_view/token_contract_details_view.dart new file mode 100644 index 000000000..c73ea6a75 --- /dev/null +++ b/lib/pages/token_view/token_contract_details_view.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class TokenContractDetailsView extends ConsumerStatefulWidget { + const TokenContractDetailsView({ + Key? key, + required this.contractAddress, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/tokenContractDetailsView"; + + final String contractAddress; + final String walletId; + + @override + ConsumerState createState() => + _TokenContractDetailsViewState(); +} + +class _TokenContractDetailsViewState + extends ConsumerState { + final isDesktop = Util.isDesktop; + + late EthContract contract; + + @override + void initState() { + contract = MainDB.instance.isar.ethContracts + .where() + .addressEqualTo(widget.contractAddress) + .findFirstSync()!; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.backgroundAppBar, + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Contract details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Item( + title: "Contract address", + data: contract.address, + button: SimpleCopyButton( + data: contract.address, + ), + ), + const SizedBox( + height: 12, + ), + _Item( + title: "Name", + data: contract.name, + button: SimpleCopyButton( + data: contract.name, + ), + ), + const SizedBox( + height: 12, + ), + _Item( + title: "Symbol", + data: contract.symbol, + button: SimpleCopyButton( + data: contract.symbol, + ), + ), + const SizedBox( + height: 12, + ), + _Item( + title: "Type", + data: contract.type.name, + button: SimpleCopyButton( + data: contract.type.name, + ), + ), + const SizedBox( + height: 12, + ), + _Item( + title: "Decimals", + data: contract.decimals.toString(), + button: SimpleCopyButton( + data: contract.decimals.toString(), + ), + ), + ], + ), + ); + } +} + +class _Item extends StatelessWidget { + const _Item({ + Key? key, + required this.title, + required this.data, + required this.button, + }) : super(key: key); + + final String title; + final String data; + final Widget button; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: STextStyles.itemSubtitle(context), + ), + button, + ], + ), + const SizedBox( + height: 5, + ), + data.isNotEmpty + ? SelectableText( + data, + style: STextStyles.w500_14(context), + ) + : Text( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 54c8e920c..665b829c3 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; +import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; @@ -15,6 +16,7 @@ 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:tuple/tuple.dart'; final tokenServiceStateProvider = StateProvider((ref) => null); final tokenServiceProvider = ChangeNotifierProvider( @@ -118,7 +120,15 @@ class _TokenViewState extends ConsumerState { Assets.svg.verticalEllipsis, ), onPressed: () { - // todo: contract details + // todo: context menu + Navigator.of(context).pushNamed( + TokenContractDetailsView.routeName, + arguments: Tuple2( + ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.address)), + widget.walletId, + ), + ); }, ), ], diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 7103d5f6b..a6e69f8d6 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -95,6 +95,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; +import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart'; @@ -231,6 +232,21 @@ class RouteGenerator { ), ); + case TokenContractDetailsView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => TokenContractDetailsView( + contractAddress: args.item1, + walletId: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SingleFieldEditView.routeName: if (args is Tuple2) { return getRoute( From 1cca6e8e70ed3a8ffcef67a20f01a2d358f4417f Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 18:03:22 -0600 Subject: [PATCH 107/208] contract_abi extension to parse non etherscan abi --- .../extensions/impl/contract_abi.dart | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 lib/utilities/extensions/impl/contract_abi.dart diff --git a/lib/utilities/extensions/impl/contract_abi.dart b/lib/utilities/extensions/impl/contract_abi.dart new file mode 100644 index 000000000..c5a2877d8 --- /dev/null +++ b/lib/utilities/extensions/impl/contract_abi.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; + +import 'package:web3dart/web3dart.dart'; + +extension ContractAbiExtensions on ContractAbi { + static ContractAbi fromJsonList({ + required String name, + required String jsonList, + }) { + final List functions = []; + final List events = []; + + final list = List>.from(jsonDecode(jsonList) as List); + + for (final json in list) { + final type = json["type"] as String; + final name = json["name"] as String? ?? ""; + + if (type == "event") { + final anonymous = json["anonymous"] as bool? ?? false; + final List> components = []; + + for (final input in json["inputs"] as List) { + components.add( + EventComponent( + _parseParam(input as Map), + input['indexed'] as bool? ?? false, + ), + ); + } + + events.add(ContractEvent(anonymous, name, components)); + } else { + final mutability = _mutabilityNames[json['stateMutability']]; + final parsedType = _functionTypeNames[json['type']]; + if (parsedType != null) { + final inputs = _parseParams(json['inputs'] as List?); + final outputs = _parseParams(json['outputs'] as List?); + + functions.add( + ContractFunction( + name, + inputs, + outputs: outputs, + type: parsedType, + mutability: mutability ?? StateMutability.nonPayable, + ), + ); + } + } + } + + return ContractAbi(name, functions, events); + } + + static const Map _functionTypeNames = { + 'function': ContractFunctionType.function, + 'constructor': ContractFunctionType.constructor, + 'fallback': ContractFunctionType.fallback, + }; + + static const Map _mutabilityNames = { + 'pure': StateMutability.pure, + 'view': StateMutability.view, + 'nonpayable': StateMutability.nonPayable, + 'payable': StateMutability.payable, + }; + + static List> _parseParams(List? data) { + if (data == null || data.isEmpty) return []; + + final elements = >[]; + for (final entry in data) { + elements.add(_parseParam(entry as Map)); + } + + return elements; + } + + static FunctionParameter _parseParam(Map entry) { + final name = entry['name'] as String; + final typeName = entry['type'] as String; + + if (typeName.contains('tuple')) { + final components = entry['components'] as List; + return _parseTuple(name, typeName, _parseParams(components)); + } else { + final type = parseAbiType(entry['type'] as String); + return FunctionParameter(name, type); + } + } + + static CompositeFunctionParameter _parseTuple(String name, String typeName, + List> components) { + // The type will have the form tuple[3][]...[1], where the indices after the + // tuple indicate that the type is part of an array. + assert(RegExp(r'^tuple(?:\[\d*\])*$').hasMatch(typeName), + '$typeName is an invalid tuple type'); + + final arrayLengths = []; + var remainingName = typeName; + + while (remainingName != 'tuple') { + final arrayMatch = RegExp(r'^(.*)\[(\d*)\]$').firstMatch(remainingName)!; + remainingName = arrayMatch.group(1)!; + + final insideSquareBrackets = arrayMatch.group(2)!; + if (insideSquareBrackets.isEmpty) { + arrayLengths.insert(0, null); + } else { + arrayLengths.insert(0, int.parse(insideSquareBrackets)); + } + } + + return CompositeFunctionParameter(name, components, arrayLengths); + } +} From 85c416fb50e407d712e7277f9eeabfbd67236fc8 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 18:04:47 -0600 Subject: [PATCH 108/208] eth trueblocks fetch token abi call --- lib/services/ethereum/ethereum_api.dart | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 0f9c42c6c..2e7827d63 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -427,34 +427,27 @@ abstract class EthereumAPI { } } - static Future> getTokenAbi( - String contractAddress) async { + static Future> getTokenAbi({ + required String name, + required String contractAddress, + }) async { try { final response = await get( Uri.parse( - "$stackBaseServer/abis?addrs=$contractAddress", + "$stackBaseServer/abis?addrs=$contractAddress&verbose=true", ), ); if (response.statusCode == 200) { final json = jsonDecode(response.body)["data"] as List; - // trueblocks api does not contain the `anonymous` value - // web3dart expects it so hack it in - // TODO: fix this if we ever actually need to use contract ABI events - for (final map in json) { - if (map["type"] == "event") { - map["anonymous"] = false; - } - } - return EthereumResponse( jsonEncode(json), null, ); } else { throw EthApiException( - "getTokenAbi($contractAddress) failed with status code: " + "getTokenAbi($name, $contractAddress) failed with status code: " "${response.statusCode}", ); } @@ -465,7 +458,7 @@ abstract class EthereumAPI { ); } catch (e, s) { Logging.instance.log( - "getTokenAbi(): $e\n$s", + "getTokenAbi($name, $contractAddress): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( From b27b90c08adcdb45e58cb613074707408d239fc8 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 18:05:35 -0600 Subject: [PATCH 109/208] WIP eth token abi fetch and parsing --- lib/services/ethereum/ethereum_api.dart | 125 ++++++++++++------ .../ethereum/ethereum_token_service.dart | 97 +++++++++++--- 2 files changed, 161 insertions(+), 61 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 2e7827d63..379b276d9 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -468,46 +468,89 @@ abstract class EthereumAPI { } } - /// Fetch the underlying contract address that a proxy contract points to - static Future> getProxyTokenImplementation( - String contractAddress) async { - try { - final response = await get(Uri.parse( - // "$stackURI?module=contract&action=getsourcecode&address=$contractAddress")); - "$etherscanApi?module=contract&action=getsourcecode&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); - if (response.statusCode == 200) { - final json = jsonDecode(response.body); - if (json["message"] == "OK") { - final list = json["result"] as List; - final map = Map.from(list.first as Map); + // static Future> getTokenAbi22( + // String contractAddress) async { + // try { + // final response = await get( + // Uri.parse( + // "https://api.etherscan.io/api?module=contract&action=getabi&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP", + // ), + // ); + // + // if (response.statusCode == 200) { + // final json = jsonDecode(response.body) as Map; + // print("========================== 222222222222222 ================"); + // dev.log((jsonDecode(json["result"] as String)).toString()); + // print( + // "============================ 2222222222222222222 =============="); + // + // return EthereumResponse( + // json["result"] as String, + // null, + // ); + // } else { + // throw EthApiException( + // "getTokenAbi($contractAddress) failed with status code: " + // "${response.statusCode}", + // ); + // } + // } on EthApiException catch (e) { + // return EthereumResponse( + // null, + // e, + // ); + // } catch (e, s) { + // Logging.instance.log( + // "getTokenAbi(): $e\n$s", + // level: LogLevel.Error, + // ); + // return EthereumResponse( + // null, + // EthApiException(e.toString()), + // ); + // } + // } - return EthereumResponse( - map["Implementation"] as String, - null, - ); - } else { - throw EthApiException(json["message"] as String); - } - } else { - throw EthApiException( - "fetchProxyTokenImplementation($contractAddress) failed with status code: " - "${response.statusCode}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.log( - "fetchProxyTokenImplementation(): $e\n$s", - level: LogLevel.Error, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } + // /// Fetch the underlying contract address that a proxy contract points to + // static Future> getProxyTokenImplementation( + // String contractAddress) async { + // try { + // final response = await get(Uri.parse( + // "$stackURI?module=contract&action=getsourcecode&address=$contractAddress")); + // // "$etherscanApi?module=contract&action=getsourcecode&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + // if (response.statusCode == 200) { + // final json = jsonDecode(response.body); + // if (json["message"] == "OK") { + // final list = json["result"] as List; + // final map = Map.from(list.first as Map); + // + // return EthereumResponse( + // map["Implementation"] as String, + // null, + // ); + // } else { + // throw EthApiException(json["message"] as String); + // } + // } else { + // throw EthApiException( + // "fetchProxyTokenImplementation($contractAddress) failed with status code: " + // "${response.statusCode}", + // ); + // } + // } on EthApiException catch (e) { + // return EthereumResponse( + // null, + // e, + // ); + // } catch (e, s) { + // Logging.instance.log( + // "fetchProxyTokenImplementation(): $e\n$s", + // level: LogLevel.Error, + // ); + // return EthereumResponse( + // null, + // EthApiException(e.toString()), + // ); + // } + // } } diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 4aa431f3a..6cc8b2af2 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; @@ -24,6 +25,7 @@ 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/extensions/impl/contract_abi.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -116,7 +118,10 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { required EthContract forContract, required String usingContractAddress, }) async { - final abiResponse = await EthereumAPI.getTokenAbi(usingContractAddress); + final abiResponse = await EthereumAPI.getTokenAbi( + name: forContract.name, + contractAddress: usingContractAddress, + ); // Fetch token ABI so we can call token functions if (abiResponse.value != null) { final updatedToken = forContract.copyWith(abi: abiResponse.value!); @@ -132,12 +137,12 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { final contractAddress = web3dart.EthereumAddress.fromHex(tokenContract.address); - if (tokenContract.abi == null) { - _tokenContract = await _updateTokenABI( - forContract: tokenContract, - usingContractAddress: contractAddress.hex, - ); - } + // if (tokenContract.abi == null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddress.hex, + ); + // } String? mnemonicString = await ethWallet.mnemonicString; @@ -149,7 +154,10 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { _credentials = web3dart.EthPrivateKey.fromHex(privateKey); _deployedContract = web3dart.DeployedContract( - web3dart.ContractAbi.fromJson(tokenContract.abi!, tokenContract.name), + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), contractAddress, ); @@ -157,24 +165,73 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { _balanceFunction = _deployedContract.function('balanceOf'); _sendFunction = _deployedContract.function('transfer'); } catch (_) { - // function not found so likely a proxy so we need to fetch the impl - final contractAddressResponse = - await EthereumAPI.getProxyTokenImplementation(contractAddress.hex); + //==================================================================== + final list = List>.from( + jsonDecode(tokenContract.abi!) as List); + final functionNames = list.map((e) => e["name"] as String); - if (contractAddressResponse.value != null) { - _tokenContract = await _updateTokenABI( - forContract: tokenContract, - usingContractAddress: contractAddressResponse.value!, + if (!functionNames.contains("balanceOf")) { + list.add( + { + "encoding": "0x70a08231", + "inputs": [ + {"name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [ + {"name": "val_0", "type": "uint256"} + ], + "signature": "balanceOf(address)", + "type": "function" + }, ); - } else { - throw contractAddressResponse.exception!; } + + if (!functionNames.contains("transfer")) { + list.add( + { + "encoding": "0xa9059cbb", + "inputs": [ + {"name": "dst", "type": "address"}, + {"name": "rawAmount", "type": "uint256"} + ], + "name": "transfer", + "outputs": [ + {"name": "val_0", "type": "bool"} + ], + "signature": "transfer(address,uint256)", + "type": "function" + }, + ); + } + //-------------------------------------------------------------------- + //==================================================================== + + // function not found so likely a proxy so we need to fetch the impl + //==================================================================== + final updatedToken = tokenContract.copyWith(abi: jsonEncode(list)); + // Store updated contract + final id = await MainDB.instance.putEthContract(updatedToken); + _tokenContract = updatedToken..id = id; + //-------------------------------------------------------------------- + // final contractAddressResponse = + // await EthereumAPI.getProxyTokenImplementation(contractAddress.hex); + // + // if (contractAddressResponse.value != null) { + // _tokenContract = await _updateTokenABI( + // forContract: tokenContract, + // usingContractAddress: contractAddressResponse.value!, + // ); + // } else { + // throw contractAddressResponse.exception!; + // } + //==================================================================== } _deployedContract = web3dart.DeployedContract( - web3dart.ContractAbi.fromJson( - tokenContract.abi!, - tokenContract.name, + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, ), contractAddress, ); From 44a87df41c5fe7d485681e91044a483e4e0ba116 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 18:21:09 -0600 Subject: [PATCH 110/208] merge clean up --- .../isar/models/blockchain_data/address.dart | 2 + .../create_or_restore_wallet_view.dart | 1 - .../addresses/address_details_view.dart | 2 +- .../appearance_settings_view.dart | 1 - .../settings_menu/appearance_settings.dart | 1 - lib/utilities/db_version_migration.dart | 2 +- .../pages/send_view/send_view_test.mocks.dart | 26 +++++++++++- ...d_address_book_view_screen_test.mocks.dart | 5 +++ ..._entry_details_view_screen_test.mocks.dart | 5 +++ ...ess_book_entry_view_screen_test.mocks.dart | 5 +++ .../exchange/exchange_view_test.mocks.dart | 17 ++++++++ .../lockscreen_view_screen_test.mocks.dart | 5 +++ .../main_view_screen_testA_test.mocks.dart | 5 +++ .../main_view_screen_testB_test.mocks.dart | 5 +++ .../main_view_screen_testC_test.mocks.dart | 5 +++ .../backup_key_view_screen_test.mocks.dart | 5 +++ ...up_key_warning_view_screen_test.mocks.dart | 5 +++ .../create_pin_view_screen_test.mocks.dart | 5 +++ ...restore_wallet_view_screen_test.mocks.dart | 5 +++ ...ify_backup_key_view_screen_test.mocks.dart | 5 +++ .../currency_view_screen_test.mocks.dart | 5 +++ ...dd_custom_node_view_screen_test.mocks.dart | 5 +++ .../node_details_view_screen_test.mocks.dart | 5 +++ .../wallet_backup_view_screen_test.mocks.dart | 5 +++ ...rescan_warning_view_screen_test.mocks.dart | 5 +++ ...elete_mnemonic_view_screen_test.mocks.dart | 5 +++ ...allet_settings_view_screen_test.mocks.dart | 5 +++ .../settings_view_screen_test.mocks.dart | 5 +++ ...search_results_view_screen_test.mocks.dart | 5 +++ .../confirm_send_view_screen_test.mocks.dart | 5 +++ .../receive_view_screen_test.mocks.dart | 5 +++ .../send_view_screen_test.mocks.dart | 5 +++ .../wallet_view_screen_test.mocks.dart | 5 +++ .../managed_favorite_test.mocks.dart | 26 +++++++++++- .../table_view/table_view_row_test.mocks.dart | 26 +++++++++++- .../transaction_card_test.mocks.dart | 41 ++++++++++++++++++- ...et_info_row_balance_future_test.mocks.dart | 26 +++++++++++- .../wallet_info_row_test.mocks.dart | 26 +++++++++++- 38 files changed, 311 insertions(+), 11 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 80b3d8a98..25281a629 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -141,6 +141,8 @@ enum AddressType { return "Unknown"; case AddressType.nonWallet: return "Non wallet/unknown"; + case AddressType.ethereum: + return "Ethereum"; } } } diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart index 09cf78953..802910275 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart @@ -5,7 +5,6 @@ import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_or_restore_wallet_title.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_wallet_button_group.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index 1ddf2f666..1eb3b820f 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_tag.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart index adde8eb4a..981ddc69c 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings_view.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/db/hive/db.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; diff --git a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart index f3250fe23..f10d91555 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/appearance_settings.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index 44e3682c8..220525f73 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -1,7 +1,7 @@ import 'package:hive/hive.dart'; +import 'package:isar/isar.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:isar/isar.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 149d690b1..539f7b059 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i14; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i13; +import 'package:stackwallet/db/isar/main_db.dart' as _i13; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i11; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i10; import 'package:stackwallet/models/balance.dart' as _i12; @@ -1641,6 +1641,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i13.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2837,6 +2856,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart index a36548d2d..7d01dfbba 100644 --- a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart @@ -378,6 +378,11 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart index b78f3f23e..e82f51d64 100644 --- a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart @@ -339,6 +339,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart index 68697c039..d150ff23d 100644 --- a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart @@ -337,6 +337,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index d926e9e5b..7a0074677 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -732,6 +732,23 @@ class MockChangeNowAPI extends _i1.Mock implements _i12.ChangeNowAPI { )), ) as _i7.Future<_i2.ExchangeResponse>>); @override + _i7.Future<_i2.ExchangeResponse>> getCurrenciesV2() => + (super.noSuchMethod( + Invocation.method( + #getCurrenciesV2, + [], + ), + returnValue: + _i7.Future<_i2.ExchangeResponse>>.value( + _FakeExchangeResponse_0>( + this, + Invocation.method( + #getCurrenciesV2, + [], + ), + )), + ) as _i7.Future<_i2.ExchangeResponse>>); + @override _i7.Future<_i2.ExchangeResponse>> getPairedCurrencies({ required String? ticker, bool? fixedRate, diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index 439ecd5c9..1a535475b 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -646,6 +646,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart index d624d4171..4833c56ee 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart @@ -433,6 +433,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart index 70c08470b..33c356d74 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart @@ -433,6 +433,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart index 1f2176350..b19d16114 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart @@ -433,6 +433,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart index 7377d155e..34a6a36fe 100644 --- a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart index 2b31cb96c..f3967ade5 100644 --- a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart @@ -431,6 +431,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 54eec30cf..f1d3b0c4b 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -646,6 +646,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index e92f4d289..20ac25812 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -487,6 +487,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart index 13a6568c8..93d4f4874 100644 --- a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart index 82f4bcd3e..cfe9cd50e 100644 --- a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index fc4171b63..8e5c07700 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -423,6 +423,11 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index f990ec83a..c3d4a7279 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -423,6 +423,11 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart index 44dd68cee..8efc17d42 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart index a7764e149..5f5c9ee97 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart index 4c7dbfd7a..c39df921e 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart @@ -431,6 +431,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index e736cfdd8..6f3828684 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -688,6 +688,11 @@ class MockManager extends _i1.Mock implements _i15.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart index be1b24bcb..ea371aad5 100644 --- a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart @@ -431,6 +431,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart index 7cf8968b1..9b4056348 100644 --- a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart +++ b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart @@ -210,6 +210,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart index 81980dca0..b0e4bcc7d 100644 --- a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart @@ -209,6 +209,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart index 3055cf6c6..6c02b46e1 100644 --- a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart index 533578e6c..b3fcf528f 100644 --- a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart @@ -250,6 +250,11 @@ class MockManager extends _i1.Mock implements _i8.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart index f71a62d92..5cbb93bc1 100644 --- a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart @@ -210,6 +210,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index f8bda0845..5abf517b7 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; @@ -1432,6 +1432,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2411,6 +2430,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index 84cfac4c8..cc022b12e 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; @@ -1419,6 +1419,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { returnValueForMissingStub: _i21.Future.value(), ) as _i21.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i21.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i21.Future.value(), + returnValueForMissingStub: _i21.Future.value(), + ) as _i21.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2136,6 +2155,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 51afbec22..60a51073a 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -10,7 +10,7 @@ import 'package:decimal/decimal.dart' as _i14; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i13; +import 'package:stackwallet/db/isar/main_db.dart' as _i13; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i12; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i11; import 'package:stackwallet/models/balance.dart' as _i9; @@ -556,6 +556,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -1956,6 +1961,25 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { returnValueForMissingStub: _i18.Future.value(), ) as _i18.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i18.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i18.Future.value(), + returnValueForMissingStub: _i18.Future.value(), + ) as _i18.Future); + @override void initWalletDB({_i13.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2532,6 +2556,21 @@ class MockPriceService extends _i1.Mock implements _i28.PriceService { ), ) as _i15.Tuple2<_i14.Decimal, double>); @override + _i15.Tuple2<_i14.Decimal, double> getTokenPrice(String? contractAddress) => + (super.noSuchMethod( + Invocation.method( + #getTokenPrice, + [contractAddress], + ), + returnValue: _FakeTuple2_13<_i14.Decimal, double>( + this, + Invocation.method( + #getTokenPrice, + [contractAddress], + ), + ), + ) as _i15.Tuple2<_i14.Decimal, double>); + @override _i18.Future updatePrice() => (super.noSuchMethod( Invocation.method( #updatePrice, diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index 028cd047e..5c2641dbb 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; @@ -1431,6 +1431,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2348,6 +2367,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 8b75cb42e..1f786dbab 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; @@ -1431,6 +1431,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2348,6 +2367,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, From af2c674809098ba038599a8d0bd987da55947775 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 23 Mar 2023 18:26:55 -0600 Subject: [PATCH 111/208] remove unused variable --- lib/services/ethereum/ethereum_api.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 379b276d9..f09f641ea 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -34,8 +34,6 @@ class EthereumResponse { abstract class EthereumAPI { static String get stackBaseServer => DefaultNodes.ethereum.host; - static String stackURI = "$stackBaseServer/eth/mainnet/api"; - // static const etherscanApi = // "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update From 361d3a8095cca7655c2485bd40bc1722dd811bab Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 08:37:47 -0600 Subject: [PATCH 112/208] get token info fix --- lib/services/ethereum/ethereum_api.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index f09f641ea..89543fde8 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -370,9 +370,9 @@ abstract class EthereumAPI { ); if (response.statusCode == 200) { - final json = jsonDecode(response.body); - if (json["message"] == "OK") { - final map = Map.from(json["result"] as Map); + final json = jsonDecode(response.body) as Map; + if (json["data"] is List) { + final map = Map.from(json["data"].first as Map); EthContract? token; if (map["isErc20"] == true) { token = EthContract( @@ -400,7 +400,7 @@ abstract class EthereumAPI { null, ); } else { - throw EthApiException(json["message"] as String); + throw EthApiException(response.body); } } else { throw EthApiException( From e53683ac68a983ec4fc4e78c4ef68ed9089b6a5b Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 10:11:18 -0600 Subject: [PATCH 113/208] WIP token tx parsing --- lib/dto/ethereum/eth_token_tx_dto.dart | 63 ++++++++----- .../ethereum/ethereum_token_service.dart | 93 ++++++++++++------- 2 files changed, 97 insertions(+), 59 deletions(-) diff --git a/lib/dto/ethereum/eth_token_tx_dto.dart b/lib/dto/ethereum/eth_token_tx_dto.dart index 2d2ddfd1f..e7329ef79 100644 --- a/lib/dto/ethereum/eth_token_tx_dto.dart +++ b/lib/dto/ethereum/eth_token_tx_dto.dart @@ -21,26 +21,33 @@ class EthTokenTxDto { required this.transactionIndex, }); - EthTokenTxDto.fromMap(Map json) - : address = json['address'] as String, - blockNumber = json['blockNumber'] as int, - logIndex = json['logIndex'] as int, - topics = List.from(json['topics'] as List), - data = json['data'] as String, - articulatedLog = ArticulatedLog.fromJson(json['articulatedLog']), - compressedLog = json['compressedLog'] as String, - transactionHash = json['transactionHash'] as String, - transactionIndex = json['transactionIndex'] as int; + EthTokenTxDto.fromMap(Map map) + : address = map['address'] as String, + blockNumber = map['blockNumber'] as int, + logIndex = map['logIndex'] as int, + topics = List.from(map['topics'] as List), + data = map['data'] as String, + articulatedLog = map['articulatedLog'] == null + ? null + : ArticulatedLog.fromMap( + Map.from( + map['articulatedLog'] as Map, + ), + ), + compressedLog = map['compressedLog'] as String, + transactionHash = map['transactionHash'] as String, + transactionIndex = map['transactionIndex'] as int; final String address; final int blockNumber; final int logIndex; final List topics; final String data; - final ArticulatedLog articulatedLog; + final ArticulatedLog? articulatedLog; final String compressedLog; final String transactionHash; final int transactionIndex; + EthTokenTxDto copyWith({ String? address, int? blockNumber, @@ -63,19 +70,25 @@ class EthTokenTxDto { transactionHash: transactionHash ?? this.transactionHash, transactionIndex: transactionIndex ?? this.transactionIndex, ); - Map toJson() { + + Map toMap() { final map = {}; map['address'] = address; map['blockNumber'] = blockNumber; map['logIndex'] = logIndex; map['topics'] = topics; map['data'] = data; - map['articulatedLog'] = articulatedLog.toJson(); + map['articulatedLog'] = articulatedLog?.toMap(); map['compressedLog'] = compressedLog; map['transactionHash'] = transactionHash; map['transactionIndex'] = transactionIndex; return map; } + + @override + String toString() { + return toMap().toString(); + } } /// name : "Transfer" @@ -87,9 +100,13 @@ class ArticulatedLog { required this.inputs, }); - ArticulatedLog.fromJson(dynamic json) - : name = json['name'] as String, - inputs = Inputs.fromJson(json['inputs']); + ArticulatedLog.fromMap(Map map) + : name = map['name'] as String, + inputs = Inputs.fromMap( + Map.from( + map['inputs'] as Map, + ), + ); final String name; final Inputs inputs; @@ -103,10 +120,10 @@ class ArticulatedLog { inputs: inputs ?? this.inputs, ); - Map toJson() { + Map toMap() { final map = {}; map['name'] = name; - map['inputs'] = inputs.toJson(); + map['inputs'] = inputs.toMap(); return map; } } @@ -122,10 +139,10 @@ class Inputs { required this.to, }); - Inputs.fromJson(dynamic json) - : amount = json['_amount'] as String, - from = json['_from'] as String, - to = json['_to'] as String; + Inputs.fromMap(Map map) + : amount = map['_amount'] as String, + from = map['_from'] as String, + to = map['_to'] as String; final String amount; final String from; @@ -142,7 +159,7 @@ class Inputs { to: to ?? this.to, ); - Map toJson() { + Map toMap() { final map = {}; map['_amount'] = amount; map['_from'] = from; diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 6cc8b2af2..ad4f511ae 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -25,6 +25,7 @@ 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/extensions/extensions.dart'; import 'package:stackwallet/utilities/extensions/impl/contract_abi.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; @@ -340,6 +341,9 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { .sortByTimestampDesc() .findAll(); + String _addressFromTopic(String topic) => + checksumEthereumAddress("0x${topic.substring(topic.length - 40)}"); + Future _refreshTransactions() async { String addressString = checksumEthereumAddress(await currentReceivingAddress); @@ -382,12 +386,47 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { final List> txnsData = []; for (final tuple in data) { - bool isIncoming; - if (checksumEthereumAddress(tuple.item1.articulatedLog.inputs.from) == - addressString) { - isIncoming = false; + int amount; + String fromAddress, toAddress; + if (tuple.item1.articulatedLog == null) { + if (tuple.item1.topics.length != 3) { + throw Exception("Topics length != 3 for " + "${ethWallet.walletName} ${ethWallet.walletId}: " + "${tuple.item1.toString()}"); + } + amount = tuple.item1.data.toBigIntFromHex.toInt(); + + fromAddress = _addressFromTopic( + tuple.item1.topics[1], + ); + toAddress = _addressFromTopic( + tuple.item1.topics[2], + ); } else { + amount = int.parse( + tuple.item1.articulatedLog!.inputs.amount, + ); + fromAddress = checksumEthereumAddress( + tuple.item1.articulatedLog!.inputs.from, + ); + toAddress = checksumEthereumAddress( + tuple.item1.articulatedLog!.inputs.to, + ); + } + + bool isIncoming; + bool isSentToSelf = false; + if (fromAddress == addressString) { + isIncoming = false; + if (toAddress == addressString) { + isSentToSelf = true; + } + } else if (toAddress == addressString) { isIncoming = true; + } else { + throw Exception("Unknown token transaction found for " + "${ethWallet.walletName} ${ethWallet.walletId}: " + "${tuple.item1.toString()}"); } final txn = Transaction( @@ -396,7 +435,7 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { timestamp: tuple.item2.timestamp, type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, subType: TransactionSubType.ethToken, - amount: int.parse(tuple.item1.articulatedLog.inputs.amount), + amount: amount, fee: tuple.item2.gasUsed * tuple.item2.gasPrice.toInt(), height: tuple.item1.blockNumber, isCancelled: false, @@ -410,39 +449,21 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { Address? transactionAddress = await ethWallet.db .getAddresses(ethWallet.walletId) .filter() - .valueEqualTo(addressString) + .valueEqualTo(toAddress) .findFirst(); - if (transactionAddress == null) { - if (isIncoming) { - transactionAddress = Address( - walletId: ethWallet.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; - - transactionAddress = Address( - walletId: ethWallet.walletId, - value: addressString, - publicKey: [], - derivationIndex: isSentToSelf ? 0 : -1, - derivationPath: isSentToSelf - ? (DerivationPath()..value = "$hdPathEthereum/0") - : null, - type: AddressType.ethereum, - subType: isSentToSelf - ? AddressSubType.receiving - : AddressSubType.nonWallet, - ); - } - } + transactionAddress ??= Address( + walletId: ethWallet.walletId, + value: toAddress, + publicKey: [], + derivationIndex: isSentToSelf ? 0 : -1, + derivationPath: isSentToSelf + ? (DerivationPath()..value = "$hdPathEthereum/0") + : null, + type: AddressType.ethereum, + subType: + isSentToSelf ? AddressSubType.receiving : AddressSubType.nonWallet, + ); txnsData.add(Tuple2(txn, transactionAddress)); } From f44774745e21a6964c27a67f78ebc4a9f2f113d2 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 11:08:02 -0600 Subject: [PATCH 114/208] WIP amount class --- lib/utilities/amount.dart | 43 +++++++++++++++++ pubspec.lock | 2 +- pubspec.yaml | 1 + test/utilities/amount_test.dart | 81 +++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 lib/utilities/amount.dart create mode 100644 test/utilities/amount_test.dart diff --git a/lib/utilities/amount.dart b/lib/utilities/amount.dart new file mode 100644 index 000000000..6955e2489 --- /dev/null +++ b/lib/utilities/amount.dart @@ -0,0 +1,43 @@ +import 'package:decimal/decimal.dart'; + +final _ten = BigInt.from(10); + +class Amount { + Amount({ + required BigInt rawValue, + required this.fractionDigits, + }) : assert(fractionDigits >= 0), + _value = rawValue; + + /// truncate double value to [fractionDigits] places + Amount.fromDouble(double amount, {required this.fractionDigits}) + : assert(fractionDigits >= 0), + _value = + Decimal.parse(amount.toString()).shift(fractionDigits).toBigInt(); + + /// truncate decimal value to [fractionDigits] places + Amount.fromDecimal(Decimal amount, {required this.fractionDigits}) + : assert(fractionDigits >= 0), + _value = amount.shift(fractionDigits).toBigInt(); + + // =========================================================================== + // ======= Instance properties =============================================== + + final int fractionDigits; + final BigInt _value; + + // =========================================================================== + // ======= Getters =========================================================== + + /// raw base value + BigInt get raw => _value; + + /// actual decimal vale represented + Decimal get decimal => + (Decimal.fromBigInt(_value) / _ten.pow(fractionDigits).toDecimal()) + .toDecimal(scaleOnInfinitePrecision: fractionDigits); + + /// convenience getter + @Deprecated("provided for convenience only. Use fractionDigits instead.") + int get decimals => fractionDigits; +} diff --git a/pubspec.lock b/pubspec.lock index 6aed3f88c..704bbe10e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1358,7 +1358,7 @@ packages: source: hosted version: "4.0.0" rational: - dependency: transitive + dependency: "direct main" description: name: rational sha256: ba58e9e18df9abde280e8b10051e4bce85091e41e8e7e411b6cde2e738d357cf diff --git a/pubspec.yaml b/pubspec.yaml index 1db8de99b..fc75379ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -150,6 +150,7 @@ dependencies: dart_bs58: ^1.0.1 dart_bs58check: ^3.0.2 hex: ^0.2.0 + rational: ^2.2.2 dev_dependencies: flutter_test: diff --git a/test/utilities/amount_test.dart b/test/utilities/amount_test.dart new file mode 100644 index 000000000..0274ea2bb --- /dev/null +++ b/test/utilities/amount_test.dart @@ -0,0 +1,81 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/amount.dart'; + +void main() { + test("Basic Amount Constructor tests", () { + Amount amount = Amount(rawValue: BigInt.two, fractionDigits: 0); + expect(amount.fractionDigits, 0); + expect(amount.raw, BigInt.two); + expect(amount.decimal, Decimal.fromInt(2)); + + amount = Amount(rawValue: BigInt.two, fractionDigits: 2); + expect(amount.fractionDigits, 2); + expect(amount.raw, BigInt.two); + expect(amount.decimal, Decimal.parse("0.02")); + + amount = Amount(rawValue: BigInt.from(123456789), fractionDigits: 7); + expect(amount.fractionDigits, 7); + expect(amount.raw, BigInt.from(123456789)); + expect(amount.decimal, Decimal.parse("12.3456789")); + + bool didThrow = false; + try { + amount = Amount(rawValue: BigInt.one, fractionDigits: -1); + } catch (_) { + didThrow = true; + } + expect(didThrow, true); + }); + + test("Named fromDouble Amount Constructor tests", () { + Amount amount = Amount.fromDouble(2.0, fractionDigits: 0); + expect(amount.fractionDigits, 0); + expect(amount.raw, BigInt.two); + expect(amount.decimal, Decimal.fromInt(2)); + + amount = Amount.fromDouble(2.0, fractionDigits: 2); + expect(amount.fractionDigits, 2); + expect(amount.raw, BigInt.from(200)); + expect(amount.decimal, Decimal.fromInt(2)); + + amount = Amount.fromDouble(0.0123456789, fractionDigits: 7); + expect(amount.fractionDigits, 7); + expect(amount.raw, BigInt.from(123456)); + expect(amount.decimal, Decimal.parse("0.0123456")); + + bool didThrow = false; + try { + amount = Amount.fromDouble(2.0, fractionDigits: -1); + } catch (_) { + didThrow = true; + } + expect(didThrow, true); + }); + + test("Named fromDecimal Amount Constructor tests", () { + Amount amount = Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 0); + expect(amount.fractionDigits, 0); + expect(amount.raw, BigInt.two); + expect(amount.decimal, Decimal.fromInt(2)); + + amount = Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 2); + expect(amount.fractionDigits, 2); + expect(amount.raw, BigInt.from(200)); + expect(amount.decimal, Decimal.fromInt(2)); + + amount = + Amount.fromDecimal(Decimal.parse("0.0123456789"), fractionDigits: 7); + expect(amount.fractionDigits, 7); + expect(amount.raw, BigInt.from(123456)); + expect(amount.decimal, Decimal.parse("0.0123456")); + + bool didThrow = false; + try { + amount = Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: -1); + } catch (_) { + didThrow = true; + } + expect(didThrow, true); + }); +} From 3ab605c065d8b47c1baab0b4859f596e7422810c Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 15:30:29 -0600 Subject: [PATCH 115/208] amount serialization --- lib/utilities/amount.dart | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/utilities/amount.dart b/lib/utilities/amount.dart index 6955e2489..74d37b6b5 100644 --- a/lib/utilities/amount.dart +++ b/lib/utilities/amount.dart @@ -1,8 +1,11 @@ +import 'dart:convert'; + import 'package:decimal/decimal.dart'; +import 'package:equatable/equatable.dart'; final _ten = BigInt.from(10); -class Amount { +class Amount implements Equatable { Amount({ required BigInt rawValue, required this.fractionDigits, @@ -40,4 +43,38 @@ class Amount { /// convenience getter @Deprecated("provided for convenience only. Use fractionDigits instead.") int get decimals => fractionDigits; + + Map toMap() { + // =========================================================================== + // ======= Serialization ===================================================== + + return {"raw": raw.toString(), "fractionDigits": fractionDigits}; + } + + String toJsonString() { + return jsonEncode(toMap()); + } + + // =========================================================================== + // ======= Deserialization =================================================== + + static Amount fromSerializedJsonString(String json) { + final map = jsonDecode(json) as Map; + return Amount( + rawValue: BigInt.parse(map["raw"] as String), + fractionDigits: map["fractionDigits"] as int, + ); + } + + // =========================================================================== + // ======= Overrides ========================================================= + + @override + String toString() => "Amount($raw, $fractionDigits)"; + + @override + List get props => [fractionDigits, _value]; + + @override + bool? get stringify => false; } From b2b9accee121f85a5ebed93a6cf3ed6cc710a1d0 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 15:31:05 -0600 Subject: [PATCH 116/208] add serialized amount string to transaction --- .../models/blockchain_data/transaction.dart | 16 ++++++++ .../coins/bitcoin/bitcoin_wallet.dart | 5 +++ .../coins/bitcoincash/bitcoincash_wallet.dart | 9 ++++ .../coins/dogecoin/dogecoin_wallet.dart | 5 +++ .../coins/epiccash/epiccash_wallet.dart | 5 +++ .../coins/ethereum/ethereum_wallet.dart | 5 +++ lib/services/coins/firo/firo_wallet.dart | 41 +++++++++++++++---- .../coins/litecoin/litecoin_wallet.dart | 5 +++ lib/services/coins/monero/monero_wallet.dart | 5 +++ .../coins/namecoin/namecoin_wallet.dart | 5 +++ .../coins/particl/particl_wallet.dart | 10 +++++ .../coins/wownero/wownero_wallet.dart | 8 +++- .../ethereum/ethereum_token_service.dart | 5 +++ lib/services/mixins/electrum_x_parsing.dart | 5 +++ lib/utilities/db_version_migration.dart | 5 +++ 15 files changed, 124 insertions(+), 10 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index 538504748..bf6faac8c 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -5,6 +5,7 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/input.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/output.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:tuple/tuple.dart'; part 'transaction.g.dart'; @@ -18,6 +19,7 @@ class Transaction { required this.type, required this.subType, required this.amount, + required this.amountString, required this.fee, required this.height, required this.isCancelled, @@ -35,6 +37,7 @@ class Transaction { TransactionType? type, TransactionSubType? subType, int? amount, + String? amountString, int? fee, int? height, bool? isCancelled, @@ -54,6 +57,7 @@ class Transaction { type: type ?? this.type, subType: subType ?? this.subType, amount: amount ?? this.amount, + amountString: amountString ?? this.amountString, fee: fee ?? this.fee, height: height ?? this.height, isCancelled: isCancelled ?? this.isCancelled, @@ -86,6 +90,8 @@ class Transaction { late final int amount; + late String? amountString; + late final int fee; late final int? height; @@ -105,6 +111,13 @@ class Transaction { @Backlink(to: "transactions") final address = IsarLink

(); + @ignore + Amount? _cachedAmount; + + @ignore + Amount get realAmount => + _cachedAmount ??= Amount.fromSerializedJsonString(amountString!); + int getConfirmations(int currentChainHeight) { if (height == null || height! <= 0) return 0; return max(0, currentChainHeight - (height! - 1)); @@ -124,6 +137,7 @@ class Transaction { "type: ${type.name}, " "subType: ${subType.name}, " "amount: $amount, " + "amountString: $amountString, " "fee: $fee, " "height: $height, " "isCancelled: $isCancelled, " @@ -143,6 +157,7 @@ class Transaction { "type": type.name, "subType": subType.name, "amount": amount, + "amountString": amountString, "fee": fee, "height": height, "isCancelled": isCancelled, @@ -168,6 +183,7 @@ class Transaction { type: TransactionType.values.byName(json["type"] as String), subType: TransactionSubType.values.byName(json["subType"] as String), amount: json["amount"] as int, + amountString: json["amountString"] as String, fee: json["fee"] as int, height: json["height"] as int?, isCancelled: json["isCancelled"] as bool, diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index ed969bf5d..c622479fb 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -34,6 +34,7 @@ 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/address_utils.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -1335,6 +1336,10 @@ class BitcoinWallet extends CoinServiceAPI type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: txData["recipientAmt"] as int, + amountString: Amount( + rawValue: BigInt.from(txData["recipientAmt"] as int), + fractionDigits: coin.decimals, + ).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index bd934552f..d954371d7 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -31,6 +31,7 @@ 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/address_utils.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -1250,6 +1251,10 @@ class BitcoinCashWallet extends CoinServiceAPI type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: txData["recipientAmt"] as int, + amountString: Amount( + rawValue: BigInt.from(txData["recipientAmt"] as int), + fractionDigits: coin.decimals, + ).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, @@ -2309,6 +2314,10 @@ class BitcoinCashWallet extends CoinServiceAPI type: type, subType: isar_models.TransactionSubType.none, amount: amount, + amountString: Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.decimals, + ).toJsonString(), fee: fee, height: txData["height"] as int?, isCancelled: false, diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 1c21407c8..232b47889 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -33,6 +33,7 @@ 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/address_utils.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -1117,6 +1118,10 @@ class DogecoinWallet extends CoinServiceAPI type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: txData["recipientAmt"] as int, + amountString: Amount( + rawValue: BigInt.from(txData["recipientAmt"] as int), + fractionDigits: coin.decimals, + ).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index d23c349d4..a2f8a8713 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -27,6 +27,7 @@ import 'package:stackwallet/services/mixins/epic_cash_hive.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/utilities/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_epicboxes.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; @@ -1688,6 +1689,10 @@ class EpicCashWallet extends CoinServiceAPI : isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: amt, + amountString: Amount( + rawValue: BigInt.from(amt), + fractionDigits: coin.decimals, + ).toJsonString(), fee: (tx["fee"] == null) ? 0 : int.parse(tx["fee"] as String), height: height, isCancelled: tx["tx_type"] == "TxSentCancelled" || diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 636c12c9c..5fffad83c 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -22,6 +22,7 @@ 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/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; @@ -881,6 +882,10 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { isIncoming ? TransactionType.incoming : TransactionType.outgoing, subType: TransactionSubType.none, amount: transactionAmount, + amountString: Amount( + rawValue: BigInt.from(transactionAmount), + fractionDigits: coin.decimals, + ).toJsonString(), fee: txFee, height: height, isCancelled: txFailed, diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 83d718fb0..6a65d9f18 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -32,6 +32,7 @@ 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/address_utils.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -472,6 +473,10 @@ Future> staticProcessRestore( type: element.type, subType: isar_models.TransactionSubType.mint, amount: element.amount, + amountString: Amount( + rawValue: BigInt.from(element.amount), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: sharedFee, height: element.height, isCancelled: false, @@ -899,6 +904,10 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: txData["recipientAmt"] as int, + amountString: Amount( + rawValue: BigInt.from(txData["recipientAmt"] as int), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, @@ -3072,6 +3081,11 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { } await firoUpdateLelantusCoins(coins); + final amount = Format.decimalAmountToSatoshis( + Decimal.parse(transactionInfo["amount"].toString()), + coin, + ); + // add the send transaction final transaction = isar_models.Transaction( walletId: walletId, @@ -3086,10 +3100,11 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { : transactionInfo["subType"] == "join" ? isar_models.TransactionSubType.join : isar_models.TransactionSubType.none, - amount: Format.decimalAmountToSatoshis( - Decimal.parse(transactionInfo["amount"].toString()), - coin, - ), + amount: amount, + amountString: Amount( + rawValue: BigInt.from(amount), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: Format.decimalAmountToSatoshis( Decimal.parse(transactionInfo["fees"].toString()), coin, @@ -3649,6 +3664,10 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { type: type, subType: subType, amount: amount, + amountString: Amount( + rawValue: BigInt.from(amount), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: fees, height: txObject["height"] as int? ?? 0, isCancelled: false, @@ -4969,6 +4988,11 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { tx["address"] = tx["vout"][sendIndex]["scriptPubKey"]["addresses"][0]; tx["fees"] = tx["vin"][0]["nFees"]; + final amount = Format.decimalAmountToSatoshis( + Decimal.parse(tx["amount"].toString()), + coin, + ); + final txn = isar_models.Transaction( walletId: walletId, txid: tx["txid"] as String, @@ -4976,10 +5000,11 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { (DateTime.now().millisecondsSinceEpoch ~/ 1000), type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.join, - amount: Format.decimalAmountToSatoshis( - Decimal.parse(tx["amount"].toString()), - coin, - ), + amount: amount, + amountString: Amount( + rawValue: BigInt.from(amount), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: Format.decimalAmountToSatoshis( Decimal.parse(tx["fees"].toString()), coin, diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 542c88b60..e5207b562 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -31,6 +31,7 @@ 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/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -1242,6 +1243,10 @@ class LitecoinWallet extends CoinServiceAPI type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: txData["recipientAmt"] as int, + amountString: Amount( + rawValue: BigInt.from(txData["recipientAmt"] as int), + fractionDigits: coin.decimals, + ).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 11a0aacd2..c8362426c 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -38,6 +38,7 @@ 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/utilities/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -926,6 +927,10 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { type: type, subType: isar_models.TransactionSubType.none, amount: tx.value.amount ?? 0, + amountString: Amount( + rawValue: BigInt.from(tx.value.amount ?? 0), + fractionDigits: coin.decimals, + ).toJsonString(), fee: tx.value.fee ?? 0, height: tx.value.height, isCancelled: false, diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 400726329..17007da2c 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -31,6 +31,7 @@ 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/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -1232,6 +1233,10 @@ class NamecoinWallet extends CoinServiceAPI type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: txData["recipientAmt"] as int, + amountString: Amount( + rawValue: BigInt.from(txData["recipientAmt"] as int), + fractionDigits: coin.decimals, + ).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 692ad7e62..4a467d119 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -44,6 +44,8 @@ import 'package:stackwallet/utilities/prefs.dart'; import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; +import '../../../utilities/amount.dart'; + const int MINIMUM_CONFIRMATIONS = 1; const int DUST_LIMIT = 294; @@ -1160,6 +1162,10 @@ class ParticlWallet extends CoinServiceAPI type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: txData["recipientAmt"] as int, + amountString: Amount( + rawValue: BigInt.from(txData["recipientAmt"] as int), + fractionDigits: coin.decimals, + ).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, @@ -2351,6 +2357,10 @@ class ParticlWallet extends CoinServiceAPI type: type, subType: isar_models.TransactionSubType.none, amount: amount, + amountString: Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.decimals, + ).toJsonString(), fee: fee, height: txObject["height"] as int, inputs: inputs, diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 7dec040e1..7eb5ee8b1 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -40,6 +40,7 @@ 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/utilities/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -204,8 +205,7 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { try { aprox = (await prepareSend( // This address is only used for getting an approximate fee, never for sending - address: - "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", + address: "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", satoshiAmount: satoshiAmount, args: {"feeRate": feeRateType}))['fee']; await Future.delayed(const Duration(milliseconds: 500)); @@ -1006,6 +1006,10 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { type: type, subType: isar_models.TransactionSubType.none, amount: tx.value.amount ?? 0, + amountString: Amount( + rawValue: BigInt.from(tx.value.amount ?? 0), + fractionDigits: coin.decimals, + ).toJsonString(), fee: tx.value.fee ?? 0, height: tx.value.height, isCancelled: false, diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index ad4f511ae..b2caa6cc6 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -21,6 +21,7 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/eth_token_cache.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/amount.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'; @@ -436,6 +437,10 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, subType: TransactionSubType.ethToken, amount: amount, + amountString: Amount( + rawValue: BigInt.from(amount), + fractionDigits: tokenContract.decimals, + ).toJsonString(), fee: tuple.item2.gasUsed * tuple.item2.gasPrice.toInt(), height: tuple.item1.blockNumber, isCancelled: false, diff --git a/lib/services/mixins/electrum_x_parsing.dart b/lib/services/mixins/electrum_x_parsing.dart index 440a1981a..5432ea496 100644 --- a/lib/services/mixins/electrum_x_parsing.dart +++ b/lib/services/mixins/electrum_x_parsing.dart @@ -4,6 +4,7 @@ import 'package:bip47/src/util.dart'; import 'package:decimal/decimal.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:tuple/tuple.dart'; @@ -220,6 +221,10 @@ mixin ElectrumXParsing { type: type, subType: txSubType, amount: amount, + amountString: Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.decimals, + ).toJsonString(), fee: fee, height: txData["height"] as int?, isCancelled: false, diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index 220525f73..569083205 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/wallets_service.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -327,6 +328,10 @@ class DbVersionMigrator with WalletDB { : isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: tx.amount, + amountString: Amount( + rawValue: BigInt.from(tx.amount), + fractionDigits: info.coin.decimals, + ).toJsonString(), fee: tx.fees, height: tx.height, isCancelled: tx.isCancelled, From 66919ec4b8ee247e0698c5adca3c05710304d1a8 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 15:46:12 -0600 Subject: [PATCH 117/208] migrate serialized amount string in transaction --- lib/utilities/constants.dart | 2 +- lib/utilities/db_version_migration.dart | 50 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index c11dbcd01..562d44047 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -42,7 +42,7 @@ abstract class Constants { // Enable Logger.print statements static const bool disableLogger = false; - static const int currentHiveDbVersion = 7; + static const int currentHiveDbVersion = 8; static const int rescanV1 = 1; diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index 569083205..5ecf0746a 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -279,6 +279,17 @@ class DbVersionMigrator with WalletDB { // try to continue migrating return await migrate(7, secureStore: secureStore); + case 7: + // migrate + await _v7(secureStore); + + // update version + await DB.instance.put( + boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 7); + + // try to continue migrating + return await migrate(8, secureStore: secureStore); + default: // finally return return; @@ -396,4 +407,43 @@ class DbVersionMigrator with WalletDB { } } } + + Future _v7(SecureStorageInterface secureStore) async { + await Hive.openBox(DB.boxNameAllWalletsData); + final walletsService = WalletsService(secureStorageInterface: secureStore); + final walletInfoList = await walletsService.walletNames; + await MainDB.instance.initMainDB(); + + for (final walletId in walletInfoList.keys) { + final info = walletInfoList[walletId]!; + assert(info.walletId == walletId); + + final count = await MainDB.instance.getTransactions(walletId).count(); + + for (var i = 0; i < count; i += 50) { + final txns = await MainDB.instance + .getTransactions(walletId) + .offset(i) + .limit(50) + .findAll(); + + // migrate amount to serialized amount string + final txnsData = txns + .map( + (tx) => Tuple2( + tx + ..amountString = Amount( + rawValue: BigInt.from(tx.amount), + fractionDigits: info.coin.decimals, + ).toJsonString(), + tx.address.value, + ), + ) + .toList(); + + // update db records + await MainDB.instance.addNewTransactionData(txnsData, walletId); + } + } + } } From f7b2f01f0189da6f6090bc58e559c4076314b95d Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 15:55:04 -0600 Subject: [PATCH 118/208] update generated transaction part --- .../models/blockchain_data/transaction.g.dart | 310 +++++++++++++++--- 1 file changed, 259 insertions(+), 51 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index b70e0eec7..a511d01f5 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -22,72 +22,77 @@ const TransactionSchema = CollectionSchema( name: r'amount', type: IsarType.long, ), - r'fee': PropertySchema( + r'amountString': PropertySchema( id: 1, + name: r'amountString', + type: IsarType.string, + ), + r'fee': PropertySchema( + id: 2, name: r'fee', type: IsarType.long, ), r'height': PropertySchema( - id: 2, + id: 3, name: r'height', type: IsarType.long, ), r'inputs': PropertySchema( - id: 3, + id: 4, name: r'inputs', type: IsarType.objectList, target: r'Input', ), r'isCancelled': PropertySchema( - id: 4, + id: 5, name: r'isCancelled', type: IsarType.bool, ), r'isLelantus': PropertySchema( - id: 5, + id: 6, name: r'isLelantus', type: IsarType.bool, ), r'otherData': PropertySchema( - id: 6, + id: 7, name: r'otherData', type: IsarType.string, ), r'outputs': PropertySchema( - id: 7, + id: 8, name: r'outputs', type: IsarType.objectList, target: r'Output', ), r'slateId': PropertySchema( - id: 8, + id: 9, name: r'slateId', type: IsarType.string, ), r'subType': PropertySchema( - id: 9, + id: 10, name: r'subType', type: IsarType.byte, enumMap: _TransactionsubTypeEnumValueMap, ), r'timestamp': PropertySchema( - id: 10, + id: 11, name: r'timestamp', type: IsarType.long, ), r'txid': PropertySchema( - id: 11, + id: 12, name: r'txid', type: IsarType.string, ), r'type': PropertySchema( - id: 12, + id: 13, name: r'type', type: IsarType.byte, enumMap: _TransactiontypeEnumValueMap, ), r'walletId': PropertySchema( - id: 13, + id: 14, name: r'walletId', type: IsarType.string, ) @@ -165,6 +170,12 @@ int _transactionEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; + { + final value = object.amountString; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.inputs.length * 3; { final offsets = allOffsets[Input]!; @@ -205,29 +216,30 @@ void _transactionSerialize( Map> allOffsets, ) { writer.writeLong(offsets[0], object.amount); - writer.writeLong(offsets[1], object.fee); - writer.writeLong(offsets[2], object.height); + writer.writeString(offsets[1], object.amountString); + writer.writeLong(offsets[2], object.fee); + writer.writeLong(offsets[3], object.height); writer.writeObjectList( - offsets[3], + offsets[4], allOffsets, InputSchema.serialize, object.inputs, ); - writer.writeBool(offsets[4], object.isCancelled); - writer.writeBool(offsets[5], object.isLelantus); - writer.writeString(offsets[6], object.otherData); + writer.writeBool(offsets[5], object.isCancelled); + writer.writeBool(offsets[6], object.isLelantus); + writer.writeString(offsets[7], object.otherData); writer.writeObjectList( - offsets[7], + offsets[8], allOffsets, OutputSchema.serialize, object.outputs, ); - writer.writeString(offsets[8], object.slateId); - writer.writeByte(offsets[9], object.subType.index); - writer.writeLong(offsets[10], object.timestamp); - writer.writeString(offsets[11], object.txid); - writer.writeByte(offsets[12], object.type.index); - writer.writeString(offsets[13], object.walletId); + writer.writeString(offsets[9], object.slateId); + writer.writeByte(offsets[10], object.subType.index); + writer.writeLong(offsets[11], object.timestamp); + writer.writeString(offsets[12], object.txid); + writer.writeByte(offsets[13], object.type.index); + writer.writeString(offsets[14], object.walletId); } Transaction _transactionDeserialize( @@ -238,34 +250,35 @@ Transaction _transactionDeserialize( ) { final object = Transaction( amount: reader.readLong(offsets[0]), - fee: reader.readLong(offsets[1]), - height: reader.readLongOrNull(offsets[2]), + amountString: reader.readStringOrNull(offsets[1]), + fee: reader.readLong(offsets[2]), + height: reader.readLongOrNull(offsets[3]), inputs: reader.readObjectList( - offsets[3], + offsets[4], InputSchema.deserialize, allOffsets, Input(), ) ?? [], - isCancelled: reader.readBool(offsets[4]), - isLelantus: reader.readBoolOrNull(offsets[5]), - otherData: reader.readStringOrNull(offsets[6]), + isCancelled: reader.readBool(offsets[5]), + isLelantus: reader.readBoolOrNull(offsets[6]), + otherData: reader.readStringOrNull(offsets[7]), outputs: reader.readObjectList( - offsets[7], + offsets[8], OutputSchema.deserialize, allOffsets, Output(), ) ?? [], - slateId: reader.readStringOrNull(offsets[8]), + slateId: reader.readStringOrNull(offsets[9]), subType: - _TransactionsubTypeValueEnumMap[reader.readByteOrNull(offsets[9])] ?? + _TransactionsubTypeValueEnumMap[reader.readByteOrNull(offsets[10])] ?? TransactionSubType.none, - timestamp: reader.readLong(offsets[10]), - txid: reader.readString(offsets[11]), - type: _TransactiontypeValueEnumMap[reader.readByteOrNull(offsets[12])] ?? + timestamp: reader.readLong(offsets[11]), + txid: reader.readString(offsets[12]), + type: _TransactiontypeValueEnumMap[reader.readByteOrNull(offsets[13])] ?? TransactionType.outgoing, - walletId: reader.readString(offsets[13]), + walletId: reader.readString(offsets[14]), ); object.id = id; return object; @@ -281,10 +294,12 @@ P _transactionDeserializeProp

( case 0: return (reader.readLong(offset)) as P; case 1: - return (reader.readLong(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 2: - return (reader.readLongOrNull(offset)) as P; + return (reader.readLong(offset)) as P; case 3: + return (reader.readLongOrNull(offset)) as P; + case 4: return (reader.readObjectList( offset, InputSchema.deserialize, @@ -292,13 +307,13 @@ P _transactionDeserializeProp

( Input(), ) ?? []) as P; - case 4: - return (reader.readBool(offset)) as P; case 5: - return (reader.readBoolOrNull(offset)) as P; + return (reader.readBool(offset)) as P; case 6: - return (reader.readStringOrNull(offset)) as P; + return (reader.readBoolOrNull(offset)) as P; case 7: + return (reader.readStringOrNull(offset)) as P; + case 8: return (reader.readObjectList( offset, OutputSchema.deserialize, @@ -306,19 +321,19 @@ P _transactionDeserializeProp

( Output(), ) ?? []) as P; - case 8: - return (reader.readStringOrNull(offset)) as P; case 9: + return (reader.readStringOrNull(offset)) as P; + case 10: return (_TransactionsubTypeValueEnumMap[reader.readByteOrNull(offset)] ?? TransactionSubType.none) as P; - case 10: - return (reader.readLong(offset)) as P; case 11: - return (reader.readString(offset)) as P; + return (reader.readLong(offset)) as P; case 12: + return (reader.readString(offset)) as P; + case 13: return (_TransactiontypeValueEnumMap[reader.readByteOrNull(offset)] ?? TransactionType.outgoing) as P; - case 13: + case 14: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -821,6 +836,160 @@ extension TransactionQueryFilter }); } + QueryBuilder + amountStringIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'amountString', + )); + }); + } + + QueryBuilder + amountStringIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'amountString', + )); + }); + } + + QueryBuilder + amountStringEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'amountString', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'amountString', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'amountString', + value: '', + )); + }); + } + + QueryBuilder + amountStringIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'amountString', + value: '', + )); + }); + } + QueryBuilder feeEqualTo( int value) { return QueryBuilder.apply(this, (query) { @@ -1998,6 +2167,19 @@ extension TransactionQuerySortBy }); } + QueryBuilder sortByAmountString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amountString', Sort.asc); + }); + } + + QueryBuilder + sortByAmountStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amountString', Sort.desc); + }); + } + QueryBuilder sortByFee() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'fee', Sort.asc); @@ -2145,6 +2327,19 @@ extension TransactionQuerySortThenBy }); } + QueryBuilder thenByAmountString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amountString', Sort.asc); + }); + } + + QueryBuilder + thenByAmountStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amountString', Sort.desc); + }); + } + QueryBuilder thenByFee() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'fee', Sort.asc); @@ -2298,6 +2493,13 @@ extension TransactionQueryWhereDistinct }); } + QueryBuilder distinctByAmountString( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'amountString', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByFee() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'fee'); @@ -2383,6 +2585,12 @@ extension TransactionQueryProperty }); } + QueryBuilder amountStringProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'amountString'); + }); + } + QueryBuilder feeProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'fee'); From 7e6642cd8480b833d6755a1afe7895ab27b56d58 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 15:55:17 -0600 Subject: [PATCH 119/208] update tests --- test/services/coins/firo/firo_wallet_test.dart | 5 +++++ test/services/coins/manager_test.dart | 5 +++++ test/widget_tests/transaction_card_test.dart | 17 +++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/test/services/coins/firo/firo_wallet_test.dart b/test/services/coins/firo/firo_wallet_test.dart index 5cdf9ce50..2c25bd711 100644 --- a/test/services/coins/firo/firo_wallet_test.dart +++ b/test/services/coins/firo/firo_wallet_test.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/models/paymint/transactions_model.dart' as old; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; @@ -97,6 +98,10 @@ void main() { ? TransactionSubType.join : TransactionSubType.none, amount: t.amount, + amountString: Amount( + rawValue: BigInt.from(t.amount), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: t.fees, height: t.height, isCancelled: t.isCancelled, diff --git a/test/services/coins/manager_test.dart b/test/services/coins/manager_test.dart index 2a5661ab2..27f8b3b81 100644 --- a/test/services/coins/manager_test.dart +++ b/test/services/coins/manager_test.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'manager_test.mocks.dart'; @@ -103,6 +104,10 @@ void main() { type: TransactionType.incoming, subType: TransactionSubType.mint, amount: 123, + amountString: Amount( + rawValue: BigInt.from(123), + fractionDigits: wallet.coin.decimals, + ).toJsonString(), fee: 3, height: 123, isCancelled: false, diff --git a/test/widget_tests/transaction_card_test.dart b/test/widget_tests/transaction_card_test.dart index d8cb9d390..984ed2baf 100644 --- a/test/widget_tests/transaction_card_test.dart +++ b/test/widget_tests/transaction_card_test.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/services/locale_service.dart'; import 'package:stackwallet/services/notes_service.dart'; import 'package:stackwallet/services/price_service.dart'; import 'package:stackwallet/services/wallets.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; @@ -51,6 +52,10 @@ void main() { timestamp: 1648595998, type: TransactionType.outgoing, amount: 100000000, + amountString: Amount( + rawValue: BigInt.from(100000000), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: 3794, height: 450123, subType: TransactionSubType.none, @@ -152,6 +157,10 @@ void main() { timestamp: 1648595998, type: TransactionType.outgoing, amount: 9659, + amountString: Amount( + rawValue: BigInt.from(9659), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: 3794, height: 450123, subType: TransactionSubType.mint, @@ -250,6 +259,10 @@ void main() { timestamp: 1648595998, type: TransactionType.incoming, amount: 100000000, + amountString: Amount( + rawValue: BigInt.from(100000000), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: 3794, height: 450123, subType: TransactionSubType.none, @@ -342,6 +355,10 @@ void main() { timestamp: 1648595998, type: TransactionType.outgoing, amount: 100000000, + amountString: Amount( + rawValue: BigInt.from(100000000), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: 3794, height: 450123, subType: TransactionSubType.none, From 678a926ff337b46d6936db3c526ffbf2bd5152bc Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 16:43:34 -0600 Subject: [PATCH 120/208] bch zero conf fixes --- .../transaction_details_view.dart | 30 ++++++++++++------- .../coins/bitcoincash/bitcoincash_wallet.dart | 6 ++-- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 77decdfcb..056b8da4b 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -968,17 +968,25 @@ class _TransactionDetailsViewState ? const EdgeInsets.all(16) : const EdgeInsets.all(12), child: Builder(builder: (context) { - final height = widget.coin != Coin.epicCash && - _transaction.isConfirmed( - currentHeight, - coin.requiredConfirmations, - ) - ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" - : _transaction.getConfirmations( - currentHeight) > - 0 - ? "${_transaction.height}" - : "Pending"; + final String height; + + if (widget.coin == Coin.bitcoincash || + widget.coin == Coin.bitcoincashTestnet) { + height = + "${_transaction.height != null && _transaction.height! > 0 ? _transaction.height! : "Pending"}"; + } else { + height = widget.coin != Coin.epicCash && + _transaction.isConfirmed( + currentHeight, + coin.requiredConfirmations, + ) + ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" + : _transaction.getConfirmations( + currentHeight) > + 0 + ? "${_transaction.height}" + : "Pending"; + } return Row( mainAxisAlignment: diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index d954371d7..a6d35e8f8 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -2120,8 +2120,10 @@ class BitcoinCashWallet extends CoinServiceAPI .txidEqualTo(txHash["tx_hash"] as String) .findFirst(); - if (storedTx == null || - !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) { + if (storedTx == null || storedTx.address.value == null + // zero conf messes this up + // !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS) + ) { final tx = await cachedElectrumXClient.getTransaction( txHash: txHash["tx_hash"] as String, verbose: true, From 7c3b50552a943f066be8db8749a771ab38533a68 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 16:43:47 -0600 Subject: [PATCH 121/208] tests fix --- test/services/coins/manager_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/services/coins/manager_test.dart b/test/services/coins/manager_test.dart index 27f8b3b81..265f9f2cd 100644 --- a/test/services/coins/manager_test.dart +++ b/test/services/coins/manager_test.dart @@ -75,6 +75,7 @@ void main() { group("get balances", () { test("balance", () async { final CoinServiceAPI wallet = MockFiroWallet(); + when(wallet.coin).thenAnswer((_) => Coin.firo); when(wallet.balance).thenAnswer( (_) => Balance( coin: Coin.firo, From f4d61a9f58d28d8f41e4e6f694fa6fd07b00d38b Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 24 Mar 2023 17:40:20 -0600 Subject: [PATCH 122/208] use realAmount custom Amount class data in ui --- .../isar/models/blockchain_data/transaction.dart | 1 + .../transaction_views/all_transactions_view.dart | 16 ++++++++++------ .../transaction_details_view.dart | 3 ++- lib/widgets/transaction_card.dart | 15 +++++++++------ 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index bf6faac8c..b6a7a74a6 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -88,6 +88,7 @@ class Transaction { @enumerated late final TransactionSubType subType; + @Deprecated("May be inaccurate for large amounts (eth for example)") late final int amount; late String? amountString; diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 5b72331a3..bc4894f0d 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -111,7 +111,8 @@ class _TransactionDetailsViewState extends ConsumerState { return false; } - if (filter.amount != null && filter.amount != tx.amount) { + if (filter.amount != null && + BigInt.from(filter.amount!) != tx.realAmount.raw) { return false; } @@ -953,9 +954,13 @@ class _DesktopTransactionCardRowState flex: 6, child: Builder( builder: (_) { - final amount = _transaction.amount; + final amount = _transaction.realAmount; return Text( - "$prefix${Format.satoshiAmountToPrettyString(amount, locale, coin)} ${coin.ticker}", + "$prefix${Format.localizedStringAsFixed( + value: amount.decimal, + locale: locale, + decimalPlaces: coin.decimals, + )} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall(context) .copyWith( color: Theme.of(context) @@ -972,12 +977,11 @@ class _DesktopTransactionCardRowState flex: 4, child: Builder( builder: (_) { - int value = _transaction.amount; + final amount = _transaction.realAmount; return Text( "$prefix${Format.localizedStringAsFixed( - value: Format.satoshisToAmount(value, coin: coin) * - price, + value: amount.decimal * price, locale: locale, decimalPlaces: 2, )} $baseCurrency", diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 056b8da4b..8e12d1f0d 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -78,7 +78,8 @@ class _TransactionDetailsViewState walletId = widget.walletId; coin = widget.coin; - amount = Format.satoshisToAmount(_transaction.amount, coin: coin); + amount = _transaction.realAmount + .decimal; //Format.satoshisToAmount(_transaction.amount, coin: coin); fee = Format.satoshisToAmount(_transaction.fee, coin: coin); if ((coin == Coin.firo || coin == Coin.firoTestNet) && diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index e269ace4d..c331604be 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -221,9 +221,14 @@ class _TransactionCardState extends ConsumerState { fit: BoxFit.scaleDown, child: Builder( builder: (_) { - final amount = _transaction.amount; + final amount = _transaction.realAmount; + return Text( - "$prefix${Format.satoshiAmountToPrettyString(amount, locale, coin)} ${coin.ticker}", + "$prefix${Format.localizedStringAsFixed( + value: amount.decimal, + locale: locale, + decimalPlaces: coin.decimals, + )} ${coin.ticker}", style: STextStyles.itemSubtitle12(context), ); }, @@ -260,13 +265,11 @@ class _TransactionCardState extends ConsumerState { fit: BoxFit.scaleDown, child: Builder( builder: (_) { - int value = _transaction.amount; + final amount = _transaction.realAmount; return Text( "$prefix${Format.localizedStringAsFixed( - value: Format.satoshisToAmount(value, - coin: coin) * - price, + value: amount.decimal * price, locale: locale, decimalPlaces: 2, )} $baseCurrency", From 7b353be350308b249610338e73c936dcde8e52f3 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 08:01:35 -0600 Subject: [PATCH 123/208] add extension functionality --- lib/utilities/extensions/impl/string.dart | 3 +++ lib/utilities/extensions/impl/uint8_list.dart | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/utilities/extensions/impl/string.dart b/lib/utilities/extensions/impl/string.dart index 4e996e05b..e5021e3f1 100644 --- a/lib/utilities/extensions/impl/string.dart +++ b/lib/utilities/extensions/impl/string.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:dart_bs58/dart_bs58.dart'; @@ -6,6 +7,8 @@ import 'package:hex/hex.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; extension StringExtensions on String { + Uint8List get toUint8ListFromUtf8 => Uint8List.fromList(utf8.encode(this)); + Uint8List get toUint8ListFromHex => Uint8List.fromList(HEX.decode(startsWith("0x") ? substring(2) : this)); diff --git a/lib/utilities/extensions/impl/uint8_list.dart b/lib/utilities/extensions/impl/uint8_list.dart index 5dcf0b435..04980f91a 100644 --- a/lib/utilities/extensions/impl/uint8_list.dart +++ b/lib/utilities/extensions/impl/uint8_list.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:dart_bs58/dart_bs58.dart'; @@ -5,6 +6,8 @@ import 'package:dart_bs58check/dart_bs58check.dart'; import 'package:hex/hex.dart'; extension Uint8ListExtensions on Uint8List { + String get toUtf8String => utf8.decode(this); + String get toHex { return HEX.encode(this); } From 4bd87e8dceec9c073e6e7ef2de44792dd58f6e26 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 08:41:59 -0600 Subject: [PATCH 124/208] start using Amount --- lib/dto/ethereum/eth_token_tx_extra_dto.dart | 50 +++++++++------- lib/dto/ethereum/eth_tx_dto.dart | 58 ++++++++++--------- .../coins/ethereum/ethereum_wallet.dart | 13 ++--- 3 files changed, 64 insertions(+), 57 deletions(-) diff --git a/lib/dto/ethereum/eth_token_tx_extra_dto.dart b/lib/dto/ethereum/eth_token_tx_extra_dto.dart index 60a032255..b84aa8a2f 100644 --- a/lib/dto/ethereum/eth_token_tx_extra_dto.dart +++ b/lib/dto/ethereum/eth_token_tx_extra_dto.dart @@ -1,5 +1,8 @@ import 'dart:convert'; +import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + class EthTokenTxExtraDTO { EthTokenTxExtraDTO({ required this.blockHash, @@ -27,20 +30,16 @@ class EthTokenTxExtraDTO { timestamp: map['timestamp'] as int, from: map['from'] as String, to: map['to'] as String, - value: int.parse(map['value'] as String), - gas: map['gas'] as int, - gasPrice: map['gasPrice'] as int, + value: Amount( + rawValue: BigInt.parse(map['value'] as String), + fractionDigits: Coin.ethereum.decimals, + ), + gas: _amountFromJsonNum(map['gas']), + gasPrice: _amountFromJsonNum(map['gasPrice']), nonce: map['nonce'] as int, input: map['input'] as String, - gasCost: map['gasCost'] as int, - gasUsed: map['gasUsed'] as int, - ); - - factory EthTokenTxExtraDTO.fromJsonString(String jsonString) => - EthTokenTxExtraDTO.fromMap( - Map.from( - jsonDecode(jsonString) as Map, - ), + gasCost: _amountFromJsonNum(map['gasCost']), + gasUsed: _amountFromJsonNum(map['gasUsed']), ); final String hash; @@ -50,13 +49,20 @@ class EthTokenTxExtraDTO { final int timestamp; final String from; final String to; - final int value; - final int gas; - final int gasPrice; + final Amount value; + final Amount gas; + final Amount gasPrice; final String input; final int nonce; - final int gasCost; - final int gasUsed; + final Amount gasCost; + final Amount gasUsed; + + static Amount _amountFromJsonNum(dynamic json) { + return Amount( + rawValue: BigInt.from(json as num), + fractionDigits: Coin.ethereum.decimals, + ); + } EthTokenTxExtraDTO copyWith({ String? hash, @@ -66,13 +72,13 @@ class EthTokenTxExtraDTO { int? timestamp, String? from, String? to, - int? value, - int? gas, - int? gasPrice, + Amount? value, + Amount? gas, + Amount? gasPrice, int? nonce, String? input, - int? gasCost, - int? gasUsed, + Amount? gasCost, + Amount? gasUsed, }) => EthTokenTxExtraDTO( hash: hash ?? this.hash, diff --git a/lib/dto/ethereum/eth_tx_dto.dart b/lib/dto/ethereum/eth_tx_dto.dart index f839711bb..ed1b37e1d 100644 --- a/lib/dto/ethereum/eth_tx_dto.dart +++ b/lib/dto/ethereum/eth_tx_dto.dart @@ -1,5 +1,8 @@ import 'dart:convert'; +import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + class EthTxDTO { EthTxDTO({ required this.hash, @@ -29,22 +32,16 @@ class EthTxDTO { timestamp: map['timestamp'] as int, from: map['from'] as String, to: map['to'] as String, - value: map['value'] as int, - gas: map['gas'] as int, - gasPrice: map['gasPrice'] as int, - maxFeePerGas: map['maxFeePerGas'] as int, - maxPriorityFeePerGas: map['maxPriorityFeePerGas'] as int, + value: _amountFromJsonNum(map['value']), + gas: _amountFromJsonNum(map['gas']), + gasPrice: _amountFromJsonNum(map['gasPrice']), + maxFeePerGas: _amountFromJsonNum(map['maxFeePerGas']), + maxPriorityFeePerGas: _amountFromJsonNum(map['maxPriorityFeePerGas']), isError: map['isError'] as int, hasToken: map['hasToken'] as int, compressedTx: map['compressedTx'] as String, - gasCost: map['gasCost'] as int, - gasUsed: map['gasUsed'] as int, - ); - - factory EthTxDTO.fromJsonString(String jsonString) => EthTxDTO.fromMap( - Map.from( - jsonDecode(jsonString) as Map, - ), + gasCost: _amountFromJsonNum(map['gasCost']), + gasUsed: _amountFromJsonNum(map['gasUsed']), ); final String hash; @@ -54,16 +51,23 @@ class EthTxDTO { final int timestamp; final String from; final String to; - final int value; - final int gas; - final int gasPrice; - final int maxFeePerGas; - final int maxPriorityFeePerGas; + final Amount value; + final Amount gas; + final Amount gasPrice; + final Amount maxFeePerGas; + final Amount maxPriorityFeePerGas; final int isError; final int hasToken; final String compressedTx; - final int gasCost; - final int gasUsed; + final Amount gasCost; + final Amount gasUsed; + + static Amount _amountFromJsonNum(dynamic json) { + return Amount( + rawValue: BigInt.from(json as num), + fractionDigits: Coin.ethereum.decimals, + ); + } EthTxDTO copyWith({ String? hash, @@ -73,16 +77,16 @@ class EthTxDTO { int? timestamp, String? from, String? to, - int? value, - int? gas, - int? gasPrice, - int? maxFeePerGas, - int? maxPriorityFeePerGas, + Amount? value, + Amount? gas, + Amount? gasPrice, + Amount? maxFeePerGas, + Amount? maxPriorityFeePerGas, int? isError, int? hasToken, String? compressedTx, - int? gasCost, - int? gasUsed, + Amount? gasCost, + Amount? gasUsed, }) => EthTxDTO( hash: hash ?? this.hash, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 5fffad83c..c9a5c5ad2 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -854,7 +854,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { final allTxs = txsResponse.value!; final List> txnsData = []; for (final element in allTxs) { - int transactionAmount = element.value; + Amount transactionAmount = element.value; bool isIncoming; bool txFailed = false; @@ -869,7 +869,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { //Calculate fees (GasLimit * gasPrice) // int txFee = element.gasPrice * element.gasUsed; - int txFee = element.gasCost; + Amount txFee = element.gasCost; final String addressString = checksumEthereumAddress(element.to); final int height = element.blockNumber; @@ -881,12 +881,9 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, subType: TransactionSubType.none, - amount: transactionAmount, - amountString: Amount( - rawValue: BigInt.from(transactionAmount), - fractionDigits: coin.decimals, - ).toJsonString(), - fee: txFee, + amount: transactionAmount.raw.toInt(), + amountString: transactionAmount.toJsonString(), + fee: txFee.raw.toInt(), height: height, isCancelled: txFailed, isLelantus: false, From 6612dde2ef5de7ac54e7e611edf8c5bcc68904a9 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 08:43:35 -0600 Subject: [PATCH 125/208] show only token transfers in token tx history (for now) --- .../ethereum/ethereum_token_service.dart | 136 ++++++++---------- lib/utilities/eth_commons.dart | 4 + 2 files changed, 66 insertions(+), 74 deletions(-) diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index b2caa6cc6..7d184bb7d 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -387,15 +387,14 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { final List> txnsData = []; for (final tuple in data) { - int amount; - String fromAddress, toAddress; - if (tuple.item1.articulatedLog == null) { - if (tuple.item1.topics.length != 3) { - throw Exception("Topics length != 3 for " - "${ethWallet.walletName} ${ethWallet.walletId}: " - "${tuple.item1.toString()}"); - } - amount = tuple.item1.data.toBigIntFromHex.toInt(); + // ignore all non Transfer events (for now) + if (tuple.item1.topics[0] == kTransferEventSignature) { + final Amount amount; + String fromAddress, toAddress; + amount = Amount( + rawValue: tuple.item1.data.toBigIntFromHex, + fractionDigits: tokenContract.decimals, + ); fromAddress = _addressFromTopic( tuple.item1.topics[1], @@ -403,74 +402,63 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { toAddress = _addressFromTopic( tuple.item1.topics[2], ); - } else { - amount = int.parse( - tuple.item1.articulatedLog!.inputs.amount, - ); - fromAddress = checksumEthereumAddress( - tuple.item1.articulatedLog!.inputs.from, - ); - toAddress = checksumEthereumAddress( - tuple.item1.articulatedLog!.inputs.to, - ); - } - bool isIncoming; - bool isSentToSelf = false; - if (fromAddress == addressString) { - isIncoming = false; - if (toAddress == addressString) { - isSentToSelf = true; + bool isIncoming; + bool isSentToSelf = false; + if (fromAddress == addressString) { + isIncoming = false; + if (toAddress == addressString) { + isSentToSelf = true; + } + } else if (toAddress == addressString) { + isIncoming = true; + } else { + throw Exception("Unknown token transaction found for " + "${ethWallet.walletName} ${ethWallet.walletId}: " + "${tuple.item1.toString()}"); } - } else if (toAddress == addressString) { - isIncoming = true; - } else { - throw Exception("Unknown token transaction found for " - "${ethWallet.walletName} ${ethWallet.walletId}: " - "${tuple.item1.toString()}"); + + final txn = Transaction( + walletId: ethWallet.walletId, + txid: tuple.item1.transactionHash, + timestamp: tuple.item2.timestamp, + type: + isIncoming ? TransactionType.incoming : TransactionType.outgoing, + subType: TransactionSubType.ethToken, + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: (tuple.item2.gasUsed.raw * tuple.item2.gasPrice.raw).toInt(), + height: tuple.item1.blockNumber, + isCancelled: false, + isLelantus: false, + slateId: null, + otherData: tuple.item1.address, + inputs: [], + outputs: [], + ); + + Address? transactionAddress = await ethWallet.db + .getAddresses(ethWallet.walletId) + .filter() + .valueEqualTo(toAddress) + .findFirst(); + + transactionAddress ??= Address( + walletId: ethWallet.walletId, + value: toAddress, + publicKey: [], + derivationIndex: isSentToSelf ? 0 : -1, + derivationPath: isSentToSelf + ? (DerivationPath()..value = "$hdPathEthereum/0") + : null, + type: AddressType.ethereum, + subType: isSentToSelf + ? AddressSubType.receiving + : AddressSubType.nonWallet, + ); + + txnsData.add(Tuple2(txn, transactionAddress)); } - - final txn = Transaction( - walletId: ethWallet.walletId, - txid: tuple.item1.transactionHash, - timestamp: tuple.item2.timestamp, - type: isIncoming ? TransactionType.incoming : TransactionType.outgoing, - subType: TransactionSubType.ethToken, - amount: amount, - amountString: Amount( - rawValue: BigInt.from(amount), - fractionDigits: tokenContract.decimals, - ).toJsonString(), - fee: tuple.item2.gasUsed * tuple.item2.gasPrice.toInt(), - height: tuple.item1.blockNumber, - isCancelled: false, - isLelantus: false, - slateId: null, - otherData: tuple.item1.address, - inputs: [], - outputs: [], - ); - - Address? transactionAddress = await ethWallet.db - .getAddresses(ethWallet.walletId) - .filter() - .valueEqualTo(toAddress) - .findFirst(); - - transactionAddress ??= Address( - walletId: ethWallet.walletId, - value: toAddress, - publicKey: [], - derivationIndex: isSentToSelf ? 0 : -1, - derivationPath: isSentToSelf - ? (DerivationPath()..value = "$hdPathEthereum/0") - : null, - type: AddressType.ethereum, - subType: - isSentToSelf ? AddressSubType.receiving : AddressSubType.nonWallet, - ); - - txnsData.add(Tuple2(txn, transactionAddress)); } await ethWallet.db.addNewTransactionData(txnsData, ethWallet.walletId); diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index c250d68c5..c3b3b83c7 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -56,6 +56,10 @@ class GasTracker { const hdPathEthereum = "m/44'/60'/0'/0"; +// equal to "0x${keccak256("Transfer(address,address,uint256)".toUint8ListFromUtf8).toHex}"; +const kTransferEventSignature = + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + String getPrivateKey(String mnemonic, String mnemonicPassphrase) { final isValidMnemonic = bip39.validateMnemonic(mnemonic); if (!isValidMnemonic) { From ad53b30e2e0d84b9cfc88f78c4371bde8edc4519 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 09:59:23 -0600 Subject: [PATCH 126/208] start using provider for MainDB in ui and add contract getters --- lib/db/isar/main_db.dart | 6 ++++++ lib/providers/db/main_db_provider.dart | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 lib/providers/db/main_db_provider.dart diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index efe2fdd6b..9c0bd1f38 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -404,6 +404,12 @@ class MainDB { QueryBuilder getEthContracts() => isar.ethContracts.where(); + Future getEthContract(String contractAddress) => + isar.ethContracts.where().addressEqualTo(contractAddress).findFirst(); + + EthContract? getEthContractSync(String contractAddress) => + isar.ethContracts.where().addressEqualTo(contractAddress).findFirstSync(); + Future putEthContract(EthContract contract) => isar.writeTxn(() async { return await isar.ethContracts.put(contract); }); diff --git a/lib/providers/db/main_db_provider.dart b/lib/providers/db/main_db_provider.dart new file mode 100644 index 000000000..2f3b6479c --- /dev/null +++ b/lib/providers/db/main_db_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; + +final mainDBProvider = Provider((ref) => MainDB.instance); From e2196c14d490a06540c70724a7b853e36e358ece Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 09:59:57 -0600 Subject: [PATCH 127/208] tx card modification to properly display eth token transaction info --- lib/widgets/transaction_card.dart | 51 ++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index c331604be..47793d106 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -33,6 +34,10 @@ class TransactionCard extends ConsumerStatefulWidget { class _TransactionCardState extends ConsumerState { late final Transaction _transaction; late final String walletId; + late final bool isTokenTx; + late final String prefix; + late final String unit; + late final Coin coin; String whatIsIt( TransactionType type, @@ -93,6 +98,29 @@ class _TransactionCardState extends ConsumerState { void initState() { walletId = widget.walletId; _transaction = widget.transaction; + isTokenTx = _transaction.subType == TransactionSubType.ethToken; + if (Util.isDesktop) { + if (_transaction.type == TransactionType.outgoing) { + prefix = "-"; + } else if (_transaction.type == TransactionType.incoming) { + prefix = "+"; + } else { + prefix = ""; + } + } else { + prefix = ""; + } + coin = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .coin; + + unit = isTokenTx + ? ref + .read(mainDBProvider) + .getEthContractSync(_transaction.otherData!)! + .symbol + : coin.ticker; super.initState(); } @@ -100,28 +128,16 @@ class _TransactionCardState extends ConsumerState { Widget build(BuildContext context) { final locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale)); - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId))); final baseCurrency = ref .watch(prefsChangeNotifierProvider.select((value) => value.currency)); - final coin = manager.coin; - final price = ref - .watch(priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin))) + .watch(priceAnd24hChangeNotifierProvider.select((value) => isTokenTx + ? value.getTokenPrice(_transaction.otherData!) + : value.getPrice(coin))) .item1; - String prefix = ""; - if (Util.isDesktop) { - if (_transaction.type == TransactionType.outgoing) { - prefix = "-"; - } else if (_transaction.type == TransactionType.incoming) { - prefix = "+"; - } - } - final currentHeight = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId).currentHeight)); @@ -183,8 +199,7 @@ class _TransactionCardState extends ConsumerState { children: [ TxIcon( transaction: _transaction, - coin: ref.watch(walletsChangeNotifierProvider.select( - (value) => value.getManager(widget.walletId).coin)), + coin: coin, currentHeight: currentHeight, ), const SizedBox( @@ -228,7 +243,7 @@ class _TransactionCardState extends ConsumerState { value: amount.decimal, locale: locale, decimalPlaces: coin.decimals, - )} ${coin.ticker}", + )} $unit", style: STextStyles.itemSubtitle12(context), ); }, From 46e62f2b2c4524db08f39cdfd4a48a2cccab6202 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 10:36:47 -0600 Subject: [PATCH 128/208] tx details modification to properly display eth token transaction info --- .../transaction_details_view.dart | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 8e12d1f0d..da59a3bb6 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -11,10 +11,12 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/block_explorers.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -65,9 +67,11 @@ class _TransactionDetailsViewState late final String walletId; late final Coin coin; - late final Decimal amount; + late final Amount amount; late final Decimal fee; late final String amountPrefix; + late final String unit; + late final bool isTokenTx; bool showFeePending = false; @@ -75,11 +79,12 @@ class _TransactionDetailsViewState void initState() { isDesktop = Util.isDesktop; _transaction = widget.transaction; + isTokenTx = _transaction.subType == TransactionSubType.ethToken; walletId = widget.walletId; coin = widget.coin; - amount = _transaction.realAmount - .decimal; //Format.satoshisToAmount(_transaction.amount, coin: coin); + amount = _transaction + .realAmount; //Format.satoshisToAmount(_transaction.amount, coin: coin); fee = Format.satoshisToAmount(_transaction.fee, coin: coin); if ((coin == Coin.firo || coin == Coin.firoTestNet) && @@ -89,6 +94,13 @@ class _TransactionDetailsViewState amountPrefix = _transaction.type == TransactionType.outgoing ? "-" : "+"; } + unit = isTokenTx + ? ref + .read(mainDBProvider) + .getEthContractSync(_transaction.otherData!)! + .symbol + : coin.ticker; + // if (coin == Coin.firo || coin == Coin.firoTestNet) { // showFeePending = true; // } else { @@ -434,15 +446,15 @@ class _TransactionDetailsViewState children: [ SelectableText( "$amountPrefix${Format.localizedStringAsFixed( - value: amount, + value: amount.decimal, locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), - decimalPlaces: Constants - .decimalPlacesForCoin(coin), - )} ${coin.ticker}", + decimalPlaces: + amount.fractionDigits, + )} $unit", style: isDesktop ? STextStyles .desktopTextExtraExtraSmall( @@ -465,11 +477,16 @@ class _TransactionDetailsViewState value.externalCalls))) SelectableText( "$amountPrefix${Format.localizedStringAsFixed( - value: amount * + value: amount.decimal * ref.watch( priceAnd24hChangeNotifierProvider - .select((value) => - value + .select((value) => isTokenTx + ? value + .getTokenPrice( + _transaction + .otherData!) + .item1 + : value .getPrice( coin) .item1), @@ -874,7 +891,7 @@ class _TransactionDetailsViewState ? const EdgeInsets.all(16) : const EdgeInsets.all(12), child: Builder(builder: (context) { - final feeString = showFeePending + String feeString = showFeePending ? _transaction.isConfirmed( currentHeight, coin.requiredConfirmations, @@ -897,6 +914,9 @@ class _TransactionDetailsViewState (value) => value.locale)), decimalPlaces: Constants.decimalPlacesForCoin(coin)); + if (isTokenTx) { + feeString += " ${coin.ticker}"; + } return Row( mainAxisAlignment: @@ -1138,18 +1158,20 @@ class _TransactionDetailsViewState .externalApplication, ); } catch (_) { - unawaited( - showDialog( - context: context, - builder: (_) => - StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", + if (mounted) { + unawaited( + showDialog( + context: context, + builder: (_) => + StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), ), - ), - ); + ); + } } finally { // Future.delayed( // const Duration(seconds: 1), From 748d70e34fc37d10d7e768b4b4be5674d83ec81e Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 10:38:50 -0600 Subject: [PATCH 129/208] tx card decimal places fix --- lib/widgets/transaction_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index 47793d106..b4d74e213 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -242,7 +242,7 @@ class _TransactionCardState extends ConsumerState { "$prefix${Format.localizedStringAsFixed( value: amount.decimal, locale: locale, - decimalPlaces: coin.decimals, + decimalPlaces: amount.fractionDigits, )} $unit", style: STextStyles.itemSubtitle12(context), ); From 21e0113439fff95952c102d3545e608ab1ad0629 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 14:29:08 -0600 Subject: [PATCH 130/208] sync getter --- lib/services/wallets_service.dart | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/services/wallets_service.dart b/lib/services/wallets_service.dart index 085937ca6..cbba6bf5a 100644 --- a/lib/services/wallets_service.dart +++ b/lib/services/wallets_service.dart @@ -140,6 +140,29 @@ class WalletsService extends ChangeNotifier { name, WalletInfo.fromJson(Map.from(dyn as Map)))); } + Map fetchWalletsData() { + final names = DB.instance.get( + boxName: DB.boxNameAllWalletsData, key: 'names') as Map? ?? + {}; + + Logging.instance.log("Fetched wallet names: $names", level: LogLevel.Info); + final mapped = Map.from(names); + mapped.removeWhere((name, dyn) { + final jsonObject = Map.from(dyn as Map); + try { + Coin.values.byName(jsonObject["coin"] as String); + return false; + } catch (e, s) { + Logging.instance.log("Error, ${jsonObject["coin"]} does not exist", + level: LogLevel.Error); + return true; + } + }); + + return mapped.map((name, dyn) => MapEntry( + name, WalletInfo.fromJson(Map.from(dyn as Map)))); + } + Future addExistingStackWallet({ required String name, required String walletId, From 1e6b9e583075581a573b943d0ca00fec21f621e6 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 14:29:30 -0600 Subject: [PATCH 131/208] small tweaks --- .../sub_widgets/wallet_info_row_coin_icon.dart | 4 ++-- lib/widgets/wallet_info_row/wallet_info_row.dart | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart index ec5924de6..670fb3b8c 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart @@ -17,13 +17,13 @@ class WalletInfoCoinIcon extends StatelessWidget { color: Theme.of(context) .extension()! .colorForCoin(coin) - .withOpacity(0.5), + .withOpacity(0.4), borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), child: Padding( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.all(6), child: SvgPicture.asset( Assets.svg.iconFor(coin: coin), width: 20, diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index 5bb51e2e6..a5dc81aea 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -13,12 +13,12 @@ class WalletInfoRow extends ConsumerWidget { const WalletInfoRow({ Key? key, required this.walletId, - this.onPressed, + this.onPressedDesktop, this.padding = const EdgeInsets.all(0), }) : super(key: key); final String walletId; - final VoidCallback? onPressed; + final VoidCallback? onPressedDesktop; final EdgeInsets padding; @override @@ -31,7 +31,7 @@ class WalletInfoRow extends ConsumerWidget { return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( - onTap: onPressed, + onTap: onPressedDesktop, child: Padding( padding: padding, child: Container( From 41d9c8ca0cc3355cd2dd969718edf9a7f7d83ca9 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 14:29:55 -0600 Subject: [PATCH 132/208] select wallet for token view --- .../select_wallet_for_token_view.dart | 205 ++++++++++++++++++ lib/route_generator.dart | 16 ++ 2 files changed, 221 insertions(+) create mode 100644 lib/pages/add_wallet_views/select_wallet_for_token_view.dart diff --git a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart new file mode 100644 index 000000000..e376d3202 --- /dev/null +++ b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart @@ -0,0 +1,205 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; +import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; +import 'package:stackwallet/providers/global/wallets_service_provider.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; + +class SelectWalletForTokenView extends ConsumerStatefulWidget { + const SelectWalletForTokenView({ + Key? key, + required this.entity, + }) : super(key: key); + + static const String routeName = "/selectWalletForTokenView"; + + final EthTokenEntity entity; + + @override + ConsumerState createState() => + _SelectWalletForTokenViewState(); +} + +class _SelectWalletForTokenViewState + extends ConsumerState { + final isDesktop = Util.isDesktop; + late final List ethWalletIds; + + String? _selectedWalletId; + + void _onContinue() { + // + } + + void _onAddNewEthWallet() { + ref.read(createSpecialEthWalletRoutingFlag.notifier).state = true; + Navigator.of(context).pushNamed( + CreateOrRestoreWalletView.routeName, + arguments: CoinEntity(widget.entity.coin), + ); + } + + @override + void initState() { + final walletsData = + ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + walletsData.removeWhere((key, value) => value.coin != widget.entity.coin); + ethWalletIds = []; + + // TODO: proper wallet data class instead of this Hive silliness + for (final walletId in walletsData.values.map((e) => e.walletId).toList()) { + final walletContracts = DB.instance.get( + boxName: walletId, + key: DBKeys.ethTokenContracts, + ) as List? ?? + []; + if (!walletContracts.contains(widget.entity.token.address)) { + ethWalletIds.add(walletId); + } + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; + return true; + }, + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + + // child: LayoutBuilder( + // builder: (ctx, constraints) { + // return SingleChildScrollView( + // child: ConstrainedBox( + // constraints: + // BoxConstraints(minHeight: constraints.maxHeight), + // child: IntrinsicHeight( + // child: child, + // ), + // ), + // ); + // }, + // ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Select Ethereum wallet", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "You are adding an ETH token.", + textAlign: TextAlign.center, + style: STextStyles.subtitle(context), + ), + const SizedBox( + height: 8, + ), + Text( + "You must choose an Ethereum wallet in order to use ${widget.entity.name}", + textAlign: TextAlign.center, + style: STextStyles.subtitle(context), + ), + const SizedBox( + height: 16, + ), + ethWalletIds.isEmpty + ? RoundedWhiteContainer( + child: Text( + "You do not have any Ethereum wallets", + style: STextStyles.label(context), + ), + ) + : Expanded( + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(8), + child: ListView.separated( + itemCount: ethWalletIds.length, + shrinkWrap: true, + separatorBuilder: (_, __) => const SizedBox( + height: 6, + ), + itemBuilder: (_, index) { + return RoundedContainer( + padding: const EdgeInsets.all(8), + onPressed: () { + setState(() { + _selectedWalletId = ethWalletIds[index]; + }); + }, + color: _selectedWalletId == ethWalletIds[index] + ? Theme.of(context) + .extension()! + .highlight + : Colors.transparent, + child: WalletInfoRow( + walletId: ethWalletIds[index], + ), + ); + }, + ), + ), + ], + ), + ), + if (ethWalletIds.isEmpty) + const SizedBox( + height: 16, + ), + ethWalletIds.isEmpty + ? PrimaryButton( + label: "Add new Ethereum wallet", + onPressed: _onAddNewEthWallet, + ) + : PrimaryButton( + label: "Continue", + enabled: _selectedWalletId != null, + onPressed: _onContinue, + ), + ], + ), + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 7903b1796..9ee9e85ad 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; @@ -20,6 +21,7 @@ import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_vi import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; @@ -225,6 +227,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SelectWalletForTokenView.routeName: + if (args is EthTokenEntity) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SelectWalletForTokenView( + entity: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case AddCustomTokenView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, From 5da5e2c517275296681072012c264b936c3b1ad6 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 14:30:31 -0600 Subject: [PATCH 133/208] add wallet next button token option addition --- .../sub_widgets/next_button.dart | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart index 40081474c..edb325fd9 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -25,13 +27,17 @@ class AddWalletNextButton extends ConsumerWidget { onPressed: !enabled ? null : () { - final selectedCoin = - ref.read(addWalletSelectedEntityStateProvider.state).state; - - Navigator.of(context).pushNamed( - CreateOrRestoreWalletView.routeName, - arguments: selectedCoin, - ); + if (selectedCoin is EthTokenEntity) { + Navigator.of(context).pushNamed( + SelectWalletForTokenView.routeName, + arguments: selectedCoin, + ); + } else { + Navigator.of(context).pushNamed( + CreateOrRestoreWalletView.routeName, + arguments: selectedCoin, + ); + } }, style: enabled ? Theme.of(context) From 00ccb41a8c200ac028d0da11c5fd97d595831d93 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 14:30:46 -0600 Subject: [PATCH 134/208] expand tokens by default --- lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 781022ac7..e772ee7ed 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -376,7 +376,7 @@ class _AddWalletViewState extends ConsumerState { ExpandingSubListItem( title: "Tokens", entities: filter(_searchTerm, tokenEntities), - initialState: ExpandableState.collapsed, + initialState: ExpandableState.expanded, ), ], ), From 514ff042dd5a3f76e29dc80aa44dc9102c0b935c Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 27 Mar 2023 14:31:44 -0600 Subject: [PATCH 135/208] WIP eth token wallet creation routing --- .../verify_recovery_phrase_view.dart | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 64324fb20..4fe20aeee 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; @@ -24,6 +25,8 @@ import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:tuple/tuple.dart'; +final createSpecialEthWalletRoutingFlag = StateProvider((ref) => false); + class VerifyRecoveryPhraseView extends ConsumerStatefulWidget { const VerifyRecoveryPhraseView({ Key? key, @@ -95,6 +98,12 @@ class _VerifyRecoveryPhraseViewState .read(walletsChangeNotifierProvider.notifier) .addWallet(walletId: _manager.walletId, manager: _manager); + final isCreateSpecialEthWallet = + ref.read(createSpecialEthWalletRoutingFlag); + if (isCreateSpecialEthWallet) { + ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; + } + if (mounted) { if (isDesktop) { Navigator.of(context).popUntil( @@ -103,19 +112,27 @@ class _VerifyRecoveryPhraseViewState ), ); } else { - unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), - ); - if (widget.manager.coin == Coin.ethereum) { - unawaited( - Navigator.of(context).pushNamed( - AddTokenView.routeName, - arguments: widget.manager.walletId, + if (isCreateSpecialEthWallet) { + Navigator.of(context).popUntil( + ModalRoute.withName( + SelectWalletForTokenView.routeName, ), ); + } else { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + if (widget.manager.coin == Coin.ethereum) { + unawaited( + Navigator.of(context).pushNamed( + AddTokenView.routeName, + arguments: widget.manager.walletId, + ), + ); + } } } } From 9ad0343d63bd4be47d55383baf016956b173a9ef Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Mar 2023 13:47:17 -0600 Subject: [PATCH 136/208] refresh eth wallets list on adding new token wallet --- .../add_wallet_view/add_wallet_view.dart | 6 ++- .../select_wallet_for_token_view.dart | 46 ++++++++++++++++--- .../verify_recovery_phrase_view.dart | 6 +++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index e772ee7ed..a359acf49 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -15,7 +15,7 @@ import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/a import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_eth_tokens.dart'; @@ -124,6 +124,10 @@ class _AddWalletViewState extends ConsumerState { tokenEntities.addAll(contracts.map((e) => EthTokenEntity(e))); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.refresh(addWalletSelectedEntityStateProvider); + }); + super.initState(); } diff --git a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart index e376d3202..29d47ecd7 100644 --- a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart +++ b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart @@ -5,7 +5,11 @@ import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entit import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; +import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_service_provider.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/services/wallets_service.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -17,6 +21,9 @@ import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; +final newEthWalletTriggerTempUntilHiveCompletelyDeleted = + StateProvider((ref) => false); + class SelectWalletForTokenView extends ConsumerStatefulWidget { const SelectWalletForTokenView({ Key? key, @@ -40,7 +47,16 @@ class _SelectWalletForTokenViewState String? _selectedWalletId; void _onContinue() { - // + final wallet = ref + .read(walletsChangeNotifierProvider) + .getManager(_selectedWalletId!) + .wallet as EthereumWallet; + + final tokenSet = wallet.getWalletTokenContractAddresses().toSet(); + tokenSet.add(widget.entity.token.address); + wallet.updateWalletTokenContractAddresses(tokenSet.toList()); + + Navigator.of(context).pushNamed(HomeView.routeName); } void _onAddNewEthWallet() { @@ -51,12 +67,13 @@ class _SelectWalletForTokenViewState ); } - @override - void initState() { - final walletsData = - ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + late int _cachedWalletCount; + + void _updateWalletsList(Map walletsData) { + _cachedWalletCount = walletsData.length; + walletsData.removeWhere((key, value) => value.coin != widget.entity.coin); - ethWalletIds = []; + ethWalletIds.clear(); // TODO: proper wallet data class instead of this Hive silliness for (final walletId in walletsData.values.map((e) => e.walletId).toList()) { @@ -69,12 +86,29 @@ class _SelectWalletForTokenViewState ethWalletIds.add(walletId); } } + } + + @override + void initState() { + ethWalletIds = []; + + final walletsData = + ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + _updateWalletsList(walletsData); super.initState(); } @override Widget build(BuildContext context) { + // dumb hack + ref.watch(newEthWalletTriggerTempUntilHiveCompletelyDeleted); + final walletsData = + ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + if (walletsData.length != _cachedWalletCount) { + _updateWalletsList(walletsData); + } + return WillPopScope( onWillPop: () async { ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 4fe20aeee..a30603ffe 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -102,6 +102,12 @@ class _VerifyRecoveryPhraseViewState ref.read(createSpecialEthWalletRoutingFlag); if (isCreateSpecialEthWallet) { ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; + ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state = + !ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state; } if (mounted) { From 5ce5ae3f4dd442f70e6aa34f75a1d46c73b00be7 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Mar 2023 13:55:36 -0600 Subject: [PATCH 137/208] refresh eth wallets list on adding new (restored) token wallet --- .../restore_wallet_view.dart | 64 ++++++++++++++----- .../verify_recovery_phrase_view.dart | 16 +++-- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 45df3e2fc..29ca47251 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -16,6 +16,8 @@ import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/confirm_r import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart'; +import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; @@ -310,24 +312,54 @@ class _RestoreWalletViewState extends ConsumerState { .read(walletsChangeNotifierProvider.notifier) .addWallet(walletId: manager.walletId, manager: manager); - if (mounted) { - if (isDesktop) { - Navigator.of(context) - .popUntil(ModalRoute.withName(DesktopHomeView.routeName)); - } else { - unawaited(Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, (route) => false)); - } + final isCreateSpecialEthWallet = + ref.read(createSpecialEthWalletRoutingFlag); + if (isCreateSpecialEthWallet) { + ref.read(createSpecialEthWalletRoutingFlag.notifier).state = + false; + ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state = + !ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted + .state) + .state; + } + + if (mounted) { + if (isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + if (isCreateSpecialEthWallet) { + Navigator.of(context).popUntil( + ModalRoute.withName( + SelectWalletForTokenView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + } + } + + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const RestoreSucceededDialog(); + }, + ); } - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const RestoreSucceededDialog(); - }, - ); if (!Platform.isLinux && !isDesktop) { await Wakelock.disable(); } diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index a30603ffe..6e12a1402 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -141,14 +141,16 @@ class _VerifyRecoveryPhraseViewState } } } - } - unawaited(showFloatingFlushBar( - type: FlushBarType.success, - message: "Correct! Your wallet is set up.", - iconAsset: Assets.svg.check, - context: context, - )); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Correct! Your wallet is set up.", + iconAsset: Assets.svg.check, + context: context, + ), + ); + } } else { unawaited(showFloatingFlushBar( type: FlushBarType.warning, From 9c0994aa004d48c051a7fd7b6c502bdba234c4b2 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Mar 2023 14:01:48 -0600 Subject: [PATCH 138/208] open token selection screen on successful eth wallet restore --- .../restore_wallet_view/restore_wallet_view.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 29ca47251..dda0359b3 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -12,6 +12,7 @@ import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart'; @@ -347,6 +348,14 @@ class _RestoreWalletViewState extends ConsumerState { (route) => false, ), ); + if (manager.coin == Coin.ethereum) { + unawaited( + Navigator.of(context).pushNamed( + AddTokenView.routeName, + arguments: manager.walletId, + ), + ); + } } } From 5e5730d5a54f60fb38055ba53a20e08b21c43762 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Mar 2023 16:18:11 -0600 Subject: [PATCH 139/208] eth token wallet general ui and wallet selection interface --- .../exchange_view/choose_from_stack_view.dart | 2 +- .../token_view/sub_widgets/token_summary.dart | 39 +++-- .../wallets_view/eth_wallets_overview.dart | 73 ++++++++++ .../sub_widgets/wallet_list_item.dart | 27 ++-- lib/route_generator.dart | 10 ++ .../coins/ethereum/ethereum_wallet.dart | 23 +++ lib/services/mixins/eth_token_cache.dart | 6 +- lib/widgets/master_wallet_card.dart | 137 ++++++++++++++++++ lib/widgets/wallet_card.dart | 71 +++++++-- .../wallet_info_row_balance_future.dart | 37 +++-- .../wallet_info_row_coin_icon.dart | 42 +++++- .../wallet_info_row/wallet_info_row.dart | 51 ++++++- 12 files changed, 455 insertions(+), 63 deletions(-) create mode 100644 lib/pages/wallets_view/eth_wallets_overview.dart create mode 100644 lib/widgets/master_wallet_card.dart diff --git a/lib/pages/exchange_view/choose_from_stack_view.dart b/lib/pages/exchange_view/choose_from_stack_view.dart index 7c7669430..505554a7c 100644 --- a/lib/pages/exchange_view/choose_from_stack_view.dart +++ b/lib/pages/exchange_view/choose_from_stack_view.dart @@ -112,7 +112,7 @@ class _ChooseFromStackViewState extends ConsumerState { const SizedBox( height: 2, ), - WalletInfoRowBalanceFuture( + WalletInfoRowBalance( walletId: walletIds[index], ), ], diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index eece604e5..208cdf699 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -69,18 +69,8 @@ class TokenSummary extends ConsumerWidget { const SizedBox( width: 10, ), - RoundedContainer( - padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), - radiusMultiplier: 0.25, - color: const Color( - 0xFF4D5798), // TODO: color theme for multi themes - child: Text( - ref.watch(walletsChangeNotifierProvider.select( - (value) => value.getManager(walletId).coin.ticker)), - style: STextStyles.w600_12(context).copyWith( - color: Colors.white, // TODO: design is wrong? - ), - ), + CoinTickerTag( + walletId: walletId, ), ], ), @@ -195,3 +185,28 @@ class TokenOptionsButton extends StatelessWidget { ); } } + +class CoinTickerTag extends ConsumerWidget { + const CoinTickerTag({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return RoundedContainer( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + radiusMultiplier: 0.25, + color: const Color(0xFF4D5798), // TODO: color theme for multi themes + child: Text( + ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).coin.ticker)), + style: STextStyles.w600_12(context).copyWith( + color: Colors.white, // TODO: design is wrong? + ), + ), + ); + } +} diff --git a/lib/pages/wallets_view/eth_wallets_overview.dart b/lib/pages/wallets_view/eth_wallets_overview.dart new file mode 100644 index 000000000..35e1b4411 --- /dev/null +++ b/lib/pages/wallets_view/eth_wallets_overview.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/master_wallet_card.dart'; + +class EthWalletsOverview extends ConsumerStatefulWidget { + const EthWalletsOverview({Key? key}) : super(key: key); + + static const routeName = "/ethWalletsOverview"; + + @override + ConsumerState createState() => _EthWalletsOverviewState(); +} + +class _EthWalletsOverviewState extends ConsumerState { + final isDesktop = Util.isDesktop; + + final List ethWalletIds = []; + + @override + void initState() { + final walletsData = + ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + walletsData.removeWhere((key, value) => value.coin != Coin.ethereum); + ethWalletIds.clear(); + + ethWalletIds.addAll(walletsData.values.map((e) => e.walletId)); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Background( + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: const AppBarBackButton(), + title: Text( + "Ethereum (ETH) wallets", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + child: ListView.separated( + itemCount: ethWalletIds.length, + separatorBuilder: (_, __) => const SizedBox( + height: 8, + ), + itemBuilder: (_, index) => MasterWalletCard( + walletId: ethWalletIds[index], + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 01b78c33f..c2e5991d6 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_sheet/wallets_sheet.dart'; +import 'package:stackwallet/pages/wallets_view/eth_wallets_overview.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -45,7 +46,13 @@ class WalletListItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () async { - if (walletCount == 1) { + if (coin == Coin.ethereum) { + unawaited( + Navigator.of(context).pushNamed( + EthWalletsOverview.routeName, + ), + ); + } else if (walletCount == 1) { final providersByCoin = ref .watch(walletsChangeNotifierProvider .select((value) => value.getManagerProvidersByCoin())) @@ -57,15 +64,17 @@ class WalletListItem extends ConsumerWidget { if (coin == Coin.monero || coin == Coin.wownero) { await manager.initializeExisting(); } - unawaited( - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: Tuple2( - manager.walletId, - providersByCoin.first, + if (context.mounted) { + unawaited( + Navigator.of(context).pushNamed( + WalletView.routeName, + arguments: Tuple2( + manager.walletId, + providersByCoin.first, + ), ), - ), - ); + ); + } } else { unawaited( showModalBottomSheet( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 9ee9e85ad..2a94028fa 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -105,6 +105,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.d import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_search_filter_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages/wallets_view/eth_wallets_overview.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_view.dart'; @@ -250,6 +251,15 @@ class RouteGenerator { ), ); + case EthWalletsOverview.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const EthWalletsOverview(), + settings: RouteSettings( + name: settings.name, + ), + ); + case TokenContractDetailsView.routeName: if (args is Tuple2) { return getRoute( diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index c9a5c5ad2..581770a2c 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -5,11 +5,13 @@ import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/db/isar/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/token_balance.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; @@ -17,6 +19,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha 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/eth_token_cache.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; @@ -76,6 +79,26 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { ); } + TokenBalance getCachedTokenBalance(EthContract contract) { + final jsonString = DB.instance.get( + boxName: _walletId, + key: TokenCacheKeys.tokenBalance(contract.address), + ) as String?; + if (jsonString == null) { + return TokenBalance( + contractAddress: contract.address, + decimalPlaces: contract.decimals, + total: 0, + spendable: 0, + blockedTotal: 0, + pendingSpendable: 0, + ); + } + return TokenBalance.fromJson( + jsonString, + ); + } + // Future removeTokenContract(String contractAddress) async { // final set = getWalletTokenContractAddresses().toSet(); // set.removeWhere((e) => e == contractAddress); diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart index 87ec3c189..9bb11bea4 100644 --- a/lib/services/mixins/eth_token_cache.dart +++ b/lib/services/mixins/eth_token_cache.dart @@ -2,7 +2,7 @@ import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/token_balance.dart'; -abstract class _Keys { +abstract class TokenCacheKeys { static String tokenBalance(String contractAddress) { return "tokenBalanceCache_$contractAddress"; } @@ -21,7 +21,7 @@ mixin EthTokenCache { TokenBalance getCachedBalance() { final jsonString = DB.instance.get( boxName: _walletId, - key: _Keys.tokenBalance(_token.address), + key: TokenCacheKeys.tokenBalance(_token.address), ) as String?; if (jsonString == null) { return TokenBalance( @@ -41,7 +41,7 @@ mixin EthTokenCache { Future updateCachedBalance(TokenBalance balance) async { await DB.instance.put( boxName: _walletId, - key: _Keys.tokenBalance(_token.address), + key: TokenCacheKeys.tokenBalance(_token.address), value: balance.toJsonIgnoreCoin(), ); } diff --git a/lib/widgets/master_wallet_card.dart b/lib/widgets/master_wallet_card.dart new file mode 100644 index 000000000..af8b6e196 --- /dev/null +++ b/lib/widgets/master_wallet_card.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/expandable.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/wallet_card.dart'; +import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; + +class MasterWalletCard extends ConsumerStatefulWidget { + const MasterWalletCard({ + Key? key, + required this.walletId, + this.popPrevious = false, + }) : super(key: key); + + final String walletId; + final bool popPrevious; + + @override + ConsumerState createState() => _MasterWalletCardState(); +} + +class _MasterWalletCardState extends ConsumerState { + final expandableController = ExpandableController(); + final rotateIconController = RotateIconController(); + late final List tokenContractAddresses; + + @override + void initState() { + final ethWallet = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as EthereumWallet; + + tokenContractAddresses = ethWallet.getWalletTokenContractAddresses(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: Expandable( + controller: expandableController, + expandOverride: () {}, + header: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: WalletInfoRow( + walletId: widget.walletId, + ), + ), + MaterialButton( + padding: const EdgeInsets.all(5), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + minWidth: 32, + height: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + elevation: 0, + hoverElevation: 0, + disabledElevation: 0, + highlightElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + if (expandableController.state == ExpandableState.collapsed) { + rotateIconController.forward?.call(); + } else { + rotateIconController.reverse?.call(); + } + expandableController.toggle?.call(); + }, + child: RotateIcon( + controller: rotateIconController, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + ), + curve: Curves.easeInOut, + ), + ), + ], + ), + ), + body: ListView( + shrinkWrap: true, + primary: false, + children: [ + Container( + width: double.infinity, + height: 1.5, + color: + Theme.of(context).extension()!.backgroundAppBar, + ), + Padding( + padding: const EdgeInsets.all( + 7, + ), + child: WalletSheetCard( + walletId: widget.walletId, + popPrevious: true, + ), + ), + ...tokenContractAddresses.map( + (e) => Padding( + padding: const EdgeInsets.only( + left: 7, + right: 7, + bottom: 7, + ), + child: WalletSheetCard( + walletId: widget.walletId, + contractAddress: e, + // popPrevious: true, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 194033de3..4648d6bf7 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -1,21 +1,33 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; import 'package:tuple/tuple.dart'; +import '../utilities/show_loading.dart'; + class WalletSheetCard extends ConsumerWidget { const WalletSheetCard({ Key? key, required this.walletId, + this.contractAddress, this.popPrevious = false, }) : super(key: key); final String walletId; + final String? contractAddress; final bool popPrevious; @override @@ -33,25 +45,56 @@ class WalletSheetCard extends ConsumerWidget { ), ), onPressed: () async { - final manager = ref - .read(walletsChangeNotifierProvider) - .getManager(walletId); - if (manager.coin == Coin.monero || - manager.coin == Coin.wownero) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { await manager.initializeExisting(); } - if (popPrevious) Navigator.of(context).pop(); - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: Tuple2( - walletId, - ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(walletId)), - ); + if (context.mounted) { + if (popPrevious) Navigator.of(context).pop(); + unawaited( + Navigator.of(context).pushNamed( + WalletView.routeName, + arguments: Tuple2( + walletId, + ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(walletId)), + ), + ); + + if (contractAddress != null) { + final contract = ref + .read(mainDBProvider) + .getEthContractSync(contractAddress!)!; + ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( + token: contract, + secureStore: ref.read(secureStoreProvider), + ethWallet: manager.wallet as EthereumWallet, + tracker: TransactionNotificationTracker( + walletId: walletId, + ), + ); + + await showLoading( + whileFuture: ref.read(tokenServiceProvider)!.initialize(), + context: context, + opaqueBG: true, + message: "Loading ${contract.name}", + ); + + if (context.mounted) { + await Navigator.of(context).pushNamed( + TokenView.routeName, + arguments: walletId, + ); + } + } + } }, child: WalletInfoRow( walletId: walletId, + contractAddress: contractAddress, ), ), ); diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart index 542cb545a..ddd491229 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart @@ -1,20 +1,25 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; -class WalletInfoRowBalanceFuture extends ConsumerWidget { - const WalletInfoRowBalanceFuture({Key? key, required this.walletId}) - : super(key: key); +class WalletInfoRowBalance extends ConsumerWidget { + const WalletInfoRowBalance({ + Key? key, + required this.walletId, + this.contractAddress, + }) : super(key: key); final String walletId; + final String? contractAddress; @override Widget build(BuildContext context, WidgetRef ref) { @@ -28,18 +33,30 @@ class WalletInfoRowBalanceFuture extends ConsumerWidget { ), ); - Decimal balance = manager.balance.getTotal(); - - if (manager.coin == Coin.firo || manager.coin == Coin.firoTestNet) { - balance += (manager.wallet as FiroWallet).balancePrivate.getTotal(); + Decimal balance; + int decimals; + String unit; + if (contractAddress == null) { + balance = manager.balance.getTotal(); + if (manager.coin == Coin.firo || manager.coin == Coin.firoTestNet) { + balance += (manager.wallet as FiroWallet).balancePrivate.getTotal(); + } + unit = manager.coin.ticker; + decimals = manager.coin.decimals; + } else { + final ethWallet = manager.wallet as EthereumWallet; + final contract = MainDB.instance.getEthContractSync(contractAddress!)!; + balance = ethWallet.getCachedTokenBalance(contract).getTotal(); + unit = contract.symbol; + decimals = contract.decimals; } return Text( "${Format.localizedStringAsFixed( value: balance, locale: locale, - decimalPlaces: Constants.decimalPlacesForCoin(manager.coin), - )} ${manager.coin.ticker}", + decimalPlaces: decimals, + )} $unit", style: Util.isDesktop ? STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context).extension()!.textSubtitle1, diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart index 670fb3b8c..a0ebceb12 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart @@ -1,17 +1,41 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.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/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class WalletInfoCoinIcon extends StatelessWidget { - const WalletInfoCoinIcon({Key? key, required this.coin}) : super(key: key); + const WalletInfoCoinIcon({ + Key? key, + required this.coin, + this.contractAddress, + }) : super(key: key); final Coin coin; + final String? contractAddress; @override Widget build(BuildContext context) { + Currency? currency; + if (contractAddress != null) { + currency = ExchangeDataLoadingService.instance.isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + contractAddress!, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirstSync(); + } + return Container( decoration: BoxDecoration( color: Theme.of(context) @@ -24,11 +48,17 @@ class WalletInfoCoinIcon extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.all(6), - child: SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 20, - height: 20, - ), + child: currency != null && currency.image.isNotEmpty + ? SvgPicture.network( + currency.image, + width: 20, + height: 20, + ) + : SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 20, + height: 20, + ), ), ); } diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index a5dc81aea..3863c08d3 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -14,10 +17,12 @@ class WalletInfoRow extends ConsumerWidget { Key? key, required this.walletId, this.onPressedDesktop, + this.contractAddress, this.padding = const EdgeInsets.all(0), }) : super(key: key); final String walletId; + final String? contractAddress; final VoidCallback? onPressedDesktop; final EdgeInsets padding; @@ -27,6 +32,12 @@ class WalletInfoRow extends ConsumerWidget { .watch(walletsChangeNotifierProvider.notifier) .getManagerProvider(walletId)); + EthContract? contract; + if (contractAddress != null) { + contract = ref.watch(mainDBProvider + .select((value) => value.getEthContractSync(contractAddress!))); + } + if (Util.isDesktop) { return MouseRegion( cursor: SystemMouseCursors.click, @@ -42,7 +53,10 @@ class WalletInfoRow extends ConsumerWidget { flex: 4, child: Row( children: [ - WalletInfoCoinIcon(coin: manager.coin), + WalletInfoCoinIcon( + coin: manager.coin, + contractAddress: contractAddress, + ), const SizedBox( width: 12, ), @@ -60,7 +74,7 @@ class WalletInfoRow extends ConsumerWidget { ), Expanded( flex: 4, - child: WalletInfoRowBalanceFuture( + child: WalletInfoRowBalance( walletId: walletId, ), ), @@ -89,7 +103,10 @@ class WalletInfoRow extends ConsumerWidget { } else { return Row( children: [ - WalletInfoCoinIcon(coin: manager.coin), + WalletInfoCoinIcon( + coin: manager.coin, + contractAddress: contractAddress, + ), const SizedBox( width: 12, ), @@ -98,14 +115,32 @@ class WalletInfoRow extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), - ), + contract != null + ? Row( + children: [ + Text( + contract.name, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + width: 4, + ), + CoinTickerTag( + walletId: walletId, + ), + ], + ) + : Text( + manager.walletName, + style: STextStyles.titleBold12(context), + ), const SizedBox( height: 2, ), - WalletInfoRowBalanceFuture(walletId: walletId), + WalletInfoRowBalance( + walletId: walletId, + contractAddress: contractAddress, + ), ], ), ), From b0d37804faa4da0b8ca987aedabe980a132dda52 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Mar 2023 16:21:26 -0600 Subject: [PATCH 140/208] token view pop fix --- lib/widgets/master_wallet_card.dart | 2 +- lib/widgets/wallet_card.dart | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/widgets/master_wallet_card.dart b/lib/widgets/master_wallet_card.dart index af8b6e196..65f65c3a1 100644 --- a/lib/widgets/master_wallet_card.dart +++ b/lib/widgets/master_wallet_card.dart @@ -125,7 +125,7 @@ class _MasterWalletCardState extends ConsumerState { child: WalletSheetCard( walletId: widget.walletId, contractAddress: e, - // popPrevious: true, + popPrevious: true, ), ), ), diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 4648d6bf7..fa134aa15 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -12,12 +12,11 @@ import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; import 'package:tuple/tuple.dart'; -import '../utilities/show_loading.dart'; - class WalletSheetCard extends ConsumerWidget { const WalletSheetCard({ Key? key, @@ -45,15 +44,17 @@ class WalletSheetCard extends ConsumerWidget { ), ), onPressed: () async { + final nav = Navigator.of(context); + final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { await manager.initializeExisting(); } if (context.mounted) { - if (popPrevious) Navigator.of(context).pop(); + if (popPrevious) nav.pop(); unawaited( - Navigator.of(context).pushNamed( + nav.pushNamed( WalletView.routeName, arguments: Tuple2( walletId, @@ -83,12 +84,15 @@ class WalletSheetCard extends ConsumerWidget { message: "Loading ${contract.name}", ); - if (context.mounted) { - await Navigator.of(context).pushNamed( - TokenView.routeName, - arguments: walletId, - ); - } + // pop loading + nav.pop(); + + // if (context.mounted) { + await nav.pushNamed( + TokenView.routeName, + arguments: walletId, + ); + // } } } }, From 1f4ba55828c23a44c5f201b8824b4e5205a90500 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Mar 2023 16:33:33 -0600 Subject: [PATCH 141/208] button label fix --- lib/pages/token_view/sub_widgets/token_summary.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 208cdf699..097a0a27a 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -109,7 +109,7 @@ class TokenWalletOptions extends StatelessWidget { ), TokenOptionsButton( onPressed: () {}, - subLabel: "Receive", + subLabel: "Send", iconAssetSVG: Assets.svg.send(context), ), const SizedBox( From 7a177a66160edbf767efef567eb9de39088bf299 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 28 Mar 2023 17:17:59 -0600 Subject: [PATCH 142/208] open exchange view, receive view, and WIP buy view from within token wallet --- lib/pages/buy_view/buy_in_wallet_view.dart | 9 ++- lib/pages/buy_view/buy_view.dart | 17 +++-- lib/pages/exchange_view/exchange_form.dart | 19 ++++-- .../wallet_initiated_exchange_view.dart | 4 ++ .../token_view/sub_widgets/token_summary.dart | 62 +++++++++++++++++-- lib/route_generator.dart | 32 ++++++++++ 6 files changed, 122 insertions(+), 21 deletions(-) diff --git a/lib/pages/buy_view/buy_in_wallet_view.dart b/lib/pages/buy_view/buy_in_wallet_view.dart index 09cbb6857..40ca89a95 100644 --- a/lib/pages/buy_view/buy_in_wallet_view.dart +++ b/lib/pages/buy_view/buy_in_wallet_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/buy_view/buy_view.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -10,11 +11,13 @@ class BuyInWalletView extends StatefulWidget { const BuyInWalletView({ Key? key, required this.coin, + this.contract, }) : super(key: key); static const String routeName = "/stackBuyInWalletView"; final Coin? coin; + final EthContract? contract; @override State createState() => _BuyInWalletViewState(); @@ -41,7 +44,11 @@ class _BuyInWalletViewState extends State { style: STextStyles.navBarTitle(context), ), ), - body: BuyView(coin: widget.coin), + body: BuyView( + coin: widget.coin, + tokenContract, + widget.contract, + ), ), ); } diff --git a/lib/pages/buy_view/buy_view.dart b/lib/pages/buy_view/buy_view.dart index dca907b59..4136bc5e1 100644 --- a/lib/pages/buy_view/buy_view.dart +++ b/lib/pages/buy_view/buy_view.dart @@ -1,23 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/buy_view/buy_form.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -class BuyView extends StatefulWidget { +class BuyView extends StatelessWidget { const BuyView({ Key? key, this.coin, + this.tokenContract, }) : super(key: key); static const String routeName = "/stackBuyView"; final Coin? coin; - - @override - State createState() => _BuyViewState(); -} - -class _BuyViewState extends State { - late final Coin? coin; + final EthContract? tokenContract; @override Widget build(BuildContext context) { @@ -30,7 +26,10 @@ class _BuyViewState extends State { right: 16, top: 16, ), - child: BuyForm(coin: widget.coin), + child: BuyForm( + coin: coin, + tokenContract: tokenContract, + ), ), ); } diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index ab7cab56e..76bba50ad 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/models/exchange/aggregate_currency.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_1_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_2_view.dart'; @@ -43,10 +44,12 @@ class ExchangeForm extends ConsumerStatefulWidget { Key? key, this.walletId, this.coin, + this.contract, }) : super(key: key); final String? walletId; final Coin? coin; + final EthContract? contract; @override ConsumerState createState() => _ExchangeFormState(); @@ -161,10 +164,16 @@ class _ExchangeFormState extends ConsumerState { final type = (ref.read(exchangeFormStateProvider).exchangeRateType); final fromTicker = ref.read(exchangeFormStateProvider).fromTicker ?? ""; - if (walletInitiated && - fromTicker.toLowerCase() == coin!.ticker.toLowerCase()) { - // do not allow changing away from wallet coin - return; + if (walletInitiated) { + if (widget.contract != null && + fromTicker.toLowerCase() == widget.contract!.symbol.toLowerCase()) { + return; + } + + if (fromTicker.toLowerCase() == coin!.ticker.toLowerCase()) { + // do not allow changing away from wallet coin + return; + } } final selectedCurrency = await _showCurrencySelectionSheet( @@ -620,7 +629,7 @@ class _ExchangeFormState extends ConsumerState { ref.read(exchangeFormStateProvider).reset(shouldNotifyListeners: true); ExchangeDataLoadingService.instance .getAggregateCurrency( - coin!.ticker, + widget.contract == null ? coin!.ticker : widget.contract!.symbol, ExchangeRateType.estimated, ) .then((value) { diff --git a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart index 99ba04979..c2fd2d092 100644 --- a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart +++ b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/exchange_view/exchange_form.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart'; import 'package:stackwallet/providers/exchange/exchange_form_state_provider.dart'; @@ -20,12 +21,14 @@ class WalletInitiatedExchangeView extends ConsumerStatefulWidget { Key? key, required this.walletId, required this.coin, + this.contract, }) : super(key: key); static const String routeName = "/walletInitiatedExchange"; final String walletId; final Coin coin; + final EthContract? contract; @override ConsumerState createState() => @@ -172,6 +175,7 @@ class _WalletInitiatedExchangeViewState ExchangeForm( walletId: walletId, coin: coin, + contract: widget.contract, ), ], ), diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 097a0a27a..4fce496e8 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -1,6 +1,12 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; +import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; +import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -9,6 +15,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:tuple/tuple.dart'; class TokenSummary extends ConsumerWidget { const TokenSummary({ @@ -84,7 +91,10 @@ class TokenSummary extends ConsumerWidget { const SizedBox( height: 20, ), - const TokenWalletOptions(), + TokenWalletOptions( + walletId: walletId, + tokenContract: token, + ), ], ), ); @@ -92,7 +102,39 @@ class TokenSummary extends ConsumerWidget { } class TokenWalletOptions extends StatelessWidget { - const TokenWalletOptions({Key? key}) : super(key: key); + const TokenWalletOptions({ + Key? key, + required this.walletId, + required this.tokenContract, + }) : super(key: key); + + final String walletId; + final EthContract tokenContract; + + void _onExchangePressed(BuildContext context) async { + unawaited( + Navigator.of(context).pushNamed( + WalletInitiatedExchangeView.routeName, + arguments: Tuple3( + walletId, + Coin.ethereum, + tokenContract, + ), + ), + ); + } + + void _onBuyPressed(BuildContext context) { + unawaited( + Navigator.of(context).pushNamed( + BuyInWalletView.routeName, + arguments: Tuple2( + Coin.ethereum, + tokenContract, + ), + ), + ); + } @override Widget build(BuildContext context) { @@ -100,7 +142,15 @@ class TokenWalletOptions extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ TokenOptionsButton( - onPressed: () {}, + onPressed: () { + Navigator.of(context).pushNamed( + ReceiveView.routeName, + arguments: Tuple2( + walletId, + Coin.ethereum, + ), + ); + }, subLabel: "Receive", iconAssetSVG: Assets.svg.receive(context), ), @@ -116,15 +166,15 @@ class TokenWalletOptions extends StatelessWidget { width: 16, ), TokenOptionsButton( - onPressed: () {}, - subLabel: "Exchange", + onPressed: () => _onExchangePressed(context), + subLabel: "Swap", iconAssetSVG: Assets.svg.exchange(context), ), const SizedBox( width: 16, ), TokenOptionsButton( - onPressed: () {}, + onPressed: () => _onBuyPressed(context), subLabel: "Buy", iconAssetSVG: Assets.svg.creditCard, ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 2a94028fa..3ac49a359 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1088,6 +1088,26 @@ class RouteGenerator { ), ); } + if (args is Tuple3) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => Stack( + children: [ + WalletInitiatedExchangeView( + walletId: args.item1, + coin: args.item2, + contract: args.item3, + ), + // ExchangeLoadingOverlayView( + // unawaitedLoad: args.item3, + // ), + ], + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } return _routeError("${settings.name} invalid args: ${args.toString()}"); case NotificationsView.routeName: @@ -1350,6 +1370,18 @@ class RouteGenerator { ), ); } + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => BuyInWalletView( + coin: args.item1, + contract: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } return _routeError("${settings.name} invalid args: ${args.toString()}"); case DesktopBuyView.routeName: From 58afc461524ed022f90919f0be9ae08b6fc8001f Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Mar 2023 08:29:40 -0600 Subject: [PATCH 143/208] WIP token buy default fill in value --- lib/pages/buy_view/buy_form.dart | 19 +++++++++++++++++-- lib/pages/buy_view/buy_in_wallet_view.dart | 3 +-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index feea9579d..d98ac0a56 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/models/buy/response_objects/crypto.dart'; import 'package:stackwallet/models/buy/response_objects/fiat.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; import 'package:stackwallet/pages/buy_view/buy_quote_preview.dart'; import 'package:stackwallet/pages/buy_view/sub_widgets/crypto_selection_view.dart'; @@ -49,6 +50,7 @@ class BuyForm extends ConsumerStatefulWidget { const BuyForm({ Key? key, this.coin, + this.tokenContract, this.clipboard = const ClipboardWrapper(), this.scanner = const BarcodeScannerWrapper(), }) : super(key: key); @@ -57,6 +59,7 @@ class BuyForm extends ConsumerStatefulWidget { final ClipboardInterface clipboard; final BarcodeScannerInterface scanner; + final EthContract? tokenContract; @override ConsumerState createState() => _BuyFormState(); @@ -461,7 +464,7 @@ class _BuyFormState extends ConsumerState { // TODO launch URL }, ); - } else { + } else if (mounted) { await showDialog( context: context, barrierDismissible: true, @@ -529,7 +532,7 @@ class _BuyFormState extends ConsumerState { }, ); } - } else { + } else if (mounted) { // Error; probably amount out of bounds // String errorMessage = "${quoteResponse.exception?.errorMessage}"; // if (errorMessage.contains('must be between')) { @@ -744,6 +747,18 @@ class _BuyFormState extends ConsumerState { 'name': widget.coin?.prettyName ?? 'Bitcoin' }); + // THIS IS BAD. No way to be certain the simplex ticker points to the same + // contract as the ticker symbol of this contract + // if (widget.tokenContract != null && + // DefaultTokens.list + // .where((e) => e.address == widget.tokenContract!.address) + // .isNotEmpty) { + // selectedCrypto = Crypto.fromJson({ + // 'ticker': widget.tokenContract!.symbol, + // 'name': widget.tokenContract!.name, + // }); + // } + // TODO set initial crypto to open wallet if a wallet is open super.initState(); diff --git a/lib/pages/buy_view/buy_in_wallet_view.dart b/lib/pages/buy_view/buy_in_wallet_view.dart index 40ca89a95..e46446a7e 100644 --- a/lib/pages/buy_view/buy_in_wallet_view.dart +++ b/lib/pages/buy_view/buy_in_wallet_view.dart @@ -46,8 +46,7 @@ class _BuyInWalletViewState extends State { ), body: BuyView( coin: widget.coin, - tokenContract, - widget.contract, + tokenContract: widget.contract, ), ), ); From a5350fcc5b3cce75b7362f414b9fe933b7b317c2 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Mar 2023 08:30:56 -0600 Subject: [PATCH 144/208] comment out unused variables --- lib/pages/buy_view/buy_form.dart | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index d98ac0a56..bd77af223 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -105,11 +105,11 @@ class _BuyFormState extends ConsumerState { static Decimal minFiat = Decimal.fromInt(50); static Decimal maxFiat = Decimal.fromInt(20000); - // We can't get crypto min and max without asking for a quote - static Decimal minCrypto = Decimal.parse((0.00000001) - .toString()); // lol how to go from double->Decimal more easily? - static Decimal maxCrypto = Decimal.parse((10000.00000000).toString()); - static String boundedCryptoTicker = ''; + // // We can't get crypto min and max without asking for a quote + // static Decimal minCrypto = Decimal.parse((0.00000001) + // .toString()); // lol how to go from double->Decimal more easily? + // static Decimal maxCrypto = Decimal.parse((10000.00000000).toString()); + // static String boundedCryptoTicker = ''; String _amountOutOfRangeErrorString = ""; void validateAmount() { @@ -168,13 +168,13 @@ class _BuyFormState extends ConsumerState { coins: ref.read(simplexProvider).supportedCryptos, onSelected: (crypto) { setState(() { - if (selectedCrypto?.ticker != _BuyFormState.boundedCryptoTicker) { - // Reset crypto mins and maxes ... we don't know these bounds until we request a quote - _BuyFormState.minCrypto = Decimal.parse((0.00000001) - .toString()); // lol how to go from double->Decimal more easily? - _BuyFormState.maxCrypto = - Decimal.parse((10000.00000000).toString()); - } + // if (selectedCrypto?.ticker != _BuyFormState.boundedCryptoTicker) { + // // Reset crypto mins and maxes ... we don't know these bounds until we request a quote + // _BuyFormState.minCrypto = Decimal.parse((0.00000001) + // .toString()); // lol how to go from double->Decimal more easily? + // _BuyFormState.maxCrypto = + // Decimal.parse((10000.00000000).toString()); + // } selectedCrypto = crypto; }); }, From ac0c65e26bab5bd021a204e7890fb2a2022c2076 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Mar 2023 08:58:30 -0600 Subject: [PATCH 145/208] db version store bug fix --- lib/utilities/db_version_migration.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index 5ecf0746a..ab893b5f9 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -285,7 +285,7 @@ class DbVersionMigrator with WalletDB { // update version await DB.instance.put( - boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 7); + boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 8); // try to continue migrating return await migrate(8, secureStore: secureStore); From bfeb3e0d30b350cfe8093d2a47b0e9c4d6f08243 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Mar 2023 12:49:12 -0600 Subject: [PATCH 146/208] WIP eth fees --- .../coins/ethereum/ethereum_wallet.dart | 13 +++--- lib/services/ethereum/ethereum_api.dart | 17 ++++---- .../ethereum/ethereum_token_service.dart | 9 +--- lib/utilities/eth_commons.dart | 43 +++---------------- 4 files changed, 22 insertions(+), 60 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 581770a2c..06c7ea41c 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -245,7 +245,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { - final fee = estimateFee(feeRate, _gasLimit, 18); + final fee = estimateFee(feeRate, _gasLimit, coin.decimals); return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin); } @@ -258,10 +258,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - Future get fees => _feeObject ??= _getFees(); - Future? _feeObject; - - Future _getFees() => EthereumAPI.getFees(); + Future get fees => EthereumAPI.getFees(); //Full rescan is not needed for ETH since we have a balance @override @@ -758,13 +755,13 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.50, walletId)); - final feeObj = _getFees(); + // final feeObj = _getFees(); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.60, walletId)); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.70, walletId)); - _feeObject = Future(() => feeObj); + // _feeObject = Future(() => feeObj); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.80, walletId)); @@ -772,7 +769,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { await Future.wait([ updateBalance(), newTxDataFuture, - feeObj, + // feeObj, allTxsToWatch, ]); GlobalEventBus.instance diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 89543fde8..cfc5c456c 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -34,12 +34,6 @@ class EthereumResponse { abstract class EthereumAPI { static String get stackBaseServer => DefaultNodes.ethereum.host; - // static const etherscanApi = - // "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update - - static const gasTrackerUrl = - "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; - static Future>> getEthTransactions( String address) async { try { @@ -336,10 +330,15 @@ abstract class EthereumAPI { } static Future getGasOracle() async { - final response = await get(Uri.parse(gasTrackerUrl)); + final response = await get( + Uri.parse( + "https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP", + ), + ); if (response.statusCode == 200) { - return GasTracker.fromJson( - json.decode(response.body) as Map); + final json = jsonDecode(response.body) as Map; + + return GasTracker.fromJson(json["result"] as Map); } else { throw Exception('Failed to load gas oracle'); } diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 7d184bb7d..6081d3d55 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -105,16 +105,11 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { .findFirst(); Future estimateFeeFor(int satoshiAmount, int feeRate) async { - final fee = estimateFee(feeRate, _gasLimit, tokenContract.decimals); + final fee = estimateFee(feeRate, _gasLimit, coin.decimals); return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin); } - Future get fees => _feeObject ??= _getFees(); - Future? _feeObject; - - Future _getFees() async { - return await EthereumAPI.getFees(); - } + Future get fees => EthereumAPI.getFees(); Future _updateTokenABI({ required EthContract forContract, diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index c3b3b83c7..c71aa27af 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -4,40 +4,11 @@ import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import "package:hex/hex.dart"; -class AddressTransaction { - final String message; - final List result; - final String status; - - const AddressTransaction({ - required this.message, - required this.result, - required this.status, - }); - - factory AddressTransaction.fromJson(List json) { - return AddressTransaction( - message: "", - result: json, - status: "", - ); - } - - @override - String toString() { - return "AddressTransaction: {" - "\n\t message: $message," - "\n\t status: $status," - "\n\t result: $result," - "\n}"; - } -} - class GasTracker { - final double average; - final double fast; - final double slow; - // final Map data; + // gwei + final int average; + final int fast; + final int slow; const GasTracker({ required this.average, @@ -47,9 +18,9 @@ class GasTracker { factory GasTracker.fromJson(Map json) { return GasTracker( - average: json['average'] as double, - fast: json['fast'] as double, - slow: json['slow'] as double, + average: int.parse(json['ProposeGasPrice'] as String), + fast: int.parse(json['FastGasPrice'] as String), + slow: int.parse(json['SafeGasPrice'] as String), ); } } From d2d353110291f39edc555db2ff9573d36de19166 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Mar 2023 15:29:27 -0600 Subject: [PATCH 147/208] token summary refresh button edit --- .../token_view/sub_widgets/token_summary.dart | 126 ++++++++++-------- lib/pages/token_view/token_view.dart | 59 ++++---- 2 files changed, 98 insertions(+), 87 deletions(-) diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 4fce496e8..705b89b95 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -8,7 +8,9 @@ import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/providers/global/wallets_provider.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/enums/coin_enum.dart'; @@ -21,9 +23,11 @@ class TokenSummary extends ConsumerWidget { const TokenSummary({ Key? key, required this.walletId, + required this.initialSyncStatus, }) : super(key: key); final String walletId; + final WalletSyncStatus initialSyncStatus; @override Widget build(BuildContext context, WidgetRef ref) { @@ -32,71 +36,85 @@ class TokenSummary extends ConsumerWidget { final balance = ref.watch(tokenServiceProvider.select((value) => value!.balance)); - return RoundedContainer( - color: const Color(0xFFE9EAFF), // todo: fix color - // color: Theme.of(context).extension()!., - padding: const EdgeInsets.all(24), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, + return Stack( + children: [ + RoundedContainer( + color: const Color(0xFFE9EAFF), // todo: fix color + // color: Theme.of(context).extension()!., + padding: const EdgeInsets.all(24), + child: Column( children: [ - SvgPicture.asset( - Assets.svg.walletDesktop, - color: const Color(0xFF8488AB), // todo: fix color - width: 12, - height: 12, - ), - const SizedBox( - width: 6, - ), - Text( - ref.watch( - walletsChangeNotifierProvider.select( - (value) => value.getManager(walletId).walletName, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.walletDesktop, + color: const Color(0xFF8488AB), // todo: fix color + width: 12, + height: 12, ), - ), - style: STextStyles.w500_12(context).copyWith( - color: const Color(0xFF8488AB), // todo: fix color - ), - ), - ], - ), - const SizedBox( - height: 6, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "${balance.getTotal()}" - " ${token.symbol}", - style: STextStyles.pageTitleH1(context), + const SizedBox( + width: 6, + ), + Text( + ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).walletName, + ), + ), + style: STextStyles.w500_12(context).copyWith( + color: const Color(0xFF8488AB), // todo: fix color + ), + ), + ], ), const SizedBox( - width: 10, + height: 6, ), - CoinTickerTag( + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "${balance.getTotal()}" + " ${token.symbol}", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + width: 10, + ), + CoinTickerTag( + walletId: walletId, + ), + ], + ), + const SizedBox( + height: 6, + ), + Text( + "FIXME: price", + style: STextStyles.subtitle500(context), + ), + const SizedBox( + height: 20, + ), + TokenWalletOptions( walletId: walletId, + tokenContract: token, ), ], ), - const SizedBox( - height: 6, - ), - Text( - "FIXME: price", - style: STextStyles.subtitle500(context), - ), - const SizedBox( - height: 20, - ), - TokenWalletOptions( + ), + Positioned( + top: 10, + right: 10, + child: WalletRefreshButton( walletId: walletId, - tokenContract: token, + initialSyncStatus: initialSyncStatus, + tokenContractAddress: ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.address)), ), - ], - ), + ), + ], ); } } diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 665b829c3..0c8a31a5d 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -5,7 +5,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; @@ -68,14 +67,11 @@ class _TokenViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - // centerTitle: true + centerTitle: true, title: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - width: 5, - ), Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -103,36 +99,32 @@ class _TokenViewState extends ConsumerState { ], ), ), - const SizedBox( - width: 28, - ), - WalletRefreshButton( - walletId: widget.walletId, - initialSyncStatus: initialSyncStatus, - tokenContractAddress: ref.watch(tokenServiceProvider - .select((value) => value!.tokenContract.address)), - ), - const SizedBox( - width: 6, - ), - AppBarIconButton( - icon: SvgPicture.asset( - Assets.svg.verticalEllipsis, - ), - onPressed: () { - // todo: context menu - Navigator.of(context).pushNamed( - TokenContractDetailsView.routeName, - arguments: Tuple2( - ref.watch(tokenServiceProvider - .select((value) => value!.tokenContract.address)), - widget.walletId, - ), - ); - }, - ), ], ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 2), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, + ), + onPressed: () { + // todo: context menu + Navigator.of(context).pushNamed( + TokenContractDetailsView.routeName, + arguments: Tuple2( + ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.address)), + widget.walletId, + ), + ); + }, + ), + ), + ), + ], ), body: Container( color: Theme.of(context).extension()!.background, @@ -145,6 +137,7 @@ class _TokenViewState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16), child: TokenSummary( walletId: widget.walletId, + initialSyncStatus: initialSyncStatus, ), ), const SizedBox( From 46af7c263971afb44e2d144b0b7a5ad065087205 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Mar 2023 15:36:49 -0600 Subject: [PATCH 148/208] Correct description if user has eth wallets but they already have the token --- .../add_wallet_views/select_wallet_for_token_view.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart index 29d47ecd7..702fb4704 100644 --- a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart +++ b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart @@ -43,6 +43,7 @@ class _SelectWalletForTokenViewState extends ConsumerState { final isDesktop = Util.isDesktop; late final List ethWalletIds; + bool _hasEthWallets = false; String? _selectedWalletId; @@ -75,6 +76,8 @@ class _SelectWalletForTokenViewState walletsData.removeWhere((key, value) => value.coin != widget.entity.coin); ethWalletIds.clear(); + _hasEthWallets = walletsData.isNotEmpty; + // TODO: proper wallet data class instead of this Hive silliness for (final walletId in walletsData.values.map((e) => e.walletId).toList()) { final walletContracts = DB.instance.get( @@ -179,7 +182,9 @@ class _SelectWalletForTokenViewState ethWalletIds.isEmpty ? RoundedWhiteContainer( child: Text( - "You do not have any Ethereum wallets", + _hasEthWallets + ? "All current Ethereum wallets already have ${widget.entity.name}" + : "You do not have any Ethereum wallets", style: STextStyles.label(context), ), ) From 2181e10163ac4c0cf4f5e110f2277d26877c80ec Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Mar 2023 15:40:01 -0600 Subject: [PATCH 149/208] show notification on successful token addition to wallet --- .../add_wallet_views/select_wallet_for_token_view.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart index 702fb4704..162c2c01c 100644 --- a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart +++ b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; @@ -58,6 +59,12 @@ class _SelectWalletForTokenViewState wallet.updateWalletTokenContractAddresses(tokenSet.toList()); Navigator.of(context).pushNamed(HomeView.routeName); + showFloatingFlushBar( + type: FlushBarType.success, + message: + "${widget.entity.name} (${widget.entity.ticker}) added to ${wallet.walletName}", + context: context, + ); } void _onAddNewEthWallet() { From b7eaf9a8c2b6cc950c2b177d4871778cc3657d67 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 29 Mar 2023 15:57:52 -0600 Subject: [PATCH 150/208] fetch correct proxy token ABI --- lib/services/ethereum/ethereum_api.dart | 121 ++++++------------ .../ethereum/ethereum_token_service.dart | 108 ++++++++-------- 2 files changed, 91 insertions(+), 138 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index cfc5c456c..a0f134614 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -465,89 +465,42 @@ abstract class EthereumAPI { } } - // static Future> getTokenAbi22( - // String contractAddress) async { - // try { - // final response = await get( - // Uri.parse( - // "https://api.etherscan.io/api?module=contract&action=getabi&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP", - // ), - // ); - // - // if (response.statusCode == 200) { - // final json = jsonDecode(response.body) as Map; - // print("========================== 222222222222222 ================"); - // dev.log((jsonDecode(json["result"] as String)).toString()); - // print( - // "============================ 2222222222222222222 =============="); - // - // return EthereumResponse( - // json["result"] as String, - // null, - // ); - // } else { - // throw EthApiException( - // "getTokenAbi($contractAddress) failed with status code: " - // "${response.statusCode}", - // ); - // } - // } on EthApiException catch (e) { - // return EthereumResponse( - // null, - // e, - // ); - // } catch (e, s) { - // Logging.instance.log( - // "getTokenAbi(): $e\n$s", - // level: LogLevel.Error, - // ); - // return EthereumResponse( - // null, - // EthApiException(e.toString()), - // ); - // } - // } + /// Fetch the underlying contract address that a proxy contract points to + static Future> getProxyTokenImplementationAddress( + String contractAddress, + ) async { + try { + final response = await get(Uri.parse( + "$stackBaseServer/state?addrs=$contractAddress&parts=proxy")); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + final list = json["data"] as List; + final map = Map.from(list.first as Map); - // /// Fetch the underlying contract address that a proxy contract points to - // static Future> getProxyTokenImplementation( - // String contractAddress) async { - // try { - // final response = await get(Uri.parse( - // "$stackURI?module=contract&action=getsourcecode&address=$contractAddress")); - // // "$etherscanApi?module=contract&action=getsourcecode&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); - // if (response.statusCode == 200) { - // final json = jsonDecode(response.body); - // if (json["message"] == "OK") { - // final list = json["result"] as List; - // final map = Map.from(list.first as Map); - // - // return EthereumResponse( - // map["Implementation"] as String, - // null, - // ); - // } else { - // throw EthApiException(json["message"] as String); - // } - // } else { - // throw EthApiException( - // "fetchProxyTokenImplementation($contractAddress) failed with status code: " - // "${response.statusCode}", - // ); - // } - // } on EthApiException catch (e) { - // return EthereumResponse( - // null, - // e, - // ); - // } catch (e, s) { - // Logging.instance.log( - // "fetchProxyTokenImplementation(): $e\n$s", - // level: LogLevel.Error, - // ); - // return EthereumResponse( - // null, - // EthApiException(e.toString()), - // ); - // } - // } + return EthereumResponse( + map["proxy"] as String, + null, + ); + } else { + throw EthApiException( + "getProxyTokenImplementationAddress($contractAddress) failed with" + " status code: ${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getProxyTokenImplementationAddress($contractAddress) : $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } } diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 6081d3d55..c57e668b2 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; @@ -163,65 +162,66 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { _sendFunction = _deployedContract.function('transfer'); } catch (_) { //==================================================================== - final list = List>.from( - jsonDecode(tokenContract.abi!) as List); - final functionNames = list.map((e) => e["name"] as String); - - if (!functionNames.contains("balanceOf")) { - list.add( - { - "encoding": "0x70a08231", - "inputs": [ - {"name": "account", "type": "address"} - ], - "name": "balanceOf", - "outputs": [ - {"name": "val_0", "type": "uint256"} - ], - "signature": "balanceOf(address)", - "type": "function" - }, - ); - } - - if (!functionNames.contains("transfer")) { - list.add( - { - "encoding": "0xa9059cbb", - "inputs": [ - {"name": "dst", "type": "address"}, - {"name": "rawAmount", "type": "uint256"} - ], - "name": "transfer", - "outputs": [ - {"name": "val_0", "type": "bool"} - ], - "signature": "transfer(address,uint256)", - "type": "function" - }, - ); - } + // final list = List>.from( + // jsonDecode(tokenContract.abi!) as List); + // final functionNames = list.map((e) => e["name"] as String); + // + // if (!functionNames.contains("balanceOf")) { + // list.add( + // { + // "encoding": "0x70a08231", + // "inputs": [ + // {"name": "account", "type": "address"} + // ], + // "name": "balanceOf", + // "outputs": [ + // {"name": "val_0", "type": "uint256"} + // ], + // "signature": "balanceOf(address)", + // "type": "function" + // }, + // ); + // } + // + // if (!functionNames.contains("transfer")) { + // list.add( + // { + // "encoding": "0xa9059cbb", + // "inputs": [ + // {"name": "dst", "type": "address"}, + // {"name": "rawAmount", "type": "uint256"} + // ], + // "name": "transfer", + // "outputs": [ + // {"name": "val_0", "type": "bool"} + // ], + // "signature": "transfer(address,uint256)", + // "type": "function" + // }, + // ); + // } //-------------------------------------------------------------------- //==================================================================== // function not found so likely a proxy so we need to fetch the impl //==================================================================== - final updatedToken = tokenContract.copyWith(abi: jsonEncode(list)); - // Store updated contract - final id = await MainDB.instance.putEthContract(updatedToken); - _tokenContract = updatedToken..id = id; + // final updatedToken = tokenContract.copyWith(abi: jsonEncode(list)); + // // Store updated contract + // final id = await MainDB.instance.putEthContract(updatedToken); + // _tokenContract = updatedToken..id = id; //-------------------------------------------------------------------- - // final contractAddressResponse = - // await EthereumAPI.getProxyTokenImplementation(contractAddress.hex); - // - // if (contractAddressResponse.value != null) { - // _tokenContract = await _updateTokenABI( - // forContract: tokenContract, - // usingContractAddress: contractAddressResponse.value!, - // ); - // } else { - // throw contractAddressResponse.exception!; - // } + final contractAddressResponse = + await EthereumAPI.getProxyTokenImplementationAddress( + contractAddress.hex); + + if (contractAddressResponse.value != null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddressResponse.value!, + ); + } else { + throw contractAddressResponse.exception!; + } //==================================================================== } From af460b54a623434f7ea9e3b74353a9653f27e83e Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 10:40:35 -0600 Subject: [PATCH 151/208] token icon based on changenow currency image url --- lib/widgets/icon_widgets/eth_token_icon.dart | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 lib/widgets/icon_widgets/eth_token_icon.dart diff --git a/lib/widgets/icon_widgets/eth_token_icon.dart b/lib/widgets/icon_widgets/eth_token_icon.dart new file mode 100644 index 000000000..2b94d1554 --- /dev/null +++ b/lib/widgets/icon_widgets/eth_token_icon.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; +import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class EthTokenIcon extends StatefulWidget { + const EthTokenIcon({ + Key? key, + required this.contractAddress, + }) : super(key: key); + + final String contractAddress; + + @override + State createState() => _EthTokenIconState(); +} + +class _EthTokenIconState extends State { + late final String? imageUrl; + + @override + void initState() { + imageUrl = ExchangeDataLoadingService.instance.isar.currencies + .where() + .filter() + .tokenContractEqualTo(widget.contractAddress, caseSensitive: false) + .findFirstSync() + ?.image; + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (imageUrl == null || imageUrl!.isEmpty) { + return SvgPicture.asset( + Assets.svg.iconFor(coin: Coin.ethereum), + width: 22, + height: 22, + ); + } else { + return SvgPicture.network( + imageUrl!, + width: 22, + height: 22, + ); + } + } +} From 61894c9e8edd9c12f68b1e5ac8b5da5e7e0e503b Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 10:41:28 -0600 Subject: [PATCH 152/208] price service token tweaks --- lib/main.dart | 8 +++++++- .../global_settings_view/hidden_settings.dart | 15 ++++++++++----- lib/services/price_service.dart | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6519a6596..06a97e881 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,7 +23,7 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; -import 'package:stackwallet/models/isar/models/log.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/models.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/notification_model.dart'; @@ -251,6 +251,12 @@ class _MaterialAppWithThemeState extends ConsumerState await ref.read(storageCryptoHandlerProvider).hasPassword(); } await MainDB.instance.initMainDB(); + ref + .read(priceAnd24hChangeNotifierProvider) + .tokenContractAddressesToCheck + .addAll( + await MainDB.instance.getEthContracts().addressProperty().findAll(), + ); } Future load() async { diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index bd7ba53c1..d0f63db3f 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/services/price_service.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -164,10 +163,16 @@ class HiddenSettings extends StatelessWidget { Consumer(builder: (_, ref, __) { return GestureDetector( onTap: () async { - PriceService.tokenContractAddressesToCheck.add( - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - PriceService.tokenContractAddressesToCheck.add( - "0xdAC17F958D2ee523a2206206994597C13D831ec7"); + ref + .read(priceAnd24hChangeNotifierProvider) + .tokenContractAddressesToCheck + .add( + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + ref + .read(priceAnd24hChangeNotifierProvider) + .tokenContractAddressesToCheck + .add( + "0xdAC17F958D2ee523a2206206994597C13D831ec7"); await ref .read(priceAnd24hChangeNotifierProvider) .updatePrice(); diff --git a/lib/services/price_service.dart b/lib/services/price_service.dart index ee5b5a9d7..b699af943 100644 --- a/lib/services/price_service.dart +++ b/lib/services/price_service.dart @@ -9,7 +9,7 @@ import 'package:tuple/tuple.dart'; class PriceService extends ChangeNotifier { late final String baseTicker; - static Set tokenContractAddressesToCheck = {}; + final Set tokenContractAddressesToCheck = {}; final Duration updateInterval = const Duration(seconds: 60); Timer? _timer; From 8452758d1795332adcb01787643caada42817e0b Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 10:49:07 -0600 Subject: [PATCH 153/208] added extra Amount functionality + tests --- lib/utilities/amount.dart | 17 +++++++++++++++ test/utilities/amount_test.dart | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/lib/utilities/amount.dart b/lib/utilities/amount.dart index 74d37b6b5..24d7b507c 100644 --- a/lib/utilities/amount.dart +++ b/lib/utilities/amount.dart @@ -12,6 +12,12 @@ class Amount implements Equatable { }) : assert(fractionDigits >= 0), _value = rawValue; + /// special zero case with [fractionDigits] set to 0 + static Amount get zero => Amount( + rawValue: BigInt.zero, + fractionDigits: 0, + ); + /// truncate double value to [fractionDigits] places Amount.fromDouble(double amount, {required this.fractionDigits}) : assert(fractionDigits >= 0), @@ -66,6 +72,17 @@ class Amount implements Equatable { ); } + // =========================================================================== + // ======= operators ========================================================= + + bool operator >(Amount other) => raw > other.raw; + + bool operator <(Amount other) => raw < other.raw; + + bool operator >=(Amount other) => raw >= other.raw; + + bool operator <=(Amount other) => raw <= other.raw; + // =========================================================================== // ======= Overrides ========================================================= diff --git a/test/utilities/amount_test.dart b/test/utilities/amount_test.dart index 0274ea2bb..882055b3f 100644 --- a/test/utilities/amount_test.dart +++ b/test/utilities/amount_test.dart @@ -78,4 +78,41 @@ void main() { } expect(didThrow, true); }); + + group("operators", () { + final one = Amount(rawValue: BigInt.one, fractionDigits: 0); + final two = Amount(rawValue: BigInt.two, fractionDigits: 0); + + test(">", () { + expect(one > two, false); + expect(one > one, false); + + expect(two > two, false); + expect(two > one, true); + }); + + test("<", () { + expect(one < two, true); + expect(one < one, false); + + expect(two < two, false); + expect(two < one, false); + }); + + test(">=", () { + expect(one >= two, false); + expect(one >= one, true); + + expect(two >= two, true); + expect(two >= one, true); + }); + + test("<=", () { + expect(one <= two, true); + expect(one <= one, true); + + expect(two <= two, true); + expect(two <= one, false); + }); + }); } From 3064d31a3bb6e3f9e7fd7960d2f8bfce10a3af18 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 10:55:02 -0600 Subject: [PATCH 154/208] WIP token send view --- lib/pages/send_view/token_send_view.dart | 1251 +++++++++++++++++ .../token_view/sub_widgets/token_summary.dart | 12 +- lib/route_generator.dart | 17 + 3 files changed, 1279 insertions(+), 1 deletion(-) create mode 100644 lib/pages/send_view/token_send_view.dart diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart new file mode 100644 index 000000000..5f58508f3 --- /dev/null +++ b/lib/pages/send_view/token_send_view.dart @@ -0,0 +1,1251 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/send_view_auto_fill_data.dart'; +import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; +import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/animated_text.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/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class TokenSendView extends ConsumerStatefulWidget { + const TokenSendView({ + Key? key, + required this.walletId, + required this.coin, + required this.tokenContract, + this.autoFillData, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + }) : super(key: key); + + static const String routeName = "/tokenSendView"; + + final String walletId; + final Coin coin; + final EthContract tokenContract; + final SendViewAutoFillData? autoFillData; + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + + @override + ConsumerState createState() => _TokenSendViewState(); +} + +class _TokenSendViewState extends ConsumerState { + late final String walletId; + late final Coin coin; + late final EthContract tokenContract; + late final ClipboardInterface clipboard; + late final BarcodeScannerInterface scanner; + + late TextEditingController sendToController; + late TextEditingController cryptoAmountController; + late TextEditingController baseAmountController; + late TextEditingController noteController; + late TextEditingController feeController; + + late final SendViewAutoFillData? _data; + + final _addressFocusNode = FocusNode(); + final _noteFocusNode = FocusNode(); + final _cryptoFocus = FocusNode(); + final _baseFocus = FocusNode(); + + Amount? _amountToSend; + Amount? _cachedAmountToSend; + String? _address; + + bool _addressToggleFlag = false; + + bool _cryptoAmountChangeLock = false; + late VoidCallback onCryptoAmountChanged; + + final updateFeesTimerDuration = const Duration(milliseconds: 500); + + Timer? _cryptoAmountChangedFeeUpdateTimer; + Timer? _baseAmountChangedFeeUpdateTimer; + late Future _calculateFeesFuture; + Map cachedFees = {}; + + void _onTokenSendViewPasteAddressFieldButtonPressed() async { + final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring(0, content.indexOf("\n")); + } + sendToController.text = content.trim(); + _address = content.trim(); + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } + + void _onTokenSendViewScanQrButtonPressed() async { + try { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + // Future.delayed( + // const Duration(seconds: 2), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + + Logging.instance.log("qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); + + if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { + // auto fill address + _address = (results["address"] ?? "").trim(); + sendToController.text = _address!; + + // autofill notes field + if (results["message"] != null) { + noteController.text = results["message"]!; + } else if (results["label"] != null) { + noteController.text = results["label"]!; + } + + // autofill amount field + if (results["amount"] != null) { + final amount = Decimal.parse(results["amount"]!); + cryptoAmountController.text = Format.localizedStringAsFixed( + value: amount, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlacesForCoin(coin), + ); + amount.toString(); + _amountToSend = Amount.fromDecimal( + amount, + fractionDigits: tokenContract.decimals, + ); + } + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(qrResult.rawContent)) { + _address = qrResult.rawContent.trim(); + sendToController.text = _address ?? ""; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true; + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); + } + } + + void _onFiatAmountFieldChanged(String baseAmountString) { + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = Amount.fromDecimal( + baseAmountString.contains(",") + ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) + : Decimal.parse(baseAmountString), + fractionDigits: tokenContract.decimals, + ); + + final _price = ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + .item1; + + if (_price == Decimal.zero) { + _amountToSend = Amount.zero; + } else { + _amountToSend = baseAmount <= Amount.zero + ? Amount.zero + : Amount.fromDecimal( + (baseAmount.decimal / _price).toDecimal( + scaleOnInfinitePrecision: tokenContract.decimals), + fractionDigits: tokenContract.decimals); + } + if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final amountString = Format.localizedStringAsFixed( + value: _amountToSend!.decimal, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlacesForCoin(coin), + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Amount.zero; + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis( + // _amountToSend!)); + // }); + _updatePreviewButtonState(_address, _amountToSend); + } + + void _cryptoAmountChanged() async { + if (!_cryptoAmountChangeLock) { + final String cryptoAmount = cryptoAmountController.text; + if (cryptoAmount.isNotEmpty && + cryptoAmount != "." && + cryptoAmount != ",") { + _amountToSend = Amount.fromDecimal( + cryptoAmount.contains(",") + ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) + : Decimal.parse(cryptoAmount), + fractionDigits: tokenContract.decimals); + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final price = ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + .item1; + + if (price > Decimal.zero) { + final String fiatAmountString = Format.localizedStringAsFixed( + value: _amountToSend!.decimal * price, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: 2, + ); + + baseAmountController.text = fiatAmountString; + } + } else { + _amountToSend = null; + baseAmountController.text = ""; + } + + _updatePreviewButtonState(_address, _amountToSend); + + _cryptoAmountChangedFeeUpdateTimer?.cancel(); + _cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { + if (coin != Coin.epicCash && !_baseFocus.hasFocus) { + setState(() { + _calculateFeesFuture = calculateFees( + _amountToSend == null ? 0 : _amountToSend!.raw.toInt(), + ); + }); + } + }); + } + } + + void _baseAmountChanged() { + _baseAmountChangedFeeUpdateTimer?.cancel(); + _baseAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { + if (coin != Coin.epicCash && !_cryptoFocus.hasFocus) { + setState(() { + _calculateFeesFuture = calculateFees( + _amountToSend == null ? 0 : _amountToSend!.raw.toInt(), + ); + }); + } + }); + } + + String? _updateInvalidAddressText(String address, Manager manager) { + if (_data != null && _data!.contactLabel == address) { + return null; + } + if (address.isNotEmpty && !manager.validateAddress(address)) { + return "Invalid address"; + } + return null; + } + + void _updatePreviewButtonState(String? address, Amount? amount) { + final isValidAddress = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(address ?? ""); + ref.read(previewTxButtonStateProvider.state).state = + (isValidAddress && amount != null && amount > Amount.zero); + } + + Future calculateFees(int amount) async { + if (amount <= 0) { + return "0"; + } + + if (cachedFees[amount] != null) { + return cachedFees[amount]!; + } + + final wallet = ref.read(tokenServiceProvider)!; + final feeObject = await wallet.fees; + + late final int feeRate; + + switch (ref.read(feeRateTypeStateProvider.state).state) { + case FeeRateType.fast: + feeRate = feeObject.fast; + break; + case FeeRateType.average: + feeRate = feeObject.medium; + break; + case FeeRateType.slow: + feeRate = feeObject.slow; + break; + } + + int fee; + + fee = await wallet.estimateFeeFor(amount, feeRate); + cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + + return cachedFees[amount]!; + } + + Future _previewTransaction() async { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + final tokenWallet = ref.read(tokenServiceProvider)!; + + final Amount amount = _amountToSend!; + final Amount availableBalance = Amount.fromDecimal( + tokenWallet.balance.getSpendable(), + fractionDigits: tokenContract.decimals, + ); + + // confirm send all + if (amount == availableBalance) { + bool? shouldSendAll; + if (mounted) { + shouldSendAll = await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Confirm send all", + message: + "You are about to send your entire balance. Would you like to continue?", + leftButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Cancel", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Yes", + style: STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ); + } + + if (shouldSendAll == null || shouldSendAll == false) { + // cancel preview + return; + } + } + + try { + bool wasCancelled = false; + + if (mounted) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + coin: manager.coin, + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + } + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + Map txData; + Future> txDataFuture; + + txDataFuture = tokenWallet.prepareSend( + address: _address!, + satoshiAmount: amount.raw.toInt(), + args: { + "feeRate": ref.read(feeRateTypeStateProvider), + }, + ); + + final results = await Future.wait([ + txDataFuture, + time, + ]); + + txData = results.first as Map; + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context).pop(); + txData["note"] = noteController.text; + + txData["address"] = _address; + + unawaited(Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + ), + settings: const RouteSettings( + name: ConfirmTransactionView.routeName, + ), + ), + )); + } + } catch (e) { + if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + unawaited(showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + )); + } + } + } + + @override + void initState() { + ref.refresh(feeSheetSessionCacheProvider); + + _calculateFeesFuture = calculateFees(0); + _data = widget.autoFillData; + walletId = widget.walletId; + coin = widget.coin; + tokenContract = widget.tokenContract; + clipboard = widget.clipboard; + scanner = widget.barcodeScanner; + + sendToController = TextEditingController(); + cryptoAmountController = TextEditingController(); + baseAmountController = TextEditingController(); + noteController = TextEditingController(); + feeController = TextEditingController(); + + onCryptoAmountChanged = _cryptoAmountChanged; + cryptoAmountController.addListener(onCryptoAmountChanged); + baseAmountController.addListener(_baseAmountChanged); + + if (_data != null) { + if (_data!.amount != null) { + cryptoAmountController.text = _data!.amount!.toString(); + } + sendToController.text = _data!.contactLabel; + _address = _data!.address.trim(); + _addressToggleFlag = true; + } + + super.initState(); + } + + @override + void dispose() { + _cryptoAmountChangedFeeUpdateTimer?.cancel(); + _baseAmountChangedFeeUpdateTimer?.cancel(); + + cryptoAmountController.removeListener(onCryptoAmountChanged); + baseAmountController.removeListener(_baseAmountChanged); + + sendToController.dispose(); + cryptoAmountController.dispose(); + baseAmountController.dispose(); + noteController.dispose(); + feeController.dispose(); + + _noteFocusNode.dispose(); + _addressFocusNode.dispose(); + _cryptoFocus.dispose(); + _baseFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final provider = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManagerProvider(walletId))); + final String locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Send ${tokenContract.symbol}", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + EthTokenIcon( + contractAddress: tokenContract.address, + ), + const SizedBox( + width: 6, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + ref.watch(provider.select( + (value) => value.walletName)), + style: STextStyles.titleBold12(context) + .copyWith(fontSize: 14), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + Text( + "Available balance", + style: STextStyles.label(context) + .copyWith(fontSize: 10), + ), + ], + ), + const Spacer(), + GestureDetector( + onTap: () { + cryptoAmountController.text = ref + .read(tokenServiceProvider)! + .balance + .getSpendable() + .toStringAsFixed( + tokenContract.decimals); + }, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + "${ref.read(tokenServiceProvider)!.balance.getSpendable().toStringAsFixed(tokenContract.decimals)} ${tokenContract.symbol}", + style: + STextStyles.titleBold12(context) + .copyWith( + fontSize: 10, + ), + textAlign: TextAlign.right, + ), + Text( + "${Format.localizedStringAsFixed( + value: ref + .read( + tokenServiceProvider)! + .balance + .getSpendable() * + ref.watch( + priceAnd24hChangeNotifierProvider + .select((value) => value + .getTokenPrice( + tokenContract + .address) + .item1)), + locale: locale, + decimalPlaces: 2, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.subtitle(context) + .copyWith( + fontSize: 8, + ), + textAlign: TextAlign.right, + ) + ], + ), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + Text( + "Send to", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("tokenSendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _address = newValue.trim(); + _updatePreviewButtonState( + _address, _amountToSend); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${tokenContract.symbol} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "tokenSendViewClearAddressFieldButtonKey"), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "tokenSendViewPasteAddressFieldButtonKey"), + onTap: + _onTokenSendViewPasteAddressFieldButtonPressed, + child: sendToController + .text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey"), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: widget.coin, + ); + }, + child: const AddressBookIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey"), + onTap: + _onTokenSendViewScanQrButtonPressed, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ref + .read(walletsChangeNotifierProvider) + .getManager(walletId), + ); + + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: + STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension()! + .textError, + ), + ), + ), + ); + } + }, + ), + const SizedBox( + height: 12, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: "Send all ${tokenContract.symbol}", + onTap: () async { + cryptoAmountController.text = ref + .read(tokenServiceProvider)! + .balance + .getSpendable() + .toStringAsFixed(tokenContract.decimals); + + _cryptoAmountChanged(); + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + key: + const Key("amountInputFieldCryptoTextFieldKey"), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, + newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: + STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + tokenContract.symbol, + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + if (Prefs.instance.externalCalls) + const SizedBox( + height: 8, + ), + if (Prefs.instance.externalCalls) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + key: + const Key("amountInputFieldFiatTextFieldKey"), + controller: baseAmountController, + focusNode: _baseFocus, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a fiat amount with 2 decimal places + TextInputFormatter.withFunction((oldValue, + newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: _onFiatAmountFieldChanged, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: + STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)), + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 12, + ), + if (coin != Coin.epicCash) + Text( + "Transaction fee (estimated)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: feeController, + readOnly: true, + textInputAction: TextInputAction.none, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: RawMaterialButton( + splashColor: Theme.of(context) + .extension()! + .highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + TransactionFeeSelectionSheet( + walletId: walletId, + amount: Decimal.tryParse( + cryptoAmountController.text) ?? + Decimal.zero, + updateChosen: (String fee) { + setState(() { + _calculateFeesFuture = + Future(() => fee); + }); + }, + ), + ); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch(feeRateTypeStateProvider + .state) + .state + .prettyName, + style: STextStyles.itemSubtitle12( + context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "~${snapshot.data! as String} ${coin.ticker}", + style: + STextStyles.itemSubtitle( + context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: + STextStyles.itemSubtitle( + context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + ], + ), + ), + ) + ], + ), + const Spacer(), + const SizedBox( + height: 12, + ), + TextButton( + onPressed: ref + .watch(previewTxButtonStateProvider.state) + .state + ? _previewTransaction + : null, + style: ref + .watch(previewTxButtonStateProvider.state) + .state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + child: Text( + "Preview", + style: STextStyles.button(context), + ), + ), + const SizedBox( + height: 4, + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 705b89b95..688a9c131 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; +import 'package:stackwallet/pages/send_view/token_send_view.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; @@ -176,7 +177,16 @@ class TokenWalletOptions extends StatelessWidget { width: 16, ), TokenOptionsButton( - onPressed: () {}, + onPressed: () { + Navigator.of(context).pushNamed( + TokenSendView.routeName, + arguments: Tuple3( + walletId, + Coin.ethereum, + tokenContract, + ), + ); + }, subLabel: "Send", iconAssetSVG: Assets.svg.send(context), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 3ac49a359..80ba39a10 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -60,6 +60,7 @@ import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_vi import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; +import 'package:stackwallet/pages/send_view/token_send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; @@ -1053,6 +1054,22 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case TokenSendView.routeName: + if (args is Tuple3) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => TokenSendView( + walletId: args.item1, + coin: args.item2, + tokenContract: args.item3, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ConfirmTransactionView.routeName: if (args is Tuple2, String>) { return getRoute( From 31cc4beb431ea2fb9e0bfb2f41fed58fe3039a0c Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 11:05:43 -0600 Subject: [PATCH 155/208] test fixes --- .../sub_widgets/wallet_info_row_balance_future_test.dart | 4 ++-- test/widget_tests/wallet_info_row/wallet_info_row_test.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.dart index 5747020ce..2d4800a36 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.dart @@ -51,7 +51,7 @@ void main() { (realInvocation) => ChangeNotifierProvider((ref) => manager)); const walletInfoRowBalance = - WalletInfoRowBalanceFuture(walletId: "some-wallet-id"); + WalletInfoRowBalance(walletId: "some-wallet-id"); await widgetTester.pumpWidget( ProviderScope( overrides: [ @@ -76,6 +76,6 @@ void main() { await widgetTester.pumpAndSettle(); - expect(find.byType(WalletInfoRowBalanceFuture), findsOneWidget); + expect(find.byType(WalletInfoRowBalance), findsOneWidget); }); } diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.dart index 67f7527be..c14cbb896 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.dart @@ -74,6 +74,6 @@ void main() { await widgetTester.pumpAndSettle(); expect(find.text("some wallet"), findsOneWidget); - expect(find.byType(WalletInfoRowBalanceFuture), findsOneWidget); + expect(find.byType(WalletInfoRowBalance), findsOneWidget); }); } From f0d414bc869c145d21cf05c040c43747f026bf2f Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 12:26:19 -0600 Subject: [PATCH 156/208] formatting clean up --- lib/utilities/amount.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utilities/amount.dart b/lib/utilities/amount.dart index 24d7b507c..ec9e746cc 100644 --- a/lib/utilities/amount.dart +++ b/lib/utilities/amount.dart @@ -50,10 +50,10 @@ class Amount implements Equatable { @Deprecated("provided for convenience only. Use fractionDigits instead.") int get decimals => fractionDigits; - Map toMap() { - // =========================================================================== - // ======= Serialization ===================================================== + // =========================================================================== + // ======= Serialization ===================================================== + Map toMap() { return {"raw": raw.toString(), "fractionDigits": fractionDigits}; } From 7e6f0f18cdc3c23029cee976ed79b796c901c985 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 12:57:46 -0600 Subject: [PATCH 157/208] swap from within wallet fix if started in incognito mode --- lib/pages/wallet_view/wallet_view.dart | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 42dfe47c5..3cca80566 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -270,11 +270,26 @@ class _WalletViewState extends ConsumerState { ), ); } else { - final currency = await showLoading( - whileFuture: ExchangeDataLoadingService.instance.isar.currencies + Future _future; + try { + _future = ExchangeDataLoadingService.instance.isar.currencies .where() .tickerEqualToAnyExchangeNameName(coin.ticker) - .findFirst(), + .findFirst(); + } catch (_) { + _future = ExchangeDataLoadingService.instance + .init() + .then( + (_) => ExchangeDataLoadingService.instance.loadAll(), + ) + .then((_) => ExchangeDataLoadingService.instance.isar.currencies + .where() + .tickerEqualToAnyExchangeNameName(coin.ticker) + .findFirst()); + } + + final currency = await showLoading( + whileFuture: _future, context: context, message: "Loading...", ); From bf03e6913d4b0f0f27d0c09efe0b4fbc6b7bf082 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 12:58:15 -0600 Subject: [PATCH 158/208] token summary price fix --- .../token_view/sub_widgets/token_summary.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 688a9c131..526f80b91 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -10,11 +10,15 @@ import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/token_send_view.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/price_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.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/enums/coin_enum.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/rounded_container.dart'; @@ -92,7 +96,17 @@ class TokenSummary extends ConsumerWidget { height: 6, ), Text( - "FIXME: price", + "${Format.localizedStringAsFixed( + value: ref + .read(tokenServiceProvider)! + .balance + .getSpendable() * + ref.watch(priceAnd24hChangeNotifierProvider.select( + (value) => value.getTokenPrice(token.address).item1)), + locale: ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)), + decimalPlaces: 2, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", style: STextStyles.subtitle500(context), ), const SizedBox( From a04223e0b7cf959f6d4b12bfe619dae17be58252 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 14:50:58 -0600 Subject: [PATCH 159/208] eth gas tracker updated --- lib/services/ethereum/ethereum_api.dart | 67 ++++++++++++++++++------- lib/utilities/constants.dart | 2 +- lib/utilities/enums/coin_enum.dart | 1 - lib/utilities/eth_commons.dart | 31 +++++++++--- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index a0f134614..9c65057f1 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -329,31 +329,62 @@ abstract class EthereumAPI { } } - static Future getGasOracle() async { - final response = await get( - Uri.parse( - "https://api.etherscan.io/api?module=gastracker&action=gasoracle&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP", - ), - ); - if (response.statusCode == 200) { - final json = jsonDecode(response.body) as Map; + static Future> getGasOracle() async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/gas-prices", + ), + ); - return GasTracker.fromJson(json["result"] as Map); - } else { - throw Exception('Failed to load gas oracle'); + if (response.statusCode == 200) { + final json = jsonDecode(response.body) as Map; + if (json["success"] == true) { + return EthereumResponse( + GasTracker.fromJson( + Map.from(json["result"] as Map), + ), + null, + ); + } else { + throw EthApiException( + "getGasOracle() failed with response: " + "${response.body}", + ); + } + } else { + throw EthApiException( + "getGasOracle() failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getGasOracle(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); } } static Future getFees() async { - GasTracker fees = await getGasOracle(); - final feesFast = fees.fast * (pow(10, 9)); - final feesStandard = fees.average * (pow(10, 9)); - final feesSlow = fees.slow * (pow(10, 9)); + final fees = (await getGasOracle()).value!; + final feesFast = fees.fast.shift(9).toBigInt(); + final feesStandard = fees.average.shift(9).toBigInt(); + final feesSlow = fees.slow.shift(9).toBigInt(); return FeeObject( - numberOfBlocksFast: 1, - numberOfBlocksAverage: 3, - numberOfBlocksSlow: 3, + numberOfBlocksFast: fees.numberOfBlocksFast, + numberOfBlocksAverage: fees.numberOfBlocksAverage, + numberOfBlocksSlow: fees.numberOfBlocksSlow, fast: feesFast.toInt(), medium: feesStandard.toInt(), slow: feesSlow.toInt()); diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 562d44047..92e99849d 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -159,7 +159,7 @@ abstract class Constants { return 60; case Coin.ethereum: - return 60; + return 15; case Coin.monero: return 120; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index e62203628..0490174f0 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -17,7 +17,6 @@ import 'package:stackwallet/services/coins/particl/particl_wallet.dart' as particl; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; import 'package:stackwallet/utilities/constants.dart'; -import 'dart:io' show Platform; enum Coin { bitcoin, diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index c71aa27af..5b71e917d 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -2,25 +2,42 @@ import 'dart:math'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; +import 'package:decimal/decimal.dart'; import "package:hex/hex.dart"; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; class GasTracker { - // gwei - final int average; - final int fast; - final int slow; + final Decimal average; + final Decimal fast; + final Decimal slow; + + final int numberOfBlocksFast; + final int numberOfBlocksAverage; + final int numberOfBlocksSlow; + + final int timestamp; const GasTracker({ required this.average, required this.fast, required this.slow, + required this.numberOfBlocksFast, + required this.numberOfBlocksAverage, + required this.numberOfBlocksSlow, + required this.timestamp, }); factory GasTracker.fromJson(Map json) { + final targetTime = Constants.targetBlockTimeInSeconds(Coin.ethereum); return GasTracker( - average: int.parse(json['ProposeGasPrice'] as String), - fast: int.parse(json['FastGasPrice'] as String), - slow: int.parse(json['SafeGasPrice'] as String), + average: Decimal.parse(json["average"]["price"].toString()), + fast: Decimal.parse(json["fast"]["price"].toString()), + slow: Decimal.parse(json["slow"]["price"].toString()), + numberOfBlocksAverage: (json["average"]["time"] as int) ~/ targetTime, + numberOfBlocksFast: (json["fast"]["time"] as int) ~/ targetTime, + numberOfBlocksSlow: (json["slow"]["time"] as int) ~/ targetTime, + timestamp: json["timestamp"] as int, ); } } From 20c3da72a31e8c7530c9df00749e64dfad8a4d6a Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 30 Mar 2023 15:15:43 -0600 Subject: [PATCH 160/208] fix fee sheet for eth tokens --- .../transaction_fee_selection_sheet.dart | 179 ++++++++++-------- lib/pages/send_view/token_send_view.dart | 1 + 2 files changed, 100 insertions(+), 80 deletions(-) diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index 8ad6d0ca5..c6584ee9f 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -3,6 +3,7 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; @@ -11,6 +12,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/format.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/animated_text.dart'; @@ -34,11 +36,13 @@ class TransactionFeeSelectionSheet extends ConsumerStatefulWidget { required this.walletId, required this.amount, required this.updateChosen, + this.isToken = false, }) : super(key: key); final String walletId; final Decimal amount; final Function updateChosen; + final bool isToken; @override ConsumerState createState() => @@ -68,88 +72,109 @@ class _TransactionFeeSelectionSheetState switch (feeRateType) { case FeeRateType.fast: if (ref.read(feeSheetSessionCacheProvider).fast[amount] == null) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); + if (widget.isToken == false) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); - if (coin == Coin.monero || coin == Coin.wownero) { - final fee = await manager.estimateFeeFor( - amount, MoneroTransactionPriority.fast.raw!); - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); - } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && - ref.read(publicPrivateBalanceStateProvider.state).state != - "Private") { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.fast.raw!); + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); + } else { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate), + coin: coin); + } } else { + final tokenWallet = ref.read(tokenServiceProvider)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + Format.satoshisToAmount(fee, coin: coin); } } return ref.read(feeSheetSessionCacheProvider).fast[amount]!; case FeeRateType.average: if (ref.read(feeSheetSessionCacheProvider).average[amount] == null) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); - if (coin == Coin.monero || coin == Coin.wownero) { - final fee = await manager.estimateFeeFor( - amount, MoneroTransactionPriority.regular.raw!); - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); - } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && - ref.read(publicPrivateBalanceStateProvider.state).state != - "Private") { - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + if (widget.isToken == false) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.regular.raw!); + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); + } else { + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate), + coin: coin); + } } else { + final tokenWallet = ref.read(tokenServiceProvider)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + Format.satoshisToAmount(fee, coin: coin); } } return ref.read(feeSheetSessionCacheProvider).average[amount]!; case FeeRateType.slow: if (ref.read(feeSheetSessionCacheProvider).slow[amount] == null) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); - if (coin == Coin.monero || coin == Coin.wownero) { - final fee = await manager.estimateFeeFor( - amount, MoneroTransactionPriority.slow.raw!); - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); - } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && - ref.read(publicPrivateBalanceStateProvider.state).state != - "Private") { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + if (widget.isToken == false) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.slow.raw!); + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + fee, + coin: coin, + ); + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate), + coin: coin); + } else { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate), + coin: coin); + } } else { + final tokenWallet = ref.read(tokenServiceProvider)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + Format.satoshisToAmount(fee, coin: coin); } } return ref.read(feeSheetSessionCacheProvider).slow[amount]!; @@ -231,7 +256,9 @@ class _TransactionFeeSelectionSheetState height: 36, ), FutureBuilder( - future: manager.fees, + future: widget.isToken + ? ref.read(tokenServiceProvider)!.fees + : manager.fees, builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -336,7 +363,7 @@ class _TransactionFeeSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "(~${snapshot.data!} ${manager.coin.ticker})", + "(~${snapshot.data!.toStringAsFixed(manager.coin.decimals)} ${manager.coin.ticker})", style: STextStyles.itemSubtitle( context), textAlign: TextAlign.left, @@ -468,7 +495,7 @@ class _TransactionFeeSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "(~${snapshot.data!} ${manager.coin.ticker})", + "(~${snapshot.data!.toStringAsFixed(manager.coin.decimals)} ${manager.coin.ticker})", style: STextStyles.itemSubtitle( context), textAlign: TextAlign.left, @@ -523,7 +550,6 @@ class _TransactionFeeSelectionSheetState FeeRateType.slow; } String? fee = getAmount(FeeRateType.slow, manager.coin); - print("fee $fee"); if (fee != null) { widget.updateChosen(fee); } @@ -602,7 +628,7 @@ class _TransactionFeeSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "(~${snapshot.data!} ${manager.coin.ticker})", + "(~${snapshot.data!.toStringAsFixed(manager.coin.decimals)} ${manager.coin.ticker})", style: STextStyles.itemSubtitle( context), textAlign: TextAlign.left, @@ -660,18 +686,13 @@ class _TransactionFeeSelectionSheetState String? getAmount(FeeRateType feeRateType, Coin coin) { try { - print(feeRateType); - var amount = Format.decimalAmountToSatoshis(this.amount, coin); - print(amount); - print(ref.read(feeSheetSessionCacheProvider).fast); - print(ref.read(feeSheetSessionCacheProvider).average); - print(ref.read(feeSheetSessionCacheProvider).slow); + final amount = Format.decimalAmountToSatoshis(this.amount, coin); switch (feeRateType) { case FeeRateType.fast: if (ref.read(feeSheetSessionCacheProvider).fast[amount] != null) { return (ref.read(feeSheetSessionCacheProvider).fast[amount] as Decimal) - .toString(); + .toStringAsFixed(coin.decimals); } return null; @@ -679,22 +700,20 @@ class _TransactionFeeSelectionSheetState if (ref.read(feeSheetSessionCacheProvider).average[amount] != null) { return (ref.read(feeSheetSessionCacheProvider).average[amount] as Decimal) - .toString(); + .toStringAsFixed(coin.decimals); } return null; case FeeRateType.slow: - print(ref.read(feeSheetSessionCacheProvider).slow); - print(ref.read(feeSheetSessionCacheProvider).slow[amount]); if (ref.read(feeSheetSessionCacheProvider).slow[amount] != null) { return (ref.read(feeSheetSessionCacheProvider).slow[amount] as Decimal) - .toString(); + .toStringAsFixed(coin.decimals); } return null; } } catch (e, s) { - print("$e $s"); + Logging.instance.log("$e $s", level: LogLevel.Warning); return null; } } diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 5f58508f3..b5c676d99 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -1136,6 +1136,7 @@ class _TokenSendViewState extends ConsumerState { builder: (_) => TransactionFeeSelectionSheet( walletId: walletId, + isToken: true, amount: Decimal.tryParse( cryptoAmountController.text) ?? Decimal.zero, From b3c4e690c7e9bb68953ec984f0f51e765cfc609a Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 09:25:08 -0600 Subject: [PATCH 161/208] move confirmSend to a nicer place --- .../coins/ethereum/ethereum_wallet.dart | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 06c7ea41c..265e39ec2 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -220,29 +220,6 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { await updateCachedBalance(_balance!); } - @override - Future confirmSend({required Map txData}) async { - web3.Web3Client client = getEthClient(); - final chainId = await client.getChainId(); - final amount = txData['recipientAmt'] as int; - final decimalAmount = Format.satoshisToAmount(amount, coin: coin); - final bigIntAmount = amountToBigInt( - decimalAmount.toDouble(), - Constants.decimalPlacesForCoin(coin), - ); - - final tx = web3.Transaction( - to: web3.EthereumAddress.fromHex(txData['address'] as String), - gasPrice: web3.EtherAmount.fromUnitAndValue( - web3.EtherUnit.wei, txData['feeInWei']), - maxGas: _gasLimit, - value: web3.EtherAmount.inWei(bigIntAmount)); - final transaction = await client.sendTransaction(_credentials, tx, - chainId: chainId.toInt()); - - return transaction; - } - @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { final fee = estimateFee(feeRate, _gasLimit, coin.decimals); @@ -484,6 +461,29 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { return txData; } + @override + Future confirmSend({required Map txData}) async { + web3.Web3Client client = getEthClient(); + final chainId = await client.getChainId(); + final amount = txData['recipientAmt'] as int; + final decimalAmount = Format.satoshisToAmount(amount, coin: coin); + final bigIntAmount = amountToBigInt( + decimalAmount.toDouble(), + Constants.decimalPlacesForCoin(coin), + ); + + final tx = web3.Transaction( + to: web3.EthereumAddress.fromHex(txData['address'] as String), + gasPrice: web3.EtherAmount.fromUnitAndValue( + web3.EtherUnit.wei, txData['feeInWei']), + maxGas: _gasLimit, + value: web3.EtherAmount.inWei(bigIntAmount)); + final txid = await client.sendTransaction(_credentials, tx, + chainId: chainId.toInt()); + + return txid; + } + @override Future recoverFromMnemonic({ required String mnemonic, From 42d168dc57d427d554d840bf163da4f077cba4fb Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 09:25:51 -0600 Subject: [PATCH 162/208] add token tx send support to gui --- .../send_view/confirm_transaction_view.dart | 39 +++++++++++++------ lib/pages/send_view/token_send_view.dart | 1 + 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index ac7e41592..ce212c8f7 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; @@ -46,6 +47,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { this.isTradeTransaction = false, this.isPaynymTransaction = false, this.isPaynymNotificationTransaction = false, + this.isTokenTx = false, this.onSuccessInsteadOfRouteOnSuccess, }) : super(key: key); @@ -57,6 +59,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { final bool isTradeTransaction; final bool isPaynymTransaction; final bool isPaynymNotificationTransaction; + final bool isTokenTx; final VoidCallback? onSuccessInsteadOfRouteOnSuccess; @override @@ -102,7 +105,11 @@ class _ConfirmTransactionViewState final note = noteController.text; try { - if (widget.isPaynymNotificationTransaction) { + if (widget.isTokenTx) { + txidFuture = ref + .read(tokenServiceProvider)! + .confirmSend(txData: transactionInfo); + } else if (widget.isPaynymNotificationTransaction) { txidFuture = (manager.wallet as PaynymWalletInterface) .broadcastNotificationTx(preparedTx: transactionInfo); } else if (widget.isPaynymTransaction) { @@ -132,7 +139,11 @@ class _ConfirmTransactionViewState .read(notesServiceChangeNotifierProvider(walletId)) .editOrAddNote(txid: txid, note: note); - unawaited(manager.refresh()); + if (widget.isTokenTx) { + unawaited(ref.read(tokenServiceProvider)!.refresh()); + } else { + unawaited(manager.refresh()); + } // pop back to wallet if (mounted) { @@ -258,6 +269,15 @@ class _ConfirmTransactionViewState final managerProvider = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManagerProvider(walletId))); + final String unit; + if (widget.isTokenTx) { + unit = ref.watch( + tokenServiceProvider.select((value) => value!.tokenContract.symbol)); + } else { + unit = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).coin.ticker)); + } + return ConditionalParent( condition: !isDesktop, builder: (child) => Background( @@ -324,7 +344,7 @@ class _ConfirmTransactionViewState ).pop(), ), Text( - "Confirm ${ref.watch(managerProvider.select((value) => value.coin.ticker.toUpperCase()))} transaction", + "Confirm $unit transaction", style: STextStyles.desktopH3(context), ), ], @@ -341,7 +361,7 @@ class _ConfirmTransactionViewState crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", + "Send $unit", style: STextStyles.pageTitleH1(context), ), const SizedBox( @@ -388,9 +408,7 @@ class _ConfirmTransactionViewState .select((value) => value.locale), ), ref.watch( managerProvider.select((value) => value.coin), - ))} ${ref.watch( - managerProvider.select((value) => value.coin), - ).ticker}", + ))} $unit", style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, ), @@ -492,10 +510,7 @@ class _ConfirmTransactionViewState width: 16, ), Text( - "Send ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", + "Send $unit", style: STextStyles.desktopTextMedium(context), ), ], @@ -559,7 +574,7 @@ class _ConfirmTransactionViewState .select((value) => value.locale), ), coin, - )} ${coin.ticker}", + )} $unit", style: STextStyles .desktopTextExtraExtraSmall( context) diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index b5c676d99..574f836ca 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -513,6 +513,7 @@ class _TokenSendViewState extends ConsumerState { builder: (_) => ConfirmTransactionView( transactionInfo: txData, walletId: walletId, + isTokenTx: true, ), settings: const RouteSettings( name: ConfirmTransactionView.routeName, From 8d0dcafccf0ea0c95edc16bd82d7a8e7e52c97c6 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 09:26:43 -0600 Subject: [PATCH 163/208] WIP token send logic --- .../ethereum/ethereum_token_service.dart | 114 ++++++++++-------- 1 file changed, 67 insertions(+), 47 deletions(-) diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index c57e668b2..636f4335a 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -66,27 +66,78 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { Coin get coin => Coin.ethereum; - Future confirmSend({required Map txData}) async { - final amount = txData['recipientAmt']; + Future> prepareSend({ + required String address, + required int satoshiAmount, + Map? 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); + final decimalAmount = - Format.satoshisToAmount(amount as int, coin: Coin.ethereum); + Format.satoshisToAmount(satoshiAmount, coin: Coin.ethereum); final bigIntAmount = amountToBigInt(decimalAmount.toDouble(), tokenContract.decimals); - final sentTx = await _client.sendTransaction( - _credentials, - web3dart.Transaction.callContract( - contract: _deployedContract, - function: _sendFunction, - parameters: [ - web3dart.EthereumAddress.fromHex(txData['address'] as String), - bigIntAmount - ], - maxGas: _gasLimit, - gasPrice: web3dart.EtherAmount.fromUnitAndValue( - web3dart.EtherUnit.wei, txData['feeInWei']))); + final client = await getEthClient(); - return sentTx; + final est = await client.estimateGas( + sender: web3dart.EthereumAddress.fromHex(await currentReceivingAddress), + to: web3dart.EthereumAddress.fromHex(address), + data: _sendFunction.encodeCall( + [web3dart.EthereumAddress.fromHex(address), bigIntAmount]), + gasPrice: web3dart.EtherAmount.fromUnitAndValue( + web3dart.EtherUnit.wei, + fee, + ), + amountOfGas: BigInt.from(_gasLimit), + value: web3dart.EtherAmount.inWei(BigInt.one), + ); + + final tx = web3dart.Transaction.callContract( + contract: _deployedContract, + function: _sendFunction, + parameters: [web3dart.EthereumAddress.fromHex(address), bigIntAmount], + maxGas: _gasLimit, + gasPrice: web3dart.EtherAmount.fromUnitAndValue( + web3dart.EtherUnit.wei, + fee, + ), + nonce: args?["nonce"] as int?, + ); + + Map txData = { + "fee": feeEstimate, + "feeInWei": fee, + "address": address, + "recipientAmt": satoshiAmount, + "ethTx": tx, + }; + + return txData; + } + + Future confirmSend({required Map txData}) async { + final txid = await _client.sendTransaction( + _credentials, + txData["ethTx"] as web3dart.Transaction, + ); + + return txid; } Future get currentReceivingAddress async { @@ -243,37 +294,6 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { bool get isRefreshing => _refreshLock; - Future> prepareSend( - {required String address, - required int satoshiAmount, - Map? 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); - - Map txData = { - "fee": feeEstimate, - "feeInWei": fee, - "address": address, - "recipientAmt": satoshiAmount, - }; - - return txData; - } - bool _refreshLock = false; Future refresh() async { From 606e70a0613a73d7f2c2dd0109e0db1ec5b1479d Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 09:39:03 -0600 Subject: [PATCH 164/208] add nonce property to Transaction --- .../models/blockchain_data/transaction.dart | 2 + .../models/blockchain_data/transaction.g.dart | 178 +++++++++++++++--- 2 files changed, 149 insertions(+), 31 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index b6a7a74a6..8fd1b9840 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -105,6 +105,8 @@ class Transaction { late final String? otherData; + late final int? nonce; + late final List inputs; late final List outputs; diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index a511d01f5..850b503bc 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -53,46 +53,51 @@ const TransactionSchema = CollectionSchema( name: r'isLelantus', type: IsarType.bool, ), - r'otherData': PropertySchema( + r'nonce': PropertySchema( id: 7, + name: r'nonce', + type: IsarType.long, + ), + r'otherData': PropertySchema( + id: 8, name: r'otherData', type: IsarType.string, ), r'outputs': PropertySchema( - id: 8, + id: 9, name: r'outputs', type: IsarType.objectList, target: r'Output', ), r'slateId': PropertySchema( - id: 9, + id: 10, name: r'slateId', type: IsarType.string, ), r'subType': PropertySchema( - id: 10, + id: 11, name: r'subType', type: IsarType.byte, enumMap: _TransactionsubTypeEnumValueMap, ), r'timestamp': PropertySchema( - id: 11, + id: 12, name: r'timestamp', type: IsarType.long, ), r'txid': PropertySchema( - id: 12, + id: 13, name: r'txid', type: IsarType.string, ), r'type': PropertySchema( - id: 13, + id: 14, name: r'type', type: IsarType.byte, enumMap: _TransactiontypeEnumValueMap, ), r'walletId': PropertySchema( - id: 14, + id: 15, name: r'walletId', type: IsarType.string, ) @@ -227,19 +232,20 @@ void _transactionSerialize( ); writer.writeBool(offsets[5], object.isCancelled); writer.writeBool(offsets[6], object.isLelantus); - writer.writeString(offsets[7], object.otherData); + writer.writeLong(offsets[7], object.nonce); + writer.writeString(offsets[8], object.otherData); writer.writeObjectList( - offsets[8], + offsets[9], allOffsets, OutputSchema.serialize, object.outputs, ); - writer.writeString(offsets[9], object.slateId); - writer.writeByte(offsets[10], object.subType.index); - writer.writeLong(offsets[11], object.timestamp); - writer.writeString(offsets[12], object.txid); - writer.writeByte(offsets[13], object.type.index); - writer.writeString(offsets[14], object.walletId); + writer.writeString(offsets[10], object.slateId); + writer.writeByte(offsets[11], object.subType.index); + writer.writeLong(offsets[12], object.timestamp); + writer.writeString(offsets[13], object.txid); + writer.writeByte(offsets[14], object.type.index); + writer.writeString(offsets[15], object.walletId); } Transaction _transactionDeserialize( @@ -262,25 +268,26 @@ Transaction _transactionDeserialize( [], isCancelled: reader.readBool(offsets[5]), isLelantus: reader.readBoolOrNull(offsets[6]), - otherData: reader.readStringOrNull(offsets[7]), + otherData: reader.readStringOrNull(offsets[8]), outputs: reader.readObjectList( - offsets[8], + offsets[9], OutputSchema.deserialize, allOffsets, Output(), ) ?? [], - slateId: reader.readStringOrNull(offsets[9]), + slateId: reader.readStringOrNull(offsets[10]), subType: - _TransactionsubTypeValueEnumMap[reader.readByteOrNull(offsets[10])] ?? + _TransactionsubTypeValueEnumMap[reader.readByteOrNull(offsets[11])] ?? TransactionSubType.none, - timestamp: reader.readLong(offsets[11]), - txid: reader.readString(offsets[12]), - type: _TransactiontypeValueEnumMap[reader.readByteOrNull(offsets[13])] ?? + timestamp: reader.readLong(offsets[12]), + txid: reader.readString(offsets[13]), + type: _TransactiontypeValueEnumMap[reader.readByteOrNull(offsets[14])] ?? TransactionType.outgoing, - walletId: reader.readString(offsets[14]), + walletId: reader.readString(offsets[15]), ); object.id = id; + object.nonce = reader.readLongOrNull(offsets[7]); return object; } @@ -312,8 +319,10 @@ P _transactionDeserializeProp

( case 6: return (reader.readBoolOrNull(offset)) as P; case 7: - return (reader.readStringOrNull(offset)) as P; + return (reader.readLongOrNull(offset)) as P; case 8: + return (reader.readStringOrNull(offset)) as P; + case 9: return (reader.readObjectList( offset, OutputSchema.deserialize, @@ -321,19 +330,19 @@ P _transactionDeserializeProp

( Output(), ) ?? []) as P; - case 9: - return (reader.readStringOrNull(offset)) as P; case 10: + return (reader.readStringOrNull(offset)) as P; + case 11: return (_TransactionsubTypeValueEnumMap[reader.readByteOrNull(offset)] ?? TransactionSubType.none) as P; - case 11: - return (reader.readLong(offset)) as P; case 12: - return (reader.readString(offset)) as P; + return (reader.readLong(offset)) as P; case 13: + return (reader.readString(offset)) as P; + case 14: return (_TransactiontypeValueEnumMap[reader.readByteOrNull(offset)] ?? TransactionType.outgoing) as P; - case 14: + case 15: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -1294,6 +1303,77 @@ extension TransactionQueryFilter }); } + QueryBuilder nonceIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'nonce', + )); + }); + } + + QueryBuilder + nonceIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'nonce', + )); + }); + } + + QueryBuilder nonceEqualTo( + int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'nonce', + value: value, + )); + }); + } + + QueryBuilder + nonceGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'nonce', + value: value, + )); + }); + } + + QueryBuilder nonceLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'nonce', + value: value, + )); + }); + } + + QueryBuilder nonceBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'nonce', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder otherDataIsNull() { return QueryBuilder.apply(this, (query) { @@ -2228,6 +2308,18 @@ extension TransactionQuerySortBy }); } + QueryBuilder sortByNonce() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'nonce', Sort.asc); + }); + } + + QueryBuilder sortByNonceDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'nonce', Sort.desc); + }); + } + QueryBuilder sortByOtherData() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'otherData', Sort.asc); @@ -2400,6 +2492,18 @@ extension TransactionQuerySortThenBy }); } + QueryBuilder thenByNonce() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'nonce', Sort.asc); + }); + } + + QueryBuilder thenByNonceDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'nonce', Sort.desc); + }); + } + QueryBuilder thenByOtherData() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'otherData', Sort.asc); @@ -2524,6 +2628,12 @@ extension TransactionQueryWhereDistinct }); } + QueryBuilder distinctByNonce() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'nonce'); + }); + } + QueryBuilder distinctByOtherData( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -2621,6 +2731,12 @@ extension TransactionQueryProperty }); } + QueryBuilder nonceProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'nonce'); + }); + } + QueryBuilder otherDataProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'otherData'); From f969179ea60555c63312afac44fd6438014354de Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 09:39:17 -0600 Subject: [PATCH 165/208] updated mocks --- test/pages/send_view/send_view_test.mocks.dart | 8 ++++++++ test/screen_tests/lockscreen_view_screen_test.mocks.dart | 8 ++++++++ .../main_view_screen_testA_test.mocks.dart | 8 ++++++++ .../main_view_screen_testB_test.mocks.dart | 8 ++++++++ .../main_view_screen_testC_test.mocks.dart | 8 ++++++++ .../backup_key_warning_view_screen_test.mocks.dart | 8 ++++++++ .../onboarding/create_pin_view_screen_test.mocks.dart | 8 ++++++++ .../name_your_wallet_view_screen_test.mocks.dart | 8 ++++++++ .../onboarding/restore_wallet_view_screen_test.mocks.dart | 8 ++++++++ .../change_pin_view_screen_test.mocks.dart | 8 ++++++++ .../rename_wallet_view_screen_test.mocks.dart | 8 ++++++++ .../wallet_delete_mnemonic_view_screen_test.mocks.dart | 8 ++++++++ .../wallet_settings_view_screen_test.mocks.dart | 8 ++++++++ .../settings_view/settings_view_screen_test.mocks.dart | 8 ++++++++ test/widget_tests/managed_favorite_test.mocks.dart | 8 ++++++++ .../table_view/table_view_row_test.mocks.dart | 8 ++++++++ test/widget_tests/transaction_card_test.mocks.dart | 5 +++++ .../wallet_info_row_balance_future_test.mocks.dart | 8 ++++++++ .../wallet_info_row/wallet_info_row_test.mocks.dart | 8 ++++++++ 19 files changed, 149 insertions(+) diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 539f7b059..08f60e26e 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -486,6 +486,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i22.Future.value(false), ) as _i22.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i22.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index 1a535475b..7bb2b91da 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -107,6 +107,14 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { returnValue: _i7.Future.value(false), ) as _i7.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i7.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart index 4833c56ee..1e4967d64 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart @@ -94,6 +94,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart index 33c356d74..48d6ab6a0 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart @@ -94,6 +94,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart index b19d16114..5c86479c4 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart @@ -94,6 +94,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart index f3967ade5..4bfdcfbcf 100644 --- a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart @@ -92,6 +92,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index f1d3b0c4b..7194665b1 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -107,6 +107,14 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { returnValue: _i7.Future.value(false), ) as _i7.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i7.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/onboarding/name_your_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/name_your_wallet_view_screen_test.mocks.dart index 29cdf0642..65cd8fea1 100644 --- a/test/screen_tests/onboarding/name_your_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/name_your_wallet_view_screen_test.mocks.dart @@ -56,6 +56,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i3.Future.value(false), ) as _i3.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i3.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index 20ac25812..184da3216 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -148,6 +148,14 @@ class MockWalletsService extends _i1.Mock implements _i9.WalletsService { returnValue: _i8.Future.value(false), ) as _i8.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i8.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/change_pin_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/change_pin_view_screen_test.mocks.dart index 280d08e09..799feb0cb 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/change_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/change_pin_view_screen_test.mocks.dart @@ -56,6 +56,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i3.Future.value(false), ) as _i3.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i3.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rename_wallet_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rename_wallet_view_screen_test.mocks.dart index 12e07fd2f..b11c9ad89 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rename_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rename_wallet_view_screen_test.mocks.dart @@ -56,6 +56,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i3.Future.value(false), ) as _i3.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i3.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart index c39df921e..5abd560b6 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart @@ -92,6 +92,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index 6f3828684..a54af1fac 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -349,6 +349,14 @@ class MockWalletsService extends _i1.Mock implements _i13.WalletsService { returnValue: _i8.Future.value(false), ) as _i8.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i8.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart index ea371aad5..89e91910d 100644 --- a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart @@ -92,6 +92,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 5abf517b7..0c1347147 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -482,6 +482,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i22.Future.value(false), ) as _i22.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i22.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index cc022b12e..eb5f34eea 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -469,6 +469,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i21.Future.value(false), ) as _i21.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i21.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 60a51073a..45a8b158b 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -2528,6 +2528,11 @@ class MockPriceService extends _i1.Mock implements _i28.PriceService { returnValueForMissingStub: null, ); @override + Set get tokenContractAddressesToCheck => (super.noSuchMethod( + Invocation.getter(#tokenContractAddressesToCheck), + returnValue: {}, + ) as Set); + @override Duration get updateInterval => (super.noSuchMethod( Invocation.getter(#updateInterval), returnValue: _FakeDuration_12( diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index 5c2641dbb..85ff2ef99 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -481,6 +481,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i22.Future.value(false), ) as _i22.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i22.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 1f786dbab..90fd61806 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -481,6 +481,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i22.Future.value(false), ) as _i22.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i22.Future addExistingStackWallet({ required String? name, required String? walletId, From c8139007e379c6ccc103f4d3f3095373de75cdbd Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 10:15:42 -0600 Subject: [PATCH 166/208] use new transaction nonce property --- .../models/blockchain_data/transaction.dart | 6 ++ .../models/blockchain_data/transaction.g.dart | 2 +- .../coins/bitcoin/bitcoin_wallet.dart | 1 + .../coins/bitcoincash/bitcoincash_wallet.dart | 2 + .../coins/dogecoin/dogecoin_wallet.dart | 1 + .../coins/epiccash/epiccash_wallet.dart | 1 + .../coins/ethereum/ethereum_wallet.dart | 22 ++++++- lib/services/coins/firo/firo_wallet.dart | 5 ++ .../coins/litecoin/litecoin_wallet.dart | 1 + lib/services/coins/monero/monero_wallet.dart | 1 + .../coins/namecoin/namecoin_wallet.dart | 1 + .../coins/particl/particl_wallet.dart | 2 + .../coins/wownero/wownero_wallet.dart | 1 + lib/services/ethereum/ethereum_api.dart | 59 +++++++++++++++++++ .../ethereum/ethereum_token_service.dart | 1 + lib/services/mixins/electrum_x_parsing.dart | 1 + lib/utilities/db_version_migration.dart | 1 + .../services/coins/firo/firo_wallet_test.dart | 1 + test/services/coins/manager_test.dart | 1 + test/widget_tests/transaction_card_test.dart | 4 ++ 20 files changed, 110 insertions(+), 4 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index 8fd1b9840..b1dacefba 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -28,6 +28,7 @@ class Transaction { required this.otherData, required this.inputs, required this.outputs, + required this.nonce, }); Tuple2 copyWith({ @@ -46,6 +47,7 @@ class Transaction { String? otherData, List? inputs, List? outputs, + int? nonce, Id? id, Address? address, }) { @@ -64,6 +66,7 @@ class Transaction { isLelantus: isLelantus ?? this.isLelantus, slateId: slateId ?? this.slateId, otherData: otherData ?? this.otherData, + nonce: nonce ?? this.nonce, inputs: inputs ?? this.inputs, outputs: outputs ?? this.outputs) ..id = id ?? this.id, @@ -147,6 +150,7 @@ class Transaction { "isLelantus: $isLelantus, " "slateId: $slateId, " "otherData: $otherData, " + "nonce: $nonce, " "address: ${address.value}, " "inputsLength: ${inputs.length}, " "outputsLength: ${outputs.length}, " @@ -167,6 +171,7 @@ class Transaction { "isLelantus": isLelantus, "slateId": slateId, "otherData": otherData, + "nonce": nonce, "address": address.value?.toJsonString(), "inputs": inputs.map((e) => e.toJsonString()).toList(), "outputs": outputs.map((e) => e.toJsonString()).toList(), @@ -193,6 +198,7 @@ class Transaction { isLelantus: json["isLelantus"] as bool?, slateId: json["slateId"] as String?, otherData: json["otherData"] as String?, + nonce: json["nonce"] as int?, inputs: List.from(json["inputs"] as List) .map((e) => Input.fromJsonString(e)) .toList(), diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index 850b503bc..74a5a1652 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -268,6 +268,7 @@ Transaction _transactionDeserialize( [], isCancelled: reader.readBool(offsets[5]), isLelantus: reader.readBoolOrNull(offsets[6]), + nonce: reader.readLongOrNull(offsets[7]), otherData: reader.readStringOrNull(offsets[8]), outputs: reader.readObjectList( offsets[9], @@ -287,7 +288,6 @@ Transaction _transactionDeserialize( walletId: reader.readString(offsets[15]), ); object.id = id; - object.nonce = reader.readLongOrNull(offsets[7]); return object; } diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index c622479fb..ce5a76a7b 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -1346,6 +1346,7 @@ class BitcoinWallet extends CoinServiceAPI isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 73d44b91e..d07dbc8f0 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1261,6 +1261,7 @@ class BitcoinCashWallet extends CoinServiceAPI isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -2326,6 +2327,7 @@ class BitcoinCashWallet extends CoinServiceAPI isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: inputs, outputs: outputs, ); diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 232b47889..3c12aada5 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -1128,6 +1128,7 @@ class DogecoinWallet extends CoinServiceAPI isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index a2f8a8713..5808e9fa9 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -1699,6 +1699,7 @@ class EpicCashWallet extends CoinServiceAPI tx["tx_type"] == "TxReceivedCancelled", isLelantus: false, slateId: slateId, + nonce: null, otherData: tx["id"].toString(), inputs: [], outputs: [], diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 265e39ec2..0afd30bf8 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -868,12 +868,26 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { Future _refreshTransactions() async { String thisAddress = await currentReceivingAddress; - final txsResponse = await EthereumAPI.getEthTransactions(thisAddress); + final response = await EthereumAPI.getEthTransactions(thisAddress); + + if (response.value == null) { + Logging.instance.log( + "Failed to refresh transactions for ${coin.prettyName} $walletName " + "$walletId: ${response.exception}", + level: LogLevel.Warning, + ); + return; + } + + final txsResponse = + await EthereumAPI.getEthTransactionNonces(response.value!); if (txsResponse.value != null) { final allTxs = txsResponse.value!; final List> txnsData = []; - for (final element in allTxs) { + for (final tuple in allTxs) { + final element = tuple.item1; + Amount transactionAmount = element.value; bool isIncoming; @@ -909,6 +923,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { isLelantus: false, slateId: null, otherData: null, + nonce: tuple.item2, inputs: [], outputs: [], ); @@ -966,7 +981,8 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { } } else { Logging.instance.log( - "Failed to refresh transactions for ${coin.prettyName} $walletName $walletId", + "Failed to refresh transactions with nonces for ${coin.prettyName} " + "$walletName $walletId: ${txsResponse.exception}", level: LogLevel.Warning, ); } diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index c89ba8114..e973f3092 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -483,6 +483,7 @@ Future> staticProcessRestore( isLelantus: true, slateId: null, otherData: txid, + nonce: null, inputs: element.inputs, outputs: element.outputs, )..address.value = element.address.value; @@ -914,6 +915,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -3070,6 +3072,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { isCancelled: false, isLelantus: true, slateId: null, + nonce: null, otherData: transactionInfo["otherData"] as String?, inputs: [], outputs: [], @@ -3631,6 +3634,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: ins, outputs: outs, ); @@ -4971,6 +4975,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { isLelantus: true, slateId: null, otherData: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index e5207b562..7feead18f 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -1253,6 +1253,7 @@ class LitecoinWallet extends CoinServiceAPI isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index c8362426c..c0543a36a 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -937,6 +937,7 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 17007da2c..55076ad66 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -1243,6 +1243,7 @@ class NamecoinWallet extends CoinServiceAPI isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 4a467d119..5550c63d3 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -1172,6 +1172,7 @@ class ParticlWallet extends CoinServiceAPI isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -2367,6 +2368,7 @@ class ParticlWallet extends CoinServiceAPI outputs: outputs, isCancelled: false, isLelantus: false, + nonce: null, slateId: null, otherData: null, ); diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 7eb5ee8b1..6a23276b3 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1016,6 +1016,7 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 9c65057f1..dc474340a 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -11,6 +11,7 @@ import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:tuple/tuple.dart'; class EthApiException with Exception { EthApiException(this.message); @@ -89,6 +90,64 @@ abstract class EthereumAPI { } } + static Future>>> + getEthTransactionNonces( + List txns, + ) async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/transactions?transactions=${txns.map((e) => e.hash).join(" ")}", + ), + ); + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + final json = jsonDecode(response.body) as Map; + final list = List>.from(json["data"] as List); + + final List> result = []; + + for (final dto in txns) { + final data = + list.firstWhere((e) => e["hash"] == dto.hash, orElse: () => {}); + + final nonce = data["nonce"] as int?; + result.add(Tuple2(dto, nonce)); + } + return EthereumResponse( + result, + null, + ); + } else { + throw EthApiException( + "getEthTransactionNonces($txns) response is empty but status code is " + "${response.statusCode}", + ); + } + } else { + throw EthApiException( + "getEthTransactionNonces($txns) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getEthTransactionNonces($txns): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + static Future>> getEthTokenTransactionsByTxids(List txids) async { try { diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 636f4335a..4cbe18d72 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -447,6 +447,7 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { isCancelled: false, isLelantus: false, slateId: null, + nonce: tuple.item2.nonce, otherData: tuple.item1.address, inputs: [], outputs: [], diff --git a/lib/services/mixins/electrum_x_parsing.dart b/lib/services/mixins/electrum_x_parsing.dart index 5432ea496..76734bf08 100644 --- a/lib/services/mixins/electrum_x_parsing.dart +++ b/lib/services/mixins/electrum_x_parsing.dart @@ -231,6 +231,7 @@ mixin ElectrumXParsing { isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: ins, outputs: outs, ); diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index ab893b5f9..ff740a87f 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -349,6 +349,7 @@ class DbVersionMigrator with WalletDB { isLelantus: false, slateId: tx.slateId, otherData: tx.otherData, + nonce: null, inputs: [], outputs: [], ); diff --git a/test/services/coins/firo/firo_wallet_test.dart b/test/services/coins/firo/firo_wallet_test.dart index 2c25bd711..ea5e4521d 100644 --- a/test/services/coins/firo/firo_wallet_test.dart +++ b/test/services/coins/firo/firo_wallet_test.dart @@ -108,6 +108,7 @@ void main() { isLelantus: null, slateId: t.slateId, otherData: t.otherData, + nonce: null, inputs: [], outputs: [], ), diff --git a/test/services/coins/manager_test.dart b/test/services/coins/manager_test.dart index 265f9f2cd..55ed98753 100644 --- a/test/services/coins/manager_test.dart +++ b/test/services/coins/manager_test.dart @@ -115,6 +115,7 @@ void main() { isLelantus: true, slateId: null, otherData: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/test/widget_tests/transaction_card_test.dart b/test/widget_tests/transaction_card_test.dart index 984ed2baf..959c4e993 100644 --- a/test/widget_tests/transaction_card_test.dart +++ b/test/widget_tests/transaction_card_test.dart @@ -64,6 +64,7 @@ void main() { isLelantus: null, slateId: '', otherData: '', + nonce: null, inputs: [], outputs: [], )..address.value = Address( @@ -169,6 +170,7 @@ void main() { isLelantus: null, slateId: '', otherData: '', + nonce: null, inputs: [], outputs: [], )..address.value = Address( @@ -271,6 +273,7 @@ void main() { isLelantus: null, slateId: '', otherData: '', + nonce: null, inputs: [], outputs: [], )..address.value = Address( @@ -367,6 +370,7 @@ void main() { isLelantus: null, slateId: '', otherData: '', + nonce: null, inputs: [], outputs: [], )..address.value = Address( From 06dba00dbd3997ca348ea0fde14d7972f7fef578 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 10:29:23 -0600 Subject: [PATCH 167/208] show nonce in tx details --- .../transaction_details_view.dart | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index da59a3bb6..76a1de1e9 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -1069,6 +1069,46 @@ class _TransactionDetailsViewState ); }), ), + if (coin == Coin.ethereum) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (coin == Coin.ethereum) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Nonce", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + SelectableText( + _transaction.nonce.toString(), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + ], + ), + ), isDesktop ? const _Divider() : const SizedBox( From 85e7c6d820190ee583e76a75193736480d295b41 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 10:35:10 -0600 Subject: [PATCH 168/208] expand tokens by default on desktop --- lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index a359acf49..bb90a5c18 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -255,7 +255,7 @@ class _AddWalletViewState extends ConsumerState { ExpandingSubListItem( title: "Tokens", entities: filter(_searchTerm, tokenEntities), - initialState: ExpandableState.collapsed, + initialState: ExpandableState.expanded, trailing: AddCustomTokenSelector( addFunction: _addToken, ), From f4f58b047369a4e5b67c536a261ab22672f4962c Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 11:04:11 -0600 Subject: [PATCH 169/208] desktop add global custom eth token view --- .../add_token_view/add_custom_token_view.dart | 287 ++++++++++++------ .../add_wallet_view/add_wallet_view.dart | 28 +- 2 files changed, 218 insertions(+), 97 deletions(-) diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart index 01f7b6f49..76dfceb45 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -10,8 +10,11 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; class AddCustomTokenView extends ConsumerStatefulWidget { @@ -40,97 +43,183 @@ class _AddCustomTokenViewState extends ConsumerState { @override Widget build(BuildContext context) { - return Background( - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: Padding( + padding: const EdgeInsets.only( + top: 10, + left: 16, + right: 16, + bottom: 16, + ), + child: child, ), ), - body: Padding( - padding: const EdgeInsets.only( - top: 10, - left: 16, - right: 16, - bottom: 16, - ), - child: Column( - children: [ + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Add custom ETH token", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 16, + ), + child: child, + ), + ), + ], + ), + child: Column( + children: [ + if (!isDesktop) Text( "Add custom ETH token", style: STextStyles.pageTitleH1(context), ), + if (!isDesktop) const SizedBox( height: 16, ), - TextField( - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: contractController, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Contract address", - hintStyle: STextStyles.fieldLabel(context), - ), + TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: contractController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Contract address", + hintStyle: STextStyles.fieldLabel(context), ), - const SizedBox( - height: 8, - ), - PrimaryButton( - label: "Search", - onPressed: () async { - final response = await showLoading( - whileFuture: EthereumAPI.getTokenContractInfoByAddress( - contractController.text), - context: context, - message: "Looking up contract", - ); - currentToken = response.value; - if (currentToken != null) { - nameController.text = currentToken!.name; - symbolController.text = currentToken!.symbol; - decimalsController.text = currentToken!.decimals.toString(); - } else { - nameController.text = ""; - symbolController.text = ""; - decimalsController.text = ""; - if (mounted) { - unawaited( - showDialog( - context: context, - builder: (context) => StackOkDialog( - title: "Failed to look up token", - message: response.exception?.message, - ), + ), + SizedBox( + height: isDesktop ? 16 : 8, + ), + PrimaryButton( + label: "Search", + onPressed: () async { + final response = await showLoading( + whileFuture: EthereumAPI.getTokenContractInfoByAddress( + contractController.text), + context: context, + message: "Looking up contract", + ); + currentToken = response.value; + if (currentToken != null) { + nameController.text = currentToken!.name; + symbolController.text = currentToken!.symbol; + decimalsController.text = currentToken!.decimals.toString(); + } else { + nameController.text = ""; + symbolController.text = ""; + decimalsController.text = ""; + if (mounted) { + unawaited( + showDialog( + context: context, + builder: (context) => StackOkDialog( + title: "Failed to look up token", + message: response.exception?.message, ), - ); - } + ), + ); } - setState(() { - addTokenButtonEnabled = currentToken != null; - }); - }, + } + setState(() { + addTokenButtonEnabled = currentToken != null; + }); + }, + ), + SizedBox( + height: isDesktop ? 16 : 8, + ), + TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: nameController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Token name", + hintStyle: STextStyles.fieldLabel(context), ), - const SizedBox( - height: 8, - ), - TextField( - enabled: enableSubFields, - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: nameController, - style: STextStyles.field(context), - decoration: InputDecoration( - hintText: "Token name", - hintStyle: STextStyles.fieldLabel(context), - ), - ), - const SizedBox( - height: 8, + ), + SizedBox( + height: isDesktop ? 16 : 8, + ), + if (isDesktop) + Row( + children: [ + Expanded( + child: TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: symbolController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Ticker", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: decimalsController, + style: STextStyles.field(context), + inputFormatters: [ + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*)$').hasMatch(newValue.text) + ? newValue + : oldValue), + ], + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: false, + ), + decoration: InputDecoration( + hintText: "Decimals", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + ), + ], ), + if (!isDesktop) TextField( enabled: enableSubFields, autocorrect: !isDesktop, @@ -142,9 +231,11 @@ class _AddCustomTokenViewState extends ConsumerState { hintStyle: STextStyles.fieldLabel(context), ), ), + if (!isDesktop) const SizedBox( height: 8, ), + if (!isDesktop) TextField( enabled: enableSubFields, autocorrect: !isDesktop, @@ -166,19 +257,35 @@ class _AddCustomTokenViewState extends ConsumerState { hintStyle: STextStyles.fieldLabel(context), ), ), - const SizedBox( - height: 16, - ), - const Spacer(), - PrimaryButton( - label: "Add token", - enabled: addTokenButtonEnabled, - onPressed: () { - Navigator.of(context).pop(currentToken!); - }, - ), - ], - ), + const SizedBox( + height: 16, + ), + const Spacer(), + Row( + children: [ + if (isDesktop) + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Add token", + enabled: addTokenButtonEnabled, + onPressed: () { + Navigator.of(context).pop(currentToken!); + }, + ), + ), + ], + ), + ], ), ), ); diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index bb90a5c18..2307d0591 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; @@ -80,17 +81,30 @@ class _AddWalletViewState extends ConsumerState { } Future _addToken() async { - final token = await Navigator.of(context).pushNamed( - AddCustomTokenView.routeName, - ); - if (token is EthContract) { - await MainDB.instance.putEthContract(token); + EthContract? contract; + if (isDesktop) { + contract = await showDialog( + context: context, + builder: (context) => const DesktopDialog( + maxWidth: 580, + maxHeight: 500, + child: AddCustomTokenView(), + ), + ); + } else { + contract = await Navigator.of(context).pushNamed( + AddCustomTokenView.routeName, + ); + } + + if (contract != null) { + await MainDB.instance.putEthContract(contract); if (mounted) { setState(() { if (tokenEntities - .where((e) => e.token.address == token.address) + .where((e) => e.token.address == contract!.address) .isEmpty) { - tokenEntities.add(EthTokenEntity(token)); + tokenEntities.add(EthTokenEntity(contract!)); tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); } }); From 2458d2912b9239771542952e6ff1a8365bb0b1e1 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 14:38:04 -0600 Subject: [PATCH 170/208] WIP desktop add eth token to wallet gui --- .../select_wallet_for_token_view.dart | 187 ++++++++++-------- lib/widgets/eth_wallet_radio.dart | 77 ++++++++ .../wallet_info_row_coin_icon.dart | 6 +- 3 files changed, 187 insertions(+), 83 deletions(-) create mode 100644 lib/widgets/eth_wallet_radio.dart diff --git a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart index 162c2c01c..c73393e4c 100644 --- a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart +++ b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart @@ -17,7 +17,10 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/eth_wallet_radio.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; @@ -159,91 +162,111 @@ class _SelectWalletForTokenViewState ), ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "Select Ethereum wallet", - textAlign: TextAlign.center, - style: STextStyles.pageTitleH1(context), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopScaffold( + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), ), - const SizedBox( - height: 8, + body: SizedBox( + width: 480, + child: child, ), - Text( - "You are adding an ETH token.", - textAlign: TextAlign.center, - style: STextStyles.subtitle(context), - ), - const SizedBox( - height: 8, - ), - Text( - "You must choose an Ethereum wallet in order to use ${widget.entity.name}", - textAlign: TextAlign.center, - style: STextStyles.subtitle(context), - ), - const SizedBox( - height: 16, - ), - ethWalletIds.isEmpty - ? RoundedWhiteContainer( - child: Text( - _hasEthWallets - ? "All current Ethereum wallets already have ${widget.entity.name}" - : "You do not have any Ethereum wallets", - style: STextStyles.label(context), - ), - ) - : Expanded( - child: Column( - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(8), - child: ListView.separated( - itemCount: ethWalletIds.length, - shrinkWrap: true, - separatorBuilder: (_, __) => const SizedBox( - height: 6, - ), - itemBuilder: (_, index) { - return RoundedContainer( - padding: const EdgeInsets.all(8), - onPressed: () { - setState(() { - _selectedWalletId = ethWalletIds[index]; - }); - }, - color: _selectedWalletId == ethWalletIds[index] - ? Theme.of(context) - .extension()! - .highlight - : Colors.transparent, - child: WalletInfoRow( - walletId: ethWalletIds[index], - ), - ); - }, - ), - ), - ], - ), - ), - if (ethWalletIds.isEmpty) - const SizedBox( - height: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Select Ethereum wallet", + textAlign: TextAlign.center, + style: STextStyles.pageTitleH1(context), ), - ethWalletIds.isEmpty - ? PrimaryButton( - label: "Add new Ethereum wallet", - onPressed: _onAddNewEthWallet, - ) - : PrimaryButton( - label: "Continue", - enabled: _selectedWalletId != null, - onPressed: _onContinue, - ), - ], + SizedBox( + height: isDesktop ? 16 : 8, + ), + Text( + "You are adding an ETH token.", + textAlign: TextAlign.center, + style: STextStyles.subtitle(context), + ), + const SizedBox( + height: 8, + ), + Text( + "You must choose an Ethereum wallet in order to use ${widget.entity.name}", + textAlign: TextAlign.center, + style: STextStyles.subtitle(context), + ), + SizedBox( + height: isDesktop ? 60 : 16, + ), + ethWalletIds.isEmpty + ? RoundedWhiteContainer( + child: Text( + _hasEthWallets + ? "All current Ethereum wallets already have ${widget.entity.name}" + : "You do not have any Ethereum wallets", + style: STextStyles.label(context), + ), + ) + : Expanded( + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(8), + child: ListView.separated( + itemCount: ethWalletIds.length, + shrinkWrap: true, + separatorBuilder: (_, __) => SizedBox( + height: isDesktop ? 12 : 6, + ), + itemBuilder: (_, index) { + return RoundedContainer( + padding: const EdgeInsets.all(8), + onPressed: () { + setState(() { + _selectedWalletId = ethWalletIds[index]; + }); + }, + color: isDesktop + ? Colors.transparent + : _selectedWalletId == ethWalletIds[index] + ? Theme.of(context) + .extension()! + .highlight + : Colors.transparent, + child: isDesktop + ? EthWalletRadio( + walletId: ethWalletIds[index], + selectedWalletId: _selectedWalletId, + ) + : WalletInfoRow( + walletId: ethWalletIds[index], + ), + ); + }, + ), + ), + ], + ), + ), + if (ethWalletIds.isEmpty) + const SizedBox( + height: 16, + ), + ethWalletIds.isEmpty + ? PrimaryButton( + label: "Add new Ethereum wallet", + onPressed: _onAddNewEthWallet, + ) + : PrimaryButton( + label: "Continue", + enabled: _selectedWalletId != null, + onPressed: _onContinue, + ), + ], + ), ), ), ); diff --git a/lib/widgets/eth_wallet_radio.dart b/lib/widgets/eth_wallet_radio.dart new file mode 100644 index 000000000..c20a4cbb9 --- /dev/null +++ b/lib/widgets/eth_wallet_radio.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; + +class EthWalletRadio extends ConsumerStatefulWidget { + const EthWalletRadio({ + Key? key, + required this.walletId, + this.selectedWalletId, + }) : super(key: key); + + final String walletId; + final String? selectedWalletId; + + @override + ConsumerState createState() => _EthWalletRadioState(); +} + +class _EthWalletRadioState extends ConsumerState { + @override + Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + + return Padding( + padding: EdgeInsets.zero, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + IgnorePointer( + child: Radio( + value: widget.walletId, + groupValue: widget.selectedWalletId, + onChanged: (_) { + // do nothing since changing updating the ui is already + // done elsewhere + }, + ), + ), + const SizedBox( + width: 12, + ), + WalletInfoCoinIcon( + coin: manager.coin, + size: 40, + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + manager.walletName, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark, + ), + ), + WalletInfoRowBalance( + walletId: widget.walletId, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart index a0ebceb12..afe47e250 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart @@ -13,11 +13,13 @@ class WalletInfoCoinIcon extends StatelessWidget { const WalletInfoCoinIcon({ Key? key, required this.coin, + this.size = 32, this.contractAddress, }) : super(key: key); final Coin coin; final String? contractAddress; + final double size; @override Widget build(BuildContext context) { @@ -37,6 +39,8 @@ class WalletInfoCoinIcon extends StatelessWidget { } return Container( + width: size, + height: size, decoration: BoxDecoration( color: Theme.of(context) .extension()! @@ -47,7 +51,7 @@ class WalletInfoCoinIcon extends StatelessWidget { ), ), child: Padding( - padding: const EdgeInsets.all(6), + padding: EdgeInsets.all(size / 5), child: currency != null && currency.image.isNotEmpty ? SvgPicture.network( currency.image, From bcadb07e9b893cd387bbfb3c7f5ee578b3236d59 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 15:14:45 -0600 Subject: [PATCH 171/208] temp debugging print --- lib/services/ethereum/ethereum_token_service.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 4cbe18d72..1b609d69c 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -108,6 +108,11 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { value: web3dart.EtherAmount.inWei(BigInt.one), ); + print("=============================================================="); + print("client.estimateGas: $est"); + print("estimateFeeFor : $feeEstimate"); + print("=============================================================="); + final tx = web3dart.Transaction.callContract( contract: _deployedContract, function: _sendFunction, From d4cba56e8129965c99e316bd8d7b0fa9d90241dd Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 15:38:22 -0600 Subject: [PATCH 172/208] fetch raw tx call --- lib/dto/ethereum/pending_eth_tx_dto.dart | 150 +++++++++++++++++++++++ lib/services/ethereum/ethereum_api.dart | 63 ++++++++++ 2 files changed, 213 insertions(+) create mode 100644 lib/dto/ethereum/pending_eth_tx_dto.dart diff --git a/lib/dto/ethereum/pending_eth_tx_dto.dart b/lib/dto/ethereum/pending_eth_tx_dto.dart new file mode 100644 index 000000000..0c1fa03b6 --- /dev/null +++ b/lib/dto/ethereum/pending_eth_tx_dto.dart @@ -0,0 +1,150 @@ +/// blockHash : null +/// blockNumber : null +/// from : "0x..." +/// gas : "0x7e562" +/// maxPriorityFeePerGas : "0x444380" +/// maxFeePerGas : "0x342570c00" +/// hash : "0x...da64e4" +/// input : "....." +/// nonce : "0x70" +/// to : "0x00....." +/// transactionIndex : null +/// value : "0x0" +/// type : "0x2" +/// accessList : [] +/// chainId : "0x1" +/// v : "0x0" +/// r : "0xd..." +/// s : "0x17d...6e6" + +class PendingEthTxDto { + PendingEthTxDto({ + required this.blockHash, + required this.blockNumber, + required this.from, + required this.gas, + required this.maxPriorityFeePerGas, + required this.maxFeePerGas, + required this.hash, + required this.input, + required this.nonce, + required this.to, + required this.transactionIndex, + required this.value, + required this.type, + required this.accessList, + required this.chainId, + required this.v, + required this.r, + required this.s, + }); + + factory PendingEthTxDto.fromMap(Map map) => PendingEthTxDto( + blockHash: map['blockHash'] as String?, + blockNumber: map['blockNumber'] as int?, + from: map['from'] as String, + gas: map['gas'] as String, + maxPriorityFeePerGas: map['maxPriorityFeePerGas'] as String, + maxFeePerGas: map['maxFeePerGas'] as String, + hash: map['hash'] as String, + input: map['input'] as String, + nonce: map['nonce'] as String, + to: map['to'] as String, + transactionIndex: map['transactionIndex'] as int?, + value: map['value'] as String, + type: map['type'] as String, + accessList: map['accessList'] as List? ?? [], + chainId: map['chainId'] as String, + v: map['v'] as String, + r: map['r'] as String, + s: map['s'] as String, + ); + + final String? blockHash; + final int? blockNumber; + final String from; + final String gas; + final String maxPriorityFeePerGas; + final String maxFeePerGas; + final String hash; + final String input; + final String nonce; + final String to; + final int? transactionIndex; + final String value; + final String type; + final List accessList; + final String chainId; + final String v; + final String r; + final String s; + + PendingEthTxDto copyWith({ + String? blockHash, + int? blockNumber, + String? from, + String? gas, + String? maxPriorityFeePerGas, + String? maxFeePerGas, + String? hash, + String? input, + String? nonce, + String? to, + int? transactionIndex, + String? value, + String? type, + List? accessList, + String? chainId, + String? v, + String? r, + String? s, + }) => + PendingEthTxDto( + blockHash: blockHash ?? this.blockHash, + blockNumber: blockNumber ?? this.blockNumber, + from: from ?? this.from, + gas: gas ?? this.gas, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, + maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, + hash: hash ?? this.hash, + input: input ?? this.input, + nonce: nonce ?? this.nonce, + to: to ?? this.to, + transactionIndex: transactionIndex ?? this.transactionIndex, + value: value ?? this.value, + type: type ?? this.type, + accessList: accessList ?? this.accessList, + chainId: chainId ?? this.chainId, + v: v ?? this.v, + r: r ?? this.r, + s: s ?? this.s, + ); + + Map toMap() { + final map = {}; + map['blockHash'] = blockHash; + map['blockNumber'] = blockNumber; + map['from'] = from; + map['gas'] = gas; + map['maxPriorityFeePerGas'] = maxPriorityFeePerGas; + map['maxFeePerGas'] = maxFeePerGas; + map['hash'] = hash; + map['input'] = input; + map['nonce'] = nonce; + map['to'] = to; + map['transactionIndex'] = transactionIndex; + map['value'] = value; + map['type'] = type; + map['accessList'] = accessList; + map['chainId'] = chainId; + map['v'] = v; + map['r'] = r; + map['s'] = s; + return map; + } + + @override + String toString() { + return toMap().toString(); + } +} diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index dc474340a..938858806 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -6,6 +6,7 @@ import 'package:http/http.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart'; import 'package:stackwallet/dto/ethereum/eth_tx_dto.dart'; +import 'package:stackwallet/dto/ethereum/pending_eth_tx_dto.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; @@ -90,6 +91,68 @@ abstract class EthereumAPI { } } + static Future> getEthTransactionByHash( + String txid) async { + try { + final response = await post( + Uri.parse( + "$stackBaseServer/v1/mainnet", + ), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + "jsonrpc": "2.0", + "method": "eth_getTransactionByHash", + "params": [ + txid, + ], + "id": DateTime.now().millisecondsSinceEpoch, + }), + ); + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + try { + final json = jsonDecode(response.body) as Map; + final result = json["result"] as Map; + return EthereumResponse( + PendingEthTxDto.fromMap(Map.from(result)), + null, + ); + } catch (_) { + throw EthApiException( + "getEthTransactionByHash($txid) failed with response: " + "${response.body}", + ); + } + } else { + throw EthApiException( + "getEthTransactionByHash($txid) response is empty but status code is " + "${response.statusCode}", + ); + } + } else { + throw EthApiException( + "getEthTransactionByHash($txid) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getEthTransactionByHash($txid): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + static Future>>> getEthTransactionNonces( List txns, From 521a9bb0a3e9b1412f93738727c1cf65b4621afa Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 15:42:28 -0600 Subject: [PATCH 173/208] disable send all for eth + tokens --- lib/pages/send_view/send_view.dart | 69 +++++++------ lib/pages/send_view/token_send_view.dart | 124 +++++++++++------------ 2 files changed, 98 insertions(+), 95 deletions(-) diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 508b9d6cc..166456c0f 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -364,7 +364,8 @@ class _SendViewState extends ConsumerState { final coinControlEnabled = ref.read(prefsChangeNotifierProvider).enableCoinControl; - if (!(manager.hasCoinControlSupport && coinControlEnabled) || + if (coin != Coin.ethereum && + !(manager.hasCoinControlSupport && coinControlEnabled) || (manager.hasCoinControlSupport && coinControlEnabled && selectedUTXOs.isEmpty)) { @@ -1395,43 +1396,45 @@ class _SendViewState extends ConsumerState { style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - CustomTextButton( - text: "Send all ${coin.ticker}", - onTap: () async { - if (coin == Coin.firo || - coin == Coin.firoTestNet) { - final firoWallet = - ref.read(provider).wallet as FiroWallet; - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - cryptoAmountController.text = firoWallet - .availablePrivateBalance() - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); + if (coin != Coin.ethereum) + CustomTextButton( + text: "Send all ${coin.ticker}", + onTap: () async { + if (coin == Coin.firo || + coin == Coin.firoTestNet) { + final firoWallet = ref + .read(provider) + .wallet as FiroWallet; + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + cryptoAmountController.text = firoWallet + .availablePrivateBalance() + .toStringAsFixed( + Constants.decimalPlacesForCoin( + coin)); + } else { + cryptoAmountController.text = firoWallet + .availablePublicBalance() + .toStringAsFixed( + Constants.decimalPlacesForCoin( + coin)); + } } else { - cryptoAmountController.text = firoWallet - .availablePublicBalance() + cryptoAmountController.text = (ref + .read(provider) + .balance + .getSpendable()) .toStringAsFixed( Constants.decimalPlacesForCoin( coin)); } - } else { - cryptoAmountController.text = (ref - .read(provider) - .balance - .getSpendable()) - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); - } - _cryptoAmountChanged(); - }, - ), + _cryptoAmountChanged(); + }, + ), ], ), const SizedBox( diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 574f836ca..5756cf99b 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -402,56 +402,56 @@ class _TokenSendViewState extends ConsumerState { fractionDigits: tokenContract.decimals, ); - // confirm send all - if (amount == availableBalance) { - bool? shouldSendAll; - if (mounted) { - shouldSendAll = await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return StackDialog( - title: "Confirm send all", - message: - "You are about to send your entire balance. Would you like to continue?", - leftButton: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonStyle(context), - child: Text( - "Cancel", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark), - ), - onPressed: () { - Navigator.of(context).pop(false); - }, - ), - rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - child: Text( - "Yes", - style: STextStyles.button(context), - ), - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ); - }, - ); - } - - if (shouldSendAll == null || shouldSendAll == false) { - // cancel preview - return; - } - } + // // confirm send all + // if (amount == availableBalance) { + // bool? shouldSendAll; + // if (mounted) { + // shouldSendAll = await showDialog( + // context: context, + // useSafeArea: false, + // barrierDismissible: true, + // builder: (context) { + // return StackDialog( + // title: "Confirm send all", + // message: + // "You are about to send your entire balance. Would you like to continue?", + // leftButton: TextButton( + // style: Theme.of(context) + // .extension()! + // .getSecondaryEnabledButtonStyle(context), + // child: Text( + // "Cancel", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .accentColorDark), + // ), + // onPressed: () { + // Navigator.of(context).pop(false); + // }, + // ), + // rightButton: TextButton( + // style: Theme.of(context) + // .extension()! + // .getPrimaryEnabledButtonStyle(context), + // child: Text( + // "Yes", + // style: STextStyles.button(context), + // ), + // onPressed: () { + // Navigator.of(context).pop(true); + // }, + // ), + // ); + // }, + // ); + // } + // + // if (shouldSendAll == null || shouldSendAll == false) { + // // cancel preview + // return; + // } + // } try { bool wasCancelled = false; @@ -908,18 +908,18 @@ class _TokenSendViewState extends ConsumerState { style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - CustomTextButton( - text: "Send all ${tokenContract.symbol}", - onTap: () async { - cryptoAmountController.text = ref - .read(tokenServiceProvider)! - .balance - .getSpendable() - .toStringAsFixed(tokenContract.decimals); - - _cryptoAmountChanged(); - }, - ), + // CustomTextButton( + // text: "Send all ${tokenContract.symbol}", + // onTap: () async { + // cryptoAmountController.text = ref + // .read(tokenServiceProvider)! + // .balance + // .getSpendable() + // .toStringAsFixed(tokenContract.decimals); + // + // _cryptoAmountChanged(); + // }, + // ), ], ), const SizedBox( From 1de0eec6cccc07c4925470d54317c6ce3ee0311c Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 16:31:19 -0600 Subject: [PATCH 174/208] get current address nonce --- lib/services/ethereum/ethereum_api.dart | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 938858806..13488e617 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -451,6 +451,52 @@ abstract class EthereumAPI { } } + static Future> getAddressNonce({ + required String address, + }) async { + try { + final uri = Uri.parse( + "$stackBaseServer/state?addrs=$address&parts=nonce", + ); + final response = await get(uri); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json["data"] is List) { + final map = json["data"].first as Map; + + final nonce = map["nonce"] as int; + + return EthereumResponse( + nonce, + null, + ); + } else { + throw EthApiException(json["message"] as String); + } + } else { + throw EthApiException( + "getWalletTokenBalance($address) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getWalletTokenBalance(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + static Future> getGasOracle() async { try { final response = await get( From 6704d8288967886d3cf32f52b6517e8614ddace6 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 17:17:15 -0600 Subject: [PATCH 175/208] eth + token send fixes --- .../coins/ethereum/ethereum_wallet.dart | 156 ++++++++++++++---- .../ethereum/ethereum_token_service.dart | 100 +++++++++-- 2 files changed, 212 insertions(+), 44 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 0afd30bf8..5c93e6a13 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -32,6 +32,7 @@ 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/extensions/extensions.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -183,7 +184,8 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { Future> get transactions => db .getTransactions(walletId) .filter() - .otherDataEqualTo(null) + .otherDataEqualTo( + null) // eth txns with other data where other data is the token contract address .sortByTimestampDesc() .findAll(); @@ -440,22 +442,70 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { final feeEstimate = await estimateFeeFor(satoshiAmount, fee); - bool isSendAll = false; - final availableBalance = balance.spendable; - if (satoshiAmount == availableBalance) { - isSendAll = true; - } + // bool isSendAll = false; + // final availableBalance = balance.spendable; + // if (satoshiAmount == availableBalance) { + // isSendAll = true; + // } + // + // if (isSendAll) { + // //Subtract fee amount from send amount + // satoshiAmount -= feeEstimate; + // } - if (isSendAll) { - //Subtract fee amount from send amount - satoshiAmount -= feeEstimate; - } + final decimalAmount = Format.satoshisToAmount(satoshiAmount, coin: coin); + final bigIntAmount = amountToBigInt( + decimalAmount.toDouble(), + Constants.decimalPlacesForCoin(coin), + ); + + final client = getEthClient(); + + final myAddress = await currentReceivingAddress; + final myWeb3Address = web3.EthereumAddress.fromHex(myAddress); + + final est = await client.estimateGas( + sender: myWeb3Address, + to: web3.EthereumAddress.fromHex(address), + gasPrice: web3.EtherAmount.fromUnitAndValue( + web3.EtherUnit.wei, + fee, + ), + amountOfGas: BigInt.from(_gasLimit), + value: web3.EtherAmount.inWei(bigIntAmount), + ); + + final nonce = args?["nonce"] as int? ?? + await client.getTransactionCount(myWeb3Address, + atBlock: const web3.BlockNum.pending()); + + final nResponse = await EthereumAPI.getAddressNonce(address: myAddress); + print("=============================================================="); + print("ETH client.estimateGas: $est"); + print("ETH estimateFeeFor : $feeEstimate"); + print("ETH nonce custom response: $nResponse"); + print("ETH actual nonce : $nonce"); + print("=============================================================="); + + final tx = web3.Transaction( + to: web3.EthereumAddress.fromHex(address), + gasPrice: web3.EtherAmount.fromUnitAndValue( + web3.EtherUnit.wei, + fee, + ), + maxGas: _gasLimit, + value: web3.EtherAmount.inWei(bigIntAmount), + nonce: nonce, + ); Map txData = { "fee": feeEstimate, "feeInWei": fee, "address": address, "recipientAmt": satoshiAmount, + "ethTx": tx, + "chainId": (await client.getChainId()).toInt(), + "nonce": tx.nonce, }; return txData; @@ -464,22 +514,12 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future confirmSend({required Map txData}) async { web3.Web3Client client = getEthClient(); - final chainId = await client.getChainId(); - final amount = txData['recipientAmt'] as int; - final decimalAmount = Format.satoshisToAmount(amount, coin: coin); - final bigIntAmount = amountToBigInt( - decimalAmount.toDouble(), - Constants.decimalPlacesForCoin(coin), - ); - final tx = web3.Transaction( - to: web3.EthereumAddress.fromHex(txData['address'] as String), - gasPrice: web3.EtherAmount.fromUnitAndValue( - web3.EtherUnit.wei, txData['feeInWei']), - maxGas: _gasLimit, - value: web3.EtherAmount.inWei(bigIntAmount)); - final txid = await client.sendTransaction(_credentials, tx, - chainId: chainId.toInt()); + final txid = await client.sendTransaction( + _credentials, + txData["ethTx"] as web3.Transaction, + chainId: txData["chainId"] as int, + ); return txid; } @@ -558,7 +598,6 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { .findAll(); Future refreshIfThereIsNewData() async { - web3.Web3Client client = getEthClient(); if (longMutex) return false; if (_hasCalledExit) return false; final currentChainHeight = await chainHeight; @@ -574,14 +613,16 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { } for (String txid in txnsToCheck) { - final txn = await client.getTransactionByHash(txid); - final int txBlockNumber = txn.blockNumber.blockNum; + final response = await EthereumAPI.getEthTransactionByHash(txid); + final txBlockNumber = response.value?.blockNumber; - final int txConfirmations = currentChainHeight - txBlockNumber; - bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS; - if (!isUnconfirmed) { - needsRefresh = true; - break; + if (txBlockNumber != null) { + final int txConfirmations = currentChainHeight - txBlockNumber; + bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS; + if (!isUnconfirmed) { + needsRefresh = true; + break; + } } } if (!needsRefresh) { @@ -857,7 +898,54 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future updateSentCachedTxData(Map txData) async { - //Only used for Electrumx coins + final txid = txData["txid"] as String; + final addressString = txData["address"] as String; + final response = await EthereumAPI.getEthTransactionByHash(txid); + + final transaction = Transaction( + walletId: walletId, + txid: txid, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + type: TransactionType.outgoing, + subType: TransactionSubType.none, + amount: txData["recipientAmt"] as int, + amountString: Amount( + rawValue: BigInt.from(txData["recipientAmt"] as int), + fractionDigits: coin.decimals, + ).toJsonString(), + fee: txData["fee"] as int, + height: null, + isCancelled: false, + isLelantus: false, + otherData: null, + slateId: null, + nonce: (txData["nonce"] as int?) ?? + response.value?.nonce.toBigIntFromHex.toInt(), + inputs: [], + outputs: [], + ); + + Address? address = await db.getAddress( + walletId, + addressString, + ); + + address ??= Address( + walletId: walletId, + value: addressString, + publicKey: [], + derivationIndex: -1, + derivationPath: null, + type: AddressType.ethereum, + subType: AddressSubType.nonWallet, + ); + + await db.addNewTransactionData( + [ + Tuple2(transaction, address), + ], + walletId, + ); } @override diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 1b609d69c..09fe51e21 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -95,8 +95,11 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { final client = await getEthClient(); + final myAddress = await currentReceivingAddress; + final myWeb3Address = web3dart.EthereumAddress.fromHex(myAddress); + final est = await client.estimateGas( - sender: web3dart.EthereumAddress.fromHex(await currentReceivingAddress), + sender: myWeb3Address, to: web3dart.EthereumAddress.fromHex(address), data: _sendFunction.encodeCall( [web3dart.EthereumAddress.fromHex(address), bigIntAmount]), @@ -108,9 +111,16 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { value: web3dart.EtherAmount.inWei(BigInt.one), ); + final nonce = args?["nonce"] as int? ?? + await client.getTransactionCount(myWeb3Address, + atBlock: const web3dart.BlockNum.pending()); + + final nResponse = await EthereumAPI.getAddressNonce(address: myAddress); print("=============================================================="); - print("client.estimateGas: $est"); - print("estimateFeeFor : $feeEstimate"); + print("TOKEN client.estimateGas: $est"); + print("TOKEN estimateFeeFor : $feeEstimate"); + print("TOKEN nonce custom response: $nResponse"); + print("TOKEN actual nonce : $nonce"); print("=============================================================="); final tx = web3dart.Transaction.callContract( @@ -122,7 +132,7 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { web3dart.EtherUnit.wei, fee, ), - nonce: args?["nonce"] as int?, + nonce: nonce, ); Map txData = { @@ -131,18 +141,88 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { "address": address, "recipientAmt": satoshiAmount, "ethTx": tx, + "chainId": (await client.getChainId()).toInt(), + "nonce": tx.nonce, }; return txData; } Future confirmSend({required Map txData}) async { - final txid = await _client.sendTransaction( - _credentials, - txData["ethTx"] as web3dart.Transaction, + try { + final txid = await _client.sendTransaction( + _credentials, + txData["ethTx"] as web3dart.Transaction, + chainId: txData["chainId"] as int, + ); + + try { + txData["txid"] = txid; + await updateSentCachedTxData(txData); + } catch (e, s) { + // do not rethrow as that would get handled as a send failure further up + // also this is not critical code and transaction should show up on \ + // refresh regardless + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } + + notifyListeners(); + return txid; + } catch (e) { + // rethrow to pass error in alert + rethrow; + } + } + + Future updateSentCachedTxData(Map txData) async { + final txid = txData["txid"] as String; + final addressString = txData["address"] as String; + final response = await EthereumAPI.getEthTransactionByHash(txid); + + final transaction = Transaction( + walletId: ethWallet.walletId, + txid: txid, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + type: TransactionType.outgoing, + subType: TransactionSubType.ethToken, + amount: txData["recipientAmt"] as int, + amountString: Amount( + rawValue: BigInt.from(txData["recipientAmt"] as int), + fractionDigits: tokenContract.decimals, + ).toJsonString(), + fee: txData["fee"] as int, + height: null, + isCancelled: false, + isLelantus: false, + otherData: tokenContract.address, + slateId: null, + nonce: (txData["nonce"] as int?) ?? + response.value?.nonce.toBigIntFromHex.toInt(), + inputs: [], + outputs: [], ); - return txid; + Address? address = await ethWallet.db.getAddress( + ethWallet.walletId, + addressString, + ); + + address ??= Address( + walletId: ethWallet.walletId, + value: addressString, + publicKey: [], + derivationIndex: -1, + derivationPath: null, + type: AddressType.ethereum, + subType: AddressSubType.nonWallet, + ); + + await ethWallet.db.addNewTransactionData( + [ + Tuple2(transaction, address), + ], + ethWallet.walletId, + ); } Future get currentReceivingAddress async { @@ -499,14 +579,14 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { return isValidEthereumAddress(address); } - Future getCurrentNode() async { + NodeModel getCurrentNode() { return NodeService(secureStorageInterface: _secureStore) .getPrimaryNodeFor(coin: coin) ?? DefaultNodes.getNodeFor(coin); } Future getEthClient() async { - final node = await getCurrentNode(); + final node = getCurrentNode(); return web3dart.Web3Client(node.host, Client()); } } From 9a86a6fe9ec1d0d91478ceb758a2398567cc93d3 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 18:30:32 -0600 Subject: [PATCH 176/208] remove value from estimate for token tx --- lib/services/ethereum/ethereum_token_service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 09fe51e21..81bd6ed86 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -108,7 +108,6 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { fee, ), amountOfGas: BigInt.from(_gasLimit), - value: web3dart.EtherAmount.inWei(BigInt.one), ); final nonce = args?["nonce"] as int? ?? From f7a49c8f8ae507e87bb06df390c88e7c10dcd93b Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 31 Mar 2023 18:53:52 -0600 Subject: [PATCH 177/208] fix tests --- test/services/coins/manager_test.dart | 3 +++ .../table_view/table_view_row_test.dart | 21 ++++++++++--------- test/widget_tests/transaction_card_test.dart | 8 +++---- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/test/services/coins/manager_test.dart b/test/services/coins/manager_test.dart index 55ed98753..265540549 100644 --- a/test/services/coins/manager_test.dart +++ b/test/services/coins/manager_test.dart @@ -98,6 +98,9 @@ void main() { test("transactions", () async { final CoinServiceAPI wallet = MockFiroWallet(); + + when(wallet.coin).thenAnswer((realInvocation) => Coin.firo); + final tx = Transaction( walletId: "walletId", txid: "txid", diff --git a/test/widget_tests/table_view/table_view_row_test.dart b/test/widget_tests/table_view/table_view_row_test.dart index 26292b7e5..9f732f51e 100644 --- a/test/widget_tests/table_view/table_view_row_test.dart +++ b/test/widget_tests/table_view/table_view_row_test.dart @@ -29,6 +29,8 @@ import 'table_view_row_test.mocks.dart'; ]) void main() { testWidgets('Test table view row', (widgetTester) async { + widgetTester.binding.window.physicalSizeTestValue = const Size(2500, 1800); + final mockWallet = MockWallets(); final CoinServiceAPI wallet = MockBitcoinWallet(); when(wallet.coin).thenAnswer((_) => Coin.bitcoin); @@ -56,8 +58,6 @@ void main() { when(mockWallet.getManagerProvider("wallet id 2")).thenAnswer( (realInvocation) => ChangeNotifierProvider((ref) => manager)); - final walletIds = mockWallet.getWalletIdsFor(coin: Coin.bitcoin); - await widgetTester.pumpWidget( ProviderScope( overrides: [ @@ -73,13 +73,14 @@ void main() { ), home: Material( child: TableViewRow( - cells: [ - for (int j = 1; j <= 5; j++) - TableViewCell(flex: 16, child: Text("Some Text ${j}")) - ], - expandingChild: const CoinWalletsTable( - coin: Coin.bitcoin, - )), + cells: [ + for (int j = 1; j <= 5; j++) + TableViewCell(flex: 16, child: Text("Some ${j}")) + ], + expandingChild: const CoinWalletsTable( + coin: Coin.bitcoin, + ), + ), ), ), ), @@ -87,7 +88,7 @@ void main() { await widgetTester.pumpAndSettle(); - expect(find.text("Some Text 1"), findsOneWidget); + expect(find.text("Some 1"), findsOneWidget); expect(find.byType(TableViewRow), findsWidgets); expect(find.byType(TableViewCell), findsWidgets); expect(find.byType(CoinWalletsTable), findsWidgets); diff --git a/test/widget_tests/transaction_card_test.dart b/test/widget_tests/transaction_card_test.dart index 959c4e993..5da6b9bc6 100644 --- a/test/widget_tests/transaction_card_test.dart +++ b/test/widget_tests/transaction_card_test.dart @@ -138,7 +138,7 @@ void main() { verify(mockPrefs.currency).called(1); verify(mockPriceService.getPrice(Coin.firo)).called(1); - verify(wallet.coin.ticker).called(2); + verify(wallet.coin.ticker).called(1); verify(mockLocaleService.locale).called(1); @@ -241,7 +241,7 @@ void main() { verify(mockPrefs.currency).called(1); verify(mockPriceService.getPrice(Coin.firo)).called(1); - verify(wallet.coin.ticker).called(2); + verify(wallet.coin.ticker).called(1); verify(mockLocaleService.locale).called(1); @@ -337,7 +337,7 @@ void main() { verify(mockPrefs.currency).called(1); verify(mockPriceService.getPrice(Coin.firo)).called(1); - verify(wallet.coin.ticker).called(2); + verify(wallet.coin.ticker).called(1); verify(mockLocaleService.locale).called(1); @@ -434,7 +434,7 @@ void main() { verify(mockPrefs.currency).called(2); verify(mockLocaleService.locale).called(4); - verify(wallet.coin.ticker).called(2); + verify(wallet.coin.ticker).called(1); verify(wallet.storedChainHeight).called(2); verifyNoMoreInteractions(wallet); From ce7b222fa1b386de9160531362ab7af6af958f27 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 10:55:10 -0600 Subject: [PATCH 178/208] use main db provider --- .../addresses/desktop_wallet_addresses_view.dart | 9 ++++++--- .../addresses/sub_widgets/desktop_address_list.dart | 11 +++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart b/lib/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart index ae750554a..54c5f4daa 100644 --- a/lib/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart +++ b/lib/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_details_view.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -45,8 +45,11 @@ class _DesktopWalletAddressesViewState @override void initState() { - addressCollectionWatcher = - MainDB.instance.isar.addresses.watchLazy(fireImmediately: true); + addressCollectionWatcher = ref + .read(mainDBProvider) + .isar + .addresses + .watchLazy(fireImmediately: true); addressCollectionWatcher.listen((_) => _onAddressCollectionWatcherEvent()); super.initState(); diff --git a/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart b/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart index 419d245e5..16004fc0f 100644 --- a/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart +++ b/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -41,7 +41,8 @@ class _DesktopAddressListState extends ConsumerState { List _search(String term) { if (term.isEmpty) { - return MainDB.instance + return ref + .read(mainDBProvider) .getAddresses(widget.walletId) .filter() .group((q) => q @@ -60,7 +61,8 @@ class _DesktopAddressListState extends ConsumerState { .findAllSync(); } - final labels = MainDB.instance + final labels = ref + .read(mainDBProvider) .getAddressLabels(widget.walletId) .filter() .group( @@ -82,7 +84,8 @@ class _DesktopAddressListState extends ConsumerState { return []; } - return MainDB.instance + return ref + .read(mainDBProvider) .getAddresses(widget.walletId) .filter() .anyOf( From 87aab96e58812e5dd5979a7d6766ce64b077ac5d Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 11:25:56 -0600 Subject: [PATCH 179/208] get real nonce for eth txns --- lib/services/ethereum/ethereum_api.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 13488e617..3bcb81b71 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -11,6 +11,7 @@ import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:tuple/tuple.dart'; @@ -160,7 +161,7 @@ abstract class EthereumAPI { try { final response = await get( Uri.parse( - "$stackBaseServer/transactions?transactions=${txns.map((e) => e.hash).join(" ")}", + "$stackBaseServer/transactions?transactions=${txns.map((e) => e.hash).join(" ")}&raw=true", ), ); @@ -175,7 +176,7 @@ abstract class EthereumAPI { final data = list.firstWhere((e) => e["hash"] == dto.hash, orElse: () => {}); - final nonce = data["nonce"] as int?; + final nonce = (data["nonce"] as String?)?.toBigIntFromHex.toInt(); result.add(Tuple2(dto, nonce)); } return EthereumResponse( From 5e5b3100c42062b246596ea3744b6c4a7803628a Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 15:27:36 -0600 Subject: [PATCH 180/208] add uniswap to default eth tokens --- lib/utilities/default_eth_tokens.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/utilities/default_eth_tokens.dart b/lib/utilities/default_eth_tokens.dart index 93f783697..a13a890f5 100644 --- a/lib/utilities/default_eth_tokens.dart +++ b/lib/utilities/default_eth_tokens.dart @@ -44,5 +44,12 @@ abstract class DefaultTokens { decimals: 18, type: EthContractType.erc20, ), + EthContract( + address: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + name: "Uniswap", + symbol: "UNI", + decimals: 18, + type: EthContractType.erc20, + ), ]; } From c530aef1125e2cb849ae02b72cae8f22fd4f934d Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 16:22:40 -0600 Subject: [PATCH 181/208] redo desktop enter wallet gui --- .../dialogs/desktop_coin_wallets_dialog.dart | 347 ++++++++++++++++++ .../my_stack_view/wallet_summary_table.dart | 282 +++++++++----- lib/widgets/wallet_card.dart | 162 ++++---- .../wallet_info_row/wallet_info_row.dart | 127 ++++--- 4 files changed, 711 insertions(+), 207 deletions(-) create mode 100644 lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart diff --git a/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart b/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart new file mode 100644 index 000000000..c1fd35118 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart @@ -0,0 +1,347 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/expandable.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/widgets/wallet_card.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; + +class DesktopCoinWalletsDialog extends ConsumerStatefulWidget { + const DesktopCoinWalletsDialog({ + Key? key, + required this.coin, + required this.navigatorState, + }) : super(key: key); + + final Coin coin; + final NavigatorState navigatorState; + + @override + ConsumerState createState() => + _DesktopCoinWalletsDialogState(); +} + +class _DesktopCoinWalletsDialogState + extends ConsumerState { + final isDesktop = Util.isDesktop; + + late final TextEditingController _searchController; + late final FocusNode searchFieldFocusNode; + + String _searchString = ""; + + final List walletIds = []; + + @override + void initState() { + _searchController = TextEditingController(); + searchFieldFocusNode = FocusNode(); + + final walletsData = + ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + walletsData.removeWhere((key, value) => value.coin != widget.coin); + walletIds.clear(); + + walletIds.addAll(walletsData.values.map((e) => e.walletId)); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Search...", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 12 : 10, + vertical: isDesktop ? 18 : 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: isDesktop ? 20 : 16, + height: isDesktop ? 20 : 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: ListView.separated( + itemBuilder: (_, index) => widget.coin == Coin.ethereum + ? _DesktopWalletCard( + walletId: walletIds[index], + navigatorState: widget.navigatorState, + ) + : RoundedWhiteContainer( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 20, + ), + borderColor: Theme.of(context) + .extension()! + .backgroundAppBar, + child: WalletSheetCard( + walletId: walletIds[index], + popPrevious: true, + desktopNavigatorState: widget.navigatorState, + ), + ), + separatorBuilder: (_, __) => const SizedBox( + height: 10, + ), + itemCount: walletIds.length, + ), + ), + ], + ); + } +} + +class _DesktopWalletCard extends ConsumerStatefulWidget { + const _DesktopWalletCard({ + Key? key, + required this.walletId, + required this.navigatorState, + }) : super(key: key); + + final String walletId; + final NavigatorState navigatorState; + + @override + ConsumerState<_DesktopWalletCard> createState() => _DesktopWalletCardState(); +} + +class _DesktopWalletCardState extends ConsumerState<_DesktopWalletCard> { + final expandableController = ExpandableController(); + final rotateIconController = RotateIconController(); + final List tokenContractAddresses = []; + late final Coin coin; + + @override + void initState() { + coin = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .coin; + + if (coin == Coin.ethereum) { + tokenContractAddresses.addAll( + (ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as EthereumWallet) + .getWalletTokenContractAddresses(), + ); + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + padding: EdgeInsets.zero, + borderColor: Theme.of(context).extension()!.backgroundAppBar, + child: Expandable( + controller: expandableController, + expandOverride: () {}, + header: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + WalletInfoCoinIcon( + coin: coin, + ), + const SizedBox( + width: 12, + ), + Text( + ref.watch( + walletsChangeNotifierProvider.select( + (value) => value + .getManager(widget.walletId) + .walletName, + ), + ), + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ], + ), + ), + Expanded( + flex: 4, + child: WalletInfoRowBalance( + walletId: widget.walletId, + ), + ), + ], + ), + ), + MaterialButton( + padding: const EdgeInsets.all(5), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + minWidth: 32, + height: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + elevation: 0, + hoverElevation: 0, + disabledElevation: 0, + highlightElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + if (expandableController.state == ExpandableState.collapsed) { + rotateIconController.forward?.call(); + } else { + rotateIconController.reverse?.call(); + } + expandableController.toggle?.call(); + }, + child: RotateIcon( + controller: rotateIconController, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + ), + curve: Curves.easeInOut, + ), + ), + ], + ), + ), + body: ListView( + shrinkWrap: true, + primary: false, + children: [ + Container( + width: double.infinity, + height: 1, + color: + Theme.of(context).extension()!.backgroundAppBar, + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 14, + top: 14, + bottom: 14, + ), + child: WalletSheetCard( + walletId: widget.walletId, + popPrevious: true, + desktopNavigatorState: widget.navigatorState, + ), + ), + ...tokenContractAddresses.map( + (e) => Padding( + padding: const EdgeInsets.only( + left: 32, + right: 14, + top: 14, + bottom: 14, + ), + child: WalletSheetCard( + walletId: widget.walletId, + contractAddress: e, + popPrevious: true, + desktopNavigatorState: widget.navigatorState, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index c1fd12542..35f3126c1 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -1,18 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/coin_wallets_table.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/table_view/table_view.dart'; -import 'package:stackwallet/widgets/table_view/table_view_cell.dart'; -import 'package:stackwallet/widgets/table_view/table_view_row.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletSummaryTable extends ConsumerStatefulWidget { const WalletSummaryTable({Key? key}) : super(key: key); @@ -31,97 +29,217 @@ class _WalletTableState extends ConsumerState { ), ); - return TableView( - rows: [ - for (int i = 0; i < providersByCoin.length; i++) - Builder( - key: Key("${providersByCoin[i].item1.name}_${runtimeType}_key"), - builder: (context) { - final providers = providersByCoin[i].item2; + return ListView.separated( + itemBuilder: (_, index) { + final providers = providersByCoin[index].item2; + final coin = providersByCoin[index].item1; - VoidCallback? expandOverride; - if (providers.length == 1) { - expandOverride = () async { - final manager = ref.read(providers.first); - if (manager.coin == Coin.monero || - manager.coin == Coin.wownero) { - await manager.initializeExisting(); - } - await Navigator.of(context).pushNamed( - DesktopWalletView.routeName, - arguments: manager.walletId, - ); - }; - } - - return TableViewRow( - expandOverride: expandOverride, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, - ), - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - cells: [ - TableViewCell( - flex: 4, - child: Row( + return RoundedWhiteContainer( + padding: const EdgeInsets.all(20), + onPressed: () { + showDialog( + context: context, + builder: (_) => DesktopDialog( + maxHeight: 600, + maxWidth: 700, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: providersByCoin[i].item1), - width: 28, - height: 28, - ), - const SizedBox( - width: 10, - ), - Text( - providersByCoin[i].item1.prettyName, - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "${coin.prettyName} (${coin.ticker}) wallets", + style: STextStyles.desktopH3(context), ), - ) + ), + const DesktopDialogCloseButton(), ], ), - ), - TableViewCell( - flex: 4, - child: Text( - providers.length == 1 - ? "${providers.length} wallet" - : "${providers.length} wallets", + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopCoinWalletsDialog( + coin: coin, + navigatorState: Navigator.of(context), + ), + ), + ), + ], + ), + ), + ); + }, + child: Row( + children: [ + Expanded( + flex: 4, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: providersByCoin[index].item1), + width: 28, + height: 28, + ), + const SizedBox( + width: 10, + ), + Text( + providersByCoin[index].item1.prettyName, style: STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context) .extension()! - .textSubtitle1, + .textDark, ), - ), - ), - TableViewCell( - flex: 6, - child: TablePriceInfo( - coin: providersByCoin[i].item1, - ), - ), - ], - expandingChild: CoinWalletsTable( - coin: providersByCoin[i].item1, + ) + ], ), - ); - }, + ), + Expanded( + flex: 4, + child: Text( + providers.length == 1 + ? "${providers.length} wallet" + : "${providers.length} wallets", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ), + Expanded( + flex: 6, + child: TablePriceInfo( + coin: providersByCoin[index].item1, + ), + ), + ], ), - ], + ); + }, + separatorBuilder: (_, __) => const SizedBox( + height: 10, + ), + itemCount: providersByCoin.length, ); } } +// +// class WalletSummaryTable extends ConsumerStatefulWidget { +// const WalletSummaryTable({Key? key}) : super(key: key); +// +// @override +// ConsumerState createState() => _WalletTableState(); +// } +// +// class _WalletTableState extends ConsumerState { +// @override +// Widget build(BuildContext context) { +// debugPrint("BUILD: $runtimeType"); +// final providersByCoin = ref.watch( +// walletsChangeNotifierProvider.select( +// (value) => value.getManagerProvidersByCoin(), +// ), +// ); +// +// return TableView( +// rows: [ +// for (int i = 0; i < providersByCoin.length; i++) +// Builder( +// key: Key("${providersByCoin[i].item1.name}_${runtimeType}_key"), +// builder: (context) { +// final providers = providersByCoin[i].item2; +// +// VoidCallback? expandOverride; +// if (providers.length == 1) { +// expandOverride = () async { +// final manager = ref.read(providers.first); +// if (manager.coin == Coin.monero || +// manager.coin == Coin.wownero) { +// await manager.initializeExisting(); +// } +// await Navigator.of(context).pushNamed( +// DesktopWalletView.routeName, +// arguments: manager.walletId, +// ); +// }; +// } +// +// return TableViewRow( +// expandOverride: expandOverride, +// padding: const EdgeInsets.symmetric( +// horizontal: 20, +// vertical: 16, +// ), +// decoration: BoxDecoration( +// color: Theme.of(context).extension()!.popupBG, +// borderRadius: BorderRadius.circular( +// Constants.size.circularBorderRadius, +// ), +// ), +// cells: [ +// TableViewCell( +// flex: 4, +// child: Row( +// children: [ +// SvgPicture.asset( +// Assets.svg.iconFor(coin: providersByCoin[i].item1), +// width: 28, +// height: 28, +// ), +// const SizedBox( +// width: 10, +// ), +// Text( +// providersByCoin[i].item1.prettyName, +// style: STextStyles.desktopTextExtraSmall(context) +// .copyWith( +// color: Theme.of(context) +// .extension()! +// .textDark, +// ), +// ) +// ], +// ), +// ), +// TableViewCell( +// flex: 4, +// child: Text( +// providers.length == 1 +// ? "${providers.length} wallet" +// : "${providers.length} wallets", +// style: +// STextStyles.desktopTextExtraSmall(context).copyWith( +// color: Theme.of(context) +// .extension()! +// .textSubtitle1, +// ), +// ), +// ), +// TableViewCell( +// flex: 6, +// child: TablePriceInfo( +// coin: providersByCoin[i].item1, +// ), +// ), +// ], +// expandingChild: CoinWalletsTable( +// coin: providersByCoin[i].item1, +// ), +// ); +// }, +// ), +// ], +// ); +// } +// } class TablePriceInfo extends ConsumerWidget { const TablePriceInfo({Key? key, required this.coin}) : super(key: key); diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index fa134aa15..1747fa545 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -13,6 +14,8 @@ import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; import 'package:tuple/tuple.dart'; @@ -23,83 +26,108 @@ class WalletSheetCard extends ConsumerWidget { required this.walletId, this.contractAddress, this.popPrevious = false, + this.desktopNavigatorState, }) : super(key: key); final String walletId; final String? contractAddress; final bool popPrevious; + final NavigatorState? desktopNavigatorState; + + void _openWallet(BuildContext context, WidgetRef ref) async { + final nav = Navigator.of(context); + + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { + await manager.initializeExisting(); + } + if (context.mounted) { + if (popPrevious) nav.pop(); + + if (desktopNavigatorState != null) { + unawaited( + desktopNavigatorState!.pushNamed( + DesktopWalletView.routeName, + arguments: walletId, + ), + ); + } else { + unawaited( + nav.pushNamed( + WalletView.routeName, + arguments: Tuple2( + walletId, + ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(walletId), + ), + ), + ); + } + + if (contractAddress != null) { + final contract = + ref.read(mainDBProvider).getEthContractSync(contractAddress!)!; + ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( + token: contract, + secureStore: ref.read(secureStoreProvider), + ethWallet: manager.wallet as EthereumWallet, + tracker: TransactionNotificationTracker( + walletId: walletId, + ), + ); + + await showLoading( + whileFuture: ref.read(tokenServiceProvider)!.initialize(), + context: context, + opaqueBG: true, + message: "Loading ${contract.name}", + ); + + // pop loading + nav.pop(); + + if (desktopNavigatorState != null) { + // await desktopNavigatorState !.pushNamed( + // DesktopTokenView.routeName, + // arguments: walletId, + // ); + } else { + await nav.pushNamed( + TokenView.routeName, + arguments: walletId, + ); + } + } + } + } @override Widget build(BuildContext context, WidgetRef ref) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: MaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - key: Key("walletsSheetItemButtonKey_$walletId"), - padding: const EdgeInsets.all(5), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: MaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + key: Key("walletsSheetItemButtonKey_$walletId"), + padding: const EdgeInsets.all(5), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), + onPressed: () => _openWallet(context, ref), + child: child, ), - onPressed: () async { - final nav = Navigator.of(context); - - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); - if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { - await manager.initializeExisting(); - } - if (context.mounted) { - if (popPrevious) nav.pop(); - unawaited( - nav.pushNamed( - WalletView.routeName, - arguments: Tuple2( - walletId, - ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(walletId)), - ), - ); - - if (contractAddress != null) { - final contract = ref - .read(mainDBProvider) - .getEthContractSync(contractAddress!)!; - ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( - token: contract, - secureStore: ref.read(secureStoreProvider), - ethWallet: manager.wallet as EthereumWallet, - tracker: TransactionNotificationTracker( - walletId: walletId, - ), - ); - - await showLoading( - whileFuture: ref.read(tokenServiceProvider)!.initialize(), - context: context, - opaqueBG: true, - message: "Loading ${contract.name}", - ); - - // pop loading - nav.pop(); - - // if (context.mounted) { - await nav.pushNamed( - TokenView.routeName, - arguments: walletId, - ); - // } - } - } - }, - child: WalletInfoRow( - walletId: walletId, - contractAddress: contractAddress, - ), + ), + child: WalletInfoRow( + walletId: walletId, + contractAddress: contractAddress, + onPressedDesktop: + Util.isDesktop ? () => _openWallet(context, ref) : null, ), ); } diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index 3863c08d3..26879aa7e 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart'; import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; @@ -39,64 +38,76 @@ class WalletInfoRow extends ConsumerWidget { } if (Util.isDesktop) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onPressedDesktop, - child: Padding( - padding: padding, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - Expanded( - flex: 4, - child: Row( - children: [ - WalletInfoCoinIcon( - coin: manager.coin, - contractAddress: contractAddress, - ), - const SizedBox( - width: 12, - ), - Text( - manager.walletName, - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + return Padding( + padding: padding, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Expanded( + flex: 3, + child: Row( + children: [ + WalletInfoCoinIcon( + coin: manager.coin, + contractAddress: contractAddress, + ), + const SizedBox( + width: 12, + ), + contract != null + ? Row( + children: [ + Text( + contract.name, + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + const SizedBox( + width: 4, + ), + CoinTickerTag( + walletId: walletId, + ), + ], + ) + : Text( + manager.walletName, + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), ), - ), - ], - ), - ), - Expanded( - flex: 4, - child: WalletInfoRowBalance( - walletId: walletId, - ), - ), - Expanded( - flex: 6, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SvgPicture.asset( - Assets.svg.chevronRight, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .textSubtitle1, - ) - ], - ), - ) - ], + ], + ), ), - ), + Expanded( + flex: 4, + child: WalletInfoRowBalance( + walletId: walletId, + contractAddress: contractAddress, + ), + ), + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + CustomTextButton( + text: "Open wallet", + onTap: onPressedDesktop, + ), + ], + ), + ) + ], ), ), ); From 6e5cffa4d4749a086d931732e0179a41ce41275d Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 16:37:20 -0600 Subject: [PATCH 182/208] desktop show more button fix --- .../sub_widgets/desktop_wallet_features.dart | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 5b1c3edce..da5fd5745 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -355,19 +355,21 @@ class _DesktopWalletFeaturesState extends ConsumerState { const SizedBox( width: 16, ), - SecondaryButton( - label: "More", - width: buttonWidth, - buttonHeight: ButtonHeight.l, - icon: SvgPicture.asset( - Assets.svg.bars, - height: 20, - width: 20, - color: - Theme.of(context).extension()!.buttonTextSecondary, + if (showMore) + SecondaryButton( + label: "More", + width: buttonWidth, + buttonHeight: ButtonHeight.l, + icon: SvgPicture.asset( + Assets.svg.bars, + height: 20, + width: 20, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () => _onMorePressed(), ), - onPressed: () => _onMorePressed(), - ), ], ); } From a9306e52671059800564a918b5b65554a2da960c Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 16:38:48 -0600 Subject: [PATCH 183/208] use EthTokenIcon class --- lib/pages/token_view/token_view.dart | 15 ++++++++------- lib/utilities/assets.dart | 7 ------- lib/widgets/icon_widgets/eth_token_icon.dart | 10 ++++++---- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 0c8a31a5d..851119f06 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -15,6 +15,7 @@ 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/icon_widgets/eth_token_icon.dart'; import 'package:tuple/tuple.dart'; final tokenServiceStateProvider = StateProvider((ref) => null); @@ -76,13 +77,13 @@ class _TokenViewState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - SvgPicture.asset( - Assets.svg.iconForToken( - contractAddress: ref.watch( - tokenServiceProvider.select( - (value) => value!.tokenContract.address))), - width: 24, - height: 24, + EthTokenIcon( + contractAddress: ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.address, + ), + ), + size: 24, ), const SizedBox( width: 10, diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index dfbb19d33..c1401b8cd 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -344,13 +344,6 @@ class _SVG { } } - String iconForToken({required String contractAddress}) { - switch (contractAddress) { - default: - return ethereum; - } - } - // big icons String bitcoinImage(BuildContext context) => "${_path(context)}/bitcoin.svg"; String bitcoincashImage(BuildContext context) => diff --git a/lib/widgets/icon_widgets/eth_token_icon.dart b/lib/widgets/icon_widgets/eth_token_icon.dart index 2b94d1554..0b47d526d 100644 --- a/lib/widgets/icon_widgets/eth_token_icon.dart +++ b/lib/widgets/icon_widgets/eth_token_icon.dart @@ -10,9 +10,11 @@ class EthTokenIcon extends StatefulWidget { const EthTokenIcon({ Key? key, required this.contractAddress, + this.size = 22, }) : super(key: key); final String contractAddress; + final double size; @override State createState() => _EthTokenIconState(); @@ -37,14 +39,14 @@ class _EthTokenIconState extends State { if (imageUrl == null || imageUrl!.isEmpty) { return SvgPicture.asset( Assets.svg.iconFor(coin: Coin.ethereum), - width: 22, - height: 22, + width: widget.size, + height: widget.size, ); } else { return SvgPicture.network( imageUrl!, - width: 22, - height: 22, + width: widget.size, + height: widget.size, ); } } From f10db8306a4992755382bead48a760b6f2916718 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 17:00:08 -0600 Subject: [PATCH 184/208] add content padding property to SecondaryButton --- lib/widgets/desktop/secondary_button.dart | 65 ++++++++++++----------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index 5d1ba1970..145a6446c 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -18,6 +18,7 @@ class SecondaryButton extends StatelessWidget { this.enabled = true, this.buttonHeight, this.iconSpacing = 10, + this.padding = EdgeInsets.zero, }) : super(key: key); final double? width; @@ -29,6 +30,7 @@ class SecondaryButton extends StatelessWidget { final Widget? trailingIcon; final ButtonHeight? buttonHeight; final double iconSpacing; + final EdgeInsets padding; TextStyle getStyle(bool isDesktop, BuildContext context) { if (isDesktop) { @@ -155,37 +157,40 @@ class SecondaryButton extends StatelessWidget { : Theme.of(context) .extension()! .getSecondaryDisabledButtonStyle(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (icon != null) icon!, - if (icon != null && label != null) - SizedBox( - width: iconSpacing, - ), - if (label != null) - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - label!, - style: getStyle(isDesktop, context), - ), - if (buttonHeight != null && buttonHeight == ButtonHeight.s) - const SizedBox( - height: 2, + child: Padding( + padding: padding, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (icon != null) icon!, + if (icon != null && label != null) + SizedBox( + width: iconSpacing, + ), + if (label != null) + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + label!, + style: getStyle(isDesktop, context), ), - ], - ), - if (trailingIcon != null) - SizedBox( - width: iconSpacing, - ), - if (trailingIcon != null) trailingIcon!, - ], + if (buttonHeight != null && buttonHeight == ButtonHeight.s) + const SizedBox( + height: 2, + ), + ], + ), + if (trailingIcon != null) + SizedBox( + width: iconSpacing, + ), + if (trailingIcon != null) trailingIcon!, + ], + ), ), ), ); From 728909399c9a643006cfc21d046900c813b2e3ae Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 17:05:48 -0600 Subject: [PATCH 185/208] WIP desktop token view --- .../wallet_view/desktop_token_view.dart | 228 ++++++++++++++++++ .../sub_widgets/desktop_wallet_summary.dart | 30 ++- lib/route_generator.dart | 15 ++ lib/widgets/wallet_card.dart | 9 +- 4 files changed, 272 insertions(+), 10 deletions(-) create mode 100644 lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart new file mode 100644 index 000000000..70f319cf1 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart @@ -0,0 +1,228 @@ +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/pages/token_view/sub_widgets/token_summary.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart'; +import 'package:stackwallet/providers/providers.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/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +/// [eventBus] should only be set during testing +class DesktopTokenView extends ConsumerStatefulWidget { + const DesktopTokenView({ + Key? key, + required this.walletId, + this.eventBus, + }) : super(key: key); + + static const String routeName = "/desktopTokenView"; + + final String walletId; + final EventBus? eventBus; + + @override + ConsumerState createState() => _DesktopTokenViewState(); +} + +class _DesktopTokenViewState extends ConsumerState { + late final WalletSyncStatus initialSyncStatus; + + @override + void initState() { + initialSyncStatus = ref.read(tokenServiceProvider)!.isRefreshing + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced; + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + flex: 3, + child: Row( + children: [ + const SizedBox( + width: 32, + ), + SecondaryButton( + padding: const EdgeInsets.only( + left: 12, + right: 18, + ), + buttonHeight: ButtonHeight.s, + label: ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).walletName, + ), + ), + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox( + width: 15, + ), + ], + ), + ), + center: Expanded( + flex: 4, + child: Row( + children: [ + EthTokenIcon( + contractAddress: ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.address, + ), + ), + size: 32, + ), + const SizedBox( + width: 12, + ), + Text( + ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.name, + ), + ), + style: STextStyles.desktopH3(context), + ), + const SizedBox( + width: 12, + ), + CoinTickerTag( + walletId: widget.walletId, + ), + ], + ), + ), + useSpacers: false, + isCompactHeight: true, + ), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + EthTokenIcon( + contractAddress: ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.address, + ), + ), + size: 40, + ), + const SizedBox( + width: 10, + ), + DesktopWalletSummary( + walletId: widget.walletId, + isToken: true, + initialSyncStatus: ref.watch( + walletsChangeNotifierProvider.select((value) => + value.getManager(widget.walletId).isRefreshing)) + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + const Spacer(), + DesktopWalletFeatures( + walletId: widget.walletId, + ), + ], + ), + ), + const SizedBox( + height: 24, + ), + Expanded( + child: Row( + children: [ + SizedBox( + width: 450, + child: MyWallet( + walletId: widget.walletId, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recent transactions", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + CustomTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + ), + const SizedBox( + height: 16, + ), + Expanded( + child: TokenTransactionsList( + walletId: widget.walletId, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index 4d30ebc60..75937a7d8 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -2,6 +2,7 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -19,10 +20,12 @@ class DesktopWalletSummary extends ConsumerStatefulWidget { Key? key, required this.walletId, required this.initialSyncStatus, + this.isToken = false, }) : super(key: key); final String walletId; final WalletSyncStatus initialSyncStatus; + final bool isToken; @override ConsumerState createState() => @@ -58,15 +61,30 @@ class _WDesktopWalletSummaryState extends ConsumerState { final baseCurrency = ref .watch(prefsChangeNotifierProvider.select((value) => value.currency)); - final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin))); + final priceTuple = widget.isToken + ? ref.watch(priceAnd24hChangeNotifierProvider.select((value) => + value.getTokenPrice(ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.address))))) + : ref.watch(priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); final _showAvailable = ref.watch(walletBalanceToggleStateProvider.state).state == WalletBalanceToggleState.available; - Balance balance = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId).balance)); + final unit = widget.isToken + ? ref.watch( + tokenServiceProvider.select((value) => value!.tokenContract.symbol)) + : coin.ticker; + final decimalPlaces = widget.isToken + ? ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.decimals)) + : coin.decimals; + + Balance balance = widget.isToken + ? ref.watch(tokenServiceProvider.select((value) => value!.balance)) + : ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).balance)); Decimal balanceToShow; if (coin == Coin.firo || coin == Coin.firoTestNet) { @@ -112,8 +130,8 @@ class _WDesktopWalletSummaryState extends ConsumerState { "${Format.localizedStringAsFixed( value: balanceToShow, locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", + decimalPlaces: decimalPlaces, + )} $unit", style: STextStyles.desktopH3(context), ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 4decebcdf..9ef8bc2aa 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -117,6 +117,7 @@ import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_ import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; @@ -230,6 +231,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopTokenView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => DesktopTokenView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SelectWalletForTokenView.routeName: if (args is EthTokenEntity) { return getRoute( diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 1747fa545..9e4fbc034 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; @@ -89,10 +90,10 @@ class WalletSheetCard extends ConsumerWidget { nav.pop(); if (desktopNavigatorState != null) { - // await desktopNavigatorState !.pushNamed( - // DesktopTokenView.routeName, - // arguments: walletId, - // ); + await desktopNavigatorState!.pushNamed( + DesktopTokenView.routeName, + arguments: walletId, + ); } else { await nav.pushNamed( TokenView.routeName, From 991ade962cd085aeccede8c9f0b8665641b64418 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 20:19:18 -0600 Subject: [PATCH 186/208] add hover color property to custom containers --- lib/widgets/rounded_container.dart | 3 +++ lib/widgets/rounded_white_container.dart | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/widgets/rounded_container.dart b/lib/widgets/rounded_container.dart index 5b64c7d50..c30f7a186 100644 --- a/lib/widgets/rounded_container.dart +++ b/lib/widgets/rounded_container.dart @@ -12,6 +12,7 @@ class RoundedContainer extends StatelessWidget { this.width, this.height, this.borderColor, + this.hoverColor, this.boxShadow, this.onPressed, }) : super(key: key); @@ -23,6 +24,7 @@ class RoundedContainer extends StatelessWidget { final double? width; final double? height; final Color? borderColor; + final Color? hoverColor; final List? boxShadow; final VoidCallback? onPressed; @@ -32,6 +34,7 @@ class RoundedContainer extends StatelessWidget { condition: onPressed != null, builder: (child) => RawMaterialButton( fillColor: color, + hoverColor: hoverColor, elevation: 0, highlightElevation: 0, disabledElevation: 0, diff --git a/lib/widgets/rounded_white_container.dart b/lib/widgets/rounded_white_container.dart index 8208d69ca..a72d7af5a 100644 --- a/lib/widgets/rounded_white_container.dart +++ b/lib/widgets/rounded_white_container.dart @@ -11,6 +11,7 @@ class RoundedWhiteContainer extends StatelessWidget { this.width, this.height, this.borderColor, + this.hoverColor, this.boxShadow, this.onPressed, }) : super(key: key); @@ -21,6 +22,7 @@ class RoundedWhiteContainer extends StatelessWidget { final double? width; final double? height; final Color? borderColor; + final Color? hoverColor; final List? boxShadow; final VoidCallback? onPressed; @@ -34,6 +36,7 @@ class RoundedWhiteContainer extends StatelessWidget { height: height, borderColor: borderColor, boxShadow: boxShadow, + hoverColor: hoverColor, onPressed: onPressed, child: child, ); From 96aa10653bfdedf5371a9009d93cd9f499376ee8 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 20:19:41 -0600 Subject: [PATCH 187/208] desktop main view wallets overview table list tweaks --- .../my_stack_view/my_wallets.dart | 60 ++-- .../my_stack_view/wallet_summary_table.dart | 263 +++++++----------- 2 files changed, 142 insertions(+), 181 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/my_wallets.dart b/lib/pages_desktop_specific/my_stack_view/my_wallets.dart index 1dd345f74..c2a08a35a 100644 --- a/lib/pages_desktop_specific/my_stack_view/my_wallets.dart +++ b/lib/pages_desktop_specific/my_stack_view/my_wallets.dart @@ -22,32 +22,48 @@ class _MyWalletsState extends ConsumerState { .select((value) => value.showFavoriteWallets)); return Padding( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.only( + top: 24, + left: 14, + right: 14, + bottom: 0, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showFavorites) const DesktopFavoriteWallets(), - Row( - children: [ - Text( - "All wallets", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + if (showFavorites) + const Padding( + padding: EdgeInsets.symmetric( + horizontal: 10, + ), + child: DesktopFavoriteWallets(), + ), + Padding( + padding: const EdgeInsets.all( + 10, + ), + child: Row( + children: [ + Text( + "All wallets", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), ), - ), - const Spacer(), - CustomTextButton( - text: "Add new wallet", - onTap: () { - Navigator.of( - context, - rootNavigator: true, - ).pushNamed(AddWalletView.routeName); - }, - ), - ], + const Spacer(), + CustomTextButton( + text: "Add new wallet", + onTap: () { + Navigator.of( + context, + rootNavigator: true, + ).pushNamed(AddWalletView.routeName); + }, + ), + ], + ), ), const SizedBox( height: 20, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index 35f3126c1..9376654c7 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.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/conditional_parent.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -34,47 +35,104 @@ class _WalletTableState extends ConsumerState { final providers = providersByCoin[index].item2; final coin = providersByCoin[index].item1; - return RoundedWhiteContainer( - padding: const EdgeInsets.all(20), - onPressed: () { - showDialog( - context: context, - builder: (_) => DesktopDialog( - maxHeight: 600, - maxWidth: 700, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "${coin.prettyName} (${coin.ticker}) wallets", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopCoinWalletsDialog( - coin: coin, - navigatorState: Navigator.of(context), - ), - ), - ), - ], + return ConditionalParent( + condition: index + 1 == providersByCoin.length, + builder: (child) => const Padding( + padding: EdgeInsets.only( + bottom: 16, + ), + ), + child: DesktopWalletSummaryRow( + key: Key("DesktopWalletSummaryRow_key_${coin.name}"), + coin: coin, + walletCount: providers.length, + ), + ); + }, + separatorBuilder: (_, __) => const SizedBox( + height: 10, + ), + itemCount: providersByCoin.length, + ); + } +} + +class DesktopWalletSummaryRow extends StatefulWidget { + const DesktopWalletSummaryRow({ + Key? key, + required this.coin, + required this.walletCount, + }) : super(key: key); + + final Coin coin; + final int walletCount; + + @override + State createState() => + _DesktopWalletSummaryRowState(); +} + +class _DesktopWalletSummaryRowState extends State { + bool _hovering = false; + + void _onPressed() { + showDialog( + context: context, + builder: (_) => DesktopDialog( + maxHeight: 600, + maxWidth: 700, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "${widget.coin.prettyName} (${widget.coin.ticker}) wallets", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopCoinWalletsDialog( + coin: widget.coin, + navigatorState: Navigator.of(context), ), ), - ); - }, + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState( + () => _hovering = true, + ), + onExit: (_) => setState( + () => _hovering = false, + ), + child: AnimatedScale( + scale: _hovering ? 1.00 : 0.98, + duration: const Duration( + milliseconds: 200, + ), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(20), + hoverColor: Colors.transparent, + onPressed: _onPressed, child: Row( children: [ Expanded( @@ -82,7 +140,7 @@ class _WalletTableState extends ConsumerState { child: Row( children: [ SvgPicture.asset( - Assets.svg.iconFor(coin: providersByCoin[index].item1), + Assets.svg.iconFor(coin: widget.coin), width: 28, height: 28, ), @@ -90,7 +148,7 @@ class _WalletTableState extends ConsumerState { width: 10, ), Text( - providersByCoin[index].item1.prettyName, + widget.coin.prettyName, style: STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context) @@ -104,9 +162,9 @@ class _WalletTableState extends ConsumerState { Expanded( flex: 4, child: Text( - providers.length == 1 - ? "${providers.length} wallet" - : "${providers.length} wallets", + widget.walletCount == 1 + ? "${widget.walletCount} wallet" + : "${widget.walletCount} wallets", style: STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context) .extension()! @@ -117,129 +175,16 @@ class _WalletTableState extends ConsumerState { Expanded( flex: 6, child: TablePriceInfo( - coin: providersByCoin[index].item1, + coin: widget.coin, ), ), ], ), - ); - }, - separatorBuilder: (_, __) => const SizedBox( - height: 10, + ), ), - itemCount: providersByCoin.length, ); } } -// -// class WalletSummaryTable extends ConsumerStatefulWidget { -// const WalletSummaryTable({Key? key}) : super(key: key); -// -// @override -// ConsumerState createState() => _WalletTableState(); -// } -// -// class _WalletTableState extends ConsumerState { -// @override -// Widget build(BuildContext context) { -// debugPrint("BUILD: $runtimeType"); -// final providersByCoin = ref.watch( -// walletsChangeNotifierProvider.select( -// (value) => value.getManagerProvidersByCoin(), -// ), -// ); -// -// return TableView( -// rows: [ -// for (int i = 0; i < providersByCoin.length; i++) -// Builder( -// key: Key("${providersByCoin[i].item1.name}_${runtimeType}_key"), -// builder: (context) { -// final providers = providersByCoin[i].item2; -// -// VoidCallback? expandOverride; -// if (providers.length == 1) { -// expandOverride = () async { -// final manager = ref.read(providers.first); -// if (manager.coin == Coin.monero || -// manager.coin == Coin.wownero) { -// await manager.initializeExisting(); -// } -// await Navigator.of(context).pushNamed( -// DesktopWalletView.routeName, -// arguments: manager.walletId, -// ); -// }; -// } -// -// return TableViewRow( -// expandOverride: expandOverride, -// padding: const EdgeInsets.symmetric( -// horizontal: 20, -// vertical: 16, -// ), -// decoration: BoxDecoration( -// color: Theme.of(context).extension()!.popupBG, -// borderRadius: BorderRadius.circular( -// Constants.size.circularBorderRadius, -// ), -// ), -// cells: [ -// TableViewCell( -// flex: 4, -// child: Row( -// children: [ -// SvgPicture.asset( -// Assets.svg.iconFor(coin: providersByCoin[i].item1), -// width: 28, -// height: 28, -// ), -// const SizedBox( -// width: 10, -// ), -// Text( -// providersByCoin[i].item1.prettyName, -// style: STextStyles.desktopTextExtraSmall(context) -// .copyWith( -// color: Theme.of(context) -// .extension()! -// .textDark, -// ), -// ) -// ], -// ), -// ), -// TableViewCell( -// flex: 4, -// child: Text( -// providers.length == 1 -// ? "${providers.length} wallet" -// : "${providers.length} wallets", -// style: -// STextStyles.desktopTextExtraSmall(context).copyWith( -// color: Theme.of(context) -// .extension()! -// .textSubtitle1, -// ), -// ), -// ), -// TableViewCell( -// flex: 6, -// child: TablePriceInfo( -// coin: providersByCoin[i].item1, -// ), -// ), -// ], -// expandingChild: CoinWalletsTable( -// coin: providersByCoin[i].item1, -// ), -// ); -// }, -// ), -// ], -// ); -// } -// } class TablePriceInfo extends ConsumerWidget { const TablePriceInfo({Key? key, required this.coin}) : super(key: key); From e01cef5df7380c22a1b9fe8ececadd53f7020d79 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 20:27:46 -0600 Subject: [PATCH 188/208] desktop token receive view update --- .../wallet_view/desktop_token_view.dart | 5 +++++ .../wallet_view/sub_widgets/desktop_receive.dart | 14 ++++++++++---- .../wallet_view/sub_widgets/my_wallet.dart | 3 +++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart index 70f319cf1..d268474d3 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart @@ -175,6 +175,11 @@ class _DesktopTokenViewState extends ConsumerState { width: 450, child: MyWallet( walletId: widget.walletId, + contractAddress: ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.address, + ), + ), ), ), const SizedBox( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 064c31a08..5ac5bafce 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -7,13 +7,13 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -28,10 +28,12 @@ class DesktopReceive extends ConsumerStatefulWidget { const DesktopReceive({ Key? key, required this.walletId, + this.contractAddress, this.clipboard = const ClipboardWrapper(), }) : super(key: key); final String walletId; + final String? contractAddress; final ClipboardInterface clipboard; @override @@ -149,7 +151,11 @@ class _DesktopReceiveState extends ConsumerState { Row( children: [ Text( - "Your ${coin.ticker} address", + "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.symbol, + ), + )} address", style: STextStyles.itemSubtitle(context), ), const Spacer(), @@ -199,11 +205,11 @@ class _DesktopReceiveState extends ConsumerState { ), ), ), - if (coin != Coin.epicCash) + if (coin != Coin.epicCash && coin != Coin.ethereum) const SizedBox( height: 20, ), - if (coin != Coin.epicCash) + if (coin != Coin.epicCash && coin != Coin.ethereum) SecondaryButton( buttonHeight: ButtonHeight.l, onPressed: generateNewAddress, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 6813d42cc..25eb715e4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -10,9 +10,11 @@ class MyWallet extends StatefulWidget { const MyWallet({ Key? key, required this.walletId, + this.contractAddress, }) : super(key: key); final String walletId; + final String? contractAddress; @override State createState() => _MyWalletState(); @@ -76,6 +78,7 @@ class _MyWalletState extends State { padding: const EdgeInsets.all(20), child: DesktopReceive( walletId: widget.walletId, + contractAddress: widget.contractAddress, ), ), crossFadeState: _selectedIndex == 0 From 36c4221185ff3801a8639275f93d83abbc353a34 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 3 Apr 2023 21:11:10 -0600 Subject: [PATCH 189/208] WIP desktop eth token send view --- .../sub_widgets/desktop_token_send.dart | 1014 +++++++++++++++++ .../wallet_view/sub_widgets/my_wallet.dart | 11 +- 2 files changed, 1022 insertions(+), 3 deletions(-) create mode 100644 lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart new file mode 100644 index 000000000..215acc046 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -0,0 +1,1014 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; +import 'package:stackwallet/models/send_view_auto_fill_data.dart'; +import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; +import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class DesktopTokenSend extends ConsumerStatefulWidget { + const DesktopTokenSend({ + Key? key, + required this.walletId, + this.autoFillData, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + this.accountLite, + }) : super(key: key); + + final String walletId; + final SendViewAutoFillData? autoFillData; + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + final PaynymAccountLite? accountLite; + + @override + ConsumerState createState() => _DesktopTokenSendState(); +} + +class _DesktopTokenSendState extends ConsumerState { + late final String walletId; + late final Coin coin; + late final ClipboardInterface clipboard; + late final BarcodeScannerInterface scanner; + + late TextEditingController sendToController; + late TextEditingController cryptoAmountController; + late TextEditingController baseAmountController; + // late TextEditingController feeController; + + late final SendViewAutoFillData? _data; + + final _addressFocusNode = FocusNode(); + final _cryptoFocus = FocusNode(); + final _baseFocus = FocusNode(); + + String? _note; + + Decimal? _amountToSend; + Decimal? _cachedAmountToSend; + String? _address; + + bool _addressToggleFlag = false; + + bool _cryptoAmountChangeLock = false; + late VoidCallback onCryptoAmountChanged; + + Future previewSend() async { + final tokenWallet = ref.read(tokenServiceProvider)!; + + final amount = Amount.fromDecimal( + _amountToSend!, + fractionDigits: tokenWallet.tokenContract.decimals, + ); + + // todo use Amount class + final availableBalance = tokenWallet.balance.spendable; + + // confirm send all + if (amount.raw.toInt() == availableBalance) { + final bool? shouldSendAll = await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Confirm send all", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Text( + "You are about to send your entire balance. Would you like to continue?", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Yes", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + + if (shouldSendAll == null || shouldSendAll == false) { + // cancel preview + return; + } + } + + try { + bool wasCancelled = false; + + if (mounted) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: BuildingTransactionDialog( + coin: tokenWallet.coin, + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ), + ), + ); + }, + ), + ); + } + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + Map txData; + Future> txDataFuture; + + txDataFuture = tokenWallet.prepareSend( + address: _address!, + satoshiAmount: amount.raw.toInt(), + args: { + "feeRate": ref.read(feeRateTypeStateProvider), + }, + ); + + final results = await Future.wait([ + txDataFuture, + time, + ]); + + txData = results.first as Map; + + if (!wasCancelled && mounted) { + txData["address"] = _address; + txData["note"] = _note ?? ""; + + // pop building dialog + Navigator.of( + context, + rootNavigator: true, + ).pop(); + + unawaited( + showDialog( + context: context, + builder: (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + isTokenTx: true, + routeOnSuccessName: DesktopHomeView.routeName, + ), + ), + ), + ); + } + } catch (e) { + if (mounted) { + // pop building dialog + Navigator.of( + context, + rootNavigator: true, + ).pop(); + + unawaited( + showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction failed", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Text( + e.toString(), + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + ), + const SizedBox( + height: 40, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ), + const SizedBox( + width: 32, + ), + ], + ), + ], + ), + ), + ); + }, + ), + ); + } + } + } + + void _cryptoAmountChanged() async { + if (!_cryptoAmountChangeLock) { + final String cryptoAmount = cryptoAmountController.text; + if (cryptoAmount.isNotEmpty && + cryptoAmount != "." && + cryptoAmount != ",") { + _amountToSend = cryptoAmount.contains(",") + ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) + : Decimal.parse(cryptoAmount); + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + _cachedAmountToSend = _amountToSend; + + final price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + + if (price > Decimal.zero) { + final String fiatAmountString = Format.localizedStringAsFixed( + value: _amountToSend! * price, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: 2, + ); + + baseAmountController.text = fiatAmountString; + } + } else { + _amountToSend = null; + _cachedAmountToSend = null; + baseAmountController.text = ""; + } + + _updatePreviewButtonState(_address, _amountToSend); + } + } + + String? _updateInvalidAddressText(String address, Manager manager) { + if (_data != null && _data!.contactLabel == address) { + return null; + } + if (address.isNotEmpty && !manager.validateAddress(address)) { + return "Invalid address"; + } + return null; + } + + void _updatePreviewButtonState(String? address, Decimal? amount) { + final isValidAddress = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(address ?? ""); + ref.read(previewTxButtonStateProvider.state).state = + (isValidAddress && amount != null && amount > Decimal.zero); + } + + Future scanQr() async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + Logging.instance.log("qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); + + if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { + // auto fill address + _address = results["address"] ?? ""; + sendToController.text = _address!; + + // autofill notes field + if (results["message"] != null) { + _note = results["message"]!; + } else if (results["label"] != null) { + _note = results["label"]!; + } + + // autofill amount field + if (results["amount"] != null) { + final amount = Decimal.parse(results["amount"]!); + cryptoAmountController.text = Format.localizedStringAsFixed( + value: amount, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlacesForCoin(coin), + ); + amount.toString(); + _amountToSend = amount; + } + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(qrResult.rawContent)) { + _address = qrResult.rawContent; + sendToController.text = _address ?? ""; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); + } + } + + Future pasteAddress() async { + final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring(0, content.indexOf("\n")); + } + + sendToController.text = content; + _address = content; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } + + void fiatTextFieldOnChanged(String baseAmountString) { + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = baseAmountString.contains(",") + ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) + : Decimal.parse(baseAmountString); + + var _price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + + if (_price == Decimal.zero) { + _amountToSend = Decimal.zero; + } else { + _amountToSend = baseAmount <= Decimal.zero + ? Decimal.zero + : (baseAmount / _price).toDecimal( + scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); + } + if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final amountString = Format.localizedStringAsFixed( + value: _amountToSend!, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: Constants.decimalPlacesForCoin(coin), + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Decimal.zero; + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + + _updatePreviewButtonState(_address, _amountToSend); + } + + Future sendAllTapped() async { + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .wallet as FiroWallet; + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + cryptoAmountController.text = (firoWallet.availablePrivateBalance()) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + } else { + cryptoAmountController.text = (firoWallet.availablePublicBalance()) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + } + } else { + cryptoAmountController.text = (ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .balance + .getSpendable()) + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + } + } + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.refresh(feeSheetSessionCacheProvider); + ref.read(previewTxButtonStateProvider.state).state = false; + }); + + // _calculateFeesFuture = calculateFees(0); + _data = widget.autoFillData; + walletId = widget.walletId; + coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; + clipboard = widget.clipboard; + scanner = widget.barcodeScanner; + + sendToController = TextEditingController(); + cryptoAmountController = TextEditingController(); + baseAmountController = TextEditingController(); + // feeController = TextEditingController(); + + onCryptoAmountChanged = _cryptoAmountChanged; + cryptoAmountController.addListener(onCryptoAmountChanged); + + if (_data != null) { + if (_data!.amount != null) { + cryptoAmountController.text = _data!.amount!.toString(); + } + sendToController.text = _data!.contactLabel; + _address = _data!.address; + _addressToggleFlag = true; + } + + _cryptoFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + ref.refresh(sendAmountProvider); + } else { + ref.read(sendAmountProvider.state).state = _amountToSend!; + } + } + }); + + _baseFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + ref.refresh(sendAmountProvider); + } else { + ref.read(sendAmountProvider.state).state = _amountToSend!; + } + } + }); + + super.initState(); + } + + @override + void dispose() { + cryptoAmountController.removeListener(onCryptoAmountChanged); + + sendToController.dispose(); + cryptoAmountController.dispose(); + baseAmountController.dispose(); + // feeController.dispose(); + + _addressFocusNode.dispose(); + _cryptoFocus.dispose(); + _baseFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final tokenContract = ref.watch(tokenServiceProvider)!.tokenContract; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + if (coin == Coin.firo) + Text( + "Send from", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: "Send all ${tokenContract.symbol}", + onTap: sendAllTapped, + ), + ], + ), + const SizedBox( + height: 10, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + key: const Key("amountInputFieldCryptoTextFieldKey"), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,18})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: (newValue) {}, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 22, + right: 12, + bottom: 22, + ), + hintText: "0", + hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldDefaultText, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + tokenContract.symbol, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + if (Prefs.instance.externalCalls) + const SizedBox( + height: 10, + ), + if (Prefs.instance.externalCalls) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + key: const Key("amountInputFieldFiatTextFieldKey"), + controller: baseAmountController, + focusNode: _baseFocus, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a fiat amount with 2 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: fiatTextFieldOnChanged, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 22, + right: 12, + bottom: 22, + ), + hintText: "0", + hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldDefaultText, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)), + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + Text( + "Send to", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + key: const Key("sendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _address = newValue; + _updatePreviewButtonState(_address, _amountToSend); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Enter ${tokenContract.symbol} address", + _addressFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "sendTokenViewClearAddressFieldButtonKey"), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendTokenViewPasteAddressFieldButtonKey"), + onTap: pasteAddress, + child: sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendTokenViewAddressBookButtonKey"), + onTap: () async { + final entry = + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: + STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], + ), + ), + ); + + if (entry != null) { + sendToController.text = + entry.other ?? entry.label; + + _address = entry.address; + + _updatePreviewButtonState( + _address, + _amountToSend, + ); + + setState(() { + _addressToggleFlag = true; + }); + } + }, + child: const AddressBookIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ref.read(walletsChangeNotifierProvider).getManager(walletId), + ); + + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: STextStyles.label(context).copyWith( + color: + Theme.of(context).extension()!.textError, + ), + ), + ), + ); + } + }, + ), + const SizedBox( + height: 20, + ), + Text( + "Transaction fee (estimated)", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + // TODO mod this for token fees + DesktopFeeDropDown( + walletId: walletId, + ), + const SizedBox( + height: 36, + ), + PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Preview send", + enabled: ref.watch(previewTxButtonStateProvider.state).state, + onPressed: ref.watch(previewTxButtonStateProvider.state).state + ? previewSend + : null, + ) + ], + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 25eb715e4..53192f63e 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -69,9 +70,13 @@ class _MyWalletState extends State { firstChild: Padding( key: const Key("desktopSendViewPortKey"), padding: const EdgeInsets.all(20), - child: DesktopSend( - walletId: widget.walletId, - ), + child: widget.contractAddress == null + ? DesktopSend( + walletId: widget.walletId, + ) + : DesktopTokenSend( + walletId: widget.walletId, + ), ), secondChild: Padding( key: const Key("desktopReceiveViewPortKey"), From 57ac4d47473ff33657f7add96243288ac166b1f2 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 4 Apr 2023 09:12:21 -0600 Subject: [PATCH 190/208] desktop eth token send view fixes --- .../sub_widgets/desktop_token_send.dart | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index 215acc046..ac08c3207 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -17,8 +17,6 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; -import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; -import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/amount.dart'; @@ -43,6 +41,8 @@ import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; +const _kCryptoAmountRegex = r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$'; + class DesktopTokenSend extends ConsumerStatefulWidget { const DesktopTokenSend({ Key? key, @@ -543,27 +543,9 @@ class _DesktopTokenSendState extends ConsumerState { } Future sendAllTapped() async { - if (coin == Coin.firo || coin == Coin.firoTestNet) { - final firoWallet = ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .wallet as FiroWallet; - if (ref.read(publicPrivateBalanceStateProvider.state).state == - "Private") { - cryptoAmountController.text = (firoWallet.availablePrivateBalance()) + cryptoAmountController.text = + (ref.read(tokenServiceProvider)!.balance.getSpendable()) .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - } else { - cryptoAmountController.text = (firoWallet.availablePublicBalance()) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - } - } else { - cryptoAmountController.text = (ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .balance - .getSpendable()) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - } } @override @@ -696,9 +678,12 @@ class _DesktopTokenSendState extends ConsumerState { textAlign: TextAlign.right, inputFormatters: [ // regex to validate a crypto amount with 8 decimal places - TextInputFormatter.withFunction((oldValue, newValue) => - RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,18})$') - .hasMatch(newValue.text) + TextInputFormatter.withFunction((oldValue, newValue) => RegExp( + _kCryptoAmountRegex.replaceAll( + "0,8", + "0,${tokenContract.decimals}", + ), + ).hasMatch(newValue.text) ? newValue : oldValue), ], From 2936c1b8079cbb20294352db31b86df17bf408f5 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 4 Apr 2023 10:47:36 -0600 Subject: [PATCH 191/208] Amount refinements --- lib/utilities/amount.dart | 45 ++++++++++++++++++++--- test/utilities/amount_test.dart | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/lib/utilities/amount.dart b/lib/utilities/amount.dart index ec9e746cc..04f61fdb9 100644 --- a/lib/utilities/amount.dart +++ b/lib/utilities/amount.dart @@ -1,11 +1,10 @@ import 'dart:convert'; import 'package:decimal/decimal.dart'; -import 'package:equatable/equatable.dart'; final _ten = BigInt.from(10); -class Amount implements Equatable { +class Amount { Amount({ required BigInt rawValue, required this.fractionDigits, @@ -83,6 +82,39 @@ class Amount implements Equatable { bool operator <=(Amount other) => raw <= other.raw; + Amount operator +(Amount other) { + if (fractionDigits != other.fractionDigits) { + throw ArgumentError( + "fractionDigits do not match: this=$this, other=$other"); + } + return Amount( + rawValue: raw + other.raw, + fractionDigits: fractionDigits, + ); + } + + Amount operator -(Amount other) { + if (fractionDigits != other.fractionDigits) { + throw ArgumentError( + "fractionDigits do not match: this=$this, other=$other"); + } + return Amount( + rawValue: raw - other.raw, + fractionDigits: fractionDigits, + ); + } + + Amount operator *(Amount other) { + if (fractionDigits != other.fractionDigits) { + throw ArgumentError( + "fractionDigits do not match: this=$this, other=$other"); + } + return Amount( + rawValue: raw * other.raw, + fractionDigits: fractionDigits, + ); + } + // =========================================================================== // ======= Overrides ========================================================= @@ -90,8 +122,13 @@ class Amount implements Equatable { String toString() => "Amount($raw, $fractionDigits)"; @override - List get props => [fractionDigits, _value]; + bool operator ==(Object other) => + identical(this, other) || + other is Amount && + runtimeType == other.runtimeType && + raw == other.raw && + fractionDigits == other.fractionDigits; @override - bool? get stringify => false; + int get hashCode => Object.hashAll([raw, fractionDigits]); } diff --git a/test/utilities/amount_test.dart b/test/utilities/amount_test.dart index 882055b3f..2f7368162 100644 --- a/test/utilities/amount_test.dart +++ b/test/utilities/amount_test.dart @@ -82,6 +82,9 @@ void main() { group("operators", () { final one = Amount(rawValue: BigInt.one, fractionDigits: 0); final two = Amount(rawValue: BigInt.two, fractionDigits: 0); + final four4 = Amount(rawValue: BigInt.from(4), fractionDigits: 4); + final four4_2 = Amount(rawValue: BigInt.from(4), fractionDigits: 4); + final four5 = Amount(rawValue: BigInt.from(4), fractionDigits: 5); test(">", () { expect(one > two, false); @@ -114,5 +117,65 @@ void main() { expect(two <= two, true); expect(two <= one, false); }); + + test("<=", () { + expect(one <= two, true); + expect(one <= one, true); + + expect(two <= two, true); + expect(two <= one, false); + }); + + test("==", () { + expect(one == two, false); + expect(one == one, true); + + expect(BigInt.from(2) == BigInt.from(2), true); + + expect(four4 == four4_2, true); + expect(four4 == four5, false); + }); + + test("+", () { + expect(one + two, Amount(rawValue: BigInt.from(3), fractionDigits: 0)); + expect(one + one, Amount(rawValue: BigInt.from(2), fractionDigits: 0)); + + expect( + Amount(rawValue: BigInt.from(3), fractionDigits: 0) + + Amount(rawValue: BigInt.from(-5), fractionDigits: 0), + Amount(rawValue: BigInt.from(-2), fractionDigits: 0)); + expect( + Amount(rawValue: BigInt.from(-3), fractionDigits: 0) + + Amount(rawValue: BigInt.from(6), fractionDigits: 0), + Amount(rawValue: BigInt.from(3), fractionDigits: 0)); + }); + + test("-", () { + expect(one - two, Amount(rawValue: BigInt.from(-1), fractionDigits: 0)); + expect(one - one, Amount(rawValue: BigInt.from(0), fractionDigits: 0)); + + expect( + Amount(rawValue: BigInt.from(3), fractionDigits: 0) - + Amount(rawValue: BigInt.from(-5), fractionDigits: 0), + Amount(rawValue: BigInt.from(8), fractionDigits: 0)); + expect( + Amount(rawValue: BigInt.from(-3), fractionDigits: 0) - + Amount(rawValue: BigInt.from(6), fractionDigits: 0), + Amount(rawValue: BigInt.from(-9), fractionDigits: 0)); + }); + + test("*", () { + expect(one * two, Amount(rawValue: BigInt.from(2), fractionDigits: 0)); + expect(one * one, Amount(rawValue: BigInt.from(1), fractionDigits: 0)); + + expect( + Amount(rawValue: BigInt.from(3), fractionDigits: 0) * + Amount(rawValue: BigInt.from(-5), fractionDigits: 0), + Amount(rawValue: BigInt.from(-15), fractionDigits: 0)); + expect( + Amount(rawValue: BigInt.from(-3), fractionDigits: 0) * + Amount(rawValue: BigInt.from(-6), fractionDigits: 0), + Amount(rawValue: BigInt.from(18), fractionDigits: 0)); + }); }); } From 81c612ddd779b0fdd0b0d3113e0720e1618cfc34 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 5 Apr 2023 16:06:31 -0600 Subject: [PATCH 192/208] WIP use Amount --- lib/db/isar/main_db.dart | 1 + lib/db/queries/queries.dart | 12 +- lib/models/balance.dart | 94 ++++--- lib/models/token_balance.dart | 85 +++--- lib/models/transaction_filter.dart | 6 +- lib/pages/coin_control/coin_control_view.dart | 31 ++- lib/pages/coin_control/utxo_card.dart | 14 +- lib/pages/coin_control/utxo_details_view.dart | 16 +- .../confirm_change_now_send.dart | 153 ++++++----- .../exchange_step_views/step_4_view.dart | 28 +- lib/pages/exchange_view/send_from_view.dart | 138 +++------- .../exchange_provider_options.dart | 75 +++--- .../exchange_view/trade_details_view.dart | 40 ++- .../confirm_paynym_connect_dialog.dart | 14 +- .../paynym/dialogs/paynym_details_popup.dart | 8 +- .../subwidgets/desktop_paynym_details.dart | 8 +- .../send_view/confirm_transaction_view.dart | 151 +++++------ lib/pages/send_view/send_view.dart | 248 ++++++++---------- .../firo_balance_selection_sheet.dart | 74 ++---- .../transaction_fee_selection_sheet.dart | 129 ++++----- lib/pages/send_view/token_send_view.dart | 136 +++++----- .../wallet_syncing_options_view.dart | 49 +--- .../sub_widgets/my_token_select_item.dart | 8 +- .../token_view/sub_widgets/token_summary.dart | 38 ++- .../wallet_balance_toggle_sheet.dart | 24 +- .../sub_widgets/wallet_summary_info.dart | 27 +- .../all_transactions_view.dart | 18 +- .../transaction_details_view.dart | 95 +++---- .../transaction_search_filter_view.dart | 26 +- lib/pages/wallet_view/wallet_view.dart | 6 +- .../sub_widgets/favorite_card.dart | 42 +-- .../sub_widgets/wallet_list_item.dart | 15 +- .../desktop_coin_control_use_dialog.dart | 25 +- .../coin_control/utxo_row.dart | 31 ++- .../exchange_steps/step_scaffold.dart | 6 +- .../subwidgets/desktop_choose_from_stack.dart | 67 +---- .../paynym/desktop_paynym_send_dialog.dart | 47 +--- .../my_stack_view/wallet_summary_table.dart | 8 +- .../sub_widgets/desktop_fee_dropdown.dart | 74 ++---- .../wallet_view/sub_widgets/desktop_send.dart | 109 ++++---- .../sub_widgets/desktop_token_send.dart | 88 ++++--- .../sub_widgets/desktop_wallet_features.dart | 4 +- .../sub_widgets/desktop_wallet_summary.dart | 26 +- lib/route_generator.dart | 4 +- .../coins/bitcoin/bitcoin_wallet.dart | 98 ++++--- .../coins/bitcoincash/bitcoincash_wallet.dart | 142 ++++++---- lib/services/coins/coin_service.dart | 5 +- .../coins/dogecoin/dogecoin_wallet.dart | 90 ++++--- .../coins/epiccash/epiccash_wallet.dart | 47 ++-- .../coins/ethereum/ethereum_wallet.dart | 76 +++--- lib/services/coins/firo/firo_wallet.dart | 247 ++++++++++------- .../coins/litecoin/litecoin_wallet.dart | 97 ++++--- lib/services/coins/manager.dart | 9 +- lib/services/coins/monero/monero_wallet.dart | 67 +++-- .../coins/namecoin/namecoin_wallet.dart | 92 ++++--- .../coins/particl/particl_wallet.dart | 100 ++++--- .../coins/wownero/wownero_wallet.dart | 87 +++--- .../ethereum/cached_eth_token_balance.dart | 22 +- .../ethereum/ethereum_token_service.dart | 44 ++-- .../mixins/coin_control_interface.dart | 34 ++- lib/services/mixins/electrum_x_parsing.dart | 52 ++-- lib/services/mixins/eth_token_cache.dart | 23 +- .../mixins/paynym_wallet_interface.dart | 16 +- lib/services/mixins/wallet_cache.dart | 23 +- lib/utilities/amount.dart | 52 ++++ lib/utilities/eth_commons.dart | 11 +- lib/utilities/format.dart | 105 ++++---- lib/widgets/managed_favorite.dart | 24 +- lib/widgets/transaction_card.dart | 11 +- .../wallet_info_row_balance_future.dart | 15 +- .../pages/send_view/send_view_test.mocks.dart | 16 +- ...d_address_book_view_screen_test.mocks.dart | 4 +- ..._entry_details_view_screen_test.mocks.dart | 4 +- ...ess_book_entry_view_screen_test.mocks.dart | 4 +- .../lockscreen_view_screen_test.mocks.dart | 4 +- .../main_view_screen_testA_test.mocks.dart | 4 +- .../main_view_screen_testB_test.mocks.dart | 4 +- .../main_view_screen_testC_test.mocks.dart | 4 +- .../backup_key_view_screen_test.mocks.dart | 4 +- ...up_key_warning_view_screen_test.mocks.dart | 4 +- .../create_pin_view_screen_test.mocks.dart | 4 +- ...restore_wallet_view_screen_test.mocks.dart | 4 +- ...ify_backup_key_view_screen_test.mocks.dart | 4 +- .../currency_view_screen_test.mocks.dart | 4 +- ...dd_custom_node_view_screen_test.mocks.dart | 4 +- .../node_details_view_screen_test.mocks.dart | 4 +- .../wallet_backup_view_screen_test.mocks.dart | 4 +- ...rescan_warning_view_screen_test.mocks.dart | 4 +- ...elete_mnemonic_view_screen_test.mocks.dart | 4 +- ...allet_settings_view_screen_test.mocks.dart | 4 +- .../settings_view_screen_test.mocks.dart | 4 +- ...search_results_view_screen_test.mocks.dart | 4 +- .../confirm_send_view_screen_test.mocks.dart | 4 +- .../receive_view_screen_test.mocks.dart | 4 +- .../send_view_screen_test.mocks.dart | 4 +- .../wallet_view_screen_test.mocks.dart | 4 +- .../services/coins/fake_coin_service_api.dart | 2 +- test/services/coins/manager_test.mocks.dart | 8 +- .../managed_favorite_test.mocks.dart | 16 +- .../table_view/table_view_row_test.mocks.dart | 16 +- .../transaction_card_test.mocks.dart | 16 +- test/widget_tests/wallet_card_test.mocks.dart | 8 +- ...et_info_row_balance_future_test.mocks.dart | 16 +- .../wallet_info_row_test.mocks.dart | 16 +- 104 files changed, 2211 insertions(+), 1890 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 9c0bd1f38..81188cb0e 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -3,6 +3,7 @@ import 'package:flutter_native_splash/cli_commands.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/exceptions/main_db/main_db_exception.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; diff --git a/lib/db/queries/queries.dart b/lib/db/queries/queries.dart index bc0307572..65fc6d26e 100644 --- a/lib/db/queries/queries.dart +++ b/lib/db/queries/queries.dart @@ -67,10 +67,10 @@ extension MainDBQueries on MainDB { final maybeDecimal = Decimal.tryParse(searchTerm); if (maybeDecimal != null) { qq = qq.or().valueEqualTo( - Format.decimalAmountToSatoshis( + Amount.fromDecimal( maybeDecimal, - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); } @@ -139,10 +139,10 @@ extension MainDBQueries on MainDB { final maybeDecimal = Decimal.tryParse(searchTerm); if (maybeDecimal != null) { qq = qq.or().valueEqualTo( - Format.decimalAmountToSatoshis( + Amount.fromDecimal( maybeDecimal, - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); } diff --git a/lib/models/balance.dart b/lib/models/balance.dart index 0589ac90d..c598a275a 100644 --- a/lib/models/balance.dart +++ b/lib/models/balance.dart @@ -1,15 +1,21 @@ import 'dart:convert'; -import 'package:decimal/decimal.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; + +enum Unit { + base, + u, + m, + normal; +} class Balance { final Coin coin; - final int total; - final int spendable; - final int blockedTotal; - final int pendingSpendable; + final Amount total; + final Amount spendable; + final Amount blockedTotal; + final Amount pendingSpendable; Balance({ required this.coin, @@ -19,36 +25,64 @@ class Balance { required this.pendingSpendable, }); - Decimal getTotal({bool includeBlocked = true}) => Format.satoshisToAmount( - includeBlocked ? total : total - blockedTotal, - coin: coin, - ); + // Decimal getTotal({bool includeBlocked = true}) => Format.satoshisToAmount( + // includeBlocked ? total : total - blockedTotal, + // coin: coin, + // ); + // + // Decimal getSpendable() => Format.satoshisToAmount( + // spendable, + // coin: coin, + // ); + // + // Decimal getPending() => Format.satoshisToAmount( + // pendingSpendable, + // coin: coin, + // ); + // + // Decimal getBlocked() => Format.satoshisToAmount( + // blockedTotal, + // coin: coin, + // ); - Decimal getSpendable() => Format.satoshisToAmount( - spendable, - coin: coin, - ); - - Decimal getPending() => Format.satoshisToAmount( - pendingSpendable, - coin: coin, - ); - - Decimal getBlocked() => Format.satoshisToAmount( - blockedTotal, - coin: coin, - ); - - String toJsonIgnoreCoin() => jsonEncode(toMap()..remove("coin")); + String toJsonIgnoreCoin() => jsonEncode({ + "total": total.toJsonString(), + "spendable": spendable.toJsonString(), + "blockedTotal": blockedTotal.toJsonString(), + "pendingSpendable": pendingSpendable.toJsonString(), + }); + // need to fall back to parsing from in due to cached balances being previously + // stored as int values instead of Amounts factory Balance.fromJson(String json, Coin coin) { final decoded = jsonDecode(json); return Balance( coin: coin, - total: decoded["total"] as int, - spendable: decoded["spendable"] as int, - blockedTotal: decoded["blockedTotal"] as int, - pendingSpendable: decoded["pendingSpendable"] as int, + total: decoded["total"] is String + ? Amount.fromSerializedJsonString(decoded["total"] as String) + : Amount( + rawValue: BigInt.from(decoded["total"] as int), + fractionDigits: coin.decimals, + ), + spendable: decoded["spendable"] is String + ? Amount.fromSerializedJsonString(decoded["spendable"] as String) + : Amount( + rawValue: BigInt.from(decoded["spendable"] as int), + fractionDigits: coin.decimals, + ), + blockedTotal: decoded["blockedTotal"] is String + ? Amount.fromSerializedJsonString(decoded["blockedTotal"] as String) + : Amount( + rawValue: BigInt.from(decoded["blockedTotal"] as int), + fractionDigits: coin.decimals, + ), + pendingSpendable: decoded["pendingSpendable"] is String + ? Amount.fromSerializedJsonString( + decoded["pendingSpendable"] as String) + : Amount( + rawValue: BigInt.from(decoded["pendingSpendable"] as int), + fractionDigits: coin.decimals, + ), ); } diff --git a/lib/models/token_balance.dart b/lib/models/token_balance.dart index 8984aa135..4dbdbefcf 100644 --- a/lib/models/token_balance.dart +++ b/lib/models/token_balance.dart @@ -1,14 +1,12 @@ import 'dart:convert'; -import 'package:decimal/decimal.dart'; import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; class TokenBalance extends Balance { TokenBalance({ required this.contractAddress, - required this.decimalPlaces, required super.total, required super.spendable, required super.blockedTotal, @@ -17,37 +15,35 @@ class TokenBalance extends Balance { }); final String contractAddress; - final int decimalPlaces; - @override - Decimal getTotal({bool includeBlocked = false}) => - Format.satoshisToEthTokenAmount( - includeBlocked ? total : total - blockedTotal, - decimalPlaces, - ); - - @override - Decimal getSpendable() => Format.satoshisToEthTokenAmount( - spendable, - decimalPlaces, - ); - - @override - Decimal getPending() => Format.satoshisToEthTokenAmount( - pendingSpendable, - decimalPlaces, - ); - - @override - Decimal getBlocked() => Format.satoshisToEthTokenAmount( - blockedTotal, - decimalPlaces, - ); + // @override + // Decimal getTotal({bool includeBlocked = false}) => + // Format.satoshisToEthTokenAmount( + // includeBlocked ? total : total - blockedTotal, + // decimalPlaces, + // ); + // + // @override + // Decimal getSpendable() => Format.satoshisToEthTokenAmount( + // spendable, + // decimalPlaces, + // ); + // + // @override + // Decimal getPending() => Format.satoshisToEthTokenAmount( + // pendingSpendable, + // decimalPlaces, + // ); + // + // @override + // Decimal getBlocked() => Format.satoshisToEthTokenAmount( + // blockedTotal, + // decimalPlaces, + // ); @override String toJsonIgnoreCoin() => jsonEncode({ "contractAddress": contractAddress, - "decimalPlaces": decimalPlaces, "total": total, "spendable": spendable, "blockedTotal": blockedTotal, @@ -56,15 +52,36 @@ class TokenBalance extends Balance { factory TokenBalance.fromJson( String json, + int fractionDigits, ) { final decoded = jsonDecode(json); return TokenBalance( contractAddress: decoded["contractAddress"] as String, - decimalPlaces: decoded["decimalPlaces"] as int, - total: decoded["total"] as int, - spendable: decoded["spendable"] as int, - blockedTotal: decoded["blockedTotal"] as int, - pendingSpendable: decoded["pendingSpendable"] as int, + total: decoded["total"] is String + ? Amount.fromSerializedJsonString(decoded["total"] as String) + : Amount( + rawValue: BigInt.from(decoded["total"] as int), + fractionDigits: fractionDigits, + ), + spendable: decoded["spendable"] is String + ? Amount.fromSerializedJsonString(decoded["spendable"] as String) + : Amount( + rawValue: BigInt.from(decoded["spendable"] as int), + fractionDigits: fractionDigits, + ), + blockedTotal: decoded["blockedTotal"] is String + ? Amount.fromSerializedJsonString(decoded["blockedTotal"] as String) + : Amount( + rawValue: BigInt.from(decoded["blockedTotal"] as int), + fractionDigits: fractionDigits, + ), + pendingSpendable: decoded["pendingSpendable"] is String + ? Amount.fromSerializedJsonString( + decoded["pendingSpendable"] as String) + : Amount( + rawValue: BigInt.from(decoded["pendingSpendable"] as int), + fractionDigits: fractionDigits, + ), ); } } diff --git a/lib/models/transaction_filter.dart b/lib/models/transaction_filter.dart index 7ef5f0bff..b28c9860a 100644 --- a/lib/models/transaction_filter.dart +++ b/lib/models/transaction_filter.dart @@ -1,10 +1,12 @@ +import 'package:stackwallet/utilities/amount.dart'; + class TransactionFilter { final bool sent; final bool received; final bool trade; final DateTime? from; final DateTime? to; - final int? amount; + final Amount? amount; final String keyword; TransactionFilter({ @@ -23,7 +25,7 @@ class TransactionFilter { bool? trade, DateTime? from, DateTime? to, - int? amount, + Amount? amount, String? keyword, }) { return TransactionFilter( diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index de12dba39..fb415d3fa 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -8,12 +8,13 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/coin_control/utxo_card.dart'; import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/mixins/coin_control_interface.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; @@ -682,12 +683,14 @@ class _CoinControlViewState extends ConsumerState { value += element, ); return Text( - "${Format.satoshisToAmount( - selectedSum, - coin: coin, - ).toStringAsFixed( - coin.decimals, - )} ${coin.ticker}", + "${selectedSum.toAmount(fractionDigits: coin.decimals).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: widget.requestedTotal == null ? STextStyles.w600_14(context) : STextStyles.w600_14(context).copyWith( @@ -728,12 +731,14 @@ class _CoinControlViewState extends ConsumerState { style: STextStyles.w600_14(context), ), Text( - "${Format.satoshisToAmount( - widget.requestedTotal!, - coin: coin, - ).toStringAsFixed( - coin.decimals, - )} ${coin.ticker}", + "${widget.requestedTotal!.toAmount(fractionDigits: coin.decimals).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: STextStyles.w600_14(context), ), ], diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index 685f94272..528e19fe0 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -123,10 +124,13 @@ class _UtxoCardState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - "${Format.satoshisToAmount( - utxo.value, - coin: coin, - ).toStringAsFixed(coin.decimals)} ${coin.ticker}", + "${utxo.value.toAmount(fractionDigits: coin.decimals).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )}} ${coin.ticker}", style: STextStyles.w600_14(context), ), const SizedBox( diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index 64645bff9..b1994b10d 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -6,9 +6,10 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -239,12 +240,13 @@ class _UtxoDetailsViewState extends ConsumerState { width: 16, ), Text( - "${Format.satoshisToAmount( - utxo!.value, - coin: coin, - ).toStringAsFixed( - coin.decimals, - )} ${coin.ticker}", + "${utxo!.value.toAmount(fractionDigits: coin.decimals).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: STextStyles.pageTitleH2(context), ), ], diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 23712f0fd..85faaf55a 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -11,9 +11,9 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -359,14 +359,10 @@ class _ConfirmChangeNowSendViewState mainAxisAlignment: MainAxisAlignment.end, children: [ Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - ref.watch( - managerProvider.select((value) => value.coin), + "${(transactionInfo["fee"] as int).toAmount( + fractionDigits: ref.watch( + managerProvider + .select((value) => value.coin.decimals), ), )} ${ref.watch( managerProvider.select((value) => value.coin), @@ -400,26 +396,37 @@ class _ConfirmChangeNowSendViewState .textConfirmTotalAmount, ), ), - Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int) + - (transactionInfo["recipientAmt"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - ref.watch( + Builder( + builder: (context) { + final coin = ref.watch( managerProvider.select((value) => value.coin), - ), - )} ${ref.watch( - managerProvider.select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, + ); + final fee = + (transactionInfo["fee"] as int).toAmount( + fractionDigits: coin.decimals, + ); + final amount = + transactionInfo["recipientAmt"] as Amount; + final total = amount + fee; + final locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); + return Text( + "${total.localizedStringAsFixed( + locale: locale, + )}" + " ${coin.ticker}", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ); + }, ), ], ), @@ -570,16 +577,20 @@ class _ConfirmChangeNowSendViewState final price = ref.watch( priceAnd24hChangeNotifierProvider .select((value) => value.getPrice(coin))); - final amount = Format.satoshisToAmount( - transactionInfo["recipientAmt"] as int, - coin: coin, - ); - final value = price.item1 * amount; + final amount = + transactionInfo["recipientAmt"] as Amount; + final value = (price.item1 * amount.decimal) + .toAmount(fractionDigits: 2); final currency = ref.watch(prefsChangeNotifierProvider .select((value) => value.currency)); + final locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); return Text( - " | ${value.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} $currency", + " | ${value.localizedStringAsFixed(locale: locale)} $currency", style: STextStyles.desktopTextExtraExtraSmall(context) .copyWith( @@ -592,12 +603,13 @@ class _ConfirmChangeNowSendViewState ], ), child: Text( - "${Format.satoshiAmountToPrettyString(transactionInfo["recipientAmt"] as int, ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} ${ref.watch( + "${(transactionInfo["recipientAmt"] as Amount).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -625,12 +637,17 @@ class _ConfirmChangeNowSendViewState style: STextStyles.smallMed12(context), ), Text( - "${Format.satoshiAmountToPrettyString(transactionInfo["fee"] as int, ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} ${ref.watch( + "${(transactionInfo["fee"] as int).toAmount(fractionDigits: ref.watch( + managerProvider.select( + (value) => value.coin.decimals, + ), + )).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -711,21 +728,35 @@ class _ConfirmChangeNowSendViewState .textConfirmTotalAmount, ), ), - Text( - "${Format.satoshiAmountToPrettyString((transactionInfo["fee"] as int) + (transactionInfo["recipientAmt"] as int), ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} ${ref.watch( - managerProvider.select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, + Builder( + builder: (context) { + final coin = ref.watch( + managerProvider.select((value) => value.coin), + ); + final fee = (transactionInfo["fee"] as int).toAmount( + fractionDigits: coin.decimals, + ); + final amount = + transactionInfo["recipientAmt"] as Amount; + final total = amount + fee; + final locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); + return Text( + "${total.localizedStringAsFixed( + locale: locale, + )}" + " ${coin.ticker}", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ); + }, ), ], ), diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 16eb3923f..fcdd1325e 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -15,11 +15,11 @@ import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dia import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.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/background.dart'; @@ -526,10 +526,11 @@ class _Step4ViewState extends ConsumerState { walletsChangeNotifierProvider) .getManager(tuple.item1); - final amount = - Format.decimalAmountToSatoshis( - model.sendAmount, - manager.coin); + final Amount amount = + model.sendAmount.toAmount( + fractionDigits: + manager.coin.decimals, + ); final address = model.trade!.payInAddress; @@ -565,7 +566,7 @@ class _Step4ViewState extends ConsumerState { final txDataFuture = manager.prepareSend( address: address, - satoshiAmount: amount, + amount: amount, args: { "feeRate": FeeRateType.average, @@ -670,12 +671,17 @@ class _Step4ViewState extends ConsumerState { .useMaterialPageRoute, builder: (BuildContext context) { + final coin = + coinFromTickerCaseInsensitive( + model.trade! + .payInCurrency); return SendFromView( - coin: - coinFromTickerCaseInsensitive( - model.trade! - .payInCurrency), - amount: model.sendAmount, + coin: coin, + amount: model.sendAmount + .toAmount( + fractionDigits: + coin.decimals, + ), address: model .trade!.payInAddress, trade: model.trade!, diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 780a88d92..1f3ec5d51 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -13,15 +12,14 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.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/utilities/util.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -45,7 +43,7 @@ class SendFromView extends ConsumerStatefulWidget { static const String routeName = "/sendFrom"; final Coin coin; - final Decimal amount; + final Amount amount; final String address; final Trade trade; final bool shouldPopRoot; @@ -57,14 +55,10 @@ class SendFromView extends ConsumerStatefulWidget { class _SendFromViewState extends ConsumerState { late final Coin coin; - late final Decimal amount; + late final Amount amount; late final String address; late final Trade trade; - String formatAmount(Decimal amount, Coin coin) { - return amount.toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - } - @override void initState() { coin = widget.coin; @@ -151,7 +145,13 @@ class _SendFromViewState extends ConsumerState { Row( children: [ Text( - "You need to send ${formatAmount(amount, coin)} ${coin.ticker}", + "You need to send ${amount.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: isDesktop ? STextStyles.desktopTextExtraExtraSmall(context) : STextStyles.itemSubtitle(context), @@ -202,7 +202,7 @@ class SendFromCard extends ConsumerStatefulWidget { }) : super(key: key); final String walletId; - final Decimal amount; + final Amount amount; final String address; final Trade trade; final bool fromDesktopStep4; @@ -213,13 +213,11 @@ class SendFromCard extends ConsumerStatefulWidget { class _SendFromCardState extends ConsumerState { late final String walletId; - late final Decimal amount; + late final Amount amount; late final String address; late final Trade trade; Future _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async { - final _amount = Format.decimalAmountToSatoshis(amount, manager.coin); - try { bool wasCancelled = false; @@ -265,7 +263,7 @@ class _SendFromCardState extends ConsumerState { if (shouldSendPublicFiroFunds == null) { txDataFuture = manager.prepareSend( address: address, - satoshiAmount: _amount, + amount: amount, args: { "feeRate": FeeRateType.average, // ref.read(feeRateTypeStateProvider) @@ -277,7 +275,7 @@ class _SendFromCardState extends ConsumerState { if (shouldSendPublicFiroFunds) { txDataFuture = firoWallet.prepareSendPublic( address: address, - satoshiAmount: _amount, + amount: amount, args: { "feeRate": FeeRateType.average, // ref.read(feeRateTypeStateProvider) @@ -286,7 +284,7 @@ class _SendFromCardState extends ConsumerState { } else { txDataFuture = firoWallet.prepareSend( address: address, - satoshiAmount: _amount, + amount: amount, args: { "feeRate": FeeRateType.average, // ref.read(feeRateTypeStateProvider) @@ -452,37 +450,11 @@ class _SendFromCardState extends ConsumerState { "Use private balance", style: STextStyles.itemSubtitle(context), ), - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future(() => - (manager.wallet as FiroWallet) - .availablePrivateBalance()), - builder: (builderContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: locale, - decimalPlaces: - Constants.decimalPlacesForCoin(coin), - )} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, + Text( + "${(manager.wallet as FiroWallet).availablePrivateBalance().localizedStringAsFixed( + locale: locale, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), ), ], ), @@ -540,37 +512,11 @@ class _SendFromCardState extends ConsumerState { "Use public balance", style: STextStyles.itemSubtitle(context), ), - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future(() => - (manager.wallet as FiroWallet) - .availablePublicBalance()), - builder: (builderContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: locale, - decimalPlaces: - Constants.decimalPlacesForCoin(coin), - )} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, + Text( + "${(manager.wallet as FiroWallet).availablePublicBalance().localizedStringAsFixed( + locale: locale, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), ), ], ), @@ -652,35 +598,11 @@ class _SendFromCardState extends ConsumerState { height: 2, ), if (!isFiro) - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future(() => manager.balance.getTotal()), - builder: - (builderContext, AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: locale, - decimalPlaces: - Constants.decimalPlacesForCoin(coin), - )} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, + Text( + "${manager.balance.spendable.localizedStringAsFixed( + locale: locale, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), ), ], ), diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index c0f4312d0..0a70f4347 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -9,10 +9,9 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -198,18 +197,6 @@ class _ExchangeProviderOptionsState snapshot.hasData) { final estimate = snapshot.data?.value; if (estimate != null) { - Decimal rate; - if (estimate.reversed) { - rate = (toAmount / - estimate.estimatedAmount) - .toDecimal( - scaleOnInfinitePrecision: 12); - } else { - rate = (estimate.estimatedAmount / - fromAmount) - .toDecimal( - scaleOnInfinitePrecision: 12); - } Coin coin; try { coin = coinFromTickerCaseInsensitive( @@ -217,18 +204,32 @@ class _ExchangeProviderOptionsState } catch (_) { coin = Coin.bitcoin; } + Amount rate; + if (estimate.reversed) { + rate = (toAmount / + estimate.estimatedAmount) + .toDecimal( + scaleOnInfinitePrecision: 18) + .toAmount( + fractionDigits: + coin.decimals); + } else { + rate = (estimate.estimatedAmount / + fromAmount) + .toDecimal( + scaleOnInfinitePrecision: 18) + .toAmount( + fractionDigits: + coin.decimals); + } return Text( - "1 ${sendCurrency.ticker.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, + "1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select( (value) => value.locale), ), - decimalPlaces: - Constants.decimalPlacesForCoin( - coin), )} ${receivingCurrency.ticker.toUpperCase()}", style: STextStyles.itemSubtitle12( context) @@ -435,18 +436,6 @@ class _ExchangeProviderOptionsState snapshot.hasData) { final estimate = snapshot.data?.value; if (estimate != null) { - Decimal rate; - if (estimate.reversed) { - rate = (toAmount / - estimate.estimatedAmount) - .toDecimal( - scaleOnInfinitePrecision: 12); - } else { - rate = (estimate.estimatedAmount / - fromAmount) - .toDecimal( - scaleOnInfinitePrecision: 12); - } Coin coin; try { coin = coinFromTickerCaseInsensitive( @@ -454,18 +443,32 @@ class _ExchangeProviderOptionsState } catch (_) { coin = Coin.bitcoin; } + Amount rate; + if (estimate.reversed) { + rate = (toAmount / + estimate.estimatedAmount) + .toDecimal( + scaleOnInfinitePrecision: 18) + .toAmount( + fractionDigits: coin.decimals, + ); + } else { + rate = (estimate.estimatedAmount / + fromAmount) + .toDecimal( + scaleOnInfinitePrecision: 18) + .toAmount( + fractionDigits: coin.decimals, + ); + } return Text( - "1 ${sendCurrency.ticker.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, + "1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select( (value) => value.locale), ), - decimalPlaces: - Constants.decimalPlacesForCoin( - coin), )} ${receivingCurrency.ticker.toUpperCase()}", style: STextStyles.itemSubtitle12( context) diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 76653bd18..d23d5be39 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -20,6 +20,7 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -256,11 +257,11 @@ class _TradeDetailsViewState extends ConsumerState { label: "Send from Stack", buttonHeight: ButtonHeight.l, onPressed: () { - final amount = sendAmount; - final address = trade.payInAddress; - final coin = coinFromTickerCaseInsensitive(trade.payInCurrency); + final amount = + sendAmount.toAmount(fractionDigits: coin.decimals); + final address = trade.payInAddress; Navigator.of(context).pushNamed( SendFromView.routeName, @@ -339,13 +340,32 @@ class _TradeDetailsViewState extends ConsumerState { const SizedBox( height: 4, ), - SelectableText( - "-${Format.localizedStringAsFixed(value: sendAmount, locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), decimalPlaces: trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}", - style: STextStyles.itemSubtitle(context), - ), + Builder(builder: (context) { + String text; + try { + final coin = coinFromTickerCaseInsensitive( + trade.payInCurrency); + final amount = sendAmount.toAmount( + fractionDigits: coin.decimals); + text = amount.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + ); + } catch (_) { + text = sendAmount.toStringAsFixed( + trade.payInCurrency.toLowerCase() == "xmr" + ? 12 + : 8); + } + + return SelectableText( + "-$text ${trade.payInCurrency.toUpperCase()}", + style: STextStyles.itemSubtitle(context), + ); + }), ], ), if (!isDesktop) diff --git a/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart b/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart index 780db2f81..075fbd40e 100644 --- a/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart +++ b/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -17,25 +16,22 @@ class ConfirmPaynymConnectDialog extends StatelessWidget { const ConfirmPaynymConnectDialog({ Key? key, required this.nymName, + required this.locale, required this.onConfirmPressed, required this.amount, required this.coin, }) : super(key: key); final String nymName; + final String locale; final VoidCallback onConfirmPressed; - final int amount; + final Amount amount; final Coin coin; String get title => "Connect to $nymName"; String get message => "A one-time connection fee of " - "${Format.satoshisToAmount( - amount, - coin: coin, - ).toStringAsFixed( - Constants.decimalPlacesForCoin(coin), - )} ${coin.ticker} " + "${amount.localizedStringAsFixed(locale: locale)} ${coin.ticker} " "will be charged to connect to this PayNym.\n\nThis fee " "covers the cost of creating a one-time transaction to create a " "record on the blockchain. This keeps PayNyms decentralized."; diff --git a/lib/pages/paynym/dialogs/paynym_details_popup.dart b/lib/pages/paynym/dialogs/paynym_details_popup.dart index c6dedeb39..5441ca5c3 100644 --- a/lib/pages/paynym/dialogs/paynym_details_popup.dart +++ b/lib/pages/paynym/dialogs/paynym_details_popup.dart @@ -13,9 +13,11 @@ import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -134,6 +136,7 @@ class _PaynymDetailsPopupState extends ConsumerState { context: context, builder: (context) => ConfirmPaynymConnectDialog( nymName: widget.accountLite.nymName, + locale: ref.read(localeServiceChangeNotifierProvider).locale, onConfirmPressed: () { // print("CONFIRM NOTIF TX: $preparedTx"); @@ -156,7 +159,10 @@ class _PaynymDetailsPopupState extends ConsumerState { ), ); }, - amount: (preparedTx["amount"] as int) + (preparedTx["fee"] as int), + amount: (preparedTx["amount"] as Amount) + + (preparedTx["fee"] as int).toAmount( + fractionDigits: manager.coin.decimals, + ), coin: manager.coin, ), ); diff --git a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart index ad01b1f7e..d2dfc0cc8 100644 --- a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart +++ b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart @@ -12,8 +12,10 @@ import 'package:stackwallet/pages/paynym/dialogs/confirm_paynym_connect_dialog.d import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -102,6 +104,7 @@ class _PaynymDetailsPopupState extends ConsumerState { context: context, builder: (context) => ConfirmPaynymConnectDialog( nymName: widget.accountLite.nymName, + locale: ref.read(localeServiceChangeNotifierProvider).locale, onConfirmPressed: () { Navigator.of(context, rootNavigator: true).pop(); unawaited( @@ -139,7 +142,10 @@ class _PaynymDetailsPopupState extends ConsumerState { ), ); }, - amount: (preparedTx["amount"] as int) + (preparedTx["fee"] as int), + amount: (preparedTx["amount"] as Amount) + + (preparedTx["fee"] as int).toAmount( + fractionDigits: manager.coin.decimals, + ), coin: manager.coin, ), ); diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index ce212c8f7..2f0c44092 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -18,10 +18,10 @@ import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -403,12 +403,12 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), ), Text( - "${Format.satoshiAmountToPrettyString(transactionInfo["recipientAmt"] as int, ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} $unit", + "${(transactionInfo["recipientAmt"] as Amount).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} $unit", style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, ), @@ -427,12 +427,18 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), ), Text( - "${Format.satoshiAmountToPrettyString(transactionInfo["fee"] as int, ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} ${ref.watch( + "${(transactionInfo["fee"] as int).toAmount( + fractionDigits: ref.watch( + managerProvider.select( + (value) => value.coin.decimals, + ), + ), + ).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -534,7 +540,7 @@ class _ConfirmTransactionViewState Builder( builder: (context) { final amount = - transactionInfo["recipientAmt"] as int; + transactionInfo["recipientAmt"] as Amount; final coin = ref.watch( managerProvider.select( (value) => value.coin, @@ -551,29 +557,25 @@ class _ConfirmTransactionViewState .getPrice(coin) .item1; if (price > Decimal.zero) { - fiatAmount = Format.localizedStringAsFixed( - value: Format.satoshisToAmount(amount, - coin: coin) * - price, - locale: ref - .read( - localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: 2, - ); + fiatAmount = (amount.decimal * price) + .toAmount(fractionDigits: 2) + .localizedStringAsFixed( + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + ); } } return Row( children: [ Text( - "${Format.satoshiAmountToPrettyString( - amount, - ref.watch( + "${amount.localizedStringAsFixed( + locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), - coin, )} $unit", style: STextStyles .desktopTextExtraExtraSmall( @@ -676,19 +678,19 @@ class _ConfirmTransactionViewState value.getManager(walletId))) .coin; - final fee = Format.satoshisToAmount( - transactionInfo["fee"] as int, - coin: coin, + final fee = + (transactionInfo["fee"] as int).toAmount( + fractionDigits: coin.decimals, ); return Text( - "${Format.localizedStringAsFixed( - value: fee, + "${fee.localizedStringAsFixed( locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: - Constants.decimalPlacesForCoin(coin), + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), )} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( @@ -855,17 +857,17 @@ class _ConfirmTransactionViewState .select((value) => value.getManager(walletId))) .coin; - final fee = Format.satoshisToAmount( - transactionInfo["fee"] as int, - coin: coin, + final fee = (transactionInfo["fee"] as int).toAmount( + fractionDigits: coin.decimals, ); return Text( - "${Format.localizedStringAsFixed( - value: fee, - locale: ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: Constants.decimalPlacesForCoin(coin), + "${fee.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), )} ${coin.ticker}", style: STextStyles.itemSubtitle(context), ); @@ -911,34 +913,37 @@ class _ConfirmTransactionViewState .textConfirmTotalAmount, ), ), - Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int) + - (transactionInfo["recipientAmt"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - ref.watch( - managerProvider.select((value) => value.coin), - ), - )} ${ref.watch( - managerProvider.select((value) => value.coin), - ).ticker}", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, - ), + Builder(builder: (context) { + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).coin)); + final fee = (transactionInfo["fee"] as int) + .toAmount(fractionDigits: coin.decimals); + final locale = ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ); + final amount = transactionInfo["recipientAmt"] as Amount; + return Text( + "${(amount + fee).localizedStringAsFixed( + locale: locale, + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ); + }), ], ), ), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 166456c0f..170b5b85f 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -25,13 +25,13 @@ import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -94,8 +94,8 @@ class _SendViewState extends ConsumerState { final _cryptoFocus = FocusNode(); final _baseFocus = FocusNode(); - Decimal? _amountToSend; - Decimal? _cachedAmountToSend; + Amount? _amountToSend; + Amount? _cachedAmountToSend; String? _address; String? _privateBalanceString; @@ -106,7 +106,7 @@ class _SendViewState extends ConsumerState { bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; - Decimal? _cachedBalance; + Amount? _cachedBalance; Set selectedUTXOs = {}; @@ -118,7 +118,9 @@ class _SendViewState extends ConsumerState { cryptoAmount != ",") { _amountToSend = cryptoAmount.contains(",") ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) - : Decimal.parse(cryptoAmount); + .toAmount(fractionDigits: coin.decimals) + : Decimal.parse(cryptoAmount) + .toAmount(fractionDigits: coin.decimals); if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -131,13 +133,13 @@ class _SendViewState extends ConsumerState { ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (price > Decimal.zero) { - final String fiatAmountString = Format.localizedStringAsFixed( - value: _amountToSend! * price, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: 2, - ); - - baseAmountController.text = fiatAmountString; + baseAmountController.text = (_amountToSend!.decimal * price) + .toAmount( + fractionDigits: 2, + ) + .localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); } } else { _amountToSend = null; @@ -152,11 +154,8 @@ class _SendViewState extends ConsumerState { setState(() { _calculateFeesFuture = calculateFees( _amountToSend == null - ? 0 - : Format.decimalAmountToSatoshis( - _amountToSend!, - coin, - ), + ? 0.toAmount(fractionDigits: coin.decimals) + : _amountToSend!, ); }); } @@ -176,24 +175,19 @@ class _SendViewState extends ConsumerState { setState(() { _calculateFeesFuture = calculateFees( _amountToSend == null - ? 0 - : Format.decimalAmountToSatoshis( - _amountToSend!, - coin, - ), + ? 0.toAmount(fractionDigits: coin.decimals) + : _amountToSend!, ); }); } }); } - int _currentFee = 0; + late Amount _currentFee; void _setCurrentFee(String fee, bool shouldSetState) { - final value = Format.decimalAmountToSatoshis( - Decimal.parse(fee), - coin, - ); + final value = Decimal.parse(fee).toAmount(fractionDigits: coin.decimals); + if (shouldSetState) { setState(() => _currentFee = value); } else { @@ -211,28 +205,28 @@ class _SendViewState extends ConsumerState { return null; } - void _updatePreviewButtonState(String? address, Decimal? amount) { + void _updatePreviewButtonState(String? address, Amount? amount) { if (isPaynymSend) { ref.read(previewTxButtonStateProvider.state).state = - (amount != null && amount > Decimal.zero); + (amount != null && amount > Amount.zero); } else { final isValidAddress = ref .read(walletsChangeNotifierProvider) .getManager(walletId) .validateAddress(address ?? ""); ref.read(previewTxButtonStateProvider.state).state = - (isValidAddress && amount != null && amount > Decimal.zero); + (isValidAddress && amount != null && amount > Amount.zero); } } late Future _calculateFeesFuture; - Map cachedFees = {}; - Map cachedFiroPrivateFees = {}; - Map cachedFiroPublicFees = {}; + Map cachedFees = {}; + Map cachedFiroPrivateFees = {}; + Map cachedFiroPublicFees = {}; - Future calculateFees(int amount) async { - if (amount <= 0) { + Future calculateFees(Amount amount) async { + if (amount <= Amount.zero) { return "0"; } @@ -269,7 +263,8 @@ class _SendViewState extends ConsumerState { break; } - int fee; + final String locale = ref.read(localeServiceChangeNotifierProvider).locale; + Amount fee; if (coin == Coin.monero) { MoneroTransactionPriority specialMoneroId; switch (ref.read(feeRateTypeStateProvider.state).state) { @@ -285,8 +280,7 @@ class _SendViewState extends ConsumerState { } fee = await manager.estimateFeeFor(amount, specialMoneroId.raw!); - cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cachedFees[amount] = fee.localizedStringAsFixed(locale: locale); return cachedFees[amount]!; } else if (coin == Coin.firo || coin == Coin.firoTestNet) { @@ -294,23 +288,22 @@ class _SendViewState extends ConsumerState { "Private") { fee = await manager.estimateFeeFor(amount, feeRate); - cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cachedFiroPrivateFees[amount] = + fee.localizedStringAsFixed(locale: locale); return cachedFiroPrivateFees[amount]!; } else { fee = await (manager.wallet as FiroWallet) .estimateFeeForPublic(amount, feeRate); - cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cachedFiroPublicFees[amount] = + fee.localizedStringAsFixed(locale: locale); return cachedFiroPublicFees[amount]!; } } else { fee = await manager.estimateFeeFor(amount, feeRate); - cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cachedFees[amount] = fee.localizedStringAsFixed(locale: locale); return cachedFees[amount]!; } @@ -321,7 +314,7 @@ class _SendViewState extends ConsumerState { final wallet = ref.read(provider).wallet as FiroWallet?; if (wallet != null) { - Decimal? balance; + Amount? balance; if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { balance = wallet.availablePrivateBalance(); @@ -329,8 +322,9 @@ class _SendViewState extends ConsumerState { balance = wallet.availablePublicBalance(); } - return Format.localizedStringAsFixed( - value: balance, locale: locale, decimalPlaces: 8); + return balance.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); } return null; @@ -345,20 +339,19 @@ class _SendViewState extends ConsumerState { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - final amount = Format.decimalAmountToSatoshis(_amountToSend!, coin); - int availableBalance; + final Amount amount = _amountToSend!; + final Amount availableBalance; if ((coin == Coin.firo || coin == Coin.firoTestNet)) { if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { - availableBalance = Format.decimalAmountToSatoshis( - (manager.wallet as FiroWallet).availablePrivateBalance(), coin); + availableBalance = + (manager.wallet as FiroWallet).availablePrivateBalance(); } else { - availableBalance = Format.decimalAmountToSatoshis( - (manager.wallet as FiroWallet).availablePublicBalance(), coin); + availableBalance = + (manager.wallet as FiroWallet).availablePublicBalance(); } } else { - availableBalance = - Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin); + availableBalance = manager.balance.spendable; } final coinControlEnabled = @@ -462,7 +455,7 @@ class _SendViewState extends ConsumerState { final feeRate = ref.read(feeRateTypeStateProvider); txDataFuture = wallet.preparePaymentCodeSend( paymentCode: paymentCode, - satoshiAmount: amount, + amount: amount, args: { "feeRate": feeRate, "UTXOs": (manager.hasCoinControlSupport && @@ -477,13 +470,13 @@ class _SendViewState extends ConsumerState { "Private") { txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic( address: _address!, - satoshiAmount: amount, + amount: amount, args: {"feeRate": ref.read(feeRateTypeStateProvider)}, ); } else { txDataFuture = manager.prepareSend( address: _address!, - satoshiAmount: amount, + amount: amount, args: { "feeRate": ref.read(feeRateTypeStateProvider), "UTXOs": (manager.hasCoinControlSupport && @@ -565,12 +558,14 @@ class _SendViewState extends ConsumerState { @override void initState() { + coin = widget.coin; ref.refresh(feeSheetSessionCacheProvider); + _currentFee = 0.toAmount(fractionDigits: coin.decimals); - _calculateFeesFuture = calculateFees(0); + _calculateFeesFuture = + calculateFees(0.toAmount(fractionDigits: coin.decimals)); _data = widget.autoFillData; walletId = widget.walletId; - coin = widget.coin; clipboard = widget.clipboard; scanner = widget.barcodeScanner; @@ -676,12 +671,14 @@ class _SendViewState extends ConsumerState { ref.listen(publicPrivateBalanceStateProvider, (previous, next) { if (_amountToSend == null) { setState(() { - _calculateFeesFuture = calculateFees(0); + _calculateFeesFuture = + calculateFees(0.toAmount(fractionDigits: coin.decimals)); }); } else { setState(() { _calculateFeesFuture = calculateFees( - Format.decimalAmountToSatoshis(_amountToSend!, coin)); + _amountToSend!, + ); }); } }); @@ -802,7 +799,7 @@ class _SendViewState extends ConsumerState { coin != Coin.firoTestNet) ? Future(() => ref.watch( provider.select((value) => - value.balance.getSpendable()))) + value.balance.spendable))) : ref.watch(publicPrivateBalanceStateProvider.state).state == "Private" ? Future(() => (ref @@ -814,7 +811,7 @@ class _SendViewState extends ConsumerState { .wallet as FiroWallet) .availablePublicBalance()), builder: - (_, AsyncSnapshot snapshot) { + (_, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -825,10 +822,9 @@ class _SendViewState extends ConsumerState { return GestureDetector( onTap: () { cryptoAmountController.text = - _cachedBalance!.toStringAsFixed( - Constants - .decimalPlacesForCoin( - coin)); + _cachedBalance! + .localizedStringAsFixed( + locale: locale); }, child: Container( color: Colors.transparent, @@ -837,10 +833,8 @@ class _SendViewState extends ConsumerState { CrossAxisAlignment.end, children: [ Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance!, + "${_cachedBalance!.localizedStringAsFixed( locale: locale, - decimalPlaces: 8, )} ${coin.ticker}", style: STextStyles.titleBold12( @@ -851,17 +845,11 @@ class _SendViewState extends ConsumerState { textAlign: TextAlign.right, ), Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance! * - ref.watch(priceAnd24hChangeNotifierProvider - .select((value) => - value - .getPrice( - coin) - .item1)), - locale: locale, - decimalPlaces: 2, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + "${(_cachedBalance!.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: locale, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", style: STextStyles.subtitle( context) .copyWith( @@ -1134,23 +1122,19 @@ class _SendViewState extends ConsumerState { // autofill amount field if (results["amount"] != null) { - final amount = + final Amount amount = Decimal.parse(results[ - "amount"]!); + "amount"]!) + .toAmount( + fractionDigits: + coin.decimals, + ); cryptoAmountController .text = - Format + amount .localizedStringAsFixed( - value: amount, - locale: ref - .read( - localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: Constants - .decimalPlacesForCoin( - coin), + locale: locale, ); - amount.toString(); _amountToSend = amount; } @@ -1413,24 +1397,21 @@ class _SendViewState extends ConsumerState { "Private") { cryptoAmountController.text = firoWallet .availablePrivateBalance() - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); + .localizedStringAsFixed( + locale: locale); } else { cryptoAmountController.text = firoWallet .availablePublicBalance() - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); + .localizedStringAsFixed( + locale: locale); } } else { - cryptoAmountController.text = (ref - .read(provider) - .balance - .getSpendable()) - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); + cryptoAmountController.text = ref + .read(provider) + .balance + .spendable + .localizedStringAsFixed( + locale: locale); } _cryptoAmountChanged(); }, @@ -1531,26 +1512,30 @@ class _SendViewState extends ConsumerState { if (baseAmountString.isNotEmpty && baseAmountString != "." && baseAmountString != ",") { - final baseAmount = + final Amount baseAmount = baseAmountString.contains(",") ? Decimal.parse(baseAmountString - .replaceFirst(",", ".")) - : Decimal.parse(baseAmountString); + .replaceFirst(",", ".")) + .toAmount(fractionDigits: 2) + : Decimal.parse(baseAmountString) + .toAmount(fractionDigits: 2); - var _price = ref + final Decimal _price = ref .read(priceAnd24hChangeNotifierProvider) .getPrice(coin) .item1; if (_price == Decimal.zero) { - _amountToSend = Decimal.zero; + _amountToSend = 0.toAmount( + fractionDigits: coin.decimals); } else { - _amountToSend = baseAmount <= Decimal.zero - ? Decimal.zero - : (baseAmount / _price).toDecimal( - scaleOnInfinitePrecision: - Constants.decimalPlacesForCoin( - coin)); + _amountToSend = baseAmount <= Amount.zero + ? 0.toAmount( + fractionDigits: coin.decimals) + : (baseAmount.decimal / _price) + .toDouble() + .toAmount( + fractionDigits: coin.decimals); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { @@ -1562,21 +1547,19 @@ class _SendViewState extends ConsumerState { level: LogLevel.Info); final amountString = - Format.localizedStringAsFixed( - value: _amountToSend!, + _amountToSend!.localizedStringAsFixed( locale: ref .read( localeServiceChangeNotifierProvider) .locale, - decimalPlaces: - Constants.decimalPlacesForCoin(coin), ); _cryptoAmountChangeLock = true; cryptoAmountController.text = amountString; _cryptoAmountChangeLock = false; } else { - _amountToSend = Decimal.zero; + _amountToSend = + 0.toAmount(fractionDigits: coin.decimals); _cryptoAmountChangeLock = true; cryptoAmountController.text = ""; _cryptoAmountChangeLock = false; @@ -1654,13 +1637,9 @@ class _SendViewState extends ConsumerState { .balance .spendable; - int? amount; + Amount? amount; if (_amountToSend != null) { - amount = - Format.decimalAmountToSatoshis( - _amountToSend!, - coin, - ); + amount = _amountToSend!; if (spendable == amount) { // this is now a send all @@ -1803,10 +1782,13 @@ class _SendViewState extends ConsumerState { builder: (_) => TransactionFeeSelectionSheet( walletId: walletId, - amount: Decimal.tryParse( - cryptoAmountController - .text) ?? - Decimal.zero, + amount: (Decimal.tryParse( + cryptoAmountController + .text) ?? + Decimal.zero) + .toAmount( + fractionDigits: coin.decimals, + ), updateChosen: (String fee) { _setCurrentFee( fee, diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart index dc14f2c41..f776a13e0 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -8,7 +7,6 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; class FiroBalanceSelectionSheet extends ConsumerStatefulWidget { const FiroBalanceSelectionSheet({ @@ -153,29 +151,18 @@ class _FiroBalanceSelectionSheetState const SizedBox( width: 2, ), - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future( - () => firoWallet.availablePrivateBalance()), - builder: - (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ); - } else { - return AnimatedText( - stringsToLoopThrough: - stringsToLoopThrough, - style: STextStyles.itemSubtitle(context), - ); - } - }, - ) + Text( + "${firoWallet.availablePrivateBalance().localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${manager.coin.ticker}", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), ], ), // ], @@ -245,31 +232,18 @@ class _FiroBalanceSelectionSheetState const SizedBox( width: 2, ), - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future( - () => firoWallet.availablePublicBalance()), - builder: - (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ); - } else { - return AnimatedText( - stringsToLoopThrough: - stringsToLoopThrough, - style: STextStyles.itemSubtitle(context), - ); - } - }, - ) - // ], - // ), + Text( + "${firoWallet.availablePublicBalance().localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${manager.coin.ticker}", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), ], ), ), diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index c6584ee9f..eca2609a7 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -8,10 +8,10 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -23,9 +23,9 @@ final feeSheetSessionCacheProvider = }); class FeeSheetSessionCache extends ChangeNotifier { - final Map fast = {}; - final Map average = {}; - final Map slow = {}; + final Map fast = {}; + final Map average = {}; + final Map slow = {}; void notify() => notifyListeners(); } @@ -40,7 +40,7 @@ class TransactionFeeSelectionSheet extends ConsumerStatefulWidget { }) : super(key: key); final String walletId; - final Decimal amount; + final Amount amount; final Function updateChosen; final bool isToken; @@ -52,7 +52,7 @@ class TransactionFeeSelectionSheet extends ConsumerStatefulWidget { class _TransactionFeeSelectionSheetState extends ConsumerState { late final String walletId; - late final Decimal amount; + late final Amount amount; FeeObject? feeObject; @@ -63,8 +63,8 @@ class _TransactionFeeSelectionSheetState "Calculating...", ]; - Future feeFor({ - required int amount, + Future feeFor({ + required Amount amount, required FeeRateType feeRateType, required int feeRate, required Coin coin, @@ -79,30 +79,21 @@ class _TransactionFeeSelectionSheetState if (coin == Coin.monero || coin == Coin.wownero) { final fee = await manager.estimateFeeFor( amount, MoneroTransactionPriority.fast.raw!); - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); + ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); } else { ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + await manager.estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = await tokenWallet.estimateFeeFor(amount, feeRate); - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount(fee, coin: coin); + final fee = tokenWallet.estimateFeeFor(feeRate); + ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } } return ref.read(feeSheetSessionCacheProvider).fast[amount]!; @@ -115,30 +106,21 @@ class _TransactionFeeSelectionSheetState if (coin == Coin.monero || coin == Coin.wownero) { final fee = await manager.estimateFeeFor( amount, MoneroTransactionPriority.regular.raw!); - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); + ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); } else { ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + await manager.estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = await tokenWallet.estimateFeeFor(amount, feeRate); - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount(fee, coin: coin); + final fee = tokenWallet.estimateFeeFor(feeRate); + ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } } return ref.read(feeSheetSessionCacheProvider).average[amount]!; @@ -151,30 +133,21 @@ class _TransactionFeeSelectionSheetState if (coin == Coin.monero || coin == Coin.wownero) { final fee = await manager.estimateFeeFor( amount, MoneroTransactionPriority.slow.raw!); - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); + ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); } else { ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + await manager.estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = await tokenWallet.estimateFeeFor(amount, feeRate); - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount(fee, coin: coin); + final fee = tokenWallet.estimateFeeFor(feeRate); + ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } } return ref.read(feeSheetSessionCacheProvider).slow[amount]!; @@ -347,23 +320,25 @@ class _TransactionFeeSelectionSheetState if (feeObject != null) FutureBuilder( future: feeFor( - coin: manager.coin, - feeRateType: FeeRateType.fast, - feeRate: feeObject!.fast, - amount: Format - .decimalAmountToSatoshis( - amount, manager.coin)), + coin: manager.coin, + feeRateType: FeeRateType.fast, + feeRate: feeObject!.fast, + amount: amount, + ), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), // feeObject!.fast), builder: (_, - AsyncSnapshot snapshot) { + AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { return Text( - "(~${snapshot.data!.toStringAsFixed(manager.coin.decimals)} ${manager.coin.ticker})", + "(~${snapshot.data!.decimal.toStringAsFixed( + manager.coin.decimals, + )}" + " ${manager.coin.ticker})", style: STextStyles.itemSubtitle( context), textAlign: TextAlign.left, @@ -479,23 +454,23 @@ class _TransactionFeeSelectionSheetState if (feeObject != null) FutureBuilder( future: feeFor( - coin: manager.coin, - feeRateType: FeeRateType.average, - feeRate: feeObject!.medium, - amount: Format - .decimalAmountToSatoshis( - amount, manager.coin)), + coin: manager.coin, + feeRateType: FeeRateType.average, + feeRate: feeObject!.medium, + amount: amount, + ), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), // feeObject!.fast), builder: (_, - AsyncSnapshot snapshot) { + AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { return Text( - "(~${snapshot.data!.toStringAsFixed(manager.coin.decimals)} ${manager.coin.ticker})", + "(~${snapshot.data!.decimal.toStringAsFixed(manager.coin.decimals)}" + " ${manager.coin.ticker})", style: STextStyles.itemSubtitle( context), textAlign: TextAlign.left, @@ -612,23 +587,22 @@ class _TransactionFeeSelectionSheetState if (feeObject != null) FutureBuilder( future: feeFor( - coin: manager.coin, - feeRateType: FeeRateType.slow, - feeRate: feeObject!.slow, - amount: Format - .decimalAmountToSatoshis( - amount, manager.coin)), + coin: manager.coin, + feeRateType: FeeRateType.slow, + feeRate: feeObject!.slow, + amount: amount, + ), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), // feeObject!.fast), builder: (_, - AsyncSnapshot snapshot) { + AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { return Text( - "(~${snapshot.data!.toStringAsFixed(manager.coin.decimals)} ${manager.coin.ticker})", + "(~${snapshot.data!.decimal.toStringAsFixed(manager.coin.decimals)} ${manager.coin.ticker})", style: STextStyles.itemSubtitle( context), textAlign: TextAlign.left, @@ -686,7 +660,6 @@ class _TransactionFeeSelectionSheetState String? getAmount(FeeRateType feeRateType, Coin coin) { try { - final amount = Format.decimalAmountToSatoshis(this.amount, coin); switch (feeRateType) { case FeeRateType.fast: if (ref.read(feeSheetSessionCacheProvider).fast[amount] != null) { diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 5756cf99b..5eca917b3 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -25,7 +25,6 @@ import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -34,7 +33,6 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/animated_text.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/icon_widgets/addressbook_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; @@ -102,7 +100,7 @@ class _TokenSendViewState extends ConsumerState { Timer? _cryptoAmountChangedFeeUpdateTimer; Timer? _baseAmountChangedFeeUpdateTimer; late Future _calculateFeesFuture; - Map cachedFees = {}; + String cachedFees = ""; void _onTokenSendViewPasteAddressFieldButtonPressed() async { final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); @@ -165,17 +163,13 @@ class _TokenSendViewState extends ConsumerState { // autofill amount field if (results["amount"] != null) { - final amount = Decimal.parse(results["amount"]!); - cryptoAmountController.text = Format.localizedStringAsFixed( - value: amount, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlacesForCoin(coin), - ); - amount.toString(); - _amountToSend = Amount.fromDecimal( - amount, + final Amount amount = Decimal.parse(results["amount"]!).toAmount( fractionDigits: tokenContract.decimals, ); + cryptoAmountController.text = amount.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); + _amountToSend = amount; } _updatePreviewButtonState(_address, _amountToSend); @@ -243,14 +237,10 @@ class _TokenSendViewState extends ConsumerState { Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", level: LogLevel.Info); - final amountString = Format.localizedStringAsFixed( - value: _amountToSend!.decimal, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlacesForCoin(coin), - ); - _cryptoAmountChangeLock = true; - cryptoAmountController.text = amountString; + cryptoAmountController.text = _amountToSend!.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); _cryptoAmountChangeLock = false; } else { _amountToSend = Amount.zero; @@ -291,13 +281,13 @@ class _TokenSendViewState extends ConsumerState { .item1; if (price > Decimal.zero) { - final String fiatAmountString = Format.localizedStringAsFixed( - value: _amountToSend!.decimal * price, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: 2, - ); - - baseAmountController.text = fiatAmountString; + baseAmountController.text = (_amountToSend!.decimal * price) + .toAmount( + fractionDigits: 2, + ) + .localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); } } else { _amountToSend = null; @@ -310,9 +300,7 @@ class _TokenSendViewState extends ConsumerState { _cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { if (coin != Coin.epicCash && !_baseFocus.hasFocus) { setState(() { - _calculateFeesFuture = calculateFees( - _amountToSend == null ? 0 : _amountToSend!.raw.toInt(), - ); + _calculateFeesFuture = calculateFees(); }); } }); @@ -324,9 +312,7 @@ class _TokenSendViewState extends ConsumerState { _baseAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { if (coin != Coin.epicCash && !_cryptoFocus.hasFocus) { setState(() { - _calculateFeesFuture = calculateFees( - _amountToSend == null ? 0 : _amountToSend!.raw.toInt(), - ); + _calculateFeesFuture = calculateFees(); }); } }); @@ -351,15 +337,7 @@ class _TokenSendViewState extends ConsumerState { (isValidAddress && amount != null && amount > Amount.zero); } - Future calculateFees(int amount) async { - if (amount <= 0) { - return "0"; - } - - if (cachedFees[amount] != null) { - return cachedFees[amount]!; - } - + Future calculateFees() async { final wallet = ref.read(tokenServiceProvider)!; final feeObject = await wallet.fees; @@ -377,13 +355,12 @@ class _TokenSendViewState extends ConsumerState { break; } - int fee; + final Amount fee = wallet.estimateFeeFor(feeRate); + cachedFees = fee.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); - fee = await wallet.estimateFeeFor(amount, feeRate); - cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - - return cachedFees[amount]!; + return cachedFees; } Future _previewTransaction() async { @@ -397,10 +374,6 @@ class _TokenSendViewState extends ConsumerState { final tokenWallet = ref.read(tokenServiceProvider)!; final Amount amount = _amountToSend!; - final Amount availableBalance = Amount.fromDecimal( - tokenWallet.balance.getSpendable(), - fractionDigits: tokenContract.decimals, - ); // // confirm send all // if (amount == availableBalance) { @@ -487,7 +460,7 @@ class _TokenSendViewState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( address: _address!, - satoshiAmount: amount.raw.toInt(), + amount: amount, args: { "feeRate": ref.read(feeRateTypeStateProvider), }, @@ -560,7 +533,7 @@ class _TokenSendViewState extends ConsumerState { void initState() { ref.refresh(feeSheetSessionCacheProvider); - _calculateFeesFuture = calculateFees(0); + _calculateFeesFuture = calculateFees(); _data = widget.autoFillData; walletId = widget.walletId; coin = widget.coin; @@ -703,9 +676,13 @@ class _TokenSendViewState extends ConsumerState { cryptoAmountController.text = ref .read(tokenServiceProvider)! .balance - .getSpendable() - .toStringAsFixed( - tokenContract.decimals); + .spendable + .localizedStringAsFixed( + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + ); }, child: Container( color: Colors.transparent, @@ -714,7 +691,20 @@ class _TokenSendViewState extends ConsumerState { CrossAxisAlignment.end, children: [ Text( - "${ref.read(tokenServiceProvider)!.balance.getSpendable().toStringAsFixed(tokenContract.decimals)} ${tokenContract.symbol}", + "${ref.watch( + tokenServiceProvider.select( + (value) => value! + .balance.spendable + .localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + ), + ), + )} ${tokenContract.symbol}", style: STextStyles.titleBold12(context) .copyWith( @@ -723,22 +713,11 @@ class _TokenSendViewState extends ConsumerState { textAlign: TextAlign.right, ), Text( - "${Format.localizedStringAsFixed( - value: ref - .read( - tokenServiceProvider)! - .balance - .getSpendable() * - ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => value - .getTokenPrice( - tokenContract - .address) - .item1)), - locale: locale, - decimalPlaces: 2, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + "${(ref.watch(tokenServiceProvider.select((value) => value!.balance.spendable.decimal)) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: locale, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", style: STextStyles.subtitle(context) .copyWith( fontSize: 8, @@ -1138,9 +1117,14 @@ class _TokenSendViewState extends ConsumerState { TransactionFeeSelectionSheet( walletId: walletId, isToken: true, - amount: Decimal.tryParse( - cryptoAmountController.text) ?? - Decimal.zero, + amount: (Decimal.tryParse( + cryptoAmountController + .text) ?? + Decimal.zero) + .toAmount( + fractionDigits: + tokenContract.decimals, + ), updateChosen: (String fee) { setState(() { _calculateFeesFuture = diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart index 9c398fbf6..edbc9a448 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -7,11 +6,9 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.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/utilities/util.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -144,40 +141,18 @@ class WalletSyncingOptionsView extends ConsumerWidget { const SizedBox( height: 2, ), - FutureBuilder( - future: Future( - () => manager.balance.getTotal()), - builder: (builderContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => - value.locale)), - decimalPlaces: 8, - )} ${manager.coin.ticker}", - style: STextStyles.itemSubtitle( - context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle( - context), - ); - } - }, - ), + Text( + "${manager.balance.total.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${manager.coin.ticker}", + style: + STextStyles.itemSubtitle(context), + ) ], ), const Spacer(), diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index d918adb88..fa01e031b 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -113,7 +113,13 @@ class _MyTokenSelectItemState extends ConsumerState { ), const Spacer(), Text( - "${cachedBalance.getCachedBalance().getTotal()} " + "${cachedBalance.getCachedBalance().total.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} " "${widget.token.symbol}", style: STextStyles.itemSubtitle(context), ), diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 526f80b91..1a75d1ba2 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -15,10 +15,10 @@ import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/price_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; @@ -80,7 +80,13 @@ class TokenSummary extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - "${balance.getTotal()}" + "${balance.total.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )}" " ${token.symbol}", style: STextStyles.pageTitleH1(context), ), @@ -96,17 +102,23 @@ class TokenSummary extends ConsumerWidget { height: 6, ), Text( - "${Format.localizedStringAsFixed( - value: ref - .read(tokenServiceProvider)! - .balance - .getSpendable() * - ref.watch(priceAnd24hChangeNotifierProvider.select( - (value) => value.getTokenPrice(token.address).item1)), - locale: ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: 2, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + "${(balance.total.decimal * ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getTokenPrice(token.address).item1, + ), + )).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + )}", style: STextStyles.subtitle500(context), ), const SizedBox( diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index de9d95f7e..2b41f28b5 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/balance.dart'; @@ -6,6 +5,7 @@ import 'package:stackwallet/providers/providers.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/utilities/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; @@ -112,7 +112,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { BalanceSelector( title: "Available balance", coin: coin, - balance: balance.getSpendable(), + balance: balance.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; @@ -138,7 +138,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { BalanceSelector( title: "Available private balance", coin: coin, - balance: balanceSecondary.getSpendable(), + balance: balanceSecondary.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; @@ -162,7 +162,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { BalanceSelector( title: "Full balance", coin: coin, - balance: balance.getTotal(), + balance: balance.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; @@ -188,7 +188,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { BalanceSelector( title: "Full private balance", coin: coin, - balance: balanceSecondary.getTotal(), + balance: balanceSecondary.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; @@ -217,7 +217,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { } } -class BalanceSelector extends StatelessWidget { +class BalanceSelector extends ConsumerWidget { const BalanceSelector({ Key? key, required this.title, @@ -231,14 +231,14 @@ class BalanceSelector extends StatelessWidget { final String title; final Coin coin; - final Decimal balance; + final Amount balance; final VoidCallback onPressed; final void Function(T?) onChanged; final T value; final T? groupValue; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return RawMaterialButton( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( @@ -278,7 +278,13 @@ class BalanceSelector extends StatelessWidget { height: 2, ), Text( - "${balance.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", + "${balance.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: STextStyles.itemSubtitle12(context).copyWith( color: Theme.of(context) .extension()! diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index 6339692f5..3a8e3ae7e 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -1,26 +1,24 @@ import 'dart:async'; -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/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/event_bus/events/global/balance_refreshed_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/amount.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 '../../../providers/wallet/public_private_balance_state_provider.dart'; - class WalletSummaryInfo extends ConsumerStatefulWidget { const WalletSummaryInfo({ Key? key, @@ -94,7 +92,7 @@ class _WalletSummaryInfoState extends ConsumerState { ref.watch(walletBalanceToggleStateProvider.state).state == WalletBalanceToggleState.available; - final Decimal balanceToShow; + final Amount balanceToShow; String title; if (coin == Coin.firo || coin == Coin.firoTestNet) { @@ -106,12 +104,11 @@ class _WalletSummaryInfoState extends ConsumerState { final bal = _showPrivate ? firoWallet.balancePrivate : firoWallet.balance; - balanceToShow = _showAvailable ? bal.getSpendable() : bal.getTotal(); + balanceToShow = _showAvailable ? bal.spendable : bal.total; title = _showAvailable ? "Available" : "Full"; title += _showPrivate ? " private balance" : " public balance"; } else { - balanceToShow = - _showAvailable ? balance.getSpendable() : balance.getTotal(); + balanceToShow = _showAvailable ? balance.spendable : balance.total; title = _showAvailable ? "Available balance" : "Full balance"; } @@ -151,10 +148,8 @@ class _WalletSummaryInfoState extends ConsumerState { FittedBox( fit: BoxFit.scaleDown, child: SelectableText( - "${Format.localizedStringAsFixed( - value: balanceToShow, + "${balanceToShow.localizedStringAsFixed( locale: locale, - decimalPlaces: 8, )} ${coin.ticker}", style: STextStyles.pageTitleH1(context).copyWith( fontSize: 24, @@ -166,11 +161,11 @@ class _WalletSummaryInfoState extends ConsumerState { ), if (externalCalls) Text( - "${Format.localizedStringAsFixed( - value: priceTuple.item1 * balanceToShow, - locale: locale, - decimalPlaces: 2, - )} $baseCurrency", + "${(priceTuple.item1 * balanceToShow.decimal).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: locale, + )} $baseCurrency", style: STextStyles.subtitle500(context).copyWith( color: Theme.of(context) .extension()! diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index bc4894f0d..b00d94c76 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -111,8 +112,7 @@ class _TransactionDetailsViewState extends ConsumerState { return false; } - if (filter.amount != null && - BigInt.from(filter.amount!) != tx.realAmount.raw) { + if (filter.amount != null && filter.amount! != tx.realAmount) { return false; } @@ -956,10 +956,8 @@ class _DesktopTransactionCardRowState builder: (_) { final amount = _transaction.realAmount; return Text( - "$prefix${Format.localizedStringAsFixed( - value: amount.decimal, + "$prefix${amount.localizedStringAsFixed( locale: locale, - decimalPlaces: coin.decimals, )} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall(context) .copyWith( @@ -980,11 +978,11 @@ class _DesktopTransactionCardRowState final amount = _transaction.realAmount; return Text( - "$prefix${Format.localizedStringAsFixed( - value: amount.decimal * price, - locale: locale, - decimalPlaces: 2, - )} $baseCurrency", + "$prefix${(amount.decimal * price).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: locale, + )} $baseCurrency", style: STextStyles.desktopTextExtraExtraSmall(context), ); }, diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 76a1de1e9..66e505636 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -68,7 +67,7 @@ class _TransactionDetailsViewState late final Coin coin; late final Amount amount; - late final Decimal fee; + late final Amount fee; late final String amountPrefix; late final String unit; late final bool isTokenTx; @@ -83,9 +82,8 @@ class _TransactionDetailsViewState walletId = widget.walletId; coin = widget.coin; - amount = _transaction - .realAmount; //Format.satoshisToAmount(_transaction.amount, coin: coin); - fee = Format.satoshisToAmount(_transaction.fee, coin: coin); + amount = _transaction.realAmount; + fee = _transaction.fee.toAmount(fractionDigits: coin.decimals); if ((coin == Coin.firo || coin == Coin.firoTestNet) && _transaction.subType == TransactionSubType.mint) { @@ -445,15 +443,12 @@ class _TransactionDetailsViewState : CrossAxisAlignment.start, children: [ SelectableText( - "$amountPrefix${Format.localizedStringAsFixed( - value: amount.decimal, + "$amountPrefix${amount.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), - decimalPlaces: - amount.fractionDigits, )} $unit", style: isDesktop ? STextStyles @@ -476,28 +471,26 @@ class _TransactionDetailsViewState .select((value) => value.externalCalls))) SelectableText( - "$amountPrefix${Format.localizedStringAsFixed( - value: amount.decimal * - ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => isTokenTx - ? value - .getTokenPrice( - _transaction - .otherData!) - .item1 - : value - .getPrice( - coin) - .item1), + "$amountPrefix${(amount.decimal * ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => isTokenTx + ? value + .getTokenPrice( + _transaction + .otherData!) + .item1 + : value + .getPrice( + coin) + .item1), + )).toAmount(fractionDigits: 2).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), ), - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => - value.locale), - ), - decimalPlaces: 2, - )} ${ref.watch( + )} ${ref.watch( prefsChangeNotifierProvider .select( (value) => value.currency, @@ -896,24 +889,20 @@ class _TransactionDetailsViewState currentHeight, coin.requiredConfirmations, ) - ? Format.localizedStringAsFixed( - value: fee, + ? fee.localizedStringAsFixed( locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => - value.locale)), - decimalPlaces: - Constants.decimalPlacesForCoin( - coin)) + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + ) : "Pending" - : Format.localizedStringAsFixed( - value: fee, + : fee.localizedStringAsFixed( locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale)), - decimalPlaces: - Constants.decimalPlacesForCoin(coin)); + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + ); if (isTokenTx) { feeString += " ${coin.ticker}"; } @@ -1517,13 +1506,15 @@ class IconCopyButton extends StatelessWidget { ), onPressed: () async { await Clipboard.setData(ClipboardData(text: data)); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } }, child: Padding( padding: const EdgeInsets.all(5), diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index d0e013d83..4297be65a 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/models/transaction_filter.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -75,13 +76,12 @@ class _TransactionSearchViewState _toDateString = _selectedToDate == null ? "" : Format.formatDate(_selectedToDate!); - // TODO: Fix XMR (modify Format.funcs to take optional Coin parameter) - // final amt = Format.satoshisToAmount(widget.coin == Coin.monero ? ) - String amount = ""; - if (filterState.amount != null) { - amount = Format.satoshiAmountToPrettyString(filterState.amount!, - ref.read(localeServiceChangeNotifierProvider).locale, widget.coin); - } + final String amount = filterState.amount?.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: widget.coin.decimals, + ) ?? + ""; + _amountTextEditingController.text = amount; } @@ -966,15 +966,13 @@ class _TransactionSearchViewState Future _onApplyPressed() async { final amountText = _amountTextEditingController.text; - Decimal? amountDecimal; + Amount? amount; if (amountText.isNotEmpty && !(amountText == "," || amountText == ".")) { - amountDecimal = amountText.contains(",") + amount = amountText.contains(",") ? Decimal.parse(amountText.replaceFirst(",", ".")) - : Decimal.parse(amountText); - } - int? amount; - if (amountDecimal != null) { - amount = Format.decimalAmountToSatoshis(amountDecimal, widget.coin); + .toAmount(fractionDigits: widget.coin.decimals) + : Decimal.parse(amountText) + .toAmount(fractionDigits: widget.coin.decimals); } final TransactionFilter filter = TransactionFilter( diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 3cca80566..2fa57bf18 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -1,6 +1,5 @@ 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'; @@ -38,6 +37,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; @@ -346,8 +346,8 @@ class _WalletViewState extends ConsumerState { ); final firoWallet = ref.read(managerProvider).wallet as FiroWallet; - final publicBalance = firoWallet.availablePublicBalance(); - if (publicBalance <= Decimal.zero) { + final Amount publicBalance = firoWallet.availablePublicBalance(); + if (publicBalance <= Amount.zero) { shouldPop = true; if (mounted) { Navigator.of(context).popUntil( diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 318f8e301..bcf84f0cf 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -6,10 +5,10 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -38,8 +37,8 @@ class _FavoriteCardState extends ConsumerState { late final String walletId; late final ChangeNotifierProvider managerProvider; - Decimal _cachedBalance = Decimal.zero; - Decimal _cachedFiatValue = Decimal.zero; + Amount _cachedBalance = Amount.zero; + Amount _cachedFiatValue = Amount.zero; @override void initState() { @@ -223,21 +222,23 @@ class _FavoriteCardState extends ConsumerState { ), FutureBuilder( future: Future(() => ref.watch(managerProvider - .select((value) => value.balance.getTotal()))), - builder: (builderContext, AsyncSnapshot snapshot) { + .select((value) => value.balance.total))), + builder: (builderContext, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { if (snapshot.data != null) { _cachedBalance = snapshot.data!; - if (externalCalls) { - _cachedFiatValue = _cachedBalance * - ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ) - .item1; + if (externalCalls && _cachedBalance > Amount.zero) { + _cachedFiatValue = (_cachedBalance.decimal * + ref + .watch( + priceAnd24hChangeNotifierProvider + .select( + (value) => value.getPrice(coin), + ), + ) + .item1) + .toAmount(fractionDigits: 2); } } } @@ -247,13 +248,13 @@ class _FavoriteCardState extends ConsumerState { FittedBox( fit: BoxFit.scaleDown, child: Text( - "${Format.localizedStringAsFixed( - decimalPlaces: 8, - value: _cachedBalance, + "${_cachedBalance.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), + decimalPlaces: ref.watch(managerProvider + .select((value) => value.coin.decimals)), )} ${coin.ticker}", style: STextStyles.titleBold12(context).copyWith( fontSize: 16, @@ -269,13 +270,12 @@ class _FavoriteCardState extends ConsumerState { ), if (externalCalls) Text( - "${Format.localizedStringAsFixed( - decimalPlaces: 2, - value: _cachedFiatValue, + "${_cachedFiatValue.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), + decimalPlaces: 2, )} ${ref.watch( prefsChangeNotifierProvider .select((value) => value.currency), diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index c2e5991d6..9f6d0f7b7 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -7,10 +7,10 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_sheet/wallets_sheet.dart'; import 'package:stackwallet/pages/wallets_view/eth_wallets_overview.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -108,13 +108,12 @@ class WalletListItem extends ConsumerWidget { final calls = ref.watch(prefsChangeNotifierProvider).externalCalls; - final priceString = Format.localizedStringAsFixed( - value: tuple.item1, - locale: ref - .watch(localeServiceChangeNotifierProvider.notifier) - .locale, - decimalPlaces: 2, - ); + final priceString = tuple.item1 + .toAmount(fractionDigits: 2) + .localizedStringAsFixed( + locale: ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)), + ); final double percentChange = tuple.item2; diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart index bf65a2923..7f79aa1fd 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -7,10 +6,10 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; @@ -37,7 +36,7 @@ class DesktopCoinControlUseDialog extends ConsumerStatefulWidget { }) : super(key: key); final String walletId; - final Decimal? amountToSend; + final Amount? amountToSend; @override ConsumerState createState() => @@ -114,12 +113,16 @@ class _DesktopCoinControlUseDialogState ); } - final selectedSum = Format.satoshisToAmount( - _selectedUTXOs - .map((e) => e.value) - .fold(0, (value, element) => value += element), - coin: coin, - ); + final Amount selectedSum = _selectedUTXOs.map((e) => e.value).fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), + (value, element) => value += Amount( + rawValue: BigInt.from(element), + fractionDigits: coin.decimals, + ), + ); final enableApply = widget.amountToSend == null ? selectedChanged(_selectedUTXOs) @@ -470,7 +473,7 @@ class _DesktopCoinControlUseDialogState ), ), Text( - "${widget.amountToSend!.toStringAsFixed( + "${widget.amountToSend!.decimal.toStringAsFixed( coin.decimals, )}" " ${coin.ticker}", @@ -505,7 +508,7 @@ class _DesktopCoinControlUseDialogState ), ), Text( - "${selectedSum.toStringAsFixed( + "${selectedSum.decimal.toStringAsFixed( coin.decimals, )} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart index 10348f409..04752b53f 100644 --- a/lib/pages_desktop_specific/coin_control/utxo_row.dart +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -4,9 +4,10 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -143,10 +144,16 @@ class _UtxoRowState extends ConsumerState { ), if (!widget.compact) Text( - "${Format.satoshisToAmount( - utxo.value, - coin: coin, - ).toStringAsFixed(coin.decimals)} ${coin.ticker}", + "${Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: coin.decimals, + ).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", textAlign: TextAlign.right, style: STextStyles.w600_14(context), ), @@ -163,10 +170,16 @@ class _UtxoRowState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - "${Format.satoshisToAmount( - utxo.value, - coin: coin, - ).toStringAsFixed(coin.decimals)} ${coin.ticker}", + "${Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: coin.decimals, + ).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", textAlign: TextAlign.right, style: STextStyles.w600_14(context), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index 1c4ef0143..d5c098335 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/notifications_api.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; @@ -182,10 +183,11 @@ class _StepScaffoldState extends ConsumerState { void sendFromStack() { final trade = ref.read(desktopExchangeModelProvider)!.trade!; - final amount = Decimal.parse(trade.payInAmount); final address = trade.payInAddress; - final coin = coinFromTickerCaseInsensitive(trade.payInCurrency); + final amount = Decimal.parse(trade.payInAmount).toAmount( + fractionDigits: coin.decimals, + ); showDialog( context: context, diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart index 2c1d8a44d..76bd0168a 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -6,10 +5,8 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; @@ -268,7 +265,7 @@ class _DesktopChooseFromStackState } } -class BalanceDisplay extends ConsumerStatefulWidget { +class BalanceDisplay extends ConsumerWidget { const BalanceDisplay({ Key? key, required this.walletId, @@ -277,65 +274,19 @@ class BalanceDisplay extends ConsumerStatefulWidget { final String walletId; @override - ConsumerState createState() => _BalanceDisplayState(); -} - -class _BalanceDisplayState extends ConsumerState { - late final String walletId; - - Decimal? _cachedBalance; - - static const loopedText = [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance..." - ]; - - @override - void initState() { - walletId = widget.walletId; - super.initState(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final manager = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId))); final locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale)); - // TODO redo this widget now that its not actually a future - return FutureBuilder( - future: Future(() => manager.balance.getSpendable()), - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - _cachedBalance = snapshot.data; - } - - if (_cachedBalance == null) { - return AnimatedText( - stringsToLoopThrough: loopedText, - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - ); - } else { - return Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance!, - locale: locale, - decimalPlaces: 8, - )} ${manager.coin.ticker}", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textAlign: TextAlign.right, - ); - } - }, + return Text( + "${manager.balance.spendable.localizedStringAsFixed(locale: locale)} " + "${manager.coin.ticker}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle1, + ), + textAlign: TextAlign.right, ); } } diff --git a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart index df24e69c3..77a94e7b2 100644 --- a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart @@ -10,11 +10,11 @@ import 'package:stackwallet/providers/global/price_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -120,22 +120,15 @@ class _DesktopPaynymSendDialogState crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - "${Format.localizedStringAsFixed( - value: !isFiro - ? manager.balance.getSpendable() - : ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" - ? (manager.wallet as FiroWallet) - .availablePrivateBalance() - : (manager.wallet as FiroWallet) - .availablePublicBalance(), - locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", + "${!isFiro ? manager.balance.spendable.localizedStringAsFixed( + locale: locale, + ) : ref.watch( + publicPrivateBalanceStateProvider.state, + ).state == "Private" ? (manager.wallet as FiroWallet).availablePrivateBalance().localizedStringAsFixed( + locale: locale, + ) : (manager.wallet as FiroWallet).availablePublicBalance().localizedStringAsFixed( + locale: locale, + )} ${coin.ticker}", style: STextStyles.titleBold12(context), textAlign: TextAlign.right, ), @@ -143,25 +136,7 @@ class _DesktopPaynymSendDialogState height: 2, ), Text( - "${Format.localizedStringAsFixed( - value: (!isFiro - ? manager.balance.getSpendable() - : ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" - ? (manager.wallet as FiroWallet) - .availablePrivateBalance() - : (manager.wallet as FiroWallet) - .availablePublicBalance()) * - ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin).item1)), - locale: locale, - decimalPlaces: 2, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + "${((!isFiro ? manager.balance.spendable.decimal : ref.watch(publicPrivateBalanceStateProvider.state).state == "Private" ? (manager.wallet as FiroWallet).availablePrivateBalance().decimal : (manager.wallet as FiroWallet).availablePublicBalance().decimal) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount(fractionDigits: 2).localizedStringAsFixed(locale: locale)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", style: STextStyles.baseXS(context).copyWith( color: Theme.of(context) .extension()! diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index 9376654c7..83f50b003 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -3,9 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -205,8 +205,10 @@ class TablePriceInfo extends ConsumerWidget { ), ); - final priceString = Format.localizedStringAsFixed( - value: tuple.item1, + final priceString = Amount.fromDecimal( + tuple.item1, + fractionDigits: 2, + ).localizedStringAsFixed( locale: ref .watch( localeServiceChangeNotifierProvider.notifier, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart index 25e1f47ab..7f9b1e2fe 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart @@ -1,5 +1,4 @@ import 'package:cw_core/monero_transaction_priority.dart'; -import 'package:decimal/decimal.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -10,11 +9,11 @@ import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.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'; @@ -44,8 +43,8 @@ class _DesktopFeeDropDownState extends ConsumerState { "Calculating...", ]; - Future feeFor({ - required int amount, + Future feeFor({ + required Amount amount, required FeeRateType feeRateType, required int feeRate, required Coin coin, @@ -59,24 +58,16 @@ class _DesktopFeeDropDownState extends ConsumerState { if (coin == Coin.monero || coin == Coin.wownero) { final fee = await manager.estimateFeeFor( amount, MoneroTransactionPriority.fast.raw!); - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); + ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); } else { ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + await manager.estimateFeeFor(amount, feeRate); } } return ref.read(feeSheetSessionCacheProvider).fast[amount]!; @@ -89,24 +80,16 @@ class _DesktopFeeDropDownState extends ConsumerState { if (coin == Coin.monero || coin == Coin.wownero) { final fee = await manager.estimateFeeFor( amount, MoneroTransactionPriority.regular.raw!); - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); + ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); } else { ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + await manager.estimateFeeFor(amount, feeRate); } } return ref.read(feeSheetSessionCacheProvider).average[amount]!; @@ -119,24 +102,16 @@ class _DesktopFeeDropDownState extends ConsumerState { if (coin == Coin.monero || coin == Coin.wownero) { final fee = await manager.estimateFeeFor( amount, MoneroTransactionPriority.slow.raw!); - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); + ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); } else { ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + await manager.estimateFeeFor(amount, feeRate); } } return ref.read(feeSheetSessionCacheProvider).slow[amount]!; @@ -242,7 +217,7 @@ class _DesktopFeeDropDownState extends ConsumerState { } final sendAmountProvider = - StateProvider.autoDispose((_) => Decimal.zero); + StateProvider.autoDispose((_) => Amount.zero); class FeeDropDownChild extends ConsumerWidget { const FeeDropDownChild({ @@ -257,8 +232,8 @@ class FeeDropDownChild extends ConsumerWidget { final FeeObject? feeObject; final FeeRateType feeRateType; final String walletId; - final Future Function({ - required int amount, + final Future Function({ + required Amount amount, required FeeRateType feeRateType, required int feeRate, required Coin coin, @@ -322,19 +297,20 @@ class FeeDropDownChild extends ConsumerWidget { : feeRateType == FeeRateType.slow ? feeObject!.slow : feeObject!.medium, - amount: Format.decimalAmountToSatoshis( - ref.watch(sendAmountProvider.state).state, - manager.coin, - ), + amount: ref.watch(sendAmountProvider.state).state, ), - builder: (_, AsyncSnapshot snapshot) { + builder: (_, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${feeRateType.prettyName} (~${snapshot.data!} ${manager.coin.ticker})", + "${feeRateType.prettyName} " + "(~${snapshot.data!.decimal.toStringAsFixed( + manager.coin.decimals, + )} " + "${manager.coin.ticker})", style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( color: Theme.of(context) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 97375f2e7..a988c77bd 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -25,12 +25,12 @@ import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -88,8 +88,8 @@ class _DesktopSendState extends ConsumerState { String? _note; - Decimal? _amountToSend; - Decimal? _cachedAmountToSend; + Amount? _amountToSend; + Amount? _cachedAmountToSend; String? _address; String? _privateBalanceString; @@ -106,20 +106,19 @@ class _DesktopSendState extends ConsumerState { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - final amount = Format.decimalAmountToSatoshis(_amountToSend!, coin); - int availableBalance; + final Amount amount = _amountToSend!; + final Amount availableBalance; if ((coin == Coin.firo || coin == Coin.firoTestNet)) { if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { - availableBalance = Format.decimalAmountToSatoshis( - (manager.wallet as FiroWallet).availablePrivateBalance(), coin); + availableBalance = + (manager.wallet as FiroWallet).availablePrivateBalance(); } else { - availableBalance = Format.decimalAmountToSatoshis( - (manager.wallet as FiroWallet).availablePublicBalance(), coin); + availableBalance = + (manager.wallet as FiroWallet).availablePublicBalance(); } } else { - availableBalance = - Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin); + availableBalance = manager.balance.spendable; } final coinControlEnabled = @@ -268,7 +267,7 @@ class _DesktopSendState extends ConsumerState { final feeRate = ref.read(feeRateTypeStateProvider); txDataFuture = wallet.preparePaymentCodeSend( paymentCode: paymentCode, - satoshiAmount: amount, + amount: amount, args: { "feeRate": feeRate, "UTXOs": (manager.hasCoinControlSupport && @@ -283,7 +282,7 @@ class _DesktopSendState extends ConsumerState { "Private") { txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic( address: _address!, - satoshiAmount: amount, + amount: amount, args: { "feeRate": ref.read(feeRateTypeStateProvider), "UTXOs": (manager.hasCoinControlSupport && @@ -296,7 +295,7 @@ class _DesktopSendState extends ConsumerState { } else { txDataFuture = manager.prepareSend( address: _address!, - satoshiAmount: amount, + amount: amount, args: { "feeRate": ref.read(feeRateTypeStateProvider), "UTXOs": (manager.hasCoinControlSupport && @@ -435,7 +434,9 @@ class _DesktopSendState extends ConsumerState { cryptoAmount != ",") { _amountToSend = cryptoAmount.contains(",") ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) - : Decimal.parse(cryptoAmount); + .toAmount(fractionDigits: coin.decimals) + : Decimal.parse(cryptoAmount) + .toAmount(fractionDigits: coin.decimals); if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -448,11 +449,11 @@ class _DesktopSendState extends ConsumerState { ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (price > Decimal.zero) { - final String fiatAmountString = Format.localizedStringAsFixed( - value: _amountToSend! * price, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: 2, - ); + final String fiatAmountString = (_amountToSend!.decimal * price) + .toAmount(fractionDigits: 2) + .localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); baseAmountController.text = fiatAmountString; } @@ -476,17 +477,17 @@ class _DesktopSendState extends ConsumerState { return null; } - void _updatePreviewButtonState(String? address, Decimal? amount) { + void _updatePreviewButtonState(String? address, Amount? amount) { if (isPaynymSend) { ref.read(previewTxButtonStateProvider.state).state = - (amount != null && amount > Decimal.zero); + (amount != null && amount > Amount.zero); } else { final isValidAddress = ref .read(walletsChangeNotifierProvider) .getManager(walletId) .validateAddress(address ?? ""); ref.read(previewTxButtonStateProvider.state).state = - (isValidAddress && amount != null && amount > Decimal.zero); + (isValidAddress && amount != null && amount > Amount.zero); } } @@ -498,15 +499,16 @@ class _DesktopSendState extends ConsumerState { final wallet = ref.read(provider).wallet as FiroWallet?; if (wallet != null) { - Decimal? balance; + Amount? balance; if (private) { balance = wallet.availablePrivateBalance(); } else { balance = wallet.availablePublicBalance(); } - - return Format.localizedStringAsFixed( - value: balance, locale: locale, decimalPlaces: 8); + return balance.localizedStringAsFixed( + locale: locale, + decimalPlaces: coin.decimals, + ); } return null; @@ -577,9 +579,10 @@ class _DesktopSendState extends ConsumerState { // autofill amount field if (results["amount"] != null) { - final amount = Decimal.parse(results["amount"]!); - cryptoAmountController.text = Format.localizedStringAsFixed( - value: amount, + final amount = Decimal.parse(results["amount"]!).toAmount( + fractionDigits: coin.decimals, + ); + cryptoAmountController.text = amount.localizedStringAsFixed( locale: ref.read(localeServiceChangeNotifierProvider).locale, decimalPlaces: Constants.decimalPlacesForCoin(coin), ); @@ -643,18 +646,20 @@ class _DesktopSendState extends ConsumerState { baseAmountString != ",") { final baseAmount = baseAmountString.contains(",") ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) - : Decimal.parse(baseAmountString); + .toAmount(fractionDigits: 2) + : Decimal.parse(baseAmountString).toAmount(fractionDigits: 2); var _price = ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (_price == Decimal.zero) { - _amountToSend = Decimal.zero; + _amountToSend = Decimal.zero.toAmount(fractionDigits: coin.decimals); } else { - _amountToSend = baseAmount <= Decimal.zero - ? Decimal.zero - : (baseAmount / _price).toDecimal( - scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); + _amountToSend = baseAmount <= Amount.zero + ? Decimal.zero.toAmount(fractionDigits: coin.decimals) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: coin.decimals) + .toAmount(fractionDigits: coin.decimals); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -663,17 +668,16 @@ class _DesktopSendState extends ConsumerState { Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", level: LogLevel.Info); - final amountString = Format.localizedStringAsFixed( - value: _amountToSend!, + final amountString = _amountToSend!.localizedStringAsFixed( locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlacesForCoin(coin), + decimalPlaces: coin.decimals, ); _cryptoAmountChangeLock = true; cryptoAmountController.text = amountString; _cryptoAmountChangeLock = false; } else { - _amountToSend = Decimal.zero; + _amountToSend = Decimal.zero.toAmount(fractionDigits: coin.decimals); _cryptoAmountChangeLock = true; cryptoAmountController.text = ""; _cryptoAmountChangeLock = false; @@ -694,19 +698,24 @@ class _DesktopSendState extends ConsumerState { .wallet as FiroWallet; if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { - cryptoAmountController.text = (firoWallet.availablePrivateBalance()) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cryptoAmountController.text = firoWallet + .availablePrivateBalance() + .decimal + .toStringAsFixed(coin.decimals); } else { - cryptoAmountController.text = (firoWallet.availablePublicBalance()) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cryptoAmountController.text = firoWallet + .availablePublicBalance() + .decimal + .toStringAsFixed(coin.decimals); } } else { - cryptoAmountController.text = (ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .balance - .getSpendable()) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cryptoAmountController.text = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .balance + .spendable + .decimal + .toStringAsFixed(coin.decimals); } } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index ac08c3207..b25dfce46 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -24,7 +24,6 @@ import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -72,7 +71,6 @@ class _DesktopTokenSendState extends ConsumerState { late TextEditingController sendToController; late TextEditingController cryptoAmountController; late TextEditingController baseAmountController; - // late TextEditingController feeController; late final SendViewAutoFillData? _data; @@ -82,8 +80,8 @@ class _DesktopTokenSendState extends ConsumerState { String? _note; - Decimal? _amountToSend; - Decimal? _cachedAmountToSend; + Amount? _amountToSend; + Amount? _cachedAmountToSend; String? _address; bool _addressToggleFlag = false; @@ -94,16 +92,11 @@ class _DesktopTokenSendState extends ConsumerState { Future previewSend() async { final tokenWallet = ref.read(tokenServiceProvider)!; - final amount = Amount.fromDecimal( - _amountToSend!, - fractionDigits: tokenWallet.tokenContract.decimals, - ); - - // todo use Amount class - final availableBalance = tokenWallet.balance.spendable; + final Amount amount = _amountToSend!; + final Amount availableBalance = tokenWallet.balance.spendable; // confirm send all - if (amount.raw.toInt() == availableBalance) { + if (amount == availableBalance) { final bool? shouldSendAll = await showDialog( context: context, useSafeArea: false, @@ -233,7 +226,7 @@ class _DesktopTokenSendState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( address: _address!, - satoshiAmount: amount.raw.toInt(), + amount: amount, args: { "feeRate": ref.read(feeRateTypeStateProvider), }, @@ -361,8 +354,14 @@ class _DesktopTokenSendState extends ConsumerState { cryptoAmount != "." && cryptoAmount != ",") { _amountToSend = cryptoAmount.contains(",") - ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) - : Decimal.parse(cryptoAmount); + ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")).toAmount( + fractionDigits: + ref.read(tokenServiceProvider)!.tokenContract.decimals, + ) + : Decimal.parse(cryptoAmount).toAmount( + fractionDigits: + ref.read(tokenServiceProvider)!.tokenContract.decimals, + ); if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -375,8 +374,10 @@ class _DesktopTokenSendState extends ConsumerState { ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (price > Decimal.zero) { - final String fiatAmountString = Format.localizedStringAsFixed( - value: _amountToSend! * price, + final String fiatAmountString = Amount.fromDecimal( + _amountToSend!.decimal * price, + fractionDigits: 2, + ).localizedStringAsFixed( locale: ref.read(localeServiceChangeNotifierProvider).locale, decimalPlaces: 2, ); @@ -403,13 +404,13 @@ class _DesktopTokenSendState extends ConsumerState { return null; } - void _updatePreviewButtonState(String? address, Decimal? amount) { + void _updatePreviewButtonState(String? address, Amount? amount) { final isValidAddress = ref .read(walletsChangeNotifierProvider) .getManager(walletId) .validateAddress(address ?? ""); ref.read(previewTxButtonStateProvider.state).state = - (isValidAddress && amount != null && amount > Decimal.zero); + (isValidAddress && amount != null && amount > Amount.zero); } Future scanQr() async { @@ -442,12 +443,14 @@ class _DesktopTokenSendState extends ConsumerState { // autofill amount field if (results["amount"] != null) { - final amount = Decimal.parse(results["amount"]!); - cryptoAmountController.text = Format.localizedStringAsFixed( - value: amount, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlacesForCoin(coin), + final amount = Decimal.parse(results["amount"]!).toAmount( + fractionDigits: + ref.read(tokenServiceProvider)!.tokenContract.decimals, ); + cryptoAmountController.text = amount.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); + amount.toString(); _amountToSend = amount; } @@ -498,23 +501,28 @@ class _DesktopTokenSendState extends ConsumerState { } void fiatTextFieldOnChanged(String baseAmountString) { + final int tokenDecimals = + ref.read(tokenServiceProvider)!.tokenContract.decimals; + if (baseAmountString.isNotEmpty && baseAmountString != "." && baseAmountString != ",") { final baseAmount = baseAmountString.contains(",") ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) - : Decimal.parse(baseAmountString); + .toAmount(fractionDigits: 2) + : Decimal.parse(baseAmountString).toAmount(fractionDigits: 2); - var _price = + final Decimal _price = ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (_price == Decimal.zero) { - _amountToSend = Decimal.zero; + _amountToSend = Decimal.zero.toAmount(fractionDigits: tokenDecimals); } else { - _amountToSend = baseAmount <= Decimal.zero - ? Decimal.zero - : (baseAmount / _price).toDecimal( - scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); + _amountToSend = baseAmount <= Amount.zero + ? Decimal.zero.toAmount(fractionDigits: tokenDecimals) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: tokenDecimals) + .toAmount(fractionDigits: tokenDecimals); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -523,17 +531,16 @@ class _DesktopTokenSendState extends ConsumerState { Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", level: LogLevel.Info); - final amountString = Format.localizedStringAsFixed( - value: _amountToSend!, + final amountString = _amountToSend!.localizedStringAsFixed( locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlacesForCoin(coin), + decimalPlaces: tokenDecimals, ); _cryptoAmountChangeLock = true; cryptoAmountController.text = amountString; _cryptoAmountChangeLock = false; } else { - _amountToSend = Decimal.zero; + _amountToSend = Decimal.zero.toAmount(fractionDigits: tokenDecimals); _cryptoAmountChangeLock = true; cryptoAmountController.text = ""; _cryptoAmountChangeLock = false; @@ -543,9 +550,14 @@ class _DesktopTokenSendState extends ConsumerState { } Future sendAllTapped() async { - cryptoAmountController.text = - (ref.read(tokenServiceProvider)!.balance.getSpendable()) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cryptoAmountController.text = ref + .read(tokenServiceProvider)! + .balance + .spendable + .decimal + .toStringAsFixed( + ref.read(tokenServiceProvider)!.tokenContract.decimals, + ); } @override diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index da5fd5745..b355f75cc 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -18,6 +17,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -162,7 +162,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { final firoWallet = ref.read(managerProvider).wallet as FiroWallet; final publicBalance = firoWallet.availablePublicBalance(); - if (publicBalance <= Decimal.zero) { + if (publicBalance <= Amount.zero) { shouldPop = true; if (context.mounted) { Navigator.of(context, rootNavigator: true).pop(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index 75937a7d8..45736a6f0 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/balance.dart'; @@ -9,9 +8,9 @@ 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/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/amount.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'; @@ -86,7 +85,7 @@ class _WDesktopWalletSummaryState extends ConsumerState { : ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId).balance)); - Decimal balanceToShow; + Amount balanceToShow; if (coin == Coin.firo || coin == Coin.firoTestNet) { Balance? balanceSecondary = ref .watch( @@ -101,18 +100,16 @@ class _WDesktopWalletSummaryState extends ConsumerState { WalletBalanceToggleState.available; if (_showAvailable) { - balanceToShow = showPrivate - ? balanceSecondary!.getSpendable() - : balance.getSpendable(); - } else { balanceToShow = - showPrivate ? balanceSecondary!.getTotal() : balance.getTotal(); + showPrivate ? balanceSecondary!.spendable : balance.spendable; + } else { + balanceToShow = showPrivate ? balanceSecondary!.total : balance.total; } } else { if (_showAvailable) { - balanceToShow = balance.getSpendable(); + balanceToShow = balance.spendable; } else { - balanceToShow = balance.getTotal(); + balanceToShow = balance.total; } } @@ -127,8 +124,7 @@ class _WDesktopWalletSummaryState extends ConsumerState { FittedBox( fit: BoxFit.scaleDown, child: Text( - "${Format.localizedStringAsFixed( - value: balanceToShow, + "${balanceToShow.localizedStringAsFixed( locale: locale, decimalPlaces: decimalPlaces, )} $unit", @@ -137,8 +133,10 @@ class _WDesktopWalletSummaryState extends ConsumerState { ), if (externalCalls) Text( - "${Format.localizedStringAsFixed( - value: priceTuple.item1 * balanceToShow, + "${Amount.fromDecimal( + priceTuple.item1 * balanceToShow.decimal, + fractionDigits: 2, + ).localizedStringAsFixed( locale: locale, decimalPlaces: 2, )} $baseCurrency", diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 9ef8bc2aa..985c58f12 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -144,6 +143,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncin 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/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; @@ -1280,7 +1280,7 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case SendFromView.routeName: - if (args is Tuple4) { + if (args is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => SendFromView( diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index ce5a76a7b..809b701a5 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -51,8 +51,14 @@ import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 1; -const int DUST_LIMIT = 294; -const int DUST_LIMIT_P2PKH = 546; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(294), + fractionDigits: Coin.particl.decimals, +); +final Amount DUST_LIMIT_P2PKH = Amount( + rawValue: BigInt.from(546), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; @@ -155,7 +161,7 @@ class BitcoinWallet extends CoinServiceAPI // checkChangeAddressForTransactions: // _checkP2PKHChangeAddressForTransactions, addDerivation: addDerivation, - dustLimitP2PKH: DUST_LIMIT_P2PKH, + dustLimitP2PKH: DUST_LIMIT_P2PKH.raw.toInt(), minConfirms: MINIMUM_CONFIRMATIONS, ); } @@ -1119,7 +1125,7 @@ class BitcoinWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1149,14 +1155,14 @@ class BitcoinWallet extends CoinServiceAPI // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final txData = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1468,9 +1474,18 @@ class BitcoinWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2404,8 +2419,11 @@ class BitcoinWallet extends CoinServiceAPI feeRatePerKB: selectedTxFeeRate, ); - final int roughEstimate = - roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + final int roughEstimate = roughFeeEstimate( + spendableOutputs.length, + 1, + selectedTxFeeRate, + ).raw.toInt(); if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -2476,7 +2494,7 @@ class BitcoinWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2484,7 +2502,7 @@ class BitcoinWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -3063,22 +3081,28 @@ class BitcoinWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance += Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3087,31 +3111,35 @@ class BitcoinWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; } } - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3125,7 +3153,11 @@ class BitcoinWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index d07dbc8f0..777791555 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -47,7 +47,10 @@ import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 0; -const int DUST_LIMIT = 546; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(546), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; @@ -212,10 +215,7 @@ class BitcoinCashWallet extends CoinServiceAPI @override Future get maxFee async { - final fee = (await fees).fast; - final satsFee = Format.satoshisToAmount(fee, coin: coin) * - Decimal.fromInt(Constants.satsPerCoin(coin)); - return satsFee.floor().toBigInt().toInt(); + throw UnimplementedError("Not used in bch"); } @override @@ -1049,7 +1049,7 @@ class BitcoinCashWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1078,14 +1078,14 @@ class BitcoinCashWallet extends CoinServiceAPI } // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final result = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1423,9 +1423,18 @@ class BitcoinCashWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2156,12 +2165,27 @@ class BitcoinCashWallet extends CoinServiceAPI Set inputAddresses = {}; Set outputAddresses = {}; - int totalInputValue = 0; - int totalOutputValue = 0; + Amount totalInputValue = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); + Amount totalOutputValue = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); - int amountSentFromWallet = 0; - int amountReceivedInWallet = 0; - int changeAmount = 0; + Amount amountSentFromWallet = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); + Amount amountReceivedInWallet = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); + Amount changeAmount = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); // parse inputs for (final input in txData["vin"] as List) { @@ -2178,13 +2202,13 @@ class BitcoinCashWallet extends CoinServiceAPI // check matching output if (prevOut == output["n"]) { // get value - final value = Format.decimalAmountToSatoshis( + final value = Amount.fromDecimal( Decimal.parse(output["value"].toString()), - coin, + fractionDigits: coin.decimals, ); // add value to total - totalInputValue += value; + totalInputValue = totalInputValue + value; // get input(prevOut) address final address = @@ -2197,7 +2221,7 @@ class BitcoinCashWallet extends CoinServiceAPI // if input was from my wallet, add value to amount sent if (receivingAddresses.contains(address) || changeAddresses.contains(address)) { - amountSentFromWallet += value; + amountSentFromWallet = amountSentFromWallet + value; } } } @@ -2207,9 +2231,9 @@ class BitcoinCashWallet extends CoinServiceAPI // parse outputs for (final output in txData["vout"] as List) { // get value - final value = Format.decimalAmountToSatoshis( + final value = Amount.fromDecimal( Decimal.parse(output["value"].toString()), - coin, + fractionDigits: coin.decimals, ); // add value to total @@ -2246,7 +2270,7 @@ class BitcoinCashWallet extends CoinServiceAPI txData["address"] as isar_models.Address; isar_models.TransactionType type; - int amount; + Amount amount; if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) { // tx is sent to self type = isar_models.TransactionType.sentToSelf; @@ -2301,10 +2325,10 @@ class BitcoinCashWallet extends CoinServiceAPI scriptPubKeyAddress: json["scriptPubKey"]?["addresses"]?[0] as String? ?? json['scriptPubKey']['type'] as String, - value: Format.decimalAmountToSatoshis( + value: Amount.fromDecimal( Decimal.parse(json["value"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); outputs.add(output); } @@ -2316,12 +2340,9 @@ class BitcoinCashWallet extends CoinServiceAPI (DateTime.now().millisecondsSinceEpoch ~/ 1000), type: type, subType: isar_models.TransactionSubType.none, - amount: amount, - amountString: Amount( - rawValue: BigInt.from(amount), - fractionDigits: coin.decimals, - ).toJsonString(), - fee: fee, + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: fee.raw.toInt(), height: txData["height"] as int?, isCancelled: false, isLelantus: false, @@ -2548,7 +2569,7 @@ class BitcoinCashWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2556,7 +2577,7 @@ class BitcoinCashWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > 546 satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -3117,22 +3138,28 @@ class BitcoinCashWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance += Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3141,19 +3168,19 @@ class BitcoinCashWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; @@ -3161,12 +3188,15 @@ class BitcoinCashWallet extends CoinServiceAPI } // TODO: correct formula for bch? - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from(((181 * inputCount) + (34 * outputCount) + 10) * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3180,7 +3210,11 @@ class BitcoinCashWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 4f1013ec1..1204859cc 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -244,7 +245,7 @@ abstract class CoinServiceAPI { Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }); @@ -299,7 +300,7 @@ abstract class CoinServiceAPI { bool get isConnected; - Future estimateFeeFor(int satoshiAmount, int feeRate); + Future estimateFeeFor(Amount amount, int feeRate); Future generateNewAddress(); diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 3c12aada5..15e20f62c 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -8,7 +8,6 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoindart/bitcoindart.dart'; import 'package:bitcoindart/bitcoindart.dart' as btc_dart; import 'package:bs58check/bs58check.dart' as bs58check; -import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; @@ -49,7 +48,10 @@ import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 1; -const int DUST_LIMIT = 1000000; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(1000000), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691"; @@ -227,10 +229,7 @@ class DogecoinWallet extends CoinServiceAPI @override Future get maxFee async { - final fee = (await fees).fast; - final satsFee = Format.satoshisToAmount(fee, coin: coin) * - Decimal.fromInt(Constants.satsPerCoin(coin)); - return satsFee.floor().toBigInt().toInt(); + throw UnimplementedError("Not used in dogecoin"); } @override @@ -915,7 +914,7 @@ class DogecoinWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -944,14 +943,14 @@ class DogecoinWallet extends CoinServiceAPI } // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final result = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1250,9 +1249,18 @@ class DogecoinWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2241,7 +2249,7 @@ class DogecoinWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2249,7 +2257,7 @@ class DogecoinWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > 546 satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -2830,22 +2838,29 @@ class DogecoinWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance = runningBalance + + Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -2854,19 +2869,19 @@ class DogecoinWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; @@ -2874,12 +2889,15 @@ class DogecoinWallet extends CoinServiceAPI } // TODO: correct formula for doge? - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from(((181 * inputCount) + (34 * outputCount) + 10) * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -2893,7 +2911,11 @@ class DogecoinWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 419ab5c02..61682b7e1 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -33,7 +33,6 @@ import 'package:stackwallet/utilities/default_epicboxes.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; @@ -857,20 +856,23 @@ class EpicCashWallet extends CoinServiceAPI ); @override - Future> prepareSend( - {required String address, - required int satoshiAmount, - Map? args}) async { + Future> prepareSend({ + required String address, + required Amount amount, + Map? args, + }) async { try { - int realfee = await nativeFee(satoshiAmount); - if (balance.spendable == satoshiAmount) { - satoshiAmount = balance.spendable - realfee; + int satAmount = amount.raw.toInt(); + int realfee = await nativeFee(satAmount); + + if (balance.spendable == amount) { + satAmount = balance.spendable.raw.toInt() - realfee; } Map txData = { "fee": realfee, "addresss": address, - "recipientAmt": satoshiAmount, + "recipientAmt": satAmount, }; Logging.instance.log("prepare send: $txData", level: LogLevel.Info); @@ -1939,9 +1941,13 @@ class EpicCashWallet extends CoinServiceAPI bool isActive = false; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - int currentFee = await nativeFee(satoshiAmount, ifErrorEstimateFee: true); - return currentFee; + Future estimateFeeFor(Amount amount, int feeRate) async { + int currentFee = + await nativeFee(amount.raw.toInt(), ifErrorEstimateFee: true); + return Amount( + rawValue: BigInt.from(currentFee), + fractionDigits: coin.decimals, + ); } // not used in epic currently @@ -1973,18 +1979,21 @@ class EpicCashWallet extends CoinServiceAPI _balance = Balance( coin: coin, - total: Format.decimalAmountToSatoshis( + total: Amount.fromDecimal( Decimal.parse(total) + Decimal.parse(awaiting), - coin, + fractionDigits: coin.decimals, ), - spendable: Format.decimalAmountToSatoshis( + spendable: Amount.fromDecimal( Decimal.parse(spendable), - coin, + fractionDigits: coin.decimals, ), - blockedTotal: 0, - pendingSpendable: Format.decimalAmountToSatoshis( + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), + pendingSpendable: Amount.fromDecimal( Decimal.parse(pending), - coin, + fractionDigits: coin.decimals, ), ); diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 5c93e6a13..ac05dacb3 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:bip39/bip39.dart' as bip39; -import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; @@ -34,7 +33,6 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:tuple/tuple.dart'; @@ -88,15 +86,27 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { if (jsonString == null) { return TokenBalance( contractAddress: contract.address, - decimalPlaces: contract.decimals, - total: 0, - spendable: 0, - blockedTotal: 0, - pendingSpendable: 0, + total: Amount( + rawValue: BigInt.zero, + fractionDigits: contract.decimals, + ), + spendable: Amount( + rawValue: BigInt.zero, + fractionDigits: contract.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: contract.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: contract.decimals, + ), ); } return TokenBalance.fromJson( jsonString, + contract.decimals, ); } @@ -214,18 +224,29 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { // 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, + total: Amount.fromDouble( + ethBalance.getValueInUnit(web3.EtherUnit.ether), + fractionDigits: coin.decimals, + ), + spendable: Amount.fromDouble( + ethBalance.getValueInUnit(web3.EtherUnit.ether), + fractionDigits: coin.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), ); await updateCachedBalance(_balance!); } @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - final fee = estimateFee(feeRate, _gasLimit, coin.decimals); - return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin); + Future estimateFeeFor(Amount amount, int feeRate) async { + return estimateFee(feeRate, _gasLimit, coin.decimals); } @override @@ -370,9 +391,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future get maxFee async { - final fee = (await fees).fast; - final feeEstimate = await estimateFeeFor(0, fee); - return feeEstimate; + throw UnimplementedError("Not used for eth"); } @override @@ -421,10 +440,11 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { } @override - Future> prepareSend( - {required String address, - required int satoshiAmount, - Map? args}) async { + Future> prepareSend({ + required String address, + required Amount amount, + Map? args, + }) async { final feeRateType = args?["feeRate"]; int fee = 0; final feeObject = await fees; @@ -440,7 +460,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { break; } - final feeEstimate = await estimateFeeFor(satoshiAmount, fee); + final feeEstimate = await estimateFeeFor(amount, fee); // bool isSendAll = false; // final availableBalance = balance.spendable; @@ -453,12 +473,6 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { // satoshiAmount -= feeEstimate; // } - final decimalAmount = Format.satoshisToAmount(satoshiAmount, coin: coin); - final bigIntAmount = amountToBigInt( - decimalAmount.toDouble(), - Constants.decimalPlacesForCoin(coin), - ); - final client = getEthClient(); final myAddress = await currentReceivingAddress; @@ -472,7 +486,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { fee, ), amountOfGas: BigInt.from(_gasLimit), - value: web3.EtherAmount.inWei(bigIntAmount), + value: web3.EtherAmount.inWei(amount.raw), ); final nonce = args?["nonce"] as int? ?? @@ -494,7 +508,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { fee, ), maxGas: _gasLimit, - value: web3.EtherAmount.inWei(bigIntAmount), + value: web3.EtherAmount.inWei(amount.raw), nonce: nonce, ); @@ -502,7 +516,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { "fee": feeEstimate, "feeInWei": fee, "address": address, - "recipientAmt": satoshiAmount, + "recipientAmt": amount, "ethTx": tx, "chainId": (await client.getChainId()).toInt(), "nonce": tx.nonce, diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index e973f3092..aa39eb47b 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -707,7 +707,10 @@ Future isolateCreateJoinSplitTransaction( "txid": txId, "txHex": txHex, "value": amount, - "fees": Format.satoshisToAmount(fee, coin: coin).toDouble(), + "fees": Amount( + rawValue: BigInt.from(fee), + fractionDigits: coin.decimals, + ).decimal.toDouble(), "fee": fee, "vSize": extTx.virtualSize(), "jmintValue": changeToMint, @@ -716,7 +719,10 @@ Future isolateCreateJoinSplitTransaction( "height": locktime, "txType": "Sent", "confirmed_status": false, - "amount": Format.satoshisToAmount(amount, coin: coin).toDouble(), + "amount": Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.decimals, + ).decimal.toDouble(), "recipientAmt": amount, "address": address, "timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000, @@ -1030,7 +1036,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Future> prepareSendPublic({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1059,14 +1065,17 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { // check for send all bool isSendAll = false; - final balance = - Format.decimalAmountToSatoshis(availablePublicBalance(), coin); - if (satoshiAmount == balance) { + final balance = availablePublicBalance(); + if (amount == balance) { isSendAll = true; } - final txData = - await coinSelection(satoshiAmount, rate, address, isSendAll); + final txData = await coinSelection( + amount.raw.toInt(), + rate, + address, + isSendAll, + ); Logging.instance.log("prepare send: $txData", level: LogLevel.Info); try { @@ -1139,20 +1148,22 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { // check for send all bool isSendAll = false; - final balance = - Format.decimalAmountToSatoshis(availablePrivateBalance(), coin); - if (satoshiAmount == balance) { + final balance = availablePrivateBalance(); + if (amount == balance) { // print("is send all"); isSendAll = true; } - dynamic txHexOrError = - await _createJoinSplitTransaction(satoshiAmount, address, isSendAll); + dynamic txHexOrError = await _createJoinSplitTransaction( + amount.raw.toInt(), + address, + isSendAll, + ); Logging.instance.log("txHexOrError $txHexOrError", level: LogLevel.Error); if (txHexOrError is int) { // Here, we assume that transaction crafting returned an error @@ -2305,9 +2316,10 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { Future _fetchMaxFee() async { final balance = availablePrivateBalance(); - int spendAmount = (balance * Decimal.fromInt(Constants.satsPerCoin(coin))) - .toBigInt() - .toInt(); + int spendAmount = + (balance.decimal * Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); int fee = await estimateJoinSplitFee(spendAmount); return fee; } @@ -2503,10 +2515,23 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { _balancePrivate = Balance( coin: coin, - total: intLelantusBalance + unconfirmedLelantusBalance, - spendable: intLelantusBalance, - blockedTotal: 0, - pendingSpendable: unconfirmedLelantusBalance, + total: Amount( + rawValue: + BigInt.from(intLelantusBalance + unconfirmedLelantusBalance), + fractionDigits: coin.decimals, + ), + spendable: Amount( + rawValue: BigInt.from(intLelantusBalance), + fractionDigits: coin.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.from(unconfirmedLelantusBalance), + fractionDigits: coin.decimals, + ), ); await updateCachedBalanceSecondary(_balancePrivate!); @@ -2605,8 +2630,10 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { final feesObject = await fees; - final Decimal fastFee = - Format.satoshisToAmount(feesObject.fast, coin: coin); + final Decimal fastFee = Amount( + rawValue: BigInt.from(feesObject.fast), + fractionDigits: coin.decimals, + ).decimal; int firoFee = (dvSize * fastFee * Decimal.fromInt(100000)).toDouble().ceil(); // int firoFee = (vSize * feesObject.fast * (1 / 1000.0) * 100000000).ceil(); @@ -2795,12 +2822,18 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { "txid": txId, "txHex": txHex, "value": amount - fee, - "fees": Format.satoshisToAmount(fee, coin: coin).toDouble(), + "fees": Amount( + rawValue: BigInt.from(fee), + fractionDigits: coin.decimals, + ).decimal.toDouble(), "publicCoin": "", "height": height, "txType": "Sent", "confirmed_status": false, - "amount": Format.satoshisToAmount(amount, coin: coin).toDouble(), + "amount": Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.decimals, + ).decimal.toDouble(), "timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000, "subType": "mint", "mintsMap": mintsMap, @@ -3040,9 +3073,9 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { } await firoUpdateLelantusCoins(coins); - final amount = Format.decimalAmountToSatoshis( + final amount = Amount.fromDecimal( Decimal.parse(transactionInfo["amount"].toString()), - coin, + fractionDigits: coin.decimals, ); // add the send transaction @@ -3059,15 +3092,12 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { : transactionInfo["subType"] == "join" ? isar_models.TransactionSubType.join : isar_models.TransactionSubType.none, - amount: amount, - amountString: Amount( - rawValue: BigInt.from(amount), - fractionDigits: Coin.firo.decimals, - ).toJsonString(), - fee: Format.decimalAmountToSatoshis( + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: Amount.fromDecimal( Decimal.parse(transactionInfo["fees"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), height: transactionInfo["height"] as int?, isCancelled: false, isLelantus: true, @@ -3154,9 +3184,18 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -3608,10 +3647,10 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { scriptPubKeyAddress: json["scriptPubKey"]?["addresses"]?[0] as String? ?? json['scriptPubKey']['type'] as String, - value: Format.decimalAmountToSatoshis( + value: Amount.fromDecimal( Decimal.parse(json["value"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); outs.add(output); } @@ -3692,10 +3731,22 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { final currentChainHeight = await chainHeight; final List outputArray = []; - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; + Amount satoshiBalanceTotal = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount satoshiBalancePending = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount satoshiBalanceSpendable = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount satoshiBalanceBlocked = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { @@ -3720,15 +3771,19 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { blockTime: txn["blocktime"] as int?, ); - satoshiBalanceTotal += utxo.value; + final utxoAmount = Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: coin.decimals, + ); + satoshiBalanceTotal = satoshiBalanceTotal + utxoAmount; if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; + satoshiBalanceBlocked = satoshiBalanceBlocked + utxoAmount; } else { if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - satoshiBalanceSpendable += utxo.value; + satoshiBalanceSpendable = satoshiBalanceSpendable + utxoAmount; } else { - satoshiBalancePending += utxo.value; + satoshiBalancePending = satoshiBalancePending + utxoAmount; } } @@ -4751,7 +4806,7 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { int spendAmount, ) async { var lelantusEntry = await _getLelantusEntry(); - final balance = availablePrivateBalance(); + final balance = availablePrivateBalance().decimal; int spendAmount = (balance * Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); @@ -4808,27 +4863,34 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { // return fee; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - int fee = await estimateJoinSplitFee(satoshiAmount); - return fee; + Future estimateFeeFor(Amount amount, int feeRate) async { + int fee = await estimateJoinSplitFee(amount.raw.toInt()); + return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals); } - Future estimateFeeForPublic(int satoshiAmount, int feeRate) async { + Future estimateFeeForPublic(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance = runningBalance + + Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -4837,19 +4899,24 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; - if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + final dustLimitAmount = Amount( + rawValue: BigInt.from(DUST_LIMIT), + fractionDigits: coin.decimals, + ); + + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + dustLimitAmount) { + final change = runningBalance - amount - twoOutPutFee; + if (change > dustLimitAmount && + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; @@ -4857,12 +4924,15 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { } // TODO: correct formula for firo? - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from(((181 * inputCount) + (34 * outputCount) + 10) * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -4876,7 +4946,11 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } Future>> fastFetch(List allTxHashes) async { @@ -4949,9 +5023,9 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { tx["address"] = tx["vout"][sendIndex]["scriptPubKey"]["addresses"][0]; tx["fees"] = tx["vin"][0]["nFees"]; - final amount = Format.decimalAmountToSatoshis( + final Amount amount = Amount.fromDecimal( Decimal.parse(tx["amount"].toString()), - coin, + fractionDigits: coin.decimals, ); final txn = isar_models.Transaction( @@ -4961,15 +5035,12 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { (DateTime.now().millisecondsSinceEpoch ~/ 1000), type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.join, - amount: amount, - amountString: Amount( - rawValue: BigInt.from(amount), - fractionDigits: Coin.firo.decimals, - ).toJsonString(), - fee: Format.decimalAmountToSatoshis( + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: Amount.fromDecimal( Decimal.parse(tx["fees"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), height: tx["height"] as int?, isCancelled: false, isLelantus: true, @@ -5037,12 +5108,12 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { } } - Decimal availablePrivateBalance() { - return balancePrivate.getSpendable(); + Amount availablePrivateBalance() { + return balancePrivate.spendable; } - Decimal availablePublicBalance() { - return balance.getSpendable(); + Amount availablePublicBalance() { + return balance.spendable; } Future get chainHeight async { diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 7feead18f..d9063e07d 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -47,8 +47,14 @@ import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 1; -const int DUST_LIMIT = 294; -const int DUST_LIMIT_P2PKH = 546; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(294), + fractionDigits: Coin.particl.decimals, +); +final Amount DUST_LIMIT_P2PKH = Amount( + rawValue: BigInt.from(546), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2"; @@ -1026,7 +1032,7 @@ class LitecoinWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1056,14 +1062,14 @@ class LitecoinWallet extends CoinServiceAPI // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final txData = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1423,9 +1429,18 @@ class LitecoinWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2354,8 +2369,11 @@ class LitecoinWallet extends CoinServiceAPI feeRatePerKB: selectedTxFeeRate, ); - final int roughEstimate = - roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + final int roughEstimate = roughFeeEstimate( + spendableOutputs.length, + 1, + selectedTxFeeRate, + ).raw.toInt(); if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -2412,7 +2430,7 @@ class LitecoinWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2420,7 +2438,7 @@ class LitecoinWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -3229,22 +3247,29 @@ class LitecoinWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance = runningBalance + + Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3253,31 +3278,35 @@ class LitecoinWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; } } - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3291,7 +3320,11 @@ class LitecoinWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index 408d2707a..716abb606 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/services/event_bus/events/global/updated_in_backgrou import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -91,13 +92,13 @@ class Manager with ChangeNotifier { Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { final txInfo = await _currentWallet.prepareSend( address: address, - satoshiAmount: satoshiAmount, + amount: amount, args: args, ); // notifyListeners(); @@ -214,8 +215,8 @@ class Manager with ChangeNotifier { bool get isConnected => _currentWallet.isConnected; - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - return _currentWallet.estimateFeeFor(satoshiAmount, feeRate); + Future estimateFeeFor(Amount amount, int feeRate) async { + return _currentWallet.estimateFeeFor(amount, feeRate); } Future generateNewAddress() async { diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index c0543a36a..ac4f24799 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -39,12 +39,10 @@ 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/utilities/amount.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/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; @@ -171,7 +169,7 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { (await _generateAddressForChain(0, 0)).value; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { MoneroTransactionPriority priority; switch (feeRate) { @@ -193,9 +191,9 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { break; } - final fee = walletBase!.calculateEstimatedFee(priority, satoshiAmount); + final fee = walletBase!.calculateEstimatedFee(priority, amount.raw.toInt()); - return fee; + return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals); } @override @@ -432,7 +430,7 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { String toAddress = address; @@ -457,16 +455,13 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { // check for send all bool isSendAll = false; final balance = await _availableBalance; - if (satoshiAmount == balance) { + if (amount == balance) { isSendAll = true; } Logging.instance - .log("$toAddress $satoshiAmount $args", level: LogLevel.Info); - String amountToSend = - Format.satoshisToAmount(satoshiAmount, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - Logging.instance - .log("$satoshiAmount $amountToSend", level: LogLevel.Info); + .log("$toAddress $amount $args", level: LogLevel.Info); + String amountToSend = amount.decimal.toString(); + Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); monero_output.Output output = monero_output.Output(walletBase!); output.address = toAddress; @@ -488,14 +483,16 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { PendingMoneroTransaction pendingMoneroTransaction = await (awaitPendingTransaction!) as PendingMoneroTransaction; - int realfee = Format.decimalAmountToSatoshis( - Decimal.parse(pendingMoneroTransaction.feeFormatted), coin); - debugPrint("fee? $realfee"); + final int realFee = Amount.fromDecimal( + Decimal.parse(pendingMoneroTransaction.feeFormatted), + fractionDigits: coin.decimals, + ).raw.toInt(); + Map txData = { "pendingMoneroTransaction": pendingMoneroTransaction, - "fee": realfee, + "fee": realFee, "addresss": toAddress, - "recipientAmt": satoshiAmount, + "recipientAmt": amount, }; Logging.instance.log("prepare send: $txData", level: LogLevel.Info); @@ -753,25 +750,34 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { coin: coin, total: total, spendable: available, - blockedTotal: 0, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), pendingSpendable: total - available, ); await updateCachedBalance(_balance!); } - Future get _availableBalance async { + Future get _availableBalance async { try { int runningBalance = 0; for (final entry in walletBase!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } - return runningBalance; + return Amount( + rawValue: BigInt.from(runningBalance), + fractionDigits: coin.decimals, + ); } catch (_) { - return 0; + return Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); } } - Future get _totalBalance async { + Future get _totalBalance async { try { final balanceEntries = walletBase?.balance?.entries; if (balanceEntries != null) { @@ -780,7 +786,10 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { bal = bal + element.value.fullBalance; } await _updateCachedBalance(bal); - return bal; + return Amount( + rawValue: BigInt.from(bal), + fractionDigits: coin.decimals, + ); } else { final transactions = walletBase!.transactionHistory!.transactions; int transactionBalance = 0; @@ -793,10 +802,16 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { } await _updateCachedBalance(transactionBalance); - return transactionBalance; + return Amount( + rawValue: BigInt.from(transactionBalance), + fractionDigits: coin.decimals, + ); } } catch (_) { - return _getCachedBalance(); + return Amount( + rawValue: BigInt.from(_getCachedBalance()), + fractionDigits: coin.decimals, + ); } } diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 55076ad66..df71c55e5 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -48,7 +48,10 @@ import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 2; // Find real dust limit -const int DUST_LIMIT = 546; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(546), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"; @@ -1017,7 +1020,7 @@ class NamecoinWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1047,14 +1050,14 @@ class NamecoinWallet extends CoinServiceAPI // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final txData = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1413,9 +1416,18 @@ class NamecoinWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2347,8 +2359,11 @@ class NamecoinWallet extends CoinServiceAPI feeRatePerKB: selectedTxFeeRate, ); - final int roughEstimate = - roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + final int roughEstimate = roughFeeEstimate( + spendableOutputs.length, + 1, + selectedTxFeeRate, + ).raw.toInt(); if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -2405,7 +2420,7 @@ class NamecoinWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2413,7 +2428,7 @@ class NamecoinWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -3221,22 +3236,29 @@ class NamecoinWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance = runningBalance + + Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3245,19 +3267,19 @@ class NamecoinWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; @@ -3265,12 +3287,16 @@ class NamecoinWallet extends CoinServiceAPI } // TODO: Check if this is the correct formula for namecoin - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3284,7 +3310,11 @@ class NamecoinWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 5550c63d3..cbf3661df 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -30,6 +30,7 @@ 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/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -44,10 +45,11 @@ import 'package:stackwallet/utilities/prefs.dart'; import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; -import '../../../utilities/amount.dart'; - const int MINIMUM_CONFIRMATIONS = 1; -const int DUST_LIMIT = 294; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(294), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"; @@ -945,7 +947,7 @@ class ParticlWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -975,14 +977,14 @@ class ParticlWallet extends CoinServiceAPI // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final txData = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1329,9 +1331,18 @@ class ParticlWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2343,10 +2354,10 @@ class ParticlWallet extends CoinServiceAPI json["scriptPubKey"]?["addresses"]?[0] as String? ?? json['scriptPubKey']?['type'] as String? ?? "", - value: Format.decimalAmountToSatoshis( + value: Amount.fromDecimal( Decimal.parse((json["value"] ?? 0).toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); outputs.add(output); } @@ -2514,8 +2525,11 @@ class ParticlWallet extends CoinServiceAPI feeRatePerKB: selectedTxFeeRate, ); - final int roughEstimate = - roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + final int roughEstimate = roughFeeEstimate( + spendableOutputs.length, + 1, + selectedTxFeeRate, + ).raw.toInt(); if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -2572,7 +2586,7 @@ class ParticlWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2580,7 +2594,7 @@ class ParticlWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -3281,22 +3295,28 @@ class ParticlWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance += Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3305,31 +3325,35 @@ class ParticlWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; } } - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3343,7 +3367,11 @@ class ParticlWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 6a23276b3..cd977f1f8 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -41,12 +41,10 @@ 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/utilities/amount.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/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; @@ -173,7 +171,7 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { (await _generateAddressForChain(0, 0)).value; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { MoneroTransactionPriority priority; FeeRateType feeRateType = FeeRateType.slow; switch (feeRate) { @@ -206,18 +204,28 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { aprox = (await prepareSend( // This address is only used for getting an approximate fee, never for sending address: "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", - satoshiAmount: satoshiAmount, + amount: amount, args: {"feeRate": feeRateType}))['fee']; - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); } catch (e, s) { - aprox = walletBase!.calculateEstimatedFee(priority, satoshiAmount); + aprox = walletBase!.calculateEstimatedFee( + priority, + amount.raw.toInt(), + ); } } }); - print("this is the aprox fee $aprox for $satoshiAmount"); - final fee = (aprox as int); - return fee; + print("this is the aprox fee $aprox for $amount"); + + if (aprox is Amount) { + return aprox as Amount; + } else { + return Amount( + rawValue: BigInt.from(aprox as int), + fractionDigits: coin.decimals, + ); + } } @override @@ -451,7 +459,7 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -475,16 +483,12 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { // check for send all bool isSendAll = false; final balance = await _availableBalance; - if (satoshiAmount == balance) { + if (amount == balance) { isSendAll = true; } - Logging.instance - .log("$address $satoshiAmount $args", level: LogLevel.Info); - String amountToSend = - Format.satoshisToAmount(satoshiAmount, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - Logging.instance - .log("$satoshiAmount $amountToSend", level: LogLevel.Info); + Logging.instance.log("$address $amount $args", level: LogLevel.Info); + String amountToSend = amount.decimal.toString(); + Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); wownero_output.Output output = wownero_output.Output(walletBase!); output.address = address; @@ -507,15 +511,16 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { PendingWowneroTransaction pendingWowneroTransaction = await (awaitPendingTransaction!) as PendingWowneroTransaction; - int realfee = Format.decimalAmountToSatoshis( - Decimal.parse(pendingWowneroTransaction.feeFormatted), coin); - //todo: check if print needed - // debugPrint("fee? $realfee"); + final int realFee = Amount.fromDecimal( + Decimal.parse(pendingWowneroTransaction.feeFormatted), + fractionDigits: coin.decimals, + ).raw.toInt(); + Map txData = { "pendingWowneroTransaction": pendingWowneroTransaction, - "fee": realfee, + "fee": realFee, "addresss": address, - "recipientAmt": satoshiAmount, + "recipientAmt": amount, }; Logging.instance.log("prepare send: $txData", level: LogLevel.Info); @@ -772,25 +777,34 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { coin: coin, total: total, spendable: available, - blockedTotal: 0, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), pendingSpendable: total - available, ); await updateCachedBalance(_balance!); } - Future get _availableBalance async { + Future get _availableBalance async { try { int runningBalance = 0; for (final entry in walletBase!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } - return runningBalance; + return Amount( + rawValue: BigInt.from(runningBalance), + fractionDigits: coin.decimals, + ); } catch (_) { - return 0; + return Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); } } - Future get _totalBalance async { + Future get _totalBalance async { try { final balanceEntries = walletBase?.balance?.entries; if (balanceEntries != null) { @@ -799,7 +813,10 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { bal = bal + element.value.fullBalance; } await _updateCachedBalance(bal); - return bal; + return Amount( + rawValue: BigInt.from(bal), + fractionDigits: coin.decimals, + ); } else { final transactions = walletBase!.transactionHistory!.transactions; int transactionBalance = 0; @@ -812,10 +829,16 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { } await _updateCachedBalance(transactionBalance); - return transactionBalance; + return Amount( + rawValue: BigInt.from(transactionBalance), + fractionDigits: coin.decimals, + ); } } catch (_) { - return _getCachedBalance(); + return Amount( + rawValue: BigInt.from(_getCachedBalance()), + fractionDigits: coin.decimals, + ); } } diff --git a/lib/services/ethereum/cached_eth_token_balance.dart b/lib/services/ethereum/cached_eth_token_balance.dart index d5295070e..893ec557c 100644 --- a/lib/services/ethereum/cached_eth_token_balance.dart +++ b/lib/services/ethereum/cached_eth_token_balance.dart @@ -2,6 +2,7 @@ import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/token_balance.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.dart'; import 'package:stackwallet/services/mixins/eth_token_cache.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/logger.dart'; class CachedEthTokenBalance with EthTokenCache { @@ -22,11 +23,22 @@ class CachedEthTokenBalance with EthTokenCache { await updateCachedBalance( TokenBalance( contractAddress: token.address, - decimalPlaces: token.decimals, - total: response.value!, - spendable: response.value!, - blockedTotal: 0, - pendingSpendable: 0, + total: Amount( + rawValue: BigInt.from(response.value!), + fractionDigits: token.decimals, + ), + spendable: Amount( + rawValue: BigInt.from(response.value!), + fractionDigits: token.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: token.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: token.decimals, + ), ), ); } else { diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index 81bd6ed86..c0405df89 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -28,7 +28,6 @@ import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/extensions/impl/contract_abi.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:tuple/tuple.dart'; import 'package:web3dart/web3dart.dart' as web3dart; @@ -68,7 +67,7 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { final feeRateType = args?["feeRate"]; @@ -86,12 +85,7 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { break; } - final feeEstimate = await estimateFeeFor(satoshiAmount, fee); - - final decimalAmount = - Format.satoshisToAmount(satoshiAmount, coin: Coin.ethereum); - final bigIntAmount = - amountToBigInt(decimalAmount.toDouble(), tokenContract.decimals); + final feeEstimate = estimateFeeFor(fee); final client = await getEthClient(); @@ -101,8 +95,8 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { final est = await client.estimateGas( sender: myWeb3Address, to: web3dart.EthereumAddress.fromHex(address), - data: _sendFunction.encodeCall( - [web3dart.EthereumAddress.fromHex(address), bigIntAmount]), + data: _sendFunction + .encodeCall([web3dart.EthereumAddress.fromHex(address), amount.raw]), gasPrice: web3dart.EtherAmount.fromUnitAndValue( web3dart.EtherUnit.wei, fee, @@ -125,7 +119,7 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { final tx = web3dart.Transaction.callContract( contract: _deployedContract, function: _sendFunction, - parameters: [web3dart.EthereumAddress.fromHex(address), bigIntAmount], + parameters: [web3dart.EthereumAddress.fromHex(address), amount.raw], maxGas: _gasLimit, gasPrice: web3dart.EtherAmount.fromUnitAndValue( web3dart.EtherUnit.wei, @@ -138,7 +132,7 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { "fee": feeEstimate, "feeInWei": fee, "address": address, - "recipientAmt": satoshiAmount, + "recipientAmt": amount, "ethTx": tx, "chainId": (await client.getChainId()).toInt(), "nonce": tx.nonce, @@ -238,9 +232,8 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { .sortByDerivationIndexDesc() .findFirst(); - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - final fee = estimateFee(feeRate, _gasLimit, coin.decimals); - return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin); + Amount estimateFeeFor(int feeRate) { + return estimateFee(feeRate, _gasLimit, coin.decimals); } Future get fees => EthereumAPI.getFees(); @@ -424,11 +417,22 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { final newBalance = TokenBalance( contractAddress: tokenContract.address, - total: int.parse(_balance), - spendable: int.parse(_balance), - blockedTotal: 0, - pendingSpendable: 0, - decimalPlaces: tokenContract.decimals, + total: Amount.fromDecimal( + Decimal.parse(_balance), + fractionDigits: tokenContract.decimals, + ), + spendable: Amount.fromDecimal( + Decimal.parse(_balance), + fractionDigits: tokenContract.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: tokenContract.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: tokenContract.decimals, + ), ); await updateCachedBalance(newBalance); notifyListeners(); diff --git a/lib/services/mixins/coin_control_interface.dart b/lib/services/mixins/coin_control_interface.dart index f41f5214a..e2dc2567f 100644 --- a/lib/services/mixins/coin_control_interface.dart +++ b/lib/services/mixins/coin_control_interface.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; mixin CoinControlInterface { @@ -35,24 +36,41 @@ mixin CoinControlInterface { final utxos = await _db.getUTXOs(_walletId).findAll(); final currentChainHeight = await _getChainHeight(); - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; + Amount satoshiBalanceTotal = Amount( + rawValue: BigInt.zero, + fractionDigits: _coin.decimals, + ); + Amount satoshiBalancePending = Amount( + rawValue: BigInt.zero, + fractionDigits: _coin.decimals, + ); + Amount satoshiBalanceSpendable = Amount( + rawValue: BigInt.zero, + fractionDigits: _coin.decimals, + ); + Amount satoshiBalanceBlocked = Amount( + rawValue: BigInt.zero, + fractionDigits: _coin.decimals, + ); for (final utxo in utxos) { - satoshiBalanceTotal += utxo.value; + final utxoAmount = Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: _coin.decimals, + ); + + satoshiBalanceTotal = satoshiBalanceTotal + utxoAmount; if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; + satoshiBalanceBlocked = satoshiBalanceBlocked + utxoAmount; } else { if (utxo.isConfirmed( currentChainHeight, _coin.requiredConfirmations, )) { - satoshiBalanceSpendable += utxo.value; + satoshiBalanceSpendable + satoshiBalanceSpendable + utxoAmount; } else { - satoshiBalancePending += utxo.value; + satoshiBalancePending = satoshiBalancePending + utxoAmount; } } } diff --git a/lib/services/mixins/electrum_x_parsing.dart b/lib/services/mixins/electrum_x_parsing.dart index 76734bf08..10255de32 100644 --- a/lib/services/mixins/electrum_x_parsing.dart +++ b/lib/services/mixins/electrum_x_parsing.dart @@ -6,7 +6,6 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:tuple/tuple.dart'; mixin ElectrumXParsing { @@ -33,12 +32,27 @@ mixin ElectrumXParsing { Set inputAddresses = {}; Set outputAddresses = {}; - int totalInputValue = 0; - int totalOutputValue = 0; + Amount totalInputValue = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount totalOutputValue = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); - int amountSentFromWallet = 0; - int amountReceivedInWallet = 0; - int changeAmount = 0; + Amount amountSentFromWallet = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount amountReceivedInWallet = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount changeAmount = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); // parse inputs for (final input in txData["vin"] as List) { @@ -55,9 +69,9 @@ mixin ElectrumXParsing { // check matching output if (prevOut == output["n"]) { // get value - final value = Format.decimalAmountToSatoshis( + final value = Amount.fromDecimal( Decimal.parse(output["value"].toString()), - coin, + fractionDigits: coin.decimals, ); // add value to total @@ -83,9 +97,9 @@ mixin ElectrumXParsing { // parse outputs for (final output in txData["vout"] as List) { // get value - final value = Format.decimalAmountToSatoshis( + final value = Amount.fromDecimal( Decimal.parse(output["value"].toString()), - coin, + fractionDigits: coin.decimals, ); // add value to total @@ -121,7 +135,7 @@ mixin ElectrumXParsing { Address transactionAddress = txData["address"] as Address; TransactionType type; - int amount; + Amount amount; if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) { // tx is sent to self type = TransactionType.sentToSelf; @@ -189,10 +203,10 @@ mixin ElectrumXParsing { json["scriptPubKey"]?["addresses"]?[0] as String? ?? json['scriptPubKey']?['type'] as String? ?? "", - value: Format.decimalAmountToSatoshis( + value: Amount.fromDecimal( Decimal.parse(json["value"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); outs.add(output); } @@ -220,12 +234,10 @@ mixin ElectrumXParsing { (DateTime.now().millisecondsSinceEpoch ~/ 1000), type: type, subType: txSubType, - amount: amount, - amountString: Amount( - rawValue: BigInt.from(amount), - fractionDigits: coin.decimals, - ).toJsonString(), - fee: fee, + // amount may overflow. Deprecated. Use amountString + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: fee.raw.toInt(), height: txData["height"] as int?, isCancelled: false, isLelantus: false, diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart index 9bb11bea4..f18504eb8 100644 --- a/lib/services/mixins/eth_token_cache.dart +++ b/lib/services/mixins/eth_token_cache.dart @@ -1,6 +1,7 @@ import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/token_balance.dart'; +import 'package:stackwallet/utilities/amount.dart'; abstract class TokenCacheKeys { static String tokenBalance(String contractAddress) { @@ -26,15 +27,27 @@ mixin EthTokenCache { if (jsonString == null) { return TokenBalance( contractAddress: _token.address, - decimalPlaces: _token.decimals, - total: 0, - spendable: 0, - blockedTotal: 0, - pendingSpendable: 0, + total: Amount( + rawValue: BigInt.zero, + fractionDigits: _token.decimals, + ), + spendable: Amount( + rawValue: BigInt.zero, + fractionDigits: _token.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: _token.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: _token.decimals, + ), ); } return TokenBalance.fromJson( jsonString, + _token.decimals, ); } diff --git a/lib/services/mixins/paynym_wallet_interface.dart b/lib/services/mixins/paynym_wallet_interface.dart index 8ef6b8f2c..cae5e7ff6 100644 --- a/lib/services/mixins/paynym_wallet_interface.dart +++ b/lib/services/mixins/paynym_wallet_interface.dart @@ -16,6 +16,7 @@ import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dar import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/signing_data.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/bip47_utils.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -52,7 +53,7 @@ mixin PaynymWalletInterface { }) _estimateTxFee; late final Future> Function({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) _prepareSend; late final Future Function({ @@ -93,7 +94,7 @@ mixin PaynymWalletInterface { estimateTxFee, required Future> Function({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) prepareSend, @@ -307,10 +308,11 @@ mixin PaynymWalletInterface { return Format.uint8listToString(bytes); } - Future> preparePaymentCodeSend( - {required PaymentCode paymentCode, - required int satoshiAmount, - Map? args}) async { + Future> preparePaymentCodeSend({ + required PaymentCode paymentCode, + required Amount amount, + Map? args, + }) async { if (!(await hasConnected(paymentCode.toString()))) { throw PaynymSendException( "No notification transaction sent to $paymentCode"); @@ -326,7 +328,7 @@ mixin PaynymWalletInterface { return _prepareSend( address: sendToAddress.value, - satoshiAmount: satoshiAmount, + amount: amount, args: args, ); } diff --git a/lib/services/mixins/wallet_cache.dart b/lib/services/mixins/wallet_cache.dart index b5c4e2e75..72be9aa07 100644 --- a/lib/services/mixins/wallet_cache.dart +++ b/lib/services/mixins/wallet_cache.dart @@ -1,5 +1,6 @@ import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; mixin WalletCache { @@ -70,10 +71,13 @@ mixin WalletCache { if (jsonString == null) { return Balance( coin: _coin, - total: 0, - spendable: 0, - blockedTotal: 0, - pendingSpendable: 0, + total: Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + spendable: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + blockedTotal: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + pendingSpendable: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), ); } return Balance.fromJson(jsonString, _coin); @@ -96,10 +100,13 @@ mixin WalletCache { if (jsonString == null) { return Balance( coin: _coin, - total: 0, - spendable: 0, - blockedTotal: 0, - pendingSpendable: 0, + total: Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + spendable: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + blockedTotal: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + pendingSpendable: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), ); } return Balance.fromJson(jsonString, _coin); diff --git a/lib/utilities/amount.dart b/lib/utilities/amount.dart index 04f61fdb9..8daf5d5e0 100644 --- a/lib/utilities/amount.dart +++ b/lib/utilities/amount.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:decimal/decimal.dart'; +import 'package:intl/number_symbols.dart'; +import 'package:intl/number_symbols_data.dart'; final _ten = BigInt.from(10); @@ -60,6 +62,25 @@ class Amount { return jsonEncode(toMap()); } + String localizedStringAsFixed({ + required String locale, + int? decimalPlaces, + }) { + decimalPlaces ??= fractionDigits; + assert(decimalPlaces >= 0); + + final String separator = + (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? + (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) + ?.DECIMAL_SEP ?? + "."; + + final intValue = decimal.shift(fractionDigits).toBigInt(); + final fraction = (raw - intValue).toDecimal(); + + return "$intValue$separator${fraction.toStringAsFixed(decimalPlaces).substring(2)}"; + } + // =========================================================================== // ======= Deserialization =================================================== @@ -132,3 +153,34 @@ class Amount { @override int get hashCode => Object.hashAll([raw, fractionDigits]); } + +// ============================================================================= +// ============================================================================= +// ======= Extensions ========================================================== + +extension DecimalAmountExt on Decimal { + Amount toAmount({required int fractionDigits}) { + return Amount.fromDecimal( + this, + fractionDigits: fractionDigits, + ); + } +} + +extension DoubleAmountExt on double { + Amount toAmount({required int fractionDigits}) { + return Amount.fromDouble( + this, + fractionDigits: fractionDigits, + ); + } +} + +extension IntAmountExtension on int { + Amount toAmount({required int fractionDigits}) { + return Amount( + rawValue: BigInt.from(this), + fractionDigits: fractionDigits, + ); + } +} diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index 5b71e917d..9b07bfa4f 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -7,6 +7,8 @@ import "package:hex/hex.dart"; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'amount.dart'; + class GasTracker { final Decimal average; final Decimal fast; @@ -62,17 +64,12 @@ String getPrivateKey(String mnemonic, String mnemonicPassphrase) { return HEX.encode(addressAtIndex.privateKey as List); } -double estimateFee(int feeRate, int gasLimit, int decimals) { +Amount 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); + return Amount.fromDouble(ethAmount, fractionDigits: decimals); } diff --git a/lib/utilities/format.dart b/lib/utilities/format.dart index 8aa0c1162..56d7059b7 100644 --- a/lib/utilities/format.dart +++ b/lib/utilities/format.dart @@ -1,48 +1,43 @@ -import 'dart:math'; import 'dart:typed_data'; -import 'package:decimal/decimal.dart'; -import 'package:intl/number_symbols.dart'; -import 'package:intl/number_symbols_data.dart' show numberFormatSymbols; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; abstract class Format { static String shorten(String value, int beginCount, int endCount) { return "${value.substring(0, beginCount)}...${value.substring(value.length - endCount)}"; } - static Decimal satoshisToAmount(int sats, {required Coin coin}) { - return (Decimal.fromInt(sats) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal( - scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); - } - - static Decimal satoshisToEthTokenAmount(int sats, int decimalPlaces) { - return (Decimal.fromInt(sats) / - Decimal.fromInt(pow(10, decimalPlaces).toInt())) - .toDecimal(scaleOnInfinitePrecision: decimalPlaces); - } - - /// - static String satoshiAmountToPrettyString( - int sats, String locale, Coin coin) { - final amount = satoshisToAmount(sats, coin: coin); - return localizedStringAsFixed( - value: amount, - locale: locale, - decimalPlaces: Constants.decimalPlacesForCoin(coin), - ); - } - - static int decimalAmountToSatoshis(Decimal amount, Coin coin) { - final value = (Decimal.fromInt(Constants.satsPerCoin(coin)) * amount) - .floor() - .toBigInt(); - return value.toInt(); - } + // static Decimal satoshisToAmount(int sats, {required Coin coin}) { + // return (Decimal.fromInt(sats) / + // Decimal.fromInt(Constants.satsPerCoin(coin))) + // .toDecimal( + // scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); + // } + // + // static Decimal satoshisToEthTokenAmount(int sats, int decimalPlaces) { + // return (Decimal.fromInt(sats) / + // Decimal.fromInt(pow(10, decimalPlaces).toInt())) + // .toDecimal(scaleOnInfinitePrecision: decimalPlaces); + // } + // + // /// + // static String satoshiAmountToPrettyString( + // int sats, String locale, Coin coin) { + // final amount = satoshisToAmount(sats, coin: coin); + // return localizedStringAsFixed( + // value: amount, + // locale: locale, + // decimalPlaces: Constants.decimalPlacesForCoin(coin), + // ); + // } + // + // static int decimalAmountToSatoshis(Decimal amount, Coin coin) { + // final value = (Decimal.fromInt(Constants.satsPerCoin(coin)) * amount) + // .floor() + // .toBigInt(); + // return value.toInt(); + // } // format date string from unix timestamp static String extractDateFrom(int timestamp, {bool localized = true}) { @@ -57,26 +52,26 @@ abstract class Format { return "${date.day} ${Constants.monthMapShort[date.month]} ${date.year}, ${date.hour}:$minutes"; } - static String localizedStringAsFixed({ - required Decimal value, - required String locale, - int decimalPlaces = 0, - }) { - assert(decimalPlaces >= 0); - - final String separator = - (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? - (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) - ?.DECIMAL_SEP ?? - "."; - - final intValue = value.truncate(); - final fraction = value - intValue; - - return intValue.toStringAsFixed(0) + - separator + - fraction.toStringAsFixed(decimalPlaces).substring(2); - } + // static String localizedStringAsFixed({ + // required Decimal value, + // required String locale, + // int decimalPlaces = 0, + // }) { + // assert(decimalPlaces >= 0); + // + // final String separator = + // (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? + // (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) + // ?.DECIMAL_SEP ?? + // "."; + // + // final intValue = value.truncate(); + // final fraction = value - intValue; + // + // return intValue.toStringAsFixed(0) + + // separator + + // fraction.toStringAsFixed(decimalPlaces).substring(2); + // } /// format date string as dd/mm/yy from DateTime object static String formatDate(DateTime date) { diff --git a/lib/widgets/managed_favorite.dart b/lib/widgets/managed_favorite.dart index a9d5fcd90..ccfcdf38a 100644 --- a/lib/widgets/managed_favorite.dart +++ b/lib/widgets/managed_favorite.dart @@ -5,7 +5,6 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -104,12 +103,13 @@ class _ManagedFavoriteCardState extends ConsumerState { ), Expanded( child: Text( - "${Format.localizedStringAsFixed( - value: manager.balance.getTotal(), + "${manager.balance.total.localizedStringAsFixed( locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: 8, + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + decimalPlaces: manager.coin.decimals, )} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), ), @@ -146,11 +146,13 @@ class _ManagedFavoriteCardState extends ConsumerState { height: 2, ), Text( - "${Format.localizedStringAsFixed( - value: manager.balance.getTotal(), - locale: ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: 8, + "${manager.balance.total.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + decimalPlaces: manager.coin.decimals, )} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), ), diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index b4d74e213..088e9c103 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; @@ -239,10 +240,8 @@ class _TransactionCardState extends ConsumerState { final amount = _transaction.realAmount; return Text( - "$prefix${Format.localizedStringAsFixed( - value: amount.decimal, + "$prefix${amount.localizedStringAsFixed( locale: locale, - decimalPlaces: amount.fractionDigits, )} $unit", style: STextStyles.itemSubtitle12(context), ); @@ -283,8 +282,10 @@ class _TransactionCardState extends ConsumerState { final amount = _transaction.realAmount; return Text( - "$prefix${Format.localizedStringAsFixed( - value: amount.decimal * price, + "$prefix${Amount.fromDecimal( + amount.decimal * price, + fractionDigits: 2, + ).localizedStringAsFixed( locale: locale, decimalPlaces: 2, )} $baseCurrency", diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart index ddd491229..fbc5ac65e 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart @@ -1,12 +1,11 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -33,27 +32,27 @@ class WalletInfoRowBalance extends ConsumerWidget { ), ); - Decimal balance; + Amount totalBalance; int decimals; String unit; if (contractAddress == null) { - balance = manager.balance.getTotal(); + totalBalance = manager.balance.total; if (manager.coin == Coin.firo || manager.coin == Coin.firoTestNet) { - balance += (manager.wallet as FiroWallet).balancePrivate.getTotal(); + totalBalance = + totalBalance + (manager.wallet as FiroWallet).balancePrivate.total; } unit = manager.coin.ticker; decimals = manager.coin.decimals; } else { final ethWallet = manager.wallet as EthereumWallet; final contract = MainDB.instance.getEthContractSync(contractAddress!)!; - balance = ethWallet.getCachedTokenBalance(contract).getTotal(); + totalBalance = ethWallet.getCachedTokenBalance(contract).total; unit = contract.symbol; decimals = contract.decimals; } return Text( - "${Format.localizedStringAsFixed( - value: balance, + "${totalBalance.localizedStringAsFixed( locale: locale, decimalPlaces: decimals, )} $unit", diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 08f60e26e..db7df0b52 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -1215,7 +1215,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1224,7 +1224,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1913,7 +1913,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { @override _i22.Future> preparePaymentCodeSend({ required _i18.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1922,7 +1922,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2903,7 +2903,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2912,7 +2912,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -3215,7 +3215,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -3224,7 +3224,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart index 7d01dfbba..72ac2d172 100644 --- a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart @@ -417,7 +417,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -426,7 +426,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart index e82f51d64..bcf9e5386 100644 --- a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart @@ -378,7 +378,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -387,7 +387,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart index d150ff23d..3cc4c9852 100644 --- a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart @@ -376,7 +376,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -385,7 +385,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index 7bb2b91da..51a9b0558 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -693,7 +693,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -702,7 +702,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart index 1e4967d64..6cc5c84a9 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart @@ -480,7 +480,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -489,7 +489,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart index 48d6ab6a0..70296abd2 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart @@ -480,7 +480,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -489,7 +489,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart index 5c86479c4..b5860e0cd 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart @@ -480,7 +480,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -489,7 +489,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart index 34a6a36fe..ce6a5fa73 100644 --- a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart @@ -247,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -256,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart index 4bfdcfbcf..dd1a445be 100644 --- a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart @@ -478,7 +478,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -487,7 +487,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 7194665b1..0be6762a5 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -693,7 +693,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -702,7 +702,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index 184da3216..ce7c6a728 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -534,7 +534,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -543,7 +543,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart index 93d4f4874..2df1a8d62 100644 --- a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart @@ -247,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -256,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart index cfe9cd50e..5c48821f8 100644 --- a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart @@ -247,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -256,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index 8e5c07700..09a6f027e 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -462,7 +462,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -471,7 +471,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index c3d4a7279..689918939 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -462,7 +462,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -471,7 +471,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart index 8efc17d42..8a88c264c 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart @@ -247,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -256,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart index 5f5c9ee97..dd74d3134 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart @@ -247,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -256,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart index 5abd560b6..71ab847d7 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart @@ -478,7 +478,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -487,7 +487,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index a54af1fac..94659b07c 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -735,7 +735,7 @@ class MockManager extends _i1.Mock implements _i15.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -744,7 +744,7 @@ class MockManager extends _i1.Mock implements _i15.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart index 89e91910d..d42f9998a 100644 --- a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart @@ -478,7 +478,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -487,7 +487,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart index 9b4056348..665b0333b 100644 --- a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart +++ b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart @@ -249,7 +249,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -258,7 +258,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart index b0e4bcc7d..d1190a181 100644 --- a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart @@ -248,7 +248,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -257,7 +257,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart index 6c02b46e1..b542dc860 100644 --- a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart @@ -247,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -256,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart index b3fcf528f..d0a586e2e 100644 --- a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart @@ -289,7 +289,7 @@ class MockManager extends _i1.Mock implements _i8.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -298,7 +298,7 @@ class MockManager extends _i1.Mock implements _i8.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart index 5cbb93bc1..71a400d4c 100644 --- a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart @@ -249,7 +249,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -258,7 +258,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/services/coins/fake_coin_service_api.dart b/test/services/coins/fake_coin_service_api.dart index 834d676a2..9f522d822 100644 --- a/test/services/coins/fake_coin_service_api.dart +++ b/test/services/coins/fake_coin_service_api.dart @@ -103,7 +103,7 @@ class FakeCoinServiceAPI extends CoinServiceAPI { @override Future> prepareSend( {required String address, - required int satoshiAmount, + required int amount, Map? args}) { // TODO: implement prepareSend throw UnimplementedError(); diff --git a/test/services/coins/manager_test.mocks.dart b/test/services/coins/manager_test.mocks.dart index afcf7d743..e85f304ce 100644 --- a/test/services/coins/manager_test.mocks.dart +++ b/test/services/coins/manager_test.mocks.dart @@ -392,7 +392,7 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { @override _i10.Future> prepareSendPublic({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -401,7 +401,7 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -421,7 +421,7 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { @override _i10.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -430,7 +430,7 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 0c1347147..ebe7b3724 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -1007,7 +1007,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1016,7 +1016,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1704,7 +1704,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1713,7 +1713,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2477,7 +2477,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2486,7 +2486,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2789,7 +2789,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2798,7 +2798,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index eb5f34eea..a3182a21f 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -994,7 +994,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { @override _i21.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1003,7 +1003,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1691,7 +1691,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { @override _i21.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1700,7 +1700,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2202,7 +2202,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i21.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2211,7 +2211,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2514,7 +2514,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i18.CoinServiceAPI { @override _i21.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2523,7 +2523,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i18.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 45a8b158b..686f360b1 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -595,7 +595,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i18.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -604,7 +604,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -911,7 +911,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i7.CoinServiceAPI { @override _i18.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -920,7 +920,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i7.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1361,7 +1361,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { @override _i18.Future> prepareSendPublic({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1370,7 +1370,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1390,7 +1390,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { @override _i18.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1399,7 +1399,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index cce60be42..5e55a5020 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -749,7 +749,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { @override _i20.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -758,7 +758,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1446,7 +1446,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { @override _i20.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1455,7 +1455,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index 85ff2ef99..9d99f22ed 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -1006,7 +1006,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1015,7 +1015,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1703,7 +1703,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1712,7 +1712,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2414,7 +2414,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2423,7 +2423,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2726,7 +2726,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2735,7 +2735,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 90fd61806..e4daea467 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -1006,7 +1006,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1015,7 +1015,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1703,7 +1703,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1712,7 +1712,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2414,7 +2414,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2423,7 +2423,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2726,7 +2726,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2735,7 +2735,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), From 14d68b0743a6f6d21d8f802569495cd72cdf53a7 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Apr 2023 14:21:49 -0600 Subject: [PATCH 193/208] Amount tweaks and fixes --- lib/pages/coin_control/coin_control_view.dart | 4 +- lib/pages/coin_control/utxo_card.dart | 2 +- lib/pages/coin_control/utxo_details_view.dart | 2 +- .../confirm_change_now_send.dart | 8 +-- .../paynym/dialogs/paynym_details_popup.dart | 2 +- .../subwidgets/desktop_paynym_details.dart | 2 +- .../send_view/confirm_transaction_view.dart | 10 +-- lib/pages/send_view/send_view.dart | 18 ++--- .../transaction_details_view.dart | 2 +- lib/utilities/amount.dart | 25 ++++--- test/utilities/amount_test.dart | 71 +++++++++++++++++++ 11 files changed, 110 insertions(+), 36 deletions(-) diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index fb415d3fa..2342d03aa 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -683,7 +683,7 @@ class _CoinControlViewState extends ConsumerState { value += element, ); return Text( - "${selectedSum.toAmount(fractionDigits: coin.decimals).localizedStringAsFixed( + "${selectedSum.toAmountAsRaw(fractionDigits: coin.decimals).localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select( @@ -731,7 +731,7 @@ class _CoinControlViewState extends ConsumerState { style: STextStyles.w600_14(context), ), Text( - "${widget.requestedTotal!.toAmount(fractionDigits: coin.decimals).localizedStringAsFixed( + "${widget.requestedTotal!.toAmountAsRaw(fractionDigits: coin.decimals).localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select( diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index 528e19fe0..ab25e4739 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -124,7 +124,7 @@ class _UtxoCardState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - "${utxo.value.toAmount(fractionDigits: coin.decimals).localizedStringAsFixed( + "${utxo.value.toAmountAsRaw(fractionDigits: coin.decimals).localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider.select( (value) => value.locale, diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index b1994b10d..d9d4e277f 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -240,7 +240,7 @@ class _UtxoDetailsViewState extends ConsumerState { width: 16, ), Text( - "${utxo!.value.toAmount(fractionDigits: coin.decimals).localizedStringAsFixed( + "${utxo!.value.toAmountAsRaw(fractionDigits: coin.decimals).localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider.select( (value) => value.locale, diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 85faaf55a..73e686c08 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -359,7 +359,7 @@ class _ConfirmChangeNowSendViewState mainAxisAlignment: MainAxisAlignment.end, children: [ Text( - "${(transactionInfo["fee"] as int).toAmount( + "${(transactionInfo["fee"] as int).toAmountAsRaw( fractionDigits: ref.watch( managerProvider .select((value) => value.coin.decimals), @@ -402,7 +402,7 @@ class _ConfirmChangeNowSendViewState managerProvider.select((value) => value.coin), ); final fee = - (transactionInfo["fee"] as int).toAmount( + (transactionInfo["fee"] as int).toAmountAsRaw( fractionDigits: coin.decimals, ); final amount = @@ -637,7 +637,7 @@ class _ConfirmChangeNowSendViewState style: STextStyles.smallMed12(context), ), Text( - "${(transactionInfo["fee"] as int).toAmount(fractionDigits: ref.watch( + "${(transactionInfo["fee"] as int).toAmountAsRaw(fractionDigits: ref.watch( managerProvider.select( (value) => value.coin.decimals, ), @@ -733,7 +733,7 @@ class _ConfirmChangeNowSendViewState final coin = ref.watch( managerProvider.select((value) => value.coin), ); - final fee = (transactionInfo["fee"] as int).toAmount( + final fee = (transactionInfo["fee"] as int).toAmountAsRaw( fractionDigits: coin.decimals, ); final amount = diff --git a/lib/pages/paynym/dialogs/paynym_details_popup.dart b/lib/pages/paynym/dialogs/paynym_details_popup.dart index 5441ca5c3..248f5ee7e 100644 --- a/lib/pages/paynym/dialogs/paynym_details_popup.dart +++ b/lib/pages/paynym/dialogs/paynym_details_popup.dart @@ -160,7 +160,7 @@ class _PaynymDetailsPopupState extends ConsumerState { ); }, amount: (preparedTx["amount"] as Amount) + - (preparedTx["fee"] as int).toAmount( + (preparedTx["fee"] as int).toAmountAsRaw( fractionDigits: manager.coin.decimals, ), coin: manager.coin, diff --git a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart index d2dfc0cc8..c98c05008 100644 --- a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart +++ b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart @@ -143,7 +143,7 @@ class _PaynymDetailsPopupState extends ConsumerState { ); }, amount: (preparedTx["amount"] as Amount) + - (preparedTx["fee"] as int).toAmount( + (preparedTx["fee"] as int).toAmountAsRaw( fractionDigits: manager.coin.decimals, ), coin: manager.coin, diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 2f0c44092..9d830848b 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -427,7 +427,7 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), ), Text( - "${(transactionInfo["fee"] as int).toAmount( + "${(transactionInfo["fee"] as int).toAmountAsRaw( fractionDigits: ref.watch( managerProvider.select( (value) => value.coin.decimals, @@ -678,8 +678,8 @@ class _ConfirmTransactionViewState value.getManager(walletId))) .coin; - final fee = - (transactionInfo["fee"] as int).toAmount( + final fee = (transactionInfo["fee"] as int) + .toAmountAsRaw( fractionDigits: coin.decimals, ); @@ -857,7 +857,7 @@ class _ConfirmTransactionViewState .select((value) => value.getManager(walletId))) .coin; - final fee = (transactionInfo["fee"] as int).toAmount( + final fee = (transactionInfo["fee"] as int).toAmountAsRaw( fractionDigits: coin.decimals, ); @@ -917,7 +917,7 @@ class _ConfirmTransactionViewState final coin = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId).coin)); final fee = (transactionInfo["fee"] as int) - .toAmount(fractionDigits: coin.decimals); + .toAmountAsRaw(fractionDigits: coin.decimals); final locale = ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 170b5b85f..48bb4b197 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -154,7 +154,7 @@ class _SendViewState extends ConsumerState { setState(() { _calculateFeesFuture = calculateFees( _amountToSend == null - ? 0.toAmount(fractionDigits: coin.decimals) + ? 0.toAmountAsRaw(fractionDigits: coin.decimals) : _amountToSend!, ); }); @@ -175,7 +175,7 @@ class _SendViewState extends ConsumerState { setState(() { _calculateFeesFuture = calculateFees( _amountToSend == null - ? 0.toAmount(fractionDigits: coin.decimals) + ? 0.toAmountAsRaw(fractionDigits: coin.decimals) : _amountToSend!, ); }); @@ -560,10 +560,10 @@ class _SendViewState extends ConsumerState { void initState() { coin = widget.coin; ref.refresh(feeSheetSessionCacheProvider); - _currentFee = 0.toAmount(fractionDigits: coin.decimals); + _currentFee = 0.toAmountAsRaw(fractionDigits: coin.decimals); _calculateFeesFuture = - calculateFees(0.toAmount(fractionDigits: coin.decimals)); + calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals)); _data = widget.autoFillData; walletId = widget.walletId; clipboard = widget.clipboard; @@ -672,7 +672,7 @@ class _SendViewState extends ConsumerState { if (_amountToSend == null) { setState(() { _calculateFeesFuture = - calculateFees(0.toAmount(fractionDigits: coin.decimals)); + calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals)); }); } else { setState(() { @@ -1526,11 +1526,11 @@ class _SendViewState extends ConsumerState { .item1; if (_price == Decimal.zero) { - _amountToSend = 0.toAmount( + _amountToSend = 0.toAmountAsRaw( fractionDigits: coin.decimals); } else { _amountToSend = baseAmount <= Amount.zero - ? 0.toAmount( + ? 0.toAmountAsRaw( fractionDigits: coin.decimals) : (baseAmount.decimal / _price) .toDouble() @@ -1558,8 +1558,8 @@ class _SendViewState extends ConsumerState { cryptoAmountController.text = amountString; _cryptoAmountChangeLock = false; } else { - _amountToSend = - 0.toAmount(fractionDigits: coin.decimals); + _amountToSend = 0.toAmountAsRaw( + fractionDigits: coin.decimals); _cryptoAmountChangeLock = true; cryptoAmountController.text = ""; _cryptoAmountChangeLock = false; diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 66e505636..5506a4438 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -83,7 +83,7 @@ class _TransactionDetailsViewState coin = widget.coin; amount = _transaction.realAmount; - fee = _transaction.fee.toAmount(fractionDigits: coin.decimals); + fee = _transaction.fee.toAmountAsRaw(fractionDigits: coin.decimals); if ((coin == Coin.firo || coin == Coin.firoTestNet) && _transaction.subType == TransactionSubType.mint) { diff --git a/lib/utilities/amount.dart b/lib/utilities/amount.dart index 8daf5d5e0..9688e2b79 100644 --- a/lib/utilities/amount.dart +++ b/lib/utilities/amount.dart @@ -43,9 +43,7 @@ class Amount { BigInt get raw => _value; /// actual decimal vale represented - Decimal get decimal => - (Decimal.fromBigInt(_value) / _ten.pow(fractionDigits).toDecimal()) - .toDecimal(scaleOnInfinitePrecision: fractionDigits); + Decimal get decimal => Decimal.fromBigInt(raw).shift(-1 * fractionDigits); /// convenience getter @Deprecated("provided for convenience only. Use fractionDigits instead.") @@ -69,16 +67,21 @@ class Amount { decimalPlaces ??= fractionDigits; assert(decimalPlaces >= 0); + final wholeNumber = decimal.truncate(); + + if (decimalPlaces == 0) { + return wholeNumber.toStringAsFixed(0); + } + final String separator = (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) ?.DECIMAL_SEP ?? "."; - final intValue = decimal.shift(fractionDigits).toBigInt(); - final fraction = (raw - intValue).toDecimal(); + final fraction = decimal - wholeNumber; - return "$intValue$separator${fraction.toStringAsFixed(decimalPlaces).substring(2)}"; + return "${wholeNumber.toStringAsFixed(0)}$separator${fraction.toStringAsFixed(decimalPlaces).substring(2)}"; } // =========================================================================== @@ -95,13 +98,13 @@ class Amount { // =========================================================================== // ======= operators ========================================================= - bool operator >(Amount other) => raw > other.raw; + bool operator >(Amount other) => decimal > other.decimal; - bool operator <(Amount other) => raw < other.raw; + bool operator <(Amount other) => decimal < other.decimal; - bool operator >=(Amount other) => raw >= other.raw; + bool operator >=(Amount other) => decimal >= other.decimal; - bool operator <=(Amount other) => raw <= other.raw; + bool operator <=(Amount other) => decimal <= other.decimal; Amount operator +(Amount other) { if (fractionDigits != other.fractionDigits) { @@ -177,7 +180,7 @@ extension DoubleAmountExt on double { } extension IntAmountExtension on int { - Amount toAmount({required int fractionDigits}) { + Amount toAmountAsRaw({required int fractionDigits}) { return Amount( rawValue: BigInt.from(this), fractionDigits: fractionDigits, diff --git a/test/utilities/amount_test.dart b/test/utilities/amount_test.dart index 2f7368162..2fbb24599 100644 --- a/test/utilities/amount_test.dart +++ b/test/utilities/amount_test.dart @@ -79,6 +79,77 @@ void main() { expect(didThrow, true); }); + group("serialization", () { + test("toMap", () { + expect( + Amount(rawValue: BigInt.two, fractionDigits: 8).toMap(), + {"raw": "2", "fractionDigits": 8}, + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8).toMap(), + {"raw": "200000000", "fractionDigits": 8}, + ); + expect( + Amount.fromDouble(2, fractionDigits: 8).toMap(), + {"raw": "200000000", "fractionDigits": 8}, + ); + }); + + test("toJsonString", () { + expect( + Amount(rawValue: BigInt.two, fractionDigits: 8).toJsonString(), + '{"raw":"2","fractionDigits":8}', + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8) + .toJsonString(), + '{"raw":"200000000","fractionDigits":8}', + ); + expect( + Amount.fromDouble(2, fractionDigits: 8).toJsonString(), + '{"raw":"200000000","fractionDigits":8}', + ); + }); + + test("localizedStringAsFixed", () { + expect( + Amount(rawValue: BigInt.two, fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US"), + "0.00000002", + ); + expect( + Amount(rawValue: BigInt.two, fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US", decimalPlaces: 2), + "0.00", + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US"), + "2.00000000", + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US", decimalPlaces: 4), + "2.0000", + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US", decimalPlaces: 0), + "2", + ); + }); + }); + + group("deserialization", () { + test("fromSerializedJsonString", () { + expect( + Amount.fromSerializedJsonString( + '{"raw":"200000000","fractionDigits":8}'), + Amount.fromDouble(2, fractionDigits: 8), + ); + }); + }); + group("operators", () { final one = Amount(rawValue: BigInt.one, fractionDigits: 0); final two = Amount(rawValue: BigInt.two, fractionDigits: 0); From 25ff880280a228a4f98586219f9573995db3a455 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Apr 2023 15:24:56 -0600 Subject: [PATCH 194/208] move amount.dart file --- lib/db/isar/main_db.dart | 3 +-- lib/dto/ethereum/eth_token_tx_extra_dto.dart | 2 +- lib/dto/ethereum/eth_tx_dto.dart | 2 +- lib/models/balance.dart | 2 +- lib/models/isar/models/blockchain_data/transaction.dart | 2 +- lib/models/token_balance.dart | 2 +- lib/models/transaction_filter.dart | 2 +- lib/pages/coin_control/coin_control_view.dart | 2 +- lib/pages/coin_control/utxo_card.dart | 2 +- lib/pages/coin_control/utxo_details_view.dart | 2 +- lib/pages/exchange_view/confirm_change_now_send.dart | 5 +++-- lib/pages/exchange_view/exchange_step_views/step_4_view.dart | 2 +- lib/pages/exchange_view/send_from_view.dart | 2 +- .../exchange_view/sub_widgets/exchange_provider_options.dart | 2 +- lib/pages/exchange_view/trade_details_view.dart | 2 +- lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart | 2 +- lib/pages/paynym/dialogs/paynym_details_popup.dart | 2 +- lib/pages/paynym/subwidgets/desktop_paynym_details.dart | 2 +- lib/pages/send_view/confirm_transaction_view.dart | 2 +- lib/pages/send_view/send_view.dart | 2 +- .../sub_widgets/transaction_fee_selection_sheet.dart | 2 +- lib/pages/send_view/token_send_view.dart | 2 +- lib/pages/token_view/sub_widgets/token_summary.dart | 2 +- .../wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart | 2 +- lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart | 2 +- .../wallet_view/transaction_views/all_transactions_view.dart | 2 +- .../transaction_views/transaction_details_view.dart | 2 +- .../transaction_views/transaction_search_filter_view.dart | 2 +- lib/pages/wallet_view/wallet_view.dart | 2 +- lib/pages/wallets_view/sub_widgets/favorite_card.dart | 2 +- lib/pages/wallets_view/sub_widgets/wallet_list_item.dart | 2 +- .../coin_control/desktop_coin_control_use_dialog.dart | 2 +- lib/pages_desktop_specific/coin_control/utxo_row.dart | 2 +- .../desktop_exchange/exchange_steps/step_scaffold.dart | 2 +- .../my_stack_view/paynym/desktop_paynym_send_dialog.dart | 2 +- .../my_stack_view/wallet_summary_table.dart | 2 +- .../wallet_view/sub_widgets/desktop_fee_dropdown.dart | 2 +- .../my_stack_view/wallet_view/sub_widgets/desktop_send.dart | 2 +- .../wallet_view/sub_widgets/desktop_token_send.dart | 2 +- .../wallet_view/sub_widgets/desktop_wallet_features.dart | 2 +- .../wallet_view/sub_widgets/desktop_wallet_summary.dart | 2 +- lib/route_generator.dart | 2 +- lib/services/coins/bitcoin/bitcoin_wallet.dart | 2 +- lib/services/coins/bitcoincash/bitcoincash_wallet.dart | 2 +- lib/services/coins/coin_service.dart | 2 +- lib/services/coins/dogecoin/dogecoin_wallet.dart | 2 +- lib/services/coins/epiccash/epiccash_wallet.dart | 2 +- lib/services/coins/ethereum/ethereum_wallet.dart | 2 +- lib/services/coins/firo/firo_wallet.dart | 2 +- lib/services/coins/litecoin/litecoin_wallet.dart | 2 +- lib/services/coins/manager.dart | 2 +- lib/services/coins/monero/monero_wallet.dart | 2 +- lib/services/coins/namecoin/namecoin_wallet.dart | 2 +- lib/services/coins/particl/particl_wallet.dart | 2 +- lib/services/coins/wownero/wownero_wallet.dart | 2 +- lib/services/ethereum/cached_eth_token_balance.dart | 2 +- lib/services/ethereum/ethereum_token_service.dart | 2 +- lib/services/mixins/coin_control_interface.dart | 2 +- lib/services/mixins/electrum_x_parsing.dart | 2 +- lib/services/mixins/eth_token_cache.dart | 2 +- lib/services/mixins/paynym_wallet_interface.dart | 2 +- lib/services/mixins/wallet_cache.dart | 2 +- lib/utilities/{ => amount}/amount.dart | 0 lib/utilities/db_version_migration.dart | 2 +- lib/utilities/eth_commons.dart | 3 +-- lib/widgets/transaction_card.dart | 2 +- .../sub_widgets/wallet_info_row_balance_future.dart | 2 +- test/services/coins/firo/firo_wallet_test.dart | 2 +- test/services/coins/manager_test.dart | 2 +- test/utilities/amount_test.dart | 2 +- test/widget_tests/transaction_card_test.dart | 2 +- 71 files changed, 72 insertions(+), 73 deletions(-) rename lib/utilities/{ => amount}/amount.dart (100%) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 81188cb0e..76a60676f 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -3,9 +3,8 @@ import 'package:flutter_native_splash/cli_commands.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/exceptions/main_db/main_db_exception.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:tuple/tuple.dart'; diff --git a/lib/dto/ethereum/eth_token_tx_extra_dto.dart b/lib/dto/ethereum/eth_token_tx_extra_dto.dart index b84aa8a2f..bbc457b0c 100644 --- a/lib/dto/ethereum/eth_token_tx_extra_dto.dart +++ b/lib/dto/ethereum/eth_token_tx_extra_dto.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; class EthTokenTxExtraDTO { diff --git a/lib/dto/ethereum/eth_tx_dto.dart b/lib/dto/ethereum/eth_tx_dto.dart index ed1b37e1d..a4345dec5 100644 --- a/lib/dto/ethereum/eth_tx_dto.dart +++ b/lib/dto/ethereum/eth_tx_dto.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; class EthTxDTO { diff --git a/lib/models/balance.dart b/lib/models/balance.dart index c598a275a..aa9f04bde 100644 --- a/lib/models/balance.dart +++ b/lib/models/balance.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; enum Unit { diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index b1dacefba..39d2cc0af 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -5,7 +5,7 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/input.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/output.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:tuple/tuple.dart'; part 'transaction.g.dart'; diff --git a/lib/models/token_balance.dart b/lib/models/token_balance.dart index 4dbdbefcf..727a29d11 100644 --- a/lib/models/token_balance.dart +++ b/lib/models/token_balance.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:stackwallet/models/balance.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; class TokenBalance extends Balance { diff --git a/lib/models/transaction_filter.dart b/lib/models/transaction_filter.dart index b28c9860a..7ea18aac0 100644 --- a/lib/models/transaction_filter.dart +++ b/lib/models/transaction_filter.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; class TransactionFilter { final bool sent; diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 2342d03aa..d1228f9c7 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -11,7 +11,7 @@ import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/mixins/coin_control_interface.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index ab25e4739..d0cba1f39 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -4,7 +4,7 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index d9d4e277f..f74a2c4b1 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -8,7 +8,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 73e686c08..5d9ed25c7 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -11,7 +11,7 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -733,7 +733,8 @@ class _ConfirmChangeNowSendViewState final coin = ref.watch( managerProvider.select((value) => value.coin), ); - final fee = (transactionInfo["fee"] as int).toAmountAsRaw( + final fee = + (transactionInfo["fee"] as int).toAmountAsRaw( fractionDigits: coin.decimals, ); final amount = diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index fcdd1325e..4a86254c4 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -15,7 +15,7 @@ import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dia import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 1f3ec5d51..36055d508 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -12,7 +12,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index 0a70f4347..c00e7dfe8 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -9,7 +9,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index d23d5be39..f0cb7b7a8 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -20,7 +20,7 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart b/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart index 075fbd40e..94603cf62 100644 --- a/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart +++ b/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/pages/paynym/dialogs/paynym_details_popup.dart b/lib/pages/paynym/dialogs/paynym_details_popup.dart index 248f5ee7e..6c4a8b691 100644 --- a/lib/pages/paynym/dialogs/paynym_details_popup.dart +++ b/lib/pages/paynym/dialogs/paynym_details_popup.dart @@ -17,7 +17,7 @@ import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart index c98c05008..a6929d0b0 100644 --- a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart +++ b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart @@ -15,7 +15,7 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/paynym/desktop_ import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 9d830848b..8cb4f42c9 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -18,7 +18,7 @@ import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 48bb4b197..9fa6ed18f 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -25,7 +25,7 @@ import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/address_utils.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index eca2609a7..cba5f779f 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -8,7 +8,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 5eca917b3..e79842256 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -18,7 +18,7 @@ import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/address_utils.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 1a75d1ba2..2d9db8b62 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -15,7 +15,7 @@ import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/price_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index 2b41f28b5..7c6778878 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -5,7 +5,7 @@ import 'package:stackwallet/providers/providers.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/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index 3a8e3ae7e..947919b8d 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -12,7 +12,7 @@ import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_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/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.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'; diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index b00d94c76..f0247b429 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -13,7 +13,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 5506a4438..ed5e78c64 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -15,7 +15,7 @@ import 'package:stackwallet/providers/global/address_book_service_provider.dart' import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/block_explorers.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index 4297be65a..ac1295f55 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -8,7 +8,7 @@ import 'package:stackwallet/models/transaction_filter.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 2fa57bf18..c5b43a5f2 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -37,7 +37,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index bcf84f0cf..72b2e3ffa 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -5,7 +5,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 9f6d0f7b7..ac4c6fb85 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -7,7 +7,7 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_sheet/wallets_sheet.dart'; import 'package:stackwallet/pages/wallets_view/eth_wallets_overview.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart index 7f79aa1fd..71b3e9d97 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart @@ -6,7 +6,7 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart index 04752b53f..7521c8ec0 100644 --- a/lib/pages_desktop_specific/coin_control/utxo_row.dart +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -6,7 +6,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index d5c098335..203ae2d9b 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -17,7 +17,7 @@ import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/notifications_api.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; diff --git a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart index 77a94e7b2..77c78c369 100644 --- a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart @@ -10,7 +10,7 @@ import 'package:stackwallet/providers/global/price_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index 83f50b003..9741ae905 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart index 7f9b1e2fe..9bd260a34 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart @@ -9,7 +9,7 @@ import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index a988c77bd..5710be78b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -25,7 +25,7 @@ import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/address_utils.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index b25dfce46..97be1da9a 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -19,7 +19,7 @@ import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/address_utils.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index b355f75cc..d982956f2 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -17,7 +17,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index 45736a6f0..921595f43 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -8,7 +8,7 @@ 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/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 985c58f12..bc41dfa9f 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -143,7 +143,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncin 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/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index 809b701a5..8831d0be6 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -34,7 +34,7 @@ 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/address_utils.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 777791555..a3736bb49 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -31,7 +31,7 @@ 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/address_utils.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 1204859cc..ff75822fd 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -16,7 +16,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/prefs.dart'; diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 15e20f62c..fbb769235 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -32,7 +32,7 @@ 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/address_utils.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index 972601f9b..cbbbc76c8 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -27,7 +27,7 @@ import 'package:stackwallet/services/mixins/epic_cash_hive.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/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_epicboxes.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index ac05dacb3..229e35409 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -24,7 +24,7 @@ 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/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index aa39eb47b..38c99e3bb 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -32,7 +32,7 @@ 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/address_utils.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index d9063e07d..93f2c3b3c 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -31,7 +31,7 @@ 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/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index 716abb606..380fd4c8e 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -12,7 +12,7 @@ import 'package:stackwallet/services/event_bus/events/global/updated_in_backgrou import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index ac4f24799..87b803255 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -38,7 +38,7 @@ 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/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.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'; diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index df71c55e5..fc78afd19 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -31,7 +31,7 @@ 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/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index cbf3661df..8d311cc0b 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -30,7 +30,7 @@ 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/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index cd977f1f8..84f806be2 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -40,7 +40,7 @@ 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/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.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'; diff --git a/lib/services/ethereum/cached_eth_token_balance.dart b/lib/services/ethereum/cached_eth_token_balance.dart index 893ec557c..c08b40fd5 100644 --- a/lib/services/ethereum/cached_eth_token_balance.dart +++ b/lib/services/ethereum/cached_eth_token_balance.dart @@ -2,7 +2,7 @@ import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/token_balance.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.dart'; import 'package:stackwallet/services/mixins/eth_token_cache.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/logger.dart'; class CachedEthTokenBalance with EthTokenCache { diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index c0405df89..c071b12c5 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -20,7 +20,7 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/eth_token_cache.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.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'; diff --git a/lib/services/mixins/coin_control_interface.dart b/lib/services/mixins/coin_control_interface.dart index e2dc2567f..f1cc15fa4 100644 --- a/lib/services/mixins/coin_control_interface.dart +++ b/lib/services/mixins/coin_control_interface.dart @@ -5,7 +5,7 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; mixin CoinControlInterface { diff --git a/lib/services/mixins/electrum_x_parsing.dart b/lib/services/mixins/electrum_x_parsing.dart index 10255de32..c313a91eb 100644 --- a/lib/services/mixins/electrum_x_parsing.dart +++ b/lib/services/mixins/electrum_x_parsing.dart @@ -4,7 +4,7 @@ import 'package:bip47/src/util.dart'; import 'package:decimal/decimal.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:tuple/tuple.dart'; diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart index f18504eb8..ccaa293d0 100644 --- a/lib/services/mixins/eth_token_cache.dart +++ b/lib/services/mixins/eth_token_cache.dart @@ -1,7 +1,7 @@ import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/token_balance.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; abstract class TokenCacheKeys { static String tokenBalance(String contractAddress) { diff --git a/lib/services/mixins/paynym_wallet_interface.dart b/lib/services/mixins/paynym_wallet_interface.dart index cae5e7ff6..156407420 100644 --- a/lib/services/mixins/paynym_wallet_interface.dart +++ b/lib/services/mixins/paynym_wallet_interface.dart @@ -16,7 +16,7 @@ import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dar import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/signing_data.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/bip47_utils.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/services/mixins/wallet_cache.dart b/lib/services/mixins/wallet_cache.dart index 72be9aa07..49f53381a 100644 --- a/lib/services/mixins/wallet_cache.dart +++ b/lib/services/mixins/wallet_cache.dart @@ -1,6 +1,6 @@ import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/balance.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; mixin WalletCache { diff --git a/lib/utilities/amount.dart b/lib/utilities/amount/amount.dart similarity index 100% rename from lib/utilities/amount.dart rename to lib/utilities/amount/amount.dart diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index ff740a87f..24e8b8582 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -12,7 +12,7 @@ import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/wallets_service.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index 9b07bfa4f..75aaada6a 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -4,11 +4,10 @@ import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import 'package:decimal/decimal.dart'; import "package:hex/hex.dart"; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'amount.dart'; - class GasTracker { final Decimal average; final Decimal fast; diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index 088e9c103..a2b9ba866 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -8,7 +8,7 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart index fbc5ac65e..1bbaf8a94 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart @@ -4,7 +4,7 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; diff --git a/test/services/coins/firo/firo_wallet_test.dart b/test/services/coins/firo/firo_wallet_test.dart index ea5e4521d..d7b23e51e 100644 --- a/test/services/coins/firo/firo_wallet_test.dart +++ b/test/services/coins/firo/firo_wallet_test.dart @@ -18,7 +18,7 @@ import 'package:stackwallet/models/paymint/transactions_model.dart' as old; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/address_utils.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; diff --git a/test/services/coins/manager_test.dart b/test/services/coins/manager_test.dart index 265540549..d4a8f9969 100644 --- a/test/services/coins/manager_test.dart +++ b/test/services/coins/manager_test.dart @@ -8,7 +8,7 @@ import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'manager_test.mocks.dart'; diff --git a/test/utilities/amount_test.dart b/test/utilities/amount_test.dart index 2fbb24599..65e9b463d 100644 --- a/test/utilities/amount_test.dart +++ b/test/utilities/amount_test.dart @@ -1,6 +1,6 @@ import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; void main() { test("Basic Amount Constructor tests", () { diff --git a/test/widget_tests/transaction_card_test.dart b/test/widget_tests/transaction_card_test.dart index 5da6b9bc6..02fa7bdfc 100644 --- a/test/widget_tests/transaction_card_test.dart +++ b/test/widget_tests/transaction_card_test.dart @@ -17,7 +17,7 @@ import 'package:stackwallet/services/locale_service.dart'; import 'package:stackwallet/services/notes_service.dart'; import 'package:stackwallet/services/price_service.dart'; import 'package:stackwallet/services/wallets.dart'; -import 'package:stackwallet/utilities/amount.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; From ae51ce61c34765fd9c47e735dd5282f9ca23a736 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Apr 2023 17:27:42 -0600 Subject: [PATCH 195/208] amount unit + tests --- lib/utilities/amount/amount_unit.dart | 120 ++++++++++++++++ test/utilities/amount/amount_unit_test.dart | 144 ++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 lib/utilities/amount/amount_unit.dart create mode 100644 test/utilities/amount/amount_unit_test.dart diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart new file mode 100644 index 000000000..2dd64202c --- /dev/null +++ b/lib/utilities/amount/amount_unit.dart @@ -0,0 +1,120 @@ +import 'dart:math' as math; + +import 'package:decimal/decimal.dart'; +import 'package:intl/number_symbols.dart'; +import 'package:intl/number_symbols_data.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +enum AmountUnit { + normal(0), + milli(3), + micro(6), + nano(9), + pico(12), + femto(15), + atto(18), + ; + + const AmountUnit(this.shift); + final int shift; +} + +extension AmountUnitExt on AmountUnit { + String unitForCoin(Coin coin) { + switch (this) { + case AmountUnit.normal: + return coin.ticker; + case AmountUnit.milli: + return "m${coin.ticker}"; + case AmountUnit.micro: + return "µ${coin.ticker}"; + case AmountUnit.nano: + if (coin == Coin.ethereum) { + return "gwei"; + } else if (coin == Coin.wownero || coin == Coin.monero) { + return "n${coin.ticker}"; + } else { + return "sats"; + } + case AmountUnit.pico: + if (coin == Coin.ethereum) { + return "mwei"; + } else if (coin == Coin.wownero || coin == Coin.monero) { + return "p${coin.ticker}"; + } else { + return "invalid"; + } + case AmountUnit.femto: + if (coin == Coin.ethereum) { + return "kwei"; + } else { + return "invalid"; + } + case AmountUnit.atto: + if (coin == Coin.ethereum) { + return "wei"; + } else { + return "invalid"; + } + } + } + + String displayAmount({ + required Amount amount, + required String locale, + required Coin coin, + required int maxDecimalPlaces, + }) { + assert(maxDecimalPlaces >= 0); + // ensure we don't shift past minimum atomic value + final realShift = math.min(shift, amount.fractionDigits); + + // shifted to unit + final Decimal shifted = amount.decimal.shift(realShift); + + // get shift int value without fractional value + final BigInt wholeNumber = shifted.toBigInt(); + + // get decimal places to display + final int places = math.max(0, amount.fractionDigits - realShift); + + // start building the return value with just the whole value + String returnValue = wholeNumber.toString(); + + // if any decimal places should be shown continue building the return value + if (places > 0) { + // get the fractional value + final Decimal fraction = shifted - shifted.truncate(); + + // get final decimal based on max precision wanted + final int actualDecimalPlaces = math.min(places, maxDecimalPlaces); + + // get remainder string without the prepending "0." + String remainder = fraction.toString().substring(2); + + if (remainder.length > actualDecimalPlaces) { + // trim unwanted trailing digits + remainder = remainder.substring(0, actualDecimalPlaces); + } else if (remainder.length < actualDecimalPlaces) { + // pad with zeros to achieve requested precision + for (int i = remainder.length; i < actualDecimalPlaces; i++) { + remainder += "0"; + } + } + + // get decimal separator based on locale + final String separator = + (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? + (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) + ?.DECIMAL_SEP ?? + "."; + + // append separator and fractional amount + returnValue += "$separator$remainder"; + } + + // return the value with the proper unit symbol + return "$returnValue ${unitForCoin(coin)}"; + } +} diff --git a/test/utilities/amount/amount_unit_test.dart b/test/utilities/amount/amount_unit_test.dart new file mode 100644 index 000000000..2dcd5125c --- /dev/null +++ b/test/utilities/amount/amount_unit_test.dart @@ -0,0 +1,144 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/amount/amount_unit.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +void main() { + test("displayAmount BTC", () { + final Amount amount = Amount( + rawValue: BigInt.from(1012345678), + fractionDigits: 8, + ); + + expect( + AmountUnit.normal.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "10.12345678 BTC", + ); + + expect( + AmountUnit.milli.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "10123.45678 mBTC", + ); + + expect( + AmountUnit.micro.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "10123456.78 µBTC", + ); + + expect( + AmountUnit.nano.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "1012345678 sats", + ); + final dec = Decimal.parse("10.123456789123456789"); + + expect(dec.toString(), "10.123456789123456789"); + }); + + test("displayAmount ETH", () { + final Amount amount = Amount.fromDecimal( + Decimal.parse("10.123456789123456789"), + fractionDigits: Coin.ethereum.decimals, + ); + + expect( + AmountUnit.normal.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 8, + ), + "10.12345678 ETH", + ); + + expect( + AmountUnit.normal.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 18, + ), + "10.123456789123456789 ETH", + ); + + expect( + AmountUnit.milli.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 9, + ), + "10123.456789123 mETH", + ); + + expect( + AmountUnit.micro.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 8, + ), + "10123456.78912345 µETH", + ); + + expect( + AmountUnit.nano.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 1, + ), + "10123456789.1 gwei", + ); + + expect( + AmountUnit.pico.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 18, + ), + "10123456789123.456789 mwei", + ); + + expect( + AmountUnit.femto.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 4, + ), + "10123456789123456.789 kwei", + ); + + expect( + AmountUnit.atto.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 1, + ), + "10123456789123456789 wei", + ); + }); +} From 16a6619746e57501da84c54b89fa3066d718e891 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Apr 2023 17:30:33 -0600 Subject: [PATCH 196/208] remove fromDouble constructor from amount due to possible precision errors --- lib/utilities/amount/amount.dart | 17 ---------- test/utilities/{ => amount}/amount_test.dart | 35 +------------------- 2 files changed, 1 insertion(+), 51 deletions(-) rename test/utilities/{ => amount}/amount_test.dart (85%) diff --git a/lib/utilities/amount/amount.dart b/lib/utilities/amount/amount.dart index 9688e2b79..30619bb0d 100644 --- a/lib/utilities/amount/amount.dart +++ b/lib/utilities/amount/amount.dart @@ -4,8 +4,6 @@ import 'package:decimal/decimal.dart'; import 'package:intl/number_symbols.dart'; import 'package:intl/number_symbols_data.dart'; -final _ten = BigInt.from(10); - class Amount { Amount({ required BigInt rawValue, @@ -19,12 +17,6 @@ class Amount { fractionDigits: 0, ); - /// truncate double value to [fractionDigits] places - Amount.fromDouble(double amount, {required this.fractionDigits}) - : assert(fractionDigits >= 0), - _value = - Decimal.parse(amount.toString()).shift(fractionDigits).toBigInt(); - /// truncate decimal value to [fractionDigits] places Amount.fromDecimal(Decimal amount, {required this.fractionDigits}) : assert(fractionDigits >= 0), @@ -170,15 +162,6 @@ extension DecimalAmountExt on Decimal { } } -extension DoubleAmountExt on double { - Amount toAmount({required int fractionDigits}) { - return Amount.fromDouble( - this, - fractionDigits: fractionDigits, - ); - } -} - extension IntAmountExtension on int { Amount toAmountAsRaw({required int fractionDigits}) { return Amount( diff --git a/test/utilities/amount_test.dart b/test/utilities/amount/amount_test.dart similarity index 85% rename from test/utilities/amount_test.dart rename to test/utilities/amount/amount_test.dart index 65e9b463d..c909ff122 100644 --- a/test/utilities/amount_test.dart +++ b/test/utilities/amount/amount_test.dart @@ -28,31 +28,6 @@ void main() { expect(didThrow, true); }); - test("Named fromDouble Amount Constructor tests", () { - Amount amount = Amount.fromDouble(2.0, fractionDigits: 0); - expect(amount.fractionDigits, 0); - expect(amount.raw, BigInt.two); - expect(amount.decimal, Decimal.fromInt(2)); - - amount = Amount.fromDouble(2.0, fractionDigits: 2); - expect(amount.fractionDigits, 2); - expect(amount.raw, BigInt.from(200)); - expect(amount.decimal, Decimal.fromInt(2)); - - amount = Amount.fromDouble(0.0123456789, fractionDigits: 7); - expect(amount.fractionDigits, 7); - expect(amount.raw, BigInt.from(123456)); - expect(amount.decimal, Decimal.parse("0.0123456")); - - bool didThrow = false; - try { - amount = Amount.fromDouble(2.0, fractionDigits: -1); - } catch (_) { - didThrow = true; - } - expect(didThrow, true); - }); - test("Named fromDecimal Amount Constructor tests", () { Amount amount = Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 0); expect(amount.fractionDigits, 0); @@ -89,10 +64,6 @@ void main() { Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8).toMap(), {"raw": "200000000", "fractionDigits": 8}, ); - expect( - Amount.fromDouble(2, fractionDigits: 8).toMap(), - {"raw": "200000000", "fractionDigits": 8}, - ); }); test("toJsonString", () { @@ -105,10 +76,6 @@ void main() { .toJsonString(), '{"raw":"200000000","fractionDigits":8}', ); - expect( - Amount.fromDouble(2, fractionDigits: 8).toJsonString(), - '{"raw":"200000000","fractionDigits":8}', - ); }); test("localizedStringAsFixed", () { @@ -145,7 +112,7 @@ void main() { expect( Amount.fromSerializedJsonString( '{"raw":"200000000","fractionDigits":8}'), - Amount.fromDouble(2, fractionDigits: 8), + Amount.fromDecimal(Decimal.parse("2"), fractionDigits: 8), ); }); }); From 94896dfd60cec2697c45a943fd76ba7896897b49 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 6 Apr 2023 17:49:13 -0600 Subject: [PATCH 197/208] Amount fixes --- lib/pages/send_view/send_view.dart | 5 ++++- .../coins/bitcoin/bitcoin_wallet.dart | 8 +++----- .../coins/bitcoincash/bitcoincash_wallet.dart | 8 +++----- .../coins/dogecoin/dogecoin_wallet.dart | 8 +++----- .../coins/ethereum/ethereum_wallet.dart | 17 +++++++--------- lib/services/coins/firo/firo_wallet.dart | 8 +++----- .../coins/litecoin/litecoin_wallet.dart | 8 +++----- .../coins/namecoin/namecoin_wallet.dart | 8 +++----- .../coins/particl/particl_wallet.dart | 8 +++----- .../ethereum/ethereum_token_service.dart | 8 +++----- lib/utilities/eth_commons.dart | 20 ++++++++++++------- 11 files changed, 48 insertions(+), 58 deletions(-) diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 9fa6ed18f..fc2212098 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -1533,7 +1533,10 @@ class _SendViewState extends ConsumerState { ? 0.toAmountAsRaw( fractionDigits: coin.decimals) : (baseAmount.decimal / _price) - .toDouble() + .toDecimal( + scaleOnInfinitePrecision: + coin.decimals, + ) .toAmount( fractionDigits: coin.decimals); } diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index 8831d0be6..a7dd3ed4a 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -1341,11 +1341,9 @@ class BitcoinWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, - amountString: Amount( - rawValue: BigInt.from(txData["recipientAmt"] as int), - fractionDigits: coin.decimals, - ).toJsonString(), + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index a3736bb49..a220b6db4 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1250,11 +1250,9 @@ class BitcoinCashWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, - amountString: Amount( - rawValue: BigInt.from(txData["recipientAmt"] as int), - fractionDigits: coin.decimals, - ).toJsonString(), + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index fbb769235..a4838174f 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -1116,11 +1116,9 @@ class DogecoinWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, - amountString: Amount( - rawValue: BigInt.from(txData["recipientAmt"] as int), - fractionDigits: coin.decimals, - ).toJsonString(), + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 229e35409..b5fafeff5 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -221,15 +221,14 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { 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: Amount.fromDouble( - ethBalance.getValueInUnit(web3.EtherUnit.ether), + total: Amount( + rawValue: ethBalance.getInWei, fractionDigits: coin.decimals, ), - spendable: Amount.fromDouble( - ethBalance.getValueInUnit(web3.EtherUnit.ether), + spendable: Amount( + rawValue: ethBalance.getInWei, fractionDigits: coin.decimals, ), blockedTotal: Amount( @@ -922,11 +921,9 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: TransactionType.outgoing, subType: TransactionSubType.none, - amount: txData["recipientAmt"] as int, - amountString: Amount( - rawValue: BigInt.from(txData["recipientAmt"] as int), - fractionDigits: coin.decimals, - ).toJsonString(), + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 38c99e3bb..f60dd6380 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -910,11 +910,9 @@ class FiroWallet extends CoinServiceAPI with WalletCache, WalletDB, FiroHive { timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, - amountString: Amount( - rawValue: BigInt.from(txData["recipientAmt"] as int), - fractionDigits: Coin.firo.decimals, - ).toJsonString(), + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 93f2c3b3c..881ebdd2d 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -1248,11 +1248,9 @@ class LitecoinWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, - amountString: Amount( - rawValue: BigInt.from(txData["recipientAmt"] as int), - fractionDigits: coin.decimals, - ).toJsonString(), + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index fc78afd19..c6c48308c 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -1235,11 +1235,9 @@ class NamecoinWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, - amountString: Amount( - rawValue: BigInt.from(txData["recipientAmt"] as int), - fractionDigits: coin.decimals, - ).toJsonString(), + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 8d311cc0b..6d3a2bf6c 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -1163,11 +1163,9 @@ class ParticlWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, - amountString: Amount( - rawValue: BigInt.from(txData["recipientAmt"] as int), - fractionDigits: coin.decimals, - ).toJsonString(), + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index c071b12c5..90edb1904 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -178,11 +178,9 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache { timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: TransactionType.outgoing, subType: TransactionSubType.ethToken, - amount: txData["recipientAmt"] as int, - amountString: Amount( - rawValue: BigInt.from(txData["recipientAmt"] as int), - fractionDigits: tokenContract.decimals, - ).toJsonString(), + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index 75aaada6a..9b74cdbfe 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import 'package:decimal/decimal.dart'; @@ -64,11 +62,19 @@ String getPrivateKey(String mnemonic, String mnemonicPassphrase) { } Amount estimateFee(int feeRate, int gasLimit, int decimals) { - final gweiAmount = feeRate / (pow(10, 9)); - final fee = gasLimit * gweiAmount; + final gweiAmount = feeRate.toDecimal() / (Decimal.ten.pow(9).toDecimal()); + final fee = gasLimit.toDecimal() * + gweiAmount.toDecimal( + scaleOnInfinitePrecision: Coin.ethereum.decimals, + ); //Convert gwei to ETH - final feeInWei = fee * (pow(10, 9)); - final ethAmount = feeInWei / (pow(10, decimals)); - return Amount.fromDouble(ethAmount, fractionDigits: decimals); + final feeInWei = fee * Decimal.ten.pow(9).toDecimal(); + final ethAmount = feeInWei / Decimal.ten.pow(decimals).toDecimal(); + return Amount.fromDecimal( + ethAmount.toDecimal( + scaleOnInfinitePrecision: Coin.ethereum.decimals, + ), + fractionDigits: decimals, + ); } From cbe6a5caf84b946b49fdf227fe4b600b4a919ada Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 08:45:05 -0600 Subject: [PATCH 198/208] desktop edit/add token to eth wallet --- ...view.dart => edit_wallet_tokens_view.dart} | 188 ++++++++++-------- .../sub_widgets/add_token_list.dart | 40 ---- .../sub_widgets/add_token_list_element.dart | 75 +++++-- .../restore_wallet_view.dart | 4 +- .../select_wallet_for_token_view.dart | 138 +++++++------ .../verify_recovery_phrase_view.dart | 4 +- lib/pages/token_view/my_tokens_view.dart | 4 +- lib/route_generator.dart | 17 +- 8 files changed, 262 insertions(+), 208 deletions(-) rename lib/pages/add_wallet_views/add_token_view/{add_token_view.dart => edit_wallet_tokens_view.dart} (68%) diff --git a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart similarity index 68% rename from lib/pages/add_wallet_views/add_token_view/add_token_view.dart rename to lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart index 0582440b2..4fd3db829 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart @@ -1,13 +1,17 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; @@ -27,21 +31,24 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -class AddTokenView extends ConsumerStatefulWidget { - const AddTokenView({ +class EditWalletTokensView extends ConsumerStatefulWidget { + const EditWalletTokensView({ Key? key, required this.walletId, + this.contractsToMarkSelected, }) : super(key: key); final String walletId; + final List? contractsToMarkSelected; - static const routeName = "/addToken"; + static const routeName = "/editWalletTokens"; @override - ConsumerState createState() => _AddTokenViewState(); + ConsumerState createState() => + _EditWalletTokensViewState(); } -class _AddTokenViewState extends ConsumerState { +class _EditWalletTokensViewState extends ConsumerState { late final TextEditingController _searchFieldController; late final FocusNode _searchFocusNode; @@ -82,7 +89,20 @@ class _AddTokenViewState extends ConsumerState { await ethWallet.updateTokenContracts(selectedTokens); if (mounted) { - Navigator.of(context).pop(42); + if (widget.contractsToMarkSelected == null) { + Navigator.of(context).pop(42); + } else { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopHomeView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "${ethWallet.walletName} tokens saved", + context: context, + ), + ); + } } } @@ -127,8 +147,13 @@ class _AddTokenViewState extends ConsumerState { .wallet as EthereumWallet) .getWalletTokenContractAddresses(); - for (var e in tokenEntities) { - e.selected = walletContracts.contains(e.token.address); + final shouldMarkAsSelectedContracts = [ + ...walletContracts, + ...(widget.contractsToMarkSelected ?? []), + ]; + + for (final e in tokenEntities) { + e.selected = shouldMarkAsSelectedContracts.contains(e.token.address); } super.initState(); @@ -149,10 +174,12 @@ class _AddTokenViewState extends ConsumerState { if (isDesktop) { return DesktopScaffold( - appBar: const DesktopAppBar( + appBar: DesktopAppBar( isCompactHeight: false, - leading: AppBarBackButton(), - trailing: ExitToMyStackButton(), + leading: const AppBarBackButton(), + trailing: widget.contractsToMarkSelected == null + ? const ExitToMyStackButton() + : null, ), body: Column( children: [ @@ -169,83 +196,83 @@ class _AddTokenViewState extends ConsumerState { child: RoundedWhiteContainer( radiusMultiplier: 2, padding: const EdgeInsets.only( - left: 16, - top: 16, - right: 16, + left: 20, + top: 20, + right: 20, bottom: 0, ), child: Column( children: [ - Padding( - padding: const EdgeInsets.all(4.0), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: + STextStyles.desktopTextMedium(context).copyWith( + height: 2, ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchFieldController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: - STextStyles.desktopTextMedium(context).copyWith( - height: 2, + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.symmetric( + vertical: 10, ), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.symmetric( - vertical: 10, + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + // vertical: 20, ), - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - // vertical: 20, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 24, - height: 24, - color: Theme.of(context) - .extension()! - .textFieldDefaultSearchIconLeft, - ), + child: SvgPicture.asset( + Assets.svg.search, + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, ), - suffixIcon: _searchFieldController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 10), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon( - width: 24, - height: 24, - ), - onTap: () async { - setState(() { - _searchFieldController.text = - ""; - _searchTerm = ""; - }); - }, + ), + suffixIcon: _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 10), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon( + width: 24, + height: 24, ), - ], - ), + onTap: () async { + setState(() { + _searchFieldController.text = + ""; + _searchTerm = ""; + }); + }, + ), + ], ), - ) - : null, - ), + ), + ) + : null, ), ), ), + const SizedBox( + height: 12, + ), Expanded( child: AddTokenList( walletId: widget.walletId, @@ -253,19 +280,22 @@ class _AddTokenViewState extends ConsumerState { addFunction: _addToken, ), ), + const SizedBox( + height: 12, + ), ], ), ), ), ), const SizedBox( - height: 16, + height: 26, ), SizedBox( height: 70, width: 480, child: PrimaryButton( - label: "Next", + label: widget.contractsToMarkSelected != null ? "Save" : "Next", onPressed: onNextPressed, ), ), @@ -384,7 +414,9 @@ class _AddTokenViewState extends ConsumerState { height: 16, ), PrimaryButton( - label: "Next", + label: widget.contractsToMarkSelected != null + ? "Save" + : "Next", onPressed: onNextPressed, ), ], diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart index b472f1f63..b3280c066 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart @@ -31,46 +31,6 @@ class AddTokenList extends StatelessWidget { AddCustomTokenSelector( addFunction: addFunction, ), - // Padding( - // padding: const EdgeInsets.symmetric(vertical: 4), - // child: RawMaterialButton( - // fillColor: - // Theme.of(context).extension()!.popupBG, - // elevation: 0, - // focusElevation: 0, - // hoverElevation: 0, - // highlightElevation: 0, - // constraints: const BoxConstraints(), - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular( - // Constants.size.circularBorderRadius, - // ), - // ), - // onPressed: addFunction, - // child: Padding( - // padding: const EdgeInsets.all(12), - // child: Row( - // children: [ - // SvgPicture.asset( - // Assets.svg.circlePlusFilled, - // color: Theme.of(context) - // .extension()! - // .textDark, - // width: 24, - // height: 24, - // ), - // const SizedBox( - // width: 12, - // ), - // Text( - // "Add custom token", - // style: STextStyles.w600_14(context), - // ), - // ], - // ), - // ), - // ), - // ), ], ), child: Padding( diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart index efe0c4b3a..359ad4900 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -7,6 +7,9 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -27,6 +30,8 @@ class AddTokenListElement extends StatefulWidget { } class _AddTokenListElementState extends State { + final bool isDesktop = Util.isDesktop; + @override Widget build(BuildContext context) { final currency = ExchangeDataLoadingService.instance.isar.currencies @@ -41,7 +46,14 @@ class _AddTokenListElementState extends State { .imageIsNotEmpty() .findFirstSync(); + final String mainLabel = widget.data.token.name; + final double iconSize = isDesktop ? 32 : 24; + return RoundedWhiteContainer( + padding: EdgeInsets.all(isDesktop ? 16 : 12), + borderColor: isDesktop + ? Theme.of(context).extension()!.backgroundAppBar + : null, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -50,39 +62,66 @@ class _AddTokenListElementState extends State { currency != null ? SvgPicture.network( currency.image, - width: 24, - height: 24, + width: iconSize, + height: iconSize, ) : SvgPicture.asset( widget.data.token.symbol == "BNB" ? Assets.svg.bnbIcon : Assets.svg.ethereum, - width: 24, - height: 24, + width: iconSize, + height: iconSize, ), const SizedBox( width: 12, ), - Text( - "${widget.data.token.name} (${widget.data.token.symbol})", - style: STextStyles.w600_14(context), - overflow: TextOverflow.ellipsis, + ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + const SizedBox( + height: 2, + ), + Text( + widget.data.token.symbol, + style: STextStyles.desktopTextExtraExtraSmall(context), + overflow: TextOverflow.ellipsis, + ), + ], + ), + child: Text( + isDesktop + ? mainLabel + : "$mainLabel (${widget.data.token.symbol})", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.w600_14(context), + overflow: TextOverflow.ellipsis, + ), ), ], ), const SizedBox( width: 4, ), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: widget.data.selected, - onValueChanged: (newValue) { - widget.data.selected = newValue; - }, - ), - ), + isDesktop + ? Checkbox( + value: widget.data.selected, + onChanged: (newValue) => + setState(() => widget.data.selected = newValue!), + ) + : SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: widget.data.selected, + onValueChanged: (newValue) { + widget.data.selected = newValue; + }, + ), + ), ], ), ); diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index dda0359b3..ec0d5bc2c 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -12,7 +12,7 @@ import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart'; @@ -351,7 +351,7 @@ class _RestoreWalletViewState extends ConsumerState { if (manager.coin == Coin.ethereum) { unawaited( Navigator.of(context).pushNamed( - AddTokenView.routeName, + EditWalletTokensView.routeName, arguments: manager.walletId, ), ); diff --git a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart index c73393e4c..fbb23c2ef 100644 --- a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart +++ b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart @@ -3,13 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; -import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; -import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_service_provider.dart'; -import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/wallets_service.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -24,6 +21,7 @@ import 'package:stackwallet/widgets/eth_wallet_radio.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; +import 'package:tuple/tuple.dart'; final newEthWalletTriggerTempUntilHiveCompletelyDeleted = StateProvider((ref) => false); @@ -52,21 +50,12 @@ class _SelectWalletForTokenViewState String? _selectedWalletId; void _onContinue() { - final wallet = ref - .read(walletsChangeNotifierProvider) - .getManager(_selectedWalletId!) - .wallet as EthereumWallet; - - final tokenSet = wallet.getWalletTokenContractAddresses().toSet(); - tokenSet.add(widget.entity.token.address); - wallet.updateWalletTokenContractAddresses(tokenSet.toList()); - - Navigator.of(context).pushNamed(HomeView.routeName); - showFloatingFlushBar( - type: FlushBarType.success, - message: - "${widget.entity.name} (${widget.entity.ticker}) added to ${wallet.walletName}", - context: context, + Navigator.of(context).pushNamed( + EditWalletTokensView.routeName, + arguments: Tuple2( + _selectedWalletId!, + [widget.entity.token.address], + ), ); } @@ -170,17 +159,23 @@ class _SelectWalletForTokenViewState leading: AppBarBackButton(), ), body: SizedBox( - width: 480, + width: 500, child: child, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (isDesktop) + const SizedBox( + height: 24, + ), Text( "Select Ethereum wallet", textAlign: TextAlign.center, - style: STextStyles.pageTitleH1(context), + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), SizedBox( height: isDesktop ? 16 : 8, @@ -188,7 +183,9 @@ class _SelectWalletForTokenViewState Text( "You are adding an ETH token.", textAlign: TextAlign.center, - style: STextStyles.subtitle(context), + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), ), const SizedBox( height: 8, @@ -196,62 +193,77 @@ class _SelectWalletForTokenViewState Text( "You must choose an Ethereum wallet in order to use ${widget.entity.name}", textAlign: TextAlign.center, - style: STextStyles.subtitle(context), + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), ), SizedBox( height: isDesktop ? 60 : 16, ), ethWalletIds.isEmpty ? RoundedWhiteContainer( + padding: EdgeInsets.all(isDesktop ? 16 : 12), child: Text( _hasEthWallets ? "All current Ethereum wallets already have ${widget.entity.name}" : "You do not have any Ethereum wallets", - style: STextStyles.label(context), + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.label(context), ), ) - : Expanded( - child: Column( - children: [ - RoundedWhiteContainer( - padding: const EdgeInsets.all(8), - child: ListView.separated( - itemCount: ethWalletIds.length, - shrinkWrap: true, - separatorBuilder: (_, __) => SizedBox( - height: isDesktop ? 12 : 6, - ), - itemBuilder: (_, index) { - return RoundedContainer( - padding: const EdgeInsets.all(8), - onPressed: () { - setState(() { - _selectedWalletId = ethWalletIds[index]; - }); - }, - color: isDesktop - ? Colors.transparent - : _selectedWalletId == ethWalletIds[index] - ? Theme.of(context) - .extension()! - .highlight - : Colors.transparent, - child: isDesktop - ? EthWalletRadio( - walletId: ethWalletIds[index], - selectedWalletId: _selectedWalletId, - ) - : WalletInfoRow( - walletId: ethWalletIds[index], - ), - ); - }, + : ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(8), + child: child, ), - ), - ], + ], + ), + ), + child: ListView.separated( + itemCount: ethWalletIds.length, + shrinkWrap: true, + separatorBuilder: (_, __) => SizedBox( + height: isDesktop ? 12 : 6, + ), + itemBuilder: (_, index) { + return RoundedContainer( + padding: EdgeInsets.all(isDesktop ? 16 : 8), + onPressed: () { + setState(() { + _selectedWalletId = ethWalletIds[index]; + }); + }, + color: isDesktop + ? Theme.of(context) + .extension()! + .popupBG + : _selectedWalletId == ethWalletIds[index] + ? Theme.of(context) + .extension()! + .highlight + : Colors.transparent, + child: isDesktop + ? EthWalletRadio( + walletId: ethWalletIds[index], + selectedWalletId: _selectedWalletId, + ) + : WalletInfoRow( + walletId: ethWalletIds[index], + ), + ); + }, ), ), - if (ethWalletIds.isEmpty) + if (ethWalletIds.isEmpty || isDesktop) + const SizedBox( + height: 16, + ), + if (isDesktop) const SizedBox( height: 16, ), diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 6e12a1402..6940fb120 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table.dart'; @@ -134,7 +134,7 @@ class _VerifyRecoveryPhraseViewState if (widget.manager.coin == Coin.ethereum) { unawaited( Navigator.of(context).pushNamed( - AddTokenView.routeName, + EditWalletTokensView.routeName, arguments: widget.manager.walletId, ), ); diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index 2d0782574..3dd10ed01 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_tokens_list.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; @@ -147,7 +147,7 @@ class _MyTokensViewState extends ConsumerState { ), onPressed: () async { final result = await Navigator.of(context).pushNamed( - AddTokenView.routeName, + EditWalletTokensView.routeName, arguments: widget.walletId, ); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index bc41dfa9f..e5d4d00e6 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -12,7 +12,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; @@ -217,17 +217,28 @@ class RouteGenerator { builder: (_) => const AddWalletView(), settings: RouteSettings(name: settings.name)); - case AddTokenView.routeName: + case EditWalletTokensView.routeName: if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddTokenView( + builder: (_) => EditWalletTokensView( walletId: args, ), settings: RouteSettings( name: settings.name, ), ); + } else if (args is Tuple2>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => EditWalletTokensView( + walletId: args.item1, + contractsToMarkSelected: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); From c5a5853431e9f073a7353ba44385d22eec9373e6 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 08:55:24 -0600 Subject: [PATCH 199/208] handle new eth wallet navigation on desktop --- .../verify_recovery_phrase_view.dart | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 6940fb120..33d3628d6 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -112,11 +112,27 @@ class _VerifyRecoveryPhraseViewState if (mounted) { if (isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); + if (isCreateSpecialEthWallet) { + Navigator.of(context).popUntil( + ModalRoute.withName( + SelectWalletForTokenView.routeName, + ), + ); + } else { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + if (widget.manager.coin == Coin.ethereum) { + unawaited( + Navigator.of(context).pushNamed( + EditWalletTokensView.routeName, + arguments: widget.manager.walletId, + ), + ); + } + } } else { if (isCreateSpecialEthWallet) { Navigator.of(context).popUntil( From b820b113e4121f29a57731d0f7980ee6fb09af3f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 09:14:06 -0600 Subject: [PATCH 200/208] desktop wallet alignment fix --- .../wallet_view/desktop_wallet_view.dart | 61 +++++++++++++++++- .../wallet_view/sub_widgets/my_wallet.dart | 12 ---- .../recent_desktop_transactions.dart | 64 ------------------- 3 files changed, 58 insertions(+), 79 deletions(-) delete mode 100644 lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 49d135c4f..18d211297 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -4,11 +4,12 @@ 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/pages/wallet_view/sub_widgets/transactions_list.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart'; import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; @@ -19,10 +20,12 @@ import 'package:stackwallet/services/event_bus/global_event_bus.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/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.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/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; @@ -47,6 +50,8 @@ class DesktopWalletView extends ConsumerStatefulWidget { } class _DesktopWalletViewState extends ConsumerState { + static const double sendReceiveColumnWidth = 460; + late final TextEditingController controller; late final EventBus eventBus; @@ -255,11 +260,58 @@ class _DesktopWalletViewState extends ConsumerState { const SizedBox( height: 24, ), + Row( + children: [ + SizedBox( + width: sendReceiveColumnWidth, + child: Text( + "My wallet", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recent transactions", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + CustomTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 14, + ), Expanded( child: Row( children: [ SizedBox( - width: 450, + width: sendReceiveColumnWidth, child: MyWallet( walletId: widget.walletId, ), @@ -268,7 +320,10 @@ class _DesktopWalletViewState extends ConsumerState { width: 16, ), Expanded( - child: RecentDesktopTransactions( + child: TransactionsList( + managerProvider: ref.watch( + walletsChangeNotifierProvider.select((value) => + value.getManagerProvider(widget.walletId))), walletId: widget.walletId, ), ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 53192f63e..b82aa3d02 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -4,7 +4,6 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class MyWallet extends StatefulWidget { @@ -29,17 +28,6 @@ class _MyWalletState extends State { return ListView( primary: false, children: [ - Text( - "My wallet", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconLeft, - ), - ), - const SizedBox( - height: 16, - ), Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart deleted file mode 100644 index dca501e25..000000000 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; -import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; - -class RecentDesktopTransactions extends ConsumerStatefulWidget { - const RecentDesktopTransactions({ - Key? key, - required this.walletId, - }) : super(key: key); - - final String walletId; - - @override - ConsumerState createState() => - _RecentDesktopTransactionsState(); -} - -class _RecentDesktopTransactionsState - extends ConsumerState { - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recent transactions", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconLeft, - ), - ), - CustomTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: widget.walletId, - ); - }, - ), - ], - ), - const SizedBox( - height: 16, - ), - Expanded( - child: TransactionsList( - managerProvider: ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManagerProvider(widget.walletId))), - walletId: widget.walletId, - ), - ), - ], - ); - } -} From 387e2d84037c5e77e6953385be3323f61880de18 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 13:38:55 -0600 Subject: [PATCH 201/208] desktop send/receive + transactions for eth view --- .../sub_widgets/transactions_list.dart | 1 + .../wallet_view/desktop_token_view.dart | 87 +++++---- .../wallet_view/desktop_wallet_view.dart | 1 + .../wallet_view/sub_widgets/my_wallet.dart | 126 +++++++------ .../sub_widgets/send_receive_tab_menu.dart | 165 ------------------ lib/widgets/custom_tab_view.dart | 151 ++++++++++++++++ 6 files changed, 277 insertions(+), 254 deletions(-) delete mode 100644 lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart create mode 100644 lib/widgets/custom_tab_view.dart diff --git a/lib/pages/wallet_view/sub_widgets/transactions_list.dart b/lib/pages/wallet_view/sub_widgets/transactions_list.dart index 3d9b9c95e..bdfd00394 100644 --- a/lib/pages/wallet_view/sub_widgets/transactions_list.dart +++ b/lib/pages/wallet_view/sub_widgets/transactions_list.dart @@ -252,6 +252,7 @@ class _TransactionsListState extends ConsumerState { }, child: Util.isDesktop ? ListView.separated( + shrinkWrap: true, itemBuilder: (context, index) { BorderRadius? radius; if (_transactions2.length == 1) { diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart index d268474d3..469aeed71 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart @@ -39,6 +39,8 @@ class DesktopTokenView extends ConsumerStatefulWidget { } class _DesktopTokenViewState extends ConsumerState { + static const double sendReceiveColumnWidth = 460; + late final WalletSyncStatus initialSyncStatus; @override @@ -168,11 +170,58 @@ class _DesktopTokenViewState extends ConsumerState { const SizedBox( height: 24, ), + Row( + children: [ + SizedBox( + width: sendReceiveColumnWidth, + child: Text( + "My wallet", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recent transactions", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + CustomTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 14, + ), Expanded( child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: 450, + width: sendReceiveColumnWidth, child: MyWallet( walletId: widget.walletId, contractAddress: ref.watch( @@ -186,40 +235,8 @@ class _DesktopTokenViewState extends ConsumerState { width: 16, ), Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recent transactions", - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconLeft, - ), - ), - CustomTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: widget.walletId, - ); - }, - ), - ], - ), - const SizedBox( - height: 16, - ), - Expanded( - child: TokenTransactionsList( - walletId: widget.walletId, - ), - ), - ], + child: TokenTransactionsList( + walletId: widget.walletId, ), ), ], diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 18d211297..edfb0a1cc 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -309,6 +309,7 @@ class _DesktopWalletViewState extends ConsumerState { ), Expanded( child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: sendReceiveColumnWidth, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index b82aa3d02..f57ad8c2b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/widgets/custom_tab_view.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; -class MyWallet extends StatefulWidget { +class MyWallet extends ConsumerStatefulWidget { const MyWallet({ Key? key, required this.walletId, @@ -17,67 +20,82 @@ class MyWallet extends StatefulWidget { final String? contractAddress; @override - State createState() => _MyWalletState(); + ConsumerState createState() => _MyWalletState(); } -class _MyWalletState extends State { - int _selectedIndex = 0; +class _MyWalletState extends ConsumerState { + final titles = [ + "Send", + "Receive", + ]; + + late final bool isEth; + + @override + void initState() { + isEth = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .coin == + Coin.ethereum; + + if (isEth && widget.contractAddress == null) { + titles.add("Transactions"); + } + + super.initState(); + } @override Widget build(BuildContext context) { return ListView( primary: false, children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - child: SendReceiveTabMenu( - onChanged: (index) { - setState(() { - _selectedIndex = index; - }); - }, - ), - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - child: AnimatedCrossFade( - firstChild: Padding( - key: const Key("desktopSendViewPortKey"), - padding: const EdgeInsets.all(20), - child: widget.contractAddress == null - ? DesktopSend( - walletId: widget.walletId, + RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: CustomTabView( + titles: titles, + children: [ + widget.contractAddress == null + ? Padding( + padding: const EdgeInsets.all(20), + child: DesktopSend( + walletId: widget.walletId, + ), ) - : DesktopTokenSend( - walletId: widget.walletId, + : Padding( + padding: const EdgeInsets.all(20), + child: DesktopTokenSend( + walletId: widget.walletId, + ), ), - ), - secondChild: Padding( - key: const Key("desktopReceiveViewPortKey"), - padding: const EdgeInsets.all(20), - child: DesktopReceive( - walletId: widget.walletId, - contractAddress: widget.contractAddress, + Padding( + padding: const EdgeInsets.all(20), + child: DesktopReceive( + walletId: widget.walletId, + contractAddress: widget.contractAddress, + ), ), - ), - crossFadeState: _selectedIndex == 0 - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 250), + if (isEth && widget.contractAddress == null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height - 362, + ), + child: TransactionsList( + walletId: widget.walletId, + managerProvider: ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManagerProvider( + widget.walletId, + ), + ), + ), + ), + ), + ), + ], ), ), ], diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart deleted file mode 100644 index f42ed297d..000000000 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; - -class SendReceiveTabMenu extends StatefulWidget { - const SendReceiveTabMenu({ - Key? key, - this.initialIndex = 0, - this.onChanged, - }) : super(key: key); - - final int initialIndex; - final void Function(int)? onChanged; - - @override - State createState() => _SendReceiveTabMenuState(); -} - -class _SendReceiveTabMenuState extends State { - late int _selectedIndex; - - void _onChanged(int newIndex) { - if (_selectedIndex != newIndex) { - setState(() { - _selectedIndex = newIndex; - }); - widget.onChanged?.call(_selectedIndex); - } - } - - @override - void initState() { - _selectedIndex = widget.initialIndex; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => _onChanged(0), - child: Container( - color: Colors.transparent, - child: Column( - children: [ - const SizedBox( - height: 16, - ), - AnimatedCrossFade( - firstChild: Text( - "Send", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, - ), - ), - secondChild: Text( - "Send", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - crossFadeState: _selectedIndex == 0 - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 250), - ), - const SizedBox( - height: 19, - ), - Container( - height: 2, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - ), - ), - ], - ), - ), - ), - ), - ), - Expanded( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => _onChanged(1), - child: Container( - color: Colors.transparent, - child: Column( - children: [ - const SizedBox( - height: 16, - ), - AnimatedCrossFade( - firstChild: Text( - "Receive", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, - ), - ), - secondChild: Text( - "Receive", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - crossFadeState: _selectedIndex == 1 - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 250), - ), - const SizedBox( - height: 19, - ), - Stack( - children: [ - Container( - height: 2, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - ), - ), - AnimatedSlide( - offset: Offset(_selectedIndex == 0 ? -1 : 0, 0), - duration: const Duration(milliseconds: 250), - child: Container( - height: 2, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .accentColorBlue), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/widgets/custom_tab_view.dart b/lib/widgets/custom_tab_view.dart new file mode 100644 index 000000000..49fd81d18 --- /dev/null +++ b/lib/widgets/custom_tab_view.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class CustomTabView extends StatefulWidget { + const CustomTabView({ + Key? key, + required this.titles, + required this.children, + this.initialIndex = 0, + this.childPadding, + }) : assert(titles.length == children.length), + super(key: key); + + final List titles; + final List children; + final int initialIndex; + final EdgeInsets? childPadding; + + @override + State createState() => _CustomTabViewState(); +} + +class _CustomTabViewState extends State { + final _key = GlobalKey(); + late int _selectedIndex; + + static const duration = Duration(milliseconds: 250); + + @override + void initState() { + _selectedIndex = widget.initialIndex; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + for (int i = 0; i < widget.titles.length; i++) + Expanded( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => setState(() => _selectedIndex = i), + child: Container( + color: Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 16, + ), + AnimatedCrossFade( + firstChild: Text( + widget.titles[i], + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorBlue, + ), + ), + secondChild: Text( + widget.titles[i], + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + crossFadeState: _selectedIndex == i + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 250), + ), + ], + ), + ), + ), + ), + ), + ], + ), + const SizedBox( + height: 19, + ), + Stack( + children: [ + Container( + height: 2, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .backgroundAppBar, + ), + ), + AnimatedSlide( + offset: Offset(_selectedIndex.toDouble(), 0), + duration: duration, + child: Container( + height: 2, + width: constraints.maxWidth / widget.titles.length, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .accentColorBlue, + ), + ), + ), + ], + ), + AnimatedSwitcher( + key: _key, + duration: duration, + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + layoutBuilder: (currentChild, prevChildren) { + return Stack( + alignment: Alignment.topCenter, + children: [ + ...prevChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: AnimatedAlign( + key: Key(widget.titles[_selectedIndex]), + alignment: Alignment.topCenter, + duration: duration, + child: Padding( + padding: widget.childPadding ?? EdgeInsets.zero, + child: widget.children[_selectedIndex], + ), + ), + ), + ], + ), + ); + } +} From 1305b9f37c4af16401cf7ece049203bbd33d7a39 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 14:13:28 -0600 Subject: [PATCH 202/208] token balance json serialization fix --- lib/models/token_balance.dart | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/lib/models/token_balance.dart b/lib/models/token_balance.dart index 727a29d11..f2606f04d 100644 --- a/lib/models/token_balance.dart +++ b/lib/models/token_balance.dart @@ -16,38 +16,13 @@ class TokenBalance extends Balance { final String contractAddress; - // @override - // Decimal getTotal({bool includeBlocked = false}) => - // Format.satoshisToEthTokenAmount( - // includeBlocked ? total : total - blockedTotal, - // decimalPlaces, - // ); - // - // @override - // Decimal getSpendable() => Format.satoshisToEthTokenAmount( - // spendable, - // decimalPlaces, - // ); - // - // @override - // Decimal getPending() => Format.satoshisToEthTokenAmount( - // pendingSpendable, - // decimalPlaces, - // ); - // - // @override - // Decimal getBlocked() => Format.satoshisToEthTokenAmount( - // blockedTotal, - // decimalPlaces, - // ); - @override String toJsonIgnoreCoin() => jsonEncode({ "contractAddress": contractAddress, - "total": total, - "spendable": spendable, - "blockedTotal": blockedTotal, - "pendingSpendable": pendingSpendable, + "total": total.toJsonString(), + "spendable": spendable.toJsonString(), + "blockedTotal": blockedTotal.toJsonString(), + "pendingSpendable": pendingSpendable.toJsonString(), }); factory TokenBalance.fromJson( From 6beb4ec85910dabee318ad7360747d6dd4f97257 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 14:23:12 -0600 Subject: [PATCH 203/208] desktop popup edit tokens view --- .../edit_wallet_tokens_view.dart | 306 +++++++++++------- 1 file changed, 190 insertions(+), 116 deletions(-) diff --git a/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart index 4fd3db829..7f8fd4b53 100644 --- a/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart @@ -22,10 +22,14 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; @@ -36,10 +40,12 @@ class EditWalletTokensView extends ConsumerStatefulWidget { Key? key, required this.walletId, this.contractsToMarkSelected, + this.isDesktopPopup = false, }) : super(key: key); final String walletId; final List? contractsToMarkSelected; + final bool isDesktopPopup; static const routeName = "/editWalletTokens"; @@ -173,136 +179,204 @@ class _EditWalletTokensViewState extends ConsumerState { .select((value) => value.getManager(widget.walletId).walletName)); if (isDesktop) { - return DesktopScaffold( - appBar: DesktopAppBar( - isCompactHeight: false, - leading: const AppBarBackButton(), - trailing: widget.contractsToMarkSelected == null - ? const ExitToMyStackButton() - : null, - ), - body: Column( - children: [ - AddTokenText( - isDesktop: true, - walletName: walletName, - ), - const SizedBox( - height: 16, - ), - Expanded( - child: SizedBox( - width: 480, - child: RoundedWhiteContainer( - radiusMultiplier: 2, - padding: const EdgeInsets.only( - left: 20, - top: 20, - right: 20, - bottom: 0, + return ConditionalParent( + condition: !widget.isDesktopPopup, + builder: (child) => DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: false, + leading: const AppBarBackButton(), + trailing: widget.contractsToMarkSelected == null + ? const ExitToMyStackButton() + : null, + ), + body: SizedBox( + width: 480, + child: RoundedWhiteContainer( + radiusMultiplier: 2, + padding: const EdgeInsets.only( + left: 20, + top: 20, + right: 20, + bottom: 0, + ), + child: Column( + children: [ + AddTokenText( + isDesktop: true, + walletName: walletName, ), - child: Column( + const SizedBox( + height: 16, + ), + Expanded( + child: child, + ), + const SizedBox( + height: 26, + ), + SizedBox( + height: 70, + width: 480, + child: PrimaryButton( + label: widget.contractsToMarkSelected != null + ? "Save" + : "Next", + onPressed: onNextPressed, + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ), + ), + ), + child: ConditionalParent( + condition: widget.isDesktopPopup, + builder: (child) => DesktopDialog( + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Edit tokens", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: child, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: _searchFieldController, - focusNode: _searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: - STextStyles.desktopTextMedium(context).copyWith( - height: 2, - ), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - ).copyWith( - contentPadding: const EdgeInsets.symmetric( - vertical: 10, - ), - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - // vertical: 20, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 24, - height: 24, - color: Theme.of(context) - .extension()! - .textFieldDefaultSearchIconLeft, - ), - ), - suffixIcon: _searchFieldController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 10), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon( - width: 24, - height: 24, - ), - onTap: () async { - setState(() { - _searchFieldController.text = - ""; - _searchTerm = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), + Expanded( + child: SecondaryButton( + label: "Add custom token", + buttonHeight: ButtonHeight.l, + onPressed: _addToken, ), ), const SizedBox( - height: 12, + width: 16, ), Expanded( - child: AddTokenList( - walletId: widget.walletId, - items: filter(_searchTerm, tokenEntities), - addFunction: _addToken, + child: PrimaryButton( + label: "Done", + buttonHeight: ButtonHeight.l, + onPressed: onNextPressed, ), ), - const SizedBox( - height: 12, - ), ], ), ), + const SizedBox( + height: 32, + ), + ], + ), + ), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.symmetric( + vertical: 10, + ), + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + // vertical: 20, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, + ), + ), + suffixIcon: _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 10), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon( + width: 24, + height: 24, + ), + onTap: () async { + setState(() { + _searchFieldController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), ), - ), - const SizedBox( - height: 26, - ), - SizedBox( - height: 70, - width: 480, - child: PrimaryButton( - label: widget.contractsToMarkSelected != null ? "Save" : "Next", - onPressed: onNextPressed, + const SizedBox( + height: 12, ), - ), - const SizedBox( - height: 32, - ), - ], + Expanded( + child: AddTokenList( + walletId: widget.walletId, + items: filter(_searchTerm, tokenEntities), + addFunction: widget.isDesktopPopup ? null : _addToken, + ), + ), + const SizedBox( + height: 12, + ), + ], + ), ), ); } else { From 6754c2ebdf94eba6a9aeac3ac50311f85431382f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 15:08:06 -0600 Subject: [PATCH 204/208] add token functionality to desktop eth wallet view --- .../sub_widgets/add_token_list.dart | 6 +- lib/pages/token_view/my_tokens_view.dart | 371 ++++++++---------- .../sub_widgets/my_token_select_item.dart | 125 +++--- .../sub_widgets/my_tokens_list.dart | 7 +- .../wallet_view/desktop_wallet_view.dart | 43 +- 5 files changed, 276 insertions(+), 276 deletions(-) diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart index b3280c066..a8532f307 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart @@ -13,7 +13,7 @@ class AddTokenList extends StatelessWidget { final String walletId; final List items; - final VoidCallback addFunction; + final VoidCallback? addFunction; @override Widget build(BuildContext context) { @@ -23,13 +23,13 @@ class AddTokenList extends StatelessWidget { itemCount: items.length, itemBuilder: (ctx, index) { return ConditionalParent( - condition: index == items.length - 1, + condition: index == items.length - 1 && addFunction != null, builder: (child) => Column( mainAxisSize: MainAxisSize.min, children: [ child, AddCustomTokenSelector( - addFunction: addFunction, + addFunction: addFunction!, ), ], ), diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index 3dd10ed01..78fa352d0 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -12,10 +12,9 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; -import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; -import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; @@ -34,6 +33,8 @@ class MyTokensView extends ConsumerStatefulWidget { } class _MyTokensViewState extends ConsumerState { + final bool isDesktop = Util.isDesktop; + late final String walletAddress; late final TextEditingController _searchController; final searchFieldFocusNode = FocusNode(); @@ -56,222 +57,180 @@ class _MyTokensViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final isDesktop = Util.isDesktop; - - return MasterScaffold( - background: Theme.of(context).extension()!.background, - isDesktop: isDesktop, - appBar: isDesktop - ? DesktopAppBar( - isCompactHeight: true, - background: Theme.of(context).extension()!.popupBG, - leading: Row( - children: [ - const SizedBox( - width: 32, - ), - AppBarIconButton( - size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "${ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).walletName), + )} Tokens", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addTokenAppBarIconButtonKey"), + size: 36, shadows: const [], + color: + Theme.of(context).extension()!.background, icon: SvgPicture.asset( - Assets.svg.arrowLeft, - width: 18, - height: 18, + Assets.svg.circlePlusDark, color: Theme.of(context) .extension()! - .topNavIconPrimary, + .accentColorDark, + width: 20, + height: 20, ), - onPressed: Navigator.of(context).pop, - ), - const SizedBox( - width: 12, - ), - Text( - "${ref.watch( - walletsChangeNotifierProvider.select((value) => - value.getManager(widget.walletId).walletName), - )} Tokens", - style: STextStyles.desktopH3(context), - ), - ], - ), - ) - : AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75)); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "${ref.watch( - walletsChangeNotifierProvider.select( - (value) => value.getManager(widget.walletId).walletName), - )} Tokens", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 20, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("addTokenAppBarIconButtonKey"), - size: 36, - shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: SvgPicture.asset( - Assets.svg.circlePlusDark, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () async { - final result = await Navigator.of(context).pushNamed( - EditWalletTokensView.routeName, - arguments: widget.walletId, - ); + onPressed: () async { + final result = await Navigator.of(context).pushNamed( + EditWalletTokensView.routeName, + arguments: widget.walletId, + ); - if (mounted && result == 42) { - setState(() {}); - } - }, + if (mounted && result == 42) { + setState(() {}); + } + }, + ), + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: child, + ), + ), + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(isDesktop ? 0 : 4), + child: Row( + children: [ + ConditionalParent( + condition: isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Search...", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 12 : 10, + vertical: isDesktop ? 18 : 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: isDesktop ? 20 : 16, + height: isDesktop ? 20 : 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), ), ), ), ], ), - body: Padding( - padding: EdgeInsets.only( - left: isDesktop ? 20 : 12, - top: isDesktop ? 20 : 12, - right: isDesktop ? 20 : 12, - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: Row( - children: [ - ConditionalParent( - condition: isDesktop, - builder: (child) => SizedBox( - width: 570, - child: child, - ), - child: ConditionalParent( - condition: !isDesktop, - builder: (child) => Expanded( - child: child, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: _searchController, - focusNode: searchFieldFocusNode, - onChanged: (value) { - setState(() { - _searchString = value; - }); - }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: standardInputDecoration( - "Search...", - searchFieldFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 12 : 10, - vertical: isDesktop ? 18 : 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: isDesktop ? 20 : 16, - height: isDesktop ? 20 : 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - ), - ), - if (isDesktop) - const SizedBox( - width: 20, - ), - // const NoTransActionsFound(), - ], - ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: MyTokensList( + walletId: widget.walletId, + searchTerm: _searchString, + tokenContracts: ref + .watch(walletsChangeNotifierProvider.select((value) => value + .getManager(widget.walletId) + .wallet as EthereumWallet)) + .getWalletTokenContractAddresses(), ), - const SizedBox( - height: 8, - ), - Expanded( - child: MyTokensList( - walletId: widget.walletId, - searchTerm: _searchString, - tokenContracts: ref - .watch(walletsChangeNotifierProvider.select((value) => value - .getManager(widget.walletId) - .wallet as EthereumWallet)) - .getWalletTokenContractAddresses(), - ), - ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index fa01e031b..f6a327f52 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/ethereum/cached_eth_token_balance.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.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/enums/coin_enum.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class MyTokenSelectItem extends ConsumerStatefulWidget { @@ -31,8 +31,38 @@ class MyTokenSelectItem extends ConsumerStatefulWidget { } class _MyTokenSelectItemState extends ConsumerState { + final bool isDesktop = Util.isDesktop; + late final CachedEthTokenBalance cachedBalance; + void _onPressed() async { + ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( + token: widget.token, + secureStore: ref.read(secureStoreProvider), + ethWallet: ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as EthereumWallet, + tracker: TransactionNotificationTracker( + walletId: widget.walletId, + ), + ); + + await showLoading( + whileFuture: ref.read(tokenServiceProvider)!.initialize(), + context: context, + isDesktop: isDesktop, + message: "Loading ${widget.token.name}", + ); + + if (mounted) { + await Navigator.of(context).pushNamed( + isDesktop ? DesktopTokenView.routeName : TokenView.routeName, + arguments: widget.walletId, + ); + } + } + @override void initState() { cachedBalance = CachedEthTokenBalance(widget.walletId, widget.token); @@ -57,47 +87,23 @@ class _MyTokenSelectItemState extends ConsumerState { padding: const EdgeInsets.all(0), child: MaterialButton( key: Key("walletListItemButtonKey_${widget.token.symbol}"), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), + padding: isDesktop + ? const EdgeInsets.symmetric(horizontal: 28, vertical: 24) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 13), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ), - onPressed: () async { - ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( - token: widget.token, - secureStore: ref.read(secureStoreProvider), - ethWallet: ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .wallet as EthereumWallet, - tracker: TransactionNotificationTracker( - walletId: widget.walletId, - ), - ); - - await showLoading( - whileFuture: ref.read(tokenServiceProvider)!.initialize(), - context: context, - message: "Loading ${widget.token.name}", - ); - - if (mounted) { - await Navigator.of(context).pushNamed( - TokenView.routeName, - arguments: widget.walletId, - ); - } - }, + onPressed: _onPressed, child: Row( children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: Coin.ethereum), - width: 28, - height: 28, + EthTokenIcon( + contractAddress: widget.token.address, + size: isDesktop ? 32 : 28, ), - const SizedBox( - width: 10, + SizedBox( + width: isDesktop ? 12 : 10, ), Expanded( child: Consumer( @@ -109,7 +115,9 @@ class _MyTokenSelectItemState extends ConsumerState { children: [ Text( widget.token.name, - style: STextStyles.titleBold12(context), + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + : STextStyles.titleBold12(context), ), const Spacer(), Text( @@ -121,33 +129,44 @@ class _MyTokenSelectItemState extends ConsumerState { ), )} " "${widget.token.symbol}", - style: STextStyles.itemSubtitle(context), + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + : STextStyles.itemSubtitle(context), ), ], ), const SizedBox( - height: 1, + height: 2, ), Row( children: [ Text( widget.token.symbol, - style: STextStyles.itemSubtitle(context), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), ), const Spacer(), - Text("${ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value - .getTokenPrice(widget.token.address) - .item1 - .toStringAsFixed(2), - ), - )} " - "${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}"), + Text( + "${ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value + .getTokenPrice(widget.token.address) + .item1 + .toStringAsFixed(2), + ), + )} " + "${ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + )}", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), ], ), ], diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart index fb8c8ef13..659ed5221 100644 --- a/lib/pages/token_view/sub_widgets/my_tokens_list.dart +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -4,6 +4,7 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart'; +import 'package:stackwallet/utilities/util.dart'; class MyTokensList extends StatelessWidget { const MyTokensList({ @@ -52,6 +53,8 @@ class MyTokensList extends StatelessWidget { @override Widget build(BuildContext context) { + final bool isDesktop = Util.isDesktop; + return Consumer( builder: (_, ref, __) { final tokens = _filter(searchTerm); @@ -61,7 +64,9 @@ class MyTokensList extends StatelessWidget { final token = tokens[index]; return Padding( key: Key(token.address), - padding: const EdgeInsets.all(4), + padding: isDesktop + ? const EdgeInsets.symmetric(vertical: 5) + : const EdgeInsets.all(4), child: MyTokenSelectItem( walletId: walletId, token: token, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index edfb0a1cc..7e786ace6 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -4,8 +4,9 @@ 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/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; +import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart'; @@ -282,7 +283,7 @@ class _DesktopWalletViewState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Recent transactions", + "Tokens", style: STextStyles.desktopTextExtraSmall(context) .copyWith( color: Theme.of(context) @@ -291,12 +292,20 @@ class _DesktopWalletViewState extends ConsumerState { ), ), CustomTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: widget.walletId, + text: "Edit", + onTap: () async { + final result = await showDialog( + context: context, + builder: (context) => EditWalletTokensView( + walletId: widget.walletId, + isDesktopPopup: true, + ), ); + + if (result == 42) { + // wallet tokens were edited so update ui + setState(() {}); + } }, ), ], @@ -321,12 +330,20 @@ class _DesktopWalletViewState extends ConsumerState { width: 16, ), Expanded( - child: TransactionsList( - managerProvider: ref.watch( - walletsChangeNotifierProvider.select((value) => - value.getManagerProvider(widget.walletId))), - walletId: widget.walletId, - ), + child: ref.watch(walletsChangeNotifierProvider.select( + (value) => value + .getManager(widget.walletId) + .hasTokenSupport)) + ? MyTokensView( + walletId: widget.walletId, + ) + : TransactionsList( + managerProvider: ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManagerProvider( + widget.walletId))), + walletId: widget.walletId, + ), ), ], ), From f744fd10de43e0d4c1e76cd76af6ef8389a7edc5 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 15:36:34 -0600 Subject: [PATCH 205/208] add stacked overlay center widget to ensure exact center in desktop appbar --- lib/widgets/desktop/desktop_app_bar.dart | 36 ++++++++++++++++++------ 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/widgets/desktop/desktop_app_bar.dart b/lib/widgets/desktop/desktop_app_bar.dart index a95a552e5..848d4c0a3 100644 --- a/lib/widgets/desktop/desktop_app_bar.dart +++ b/lib/widgets/desktop/desktop_app_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; const double kDesktopAppBarHeight = 96.0; const double kDesktopAppBarHeightCompact = 82.0; @@ -8,6 +9,7 @@ class DesktopAppBar extends StatelessWidget { Key? key, this.leading, this.center, + this.overlayCenter, this.trailing, this.background = Colors.transparent, required this.isCompactHeight, @@ -16,6 +18,7 @@ class DesktopAppBar extends StatelessWidget { final Widget? leading; final Widget? center; + final Widget? overlayCenter; final Widget? trailing; final Color background; final bool isCompactHeight; @@ -43,16 +46,31 @@ class DesktopAppBar extends StatelessWidget { items.add(trailing!); } - return Container( - decoration: BoxDecoration( - color: background, + return ConditionalParent( + condition: overlayCenter != null, + builder: (child) => Stack( + children: [ + child, + Positioned.fill( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [overlayCenter!], + ), + ), + ], ), - height: - isCompactHeight ? kDesktopAppBarHeightCompact : kDesktopAppBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: items, + child: Container( + decoration: BoxDecoration( + color: background, + ), + height: isCompactHeight + ? kDesktopAppBarHeightCompact + : kDesktopAppBarHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: items, + ), ), ); } From 1ff19194e74e09ae01f7b227a5a3e6bf871661cf Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 15:40:00 -0600 Subject: [PATCH 206/208] add custom token dialog modified for desktop --- .../edit_wallet_tokens_view.dart | 133 ++++++++++++------ .../sub_widgets/add_token_text.dart | 26 ++-- 2 files changed, 101 insertions(+), 58 deletions(-) diff --git a/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart index 7f8fd4b53..936ca5405 100644 --- a/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart @@ -12,7 +12,6 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/ad import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -113,18 +112,32 @@ class _EditWalletTokensViewState extends ConsumerState { } Future _addToken() async { - final token = await Navigator.of(context).pushNamed( - AddCustomTokenView.routeName, - arguments: widget.walletId, - ); - if (token is EthContract) { - await MainDB.instance.putEthContract(token); + EthContract? contract; + + if (isDesktop) { + contract = await showDialog( + context: context, + builder: (context) => const DesktopDialog( + maxWidth: 580, + maxHeight: 500, + child: AddCustomTokenView(), + ), + ); + } else { + contract = await Navigator.of(context).pushNamed( + AddCustomTokenView.routeName, + ); + } + + if (contract != null) { + await MainDB.instance.putEthContract(contract); if (mounted) { setState(() { if (tokenEntities - .where((e) => e.token.address == token.address) + .where((e) => e.token.address == contract!.address) .isEmpty) { - tokenEntities.add(AddTokenListElementData(token)..selected = true); + tokenEntities + .add(AddTokenListElementData(contract!)..selected = true); tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); } }); @@ -184,51 +197,79 @@ class _EditWalletTokensViewState extends ConsumerState { builder: (child) => DesktopScaffold( appBar: DesktopAppBar( isCompactHeight: false, + useSpacers: false, leading: const AppBarBackButton(), + overlayCenter: Text( + walletName, + style: STextStyles.desktopSubtitleH2(context), + ), trailing: widget.contractsToMarkSelected == null - ? const ExitToMyStackButton() + ? Padding( + padding: const EdgeInsets.only( + right: 24, + ), + child: SizedBox( + height: 56, + child: TextButton( + style: Theme.of(context) + .extension()! + .getSmallSecondaryEnabledButtonStyle(context), + onPressed: _addToken, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30, + ), + child: Text( + "Add custom token", + style: + STextStyles.desktopButtonSmallSecondaryEnabled( + context), + ), + ), + ), + ), + ) : null, ), body: SizedBox( width: 480, - child: RoundedWhiteContainer( - radiusMultiplier: 2, - padding: const EdgeInsets.only( - left: 20, - top: 20, - right: 20, - bottom: 0, - ), - child: Column( - children: [ - AddTokenText( - isDesktop: true, - walletName: walletName, - ), - const SizedBox( - height: 16, - ), - Expanded( + child: Column( + children: [ + const AddTokenText( + isDesktop: true, + ), + const SizedBox( + height: 16, + ), + Expanded( + child: RoundedWhiteContainer( + radiusMultiplier: 2, + padding: const EdgeInsets.only( + left: 20, + top: 20, + right: 20, + bottom: 0, + ), child: child, ), - const SizedBox( - height: 26, + ), + const SizedBox( + height: 26, + ), + SizedBox( + height: 70, + width: 480, + child: PrimaryButton( + label: widget.contractsToMarkSelected != null + ? "Save" + : "Next", + onPressed: onNextPressed, ), - SizedBox( - height: 70, - width: 480, - child: PrimaryButton( - label: widget.contractsToMarkSelected != null - ? "Save" - : "Next", - onPressed: onNextPressed, - ), - ), - const SizedBox( - height: 32, - ), - ], - ), + ), + const SizedBox( + height: 32, + ), + ], ), ), ), @@ -369,7 +410,7 @@ class _EditWalletTokensViewState extends ConsumerState { child: AddTokenList( walletId: widget.walletId, items: filter(_searchTerm, tokenEntities), - addFunction: widget.isDesktopPopup ? null : _addToken, + addFunction: isDesktop ? null : _addToken, ), ), const SizedBox( diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart index c1edfc2eb..115d8be5c 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart @@ -5,10 +5,10 @@ class AddTokenText extends StatelessWidget { const AddTokenText({ Key? key, required this.isDesktop, - required this.walletName, + this.walletName, }) : super(key: key); - final String walletName; + final String? walletName; final bool isDesktop; @override @@ -16,16 +16,18 @@ class AddTokenText extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - walletName, - textAlign: TextAlign.center, - style: isDesktop - ? STextStyles.sectionLabelMedium12(context) // todo: fixme - : STextStyles.sectionLabelMedium12(context), - ), - const SizedBox( - height: 4, - ), + if (walletName != null) + Text( + walletName!, + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.sectionLabelMedium12(context) // todo: fixme + : STextStyles.sectionLabelMedium12(context), + ), + if (walletName != null) + const SizedBox( + height: 4, + ), Text( "Edit Tokens", textAlign: TextAlign.center, From 51211b34f543f8fddd31cd4afc6511db4a32e132 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 16:02:28 -0600 Subject: [PATCH 207/208] eth token fee parsing fix --- .../ethereum/cached_eth_token_balance.dart | 10 ++-------- lib/services/ethereum/ethereum_api.dart | 18 +++++------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/lib/services/ethereum/cached_eth_token_balance.dart b/lib/services/ethereum/cached_eth_token_balance.dart index c08b40fd5..f36cf2ea3 100644 --- a/lib/services/ethereum/cached_eth_token_balance.dart +++ b/lib/services/ethereum/cached_eth_token_balance.dart @@ -23,14 +23,8 @@ class CachedEthTokenBalance with EthTokenCache { await updateCachedBalance( TokenBalance( contractAddress: token.address, - total: Amount( - rawValue: BigInt.from(response.value!), - fractionDigits: token.decimals, - ), - spendable: Amount( - rawValue: BigInt.from(response.value!), - fractionDigits: token.decimals, - ), + total: response.value!, + spendable: response.value!, blockedTotal: Amount( rawValue: BigInt.zero, fractionDigits: token.decimals, diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 3bcb81b71..d31bcfd40 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:math'; import 'package:decimal/decimal.dart'; import 'package:http/http.dart'; @@ -9,6 +8,7 @@ import 'package:stackwallet/dto/ethereum/eth_tx_dto.dart'; import 'package:stackwallet/dto/ethereum/pending_eth_tx_dto.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart'; @@ -396,7 +396,7 @@ abstract class EthereumAPI { // } // } - static Future> getWalletTokenBalance({ + static Future> getWalletTokenBalance({ required String address, required String contractAddress, }) async { @@ -411,19 +411,11 @@ abstract class EthereumAPI { if (json["data"] is List) { final map = json["data"].first as Map; - final bal = Decimal.tryParse(map["balance"].toString()); - final int balance; - if (bal == null) { - balance = 0; - } else { - final int decimals = map["decimals"] as int; - balance = (bal * Decimal.fromInt(pow(10, decimals).truncate())) - .toBigInt() - .toInt(); - } + final balance = + Decimal.tryParse(map["balance"].toString()) ?? Decimal.zero; return EthereumResponse( - balance, + Amount.fromDecimal(balance, fractionDigits: map["decimals"] as int), null, ); } else { From bb85defaeac4efa8b5e73b25a2ef96d2809951d6 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 7 Apr 2023 17:21:11 -0600 Subject: [PATCH 208/208] desktop wallets search --- .../dialogs/desktop_coin_wallets_dialog.dart | 204 +++++++++++++----- lib/widgets/expandable.dart | 12 +- 2 files changed, 156 insertions(+), 60 deletions(-) diff --git a/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart b/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart index c1fd35118..46d8a3b19 100644 --- a/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -19,6 +22,7 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/widgets/wallet_card.dart'; import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart'; import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; +import 'package:tuple/tuple.dart'; class DesktopCoinWalletsDialog extends ConsumerStatefulWidget { const DesktopCoinWalletsDialog({ @@ -44,7 +48,50 @@ class _DesktopCoinWalletsDialogState String _searchString = ""; - final List walletIds = []; + final List>> wallets = []; + + List>> _filter(String searchTerm) { + if (searchTerm.isEmpty) { + return wallets; + } + + final List>> results = []; + final term = searchTerm.toLowerCase(); + + for (final tuple in wallets) { + bool includeManager = false; + // search wallet name and total balance + includeManager |= _elementContains(tuple.item1.walletName, term); + includeManager |= _elementContains( + tuple.item1.balance.total.decimal.toString(), + term, + ); + + final List contracts = []; + + for (final contract in tuple.item2) { + if (_elementContains(contract.name, term)) { + contracts.add(contract); + } else if (_elementContains(contract.symbol, term)) { + contracts.add(contract); + } else if (_elementContains(contract.type.name, term)) { + contracts.add(contract); + } else if (_elementContains(contract.address, term)) { + contracts.add(contract); + } + } + + if (includeManager || contracts.isNotEmpty) { + results.add(Tuple2(tuple.item1, contracts)); + } + } + + return results; + } + + bool _elementContains(String element, String term) { + return element.toLowerCase().contains(term); + } @override void initState() { @@ -54,9 +101,55 @@ class _DesktopCoinWalletsDialogState final walletsData = ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); walletsData.removeWhere((key, value) => value.coin != widget.coin); - walletIds.clear(); - walletIds.addAll(walletsData.values.map((e) => e.walletId)); + if (widget.coin == Coin.ethereum) { + for (final data in walletsData.values) { + final List contracts = []; + final manager = + ref.read(walletsChangeNotifierProvider).getManager(data.walletId); + final contractAddresses = (manager.wallet as EthereumWallet) + .getWalletTokenContractAddresses(); + + // fetch each contract + for (final contractAddress in contractAddresses) { + final contract = ref + .read( + mainDBProvider, + ) + .getEthContractSync( + contractAddress, + ); + + // add it to list if it exists in DB + if (contract != null) { + contracts.add(contract); + } + } + + // add tuple to list + wallets.add( + Tuple2( + ref.read(walletsChangeNotifierProvider).getManager( + data.walletId, + ), + contracts, + ), + ); + } + } else { + // add non token wallet tuple to list + for (final data in walletsData.values) { + wallets.add( + Tuple2( + ref.read(walletsChangeNotifierProvider).getManager( + data.walletId, + ), + [], + ), + ); + } + } + super.initState(); } @@ -137,71 +230,66 @@ class _DesktopCoinWalletsDialogState height: 16, ), Expanded( - child: ListView.separated( - itemBuilder: (_, index) => widget.coin == Coin.ethereum - ? _DesktopWalletCard( - walletId: walletIds[index], - navigatorState: widget.navigatorState, - ) - : RoundedWhiteContainer( - padding: const EdgeInsets.symmetric( - vertical: 14, - horizontal: 20, + child: Builder(builder: (context) { + final data = _filter(_searchString); + return ListView.separated( + itemBuilder: (_, index) => widget.coin == Coin.ethereum + ? _DesktopWalletCard( + key: Key( + "${data[index].item1.walletName}_${data[index].item2.map((e) => e.address).join()}"), + data: data[index], + navigatorState: widget.navigatorState, + ) + : RoundedWhiteContainer( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 20, + ), + borderColor: Theme.of(context) + .extension()! + .backgroundAppBar, + child: WalletSheetCard( + walletId: data[index].item1.walletId, + popPrevious: true, + desktopNavigatorState: widget.navigatorState, + ), ), - borderColor: Theme.of(context) - .extension()! - .backgroundAppBar, - child: WalletSheetCard( - walletId: walletIds[index], - popPrevious: true, - desktopNavigatorState: widget.navigatorState, - ), - ), - separatorBuilder: (_, __) => const SizedBox( - height: 10, - ), - itemCount: walletIds.length, - ), + separatorBuilder: (_, __) => const SizedBox( + height: 10, + ), + itemCount: data.length, + ); + }), ), ], ); } } -class _DesktopWalletCard extends ConsumerStatefulWidget { +class _DesktopWalletCard extends StatefulWidget { const _DesktopWalletCard({ Key? key, - required this.walletId, + required this.data, required this.navigatorState, }) : super(key: key); - final String walletId; + final Tuple2> data; final NavigatorState navigatorState; @override - ConsumerState<_DesktopWalletCard> createState() => _DesktopWalletCardState(); + State<_DesktopWalletCard> createState() => _DesktopWalletCardState(); } -class _DesktopWalletCardState extends ConsumerState<_DesktopWalletCard> { +class _DesktopWalletCardState extends State<_DesktopWalletCard> { final expandableController = ExpandableController(); final rotateIconController = RotateIconController(); final List tokenContractAddresses = []; - late final Coin coin; @override void initState() { - coin = ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .coin; - - if (coin == Coin.ethereum) { + if (widget.data.item1.hasTokenSupport) { tokenContractAddresses.addAll( - (ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .wallet as EthereumWallet) - .getWalletTokenContractAddresses(), + widget.data.item2.map((e) => e.address), ); } @@ -214,6 +302,9 @@ class _DesktopWalletCardState extends ConsumerState<_DesktopWalletCard> { padding: EdgeInsets.zero, borderColor: Theme.of(context).extension()!.backgroundAppBar, child: Expandable( + initialState: widget.data.item1.hasTokenSupport + ? ExpandableState.expanded + : ExpandableState.collapsed, controller: expandableController, expandOverride: () {}, header: Padding( @@ -231,19 +322,13 @@ class _DesktopWalletCardState extends ConsumerState<_DesktopWalletCard> { child: Row( children: [ WalletInfoCoinIcon( - coin: coin, + coin: widget.data.item1.coin, ), const SizedBox( width: 12, ), Text( - ref.watch( - walletsChangeNotifierProvider.select( - (value) => value - .getManager(widget.walletId) - .walletName, - ), - ), + widget.data.item1.walletName, style: STextStyles.desktopTextExtraSmall(context) .copyWith( color: Theme.of(context) @@ -257,7 +342,7 @@ class _DesktopWalletCardState extends ConsumerState<_DesktopWalletCard> { Expanded( flex: 4, child: WalletInfoRowBalance( - walletId: widget.walletId, + walletId: widget.data.item1.walletId, ), ), ], @@ -290,9 +375,12 @@ class _DesktopWalletCardState extends ConsumerState<_DesktopWalletCard> { }, child: RotateIcon( controller: rotateIconController, - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 14, + icon: RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + ), ), curve: Curves.easeInOut, ), @@ -318,7 +406,7 @@ class _DesktopWalletCardState extends ConsumerState<_DesktopWalletCard> { bottom: 14, ), child: WalletSheetCard( - walletId: widget.walletId, + walletId: widget.data.item1.walletId, popPrevious: true, desktopNavigatorState: widget.navigatorState, ), @@ -332,7 +420,7 @@ class _DesktopWalletCardState extends ConsumerState<_DesktopWalletCard> { bottom: 14, ), child: WalletSheetCard( - walletId: widget.walletId, + walletId: widget.data.item1.walletId, contractAddress: e, popPrevious: true, desktopNavigatorState: widget.navigatorState, diff --git a/lib/widgets/expandable.dart b/lib/widgets/expandable.dart index aa438e34f..dbd5c67f6 100644 --- a/lib/widgets/expandable.dart +++ b/lib/widgets/expandable.dart @@ -23,6 +23,7 @@ class Expandable extends StatefulWidget { this.controller, this.expandOverride, this.curve = Curves.easeInOut, + this.initialState = ExpandableState.collapsed, }) : super(key: key); final Widget header; @@ -35,6 +36,7 @@ class Expandable extends StatefulWidget { final ExpandableController? controller; final VoidCallback? expandOverride; final Curve curve; + final ExpandableState initialState; @override State createState() => _ExpandableState(); @@ -46,7 +48,7 @@ class _ExpandableState extends State with TickerProviderStateMixin { late final Duration duration; late final ExpandableController? controller; - ExpandableState _toggleState = ExpandableState.collapsed; + late ExpandableState _toggleState; Future toggle() async { if (animation.isDismissed) { @@ -65,6 +67,7 @@ class _ExpandableState extends State with TickerProviderStateMixin { @override void initState() { + _toggleState = widget.initialState; controller = widget.controller; controller?.toggle = toggle; @@ -76,8 +79,13 @@ class _ExpandableState extends State with TickerProviderStateMixin { vsync: this, duration: duration, ); + + final tween = _toggleState == ExpandableState.collapsed + ? Tween(begin: 0.0, end: 1.0) + : Tween(begin: 1.0, end: 0.0); + animation = widget.animation ?? - Tween(begin: 0.0, end: 1.0).animate( + tween.animate( CurvedAnimation( curve: widget.curve, parent: animationController,