mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-17 01:37:54 +00:00
wallet periodic refresh, more bch impl, various other clean up and fixes
This commit is contained in:
parent
59b8fe38e2
commit
11fe9f19b5
10 changed files with 829 additions and 96 deletions
|
@ -2202,7 +2202,7 @@ class BitcoinCashWallet extends CoinServiceAPI
|
||||||
type = isar_models.TransactionType.incoming;
|
type = isar_models.TransactionType.incoming;
|
||||||
} else {
|
} else {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
"Unexpected tx found: $txData",
|
"Unexpected tx found (ignoring it): $txData",
|
||||||
level: LogLevel.Error,
|
level: LogLevel.Error,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -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:coinlib_flutter/coinlib_flutter.dart' as coinlib;
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -48,4 +50,39 @@ abstract class Bip39HDCurrency extends Bip39Currency {
|
||||||
}
|
}
|
||||||
return reversedPairs.join("");
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:bech32/bech32.dart';
|
||||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
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:coinlib_flutter/coinlib_flutter.dart' as coinlib;
|
||||||
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
|
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
|
||||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||||
|
@ -161,4 +165,52 @@ class Bitcoincash extends Bip39HDCurrency {
|
||||||
|
|
||||||
return addr.startsWith("q");
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,6 +93,28 @@ class WalletInfo {
|
||||||
? {}
|
? {}
|
||||||
: Map<String, dynamic>.from(jsonDecode(otherDataJsonString!) as Map);
|
: 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({
|
WalletInfo({
|
||||||
|
|
|
@ -25,11 +25,11 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T> {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AddressType.p2sh:
|
case AddressType.p2sh:
|
||||||
derivePathType = DerivePathType.bip44;
|
derivePathType = DerivePathType.bip49;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AddressType.p2wpkh:
|
case AddressType.p2wpkh:
|
||||||
derivePathType = DerivePathType.bip44;
|
derivePathType = DerivePathType.bip84;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -96,7 +96,7 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T> {
|
||||||
} else if (chain == 1) {
|
} else if (chain == 1) {
|
||||||
subType = AddressSubType.change;
|
subType = AddressSubType.change;
|
||||||
} else {
|
} else {
|
||||||
// TODO others?
|
// TODO: [prio=low] others or throw?
|
||||||
subType = AddressSubType.unknown;
|
subType = AddressSubType.unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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/events/global/updated_in_background_event.dart';
|
||||||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||||
import 'package:stackwallet/services/node_service.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/crypto_currency/coins/bitcoin.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
|
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
|
||||||
|
@ -16,12 +15,10 @@ class BitcoinWallet extends Bip39HDWallet with ElectrumXMixin {
|
||||||
BitcoinWallet(
|
BitcoinWallet(
|
||||||
super.cryptoCurrency, {
|
super.cryptoCurrency, {
|
||||||
required NodeService nodeService,
|
required NodeService nodeService,
|
||||||
required Prefs prefs,
|
|
||||||
}) {
|
}) {
|
||||||
// TODO: [prio=low] ensure this hack isn't needed
|
// TODO: [prio=low] ensure this hack isn't needed
|
||||||
assert(cryptoCurrency is Bitcoin);
|
assert(cryptoCurrency is Bitcoin);
|
||||||
|
|
||||||
this.prefs = prefs;
|
|
||||||
this.nodeService = nodeService;
|
this.nodeService = nodeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +57,7 @@ class BitcoinWallet extends Bip39HDWallet with ElectrumXMixin {
|
||||||
Future<void> updateTransactions() async {
|
Future<void> updateTransactions() async {
|
||||||
final currentChainHeight = await fetchChainHeight();
|
final currentChainHeight = await fetchChainHeight();
|
||||||
|
|
||||||
final data = await fetchTransactions(
|
final data = await fetchTransactionsV1(
|
||||||
addresses: await _fetchAllOwnAddresses(),
|
addresses: await _fetchAllOwnAddresses(),
|
||||||
currentChainHeight: currentChainHeight,
|
currentChainHeight: currentChainHeight,
|
||||||
);
|
);
|
||||||
|
@ -95,4 +92,14 @@ class BitcoinWallet extends Bip39HDWallet with ElectrumXMixin {
|
||||||
// TODO: implement updateUTXOs
|
// TODO: implement updateUTXOs
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> pingCheck() async {
|
||||||
|
try {
|
||||||
|
final result = await electrumX.ping();
|
||||||
|
return result;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
|
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
import 'package:stackwallet/models/balance.dart';
|
||||||
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
|
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
|
||||||
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
|
||||||
import 'package:stackwallet/services/event_bus/global_event_bus.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/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/crypto_currency/coins/bitcoin.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/bip39_hd_wallet.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
|
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
|
|
||||||
class BitcoincashWallet extends Bip39HDWallet with ElectrumXMixin {
|
class BitcoincashWallet extends Bip39HDWallet with ElectrumXMixin {
|
||||||
@override
|
@override
|
||||||
|
@ -16,12 +26,10 @@ class BitcoincashWallet extends Bip39HDWallet with ElectrumXMixin {
|
||||||
BitcoincashWallet(
|
BitcoincashWallet(
|
||||||
super.cryptoCurrency, {
|
super.cryptoCurrency, {
|
||||||
required NodeService nodeService,
|
required NodeService nodeService,
|
||||||
required Prefs prefs,
|
|
||||||
}) {
|
}) {
|
||||||
// TODO: [prio=low] ensure this hack isn't needed
|
// TODO: [prio=low] ensure this hack isn't needed
|
||||||
assert(cryptoCurrency is Bitcoin);
|
assert(cryptoCurrency is Bitcoin);
|
||||||
|
|
||||||
this.prefs = prefs;
|
|
||||||
this.nodeService = nodeService;
|
this.nodeService = nodeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,12 +40,12 @@ class BitcoincashWallet extends Bip39HDWallet with ElectrumXMixin {
|
||||||
.getAddresses(walletId)
|
.getAddresses(walletId)
|
||||||
.filter()
|
.filter()
|
||||||
.not()
|
.not()
|
||||||
.group(
|
.typeEqualTo(AddressType.nonWallet)
|
||||||
(q) => q
|
.and()
|
||||||
.typeEqualTo(AddressType.nonWallet)
|
.group((q) => q
|
||||||
.or()
|
.subTypeEqualTo(AddressSubType.receiving)
|
||||||
.subTypeEqualTo(AddressSubType.nonWallet),
|
.or()
|
||||||
)
|
.subTypeEqualTo(AddressSubType.change))
|
||||||
.findAll();
|
.findAll();
|
||||||
return allAddresses;
|
return allAddresses;
|
||||||
}
|
}
|
||||||
|
@ -45,54 +53,382 @@ class BitcoincashWallet extends Bip39HDWallet with ElectrumXMixin {
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> refresh() {
|
Future<void> updateBalance() async {
|
||||||
// TODO: implement refresh
|
final utxos = await mainDB.getUTXOs(walletId).findAll();
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
final currentChainHeight = await fetchChainHeight();
|
||||||
Future<void> updateBalance() {
|
|
||||||
// TODO: implement updateBalance
|
Amount satoshiBalanceTotal = Amount(
|
||||||
throw UnimplementedError();
|
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
|
@override
|
||||||
Future<void> updateTransactions() async {
|
Future<void> updateTransactions() async {
|
||||||
final currentChainHeight = await fetchChainHeight();
|
List<Address> allAddressesOld = await _fetchAllOwnAddresses();
|
||||||
|
|
||||||
final data = await fetchTransactions(
|
Set<String> receivingAddresses = allAddressesOld
|
||||||
addresses: await _fetchAllOwnAddresses(),
|
.where((e) => e.subType == AddressSubType.receiving)
|
||||||
currentChainHeight: currentChainHeight,
|
.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(
|
Set<String> changeAddresses = allAddressesOld
|
||||||
data
|
.where((e) => e.subType == AddressSubType.change)
|
||||||
.map(
|
.map((e) {
|
||||||
(e) => Tuple2(
|
if (bitbox.Address.detectFormat(e.value) == bitbox.Address.formatLegacy &&
|
||||||
e.transaction,
|
(cryptoCurrency.addressType(address: e.value) ==
|
||||||
e.address,
|
DerivePathType.bip44 ||
|
||||||
),
|
cryptoCurrency.addressType(address: e.value) ==
|
||||||
)
|
DerivePathType.bch44)) {
|
||||||
.toList(),
|
return bitbox.Address.toCashAddress(e.value);
|
||||||
walletId,
|
} else {
|
||||||
);
|
return e.value;
|
||||||
|
}
|
||||||
|
}).toSet();
|
||||||
|
|
||||||
// TODO: [prio=med] get rid of this and watch isar instead
|
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
|
||||||
// quick hack to notify manager to call notifyListeners if
|
|
||||||
// transactions changed
|
final List<Map<String, dynamic>> allTxHashes =
|
||||||
if (data.isNotEmpty) {
|
await fetchHistory(allAddressesSet);
|
||||||
GlobalEventBus.instance.fire(
|
|
||||||
UpdatedInBackgroundEvent(
|
List<Map<String, dynamic>> allTransactions = [];
|
||||||
"Transactions updated/added for: $walletId ${walletInfo.name}",
|
|
||||||
walletId,
|
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
|
@override
|
||||||
Future<void> updateUTXOs() {
|
Future<bool> pingCheck() async {
|
||||||
// TODO: implement updateUTXOs
|
try {
|
||||||
throw UnimplementedError();
|
final result = await electrumX.ping();
|
||||||
|
return result;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/models/tx_data.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/bip39_wallet.dart';
|
import 'package:stackwallet/wallets/wallet/bip39_wallet.dart';
|
||||||
|
|
||||||
class EpiccashWallet extends Bip39Wallet {
|
class EpiccashWallet extends Bip39Wallet {
|
||||||
EpiccashWallet(super.cryptoCurrency);
|
final NodeService nodeService;
|
||||||
|
|
||||||
|
EpiccashWallet(super.cryptoCurrency, {required this.nodeService});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TxData> confirmSend({required TxData txData}) {
|
Future<TxData> confirmSend({required TxData txData}) {
|
||||||
|
@ -34,12 +40,6 @@ class EpiccashWallet extends Bip39Wallet {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> updateNode() {
|
|
||||||
// TODO: implement updateNode
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateTransactions() {
|
Future<void> updateTransactions() {
|
||||||
// TODO: implement updateTransactions
|
// TODO: implement updateTransactions
|
||||||
|
@ -51,4 +51,30 @@ class EpiccashWallet extends Bip39Wallet {
|
||||||
// TODO: implement updateUTXOs
|
// TODO: implement updateUTXOs
|
||||||
throw UnimplementedError();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,7 @@ import 'package:bip47/src/util.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import 'package:decimal/decimal.dart';
|
||||||
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
|
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
|
||||||
import 'package:stackwallet/electrumx_rpc/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/isar_models.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/services/mixins/paynym_wallet_interface.dart';
|
import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart';
|
||||||
import 'package:stackwallet/services/node_service.dart';
|
import 'package:stackwallet/services/node_service.dart';
|
||||||
import 'package:stackwallet/utilities/amount/amount.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 List<Address> addresses,
|
||||||
required int currentChainHeight,
|
required int currentChainHeight,
|
||||||
}) async {
|
}) async {
|
||||||
final List<({String txHash, int height, String address})> allTxHashes =
|
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(
|
.map(
|
||||||
(e) => (
|
(e) => (
|
||||||
txHash: e["tx_hash"] as String,
|
txHash: e["tx_hash"] as String,
|
||||||
|
@ -55,7 +53,10 @@ mixin ElectrumXMixin on Bip39HDWallet {
|
||||||
coin: cryptoCurrency.coin,
|
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["address"] = addresses.firstWhere((e) => e.value == data.address);
|
||||||
tx["height"] = data.height;
|
tx["height"] = data.height;
|
||||||
allTransactions.add(tx);
|
allTransactions.add(tx);
|
||||||
|
@ -65,7 +66,7 @@ mixin ElectrumXMixin on Bip39HDWallet {
|
||||||
final List<({Transaction transaction, Address address})> txnsData = [];
|
final List<({Transaction transaction, Address address})> txnsData = [];
|
||||||
|
|
||||||
for (final txObject in allTransactions) {
|
for (final txObject in allTransactions) {
|
||||||
final data = await parseTransaction(
|
final data = await _parseTransactionV1(
|
||||||
txObject,
|
txObject,
|
||||||
addresses,
|
addresses,
|
||||||
);
|
);
|
||||||
|
@ -114,18 +115,8 @@ mixin ElectrumXMixin on Bip39HDWallet {
|
||||||
|
|
||||||
//============================================================================
|
//============================================================================
|
||||||
|
|
||||||
bool _duplicateTxCheck(
|
Future<List<Map<String, dynamic>>> fetchHistory(
|
||||||
List<Map<String, dynamic>> allTransactions, String txid) {
|
Iterable<String> allAddresses,
|
||||||
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,
|
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
List<Map<String, dynamic>> allTxHashes = [];
|
List<Map<String, dynamic>> allTxHashes = [];
|
||||||
|
@ -139,10 +130,10 @@ mixin ElectrumXMixin on Bip39HDWallet {
|
||||||
batches[batchNumber] = {};
|
batches[batchNumber] = {};
|
||||||
}
|
}
|
||||||
final scriptHash = cryptoCurrency.addressToScriptHash(
|
final scriptHash = cryptoCurrency.addressToScriptHash(
|
||||||
address: allAddresses[i],
|
address: allAddresses.elementAt(i),
|
||||||
);
|
);
|
||||||
final id = Logger.isTestEnv ? "$i" : const Uuid().v1();
|
final id = Logger.isTestEnv ? "$i" : const Uuid().v1();
|
||||||
requestIdToAddressMap[id] = allAddresses[i];
|
requestIdToAddressMap[id] = allAddresses.elementAt(i);
|
||||||
batches[batchNumber]!.addAll({
|
batches[batchNumber]!.addAll({
|
||||||
id: [scriptHash]
|
id: [scriptHash]
|
||||||
});
|
});
|
||||||
|
@ -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,
|
Map<String, dynamic> txData,
|
||||||
List<Address> myAddresses,
|
List<Address> myAddresses,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:stackwallet/db/isar/main_db.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/services/node_service.dart';
|
||||||
|
import 'package:stackwallet/utilities/constants.dart';
|
||||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||||
import 'package:stackwallet/utilities/flutter_secure_storage_interface.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/utilities/prefs.dart';
|
||||||
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.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/crypto_currency/crypto_currency.dart';
|
||||||
import 'package:stackwallet/wallets/isar_models/wallet_info.dart';
|
import 'package:stackwallet/wallets/isar_models/wallet_info.dart';
|
||||||
import 'package:stackwallet/wallets/models/tx_data.dart';
|
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.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';
|
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
|
||||||
|
|
||||||
abstract class Wallet<T extends CryptoCurrency> {
|
abstract class Wallet<T extends CryptoCurrency> {
|
||||||
|
@ -23,9 +36,39 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
|
|
||||||
late final MainDB mainDB;
|
late final MainDB mainDB;
|
||||||
late final SecureStorageInterface secureStorageInterface;
|
late final SecureStorageInterface secureStorageInterface;
|
||||||
late final WalletInfo walletInfo;
|
|
||||||
late final Prefs prefs;
|
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 ================================
|
// ========== Wallet Info Convenience Getters ================================
|
||||||
|
|
||||||
|
@ -143,9 +186,10 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
final Wallet wallet = _loadWallet(
|
final Wallet wallet = _loadWallet(
|
||||||
walletInfo: walletInfo,
|
walletInfo: walletInfo,
|
||||||
nodeService: nodeService,
|
nodeService: nodeService,
|
||||||
prefs: prefs,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
wallet.prefs = prefs;
|
||||||
|
|
||||||
if (wallet is ElectrumXMixin) {
|
if (wallet is ElectrumXMixin) {
|
||||||
// initialize electrumx instance
|
// initialize electrumx instance
|
||||||
await wallet.updateNode();
|
await wallet.updateNode();
|
||||||
|
@ -154,26 +198,43 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
return wallet
|
return wallet
|
||||||
..secureStorageInterface = secureStorageInterface
|
..secureStorageInterface = secureStorageInterface
|
||||||
..mainDB = mainDB
|
..mainDB = mainDB
|
||||||
..walletInfo = walletInfo;
|
.._walletInfo = walletInfo
|
||||||
|
.._watchWalletInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Wallet _loadWallet({
|
static Wallet _loadWallet({
|
||||||
required WalletInfo walletInfo,
|
required WalletInfo walletInfo,
|
||||||
required NodeService nodeService,
|
required NodeService nodeService,
|
||||||
required Prefs prefs,
|
|
||||||
}) {
|
}) {
|
||||||
switch (walletInfo.coin) {
|
switch (walletInfo.coin) {
|
||||||
case Coin.bitcoin:
|
case Coin.bitcoin:
|
||||||
return BitcoinWallet(
|
return BitcoinWallet(
|
||||||
Bitcoin(CryptoCurrencyNetwork.main),
|
Bitcoin(CryptoCurrencyNetwork.main),
|
||||||
nodeService: nodeService,
|
nodeService: nodeService,
|
||||||
prefs: prefs,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case Coin.bitcoinTestNet:
|
case Coin.bitcoinTestNet:
|
||||||
return BitcoinWallet(
|
return BitcoinWallet(
|
||||||
Bitcoin(CryptoCurrencyNetwork.test),
|
Bitcoin(CryptoCurrencyNetwork.test),
|
||||||
nodeService: nodeService,
|
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:
|
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 ==================================================
|
// ========== Must override ==================================================
|
||||||
|
|
||||||
|
@ -198,15 +309,113 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
/// delete all locally stored blockchain data and refetch it.
|
/// delete all locally stored blockchain data and refetch it.
|
||||||
Future<void> recover({required bool isRescan});
|
Future<void> recover({required bool isRescan});
|
||||||
|
|
||||||
|
Future<bool> pingCheck();
|
||||||
|
|
||||||
Future<void> updateTransactions();
|
Future<void> updateTransactions();
|
||||||
Future<void> updateUTXOs();
|
Future<void> updateUTXOs();
|
||||||
Future<void> updateBalance();
|
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();
|
Future<void> updateNode();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue