From babbd75da3c4530a357bca746a863da398c67d33 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 Mar 2023 18:40:12 -0600 Subject: [PATCH] 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: [], ), ]; }