From 99963281260c76b2067e2a722cee87cd66b94cd1 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 10 Jan 2024 16:53:12 -0600 Subject: [PATCH] More WIP eth + tokens --- .../blockchain_data/v2/transaction_v2.dart | 4 + .../blockchain_data/v2/transaction_v2.g.dart | 445 ++++++++-- .../send_view/confirm_transaction_view.dart | 12 +- .../transaction_fee_selection_sheet.dart | 16 +- lib/pages/send_view/token_send_view.dart | 46 +- .../sub_widgets/my_token_select_item.dart | 21 +- .../token_view/sub_widgets/token_summary.dart | 11 +- .../token_transaction_list_widget.dart | 230 ++---- lib/pages/token_view/token_view.dart | 18 +- .../sub_widgets/wallet_refresh_button.dart | 6 +- .../all_transactions_view.dart | 55 +- .../wallet_view/desktop_token_view.dart | 12 +- .../sub_widgets/desktop_fee_dropdown.dart | 14 +- .../sub_widgets/desktop_receive.dart | 6 +- .../sub_widgets/desktop_token_send.dart | 38 +- .../sub_widgets/desktop_wallet_summary.dart | 9 +- lib/providers/wallet_provider.dart | 64 -- lib/route_generator.dart | 12 - .../ethereum/ethereum_token_service.dart | 763 ++++-------------- lib/utilities/eth_commons.dart | 19 - .../eth/current_token_wallet_provider.dart | 7 + lib/wallets/wallet/impl/ethereum_wallet.dart | 25 +- .../impl/sub_wallets/eth_token_wallet.dart | 491 +++++++++++ lib/widgets/desktop/desktop_fee_dialog.dart | 6 +- lib/widgets/wallet_card.dart | 18 +- 25 files changed, 1260 insertions(+), 1088 deletions(-) delete mode 100644 lib/providers/wallet_provider.dart create mode 100644 lib/wallets/isar/providers/eth/current_token_wallet_provider.dart create mode 100644 lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 68255be81..3dd8678a2 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -65,6 +65,10 @@ class TransactionV2 { String? get onChainNote => _getFromOtherData(key: "onChainNote") as String?; bool get isCancelled => _getFromOtherData(key: "isCancelled") == true; + String? get contractAddress => + _getFromOtherData(key: "contractAddress") as String?; + int? get nonce => _getFromOtherData(key: "nonce") as int?; + int getConfirmations(int currentChainHeight) { if (height == null || height! <= 0) return 0; return max(0, currentChainHeight - (height! - 1)); diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart index 814ec9d94..5af5d7166 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart @@ -22,87 +22,97 @@ const TransactionV2Schema = CollectionSchema( name: r'blockHash', type: IsarType.string, ), - r'hash': PropertySchema( + r'contractAddress': PropertySchema( id: 1, + name: r'contractAddress', + type: IsarType.string, + ), + r'hash': PropertySchema( + id: 2, name: r'hash', type: IsarType.string, ), 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'InputV2', ), r'isCancelled': PropertySchema( - id: 4, + id: 5, name: r'isCancelled', type: IsarType.bool, ), r'isEpiccashTransaction': PropertySchema( - id: 5, + id: 6, name: r'isEpiccashTransaction', type: IsarType.bool, ), + r'nonce': PropertySchema( + id: 7, + name: r'nonce', + type: IsarType.long, + ), r'numberOfMessages': PropertySchema( - id: 6, + id: 8, name: r'numberOfMessages', type: IsarType.long, ), r'onChainNote': PropertySchema( - id: 7, + id: 9, name: r'onChainNote', type: IsarType.string, ), r'otherData': PropertySchema( - id: 8, + id: 10, name: r'otherData', type: IsarType.string, ), r'outputs': PropertySchema( - id: 9, + id: 11, name: r'outputs', type: IsarType.objectList, target: r'OutputV2', ), r'slateId': PropertySchema( - id: 10, + id: 12, name: r'slateId', type: IsarType.string, ), r'subType': PropertySchema( - id: 11, + id: 13, name: r'subType', type: IsarType.byte, enumMap: _TransactionV2subTypeEnumValueMap, ), r'timestamp': PropertySchema( - id: 12, + id: 14, name: r'timestamp', type: IsarType.long, ), r'txid': PropertySchema( - id: 13, + id: 15, name: r'txid', type: IsarType.string, ), r'type': PropertySchema( - id: 14, + id: 16, name: r'type', type: IsarType.byte, enumMap: _TransactionV2typeEnumValueMap, ), r'version': PropertySchema( - id: 15, + id: 17, name: r'version', type: IsarType.long, ), r'walletId': PropertySchema( - id: 16, + id: 18, name: r'walletId', type: IsarType.string, ) @@ -182,6 +192,12 @@ int _transactionV2EstimateSize( bytesCount += 3 + value.length * 3; } } + { + final value = object.contractAddress; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.hash.length * 3; bytesCount += 3 + object.inputs.length * 3; { @@ -229,32 +245,34 @@ void _transactionV2Serialize( Map> allOffsets, ) { writer.writeString(offsets[0], object.blockHash); - writer.writeString(offsets[1], object.hash); - writer.writeLong(offsets[2], object.height); + writer.writeString(offsets[1], object.contractAddress); + writer.writeString(offsets[2], object.hash); + writer.writeLong(offsets[3], object.height); writer.writeObjectList( - offsets[3], + offsets[4], allOffsets, InputV2Schema.serialize, object.inputs, ); - writer.writeBool(offsets[4], object.isCancelled); - writer.writeBool(offsets[5], object.isEpiccashTransaction); - writer.writeLong(offsets[6], object.numberOfMessages); - writer.writeString(offsets[7], object.onChainNote); - writer.writeString(offsets[8], object.otherData); + writer.writeBool(offsets[5], object.isCancelled); + writer.writeBool(offsets[6], object.isEpiccashTransaction); + writer.writeLong(offsets[7], object.nonce); + writer.writeLong(offsets[8], object.numberOfMessages); + writer.writeString(offsets[9], object.onChainNote); + writer.writeString(offsets[10], object.otherData); writer.writeObjectList( - offsets[9], + offsets[11], allOffsets, OutputV2Schema.serialize, object.outputs, ); - 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.writeLong(offsets[15], object.version); - writer.writeString(offsets[16], object.walletId); + writer.writeString(offsets[12], object.slateId); + writer.writeByte(offsets[13], object.subType.index); + writer.writeLong(offsets[14], object.timestamp); + writer.writeString(offsets[15], object.txid); + writer.writeByte(offsets[16], object.type.index); + writer.writeLong(offsets[17], object.version); + writer.writeString(offsets[18], object.walletId); } TransactionV2 _transactionV2Deserialize( @@ -265,32 +283,32 @@ TransactionV2 _transactionV2Deserialize( ) { final object = TransactionV2( blockHash: reader.readStringOrNull(offsets[0]), - hash: reader.readString(offsets[1]), - height: reader.readLongOrNull(offsets[2]), + hash: reader.readString(offsets[2]), + height: reader.readLongOrNull(offsets[3]), inputs: reader.readObjectList( - offsets[3], + offsets[4], InputV2Schema.deserialize, allOffsets, InputV2(), ) ?? [], - otherData: reader.readStringOrNull(offsets[8]), + otherData: reader.readStringOrNull(offsets[10]), outputs: reader.readObjectList( - offsets[9], + offsets[11], OutputV2Schema.deserialize, allOffsets, OutputV2(), ) ?? [], subType: - _TransactionV2subTypeValueEnumMap[reader.readByteOrNull(offsets[11])] ?? + _TransactionV2subTypeValueEnumMap[reader.readByteOrNull(offsets[13])] ?? TransactionSubType.none, - timestamp: reader.readLong(offsets[12]), - txid: reader.readString(offsets[13]), - type: _TransactionV2typeValueEnumMap[reader.readByteOrNull(offsets[14])] ?? + timestamp: reader.readLong(offsets[14]), + txid: reader.readString(offsets[15]), + type: _TransactionV2typeValueEnumMap[reader.readByteOrNull(offsets[16])] ?? TransactionType.outgoing, - version: reader.readLong(offsets[15]), - walletId: reader.readString(offsets[16]), + version: reader.readLong(offsets[17]), + walletId: reader.readString(offsets[18]), ); object.id = id; return object; @@ -306,10 +324,12 @@ P _transactionV2DeserializeProp

( case 0: return (reader.readStringOrNull(offset)) as P; case 1: - return (reader.readString(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 2: - return (reader.readLongOrNull(offset)) as P; + return (reader.readString(offset)) as P; case 3: + return (reader.readLongOrNull(offset)) as P; + case 4: return (reader.readObjectList( offset, InputV2Schema.deserialize, @@ -317,17 +337,19 @@ P _transactionV2DeserializeProp

( InputV2(), ) ?? []) as P; - case 4: - return (reader.readBool(offset)) as P; case 5: return (reader.readBool(offset)) as P; case 6: - return (reader.readLongOrNull(offset)) as P; + return (reader.readBool(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; + return (reader.readLongOrNull(offset)) as P; case 9: + return (reader.readStringOrNull(offset)) as P; + case 10: + return (reader.readStringOrNull(offset)) as P; + case 11: return (reader.readObjectList( offset, OutputV2Schema.deserialize, @@ -335,22 +357,22 @@ P _transactionV2DeserializeProp

( OutputV2(), ) ?? []) as P; - case 10: + case 12: return (reader.readStringOrNull(offset)) as P; - case 11: + case 13: return (_TransactionV2subTypeValueEnumMap[ reader.readByteOrNull(offset)] ?? TransactionSubType.none) as P; - case 12: - return (reader.readLong(offset)) as P; - case 13: - return (reader.readString(offset)) as P; case 14: + return (reader.readLong(offset)) as P; + case 15: + return (reader.readString(offset)) as P; + case 16: return (_TransactionV2typeValueEnumMap[reader.readByteOrNull(offset)] ?? TransactionType.outgoing) as P; - case 15: + case 17: return (reader.readLong(offset)) as P; - case 16: + case 18: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -963,6 +985,160 @@ extension TransactionV2QueryFilter }); } + QueryBuilder + contractAddressIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'contractAddress', + )); + }); + } + + QueryBuilder + contractAddressIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'contractAddress', + )); + }); + } + + QueryBuilder + contractAddressEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'contractAddress', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contractAddressGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'contractAddress', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contractAddressLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'contractAddress', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contractAddressBetween( + 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'contractAddress', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contractAddressStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'contractAddress', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contractAddressEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'contractAddress', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contractAddressContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'contractAddress', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contractAddressMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'contractAddress', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + contractAddressIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'contractAddress', + value: '', + )); + }); + } + + QueryBuilder + contractAddressIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'contractAddress', + value: '', + )); + }); + } + QueryBuilder hashEqualTo( String value, { bool caseSensitive = true, @@ -1335,6 +1511,80 @@ extension TransactionV2QueryFilter }); } + 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 numberOfMessagesIsNull() { return QueryBuilder.apply(this, (query) { @@ -2490,6 +2740,20 @@ extension TransactionV2QuerySortBy }); } + QueryBuilder + sortByContractAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'contractAddress', Sort.asc); + }); + } + + QueryBuilder + sortByContractAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'contractAddress', Sort.desc); + }); + } + QueryBuilder sortByHash() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'hash', Sort.asc); @@ -2541,6 +2805,18 @@ extension TransactionV2QuerySortBy }); } + 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 sortByNumberOfMessages() { return QueryBuilder.apply(this, (query) { @@ -2683,6 +2959,20 @@ extension TransactionV2QuerySortThenBy }); } + QueryBuilder + thenByContractAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'contractAddress', Sort.asc); + }); + } + + QueryBuilder + thenByContractAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'contractAddress', Sort.desc); + }); + } + QueryBuilder thenByHash() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'hash', Sort.asc); @@ -2746,6 +3036,18 @@ extension TransactionV2QuerySortThenBy }); } + 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 thenByNumberOfMessages() { return QueryBuilder.apply(this, (query) { @@ -2882,6 +3184,14 @@ extension TransactionV2QueryWhereDistinct }); } + QueryBuilder + distinctByContractAddress({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'contractAddress', + caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByHash( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -2909,6 +3219,12 @@ extension TransactionV2QueryWhereDistinct }); } + QueryBuilder distinctByNonce() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'nonce'); + }); + } + QueryBuilder distinctByNumberOfMessages() { return QueryBuilder.apply(this, (query) { @@ -2990,6 +3306,13 @@ extension TransactionV2QueryProperty }); } + QueryBuilder + contractAddressProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'contractAddress'); + }); + } + QueryBuilder hashProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'hash'); @@ -3022,6 +3345,12 @@ extension TransactionV2QueryProperty }); } + QueryBuilder nonceProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'nonce'); + }); + } + QueryBuilder numberOfMessagesProperty() { return QueryBuilder.apply(this, (query) { diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 4292443de..2335caa09 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -20,7 +20,6 @@ import 'package:stackwallet/models/isar/models/transaction_note.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'; @@ -36,6 +35,7 @@ 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/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; @@ -202,7 +202,7 @@ class _ConfirmTransactionViewState } if (widget.isTokenTx) { - unawaited(ref.read(tokenServiceProvider)!.refresh()); + unawaited(ref.read(pCurrentTokenWallet)!.refresh()); } else { unawaited(wallet.refresh()); } @@ -345,7 +345,7 @@ class _ConfirmTransactionViewState final String unit; if (widget.isTokenTx) { unit = ref.watch( - tokenServiceProvider.select((value) => value!.tokenContract.symbol)); + pCurrentTokenWallet.select((value) => value!.tokenContract.symbol)); } else { unit = coin.ticker; } @@ -518,7 +518,7 @@ class _ConfirmTransactionViewState ref.watch(pAmountFormatter(coin)).format( amountWithoutChange, ethContract: ref - .watch(tokenServiceProvider) + .watch(pCurrentTokenWallet) ?.tokenContract, ), style: STextStyles.itemSubtitle12(context), @@ -708,7 +708,7 @@ class _ConfirmTransactionViewState priceAnd24hChangeNotifierProvider) .getTokenPrice( ref - .read(tokenServiceProvider)! + .read(pCurrentTokenWallet)! .tokenContract .address, ) @@ -737,7 +737,7 @@ class _ConfirmTransactionViewState ref.watch(pAmountFormatter(coin)).format( amountWithoutChange, ethContract: ref - .read(tokenServiceProvider) + .read(pCurrentTokenWallet) ?.tokenContract), style: STextStyles .desktopTextExtraExtraSmall( 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 bf80fa94a..f2178a450 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 @@ -12,7 +12,6 @@ import 'package:cw_core/monero_transaction_priority.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'; @@ -24,6 +23,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/widgets/animated_text.dart'; @@ -110,8 +110,8 @@ class _TransactionFeeSelectionSheetState await wallet.estimateFeeFor(amount, feeRate); } } else { - final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = tokenWallet.estimateFeeFor(feeRate); + final tokenWallet = ref.read(pCurrentTokenWallet)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } } @@ -144,8 +144,8 @@ class _TransactionFeeSelectionSheetState await wallet.estimateFeeFor(amount, feeRate); } } else { - final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = tokenWallet.estimateFeeFor(feeRate); + final tokenWallet = ref.read(pCurrentTokenWallet)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } } @@ -178,8 +178,8 @@ class _TransactionFeeSelectionSheetState await wallet.estimateFeeFor(amount, feeRate); } } else { - final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = tokenWallet.estimateFeeFor(feeRate); + final tokenWallet = ref.read(pCurrentTokenWallet)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } } @@ -267,7 +267,7 @@ class _TransactionFeeSelectionSheetState ), FutureBuilder( future: widget.isToken - ? ref.read(tokenServiceProvider)!.fees + ? ref.read(pCurrentTokenWallet)!.fees : wallet.fees, builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 8fd1312c0..0cba2bf17 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -21,7 +21,6 @@ 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'; @@ -41,6 +40,8 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/token_balance_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/widgets/animated_text.dart'; @@ -353,7 +354,7 @@ class _TokenSendViewState extends ConsumerState { } Future calculateFees() async { - final wallet = ref.read(tokenServiceProvider)!; + final wallet = ref.read(pCurrentTokenWallet)!; final feeObject = await wallet.fees; late final int feeRate; @@ -372,7 +373,7 @@ class _TokenSendViewState extends ConsumerState { feeRate = -1; } - final Amount fee = wallet.estimateFeeFor(feeRate); + final Amount fee = await wallet.estimateFeeFor(Amount.zero, feeRate); cachedFees = ref.read(pAmountFormatter(coin)).format( fee, withUnitName: true, @@ -389,7 +390,7 @@ class _TokenSendViewState extends ConsumerState { const Duration(milliseconds: 100), ); final wallet = ref.read(pWallets).getWallet(walletId); - final tokenWallet = ref.read(tokenServiceProvider)!; + final tokenWallet = ref.read(pCurrentTokenWallet)!; final Amount amount = _amountToSend!; @@ -711,8 +712,11 @@ class _TokenSendViewState extends ConsumerState { .watch(pAmountFormatter(coin)) .format( ref - .read(tokenServiceProvider)! - .balance + .read(pTokenBalance(( + walletId: widget.walletId, + contractAddress: + tokenContract.address, + ))) .spendable, ethContract: tokenContract, withUnitName: false, @@ -729,18 +733,16 @@ class _TokenSendViewState extends ConsumerState { ref .watch(pAmountFormatter(coin)) .format( - ref.watch( - tokenServiceProvider.select( - (value) => value! - .balance.spendable, - ), - ), - ethContract: ref.watch( - tokenServiceProvider.select( - (value) => - value!.tokenContract, - ), - ), + ref + .watch(pTokenBalance(( + walletId: + widget.walletId, + contractAddress: + tokenContract + .address, + ))) + .spendable, + ethContract: tokenContract, ), style: STextStyles.titleBold12(context) @@ -750,7 +752,13 @@ class _TokenSendViewState extends ConsumerState { textAlign: TextAlign.right, ), Text( - "${(ref.watch(tokenServiceProvider.select((value) => value!.balance.spendable.decimal)) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( + "${(ref.watch(pTokenBalance(( + walletId: + widget.walletId, + contractAddress: + tokenContract + .address, + ))).spendable.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( fractionDigits: 2, ).fiatString( locale: locale, 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 b9316773f..08835a329 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 @@ -8,17 +8,16 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/db/main_db_provider.dart'; -import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.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/themes/stack_colors.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -26,9 +25,11 @@ 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/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/isar/providers/eth/token_balance_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/wallet/impl/ethereum_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/dialogs/basic_dialog.dart'; import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; @@ -58,7 +59,7 @@ class _MyTokenSelectItemState extends ConsumerState { WidgetRef ref, ) async { try { - await ref.read(tokenServiceProvider)!.initialize(); + await ref.read(pCurrentTokenWallet)!.init(); return true; } catch (_) { await showDialog( @@ -84,14 +85,12 @@ class _MyTokenSelectItemState extends ConsumerState { } void _onPressed() async { + final old = ref.read(tokenServiceStateProvider); + // exit previous if there is one + unawaited(old?.exit()); ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( - token: widget.token, - secureStore: ref.read(secureStoreProvider), - ethWallet: - ref.read(pWallets).getWallet(widget.walletId) as EthereumWallet, - tracker: TransactionNotificationTracker( - walletId: widget.walletId, - ), + ref.read(pWallets).getWallet(widget.walletId) as EthereumWallet, + widget.token, ); final success = await showLoading( diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 83f6f8c5f..a852d0954 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -19,7 +19,6 @@ 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/locale_provider.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; @@ -33,6 +32,8 @@ 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/wallets/isar/providers/eth/current_token_wallet_provider.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/token_balance_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_container.dart'; @@ -51,9 +52,9 @@ class TokenSummary extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final token = - ref.watch(tokenServiceProvider.select((value) => value!.tokenContract)); - final balance = - ref.watch(tokenServiceProvider.select((value) => value!.balance)); + ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract)); + final balance = ref.watch( + pTokenBalance((walletId: walletId, contractAddress: token.address))); return Stack( children: [ @@ -157,7 +158,7 @@ class TokenSummary extends ConsumerWidget { walletId: walletId, initialSyncStatus: initialSyncStatus, tokenContractAddress: ref.watch( - tokenServiceProvider.select( + pCurrentTokenWallet.select( (value) => value!.tokenContract.address, ), ), 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 262a831c4..f2037d7d3 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 @@ -12,25 +12,18 @@ 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/token_view/token_view.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.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/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list_item.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/themes/stack_colors.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/wallets/isar/providers/wallet_info_provider.dart'; -import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; -import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.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({ @@ -49,7 +42,10 @@ class _TransactionsListState extends ConsumerState { late final int minConfirms; bool _hasLoaded = false; - List _transactions2 = []; + List _transactions = []; + + late final StreamSubscription> _subscription; + late final QueryBuilder _query; BorderRadius get _borderRadiusFirst { return BorderRadius.only( @@ -73,139 +69,6 @@ class _TransactionsListState extends ConsumerState { ); } - 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(pWalletChainHeight(widget.walletId)), - minConfirms) - ? 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(pWalletName(widget.walletId)); - 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(pWalletChainHeight(widget.walletId)), minConfirms) - ? Key(tx.txid + tx.type.name + tx.address.value.toString()) - : UniqueKey(), - transaction: tx, - walletId: widget.walletId, - ), - ); - } - } - @override void initState() { minConfirms = ref @@ -213,21 +76,44 @@ class _TransactionsListState extends ConsumerState { .getWallet(widget.walletId) .cryptoCurrency .minConfirms; + + _query = ref + .read(mainDBProvider) + .isar + .transactionV2s + .where() + .walletIdEqualTo(widget.walletId) + .filter() + .subTypeEqualTo(TransactionSubType.ethToken) + .sortByTimestampDesc(); + + _subscription = _query.watch().listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _transactions = event; + }); + }); + }); super.initState(); } + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { final wallet = ref.watch(pWallets.select((value) => value.getWallet(widget.walletId))); return FutureBuilder( - future: ref - .watch(tokenServiceProvider.select((value) => value!.transactions)), - builder: (fbContext, AsyncSnapshot> snapshot) { + future: _query.findAll(), + builder: (fbContext, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { - _transactions2 = snapshot.data!; + _transactions = snapshot.data!; _hasLoaded = true; } if (!_hasLoaded) { @@ -246,35 +132,34 @@ class _TransactionsListState extends ConsumerState { ], ); } - if (_transactions2.isEmpty) { + if (_transactions.isEmpty) { return const NoTransActionsFound(); } else { - _transactions2.sort((a, b) => b.timestamp - a.timestamp); + _transactions.sort((a, b) => b.timestamp - a.timestamp); return RefreshIndicator( onRefresh: () async { - if (!ref.read(tokenServiceProvider)!.isRefreshing) { - unawaited(ref.read(tokenServiceProvider)!.refresh()); + if (!ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked) { + unawaited(ref.read(pCurrentTokenWallet)!.refresh()); } }, child: Util.isDesktop ? ListView.separated( itemBuilder: (context, index) { BorderRadius? radius; - if (_transactions2.length == 1) { + if (_transactions.length == 1) { radius = BorderRadius.circular( Constants.size.circularBorderRadius, ); - } else if (index == _transactions2.length - 1) { + } else if (index == _transactions.length - 1) { radius = _borderRadiusLast; } else if (index == 0) { radius = _borderRadiusFirst; } - final tx = _transactions2[index]; - return itemBuilder( - context, - tx, - radius, - wallet.info.coin, + final tx = _transactions[index]; + return TxListItem( + tx: tx, + coin: wallet.info.coin, + radius: radius, ); }, separatorBuilder: (context, index) { @@ -286,27 +171,26 @@ class _TransactionsListState extends ConsumerState { .background, ); }, - itemCount: _transactions2.length, + itemCount: _transactions.length, ) : ListView.builder( - itemCount: _transactions2.length, + itemCount: _transactions.length, itemBuilder: (context, index) { BorderRadius? radius; - if (_transactions2.length == 1) { + if (_transactions.length == 1) { radius = BorderRadius.circular( Constants.size.circularBorderRadius, ); - } else if (index == _transactions2.length - 1) { + } else if (index == _transactions.length - 1) { radius = _borderRadiusLast; } else if (index == 0) { radius = _borderRadiusFirst; } - final tx = _transactions2[index]; - return itemBuilder( - context, - tx, - radius, - wallet.info.coin, + final tx = _transactions[index]; + return TxListItem( + tx: tx, + coin: wallet.info.coin, + radius: radius, ); }, ), diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 54f0efe58..12d7c85c5 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -15,23 +15,19 @@ 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/transaction_views/all_transactions_view.dart'; -import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.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); -final tokenServiceProvider = ChangeNotifierProvider( - (ref) => ref.watch(tokenServiceStateProvider)); - /// [eventBus] should only be set during testing class TokenView extends ConsumerStatefulWidget { const TokenView({ @@ -56,7 +52,7 @@ class _TokenViewState extends ConsumerState { @override void initState() { - initialSyncStatus = ref.read(tokenServiceProvider)!.isRefreshing + initialSyncStatus = ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked ? WalletSyncStatus.syncing : WalletSyncStatus.synced; super.initState(); @@ -105,7 +101,7 @@ class _TokenViewState extends ConsumerState { children: [ EthTokenIcon( contractAddress: ref.watch( - tokenServiceProvider.select( + pCurrentTokenWallet.select( (value) => value!.tokenContract.address, ), ), @@ -116,7 +112,7 @@ class _TokenViewState extends ConsumerState { ), Flexible( child: Text( - ref.watch(tokenServiceProvider + ref.watch(pCurrentTokenWallet .select((value) => value!.tokenContract.name)), style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, @@ -145,7 +141,7 @@ class _TokenViewState extends ConsumerState { Navigator.of(context).pushNamed( TokenContractDetailsView.routeName, arguments: Tuple2( - ref.watch(tokenServiceProvider + ref.watch(pCurrentTokenWallet .select((value) => value!.tokenContract.address)), widget.walletId, ), @@ -190,7 +186,7 @@ class _TokenViewState extends ConsumerState { text: "See all", onTap: () { Navigator.of(context).pushNamed( - AllTransactionsView.routeName, + AllTransactionsV2View.routeName, arguments: ( walletId: widget.walletId, isTokens: true, 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 7700f3f02..9c12eb954 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart @@ -13,13 +13,13 @@ import 'dart:async'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/widgets/animated_widgets/rotating_arrows.dart'; /// [eventBus] should only be set during testing @@ -140,8 +140,8 @@ class _RefreshButtonState extends ConsumerState { wallet.refresh().then((_) => _spinController.stop?.call()); } } else { - if (!ref.read(tokenServiceProvider)!.isRefreshing) { - ref.read(tokenServiceProvider)!.refresh(); + if (!ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked) { + ref.read(pCurrentTokenWallet)!.refresh(); } } }, 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 08c0d021a..c84e8aa02 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -19,7 +19,6 @@ import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/models/isar/models/transaction_note.dart'; import 'package:stackwallet/models/transaction_filter.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/token_view/token_view.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/pages/wallet_view/transaction_views/transaction_search_filter_view.dart'; @@ -61,13 +60,11 @@ class AllTransactionsView extends ConsumerStatefulWidget { const AllTransactionsView({ Key? key, required this.walletId, - this.isTokens = false, }) : super(key: key); static const String routeName = "/allTransactions"; final String walletId; - final bool isTokens; @override ConsumerState createState() => @@ -487,41 +484,25 @@ class _TransactionDetailsViewState extends ConsumerState { final criteria = ref.watch(transactionFilterProvider.state).state; - //todo: check if print needed - // debugPrint("Consumer build called"); - - final WhereClause ww; - return FutureBuilder( - future: widget.isTokens - ? ref - .watch(mainDBProvider) - .getTransactions(walletId) - .filter() - .otherDataEqualTo(ref - .watch(tokenServiceProvider)! - .tokenContract - .address) - .sortByTimestampDesc() - .findAll() - : ref.watch(mainDBProvider).isar.transactions.buildQuery< - Transaction>( - whereClauses: [ - IndexWhereClause.equalTo( - indexName: 'walletId', - value: [widget.walletId], - ) - ], - // TODO: [prio=high] add filters to wallet or cryptocurrency class - // filter: [ - // // todo - // ], - sortBy: [ - const SortProperty( - property: "timestamp", - sort: Sort.desc, - ), - ]).findAll(), + future: ref.watch(mainDBProvider).isar.transactions.buildQuery< + Transaction>( + whereClauses: [ + IndexWhereClause.equalTo( + indexName: 'walletId', + value: [widget.walletId], + ) + ], + // TODO: [prio=high] add filters to wallet or cryptocurrency class + // filter: [ + // // todo + // ], + sortBy: [ + const SortProperty( + property: "timestamp", + sort: Sort.desc, + ), + ]).findAll(), builder: (_, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { 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 d2ade5b80..a55b16a5b 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 @@ -15,7 +15,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.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'; @@ -25,6 +24,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; @@ -57,7 +57,7 @@ class _DesktopTokenViewState extends ConsumerState { @override void initState() { - initialSyncStatus = ref.read(tokenServiceProvider)!.isRefreshing + initialSyncStatus = ref.read(pCurrentTokenWallet)!.refreshMutex.isLocked ? WalletSyncStatus.syncing : WalletSyncStatus.synced; super.initState(); @@ -114,7 +114,7 @@ class _DesktopTokenViewState extends ConsumerState { children: [ EthTokenIcon( contractAddress: ref.watch( - tokenServiceProvider.select( + pCurrentTokenWallet.select( (value) => value!.tokenContract.address, ), ), @@ -125,7 +125,7 @@ class _DesktopTokenViewState extends ConsumerState { ), Text( ref.watch( - tokenServiceProvider.select( + pCurrentTokenWallet.select( (value) => value!.tokenContract.name, ), ), @@ -153,7 +153,7 @@ class _DesktopTokenViewState extends ConsumerState { children: [ EthTokenIcon( contractAddress: ref.watch( - tokenServiceProvider.select( + pCurrentTokenWallet.select( (value) => value!.tokenContract.address, ), ), @@ -241,7 +241,7 @@ class _DesktopTokenViewState extends ConsumerState { child: MyWallet( walletId: widget.walletId, contractAddress: ref.watch( - tokenServiceProvider.select( + pCurrentTokenWallet.select( (value) => value!.tokenContract.address, ), ), 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 a84ed8ccb..255d64c63 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 @@ -15,7 +15,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/models.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/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'; @@ -27,6 +26,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/text_styles.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/widgets/animated_text.dart'; @@ -103,8 +103,8 @@ class _DesktopFeeDropDownState extends ConsumerState { await wallet.estimateFeeFor(amount, feeRate); } } else { - final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = tokenWallet.estimateFeeFor(feeRate); + final tokenWallet = ref.read(pCurrentTokenWallet)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(tokenFeeSessionCacheProvider).fast[amount] = fee; } } @@ -147,8 +147,8 @@ class _DesktopFeeDropDownState extends ConsumerState { await wallet.estimateFeeFor(amount, feeRate); } } else { - final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = tokenWallet.estimateFeeFor(feeRate); + final tokenWallet = ref.read(pCurrentTokenWallet)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(tokenFeeSessionCacheProvider).average[amount] = fee; } } @@ -191,8 +191,8 @@ class _DesktopFeeDropDownState extends ConsumerState { await wallet.estimateFeeFor(amount, feeRate); } } else { - final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = tokenWallet.estimateFeeFor(feeRate); + final tokenWallet = ref.read(pCurrentTokenWallet)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(tokenFeeSessionCacheProvider).slow[amount] = fee; } } 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 283567df4..6250dfd8e 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 @@ -20,7 +20,6 @@ import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/models/isar/models/isar_models.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/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; @@ -32,6 +31,7 @@ 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/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; @@ -307,7 +307,7 @@ class _DesktopReceiveState extends ConsumerState { children: [ Text( "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( - tokenServiceProvider.select( + pCurrentTokenWallet.select( (value) => value!.tokenContract.symbol, ), )} SPARK address", @@ -398,7 +398,7 @@ class _DesktopReceiveState extends ConsumerState { children: [ Text( "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( - tokenServiceProvider.select( + pCurrentTokenWallet.select( (value) => value!.tokenContract.symbol, ), )} address", 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 d0b0bf475..0f7784a76 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,6 @@ 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/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'; @@ -39,6 +38,8 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/token_balance_provider.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -103,10 +104,15 @@ class _DesktopTokenSendState extends ConsumerState { late VoidCallback onCryptoAmountChanged; Future previewSend() async { - final tokenWallet = ref.read(tokenServiceProvider)!; + final tokenWallet = ref.read(pCurrentTokenWallet)!; final Amount amount = _amountToSend!; - final Amount availableBalance = tokenWallet.balance.spendable; + final Amount availableBalance = ref + .read(pTokenBalance(( + walletId: walletId, + contractAddress: tokenWallet.tokenContract.address + ))) + .spendable; // confirm send all if (amount == availableBalance) { @@ -214,7 +220,7 @@ class _DesktopTokenSendState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(32), child: BuildingTransactionDialog( - coin: tokenWallet.coin, + coin: tokenWallet.cryptoCurrency.coin, onCancel: () { wasCancelled = true; @@ -389,11 +395,11 @@ class _DesktopTokenSendState extends ConsumerState { _amountToSend = cryptoAmount.contains(",") ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")).toAmount( fractionDigits: - ref.read(tokenServiceProvider)!.tokenContract.decimals, + ref.read(pCurrentTokenWallet)!.tokenContract.decimals, ) : Decimal.parse(cryptoAmount).toAmount( fractionDigits: - ref.read(tokenServiceProvider)!.tokenContract.decimals, + ref.read(pCurrentTokenWallet)!.tokenContract.decimals, ); if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { @@ -406,7 +412,7 @@ class _DesktopTokenSendState extends ConsumerState { final price = ref .read(priceAnd24hChangeNotifierProvider) .getTokenPrice( - ref.read(tokenServiceProvider)!.tokenContract.address, + ref.read(pCurrentTokenWallet)!.tokenContract.address, ) .item1; @@ -485,7 +491,7 @@ class _DesktopTokenSendState extends ConsumerState { if (results["amount"] != null) { final amount = Decimal.parse(results["amount"]!).toAmount( fractionDigits: - ref.read(tokenServiceProvider)!.tokenContract.decimals, + ref.read(pCurrentTokenWallet)!.tokenContract.decimals, ); cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( amount, @@ -543,7 +549,7 @@ class _DesktopTokenSendState extends ConsumerState { void fiatTextFieldOnChanged(String baseAmountString) { final int tokenDecimals = - ref.read(tokenServiceProvider)!.tokenContract.decimals; + ref.read(pCurrentTokenWallet)!.tokenContract.decimals; if (baseAmountString.isNotEmpty && baseAmountString != "." && @@ -556,7 +562,7 @@ class _DesktopTokenSendState extends ConsumerState { final Decimal _price = ref .read(priceAnd24hChangeNotifierProvider) .getTokenPrice( - ref.read(tokenServiceProvider)!.tokenContract.address, + ref.read(pCurrentTokenWallet)!.tokenContract.address, ) .item1; @@ -579,7 +585,7 @@ class _DesktopTokenSendState extends ConsumerState { final amountString = ref.read(pAmountFormatter(coin)).format( _amountToSend!, withUnitName: false, - ethContract: ref.read(tokenServiceProvider)!.tokenContract, + ethContract: ref.read(pCurrentTokenWallet)!.tokenContract, ); _cryptoAmountChangeLock = true; @@ -597,12 +603,14 @@ class _DesktopTokenSendState extends ConsumerState { Future sendAllTapped() async { cryptoAmountController.text = ref - .read(tokenServiceProvider)! - .balance + .read(pTokenBalance(( + walletId: walletId, + contractAddress: ref.read(pCurrentTokenWallet)!.tokenContract.address + ))) .spendable .decimal .toStringAsFixed( - ref.read(tokenServiceProvider)!.tokenContract.decimals, + ref.read(pCurrentTokenWallet)!.tokenContract.decimals, ); } @@ -686,7 +694,7 @@ class _DesktopTokenSendState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final tokenContract = ref.watch(tokenServiceProvider)!.tokenContract; + final tokenContract = ref.watch(pCurrentTokenWallet)!.tokenContract; return Column( crossAxisAlignment: CrossAxisAlignment.start, 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 b7d81c301..fcf290017 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 @@ -11,7 +11,6 @@ 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'; @@ -24,6 +23,8 @@ import 'package:stackwallet/utilities/amount/amount_formatter.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'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/token_balance_provider.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; class DesktopWalletSummary extends ConsumerStatefulWidget { @@ -70,8 +71,7 @@ class _WDesktopWalletSummaryState extends ConsumerState { .watch(prefsChangeNotifierProvider.select((value) => value.currency)); final tokenContract = widget.isToken - ? ref - .watch(tokenServiceProvider.select((value) => value!.tokenContract)) + ? ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract)) : null; final priceTuple = widget.isToken @@ -104,7 +104,8 @@ class _WDesktopWalletSummaryState extends ConsumerState { } } else { Balance balance = widget.isToken - ? ref.watch(tokenServiceProvider.select((value) => value!.balance)) + ? ref.watch(pTokenBalance( + (walletId: walletId, contractAddress: tokenContract!.address))) : ref.watch(pWalletBalance(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart deleted file mode 100644 index a25331973..000000000 --- a/lib/providers/wallet_provider.dart +++ /dev/null @@ -1,64 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'package:equatable/equatable.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/providers/db/main_db_provider.dart'; -import 'package:stackwallet/providers/global/secure_store_provider.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/wallets/wallet/impl/ethereum_wallet.dart'; - -class ContractWalletId implements Equatable { - final String walletId; - final String tokenContractAddress; - - ContractWalletId({ - required this.walletId, - required this.tokenContractAddress, - }); - - @override - List get props => [walletId, tokenContractAddress]; - - @override - bool? get stringify => true; -} - -/// provide the token wallet given a contract address and eth wallet id -final tokenWalletProvider = - Provider.family((ref, arg) { - final ethWallet = - ref.watch(pWallets).getWallet(arg.walletId) as EthereumWallet?; - final contract = - ref.read(mainDBProvider).getEthContractSync(arg.tokenContractAddress); - - if (ethWallet == null || contract == null) { - Logging.instance.log( - "Attempted to access a token wallet with walletId=${arg.walletId} where" - " contractAddress=${arg.tokenContractAddress}", - level: LogLevel.Warning, - ); - return null; - } - - final secureStore = ref.watch(secureStoreProvider); - - return EthTokenWallet( - token: contract, - ethWallet: ethWallet, - secureStore: secureStore, - tracker: TransactionNotificationTracker( - walletId: arg.walletId, - ), - ); -}); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 5b8f1943d..6334b9075 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1300,18 +1300,6 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case AllTransactionsView.routeName: - if (args is ({String walletId, bool isTokens})) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsView( - walletId: args.walletId, - isTokens: args.isTokens, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index c83c95996..b3348addf 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -1,611 +1,152 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'dart:async'; - -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/dto/ethereum/eth_token_tx_dto.dart'; -import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.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/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'; -import 'package:stackwallet/services/transaction_notification_tracker.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'; -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/logger.dart'; -import 'package:stackwallet/wallets/models/tx_data.dart'; -import 'package:stackwallet/wallets/wallet/impl/ethereum_wallet.dart'; -import 'package:tuple/tuple.dart'; -import 'package:web3dart/web3dart.dart' as web3dart; - -class EthTokenWallet extends ChangeNotifier with EthTokenCache { - final EthereumWallet ethWallet; - final TransactionNotificationTracker tracker; - final SecureStorageInterface _secureStore; - - // late web3dart.EthereumAddress _contractAddress; - late web3dart.EthPrivateKey _credentials; - late web3dart.DeployedContract _deployedContract; - late web3dart.ContractFunction _sendFunction; - late web3dart.Web3Client _client; - - static const _gasLimit = 200000; - - EthTokenWallet({ - required EthContract token, - required this.ethWallet, - required SecureStorageInterface secureStore, - required this.tracker, - }) : _secureStore = secureStore, - _tokenContract = token { - // _contractAddress = web3dart.EthereumAddress.fromHex(token.address); - initCache(ethWallet.walletId, token); - } - - EthContract get tokenContract => _tokenContract; - EthContract _tokenContract; - - Balance get balance => _balance ??= getCachedBalance(); - Balance? _balance; - - Coin get coin => Coin.ethereum; - - Future prepareSend({ - required TxData txData, - }) async { - final feeRateType = txData.feeRateType!; - 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; - case FeeRateType.custom: - throw UnimplementedError("custom eth token fees"); - } - - final feeEstimate = estimateFeeFor(fee); - - final client = await getEthClient(); - - final myAddress = await currentReceivingAddress; - final myWeb3Address = web3dart.EthereumAddress.fromHex(myAddress); - - final nonce = txData.nonce ?? - await client.getTransactionCount(myWeb3Address, - atBlock: const web3dart.BlockNum.pending()); - - final amount = txData.recipients!.first.amount; - final address = txData.recipients!.first.address; - - final tx = web3dart.Transaction.callContract( - contract: _deployedContract, - function: _sendFunction, - parameters: [web3dart.EthereumAddress.fromHex(address), amount.raw], - maxGas: _gasLimit, - gasPrice: web3dart.EtherAmount.fromUnitAndValue( - web3dart.EtherUnit.wei, - fee, - ), - nonce: nonce, - ); - - return txData.copyWith( - fee: feeEstimate, - feeInWei: BigInt.from(fee), - web3dartTransaction: tx, - chainId: await client.getChainId(), - nonce: tx.nonce, - ); - } - - Future confirmSend({required Map txData}) async { - 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 = checksumEthereumAddress(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, - // 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 Amount).raw.toInt(), - height: null, - isCancelled: false, - isLelantus: false, - otherData: tokenContract.address, - slateId: null, - nonce: (txData["nonce"] as int?) ?? - response.value?.nonce.toBigIntFromHex.toInt(), - inputs: [], - outputs: [], - numberOfMessages: null, - ); - - Address? address = await ethWallet.mainDB.getAddress( - ethWallet.walletId, - addressString, - ); - - address ??= Address( - walletId: ethWallet.walletId, - value: addressString, - publicKey: [], - derivationIndex: -1, - derivationPath: null, - type: AddressType.ethereum, - subType: AddressSubType.nonWallet, - ); - - await ethWallet.mainDB.addNewTransactionData( - [ - Tuple2(transaction, address), - ], - ethWallet.walletId, - ); - } - - Future get currentReceivingAddress async { - final address = await _currentReceivingAddress; - return checksumEthereumAddress( - address?.value ?? _credentials.address.toString()); - } - - Future get _currentReceivingAddress => ethWallet.mainDB - .getAddresses(ethWallet.walletId) - .filter() - .typeEqualTo(AddressType.ethereum) - .subTypeEqualTo(AddressSubType.receiving) - .sortByDerivationIndexDesc() - .findFirst(); - - Amount estimateFeeFor(int feeRate) { - return estimateFee(feeRate, _gasLimit, coin.decimals); - } - - Future get fees => EthereumAPI.getFees(); - - Future _updateTokenABI({ - required EthContract forContract, - required String usingContractAddress, - }) async { - 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!); - // Store updated contract - final id = await MainDB.instance.putEthContract(updatedToken); - return updatedToken..id = id; - } else { - 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.getMnemonic(); - - //Get private key for given mnemonic - String privateKey = getPrivateKey( - mnemonicString!, - (await ethWallet.getMnemonicPassphrase()) ?? "", - ); - _credentials = web3dart.EthPrivateKey.fromHex(privateKey); - - try { - _deployedContract = web3dart.DeployedContract( - ContractAbiExtensions.fromJsonList( - jsonList: tokenContract.abi!, - name: tokenContract.name, - ), - contractAddress, - ); - } catch (_) { - rethrow; - } - - try { - _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" - // }, - // ); - // } - //-------------------------------------------------------------------- - //==================================================================== - - // 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.getProxyTokenImplementationAddress( - contractAddress.hex); - - if (contractAddressResponse.value != null) { - _tokenContract = await _updateTokenABI( - forContract: tokenContract, - usingContractAddress: contractAddressResponse.value!, - ); - } else { - throw contractAddressResponse.exception!; - } - //==================================================================== - } - - try { - _deployedContract = web3dart.DeployedContract( - ContractAbiExtensions.fromJsonList( - jsonList: tokenContract.abi!, - name: tokenContract.name, - ), - contractAddress, - ); - } catch (_) { - rethrow; - } - - _sendFunction = _deployedContract.function('transfer'); - - _client = await getEthClient(); - - unawaited(refresh()); - } - - bool get isRefreshing => _refreshLock; - - bool _refreshLock = false; - - Future refresh() async { - if (!_refreshLock) { - _refreshLock = true; - try { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - ethWallet.walletId + tokenContract.address, - coin, - ), - ); - - await refreshCachedBalance(); - await _refreshTransactions(); - } catch (e, s) { - Logging.instance.log( - "Caught exception in ${tokenContract.name} ${ethWallet.info.name} ${ethWallet.walletId} refresh(): $e\n$s", - level: LogLevel.Warning, - ); - } finally { - _refreshLock = false; - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - ethWallet.walletId + tokenContract.address, - coin, - ), - ); - notifyListeners(); - } - } - } - - Future refreshCachedBalance() async { - final response = await EthereumAPI.getWalletTokenBalance( - address: _credentials.address.hex, - contractAddress: tokenContract.address, - ); - - if (response.value != null) { - await updateCachedBalance( - Balance( - total: response.value!, - spendable: response.value!, - blockedTotal: Amount( - rawValue: BigInt.zero, - fractionDigits: tokenContract.decimals, - ), - pendingSpendable: Amount( - rawValue: BigInt.zero, - fractionDigits: tokenContract.decimals, - ), - ), - ); - notifyListeners(); - } else { - Logging.instance.log( - "CachedEthTokenBalance.fetchAndUpdateCachedBalance failed: ${response.exception}", - level: LogLevel.Warning, - ); - } - } - - Future> get transactions => ethWallet.mainDB - .getTransactions(ethWallet.walletId) - .filter() - .otherDataEqualTo(tokenContract.address) - .sortByTimestampDesc() - .findAll(); - - String _addressFromTopic(String topic) => - checksumEthereumAddress("0x${topic.substring(topic.length - 40)}"); - - Future _refreshTransactions() async { - String addressString = - checksumEthereumAddress(await currentReceivingAddress); - - final response = await EthereumAPI.getTokenTransactions( - address: addressString, - tokenContractAddress: tokenContract.address, - ); - - if (response.value == null) { - if (response.exception != null && - response.exception!.message - .contains("response is empty but status code is 200")) { - Logging.instance.log( - "No ${tokenContract.name} transfers found for $addressString", - level: LogLevel.Info, - ); - return; - } - 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).toSet().toList(), - ); - - if (response2.value == null) { - throw response2.exception ?? - Exception("Failed to fetch token transactions"); - } - final List> data = []; - for (final tokenDto in response.value!) { - try { - final txExtra = response2.value!.firstWhere( - (e) => e.hash == tokenDto.transactionHash, - ); - data.add( - Tuple2( - tokenDto, - txExtra, - ), - ); - } catch (_) { - // Server indexing failed for some reason. Instead of hard crashing or - // showing no transactions we just skip it here. Not ideal but better - // than nothing showing up - Logging.instance.log( - "Server error: Transaction ${tokenDto.transactionHash} not found.", - level: LogLevel.Error, - ); - } - } - - final List> txnsData = []; - - for (final tuple in data) { - // 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], - ); - toAddress = _addressFromTopic( - tuple.item1.topics[2], - ); - - bool isIncoming; - bool isSentToSelf = false; - if (fromAddress == addressString) { - isIncoming = false; - if (toAddress == addressString) { - isSentToSelf = true; - } - } else if (toAddress == addressString) { - isIncoming = true; - } else { - // ignore for now I guess since anything here is not reflected in - // balance anyways - continue; - - // 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, - nonce: tuple.item2.nonce, - otherData: tuple.item1.address, - inputs: [], - outputs: [], - numberOfMessages: null, - ); - - Address? transactionAddress = await ethWallet.mainDB - .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.mainDB.addNewTransactionData(txnsData, ethWallet.walletId); - - // quick hack to notify manager to call notifyListeners if - // transactions changed - if (txnsData.isNotEmpty) { - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "${tokenContract.name} transactions updated/added for: ${ethWallet.walletId} ${ethWallet.info.name}", - ethWallet.walletId, - ), - ); - } - } - - bool validateAddress(String address) { - return isValidEthereumAddress(address); - } - - NodeModel getCurrentNode() { - return NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - } - - Future getEthClient() async { - final node = getCurrentNode(); - return web3dart.Web3Client(node.host, Client()); - } -} +// /* +// * This file is part of Stack Wallet. +// * +// * Copyright (c) 2023 Cypher Stack +// * All Rights Reserved. +// * The code is distributed under GPLv3 license, see LICENSE file for details. +// * Generated by Cypher Stack on 2023-05-26 +// * +// */ +// +// import 'dart:async'; +// +// 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/dto/ethereum/eth_token_tx_dto.dart'; +// import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.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/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'; +// import 'package:stackwallet/services/transaction_notification_tracker.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'; +// 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/logger.dart'; +// import 'package:stackwallet/wallets/models/tx_data.dart'; +// import 'package:stackwallet/wallets/wallet/impl/ethereum_wallet.dart'; +// import 'package:tuple/tuple.dart'; +// import 'package:web3dart/web3dart.dart' as web3dart; +// +// class EthTokenWallet extends ChangeNotifier { +// final EthereumWallet ethWallet; +// final TransactionNotificationTracker tracker; +// +// +// Future updateSentCachedTxData(Map txData) async { +// final txid = txData["txid"] as String; +// final addressString = checksumEthereumAddress(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, +// // 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 Amount).raw.toInt(), +// height: null, +// isCancelled: false, +// isLelantus: false, +// otherData: tokenContract.address, +// slateId: null, +// nonce: (txData["nonce"] as int?) ?? +// response.value?.nonce.toBigIntFromHex.toInt(), +// inputs: [], +// outputs: [], +// numberOfMessages: null, +// ); +// +// Address? address = await ethWallet.mainDB.getAddress( +// ethWallet.walletId, +// addressString, +// ); +// +// address ??= Address( +// walletId: ethWallet.walletId, +// value: addressString, +// publicKey: [], +// derivationIndex: -1, +// derivationPath: null, +// type: AddressType.ethereum, +// subType: AddressSubType.nonWallet, +// ); +// +// await ethWallet.mainDB.addNewTransactionData( +// [ +// Tuple2(transaction, address), +// ], +// ethWallet.walletId, +// ); +// } +// +// +// +// bool get isRefreshing => _refreshLock; +// +// bool _refreshLock = false; +// +// Future refresh() async { +// if (!_refreshLock) { +// _refreshLock = true; +// try { +// GlobalEventBus.instance.fire( +// WalletSyncStatusChangedEvent( +// WalletSyncStatus.syncing, +// ethWallet.walletId + tokenContract.address, +// coin, +// ), +// ); +// +// await refreshCachedBalance(); +// await _refreshTransactions(); +// } catch (e, s) { +// Logging.instance.log( +// "Caught exception in ${tokenContract.name} ${ethWallet.info.name} ${ethWallet.walletId} refresh(): $e\n$s", +// level: LogLevel.Warning, +// ); +// } finally { +// _refreshLock = false; +// GlobalEventBus.instance.fire( +// WalletSyncStatusChangedEvent( +// WalletSyncStatus.synced, +// ethWallet.walletId + tokenContract.address, +// coin, +// ), +// ); +// notifyListeners(); +// } +// } +// } +// +// +// +// Future> get transactions => ethWallet.mainDB +// .getTransactions(ethWallet.walletId) +// .filter() +// .otherDataEqualTo(tokenContract.address) +// .sortByTimestampDesc() +// .findAll(); +// +// +// +// +// +// } diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index 7ea32cf2c..f6561c8d5 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -12,7 +12,6 @@ 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'; @@ -71,21 +70,3 @@ String getPrivateKey(String mnemonic, String mnemonicPassphrase) { return HEX.encode(addressAtIndex.privateKey as List); } - -Amount estimateFee(int feeRate, int gasLimit, int decimals) { - 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 * Decimal.ten.pow(9).toDecimal(); - final ethAmount = feeInWei / Decimal.ten.pow(decimals).toDecimal(); - return Amount.fromDecimal( - ethAmount.toDecimal( - scaleOnInfinitePrecision: Coin.ethereum.decimals, - ), - fractionDigits: decimals, - ); -} diff --git a/lib/wallets/isar/providers/eth/current_token_wallet_provider.dart b/lib/wallets/isar/providers/eth/current_token_wallet_provider.dart new file mode 100644 index 000000000..78015afea --- /dev/null +++ b/lib/wallets/isar/providers/eth/current_token_wallet_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; + +final tokenServiceStateProvider = StateProvider((ref) => null); + +final pCurrentTokenWallet = + Provider((ref) => ref.watch(tokenServiceStateProvider)); diff --git a/lib/wallets/wallet/impl/ethereum_wallet.dart b/lib/wallets/wallet/impl/ethereum_wallet.dart index 70854cd55..622bac5e8 100644 --- a/lib/wallets/wallet/impl/ethereum_wallet.dart +++ b/lib/wallets/wallet/impl/ethereum_wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; @@ -56,6 +57,24 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { return web3.Web3Client(node.host, client); } + Amount estimateEthFee(int feeRate, int gasLimit, int decimals) { + final gweiAmount = feeRate.toDecimal() / (Decimal.ten.pow(9).toDecimal()); + final fee = gasLimit.toDecimal() * + gweiAmount.toDecimal( + scaleOnInfinitePrecision: cryptoCurrency.fractionDigits, + ); + + //Convert gwei to ETH + final feeInWei = fee * Decimal.ten.pow(9).toDecimal(); + final ethAmount = feeInWei / Decimal.ten.pow(decimals).toDecimal(); + return Amount.fromDecimal( + ethAmount.toDecimal( + scaleOnInfinitePrecision: cryptoCurrency.fractionDigits, + ), + fractionDigits: decimals, + ); + } + // ==================== Private ============================================== Future _initCredentials( @@ -118,7 +137,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { @override Future estimateFeeFor(Amount amount, int feeRate) async { - return estimateFee( + return estimateEthFee( feeRate, (cryptoCurrency as Ethereum).gasLimit, cryptoCurrency.fractionDigits, @@ -249,7 +268,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { //Calculate fees (GasLimit * gasPrice) // int txFee = element.gasPrice * element.gasUsed; - Amount txFee = element.gasCost; + final Amount txFee = element.gasCost; final transactionAmount = element.value; final addressFrom = checksumEthereumAddress(element.from); final addressTo = checksumEthereumAddress(element.to); @@ -267,7 +286,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { continue; } - // hack epic tx data into inputs and outputs + // hack eth tx data into inputs and outputs final List outputs = []; final List inputs = []; diff --git a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart new file mode 100644 index 000000000..6c7201903 --- /dev/null +++ b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart @@ -0,0 +1,491 @@ +import 'dart:convert'; + +import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'package:isar/isar.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/balance.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; +import 'package:stackwallet/utilities/amount/amount.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/logger.dart'; +import 'package:stackwallet/wallets/isar/models/token_wallet_info.dart'; +import 'package:stackwallet/wallets/models/tx_data.dart'; +import 'package:stackwallet/wallets/wallet/impl/ethereum_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:web3dart/web3dart.dart' as web3dart; + +class EthTokenWallet extends Wallet { + @override + int get isarTransactionVersion => 2; + + EthTokenWallet(this.ethWallet, this._tokenContract) + : super(ethWallet.cryptoCurrency); + + final EthereumWallet ethWallet; + + EthContract get tokenContract => _tokenContract; + EthContract _tokenContract; + + late web3dart.DeployedContract _deployedContract; + late web3dart.ContractFunction _sendFunction; + + static const _gasLimit = 200000; + + // =========================================================================== + + // =========================================================================== + + Future _updateTokenABI({ + required EthContract forContract, + required String usingContractAddress, + }) async { + 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!); + // Store updated contract + final id = await mainDB.putEthContract(updatedToken); + return updatedToken..id = id; + } else { + throw abiResponse.exception!; + } + } + + String _addressFromTopic(String topic) => + checksumEthereumAddress("0x${topic.substring(topic.length - 40)}"); + + // =========================================================================== + + @override + FilterOperation? get changeAddressFilterOperation => + ethWallet.changeAddressFilterOperation; + + @override + FilterOperation? get receivingAddressFilterOperation => + ethWallet.receivingAddressFilterOperation; + + @override + Future init() async { + await super.init(); + + final contractAddress = + web3dart.EthereumAddress.fromHex(tokenContract.address); + + if (tokenContract.abi == null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddress.hex, + ); + } + + // String? mnemonicString = await ethWallet.getMnemonic(); + // + // //Get private key for given mnemonic + // String privateKey = getPrivateKey( + // mnemonicString, + // (await ethWallet.getMnemonicPassphrase()), + // ); + // _credentials = web3dart.EthPrivateKey.fromHex(privateKey); + + try { + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + } catch (_) { + rethrow; + } + + try { + _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" + // }, + // ); + // } + //-------------------------------------------------------------------- + //==================================================================== + + // 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.getProxyTokenImplementationAddress( + contractAddress.hex); + + if (contractAddressResponse.value != null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddressResponse.value!, + ); + } else { + throw contractAddressResponse.exception!; + } + //==================================================================== + } + + try { + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + } catch (_) { + rethrow; + } + + _sendFunction = _deployedContract.function('transfer'); + } + + @override + Future prepareSend({required TxData txData}) async { + final feeRateType = txData.feeRateType!; + 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; + case FeeRateType.custom: + throw UnimplementedError("custom eth token fees"); + } + + final feeEstimate = await estimateFeeFor(Amount.zero, fee); + + final client = ethWallet.getEthClient(); + + final myAddress = (await getCurrentReceivingAddress())!.value; + final myWeb3Address = web3dart.EthereumAddress.fromHex(myAddress); + + final nonce = txData.nonce ?? + await client.getTransactionCount(myWeb3Address, + atBlock: const web3dart.BlockNum.pending()); + + final amount = txData.recipients!.first.amount; + final address = txData.recipients!.first.address; + + final tx = web3dart.Transaction.callContract( + contract: _deployedContract, + function: _sendFunction, + parameters: [web3dart.EthereumAddress.fromHex(address), amount.raw], + maxGas: _gasLimit, + gasPrice: web3dart.EtherAmount.fromUnitAndValue( + web3dart.EtherUnit.wei, + fee, + ), + nonce: nonce, + ); + + return txData.copyWith( + fee: feeEstimate, + feeInWei: BigInt.from(fee), + web3dartTransaction: tx, + chainId: await client.getChainId(), + nonce: tx.nonce, + ); + } + + @override + Future confirmSend({required TxData txData}) async { + try { + return await ethWallet.confirmSend(txData: txData); + } catch (e) { + // rethrow to pass error in alert + rethrow; + } + } + + @override + Future estimateFeeFor(Amount amount, int feeRate) async { + return ethWallet.estimateEthFee( + feeRate, + _gasLimit, + cryptoCurrency.fractionDigits, + ); + } + + @override + Future get fees => EthereumAPI.getFees(); + + @override + Future pingCheck() async { + return await ethWallet.pingCheck(); + } + + @override + Future recover({required bool isRescan}) { + // TODO: implement recover + throw UnimplementedError(); + } + + @override + Future updateBalance() async { + try { + final info = await mainDB.isar.tokenWalletInfo + .where() + .walletIdTokenAddressEqualTo(walletId, tokenContract.address) + .findFirst(); + final response = await EthereumAPI.getWalletTokenBalance( + address: (await getCurrentReceivingAddress())!.value, + contractAddress: tokenContract.address, + ); + + if (response.value != null && info != null) { + await info.updateCachedBalance( + Balance( + total: response.value!, + spendable: response.value!, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: tokenContract.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: tokenContract.decimals, + ), + ), + isar: mainDB.isar, + ); + } else { + Logging.instance.log( + "CachedEthTokenBalance.fetchAndUpdateCachedBalance failed: ${response.exception}", + level: LogLevel.Warning, + ); + } + } catch (e, s) { + Logging.instance.log( + "$runtimeType wallet failed to update balance: $e\n$s", + level: LogLevel.Warning, + ); + } + } + + @override + Future updateChainHeight() async { + await ethWallet.updateChainHeight(); + } + + @override + Future updateTransactions() async { + try { + final String addressString = + checksumEthereumAddress((await getCurrentReceivingAddress())!.value); + + final response = await EthereumAPI.getTokenTransactions( + address: addressString, + tokenContractAddress: tokenContract.address, + ); + + if (response.value == null) { + if (response.exception != null && + response.exception!.message + .contains("response is empty but status code is 200")) { + Logging.instance.log( + "No ${tokenContract.name} transfers found for $addressString", + level: LogLevel.Info, + ); + return; + } + 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).toSet().toList(), + ); + + if (response2.value == null) { + throw response2.exception ?? + Exception("Failed to fetch token transactions"); + } + final List<({EthTokenTxDto tx, EthTokenTxExtraDTO extra})> data = []; + for (final tokenDto in response.value!) { + try { + final txExtra = response2.value!.firstWhere( + (e) => e.hash == tokenDto.transactionHash, + ); + data.add( + ( + tx: tokenDto, + extra: txExtra, + ), + ); + } catch (_) { + // Server indexing failed for some reason. Instead of hard crashing or + // showing no transactions we just skip it here. Not ideal but better + // than nothing showing up + Logging.instance.log( + "Server error: Transaction ${tokenDto.transactionHash} not found.", + level: LogLevel.Error, + ); + } + } + + final List txns = []; + + for (final tuple in data) { + // ignore all non Transfer events (for now) + if (tuple.tx.topics[0] == kTransferEventSignature) { + final Amount amount; + final Amount txFee = tuple.extra.gasUsed * tuple.extra.gasPrice; + String fromAddress, toAddress; + amount = Amount( + rawValue: tuple.tx.data.toBigIntFromHex, + fractionDigits: tokenContract.decimals, + ); + + fromAddress = _addressFromTopic( + tuple.tx.topics[1], + ); + toAddress = _addressFromTopic( + tuple.tx.topics[2], + ); + + bool isIncoming; + bool isSentToSelf = false; + if (fromAddress == addressString) { + isIncoming = false; + if (toAddress == addressString) { + isSentToSelf = true; + } + } else if (toAddress == addressString) { + isIncoming = true; + } else { + // ignore for now I guess since anything here is not reflected in + // balance anyways + continue; + + // throw Exception("Unknown token transaction found for " + // "${ethWallet.walletName} ${ethWallet.walletId}: " + // "${tuple.item1.toString()}"); + } + + final TransactionType txType; + if (isIncoming) { + if (fromAddress == toAddress) { + txType = TransactionType.sentToSelf; + } else { + txType = TransactionType.incoming; + } + } else { + txType = TransactionType.outgoing; + } + + final otherData = { + "nonce": tuple.extra.nonce, + "isCancelled": false, + "overrideFee": txFee.toJsonString(), + "contractAddress": tuple.tx.address, + }; + + // hack eth tx data into inputs and outputs + final List outputs = []; + final List inputs = []; + + // TODO: ins outs + + final txn = TransactionV2( + walletId: walletId, + blockHash: tuple.extra.blockHash, + hash: tuple.tx.transactionHash, + txid: tuple.tx.transactionHash, + timestamp: tuple.extra.timestamp, + height: tuple.tx.blockNumber, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + version: -1, + type: txType, + subType: TransactionSubType.ethToken, + otherData: jsonEncode(otherData), + ); + + txns.add(txn); + } + } + await mainDB.updateOrPutTransactionV2s(txns); + } catch (e, s) { + Logging.instance.log( + "$runtimeType wallet failed to update transactions: $e\n$s", + level: LogLevel.Warning, + ); + } + } + + @override + Future updateNode() async { + await ethWallet.updateNode(); + } + + @override + Future updateUTXOs() async { + return await ethWallet.updateUTXOs(); + } +} diff --git a/lib/widgets/desktop/desktop_fee_dialog.dart b/lib/widgets/desktop/desktop_fee_dialog.dart index 6036d5886..a4a5a9abc 100644 --- a/lib/widgets/desktop/desktop_fee_dialog.dart +++ b/lib/widgets/desktop/desktop_fee_dialog.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/models.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/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; @@ -14,6 +13,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/text_styles.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -168,8 +168,8 @@ class _DesktopFeeDialogState extends ConsumerState { await wallet.estimateFeeFor(amount, feeRate); } } else { - final tokenWallet = ref.read(tokenServiceProvider)!; - final fee = tokenWallet.estimateFeeFor(feeRate); + final tokenWallet = ref.read(pCurrentTokenWallet)!; + final fee = await tokenWallet.estimateFeeFor(amount, feeRate); ref.read(tokenFeeSessionCacheProvider).slow[amount] = fee; } } diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index e2814ffea..4d661ab57 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -18,16 +18,15 @@ 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'; import 'package:stackwallet/providers/providers.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/utilities/logger.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart'; import 'package:stackwallet/wallets/wallet/impl/ethereum_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; @@ -55,17 +54,16 @@ class SimpleWalletCard extends ConsumerWidget { Wallet wallet, EthContract contract, ) async { + final old = ref.read(tokenServiceStateProvider); + // exit previous if there is one + unawaited(old?.exit()); ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( - token: contract, - secureStore: ref.read(secureStoreProvider), - ethWallet: wallet as EthereumWallet, - tracker: TransactionNotificationTracker( - walletId: walletId, - ), + wallet as EthereumWallet, + contract, ); try { - await ref.read(tokenServiceProvider)!.initialize(); + await ref.read(pCurrentTokenWallet)!.init(); return true; } catch (_) { await showDialog(