2023-10-30 22:58:15 +00:00
|
|
|
import 'dart:async';
|
|
|
|
|
2023-09-14 23:34:01 +00:00
|
|
|
import 'package:isar/isar.dart';
|
2023-10-30 22:58:15 +00:00
|
|
|
import 'package:mutex/mutex.dart';
|
2023-09-14 23:34:01 +00:00
|
|
|
import 'package:stackwallet/db/isar/main_db.dart';
|
2023-10-30 22:58:15 +00:00
|
|
|
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';
|
2023-09-18 21:28:31 +00:00
|
|
|
import 'package:stackwallet/services/node_service.dart';
|
2023-10-30 22:58:15 +00:00
|
|
|
import 'package:stackwallet/utilities/constants.dart';
|
2023-09-14 23:34:01 +00:00
|
|
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
|
|
|
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
2023-10-30 22:58:15 +00:00
|
|
|
import 'package:stackwallet/utilities/logger.dart';
|
2023-09-18 21:28:31 +00:00
|
|
|
import 'package:stackwallet/utilities/prefs.dart';
|
|
|
|
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
|
2023-10-30 22:58:15 +00:00
|
|
|
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart';
|
|
|
|
import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart';
|
2023-09-18 21:28:31 +00:00
|
|
|
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
2023-09-14 23:34:01 +00:00
|
|
|
import 'package:stackwallet/wallets/isar_models/wallet_info.dart';
|
|
|
|
import 'package:stackwallet/wallets/models/tx_data.dart';
|
2023-09-18 21:28:31 +00:00
|
|
|
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
|
2023-10-30 22:58:15 +00:00
|
|
|
import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart';
|
|
|
|
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
|
2023-09-18 21:28:31 +00:00
|
|
|
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
|
2023-09-14 23:34:01 +00:00
|
|
|
|
|
|
|
abstract class Wallet<T extends CryptoCurrency> {
|
2023-10-30 17:41:03 +00:00
|
|
|
int get isarTransactionVersion => 1;
|
|
|
|
|
2023-09-14 23:34:01 +00:00
|
|
|
Wallet(this.cryptoCurrency);
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
// ========== Properties =====================================================
|
|
|
|
|
|
|
|
final T cryptoCurrency;
|
|
|
|
|
|
|
|
late final MainDB mainDB;
|
|
|
|
late final SecureStorageInterface secureStorageInterface;
|
2023-09-18 21:28:31 +00:00
|
|
|
late final Prefs prefs;
|
2023-09-14 23:34:01 +00:00
|
|
|
|
2023-10-30 22:58:15 +00:00
|
|
|
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;
|
|
|
|
|
2023-09-14 23:34:01 +00:00
|
|
|
//============================================================================
|
|
|
|
// ========== Wallet Info Convenience Getters ================================
|
|
|
|
|
|
|
|
String get walletId => walletInfo.walletId;
|
|
|
|
WalletType get walletType => walletInfo.walletType;
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
// ========== Static Main ====================================================
|
|
|
|
|
|
|
|
/// Create a new wallet and save [walletInfo] to db.
|
|
|
|
static Future<Wallet> create({
|
|
|
|
required WalletInfo walletInfo,
|
|
|
|
required MainDB mainDB,
|
|
|
|
required SecureStorageInterface secureStorageInterface,
|
2023-09-18 21:28:31 +00:00
|
|
|
required NodeService nodeService,
|
|
|
|
required Prefs prefs,
|
2023-09-14 23:34:01 +00:00
|
|
|
String? mnemonic,
|
|
|
|
String? mnemonicPassphrase,
|
|
|
|
String? privateKey,
|
|
|
|
}) async {
|
|
|
|
final Wallet wallet = await _construct(
|
|
|
|
walletInfo: walletInfo,
|
|
|
|
mainDB: mainDB,
|
|
|
|
secureStorageInterface: secureStorageInterface,
|
2023-09-18 21:28:31 +00:00
|
|
|
nodeService: nodeService,
|
|
|
|
prefs: prefs,
|
2023-09-14 23:34:01 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
switch (walletInfo.walletType) {
|
|
|
|
case WalletType.bip39:
|
2023-09-18 21:28:31 +00:00
|
|
|
case WalletType.bip39HD:
|
2023-09-14 23:34:01 +00:00
|
|
|
await secureStorageInterface.write(
|
|
|
|
key: mnemonicKey(walletId: walletInfo.walletId),
|
|
|
|
value: mnemonic,
|
|
|
|
);
|
|
|
|
await secureStorageInterface.write(
|
|
|
|
key: mnemonicPassphraseKey(walletId: walletInfo.walletId),
|
|
|
|
value: mnemonicPassphrase,
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WalletType.cryptonote:
|
|
|
|
break;
|
|
|
|
|
|
|
|
case WalletType.privateKeyBased:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store in db after wallet creation
|
|
|
|
await wallet.mainDB.isar.walletInfo.put(wallet.walletInfo);
|
|
|
|
|
|
|
|
return wallet;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Load an existing wallet via [WalletInfo] using [walletId].
|
|
|
|
static Future<Wallet> load({
|
|
|
|
required String walletId,
|
|
|
|
required MainDB mainDB,
|
|
|
|
required SecureStorageInterface secureStorageInterface,
|
2023-09-18 21:28:31 +00:00
|
|
|
required NodeService nodeService,
|
|
|
|
required Prefs prefs,
|
2023-09-14 23:34:01 +00:00
|
|
|
}) async {
|
|
|
|
final walletInfo = await mainDB.isar.walletInfo
|
|
|
|
.where()
|
|
|
|
.walletIdEqualTo(walletId)
|
|
|
|
.findFirst();
|
|
|
|
|
|
|
|
if (walletInfo == null) {
|
|
|
|
throw Exception(
|
|
|
|
"WalletInfo not found for $walletId when trying to call Wallet.load()",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return await _construct(
|
|
|
|
walletInfo: walletInfo!,
|
|
|
|
mainDB: mainDB,
|
|
|
|
secureStorageInterface: secureStorageInterface,
|
2023-09-18 21:28:31 +00:00
|
|
|
nodeService: nodeService,
|
|
|
|
prefs: prefs,
|
2023-09-14 23:34:01 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
// ========== Static Util ====================================================
|
|
|
|
|
2023-09-18 21:28:31 +00:00
|
|
|
// secure storage key
|
2023-09-14 23:34:01 +00:00
|
|
|
static String mnemonicKey({
|
|
|
|
required String walletId,
|
|
|
|
}) =>
|
|
|
|
"${walletId}_mnemonic";
|
|
|
|
|
2023-09-18 21:28:31 +00:00
|
|
|
// secure storage key
|
2023-09-14 23:34:01 +00:00
|
|
|
static String mnemonicPassphraseKey({
|
|
|
|
required String walletId,
|
|
|
|
}) =>
|
|
|
|
"${walletId}_mnemonicPassphrase";
|
|
|
|
|
2023-09-18 21:28:31 +00:00
|
|
|
// secure storage key
|
2023-09-14 23:34:01 +00:00
|
|
|
static String privateKeyKey({
|
|
|
|
required String walletId,
|
|
|
|
}) =>
|
|
|
|
"${walletId}_privateKey";
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
// ========== Private ========================================================
|
|
|
|
|
|
|
|
/// Construct wallet instance by [WalletType] from [walletInfo]
|
|
|
|
static Future<Wallet> _construct({
|
|
|
|
required WalletInfo walletInfo,
|
|
|
|
required MainDB mainDB,
|
|
|
|
required SecureStorageInterface secureStorageInterface,
|
2023-09-18 21:28:31 +00:00
|
|
|
required NodeService nodeService,
|
|
|
|
required Prefs prefs,
|
2023-09-14 23:34:01 +00:00
|
|
|
}) async {
|
2023-09-18 21:28:31 +00:00
|
|
|
final Wallet wallet = _loadWallet(
|
|
|
|
walletInfo: walletInfo,
|
|
|
|
nodeService: nodeService,
|
|
|
|
);
|
2023-09-14 23:34:01 +00:00
|
|
|
|
2023-10-30 22:58:15 +00:00
|
|
|
wallet.prefs = prefs;
|
|
|
|
|
2023-09-18 21:28:31 +00:00
|
|
|
if (wallet is ElectrumXMixin) {
|
|
|
|
// initialize electrumx instance
|
|
|
|
await wallet.updateNode();
|
2023-09-14 23:34:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return wallet
|
|
|
|
..secureStorageInterface = secureStorageInterface
|
|
|
|
..mainDB = mainDB
|
2023-10-30 22:58:15 +00:00
|
|
|
.._walletInfo = walletInfo
|
|
|
|
.._watchWalletInfo();
|
2023-09-14 23:34:01 +00:00
|
|
|
}
|
|
|
|
|
2023-09-18 21:28:31 +00:00
|
|
|
static Wallet _loadWallet({
|
2023-09-14 23:34:01 +00:00
|
|
|
required WalletInfo walletInfo,
|
2023-09-18 21:28:31 +00:00
|
|
|
required NodeService nodeService,
|
2023-09-14 23:34:01 +00:00
|
|
|
}) {
|
|
|
|
switch (walletInfo.coin) {
|
|
|
|
case Coin.bitcoin:
|
2023-09-18 21:28:31 +00:00
|
|
|
return BitcoinWallet(
|
|
|
|
Bitcoin(CryptoCurrencyNetwork.main),
|
|
|
|
nodeService: nodeService,
|
|
|
|
);
|
2023-10-30 22:58:15 +00:00
|
|
|
|
2023-09-14 23:34:01 +00:00
|
|
|
case Coin.bitcoinTestNet:
|
2023-09-18 21:28:31 +00:00
|
|
|
return BitcoinWallet(
|
|
|
|
Bitcoin(CryptoCurrencyNetwork.test),
|
|
|
|
nodeService: nodeService,
|
2023-10-30 22:58:15 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
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,
|
2023-09-18 21:28:31 +00:00
|
|
|
);
|
2023-09-14 23:34:01 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
// should never hit in reality
|
2023-09-18 21:28:31 +00:00
|
|
|
throw Exception("Unknown crypto currency");
|
2023-09-14 23:34:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-30 22:58:15 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2023-09-14 23:34:01 +00:00
|
|
|
//============================================================================
|
|
|
|
// ========== Must override ==================================================
|
|
|
|
|
|
|
|
/// Create and sign a transaction in preparation to submit to network
|
|
|
|
Future<TxData> prepareSend({required TxData txData});
|
|
|
|
|
|
|
|
/// Broadcast transaction to network. On success update local wallet state to
|
|
|
|
/// reflect updated balance, transactions, utxos, etc.
|
|
|
|
Future<TxData> confirmSend({required TxData txData});
|
2023-09-18 21:28:31 +00:00
|
|
|
|
|
|
|
/// Recover a wallet by scanning the blockchain. If called on a new wallet a
|
|
|
|
/// normal recovery should occur. When called on an existing wallet and
|
|
|
|
/// [isRescan] is false then it should throw. Otherwise this function should
|
|
|
|
/// delete all locally stored blockchain data and refetch it.
|
|
|
|
Future<void> recover({required bool isRescan});
|
|
|
|
|
2023-10-30 22:58:15 +00:00
|
|
|
Future<bool> pingCheck();
|
|
|
|
|
2023-09-18 21:28:31 +00:00
|
|
|
Future<void> updateTransactions();
|
|
|
|
Future<void> updateUTXOs();
|
|
|
|
Future<void> updateBalance();
|
|
|
|
|
2023-10-30 22:58:15 +00:00
|
|
|
//===========================================
|
|
|
|
|
2023-09-18 21:28:31 +00:00
|
|
|
// Should fire events
|
2023-10-30 22:58:15 +00:00
|
|
|
Future<void> refresh() async {
|
|
|
|
// Awaiting this lock could be dangerous.
|
|
|
|
// Since refresh is periodic (generally)
|
|
|
|
if (refreshMutex.isLocked) {
|
|
|
|
return;
|
|
|
|
}
|
2023-09-18 21:28:31 +00:00
|
|
|
|
2023-10-30 22:58:15 +00:00
|
|
|
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 {}
|
2023-09-18 21:28:31 +00:00
|
|
|
|
|
|
|
Future<void> updateNode();
|
2023-09-14 23:34:01 +00:00
|
|
|
}
|