mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 20:09:23 +00:00
Merge remote-tracking branch 'origin/fusion' into fusion
This commit is contained in:
commit
bafc4d302f
13 changed files with 1160 additions and 247 deletions
|
@ -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<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 =======================================================
|
||||
|
||||
// eth contracts
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -72,7 +72,7 @@ class InputV2 {
|
|||
InputV2()
|
||||
..scriptSigHex = scriptSigHex
|
||||
..sequence = sequence
|
||||
..sequence = sequence
|
||||
..outpoint = outpoint
|
||||
..addresses = List.unmodifiable(addresses)
|
||||
..valueStringSats = valueStringSats
|
||||
..witness = witness
|
||||
|
|
|
@ -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<InputV2> inputs;
|
||||
final List<OutputV2> 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,
|
||||
}
|
||||
|
|
|
@ -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<P>(
|
|||
) ??
|
||||
[]) 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<IsarLinkBase<dynamic>> _transactionV2GetLinks(TransactionV2 object) {
|
|||
}
|
||||
|
||||
void _transactionV2Attach(
|
||||
IsarCollection<dynamic> col, Id id, TransactionV2 object) {}
|
||||
IsarCollection<dynamic> col, Id id, TransactionV2 object) {
|
||||
object.id = id;
|
||||
}
|
||||
|
||||
extension TransactionV2ByIndex on IsarCollection<TransactionV2> {
|
||||
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>
|
||||
timestampEqualTo(int value) {
|
||||
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>
|
||||
versionEqualTo(int value) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
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() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'version');
|
||||
|
|
588
lib/pages/wallet_view/sub_widgets/transaction_v2_list.dart
Normal file
588
lib/pages/wallet_view/sub_widgets/transaction_v2_list.dart
Normal 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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,17 +76,40 @@ 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,
|
||||
if (transaction is Transaction) {
|
||||
final tx = transaction as Transaction;
|
||||
txIsReceived = tx.type == TransactionType.incoming;
|
||||
assetName = _getAssetName(
|
||||
tx.isCancelled,
|
||||
txIsReceived,
|
||||
!transaction.isConfirmed(
|
||||
!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,
|
||||
|
@ -96,7 +129,8 @@ class TxIcon extends ConsumerWidget {
|
|||
),
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,6 +515,11 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
|||
? MyTokensView(
|
||||
walletId: widget.walletId,
|
||||
)
|
||||
: coin == Coin.bitcoincash ||
|
||||
coin == Coin.bitcoincashTestnet
|
||||
? TransactionsV2List(
|
||||
walletId: widget.walletId,
|
||||
)
|
||||
: TransactionsList(
|
||||
managerProvider: ref.watch(
|
||||
walletsChangeNotifierProvider.select(
|
||||
|
|
|
@ -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<List<Map<String, dynamic>>> _fetchHistory(
|
||||
List<String> allAddresses) async {
|
||||
Iterable<String> allAddresses) async {
|
||||
try {
|
||||
List<Map<String, dynamic>> 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<Map<String, dynamic>> allTxHashes =
|
||||
await _fetchHistory([...receivingAddresses, ...changeAddresses]);
|
||||
await _fetchHistory(allAddressesSet);
|
||||
|
||||
List<Map<String, dynamic>> 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! <= 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<Tuple2<isar_models.Transaction, isar_models.Address?>> txns = [];
|
||||
final List<TransactionV2> txns = [];
|
||||
|
||||
for (final txData in allTransactions) {
|
||||
Set<String> inputAddresses = {};
|
||||
Set<String> 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<InputV2> inputs = [];
|
||||
for (final jsonInput in txData["vin"] as List) {
|
||||
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(
|
||||
txHash: prevTxid,
|
||||
coin: coin,
|
||||
txHash: txid, 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) {
|
||||
// check matching output
|
||||
if (prevOut == output["n"]) {
|
||||
// get value
|
||||
final value = Amount.fromDecimal(
|
||||
Decimal.parse(output["value"].toString()),
|
||||
fractionDigits: coin.decimals,
|
||||
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
|
||||
txid: txid,
|
||||
vout: vout,
|
||||
);
|
||||
valueStringSats = prevOut.valueStringSats;
|
||||
addresses.addAll(prevOut.addresses);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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<OutputV2> outputs = [];
|
||||
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);
|
||||
}
|
||||
|
||||
final tx = isar_models.Transaction(
|
||||
walletId: walletId,
|
||||
txid: txData["txid"] as String,
|
||||
timestamp: txData["blocktime"] as int? ??
|
||||
(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,
|
||||
);
|
||||
for (final output in outputs) {
|
||||
if (allAddressesSet
|
||||
.intersection(output.addresses.toSet())
|
||||
.isNotEmpty) {}
|
||||
|
||||
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
|
||||
// transactions changed
|
||||
|
|
|
@ -112,6 +112,8 @@ mixin ElectrumXParsing {
|
|||
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
|
||||
inputs: List.unmodifiable(inputs),
|
||||
outputs: List.unmodifiable(outputs),
|
||||
subType: TransactionSubType.none,
|
||||
type: TransactionType.unknown,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<void>.value(),
|
||||
) as _i5.Future<void>);
|
||||
@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>
|
||||
getEthContracts() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
|
|
|
@ -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<void>.value(),
|
||||
) as _i21.Future<void>);
|
||||
@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>
|
||||
getEthContracts() => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
|
|
Loading…
Reference in a new issue