diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 796914117..61ec51545 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -13,6 +13,7 @@ import 'package:flutter_native_splash/cli_commands.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/exceptions/main_db/main_db_exception.dart'; import 'package:stackwallet/models/isar/models/block_explorer.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/ordinal.dart'; @@ -57,6 +58,7 @@ class MainDB { ContactEntrySchema, OrdinalSchema, LelantusCoinSchema, + TransactionV2Schema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, @@ -506,6 +508,35 @@ class MainDB { } } + Future> updateOrPutTransactionV2s( + List transactions, + ) async { + try { + List ids = []; + await isar.writeTxn(() async { + for (final tx in transactions) { + final storedTx = await isar.transactionV2s + .where() + .txidWalletIdEqualTo(tx.txid, tx.walletId) + .findFirst(); + + Id id; + if (storedTx == null) { + id = await isar.transactionV2s.put(tx); + } else { + tx.id = storedTx.id; + await isar.transactionV2s.delete(storedTx.id); + id = await isar.transactionV2s.put(tx); + } + ids.add(id); + } + }); + return ids; + } catch (e) { + throw MainDBException("failed updateOrPutAddresses: $transactions", e); + } + } + // ========== Ethereum ======================================================= // eth contracts diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index a5a814786..ecd7d51c8 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -251,5 +251,6 @@ enum TransactionSubType { bip47Notification, // bip47 payment code notification transaction flag mint, // firo specific join, // firo specific - ethToken; // eth token + ethToken, // eth token + cashFusion; } diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index c11649ddb..cd9132576 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -364,6 +364,7 @@ const _TransactionsubTypeEnumValueMap = { 'mint': 2, 'join': 3, 'ethToken': 4, + 'cashFusion': 5, }; const _TransactionsubTypeValueEnumMap = { 0: TransactionSubType.none, @@ -371,6 +372,7 @@ const _TransactionsubTypeValueEnumMap = { 2: TransactionSubType.mint, 3: TransactionSubType.join, 4: TransactionSubType.ethToken, + 5: TransactionSubType.cashFusion, }; const _TransactiontypeEnumValueMap = { 'outgoing': 0, diff --git a/lib/models/isar/models/blockchain_data/v2/input_v2.dart b/lib/models/isar/models/blockchain_data/v2/input_v2.dart index f604b75af..44a54ce19 100644 --- a/lib/models/isar/models/blockchain_data/v2/input_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/input_v2.dart @@ -72,7 +72,7 @@ class InputV2 { InputV2() ..scriptSigHex = scriptSigHex ..sequence = sequence - ..sequence = sequence + ..outpoint = outpoint ..addresses = List.unmodifiable(addresses) ..valueStringSats = valueStringSats ..witness = witness 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 f98ca53ea..cebc5d5d9 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -1,14 +1,17 @@ import 'dart:math'; import 'package:isar/isar.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/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; part 'transaction_v2.g.dart'; @Collection() class TransactionV2 { - final Id id = Isar.autoIncrement; + Id id = Isar.autoIncrement; @Index() final String walletId; @@ -28,6 +31,12 @@ class TransactionV2 { final List inputs; final List outputs; + @enumerated + final TransactionType type; + + @enumerated + final TransactionSubType subType; + TransactionV2({ required this.walletId, required this.blockHash, @@ -38,6 +47,8 @@ class TransactionV2 { required this.inputs, required this.outputs, required this.version, + required this.type, + required this.subType, }); int getConfirmations(int currentChainHeight) { @@ -50,12 +61,32 @@ class TransactionV2 { return confirmations >= minimumConfirms; } + Amount getFee({required Coin coin}) { + final inSum = + inputs.map((e) => e.value).reduce((value, element) => value += element); + final outSum = outputs + .map((e) => e.value) + .reduce((value, element) => value += element); + + return Amount(rawValue: inSum - outSum, fractionDigits: coin.decimals); + } + + Amount getAmount({required Coin coin}) { + final outSum = outputs + .map((e) => e.value) + .reduce((value, element) => value += element); + + return Amount(rawValue: outSum, fractionDigits: coin.decimals); + } + @override String toString() { return 'TransactionV2(\n' ' walletId: $walletId,\n' ' hash: $hash,\n' ' txid: $txid,\n' + ' type: $type,\n' + ' subType: $subType,\n' ' timestamp: $timestamp,\n' ' height: $height,\n' ' blockHash: $blockHash,\n' @@ -65,3 +96,12 @@ class TransactionV2 { ')'; } } + +enum TxDirection { + outgoing, + incoming; +} + +enum TxType { + normal, +} 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 38772d24e..47fd5b936 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 @@ -44,23 +44,35 @@ const TransactionV2Schema = CollectionSchema( type: IsarType.objectList, target: r'OutputV2', ), - r'timestamp': PropertySchema( + r'subType': PropertySchema( id: 5, + name: r'subType', + type: IsarType.byte, + enumMap: _TransactionV2subTypeEnumValueMap, + ), + r'timestamp': PropertySchema( + id: 6, name: r'timestamp', type: IsarType.long, ), r'txid': PropertySchema( - id: 6, + id: 7, name: r'txid', type: IsarType.string, ), + r'type': PropertySchema( + id: 8, + name: r'type', + type: IsarType.byte, + enumMap: _TransactionV2typeEnumValueMap, + ), r'version': PropertySchema( - id: 7, + id: 9, name: r'version', type: IsarType.long, ), r'walletId': PropertySchema( - id: 8, + id: 10, name: r'walletId', type: IsarType.string, ) @@ -183,10 +195,12 @@ void _transactionV2Serialize( OutputV2Schema.serialize, object.outputs, ); - writer.writeLong(offsets[5], object.timestamp); - writer.writeString(offsets[6], object.txid); - writer.writeLong(offsets[7], object.version); - writer.writeString(offsets[8], object.walletId); + writer.writeByte(offsets[5], object.subType.index); + writer.writeLong(offsets[6], object.timestamp); + writer.writeString(offsets[7], object.txid); + writer.writeByte(offsets[8], object.type.index); + writer.writeLong(offsets[9], object.version); + writer.writeString(offsets[10], object.walletId); } TransactionV2 _transactionV2Deserialize( @@ -213,11 +227,17 @@ TransactionV2 _transactionV2Deserialize( OutputV2(), ) ?? [], - timestamp: reader.readLong(offsets[5]), - txid: reader.readString(offsets[6]), - version: reader.readLong(offsets[7]), - walletId: reader.readString(offsets[8]), + subType: + _TransactionV2subTypeValueEnumMap[reader.readByteOrNull(offsets[5])] ?? + TransactionSubType.none, + timestamp: reader.readLong(offsets[6]), + txid: reader.readString(offsets[7]), + type: _TransactionV2typeValueEnumMap[reader.readByteOrNull(offsets[8])] ?? + TransactionType.outgoing, + version: reader.readLong(offsets[9]), + walletId: reader.readString(offsets[10]), ); + object.id = id; return object; } @@ -251,18 +271,54 @@ P _transactionV2DeserializeProp

( ) ?? []) as P; case 5: - return (reader.readLong(offset)) as P; + return (_TransactionV2subTypeValueEnumMap[ + reader.readByteOrNull(offset)] ?? + TransactionSubType.none) as P; case 6: - return (reader.readString(offset)) as P; - case 7: return (reader.readLong(offset)) as P; + case 7: + return (reader.readString(offset)) as P; case 8: + return (_TransactionV2typeValueEnumMap[reader.readByteOrNull(offset)] ?? + TransactionType.outgoing) as P; + case 9: + return (reader.readLong(offset)) as P; + case 10: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } } +const _TransactionV2subTypeEnumValueMap = { + 'none': 0, + 'bip47Notification': 1, + 'mint': 2, + 'join': 3, + 'ethToken': 4, + 'cashFusion': 5, +}; +const _TransactionV2subTypeValueEnumMap = { + 0: TransactionSubType.none, + 1: TransactionSubType.bip47Notification, + 2: TransactionSubType.mint, + 3: TransactionSubType.join, + 4: TransactionSubType.ethToken, + 5: TransactionSubType.cashFusion, +}; +const _TransactionV2typeEnumValueMap = { + 'outgoing': 0, + 'incoming': 1, + 'sentToSelf': 2, + 'unknown': 3, +}; +const _TransactionV2typeValueEnumMap = { + 0: TransactionType.outgoing, + 1: TransactionType.incoming, + 2: TransactionType.sentToSelf, + 3: TransactionType.unknown, +}; + Id _transactionV2GetId(TransactionV2 object) { return object.id; } @@ -272,7 +328,9 @@ List> _transactionV2GetLinks(TransactionV2 object) { } void _transactionV2Attach( - IsarCollection col, Id id, TransactionV2 object) {} + IsarCollection col, Id id, TransactionV2 object) { + object.id = id; +} extension TransactionV2ByIndex on IsarCollection { Future getByTxidWalletId(String txid, String walletId) { @@ -1275,6 +1333,62 @@ extension TransactionV2QueryFilter }); } + QueryBuilder + subTypeEqualTo(TransactionSubType value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'subType', + value: value, + )); + }); + } + + QueryBuilder + subTypeGreaterThan( + TransactionSubType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'subType', + value: value, + )); + }); + } + + QueryBuilder + subTypeLessThan( + TransactionSubType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'subType', + value: value, + )); + }); + } + + QueryBuilder + subTypeBetween( + TransactionSubType lower, + TransactionSubType upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'subType', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder timestampEqualTo(int value) { return QueryBuilder.apply(this, (query) { @@ -1466,6 +1580,61 @@ extension TransactionV2QueryFilter }); } + QueryBuilder typeEqualTo( + TransactionType value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'type', + value: value, + )); + }); + } + + QueryBuilder + typeGreaterThan( + TransactionType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'type', + value: value, + )); + }); + } + + QueryBuilder + typeLessThan( + TransactionType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeBetween( + TransactionType lower, + TransactionType upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'type', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder versionEqualTo(int value) { return QueryBuilder.apply(this, (query) { @@ -1718,6 +1887,18 @@ extension TransactionV2QuerySortBy }); } + QueryBuilder sortBySubType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'subType', Sort.asc); + }); + } + + QueryBuilder sortBySubTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'subType', Sort.desc); + }); + } + QueryBuilder sortByTimestamp() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'timestamp', Sort.asc); @@ -1743,6 +1924,18 @@ extension TransactionV2QuerySortBy }); } + QueryBuilder sortByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder sortByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } + QueryBuilder sortByVersion() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'version', Sort.asc); @@ -1820,6 +2013,18 @@ extension TransactionV2QuerySortThenBy }); } + QueryBuilder thenBySubType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'subType', Sort.asc); + }); + } + + QueryBuilder thenBySubTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'subType', Sort.desc); + }); + } + QueryBuilder thenByTimestamp() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'timestamp', Sort.asc); @@ -1845,6 +2050,18 @@ extension TransactionV2QuerySortThenBy }); } + QueryBuilder thenByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder thenByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } + QueryBuilder thenByVersion() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'version', Sort.asc); @@ -1893,6 +2110,12 @@ extension TransactionV2QueryWhereDistinct }); } + QueryBuilder distinctBySubType() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'subType'); + }); + } + QueryBuilder distinctByTimestamp() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'timestamp'); @@ -1906,6 +2129,12 @@ extension TransactionV2QueryWhereDistinct }); } + QueryBuilder distinctByType() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'type'); + }); + } + QueryBuilder distinctByVersion() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'version'); @@ -1960,6 +2189,13 @@ extension TransactionV2QueryProperty }); } + QueryBuilder + subTypeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'subType'); + }); + } + QueryBuilder timestampProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'timestamp'); @@ -1972,6 +2208,13 @@ extension TransactionV2QueryProperty }); } + QueryBuilder + typeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'type'); + }); + } + QueryBuilder versionProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'version'); diff --git a/lib/pages/wallet_view/sub_widgets/transaction_v2_list.dart b/lib/pages/wallet_view/sub_widgets/transaction_v2_list.dart new file mode 100644 index 000000000..b82cd3539 --- /dev/null +++ b/lib/pages/wallet_view/sub_widgets/transaction_v2_list.dart @@ -0,0 +1,588 @@ +/* + * 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-10-19 + * + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/price_provider.dart'; +import 'package:stackwallet/providers/global/trades_service_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/amount/amount_formatter.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/trade_card.dart'; +import 'package:tuple/tuple.dart'; + +class TransactionsV2List extends ConsumerStatefulWidget { + const TransactionsV2List({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState createState() => _TransactionsV2ListState(); +} + +class _TxListItem extends ConsumerWidget { + const _TxListItem({ + super.key, + required this.tx, + this.radius, + required this.coin, + }); + + final TransactionV2 tx; + final BorderRadius? radius; + final Coin coin; + + @override + Widget build(BuildContext context, WidgetRef ref) { + 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: [ + TransactionCardV2( + key: UniqueKey(), + transaction: tx, + ), + TradeCard( + key: Key(tx.txid + + tx.type.name + + tx.hashCode.toString() + + trade.uuid), // + trade: trade, + onTap: () async { + 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, + // TODO + // transactionIfSentFromStack: tx, + transactionIfSentFromStack: null, + walletName: ref.watch( + walletsChangeNotifierProvider.select( + (value) => value + .getManager(tx.walletId) + .walletName, + ), + ), + walletId: tx.walletId, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4( + trade.tradeId, + tx, + tx.walletId, + ref + .read(walletsChangeNotifierProvider) + .getManager(tx.walletId) + .walletName, + ), + ), + ); + } + }, + ) + ], + ), + ); + } else { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: radius, + ), + child: TransactionCardV2( + // this may mess with combined firo transactions + key: UniqueKey(), + transaction: tx, + ), + ); + } + } +} + +class _TransactionsV2ListState extends ConsumerState { + bool _hasLoaded = false; + List _transactions = []; + + BorderRadius get _borderRadiusFirst { + return BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + BorderRadius get _borderRadiusLast { + return BorderRadius.only( + bottomLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottomRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + @override + Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + + return FutureBuilder( + future: ref + .watch(mainDBProvider) + .isar + .transactionV2s + .where() + .walletIdEqualTo(widget.walletId) + .sortByTimestampDesc() + .findAll(), + builder: (fbContext, AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _transactions = snapshot.data!; + _hasLoaded = true; + } + if (!_hasLoaded) { + return const Column( + children: [ + Spacer(), + Center( + child: LoadingIndicator( + height: 50, + width: 50, + ), + ), + Spacer( + flex: 4, + ), + ], + ); + } + if (_transactions.isEmpty) { + return const NoTransActionsFound(); + } else { + _transactions.sort((a, b) => b.timestamp - a.timestamp); + return RefreshIndicator( + onRefresh: () async { + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(widget.walletId); + if (!ref.read(managerProvider).isRefreshing) { + unawaited(ref.read(managerProvider).refresh()); + } + }, + child: Util.isDesktop + ? ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) { + BorderRadius? radius; + if (_transactions.length == 1) { + radius = BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == _transactions.length - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + final tx = _transactions[index]; + return _TxListItem( + tx: tx, + coin: manager.coin, + radius: radius, + ); + }, + separatorBuilder: (context, index) { + return Container( + width: double.infinity, + height: 2, + color: Theme.of(context) + .extension()! + .background, + ); + }, + itemCount: _transactions.length, + ) + : ListView.builder( + itemCount: _transactions.length, + itemBuilder: (context, index) { + BorderRadius? radius; + bool shouldWrap = false; + if (_transactions.length == 1) { + radius = BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == _transactions.length - 1) { + radius = _borderRadiusLast; + shouldWrap = true; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + final tx = _transactions[index]; + if (shouldWrap) { + return Column( + children: [ + _TxListItem( + tx: tx, + coin: manager.coin, + radius: radius, + ), + const SizedBox( + height: WalletView.navBarHeight + 14, + ), + ], + ); + } else { + return _TxListItem( + tx: tx, + coin: manager.coin, + radius: radius, + ); + } + }, + ), + ); + } + }, + ); + } +} + +class TransactionCardV2 extends ConsumerStatefulWidget { + const TransactionCardV2({ + Key? key, + required this.transaction, + }) : super(key: key); + + final TransactionV2 transaction; + + @override + ConsumerState createState() => _TransactionCardStateV2(); +} + +class _TransactionCardStateV2 extends ConsumerState { + late final TransactionV2 _transaction; + late final String walletId; + late final String prefix; + late final String unit; + late final Coin coin; + late final TransactionType txType; + + String whatIsIt( + TransactionType type, + Coin coin, + int currentHeight, + ) { + final confirmedStatus = _transaction.isConfirmed( + currentHeight, + coin.requiredConfirmations, + ); + + if (type == TransactionType.incoming) { + // if (_transaction.isMinting) { + // return "Minting"; + // } else + if (confirmedStatus) { + return "Received"; + } else { + return "Receiving"; + } + } else if (type == TransactionType.outgoing) { + if (confirmedStatus) { + return "Sent"; + } else { + return "Sending"; + } + } else if (type == TransactionType.sentToSelf) { + return "Sent to self"; + } else { + return type.name; + } + } + + @override + void initState() { + _transaction = widget.transaction; + walletId = _transaction.walletId; + + if (Util.isDesktop) { + if (_transaction.type == TransactionType.outgoing) { + prefix = "-"; + } else if (_transaction.type == TransactionType.incoming) { + prefix = "+"; + } else { + prefix = ""; + } + } else { + prefix = ""; + } + coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; + + unit = coin.ticker; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + final baseCurrency = ref + .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + + final price = ref + .watch(priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))) + .item1; + + final currentHeight = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).currentHeight)); + + final amount = _transaction.getAmount(coin: coin); + + return Material( + color: Theme.of(context).extension()!.popupBG, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(6), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () async { + if (Util.isDesktop) { + // TODO + // await showDialog( + // context: context, + // builder: (context) => DesktopDialog( + // maxHeight: MediaQuery.of(context).size.height - 64, + // maxWidth: 580, + // child: TransactionDetailsView( + // transaction: _transaction, + // coin: coin, + // walletId: walletId, + // ), + // ), + // ); + } else { + unawaited( + Navigator.of(context).pushNamed( + TransactionDetailsView.routeName, + arguments: Tuple3( + _transaction, + coin, + walletId, + ), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + TxIcon( + transaction: _transaction, + coin: coin, + currentHeight: currentHeight, + ), + const SizedBox( + width: 14, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + whatIsIt( + _transaction.type, + coin, + currentHeight, + ), + style: STextStyles.itemSubtitle12(context), + ), + ), + ), + const SizedBox( + width: 10, + ), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Builder( + builder: (_) { + return Text( + "$prefix${ref.watch(pAmountFormatter(coin)).format(amount)}", + style: STextStyles.itemSubtitle12(context), + ); + }, + ), + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + Format.extractDateFrom(_transaction.timestamp), + style: STextStyles.label(context), + ), + ), + ), + if (ref.watch(prefsChangeNotifierProvider + .select((value) => value.externalCalls))) + const SizedBox( + width: 10, + ), + if (ref.watch(prefsChangeNotifierProvider + .select((value) => value.externalCalls))) + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Builder( + builder: (_) { + return Text( + "$prefix${Amount.fromDecimal( + amount.decimal * price, + fractionDigits: 2, + ).fiatString( + locale: locale, + )} $baseCurrency", + style: STextStyles.label(context), + ); + }, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/sub_widgets/tx_icon.dart b/lib/pages/wallet_view/sub_widgets/tx_icon.dart index bcf7c3780..4ad06f66e 100644 --- a/lib/pages/wallet_view/sub_widgets/tx_icon.dart +++ b/lib/pages/wallet_view/sub_widgets/tx_icon.dart @@ -13,6 +13,7 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/stack_theme.dart'; import 'package:stackwallet/themes/theme_providers.dart'; @@ -27,15 +28,24 @@ class TxIcon extends ConsumerWidget { required this.coin, }) : super(key: key); - final Transaction transaction; + final Object transaction; final int currentHeight; final Coin coin; static const Size size = Size(32, 32); String _getAssetName( - bool isCancelled, bool isReceived, bool isPending, IThemeAssets assets) { - if (!isReceived && transaction.subType == TransactionSubType.mint) { + bool isCancelled, + bool isReceived, + bool isPending, + TransactionSubType subType, + IThemeAssets assets, + ) { + if (subType == TransactionSubType.cashFusion) { + return Assets.svg.cashFusion; + } + + if (!isReceived && subType == TransactionSubType.mint) { if (isCancelled) { return Assets.svg.anonymizeFailed; } @@ -66,37 +76,61 @@ class TxIcon extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final txIsReceived = transaction.type == TransactionType.incoming; + final bool txIsReceived; + final String assetName; - final assetName = _getAssetName( - transaction.isCancelled, - txIsReceived, - !transaction.isConfirmed( - currentHeight, - coin.requiredConfirmations, - ), - ref.watch(themeAssetsProvider), - ); + if (transaction is Transaction) { + final tx = transaction as Transaction; + txIsReceived = tx.type == TransactionType.incoming; + assetName = _getAssetName( + tx.isCancelled, + txIsReceived, + !tx.isConfirmed( + currentHeight, + coin.requiredConfirmations, + ), + tx.subType, + ref.watch(themeAssetsProvider), + ); + } else if (transaction is TransactionV2) { + final tx = transaction as TransactionV2; + txIsReceived = tx.type == TransactionType.incoming; + assetName = _getAssetName( + false, + txIsReceived, + !tx.isConfirmed( + currentHeight, + coin.requiredConfirmations, + ), + tx.subType, + ref.watch(themeAssetsProvider), + ); + } else { + throw ArgumentError( + "Unknown transaction type ${transaction.runtimeType}", + ); + } return SizedBox( width: size.width, height: size.height, child: Center( - // if it starts with "assets" we assume its local - // TODO: a more thorough check - child: assetName.startsWith("assets") - ? SvgPicture.asset( + // if it starts with "assets" we assume its local + // TODO: a more thorough check + child: assetName.startsWith("assets") + ? SvgPicture.asset( + assetName, + width: size.width, + height: size.height, + ) + : SvgPicture.file( + File( assetName, - width: size.width, - height: size.height, - ) - : SvgPicture.file( - File( - assetName, - ), - width: size.width, - height: size.height, - )), + ), + width: size.width, + height: size.height, + ), + ), ); } } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 0df83088b..3804475eb 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -19,6 +19,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/special/firo_rescan_recovery_error_dialog.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/transaction_v2_list.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart'; @@ -514,13 +515,18 @@ class _DesktopWalletViewState extends ConsumerState { ? MyTokensView( walletId: widget.walletId, ) - : TransactionsList( - managerProvider: ref.watch( - walletsChangeNotifierProvider.select( - (value) => value.getManagerProvider( - widget.walletId))), - walletId: widget.walletId, - ), + : coin == Coin.bitcoincash || + coin == Coin.bitcoincashTestnet + ? TransactionsV2List( + walletId: widget.walletId, + ) + : TransactionsList( + managerProvider: ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManagerProvider( + widget.walletId))), + walletId: widget.walletId, + ), ), ], ), diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 05aa2c593..4c50cc306 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -19,7 +19,6 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; -import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; @@ -28,6 +27,9 @@ import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart' as stack_address; +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/isar_models.dart' as isar_models; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/signing_data.dart'; @@ -1944,7 +1946,7 @@ class BitcoinCashWallet extends CoinServiceAPI } Future>> _fetchHistory( - List allAddresses) async { + Iterable allAddresses) async { try { List> allTxHashes = []; @@ -1956,9 +1958,10 @@ class BitcoinCashWallet extends CoinServiceAPI if (batches[batchNumber] == null) { batches[batchNumber] = {}; } - final scripthash = _convertToScriptHash(allAddresses[i], _network); + final scripthash = + _convertToScriptHash(allAddresses.elementAt(i), _network); final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); - requestIdToAddressMap[id] = allAddresses[i]; + requestIdToAddressMap[id] = allAddresses.elementAt(i); batches[batchNumber]!.addAll({ id: [scripthash] }); @@ -2024,25 +2027,22 @@ class BitcoinCashWallet extends CoinServiceAPI } }).toSet(); + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + final List> allTxHashes = - await _fetchHistory([...receivingAddresses, ...changeAddresses]); + await _fetchHistory(allAddressesSet); List> allTransactions = []; for (final txHash in allTxHashes) { - final storedTx = await db - .getTransactions(walletId) - .filter() - .txidEqualTo(txHash["tx_hash"] as String) + final storedTx = await db.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) .findFirst(); if (storedTx == null || - storedTx.address.value == null || - storedTx.height == null || - (storedTx.height != null && storedTx.height! <= 0) - // zero conf messes this up - // !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS) - ) { + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0)) { final tx = await cachedElectrumXClient.getTransaction( txHash: txHash["tx_hash"] as String, verbose: true, @@ -2051,225 +2051,167 @@ class BitcoinCashWallet extends CoinServiceAPI // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}"); if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { - tx["address"] = await db - .getAddresses(walletId) - .filter() - .valueEqualTo(txHash["address"] as String) - .findFirst(); tx["height"] = txHash["height"]; allTransactions.add(tx); } } } - // - // Logging.instance.log("addAddresses: $allAddresses", level: LogLevel.Info); - // Logging.instance.log("allTxHashes: $allTxHashes", level: LogLevel.Info); - // - // Logging.instance.log("allTransactions length: ${allTransactions.length}", - // level: LogLevel.Info); - final List> txns = []; + final List txns = []; for (final txData in allTransactions) { Set inputAddresses = {}; Set outputAddresses = {}; - Amount totalInputValue = Amount( - rawValue: BigInt.from(0), - fractionDigits: coin.decimals, - ); - Amount totalOutputValue = Amount( - rawValue: BigInt.from(0), - fractionDigits: coin.decimals, - ); + // set to true if any inputs were detected as owned by this wallet + bool wasSentFromThisWallet = false; + BigInt amountSentFromThisWallet = BigInt.zero; - Amount amountSentFromWallet = Amount( - rawValue: BigInt.from(0), - fractionDigits: coin.decimals, - ); - Amount amountReceivedInWallet = Amount( - rawValue: BigInt.from(0), - fractionDigits: coin.decimals, - ); - Amount changeAmount = Amount( - rawValue: BigInt.from(0), - fractionDigits: coin.decimals, - ); + // set to true if any outputs were detected as owned by this wallet + bool wasReceivedInThisWallet = false; + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; // parse inputs - for (final input in txData["vin"] as List) { - final prevTxid = input["txid"] as String; - final prevOut = input["vout"] as int; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); - // fetch input tx to get address - final inputTx = await cachedElectrumXClient.getTransaction( - txHash: prevTxid, - coin: coin, - ); + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; - for (final output in inputTx["vout"] as List) { - // check matching output - if (prevOut == output["n"]) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: coin.decimals, - ); + final coinbase = map["coinbase"] as String?; - // add value to total - totalInputValue = totalInputValue + value; + if (coinbase == null) { + final txid = map["txid"] as String; + final vout = map["vout"] as int; - // get input(prevOut) address - final address = - output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; + final inputTx = await cachedElectrumXClient.getTransaction( + txHash: txid, coin: coin); - if (address != null) { - inputAddresses.add(address); + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) + as Map); - // if input was from my wallet, add value to amount sent - if (receivingAddresses.contains(address) || - changeAddresses.contains(address)) { - amountSentFromWallet = amountSentFromWallet + value; - } - } - } - } - } - - // parse outputs - for (final output in txData["vout"] as List) { - // get value - final value = Amount.fromDecimal( - Decimal.parse(output["value"].toString()), - fractionDigits: coin.decimals, - ); - - // add value to total - totalOutputValue += value; - - // get output address - final address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; - if (address != null) { - outputAddresses.add(address); - - // if output was to my wallet, add value to amount received - if (receivingAddresses.contains(address)) { - amountReceivedInWallet += value; - } else if (changeAddresses.contains(address)) { - changeAmount += value; - } - } - } - - final mySentFromAddresses = [ - ...receivingAddresses.intersection(inputAddresses), - ...changeAddresses.intersection(inputAddresses) - ]; - final myReceivedOnAddresses = - receivingAddresses.intersection(outputAddresses); - final myChangeReceivedOnAddresses = - changeAddresses.intersection(outputAddresses); - - final fee = totalInputValue - totalOutputValue; - - // this is the address initially used to fetch the txid - isar_models.Address transactionAddress = - txData["address"] as isar_models.Address; - - isar_models.TransactionType type; - Amount amount; - if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) { - // tx is sent to self - type = isar_models.TransactionType.sentToSelf; - amount = - amountSentFromWallet - amountReceivedInWallet - fee - changeAmount; - } else if (mySentFromAddresses.isNotEmpty) { - // outgoing tx - type = isar_models.TransactionType.outgoing; - amount = amountSentFromWallet - changeAmount - fee; - - // TODO fix this hack - final diff = outputAddresses.difference(myChangeReceivedOnAddresses); - final possible = - diff.isNotEmpty ? diff.first : myChangeReceivedOnAddresses.first; - - if (transactionAddress.value != possible) { - transactionAddress = isar_models.Address( - walletId: walletId, - value: possible, - publicKey: [], - type: stack_address.AddressType.nonWallet, - derivationIndex: -1, - derivationPath: null, - subType: stack_address.AddressSubType.nonWallet, + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: coin.decimals, ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); } - } else { - // incoming tx - type = isar_models.TransactionType.incoming; - amount = amountReceivedInWallet; - } - List inputs = []; - List outputs = []; - - for (final json in txData["vin"] as List) { - bool isCoinBase = json['coinbase'] != null; - final input = isar_models.Input( - txid: json['txid'] as String, - vout: json['vout'] as int? ?? -1, - scriptSig: json['scriptSig']?['hex'] as String?, - scriptSigAsm: json['scriptSig']?['asm'] as String?, - isCoinbase: isCoinBase ? isCoinBase : json['is_coinbase'] as bool?, - sequence: json['sequence'] as int?, - innerRedeemScriptAsm: json['innerRedeemscriptAsm'] as String?, + final input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, ); + inputs.add(input); } - for (final json in txData["vout"] as List) { - final output = isar_models.Output( - scriptPubKey: json['scriptPubKey']?['hex'] as String?, - scriptPubKeyAsm: json['scriptPubKey']?['asm'] as String?, - scriptPubKeyType: json['scriptPubKey']?['type'] as String?, - scriptPubKeyAddress: - json["scriptPubKey"]?["addresses"]?[0] as String? ?? - json['scriptPubKey']['type'] as String, - value: Amount.fromDecimal( - Decimal.parse(json["value"].toString()), - fractionDigits: coin.decimals, - ).raw.toInt(), + for (final input in inputs) { + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + amountSentFromThisWallet += input.value; + } + + inputAddresses.addAll(input.addresses); + } + + // parse outputs + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + final output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: coin.decimals, ); outputs.add(output); } - final tx = isar_models.Transaction( + for (final output in outputs) { + if (allAddressesSet + .intersection(output.addresses.toSet()) + .isNotEmpty) {} + + outputAddresses.addAll(output.addresses); + + // if output was to my wallet, add value to amount received + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + } + } + + final totalIn = inputs + .map((e) => e.value) + .reduce((value, element) => value += element); + final totalOut = outputs + .map((e) => e.value) + .reduce((value, element) => value += element); + + final fee = totalIn - totalOut; + + isar_models.TransactionType type; + + // at least one input was owned by this wallet + if (wasSentFromThisWallet) { + type = isar_models.TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { + // definitely sent all to self + type = isar_models.TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // most likely just a typical send + // do nothing here yet + } + } + } else if (wasReceivedInThisWallet) { + // only found outputs owned by this wallet + type = isar_models.TransactionType.incoming; + } else { + throw Exception("Unexpected tx found: $txData"); + } + + final tx = TransactionV2( walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, timestamp: txData["blocktime"] as int? ?? - (DateTime.now().millisecondsSinceEpoch ~/ 1000), + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), type: type, subType: isar_models.TransactionSubType.none, - amount: amount.raw.toInt(), - amountString: amount.toJsonString(), - fee: fee.raw.toInt(), - height: txData["height"] as int?, - isCancelled: false, - isLelantus: false, - slateId: null, - otherData: null, - nonce: null, - inputs: inputs, - outputs: outputs, - numberOfMessages: null, ); - txns.add(Tuple2(tx, transactionAddress)); + txns.add(tx); } - await db.addNewTransactionData(txns, walletId); + await db.updateOrPutTransactionV2s(txns); // quick hack to notify manager to call notifyListeners if // transactions changed diff --git a/lib/services/mixins/electrum_x_parsing.dart b/lib/services/mixins/electrum_x_parsing.dart index 5fe38dfe3..79a74e2a8 100644 --- a/lib/services/mixins/electrum_x_parsing.dart +++ b/lib/services/mixins/electrum_x_parsing.dart @@ -112,6 +112,8 @@ mixin ElectrumXParsing { DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), + subType: TransactionSubType.none, + type: TransactionType.unknown, ); } diff --git a/test/services/coins/firo/firo_wallet_test.mocks.dart b/test/services/coins/firo/firo_wallet_test.mocks.dart index 2487bf007..51d443e5a 100644 --- a/test/services/coins/firo/firo_wallet_test.mocks.dart +++ b/test/services/coins/firo/firo_wallet_test.mocks.dart @@ -12,6 +12,8 @@ import 'package:stackwallet/db/isar/main_db.dart' as _i9; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i6; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i3; import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i11; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart' + as _i14; import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i10; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i12; import 'package:stackwallet/services/transaction_notification_tracker.dart' @@ -1144,6 +1146,16 @@ class MockMainDB extends _i1.Mock implements _i9.MainDB { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); @override + _i5.Future> updateOrPutTransactionV2s( + List<_i14.TransactionV2>? transactions) => + (super.noSuchMethod( + Invocation.method( + #updateOrPutTransactionV2s, + [transactions], + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + @override _i4.QueryBuilder<_i12.EthContract, _i12.EthContract, _i4.QWhere> getEthContracts() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 23748d274..cc6b23b6f 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -17,6 +17,8 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i13; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i12; import 'package:stackwallet/models/balance.dart' as _i9; import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i38; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart' + as _i39; import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i37; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i24; import 'package:stackwallet/models/isar/stack_theme.dart' as _i35; @@ -3590,6 +3592,16 @@ class MockMainDB extends _i1.Mock implements _i14.MainDB { returnValueForMissingStub: _i21.Future.value(), ) as _i21.Future); @override + _i21.Future> updateOrPutTransactionV2s( + List<_i39.TransactionV2>? transactions) => + (super.noSuchMethod( + Invocation.method( + #updateOrPutTransactionV2s, + [transactions], + ), + returnValue: _i21.Future>.value([]), + ) as _i21.Future>); + @override _i18.QueryBuilder<_i24.EthContract, _i24.EthContract, _i18.QWhere> getEthContracts() => (super.noSuchMethod( Invocation.method(