Merge remote-tracking branch 'origin/fusion' into fusion

This commit is contained in:
sneurlax 2023-10-19 16:38:13 -05:00
commit bafc4d302f
13 changed files with 1160 additions and 247 deletions

View file

@ -13,6 +13,7 @@ import 'package:flutter_native_splash/cli_commands.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:stackwallet/exceptions/main_db/main_db_exception.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/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/contact_entry.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/models/isar/ordinal.dart';
@ -57,6 +58,7 @@ class MainDB {
ContactEntrySchema, ContactEntrySchema,
OrdinalSchema, OrdinalSchema,
LelantusCoinSchema, LelantusCoinSchema,
TransactionV2Schema,
], ],
directory: (await StackFileSystem.applicationIsarDirectory()).path, directory: (await StackFileSystem.applicationIsarDirectory()).path,
// inspector: kDebugMode, // inspector: kDebugMode,
@ -506,6 +508,35 @@ class MainDB {
} }
} }
Future<List<int>> updateOrPutTransactionV2s(
List<TransactionV2> transactions,
) async {
try {
List<int> 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 ======================================================= // ========== Ethereum =======================================================
// eth contracts // eth contracts

View file

@ -251,5 +251,6 @@ enum TransactionSubType {
bip47Notification, // bip47 payment code notification transaction flag bip47Notification, // bip47 payment code notification transaction flag
mint, // firo specific mint, // firo specific
join, // firo specific join, // firo specific
ethToken; // eth token ethToken, // eth token
cashFusion;
} }

View file

@ -364,6 +364,7 @@ const _TransactionsubTypeEnumValueMap = {
'mint': 2, 'mint': 2,
'join': 3, 'join': 3,
'ethToken': 4, 'ethToken': 4,
'cashFusion': 5,
}; };
const _TransactionsubTypeValueEnumMap = { const _TransactionsubTypeValueEnumMap = {
0: TransactionSubType.none, 0: TransactionSubType.none,
@ -371,6 +372,7 @@ const _TransactionsubTypeValueEnumMap = {
2: TransactionSubType.mint, 2: TransactionSubType.mint,
3: TransactionSubType.join, 3: TransactionSubType.join,
4: TransactionSubType.ethToken, 4: TransactionSubType.ethToken,
5: TransactionSubType.cashFusion,
}; };
const _TransactiontypeEnumValueMap = { const _TransactiontypeEnumValueMap = {
'outgoing': 0, 'outgoing': 0,

View file

@ -72,7 +72,7 @@ class InputV2 {
InputV2() InputV2()
..scriptSigHex = scriptSigHex ..scriptSigHex = scriptSigHex
..sequence = sequence ..sequence = sequence
..sequence = sequence ..outpoint = outpoint
..addresses = List.unmodifiable(addresses) ..addresses = List.unmodifiable(addresses)
..valueStringSats = valueStringSats ..valueStringSats = valueStringSats
..witness = witness ..witness = witness

View file

@ -1,14 +1,17 @@
import 'dart:math'; import 'dart:math';
import 'package:isar/isar.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/input_v2.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/output_v2.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
part 'transaction_v2.g.dart'; part 'transaction_v2.g.dart';
@Collection() @Collection()
class TransactionV2 { class TransactionV2 {
final Id id = Isar.autoIncrement; Id id = Isar.autoIncrement;
@Index() @Index()
final String walletId; final String walletId;
@ -28,6 +31,12 @@ class TransactionV2 {
final List<InputV2> inputs; final List<InputV2> inputs;
final List<OutputV2> outputs; final List<OutputV2> outputs;
@enumerated
final TransactionType type;
@enumerated
final TransactionSubType subType;
TransactionV2({ TransactionV2({
required this.walletId, required this.walletId,
required this.blockHash, required this.blockHash,
@ -38,6 +47,8 @@ class TransactionV2 {
required this.inputs, required this.inputs,
required this.outputs, required this.outputs,
required this.version, required this.version,
required this.type,
required this.subType,
}); });
int getConfirmations(int currentChainHeight) { int getConfirmations(int currentChainHeight) {
@ -50,12 +61,32 @@ class TransactionV2 {
return confirmations >= minimumConfirms; 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 @override
String toString() { String toString() {
return 'TransactionV2(\n' return 'TransactionV2(\n'
' walletId: $walletId,\n' ' walletId: $walletId,\n'
' hash: $hash,\n' ' hash: $hash,\n'
' txid: $txid,\n' ' txid: $txid,\n'
' type: $type,\n'
' subType: $subType,\n'
' timestamp: $timestamp,\n' ' timestamp: $timestamp,\n'
' height: $height,\n' ' height: $height,\n'
' blockHash: $blockHash,\n' ' blockHash: $blockHash,\n'
@ -65,3 +96,12 @@ class TransactionV2 {
')'; ')';
} }
} }
enum TxDirection {
outgoing,
incoming;
}
enum TxType {
normal,
}

View file

@ -44,23 +44,35 @@ const TransactionV2Schema = CollectionSchema(
type: IsarType.objectList, type: IsarType.objectList,
target: r'OutputV2', target: r'OutputV2',
), ),
r'timestamp': PropertySchema( r'subType': PropertySchema(
id: 5, id: 5,
name: r'subType',
type: IsarType.byte,
enumMap: _TransactionV2subTypeEnumValueMap,
),
r'timestamp': PropertySchema(
id: 6,
name: r'timestamp', name: r'timestamp',
type: IsarType.long, type: IsarType.long,
), ),
r'txid': PropertySchema( r'txid': PropertySchema(
id: 6, id: 7,
name: r'txid', name: r'txid',
type: IsarType.string, type: IsarType.string,
), ),
r'type': PropertySchema(
id: 8,
name: r'type',
type: IsarType.byte,
enumMap: _TransactionV2typeEnumValueMap,
),
r'version': PropertySchema( r'version': PropertySchema(
id: 7, id: 9,
name: r'version', name: r'version',
type: IsarType.long, type: IsarType.long,
), ),
r'walletId': PropertySchema( r'walletId': PropertySchema(
id: 8, id: 10,
name: r'walletId', name: r'walletId',
type: IsarType.string, type: IsarType.string,
) )
@ -183,10 +195,12 @@ void _transactionV2Serialize(
OutputV2Schema.serialize, OutputV2Schema.serialize,
object.outputs, object.outputs,
); );
writer.writeLong(offsets[5], object.timestamp); writer.writeByte(offsets[5], object.subType.index);
writer.writeString(offsets[6], object.txid); writer.writeLong(offsets[6], object.timestamp);
writer.writeLong(offsets[7], object.version); writer.writeString(offsets[7], object.txid);
writer.writeString(offsets[8], object.walletId); writer.writeByte(offsets[8], object.type.index);
writer.writeLong(offsets[9], object.version);
writer.writeString(offsets[10], object.walletId);
} }
TransactionV2 _transactionV2Deserialize( TransactionV2 _transactionV2Deserialize(
@ -213,11 +227,17 @@ TransactionV2 _transactionV2Deserialize(
OutputV2(), OutputV2(),
) ?? ) ??
[], [],
timestamp: reader.readLong(offsets[5]), subType:
txid: reader.readString(offsets[6]), _TransactionV2subTypeValueEnumMap[reader.readByteOrNull(offsets[5])] ??
version: reader.readLong(offsets[7]), TransactionSubType.none,
walletId: reader.readString(offsets[8]), 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; return object;
} }
@ -251,18 +271,54 @@ P _transactionV2DeserializeProp<P>(
) ?? ) ??
[]) as P; []) as P;
case 5: case 5:
return (reader.readLong(offset)) as P; return (_TransactionV2subTypeValueEnumMap[
reader.readByteOrNull(offset)] ??
TransactionSubType.none) as P;
case 6: case 6:
return (reader.readString(offset)) as P;
case 7:
return (reader.readLong(offset)) as P; return (reader.readLong(offset)) as P;
case 7:
return (reader.readString(offset)) as P;
case 8: 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; return (reader.readString(offset)) as P;
default: default:
throw IsarError('Unknown property with id $propertyId'); 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) { Id _transactionV2GetId(TransactionV2 object) {
return object.id; return object.id;
} }
@ -272,7 +328,9 @@ List<IsarLinkBase<dynamic>> _transactionV2GetLinks(TransactionV2 object) {
} }
void _transactionV2Attach( void _transactionV2Attach(
IsarCollection<dynamic> col, Id id, TransactionV2 object) {} IsarCollection<dynamic> col, Id id, TransactionV2 object) {
object.id = id;
}
extension TransactionV2ByIndex on IsarCollection<TransactionV2> { extension TransactionV2ByIndex on IsarCollection<TransactionV2> {
Future<TransactionV2?> getByTxidWalletId(String txid, String walletId) { Future<TransactionV2?> getByTxidWalletId(String txid, String walletId) {
@ -1275,6 +1333,62 @@ extension TransactionV2QueryFilter
}); });
} }
QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition>
subTypeEqualTo(TransactionSubType value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'subType',
value: value,
));
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition>
subTypeGreaterThan(
TransactionSubType value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'subType',
value: value,
));
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition>
subTypeLessThan(
TransactionSubType value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'subType',
value: value,
));
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition>
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<TransactionV2, TransactionV2, QAfterFilterCondition> QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition>
timestampEqualTo(int value) { timestampEqualTo(int value) {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
@ -1466,6 +1580,61 @@ extension TransactionV2QueryFilter
}); });
} }
QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition> typeEqualTo(
TransactionType value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'type',
value: value,
));
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition>
typeGreaterThan(
TransactionType value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'type',
value: value,
));
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition>
typeLessThan(
TransactionType value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'type',
value: value,
));
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition> 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<TransactionV2, TransactionV2, QAfterFilterCondition> QueryBuilder<TransactionV2, TransactionV2, QAfterFilterCondition>
versionEqualTo(int value) { versionEqualTo(int value) {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
@ -1718,6 +1887,18 @@ extension TransactionV2QuerySortBy
}); });
} }
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> sortBySubType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'subType', Sort.asc);
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> sortBySubTypeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'subType', Sort.desc);
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> sortByTimestamp() { QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> sortByTimestamp() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'timestamp', Sort.asc); return query.addSortBy(r'timestamp', Sort.asc);
@ -1743,6 +1924,18 @@ extension TransactionV2QuerySortBy
}); });
} }
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> sortByType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.asc);
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> sortByTypeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.desc);
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> sortByVersion() { QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> sortByVersion() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'version', Sort.asc); return query.addSortBy(r'version', Sort.asc);
@ -1820,6 +2013,18 @@ extension TransactionV2QuerySortThenBy
}); });
} }
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> thenBySubType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'subType', Sort.asc);
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> thenBySubTypeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'subType', Sort.desc);
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> thenByTimestamp() { QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> thenByTimestamp() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'timestamp', Sort.asc); return query.addSortBy(r'timestamp', Sort.asc);
@ -1845,6 +2050,18 @@ extension TransactionV2QuerySortThenBy
}); });
} }
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> thenByType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.asc);
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> thenByTypeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.desc);
});
}
QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> thenByVersion() { QueryBuilder<TransactionV2, TransactionV2, QAfterSortBy> thenByVersion() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'version', Sort.asc); return query.addSortBy(r'version', Sort.asc);
@ -1893,6 +2110,12 @@ extension TransactionV2QueryWhereDistinct
}); });
} }
QueryBuilder<TransactionV2, TransactionV2, QDistinct> distinctBySubType() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'subType');
});
}
QueryBuilder<TransactionV2, TransactionV2, QDistinct> distinctByTimestamp() { QueryBuilder<TransactionV2, TransactionV2, QDistinct> distinctByTimestamp() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'timestamp'); return query.addDistinctBy(r'timestamp');
@ -1906,6 +2129,12 @@ extension TransactionV2QueryWhereDistinct
}); });
} }
QueryBuilder<TransactionV2, TransactionV2, QDistinct> distinctByType() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'type');
});
}
QueryBuilder<TransactionV2, TransactionV2, QDistinct> distinctByVersion() { QueryBuilder<TransactionV2, TransactionV2, QDistinct> distinctByVersion() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'version'); return query.addDistinctBy(r'version');
@ -1960,6 +2189,13 @@ extension TransactionV2QueryProperty
}); });
} }
QueryBuilder<TransactionV2, TransactionSubType, QQueryOperations>
subTypeProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'subType');
});
}
QueryBuilder<TransactionV2, int, QQueryOperations> timestampProperty() { QueryBuilder<TransactionV2, int, QQueryOperations> timestampProperty() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'timestamp'); return query.addPropertyName(r'timestamp');
@ -1972,6 +2208,13 @@ extension TransactionV2QueryProperty
}); });
} }
QueryBuilder<TransactionV2, TransactionType, QQueryOperations>
typeProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'type');
});
}
QueryBuilder<TransactionV2, int, QQueryOperations> versionProperty() { QueryBuilder<TransactionV2, int, QQueryOperations> versionProperty() {
return QueryBuilder.apply(this, (query) { return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'version'); return query.addPropertyName(r'version');

View file

@ -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<TransactionsV2List> 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<StackColors>()!.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<void>(
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<StackColors>()!.popupBG,
borderRadius: radius,
),
child: TransactionCardV2(
// this may mess with combined firo transactions
key: UniqueKey(),
transaction: tx,
),
);
}
}
}
class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
bool _hasLoaded = false;
List<TransactionV2> _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<List<TransactionV2>> 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<StackColors>()!
.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<TransactionCardV2> createState() => _TransactionCardStateV2();
}
class _TransactionCardStateV2 extends ConsumerState<TransactionCardV2> {
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<StackColors>()!.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<void>(
// 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),
);
},
),
),
),
],
),
],
),
),
],
),
),
),
),
);
}
}

View file

@ -13,6 +13,7 @@ import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.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/models/isar_models.dart';
import 'package:stackwallet/models/isar/stack_theme.dart'; import 'package:stackwallet/models/isar/stack_theme.dart';
import 'package:stackwallet/themes/theme_providers.dart'; import 'package:stackwallet/themes/theme_providers.dart';
@ -27,15 +28,24 @@ class TxIcon extends ConsumerWidget {
required this.coin, required this.coin,
}) : super(key: key); }) : super(key: key);
final Transaction transaction; final Object transaction;
final int currentHeight; final int currentHeight;
final Coin coin; final Coin coin;
static const Size size = Size(32, 32); static const Size size = Size(32, 32);
String _getAssetName( String _getAssetName(
bool isCancelled, bool isReceived, bool isPending, IThemeAssets assets) { bool isCancelled,
if (!isReceived && transaction.subType == TransactionSubType.mint) { bool isReceived,
bool isPending,
TransactionSubType subType,
IThemeAssets assets,
) {
if (subType == TransactionSubType.cashFusion) {
return Assets.svg.cashFusion;
}
if (!isReceived && subType == TransactionSubType.mint) {
if (isCancelled) { if (isCancelled) {
return Assets.svg.anonymizeFailed; return Assets.svg.anonymizeFailed;
} }
@ -66,17 +76,40 @@ class TxIcon extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final txIsReceived = transaction.type == TransactionType.incoming; final bool txIsReceived;
final String assetName;
final assetName = _getAssetName( if (transaction is Transaction) {
transaction.isCancelled, final tx = transaction as Transaction;
txIsReceived = tx.type == TransactionType.incoming;
assetName = _getAssetName(
tx.isCancelled,
txIsReceived, txIsReceived,
!transaction.isConfirmed( !tx.isConfirmed(
currentHeight, currentHeight,
coin.requiredConfirmations, coin.requiredConfirmations,
), ),
tx.subType,
ref.watch(themeAssetsProvider), 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( return SizedBox(
width: size.width, width: size.width,
@ -96,7 +129,8 @@ class TxIcon extends ConsumerWidget {
), ),
width: size.width, width: size.width,
height: size.height, height: size.height,
)), ),
),
); );
} }
} }

View file

@ -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/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/special/firo_rescan_recovery_error_dialog.dart';
import 'package:stackwallet/pages/token_view/my_tokens_view.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/sub_widgets/transactions_list.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_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_features.dart';
@ -514,6 +515,11 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
? MyTokensView( ? MyTokensView(
walletId: widget.walletId, walletId: widget.walletId,
) )
: coin == Coin.bitcoincash ||
coin == Coin.bitcoincashTestnet
? TransactionsV2List(
walletId: widget.walletId,
)
: TransactionsList( : TransactionsList(
managerProvider: ref.watch( managerProvider: ref.watch(
walletsChangeNotifierProvider.select( walletsChangeNotifierProvider.select(

View file

@ -19,7 +19,6 @@ import 'package:bip39/bip39.dart' as bip39;
import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoindart/bitcoindart.dart'; import 'package:bitcoindart/bitcoindart.dart';
import 'package:bs58check/bs58check.dart' as bs58check; import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.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/balance.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart' import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'
as stack_address; 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/isar/models/isar_models.dart' as isar_models;
import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/signing_data.dart'; import 'package:stackwallet/models/signing_data.dart';
@ -1944,7 +1946,7 @@ class BitcoinCashWallet extends CoinServiceAPI
} }
Future<List<Map<String, dynamic>>> _fetchHistory( Future<List<Map<String, dynamic>>> _fetchHistory(
List<String> allAddresses) async { Iterable<String> allAddresses) async {
try { try {
List<Map<String, dynamic>> allTxHashes = []; List<Map<String, dynamic>> allTxHashes = [];
@ -1956,9 +1958,10 @@ class BitcoinCashWallet extends CoinServiceAPI
if (batches[batchNumber] == null) { if (batches[batchNumber] == null) {
batches[batchNumber] = {}; batches[batchNumber] = {};
} }
final scripthash = _convertToScriptHash(allAddresses[i], _network); final scripthash =
_convertToScriptHash(allAddresses.elementAt(i), _network);
final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); final id = Logger.isTestEnv ? "$i" : const Uuid().v1();
requestIdToAddressMap[id] = allAddresses[i]; requestIdToAddressMap[id] = allAddresses.elementAt(i);
batches[batchNumber]!.addAll({ batches[batchNumber]!.addAll({
id: [scripthash] id: [scripthash]
}); });
@ -2024,25 +2027,22 @@ class BitcoinCashWallet extends CoinServiceAPI
} }
}).toSet(); }).toSet();
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
final List<Map<String, dynamic>> allTxHashes = final List<Map<String, dynamic>> allTxHashes =
await _fetchHistory([...receivingAddresses, ...changeAddresses]); await _fetchHistory(allAddressesSet);
List<Map<String, dynamic>> allTransactions = []; List<Map<String, dynamic>> allTransactions = [];
for (final txHash in allTxHashes) { for (final txHash in allTxHashes) {
final storedTx = await db final storedTx = await db.isar.transactionV2s
.getTransactions(walletId) .where()
.filter() .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
.txidEqualTo(txHash["tx_hash"] as String)
.findFirst(); .findFirst();
if (storedTx == null || if (storedTx == null ||
storedTx.address.value == null ||
storedTx.height == null || storedTx.height == null ||
(storedTx.height != null && storedTx.height! <= 0) (storedTx.height != null && storedTx.height! <= 0)) {
// zero conf messes this up
// !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)
) {
final tx = await cachedElectrumXClient.getTransaction( final tx = await cachedElectrumXClient.getTransaction(
txHash: txHash["tx_hash"] as String, txHash: txHash["tx_hash"] as String,
verbose: true, verbose: true,
@ -2051,225 +2051,167 @@ class BitcoinCashWallet extends CoinServiceAPI
// Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}"); // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}");
if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
tx["address"] = await db
.getAddresses(walletId)
.filter()
.valueEqualTo(txHash["address"] as String)
.findFirst();
tx["height"] = txHash["height"]; tx["height"] = txHash["height"];
allTransactions.add(tx); 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<Tuple2<isar_models.Transaction, isar_models.Address?>> txns = []; final List<TransactionV2> txns = [];
for (final txData in allTransactions) { for (final txData in allTransactions) {
Set<String> inputAddresses = {}; Set<String> inputAddresses = {};
Set<String> outputAddresses = {}; Set<String> outputAddresses = {};
Amount totalInputValue = Amount( // set to true if any inputs were detected as owned by this wallet
rawValue: BigInt.from(0), bool wasSentFromThisWallet = false;
fractionDigits: coin.decimals, BigInt amountSentFromThisWallet = BigInt.zero;
);
Amount totalOutputValue = Amount(
rawValue: BigInt.from(0),
fractionDigits: coin.decimals,
);
Amount amountSentFromWallet = Amount( // set to true if any outputs were detected as owned by this wallet
rawValue: BigInt.from(0), bool wasReceivedInThisWallet = false;
fractionDigits: coin.decimals, BigInt amountReceivedInThisWallet = BigInt.zero;
); BigInt changeAmountReceivedInThisWallet = BigInt.zero;
Amount amountReceivedInWallet = Amount(
rawValue: BigInt.from(0),
fractionDigits: coin.decimals,
);
Amount changeAmount = Amount(
rawValue: BigInt.from(0),
fractionDigits: coin.decimals,
);
// parse inputs // parse inputs
for (final input in txData["vin"] as List) { final List<InputV2> inputs = [];
final prevTxid = input["txid"] as String; for (final jsonInput in txData["vin"] as List) {
final prevOut = input["vout"] as int; final map = Map<String, dynamic>.from(jsonInput as Map);
final List<String> addresses = [];
String valueStringSats = "0";
OutpointV2? outpoint;
final coinbase = map["coinbase"] as String?;
if (coinbase == null) {
final txid = map["txid"] as String;
final vout = map["vout"] as int;
// fetch input tx to get address
final inputTx = await cachedElectrumXClient.getTransaction( final inputTx = await cachedElectrumXClient.getTransaction(
txHash: prevTxid, txHash: txid, coin: coin);
coin: coin,
final prevOutJson = Map<String, dynamic>.from(
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
as Map);
final prevOut = OutputV2.fromElectrumXJson(
prevOutJson,
decimalPlaces: coin.decimals,
); );
for (final output in inputTx["vout"] as List) { outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
// check matching output txid: txid,
if (prevOut == output["n"]) { vout: vout,
// get value );
final value = Amount.fromDecimal( valueStringSats = prevOut.valueStringSats;
Decimal.parse(output["value"].toString()), addresses.addAll(prevOut.addresses);
fractionDigits: coin.decimals, }
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?,
); );
// add value to total
totalInputValue = totalInputValue + value;
// get input(prevOut) address
final address =
output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
if (address != null) {
inputAddresses.add(address);
// 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,
);
}
} else {
// incoming tx
type = isar_models.TransactionType.incoming;
amount = amountReceivedInWallet;
}
List<isar_models.Input> inputs = [];
List<isar_models.Output> 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?,
);
inputs.add(input); inputs.add(input);
} }
for (final json in txData["vout"] as List) { for (final input in inputs) {
final output = isar_models.Output( if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
scriptPubKey: json['scriptPubKey']?['hex'] as String?, wasSentFromThisWallet = true;
scriptPubKeyAsm: json['scriptPubKey']?['asm'] as String?, amountSentFromThisWallet += input.value;
scriptPubKeyType: json['scriptPubKey']?['type'] as String?, }
scriptPubKeyAddress:
json["scriptPubKey"]?["addresses"]?[0] as String? ?? inputAddresses.addAll(input.addresses);
json['scriptPubKey']['type'] as String, }
value: Amount.fromDecimal(
Decimal.parse(json["value"].toString()), // parse outputs
fractionDigits: coin.decimals, final List<OutputV2> outputs = [];
).raw.toInt(), for (final outputJson in txData["vout"] as List) {
final output = OutputV2.fromElectrumXJson(
Map<String, dynamic>.from(outputJson as Map),
decimalPlaces: coin.decimals,
); );
outputs.add(output); outputs.add(output);
} }
final tx = isar_models.Transaction( for (final output in outputs) {
walletId: walletId, if (allAddressesSet
txid: txData["txid"] as String, .intersection(output.addresses.toSet())
timestamp: txData["blocktime"] as int? ?? .isNotEmpty) {}
(DateTime.now().millisecondsSinceEpoch ~/ 1000),
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)); 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;
}
} }
await db.addNewTransactionData(txns, walletId); 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.timestamp().millisecondsSinceEpoch ~/ 1000,
inputs: List.unmodifiable(inputs),
outputs: List.unmodifiable(outputs),
type: type,
subType: isar_models.TransactionSubType.none,
);
txns.add(tx);
}
await db.updateOrPutTransactionV2s(txns);
// quick hack to notify manager to call notifyListeners if // quick hack to notify manager to call notifyListeners if
// transactions changed // transactions changed

View file

@ -112,6 +112,8 @@ mixin ElectrumXParsing {
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
inputs: List.unmodifiable(inputs), inputs: List.unmodifiable(inputs),
outputs: List.unmodifiable(outputs), outputs: List.unmodifiable(outputs),
subType: TransactionSubType.none,
type: TransactionType.unknown,
); );
} }

View file

@ -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/cached_electrumx.dart' as _i6;
import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i3; 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/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/contact_entry.dart' as _i10;
import 'package:stackwallet/models/isar/models/isar_models.dart' as _i12; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i12;
import 'package:stackwallet/services/transaction_notification_tracker.dart' import 'package:stackwallet/services/transaction_notification_tracker.dart'
@ -1144,6 +1146,16 @@ class MockMainDB extends _i1.Mock implements _i9.MainDB {
returnValueForMissingStub: _i5.Future<void>.value(), returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>); ) as _i5.Future<void>);
@override @override
_i5.Future<List<int>> updateOrPutTransactionV2s(
List<_i14.TransactionV2>? transactions) =>
(super.noSuchMethod(
Invocation.method(
#updateOrPutTransactionV2s,
[transactions],
),
returnValue: _i5.Future<List<int>>.value(<int>[]),
) as _i5.Future<List<int>>);
@override
_i4.QueryBuilder<_i12.EthContract, _i12.EthContract, _i4.QWhere> _i4.QueryBuilder<_i12.EthContract, _i12.EthContract, _i4.QWhere>
getEthContracts() => (super.noSuchMethod( getEthContracts() => (super.noSuchMethod(
Invocation.method( Invocation.method(

View file

@ -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/electrumx_rpc/electrumx.dart' as _i12;
import 'package:stackwallet/models/balance.dart' as _i9; 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/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/contact_entry.dart' as _i37;
import 'package:stackwallet/models/isar/models/isar_models.dart' as _i24; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i24;
import 'package:stackwallet/models/isar/stack_theme.dart' as _i35; 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<void>.value(), returnValueForMissingStub: _i21.Future<void>.value(),
) as _i21.Future<void>); ) as _i21.Future<void>);
@override @override
_i21.Future<List<int>> updateOrPutTransactionV2s(
List<_i39.TransactionV2>? transactions) =>
(super.noSuchMethod(
Invocation.method(
#updateOrPutTransactionV2s,
[transactions],
),
returnValue: _i21.Future<List<int>>.value(<int>[]),
) as _i21.Future<List<int>>);
@override
_i18.QueryBuilder<_i24.EthContract, _i24.EthContract, _i18.QWhere> _i18.QueryBuilder<_i24.EthContract, _i24.EthContract, _i18.QWhere>
getEthContracts() => (super.noSuchMethod( getEthContracts() => (super.noSuchMethod(
Invocation.method( Invocation.method(