2020-05-12 12:04:54 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
2020-06-20 07:10:00 +00:00
|
|
|
import 'package:mobx/mobx.dart';
|
|
|
|
import 'package:cake_wallet/core/transaction_history.dart';
|
|
|
|
import 'package:cake_wallet/bitcoin/file.dart';
|
|
|
|
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
|
2020-05-12 12:04:54 +00:00
|
|
|
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
|
|
|
|
import 'package:cake_wallet/bitcoin/electrum.dart';
|
2020-06-20 07:10:00 +00:00
|
|
|
|
|
|
|
part 'bitcoin_transaction_history.g.dart';
|
2020-05-12 12:04:54 +00:00
|
|
|
|
2020-06-20 07:10:00 +00:00
|
|
|
// TODO: Think about another transaction store for bitcoin transaction history..
|
|
|
|
|
|
|
|
const _transactionsHistoryFileName = 'transactions.json';
|
|
|
|
|
|
|
|
class BitcoinTransactionHistory = BitcoinTransactionHistoryBase
|
|
|
|
with _$BitcoinTransactionHistory;
|
|
|
|
|
|
|
|
abstract class BitcoinTransactionHistoryBase
|
|
|
|
extends TransactionHistoryBase<BitcoinTransactionInfo> with Store {
|
|
|
|
BitcoinTransactionHistoryBase(
|
|
|
|
{this.eclient, String dirPath, @required String password})
|
|
|
|
: path = '$dirPath/$_transactionsHistoryFileName',
|
2020-05-12 12:04:54 +00:00
|
|
|
_password = password,
|
2020-08-25 16:32:40 +00:00
|
|
|
_height = 0,
|
|
|
|
_isUpdating = false {
|
|
|
|
transactions = ObservableMap<String, BitcoinTransactionInfo>();
|
|
|
|
}
|
2020-05-12 12:04:54 +00:00
|
|
|
|
2020-07-06 20:09:03 +00:00
|
|
|
BitcoinWalletBase wallet;
|
2020-05-12 12:04:54 +00:00
|
|
|
final ElectrumClient eclient;
|
|
|
|
final String path;
|
|
|
|
final String _password;
|
|
|
|
int _height;
|
2020-08-25 16:32:40 +00:00
|
|
|
bool _isUpdating;
|
2020-05-12 12:04:54 +00:00
|
|
|
|
|
|
|
Future<void> init() async {
|
2020-08-25 16:32:40 +00:00
|
|
|
await _load();
|
2020-05-12 12:04:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future update() async {
|
2020-08-25 16:32:40 +00:00
|
|
|
if (_isUpdating) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
_isUpdating = true;
|
|
|
|
final txs = await fetchTransactions();
|
|
|
|
await add(txs);
|
|
|
|
_isUpdating = false;
|
|
|
|
} catch (_) {
|
|
|
|
_isUpdating = false;
|
|
|
|
rethrow;
|
|
|
|
}
|
2020-05-12 12:04:54 +00:00
|
|
|
}
|
|
|
|
|
2020-06-20 07:10:00 +00:00
|
|
|
@override
|
2020-08-25 16:32:40 +00:00
|
|
|
Future<Map<String, BitcoinTransactionInfo>> fetchTransactions() async {
|
2020-05-12 12:04:54 +00:00
|
|
|
final histories =
|
2020-08-25 16:32:40 +00:00
|
|
|
wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash));
|
2020-05-12 12:04:54 +00:00
|
|
|
final _historiesWithDetails = await Future.wait(histories)
|
|
|
|
.then((histories) => histories
|
2020-08-25 16:32:40 +00:00
|
|
|
// .map((h) => h.where((tx) {
|
|
|
|
// final height = tx['height'] as int ?? 0;
|
|
|
|
// // FIXME: Filter only needed transactions
|
|
|
|
// final _tx = get(tx['tx_hash'] as String);
|
|
|
|
//
|
|
|
|
// return height == 0 || height > _height;
|
|
|
|
// }))
|
2020-05-12 12:04:54 +00:00
|
|
|
.expand((i) => i)
|
|
|
|
.toList())
|
|
|
|
.then((histories) => histories.map((tx) => fetchTransactionInfo(
|
|
|
|
hash: tx['tx_hash'] as String, height: tx['height'] as int)));
|
|
|
|
final historiesWithDetails = await Future.wait(_historiesWithDetails);
|
|
|
|
|
2020-08-25 16:32:40 +00:00
|
|
|
return historiesWithDetails.fold<Map<String, BitcoinTransactionInfo>>(
|
|
|
|
<String, BitcoinTransactionInfo>{}, (acc, tx) {
|
|
|
|
acc[tx.id] = tx;
|
|
|
|
return acc;
|
|
|
|
});
|
2020-05-12 12:04:54 +00:00
|
|
|
}
|
|
|
|
|
2020-08-25 16:32:40 +00:00
|
|
|
Future<BitcoinTransactionInfo> fetchTransactionInfo(
|
2020-06-20 07:10:00 +00:00
|
|
|
{@required String hash, @required int height}) async {
|
2020-08-25 16:32:40 +00:00
|
|
|
final tx = await eclient.getTransactionExpanded(hash: hash);
|
|
|
|
return BitcoinTransactionInfo.fromElectrumVerbose(tx,
|
|
|
|
height: height, addresses: wallet.addresses);
|
2020-06-20 07:10:00 +00:00
|
|
|
}
|
|
|
|
|
2020-08-25 16:32:40 +00:00
|
|
|
Future<void> add(Map<String, BitcoinTransactionInfo> transactionsList) async {
|
|
|
|
transactionsList.entries.forEach((entry) {
|
|
|
|
_updateOrInsert(entry.value);
|
|
|
|
|
|
|
|
if (entry.value.height > _height) {
|
|
|
|
_height = entry.value.height;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-06-20 07:10:00 +00:00
|
|
|
await save();
|
2020-05-12 12:04:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> addOne(BitcoinTransactionInfo tx) async {
|
2020-08-25 16:32:40 +00:00
|
|
|
_updateOrInsert(tx);
|
|
|
|
|
|
|
|
if (tx.height > _height) {
|
|
|
|
_height = tx.height;
|
|
|
|
}
|
|
|
|
|
2020-06-20 07:10:00 +00:00
|
|
|
await save();
|
2020-05-12 12:04:54 +00:00
|
|
|
}
|
|
|
|
|
2020-08-25 16:32:40 +00:00
|
|
|
BitcoinTransactionInfo get(String id) => transactions[id];
|
|
|
|
|
|
|
|
Future<void> save() async {
|
|
|
|
final data = json.encode({'height': _height, 'transactions': transactions});
|
|
|
|
|
|
|
|
print('data');
|
|
|
|
print(data);
|
|
|
|
|
|
|
|
await writeData(path: path, password: _password, data: data);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void updateAsync({void Function() onFinished}) {
|
|
|
|
fetchTransactionsAsync((transaction) => _updateOrInsert(transaction),
|
|
|
|
onFinished: onFinished);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void fetchTransactionsAsync(
|
|
|
|
void Function(BitcoinTransactionInfo transaction) onTransactionLoaded,
|
|
|
|
{void Function() onFinished}) async {
|
|
|
|
final histories = await Future.wait(wallet.scriptHashes
|
|
|
|
.map((scriptHash) async => await eclient.getHistory(scriptHash)));
|
|
|
|
final transactionsCount =
|
|
|
|
histories.fold<int>(0, (acc, m) => acc + m.length);
|
|
|
|
var counter = 0;
|
|
|
|
|
|
|
|
final batches = histories.map((metaList) =>
|
|
|
|
_fetchBatchOfTransactions(metaList, onTransactionLoaded: (transaction) {
|
|
|
|
onTransactionLoaded(transaction);
|
|
|
|
counter += 1;
|
|
|
|
|
|
|
|
if (counter == transactionsCount) {
|
|
|
|
onFinished?.call();
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
|
|
|
await Future.wait(batches);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _fetchBatchOfTransactions(
|
|
|
|
Iterable<Map<String, dynamic>> metaList,
|
|
|
|
{void Function(BitcoinTransactionInfo tranasaction)
|
|
|
|
onTransactionLoaded}) async =>
|
|
|
|
metaList.forEach((txMeta) => fetchTransactionInfo(
|
|
|
|
hash: txMeta['tx_hash'] as String,
|
|
|
|
height: txMeta['height'] as int)
|
|
|
|
.then((transaction) => onTransactionLoaded(transaction)));
|
2020-05-12 12:04:54 +00:00
|
|
|
|
|
|
|
Future<Map<String, Object>> _read() async {
|
2020-08-25 16:32:40 +00:00
|
|
|
final content = await read(path: path, password: _password);
|
|
|
|
return json.decode(content) as Map<String, Object>;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> _load() async {
|
2020-05-12 12:04:54 +00:00
|
|
|
try {
|
2020-08-25 16:32:40 +00:00
|
|
|
final content = await _read();
|
|
|
|
final txs = content['transactions'] as Map<String, Object> ?? {};
|
|
|
|
|
|
|
|
txs.entries.forEach((entry) {
|
|
|
|
final val = entry.value;
|
|
|
|
|
|
|
|
if (val is Map<String, Object>) {
|
|
|
|
final tx = BitcoinTransactionInfo.fromJson(val);
|
|
|
|
_updateOrInsert(tx);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
_height = content['height'] as int;
|
|
|
|
} catch (_) {}
|
2020-05-12 12:04:54 +00:00
|
|
|
}
|
|
|
|
|
2020-08-25 16:32:40 +00:00
|
|
|
void _updateOrInsert(BitcoinTransactionInfo transaction) {
|
|
|
|
if (transactions[transaction.id] == null) {
|
|
|
|
transactions[transaction.id] = transaction;
|
|
|
|
} else {
|
|
|
|
final originalTx = transactions[transaction.id];
|
|
|
|
originalTx.confirmations = transaction.confirmations;
|
|
|
|
originalTx.amount = transaction.amount;
|
|
|
|
originalTx.height = transaction.height;
|
|
|
|
originalTx.date ??= transaction.date;
|
|
|
|
originalTx.isPending = transaction.isPending;
|
|
|
|
}
|
2020-05-12 12:04:54 +00:00
|
|
|
}
|
|
|
|
}
|