wallet periodic refresh, more bch impl, various other clean up and fixes

This commit is contained in:
julian 2023-10-30 16:58:15 -06:00
parent 59b8fe38e2
commit 11fe9f19b5
10 changed files with 829 additions and 96 deletions

View file

@ -2202,7 +2202,7 @@ class BitcoinCashWallet extends CoinServiceAPI
type = isar_models.TransactionType.incoming;
} else {
Logging.instance.log(
"Unexpected tx found: $txData",
"Unexpected tx found (ignoring it): $txData",
level: LogLevel.Error,
);
continue;

View file

@ -1,3 +1,5 @@
import 'package:bech32/bech32.dart';
import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
@ -48,4 +50,39 @@ abstract class Bip39HDCurrency extends Bip39Currency {
}
return reversedPairs.join("");
}
DerivePathType addressType({required String address}) {
Uint8List? decodeBase58;
Segwit? decodeBech32;
try {
decodeBase58 = bs58check.decode(address);
} catch (err) {
// Base58check decode fail
}
if (decodeBase58 != null) {
if (decodeBase58[0] == networkParams.p2pkhPrefix) {
// P2PKH
return DerivePathType.bip44;
}
if (decodeBase58[0] == networkParams.p2shPrefix) {
// P2SH
return DerivePathType.bip49;
}
throw ArgumentError('Invalid version or Network mismatch');
} else {
try {
decodeBech32 = segwit.decode(address);
} catch (err) {
// Bech32 decode fail
}
if (networkParams.bech32Hrp != decodeBech32!.hrp) {
throw ArgumentError('Invalid prefix or Network mismatch');
}
if (decodeBech32.version != 0) {
throw ArgumentError('Invalid address version');
}
// P2WPKH
return DerivePathType.bip84;
}
}
}

View file

@ -1,4 +1,8 @@
import 'dart:typed_data';
import 'package:bech32/bech32.dart';
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bs58check/bs58check.dart' as bs58check;
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
@ -161,4 +165,52 @@ class Bitcoincash extends Bip39HDCurrency {
return addr.startsWith("q");
}
// TODO: [prio=med] bch p2sh addresses?
@override
DerivePathType addressType({required String address}) {
Uint8List? decodeBase58;
Segwit? decodeBech32;
try {
if (bitbox.Address.detectFormat(address) ==
bitbox.Address.formatCashAddr) {
if (_validateCashAddr(address)) {
address = bitbox.Address.toLegacyAddress(address);
} else {
throw ArgumentError('$address is not currently supported');
}
}
} catch (_) {
// invalid cash addr format
}
try {
decodeBase58 = bs58check.decode(address);
} catch (err) {
// Base58check decode fail
}
if (decodeBase58 != null) {
if (decodeBase58[0] == networkParams.p2pkhPrefix) {
// P2PKH
return DerivePathType.bip44;
}
throw ArgumentError('Invalid version or Network mismatch');
} else {
try {
decodeBech32 = segwit.decode(address);
} catch (err) {
// Bech32 decode fail
}
if (decodeBech32 != null) {
if (networkParams.bech32Hrp != decodeBech32.hrp) {
throw ArgumentError('Invalid prefix or Network mismatch');
}
if (decodeBech32.version != 0) {
throw ArgumentError('Invalid address version');
}
}
}
throw ArgumentError('$address has no matching Script');
}
}

View file

@ -93,6 +93,28 @@ class WalletInfo {
? {}
: Map<String, dynamic>.from(jsonDecode(otherDataJsonString!) as Map);
//============================================================================
//============= Updaters ================================================
/// copies this with a new balance and updates the db
Future<void> updateBalance({
required Balance newBalance,
required Isar isar,
}) async {
final newEncoded = newBalance.toJsonIgnoreCoin();
// only update if there were changes to the balance
if (cachedBalanceString != newEncoded) {
final updated = copyWith(
cachedBalanceString: newEncoded,
);
await isar.writeTxn(() async {
await isar.walletInfo.delete(id);
await isar.walletInfo.put(updated);
});
}
}
//============================================================================
WalletInfo({

View file

@ -25,11 +25,11 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T> {
break;
case AddressType.p2sh:
derivePathType = DerivePathType.bip44;
derivePathType = DerivePathType.bip49;
break;
case AddressType.p2wpkh:
derivePathType = DerivePathType.bip44;
derivePathType = DerivePathType.bip84;
break;
default:
@ -96,7 +96,7 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T> {
} else if (chain == 1) {
subType = AddressSubType.change;
} else {
// TODO others?
// TODO: [prio=low] others or throw?
subType = AddressSubType.unknown;
}

View file

@ -3,7 +3,6 @@ import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart';
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
@ -16,12 +15,10 @@ class BitcoinWallet extends Bip39HDWallet with ElectrumXMixin {
BitcoinWallet(
super.cryptoCurrency, {
required NodeService nodeService,
required Prefs prefs,
}) {
// TODO: [prio=low] ensure this hack isn't needed
assert(cryptoCurrency is Bitcoin);
this.prefs = prefs;
this.nodeService = nodeService;
}
@ -60,7 +57,7 @@ class BitcoinWallet extends Bip39HDWallet with ElectrumXMixin {
Future<void> updateTransactions() async {
final currentChainHeight = await fetchChainHeight();
final data = await fetchTransactions(
final data = await fetchTransactionsV1(
addresses: await _fetchAllOwnAddresses(),
currentChainHeight: currentChainHeight,
);
@ -95,4 +92,14 @@ class BitcoinWallet extends Bip39HDWallet with ElectrumXMixin {
// TODO: implement updateUTXOs
throw UnimplementedError();
}
@override
Future<bool> pingCheck() async {
try {
final result = await electrumX.ping();
return result;
} catch (_) {
return false;
}
}
}

View file

@ -1,13 +1,23 @@
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:isar/isar.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart';
import 'package:stackwallet/services/coins/bitcoincash/bch_utils.dart';
import 'package:stackwallet/services/coins/bitcoincash/cashtokens.dart'
as cash_tokens;
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart';
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
import 'package:tuple/tuple.dart';
class BitcoincashWallet extends Bip39HDWallet with ElectrumXMixin {
@override
@ -16,12 +26,10 @@ class BitcoincashWallet extends Bip39HDWallet with ElectrumXMixin {
BitcoincashWallet(
super.cryptoCurrency, {
required NodeService nodeService,
required Prefs prefs,
}) {
// TODO: [prio=low] ensure this hack isn't needed
assert(cryptoCurrency is Bitcoin);
this.prefs = prefs;
this.nodeService = nodeService;
}
@ -32,12 +40,12 @@ class BitcoincashWallet extends Bip39HDWallet with ElectrumXMixin {
.getAddresses(walletId)
.filter()
.not()
.group(
(q) => q
.typeEqualTo(AddressType.nonWallet)
.or()
.subTypeEqualTo(AddressSubType.nonWallet),
)
.typeEqualTo(AddressType.nonWallet)
.and()
.group((q) => q
.subTypeEqualTo(AddressSubType.receiving)
.or()
.subTypeEqualTo(AddressSubType.change))
.findAll();
return allAddresses;
}
@ -45,54 +53,382 @@ class BitcoincashWallet extends Bip39HDWallet with ElectrumXMixin {
// ===========================================================================
@override
Future<void> refresh() {
// TODO: implement refresh
throw UnimplementedError();
}
Future<void> updateBalance() async {
final utxos = await mainDB.getUTXOs(walletId).findAll();
@override
Future<void> updateBalance() {
// TODO: implement updateBalance
throw UnimplementedError();
final currentChainHeight = await fetchChainHeight();
Amount satoshiBalanceTotal = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
);
Amount satoshiBalancePending = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
);
Amount satoshiBalanceSpendable = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
);
Amount satoshiBalanceBlocked = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
);
for (final utxo in utxos) {
final utxoAmount = Amount(
rawValue: BigInt.from(utxo.value),
fractionDigits: cryptoCurrency.fractionDigits,
);
satoshiBalanceTotal += utxoAmount;
if (utxo.isBlocked) {
satoshiBalanceBlocked += utxoAmount;
} else {
if (utxo.isConfirmed(
currentChainHeight,
cryptoCurrency.minConfirms,
)) {
satoshiBalanceSpendable += utxoAmount;
} else {
satoshiBalancePending += utxoAmount;
}
}
}
final balance = Balance(
total: satoshiBalanceTotal,
spendable: satoshiBalanceSpendable,
blockedTotal: satoshiBalanceBlocked,
pendingSpendable: satoshiBalancePending,
);
await walletInfo.updateBalance(newBalance: balance, isar: mainDB.isar);
}
@override
Future<void> updateTransactions() async {
final currentChainHeight = await fetchChainHeight();
List<Address> allAddressesOld = await _fetchAllOwnAddresses();
final data = await fetchTransactions(
addresses: await _fetchAllOwnAddresses(),
currentChainHeight: currentChainHeight,
);
Set<String> receivingAddresses = allAddressesOld
.where((e) => e.subType == AddressSubType.receiving)
.map((e) {
if (bitbox.Address.detectFormat(e.value) == bitbox.Address.formatLegacy &&
(cryptoCurrency.addressType(address: e.value) ==
DerivePathType.bip44 ||
cryptoCurrency.addressType(address: e.value) ==
DerivePathType.bch44)) {
return bitbox.Address.toCashAddress(e.value);
} else {
return e.value;
}
}).toSet();
await mainDB.addNewTransactionData(
data
.map(
(e) => Tuple2(
e.transaction,
e.address,
),
)
.toList(),
walletId,
);
Set<String> changeAddresses = allAddressesOld
.where((e) => e.subType == AddressSubType.change)
.map((e) {
if (bitbox.Address.detectFormat(e.value) == bitbox.Address.formatLegacy &&
(cryptoCurrency.addressType(address: e.value) ==
DerivePathType.bip44 ||
cryptoCurrency.addressType(address: e.value) ==
DerivePathType.bch44)) {
return bitbox.Address.toCashAddress(e.value);
} else {
return e.value;
}
}).toSet();
// TODO: [prio=med] get rid of this and watch isar instead
// quick hack to notify manager to call notifyListeners if
// transactions changed
if (data.isNotEmpty) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Transactions updated/added for: $walletId ${walletInfo.name}",
walletId,
),
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
final List<Map<String, dynamic>> allTxHashes =
await fetchHistory(allAddressesSet);
List<Map<String, dynamic>> allTransactions = [];
for (final txHash in allTxHashes) {
final storedTx = await mainDB.isar.transactionV2s
.where()
.txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
.findFirst();
if (storedTx == null ||
storedTx.height == null ||
(storedTx.height != null && storedTx.height! <= 0)) {
final tx = await electrumXCached.getTransaction(
txHash: txHash["tx_hash"] as String,
verbose: true,
coin: cryptoCurrency.coin,
);
// check for duplicates before adding to list
if (allTransactions
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
-1) {
tx["height"] = txHash["height"];
allTransactions.add(tx);
}
}
}
final List<TransactionV2> txns = [];
for (final txData in allTransactions) {
// set to true if any inputs were detected as owned by this wallet
bool wasSentFromThisWallet = false;
// 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
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;
final inputTx = await electrumXCached.getTransaction(
txHash: txid,
coin: cryptoCurrency.coin,
);
final prevOutJson = Map<String, dynamic>.from(
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
as Map);
final prevOut = OutputV2.fromElectrumXJson(
prevOutJson,
decimalPlaces: cryptoCurrency.fractionDigits,
walletOwns: false, // doesn't matter here as this is not saved
);
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
txid: txid,
vout: vout,
);
valueStringSats = prevOut.valueStringSats;
addresses.addAll(prevOut.addresses);
}
InputV2 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?,
// don't know yet if wallet owns. Need addresses first
walletOwns: false,
);
if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
wasSentFromThisWallet = true;
input = input.copyWith(walletOwns: true);
}
inputs.add(input);
}
// parse outputs
final List<OutputV2> outputs = [];
for (final outputJson in txData["vout"] as List) {
OutputV2 output = OutputV2.fromElectrumXJson(
Map<String, dynamic>.from(outputJson as Map),
decimalPlaces: cryptoCurrency.fractionDigits,
// don't know yet if wallet owns. Need addresses first
walletOwns: false,
);
// if output was to my wallet, add value to amount received
if (receivingAddresses
.intersection(output.addresses.toSet())
.isNotEmpty) {
wasReceivedInThisWallet = true;
amountReceivedInThisWallet += output.value;
output = output.copyWith(walletOwns: true);
} else if (changeAddresses
.intersection(output.addresses.toSet())
.isNotEmpty) {
wasReceivedInThisWallet = true;
changeAmountReceivedInThisWallet += output.value;
output = output.copyWith(walletOwns: true);
}
outputs.add(output);
}
final totalOut = outputs
.map((e) => e.value)
.fold(BigInt.zero, (value, element) => value + element);
TransactionType type;
TransactionSubType subType = TransactionSubType.none;
// at least one input was owned by this wallet
if (wasSentFromThisWallet) {
type = TransactionType.outgoing;
if (wasReceivedInThisWallet) {
if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
totalOut) {
// definitely sent all to self
type = TransactionType.sentToSelf;
} else if (amountReceivedInThisWallet == BigInt.zero) {
// most likely just a typical send
// do nothing here yet
}
// check vout 0 for special scripts
if (outputs.isNotEmpty) {
final output = outputs.first;
// check for fusion
if (BchUtils.isFUZE(output.scriptPubKeyHex.toUint8ListFromHex)) {
subType = TransactionSubType.cashFusion;
} else {
// check other cases here such as SLP or cash tokens etc
}
}
}
} else if (wasReceivedInThisWallet) {
// only found outputs owned by this wallet
type = TransactionType.incoming;
} else {
Logging.instance.log(
"Unexpected tx found (ignoring it): $txData",
level: LogLevel.Error,
);
continue;
}
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: subType,
);
txns.add(tx);
}
await mainDB.updateOrPutTransactionV2s(txns);
}
({String? blockedReason, bool blocked}) checkBlock(
Map<String, dynamic> jsonUTXO, String? scriptPubKeyHex) {
bool blocked = false;
String? blockedReason;
if (scriptPubKeyHex != null) {
// check for cash tokens
try {
final ctOutput =
cash_tokens.unwrap_spk(scriptPubKeyHex.toUint8ListFromHex);
if (ctOutput.token_data != null) {
// found a token!
blocked = true;
blockedReason = "Cash token output detected";
}
} catch (e, s) {
// Probably doesn't contain a cash token so just log failure
Logging.instance.log(
"Script pub key \"$scriptPubKeyHex\" cash token"
" parsing check failed: $e\n$s",
level: LogLevel.Warning,
);
}
// check for SLP tokens if not already blocked
if (!blocked && BchUtils.isSLP(scriptPubKeyHex.toUint8ListFromHex)) {
blocked = true;
blockedReason = "SLP token output detected";
}
}
return (blockedReason: blockedReason, blocked: blocked);
}
@override
Future<void> updateUTXOs() async {
final allAddresses = await _fetchAllOwnAddresses();
try {
final fetchedUtxoList = <List<Map<String, dynamic>>>[];
final Map<int, Map<String, List<dynamic>>> batches = {};
const batchSizeMax = 10;
int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) {
batches[batchNumber] = {};
}
final scriptHash = cryptoCurrency.addressToScriptHash(
address: allAddresses[i].value,
);
batches[batchNumber]!.addAll({
scriptHash: [scriptHash]
});
if (i % batchSizeMax == batchSizeMax - 1) {
batchNumber++;
}
}
for (int i = 0; i < batches.length; i++) {
final response = await electrumX.getBatchUTXOs(args: batches[i]!);
for (final entry in response.entries) {
if (entry.value.isNotEmpty) {
fetchedUtxoList.add(entry.value);
}
}
}
final List<UTXO> outputArray = [];
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final utxo = await parseUTXO(jsonUTXO: fetchedUtxoList[i][j]);
outputArray.add(utxo);
}
}
await mainDB.updateUTXOs(walletId, outputArray);
} catch (e, s) {
Logging.instance.log(
"Output fetch unsuccessful: $e\n$s",
level: LogLevel.Error,
);
}
}
@override
Future<void> updateUTXOs() {
// TODO: implement updateUTXOs
throw UnimplementedError();
Future<bool> pingCheck() async {
try {
final result = await electrumX.ping();
return result;
} catch (_) {
return false;
}
}
}

View file

@ -1,8 +1,14 @@
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/bip39_wallet.dart';
class EpiccashWallet extends Bip39Wallet {
EpiccashWallet(super.cryptoCurrency);
final NodeService nodeService;
EpiccashWallet(super.cryptoCurrency, {required this.nodeService});
@override
Future<TxData> confirmSend({required TxData txData}) {
@ -34,12 +40,6 @@ class EpiccashWallet extends Bip39Wallet {
throw UnimplementedError();
}
@override
Future<void> updateNode() {
// TODO: implement updateNode
throw UnimplementedError();
}
@override
Future<void> updateTransactions() {
// TODO: implement updateTransactions
@ -51,4 +51,30 @@ class EpiccashWallet extends Bip39Wallet {
// TODO: implement updateUTXOs
throw UnimplementedError();
}
@override
Future<void> updateNode() {
// TODO: implement updateNode
throw UnimplementedError();
}
@override
Future<bool> pingCheck() async {
try {
final node = nodeService.getPrimaryNodeFor(coin: cryptoCurrency.coin);
// force unwrap optional as we want connection test to fail if wallet
// wasn't initialized or epicbox node was set to null
return await testEpicNodeConnection(
NodeFormData()
..host = node!.host
..useSSL = node.useSSL
..port = node.port,
) !=
null;
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Info);
return false;
}
}
}

View file

@ -4,10 +4,7 @@ import 'package:bip47/src/util.dart';
import 'package:decimal/decimal.dart';
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/input.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/output.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
@ -31,12 +28,13 @@ mixin ElectrumXMixin on Bip39HDWallet {
}
}
Future<List<({Transaction transaction, Address address})>> fetchTransactions({
Future<List<({Transaction transaction, Address address})>>
fetchTransactionsV1({
required List<Address> addresses,
required int currentChainHeight,
}) async {
final List<({String txHash, int height, String address})> allTxHashes =
(await _fetchHistory(addresses.map((e) => e.value).toList()))
(await fetchHistory(addresses.map((e) => e.value).toList()))
.map(
(e) => (
txHash: e["tx_hash"] as String,
@ -55,7 +53,10 @@ mixin ElectrumXMixin on Bip39HDWallet {
coin: cryptoCurrency.coin,
);
if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
// check for duplicates before adding to list
if (allTransactions
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
-1) {
tx["address"] = addresses.firstWhere((e) => e.value == data.address);
tx["height"] = data.height;
allTransactions.add(tx);
@ -65,7 +66,7 @@ mixin ElectrumXMixin on Bip39HDWallet {
final List<({Transaction transaction, Address address})> txnsData = [];
for (final txObject in allTransactions) {
final data = await parseTransaction(
final data = await _parseTransactionV1(
txObject,
addresses,
);
@ -114,18 +115,8 @@ mixin ElectrumXMixin on Bip39HDWallet {
//============================================================================
bool _duplicateTxCheck(
List<Map<String, dynamic>> allTransactions, String txid) {
for (int i = 0; i < allTransactions.length; i++) {
if (allTransactions[i]["txid"] == txid) {
return true;
}
}
return false;
}
Future<List<Map<String, dynamic>>> _fetchHistory(
List<String> allAddresses,
Future<List<Map<String, dynamic>>> fetchHistory(
Iterable<String> allAddresses,
) async {
try {
List<Map<String, dynamic>> allTxHashes = [];
@ -139,10 +130,10 @@ mixin ElectrumXMixin on Bip39HDWallet {
batches[batchNumber] = {};
}
final scriptHash = cryptoCurrency.addressToScriptHash(
address: allAddresses[i],
address: allAddresses.elementAt(i),
);
final id = Logger.isTestEnv ? "$i" : const Uuid().v1();
requestIdToAddressMap[id] = allAddresses[i];
requestIdToAddressMap[id] = allAddresses.elementAt(i);
batches[batchNumber]!.addAll({
id: [scriptHash]
});
@ -170,7 +161,60 @@ mixin ElectrumXMixin on Bip39HDWallet {
}
}
Future<({Transaction transaction, Address address})> parseTransaction(
Future<UTXO> parseUTXO({
required Map<String, dynamic> jsonUTXO,
({
String? blockedReason,
bool blocked,
})
Function(
Map<String, dynamic>,
String? scriptPubKeyHex,
)? checkBlock,
}) async {
final txn = await electrumXCached.getTransaction(
txHash: jsonUTXO["tx_hash"] as String,
verbose: true,
coin: cryptoCurrency.coin,
);
final vout = jsonUTXO["tx_pos"] as int;
final outputs = txn["vout"] as List;
String? scriptPubKey;
String? utxoOwnerAddress;
// get UTXO owner address
for (final output in outputs) {
if (output["n"] == vout) {
scriptPubKey = output["scriptPubKey"]?["hex"] as String?;
utxoOwnerAddress =
output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;
}
}
final checkBlockResult = checkBlock?.call(jsonUTXO, scriptPubKey);
final utxo = UTXO(
walletId: walletId,
txid: txn["txid"] as String,
vout: vout,
value: jsonUTXO["value"] as int,
name: "",
isBlocked: checkBlockResult?.blocked ?? false,
blockedReason: checkBlockResult?.blockedReason,
isCoinbase: txn["is_coinbase"] as bool? ?? false,
blockHash: txn["blockhash"] as String?,
blockHeight: jsonUTXO["height"] as int?,
blockTime: txn["blocktime"] as int?,
address: utxoOwnerAddress,
);
return utxo;
}
Future<({Transaction transaction, Address address})> _parseTransactionV1(
Map<String, dynamic> txData,
List<Address> myAddresses,
) async {

View file

@ -1,14 +1,27 @@
import 'dart:async';
import 'package:isar/isar.dart';
import 'package:mutex/mutex.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/isar_models/wallet_info.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart';
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
abstract class Wallet<T extends CryptoCurrency> {
@ -23,9 +36,39 @@ abstract class Wallet<T extends CryptoCurrency> {
late final MainDB mainDB;
late final SecureStorageInterface secureStorageInterface;
late final WalletInfo walletInfo;
late final Prefs prefs;
final refreshMutex = Mutex();
WalletInfo get walletInfo => _walletInfo;
bool get shouldAutoSync => _shouldAutoSync;
set shouldAutoSync(bool shouldAutoSync) {
if (_shouldAutoSync != shouldAutoSync) {
_shouldAutoSync = shouldAutoSync;
if (!shouldAutoSync) {
_periodicRefreshTimer?.cancel();
_periodicRefreshTimer = null;
_stopNetworkAlivePinging();
} else {
_startNetworkAlivePinging();
refresh();
}
}
}
// ===== private properties ===========================================
late WalletInfo _walletInfo;
late final Stream<WalletInfo?> _walletInfoStream;
Timer? _periodicRefreshTimer;
Timer? _networkAliveTimer;
bool _shouldAutoSync = false;
bool _isConnected = false;
//============================================================================
// ========== Wallet Info Convenience Getters ================================
@ -143,9 +186,10 @@ abstract class Wallet<T extends CryptoCurrency> {
final Wallet wallet = _loadWallet(
walletInfo: walletInfo,
nodeService: nodeService,
prefs: prefs,
);
wallet.prefs = prefs;
if (wallet is ElectrumXMixin) {
// initialize electrumx instance
await wallet.updateNode();
@ -154,26 +198,43 @@ abstract class Wallet<T extends CryptoCurrency> {
return wallet
..secureStorageInterface = secureStorageInterface
..mainDB = mainDB
..walletInfo = walletInfo;
.._walletInfo = walletInfo
.._watchWalletInfo();
}
static Wallet _loadWallet({
required WalletInfo walletInfo,
required NodeService nodeService,
required Prefs prefs,
}) {
switch (walletInfo.coin) {
case Coin.bitcoin:
return BitcoinWallet(
Bitcoin(CryptoCurrencyNetwork.main),
nodeService: nodeService,
prefs: prefs,
);
case Coin.bitcoinTestNet:
return BitcoinWallet(
Bitcoin(CryptoCurrencyNetwork.test),
nodeService: nodeService,
prefs: prefs,
);
case Coin.bitcoincash:
return BitcoincashWallet(
Bitcoincash(CryptoCurrencyNetwork.main),
nodeService: nodeService,
);
case Coin.bitcoincashTestnet:
return BitcoincashWallet(
Bitcoincash(CryptoCurrencyNetwork.test),
nodeService: nodeService,
);
case Coin.epicCash:
return EpiccashWallet(
Epiccash(CryptoCurrencyNetwork.main),
nodeService: nodeService,
);
default:
@ -182,6 +243,56 @@ abstract class Wallet<T extends CryptoCurrency> {
}
}
// listen to changes in db and updated wallet info property as required
void _watchWalletInfo() {
_walletInfoStream = mainDB.isar.walletInfo.watchObject(_walletInfo.id);
_walletInfoStream.forEach((element) {
if (element != null) {
_walletInfo = element;
}
});
}
void _startNetworkAlivePinging() {
// call once on start right away
_periodicPingCheck();
// then periodically check
_networkAliveTimer = Timer.periodic(
Constants.networkAliveTimerDuration,
(_) async {
_periodicPingCheck();
},
);
}
void _periodicPingCheck() async {
bool hasNetwork = await pingCheck();
if (_isConnected != hasNetwork) {
NodeConnectionStatus status = hasNetwork
? NodeConnectionStatus.connected
: NodeConnectionStatus.disconnected;
GlobalEventBus.instance.fire(
NodeConnectionStatusChangedEvent(
status,
walletId,
cryptoCurrency.coin,
),
);
_isConnected = hasNetwork;
if (hasNetwork) {
unawaited(refresh());
}
}
}
void _stopNetworkAlivePinging() {
_networkAliveTimer?.cancel();
_networkAliveTimer = null;
}
//============================================================================
// ========== Must override ==================================================
@ -198,15 +309,113 @@ abstract class Wallet<T extends CryptoCurrency> {
/// delete all locally stored blockchain data and refetch it.
Future<void> recover({required bool isRescan});
Future<bool> pingCheck();
Future<void> updateTransactions();
Future<void> updateUTXOs();
Future<void> updateBalance();
// Should probably call the above 3 functions
// Should fire events
Future<void> refresh();
//===========================================
// Should fire events
Future<void> refresh() async {
// Awaiting this lock could be dangerous.
// Since refresh is periodic (generally)
if (refreshMutex.isLocked) {
return;
}
try {
// this acquire should be almost instant due to above check.
// Slight possibility of race but should be irrelevant
await refreshMutex.acquire();
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.syncing,
walletId,
cryptoCurrency.coin,
),
);
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId));
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId));
// if (currentHeight != storedHeight) {
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
// await _checkCurrentReceivingAddressesForTransactions();
final fetchFuture = updateTransactions();
final utxosRefreshFuture = updateUTXOs();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.50, walletId));
// final feeObj = _getFees();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.60, walletId));
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.70, walletId));
// _feeObject = Future(() => feeObj);
await utxosRefreshFuture;
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.80, walletId));
await fetchFuture;
// await getAllTxsToWatch();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.90, walletId));
await updateBalance();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId));
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
cryptoCurrency.coin,
),
);
if (shouldAutoSync) {
_periodicRefreshTimer ??=
Timer.periodic(const Duration(seconds: 150), (timer) async {
// chain height check currently broken
// if ((await chainHeight) != (await storedChainHeight)) {
// TODO: [prio=med] some kind of quick check if wallet needs to refresh to replace the old refreshIfThereIsNewData call
// if (await refreshIfThereIsNewData()) {
unawaited(refresh());
// }
// }
});
}
} catch (error, strace) {
GlobalEventBus.instance.fire(
NodeConnectionStatusChangedEvent(
NodeConnectionStatus.disconnected,
walletId,
cryptoCurrency.coin,
),
);
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
cryptoCurrency.coin,
),
);
Logging.instance.log(
"Caught exception in refreshWalletData(): $error\n$strace",
level: LogLevel.Error,
);
} finally {
refreshMutex.release();
}
}
Future<void> exit() async {}
Future<void> updateNode();
}