From af25da5a5964271e25873d3bfd511b3ce42058fa Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 4 Jan 2024 18:37:46 -0600 Subject: [PATCH] untested: ltc refactor --- lib/pages/ordinals/ordinals_view.dart | 2 +- .../ordinals/desktop_ordinals_view.dart | 3 +- lib/services/coins/coin_service.dart | 21 +- .../coins/litecoin/litecoin_wallet.dart | 7062 ++++++++--------- lib/services/mixins/ordinals_interface.dart | 138 +- lib/wallets/wallet/impl/bitcoin_wallet.dart | 8 +- .../wallet/impl/bitcoincash_wallet.dart | 8 +- lib/wallets/wallet/impl/dogecoin_wallet.dart | 8 +- lib/wallets/wallet/impl/ecash_wallet.dart | 8 +- lib/wallets/wallet/impl/epiccash_wallet.dart | 4 +- lib/wallets/wallet/impl/firo_wallet.dart | 8 +- lib/wallets/wallet/impl/litecoin_wallet.dart | 143 + lib/wallets/wallet/impl/tezos_wallet.dart | 3 +- .../intermediate/cryptonote_wallet.dart | 5 +- lib/wallets/wallet/wallet.dart | 10 +- .../electrumx_interface.dart | 23 +- .../nano_interface.dart | 3 +- .../ordinals_interface.dart | 109 + pubspec.lock | 2 +- pubspec.yaml | 5 + 20 files changed, 3899 insertions(+), 3674 deletions(-) create mode 100644 lib/wallets/wallet/impl/litecoin_wallet.dart create mode 100644 lib/wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 2b6099c50..c02d450bf 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -13,11 +13,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index effeb143d..1fb4f29af 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -13,11 +13,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; @@ -213,7 +213,6 @@ class _DesktopOrdinals extends ConsumerState { isDesktop: true, whileFuture: Future.wait([ Future.delayed(const Duration(seconds: 2)), - // TODO: [prio=high] FIX CAST as ISSUE (ref.read(pWallets).getWallet(widget.walletId) as OrdinalsInterface) .refreshInscriptions() diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 175110369..85f1763dd 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -16,7 +16,6 @@ import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; -import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; @@ -85,26 +84,10 @@ abstract class CoinServiceAPI { throw UnimplementedError("moved"); case Coin.litecoin: - return LitecoinWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - secureStore: secureStorageInterface, - client: client, - cachedClient: cachedClient, - tracker: tracker, - ); + throw UnimplementedError("moved"); case Coin.litecoinTestNet: - return LitecoinWallet( - walletId: walletId, - walletName: walletName, - coin: coin, - secureStore: secureStorageInterface, - client: client, - cachedClient: cachedClient, - tracker: tracker, - ); + throw UnimplementedError("moved"); case Coin.bitcoinTestNet: throw UnimplementedError("moved"); diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 0d025332d..cd66c9d72 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -1,3531 +1,3531 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:bech32/bech32.dart'; -import 'package:bip32/bip32.dart' as bip32; -import 'package:bip39/bip39.dart' as bip39; -import 'package:bitcoindart/bitcoindart.dart'; -import 'package:bs58check/bs58check.dart' as bs58check; -import 'package:crypto/crypto.dart'; -import 'package:decimal/decimal.dart'; -import 'package:flutter/foundation.dart'; -import 'package:isar/isar.dart'; -import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; -import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; -import 'package:stackwallet/models/balance.dart'; -import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; -import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/models/signing_data.dart'; -import 'package:stackwallet/services/coins/coin_service.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/updated_in_background_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/mixins/coin_control_interface.dart'; -import 'package:stackwallet/services/mixins/electrum_x_parsing.dart'; -import 'package:stackwallet/services/mixins/ordinals_interface.dart'; -import 'package:stackwallet/services/mixins/wallet_cache.dart'; -import 'package:stackwallet/services/mixins/wallet_db.dart'; -import 'package:stackwallet/services/mixins/xpubable.dart'; -import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/amount/amount.dart'; -import 'package:stackwallet/utilities/bip32_utils.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; -import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/format.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; -import 'package:stackwallet/widgets/crypto_notifications.dart'; -import 'package:tuple/tuple.dart'; -import 'package:uuid/uuid.dart'; - -const int MINIMUM_CONFIRMATIONS = 1; -final Amount DUST_LIMIT = Amount( - rawValue: BigInt.from(294), - fractionDigits: Coin.particl.decimals, -); -final Amount DUST_LIMIT_P2PKH = Amount( - rawValue: BigInt.from(546), - fractionDigits: Coin.particl.decimals, -); - -const String GENESIS_HASH_MAINNET = - "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2"; -const String GENESIS_HASH_TESTNET = - "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0"; - -String constructDerivePath({ - required DerivePathType derivePathType, - required int networkWIF, - int account = 0, - required int chain, - required int index, -}) { - String coinType; - switch (networkWIF) { - case 0xb0: // ltc mainnet wif - coinType = "2"; // ltc mainnet - break; - case 0xef: // ltc testnet wif - coinType = "1"; // ltc testnet - break; - default: - throw Exception("Invalid Litecoin network wif used!"); - } - - int purpose; - switch (derivePathType) { - case DerivePathType.bip44: - purpose = 44; - break; - case DerivePathType.bip49: - purpose = 49; - break; - case DerivePathType.bip84: - purpose = 84; - break; - default: - throw Exception("DerivePathType $derivePathType not supported"); - } - - return "m/$purpose'/$coinType'/$account'/$chain/$index"; -} - -class LitecoinWallet extends CoinServiceAPI - with - WalletCache, - WalletDB, - ElectrumXParsing, - CoinControlInterface, - OrdinalsInterface - implements XPubAble { - LitecoinWallet({ - required String walletId, - required String walletName, - required Coin coin, - required ElectrumXClient client, - required CachedElectrumXClient cachedClient, - required TransactionNotificationTracker tracker, - required SecureStorageInterface secureStore, - MainDB? mockableOverride, - }) { - txTracker = tracker; - _walletId = walletId; - _walletName = walletName; - _coin = coin; - _electrumXClient = client; - _cachedElectrumXClient = cachedClient; - _secureStore = secureStore; - initCache(walletId, coin); - initWalletDB(mockableOverride: mockableOverride); - initOrdinalsInterface(walletId: walletId, coin: coin, db: db); - initCoinControlInterface( - walletId: walletId, - walletName: walletName, - coin: coin, - db: db, - getChainHeight: () => chainHeight, - refreshedBalanceCallback: (balance) async { - _balance = balance; - await updateCachedBalance(_balance!); - }, - ); - } - - static const integrationTestFlag = - bool.fromEnvironment("IS_INTEGRATION_TEST"); - - final _prefs = Prefs.instance; - - Timer? timer; - late final Coin _coin; - - late final TransactionNotificationTracker txTracker; - - NetworkType get _network { - switch (coin) { - case Coin.litecoin: - return litecoin; - case Coin.litecoinTestNet: - return litecointestnet; - default: - throw Exception("Invalid network type!"); - } - } - - @override - set isFavorite(bool markFavorite) { - _isFavorite = markFavorite; - updateCachedIsFavorite(markFavorite); - } - - @override - bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); - - bool? _isFavorite; - - @override - Coin get coin => _coin; - - @override - Future> get utxos => db.getUTXOs(walletId).findAll(); - - @override - Future> get transactions => - db.getTransactions(walletId).sortByTimestampDesc().findAll(); - - @override - Future get currentReceivingAddress async => - (await _currentReceivingAddress).value; - - Future get _currentReceivingAddress async => - (await db - .getAddresses(walletId) - .filter() - .typeEqualTo(isar_models.AddressType.p2wpkh) - .subTypeEqualTo(isar_models.AddressSubType.receiving) - .sortByDerivationIndexDesc() - .findFirst()) ?? - await _generateAddressForChain(0, 0, DerivePathTypeExt.primaryFor(coin)); - - Future get currentChangeAddress async => - (await _currentChangeAddress).value; - - Future get _currentChangeAddress async => - (await db - .getAddresses(walletId) - .filter() - .typeEqualTo(isar_models.AddressType.p2wpkh) - .subTypeEqualTo(isar_models.AddressSubType.change) - .sortByDerivationIndexDesc() - .findFirst()) ?? - await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin)); - - @override - Future exit() async { - _hasCalledExit = true; - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } - - bool _hasCalledExit = false; - - @override - bool get hasCalledExit => _hasCalledExit; - - @override - Future get fees => _feeObject ??= _getFees(); - Future? _feeObject; - - @override - Future get maxFee async { - final fee = (await fees).fast as String; - final satsFee = Decimal.parse(fee) * - Decimal.fromInt(Constants.satsPerCoin(coin).toInt()); - return satsFee.floor().toBigInt().toInt(); - } - - @override - Future> get mnemonic => _getMnemonicList(); - - @override - Future get mnemonicString => - _secureStore.read(key: '${_walletId}_mnemonic'); - - @override - Future get mnemonicPassphrase => _secureStore.read( - key: '${_walletId}_mnemonicPassphrase', - ); - - Future get chainHeight async { - try { - final result = await _electrumXClient.getBlockHeadTip(); - final height = result["height"] as int; - await updateCachedChainHeight(height); - if (height > storedChainHeight) { - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "Updated current chain height in $walletId $walletName!", - walletId, - ), - ); - } - return height; - } catch (e, s) { - Logging.instance.log("Exception caught in chainHeight: $e\n$s", - level: LogLevel.Error); - return storedChainHeight; - } - } - - @override - int get storedChainHeight => getCachedChainHeight(); - - 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] == _network.pubKeyHash) { - // P2PKH - return DerivePathType.bip44; - } - if (decodeBase58[0] == _network.scriptHash) { - // P2SH - return DerivePathType.bip49; - } - throw ArgumentError('Invalid version or Network mismatch'); - } else { - try { - decodeBech32 = segwit.decode(address, _network.bech32!); - } catch (err) { - // Bech32 decode fail - } - if (_network.bech32 != decodeBech32!.hrp) { - throw ArgumentError('Invalid prefix or Network mismatch'); - } - if (decodeBech32.version != 0) { - throw ArgumentError('Invalid address version'); - } - // P2WPKH - return DerivePathType.bip84; - } - } - - bool longMutex = false; - - @override - Future recoverFromMnemonic({ - required String mnemonic, - String? mnemonicPassphrase, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height, - }) async { - longMutex = true; - final start = DateTime.now(); - try { - Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag", - level: LogLevel.Info); - if (!integrationTestFlag) { - final features = await electrumXClient.getServerFeatures(); - Logging.instance.log("features: $features", level: LogLevel.Info); - switch (coin) { - case Coin.litecoin: - if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { - throw Exception("genesis hash does not match main net!"); - } - break; - case Coin.litecoinTestNet: - if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - throw Exception("genesis hash does not match test net!"); - } - break; - default: - throw Exception( - "Attempted to generate a LitecoinWallet using a non litecoin coin type: ${coin.name}"); - } - // if (_networkType == BasicNetworkType.main) { - // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { - // throw Exception("genesis hash does not match main net!"); - // } - // } else if (_networkType == BasicNetworkType.test) { - // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - // throw Exception("genesis hash does not match test net!"); - // } - // } - } - // check to make sure we aren't overwriting a mnemonic - // this should never fail - if ((await mnemonicString) != null || - (await this.mnemonicPassphrase) != null) { - longMutex = false; - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - await _secureStore.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: mnemonicPassphrase ?? "", - ); - await _recoverWalletFromBIP32SeedPhrase( - mnemonic: mnemonic.trim(), - mnemonicPassphrase: mnemonicPassphrase ?? "", - maxUnusedAddressGap: maxUnusedAddressGap, - maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, - ); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from recoverFromMnemonic(): $e\n$s", - level: LogLevel.Error); - longMutex = false; - rethrow; - } - longMutex = false; - - final end = DateTime.now(); - Logging.instance.log( - "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", - level: LogLevel.Info); - } - - Future> _checkGaps( - int maxNumberOfIndexesToCheck, - int maxUnusedAddressGap, - int txCountBatchSize, - bip32.BIP32 root, - DerivePathType type, - int chain) async { - List addressArray = []; - int returningIndex = -1; - Map> derivations = {}; - int gapCounter = 0; - for (int index = 0; - index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap; - index += txCountBatchSize) { - List iterationsAddressArray = []; - Logging.instance.log( - "index: $index, \t GapCounter $chain ${type.name}: $gapCounter", - level: LogLevel.Info); - - final _id = "k_$index"; - Map txCountCallArgs = {}; - final Map receivingNodes = {}; - - for (int j = 0; j < txCountBatchSize; j++) { - final derivePath = constructDerivePath( - derivePathType: type, - networkWIF: root.network.wif, - chain: chain, - index: index + j, - ); - final node = await Bip32Utils.getBip32NodeFromRoot(root, derivePath); - - String addressString; - final data = PaymentData(pubkey: node.publicKey); - isar_models.AddressType addrType; - switch (type) { - case DerivePathType.bip44: - addressString = P2PKH(data: data, network: _network).data.address!; - addrType = isar_models.AddressType.p2pkh; - break; - case DerivePathType.bip49: - addressString = P2SH( - data: PaymentData( - redeem: P2WPKH( - data: data, - network: _network, - overridePrefix: _network.bech32!) - .data), - network: _network) - .data - .address!; - addrType = isar_models.AddressType.p2sh; - break; - case DerivePathType.bip84: - addressString = P2WPKH( - network: _network, - data: data, - overridePrefix: _network.bech32!) - .data - .address!; - addrType = isar_models.AddressType.p2wpkh; - break; - default: - throw Exception("DerivePathType unsupported"); - } - - final address = isar_models.Address( - walletId: walletId, - value: addressString, - publicKey: node.publicKey, - type: addrType, - derivationIndex: index + j, - derivationPath: isar_models.DerivationPath()..value = derivePath, - subType: chain == 0 - ? isar_models.AddressSubType.receiving - : isar_models.AddressSubType.change, - ); - - receivingNodes.addAll({ - "${_id}_$j": { - "node": node, - "address": address, - } - }); - txCountCallArgs.addAll({ - "${_id}_$j": addressString, - }); - } - - // get address tx counts - final counts = await _getBatchTxCount(addresses: txCountCallArgs); - - // check and add appropriate addresses - for (int k = 0; k < txCountBatchSize; k++) { - int count = counts["${_id}_$k"]!; - if (count > 0) { - final node = receivingNodes["${_id}_$k"]; - final address = node["address"] as isar_models.Address; - // add address to array - addressArray.add(address); - iterationsAddressArray.add(address.value); - // set current index - returningIndex = index + k; - // reset counter - gapCounter = 0; - // add info to derivations - derivations[address.value] = { - "pubKey": Format.uint8listToString( - (node["node"] as bip32.BIP32).publicKey), - "wif": (node["node"] as bip32.BIP32).toWIF(), - }; - } - - // increase counter when no tx history found - if (count == 0) { - gapCounter++; - } - } - // cache all the transactions while waiting for the current function to finish. - unawaited(getTransactionCacheEarly(iterationsAddressArray)); - } - return { - "addressArray": addressArray, - "index": returningIndex, - "derivations": derivations - }; - } - - Future getTransactionCacheEarly(List allAddresses) async { - try { - final List> allTxHashes = - await _fetchHistory(allAddresses); - for (final txHash in allTxHashes) { - try { - unawaited(cachedElectrumXClient.getTransaction( - txHash: txHash["tx_hash"] as String, - verbose: true, - coin: coin, - )); - } catch (e) { - continue; - } - } - } catch (e) { - // - } - } - - Future _recoverWalletFromBIP32SeedPhrase({ - required String mnemonic, - required String mnemonicPassphrase, - int maxUnusedAddressGap = 20, - int maxNumberOfIndexesToCheck = 1000, - bool isRescan = false, - }) async { - longMutex = true; - - Map> p2pkhReceiveDerivations = {}; - Map> p2shReceiveDerivations = {}; - Map> p2wpkhReceiveDerivations = {}; - Map> p2pkhChangeDerivations = {}; - Map> p2shChangeDerivations = {}; - Map> p2wpkhChangeDerivations = {}; - - final root = await Bip32Utils.getBip32Root( - mnemonic, - mnemonicPassphrase, - _network, - ); - - List p2pkhReceiveAddressArray = []; - List p2shReceiveAddressArray = []; - List p2wpkhReceiveAddressArray = []; - int p2pkhReceiveIndex = -1; - int p2shReceiveIndex = -1; - int p2wpkhReceiveIndex = -1; - - List p2pkhChangeAddressArray = []; - List p2shChangeAddressArray = []; - List p2wpkhChangeAddressArray = []; - int p2pkhChangeIndex = -1; - int p2shChangeIndex = -1; - int p2wpkhChangeIndex = -1; - - // actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3 - const txCountBatchSize = 12; - - try { - // receiving addresses - Logging.instance - .log("checking receiving addresses...", level: LogLevel.Info); - final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); - - final resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 0); - - final resultReceive84 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 0); - - Logging.instance - .log("checking change addresses...", level: LogLevel.Info); - // change addresses - final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); - - final resultChange49 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 1); - - final resultChange84 = _checkGaps(maxNumberOfIndexesToCheck, - maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 1); - - await Future.wait([ - resultReceive44, - resultReceive49, - resultReceive84, - resultChange44, - resultChange49, - resultChange84 - ]); - - p2pkhReceiveAddressArray = - (await resultReceive44)['addressArray'] as List; - p2pkhReceiveIndex = (await resultReceive44)['index'] as int; - p2pkhReceiveDerivations = (await resultReceive44)['derivations'] - as Map>; - - p2shReceiveAddressArray = - (await resultReceive49)['addressArray'] as List; - p2shReceiveIndex = (await resultReceive49)['index'] as int; - p2shReceiveDerivations = (await resultReceive49)['derivations'] - as Map>; - - p2wpkhReceiveAddressArray = - (await resultReceive84)['addressArray'] as List; - p2wpkhReceiveIndex = (await resultReceive84)['index'] as int; - p2wpkhReceiveDerivations = (await resultReceive84)['derivations'] - as Map>; - - p2pkhChangeAddressArray = - (await resultChange44)['addressArray'] as List; - p2pkhChangeIndex = (await resultChange44)['index'] as int; - p2pkhChangeDerivations = (await resultChange44)['derivations'] - as Map>; - - p2shChangeAddressArray = - (await resultChange49)['addressArray'] as List; - p2shChangeIndex = (await resultChange49)['index'] as int; - p2shChangeDerivations = (await resultChange49)['derivations'] - as Map>; - - p2wpkhChangeAddressArray = - (await resultChange84)['addressArray'] as List; - p2wpkhChangeIndex = (await resultChange84)['index'] as int; - p2wpkhChangeDerivations = (await resultChange84)['derivations'] - as Map>; - - // save the derivations (if any) - if (p2pkhReceiveDerivations.isNotEmpty) { - await addDerivations( - chain: 0, - derivePathType: DerivePathType.bip44, - derivationsToAdd: p2pkhReceiveDerivations); - } - if (p2shReceiveDerivations.isNotEmpty) { - await addDerivations( - chain: 0, - derivePathType: DerivePathType.bip49, - derivationsToAdd: p2shReceiveDerivations); - } - if (p2wpkhReceiveDerivations.isNotEmpty) { - await addDerivations( - chain: 0, - derivePathType: DerivePathType.bip84, - derivationsToAdd: p2wpkhReceiveDerivations); - } - if (p2pkhChangeDerivations.isNotEmpty) { - await addDerivations( - chain: 1, - derivePathType: DerivePathType.bip44, - derivationsToAdd: p2pkhChangeDerivations); - } - if (p2shChangeDerivations.isNotEmpty) { - await addDerivations( - chain: 1, - derivePathType: DerivePathType.bip49, - derivationsToAdd: p2shChangeDerivations); - } - if (p2wpkhChangeDerivations.isNotEmpty) { - await addDerivations( - chain: 1, - derivePathType: DerivePathType.bip84, - derivationsToAdd: p2wpkhChangeDerivations); - } - - // If restoring a wallet that never received any funds, then set receivingArray manually - // If we didn't do this, it'd store an empty array - if (p2pkhReceiveIndex == -1) { - final address = - await _generateAddressForChain(0, 0, DerivePathType.bip44); - p2pkhReceiveAddressArray.add(address); - } - if (p2shReceiveIndex == -1) { - final address = - await _generateAddressForChain(0, 0, DerivePathType.bip49); - p2shReceiveAddressArray.add(address); - } - if (p2wpkhReceiveIndex == -1) { - final address = - await _generateAddressForChain(0, 0, DerivePathType.bip84); - p2wpkhReceiveAddressArray.add(address); - } - - // If restoring a wallet that never sent any funds with change, then set changeArray - // manually. If we didn't do this, it'd store an empty array. - if (p2pkhChangeIndex == -1) { - final address = - await _generateAddressForChain(1, 0, DerivePathType.bip44); - p2pkhChangeAddressArray.add(address); - } - if (p2shChangeIndex == -1) { - final address = - await _generateAddressForChain(1, 0, DerivePathType.bip49); - p2shChangeAddressArray.add(address); - } - if (p2wpkhChangeIndex == -1) { - final address = - await _generateAddressForChain(1, 0, DerivePathType.bip84); - p2wpkhChangeAddressArray.add(address); - } - - if (isRescan) { - await db.updateOrPutAddresses([ - ...p2wpkhReceiveAddressArray, - ...p2wpkhChangeAddressArray, - ...p2pkhReceiveAddressArray, - ...p2pkhChangeAddressArray, - ...p2shReceiveAddressArray, - ...p2shChangeAddressArray, - ]); - } else { - await db.putAddresses([ - ...p2wpkhReceiveAddressArray, - ...p2wpkhChangeAddressArray, - ...p2pkhReceiveAddressArray, - ...p2pkhChangeAddressArray, - ...p2shReceiveAddressArray, - ...p2shChangeAddressArray, - ]); - } - - await _updateUTXOs(); - - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - - longMutex = false; - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s", - level: LogLevel.Error); - - longMutex = false; - rethrow; - } - } - - Future refreshIfThereIsNewData() async { - if (longMutex) return false; - if (_hasCalledExit) return false; - Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); - - try { - bool needsRefresh = false; - Set txnsToCheck = {}; - - for (final String txid in txTracker.pendings) { - if (!txTracker.wasNotifiedConfirmed(txid)) { - txnsToCheck.add(txid); - } - } - - for (String txid in txnsToCheck) { - final txn = await electrumXClient.getTransaction(txHash: txid); - int confirmations = txn["confirmations"] as int? ?? 0; - bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; - if (!isUnconfirmed) { - // unconfirmedTxs = {}; - needsRefresh = true; - break; - } - } - if (!needsRefresh) { - var allOwnAddresses = await _fetchAllOwnAddresses(); - List> allTxs = await _fetchHistory( - allOwnAddresses.map((e) => e.value).toList(growable: false)); - for (Map transaction in allTxs) { - final txid = transaction['tx_hash'] as String; - if ((await db - .getTransactions(walletId) - .filter() - .txidMatches(txid) - .findFirst()) == - null) { - Logging.instance.log( - " txid not found in address history already ${transaction['tx_hash']}", - level: LogLevel.Info); - needsRefresh = true; - break; - } - } - } - return needsRefresh; - } catch (e, s) { - Logging.instance.log( - "Exception caught in refreshIfThereIsNewData: $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future getAllTxsToWatch() async { - if (_hasCalledExit) return; - List unconfirmedTxnsToNotifyPending = []; - List unconfirmedTxnsToNotifyConfirmed = []; - - final currentChainHeight = await chainHeight; - - final txCount = await db.getTransactions(walletId).count(); - - const paginateLimit = 50; - - for (int i = 0; i < txCount; i += paginateLimit) { - final transactions = await db - .getTransactions(walletId) - .offset(i) - .limit(paginateLimit) - .findAll(); - for (final tx in transactions) { - if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - // get all transactions that were notified as pending but not as confirmed - if (txTracker.wasNotifiedPending(tx.txid) && - !txTracker.wasNotifiedConfirmed(tx.txid)) { - unconfirmedTxnsToNotifyConfirmed.add(tx); - } - } else { - // get all transactions that were not notified as pending yet - if (!txTracker.wasNotifiedPending(tx.txid)) { - unconfirmedTxnsToNotifyPending.add(tx); - } - } - } - } - - // notify on unconfirmed transactions - for (final tx in unconfirmedTxnsToNotifyPending) { - final confirmations = tx.getConfirmations(currentChainHeight); - - if (tx.type == isar_models.TransactionType.incoming) { - CryptoNotificationsEventBus.instance.fire( - CryptoNotificationEvent( - title: "Incoming transaction", - walletId: walletId, - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, - txid: tx.txid, - confirmations: confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, - walletName: walletName, - coin: coin, - ), - ); - await txTracker.addNotifiedPending(tx.txid); - } else if (tx.type == isar_models.TransactionType.outgoing) { - CryptoNotificationsEventBus.instance.fire( - CryptoNotificationEvent( - title: "Sending transaction", - walletId: walletId, - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, - txid: tx.txid, - confirmations: confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, - walletName: walletName, - coin: coin, - ), - ); - - await txTracker.addNotifiedPending(tx.txid); - } - } - - // notify on confirmed - for (final tx in unconfirmedTxnsToNotifyConfirmed) { - if (tx.type == isar_models.TransactionType.incoming) { - CryptoNotificationsEventBus.instance.fire( - CryptoNotificationEvent( - title: "Incoming transaction confirmed", - walletId: walletId, - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: false, - txid: tx.txid, - requiredConfirmations: MINIMUM_CONFIRMATIONS, - walletName: walletName, - coin: coin, - ), - ); - - await txTracker.addNotifiedConfirmed(tx.txid); - } else if (tx.type == isar_models.TransactionType.outgoing) { - CryptoNotificationsEventBus.instance.fire( - CryptoNotificationEvent( - title: "Outgoing transaction confirmed", - walletId: walletId, - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: false, - txid: tx.txid, - requiredConfirmations: MINIMUM_CONFIRMATIONS, - walletName: walletName, - coin: coin, - ), - ); - await txTracker.addNotifiedConfirmed(tx.txid); - } - } - } - - bool _shouldAutoSync = false; - - @override - bool get shouldAutoSync => _shouldAutoSync; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - refresh(); - } - } - } - - @override - bool get isRefreshing => refreshMutex; - - bool refreshMutex = false; - - //TODO Show percentages properly/more consistently - /// Refreshes display data for the wallet - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log("$walletId $walletName refreshMutex denied", - level: LogLevel.Info); - return; - } else { - refreshMutex = true; - } - - try { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); - - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); - - final currentHeight = await chainHeight; - const storedHeight = 1; //await storedChainHeight; - - Logging.instance - .log("chain height: $currentHeight", level: LogLevel.Info); - Logging.instance - .log("cached height: $storedHeight", level: LogLevel.Info); - - if (currentHeight != storedHeight) { - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); - await _checkChangeAddressForTransactions(); - - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); - await _checkCurrentReceivingAddressesForTransactions(); - - final fetchFuture = _refreshTransactions(); - 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)); - } - - refreshMutex = false; - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - - if (shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - Logging.instance.log( - "Periodic refresh check for $walletId $walletName in object instance: $hashCode", - level: LogLevel.Info); - // chain height check currently broken - // if ((await chainHeight) != (await storedChainHeight)) { - if (await refreshIfThereIsNewData()) { - await refresh(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - } - // } - }); - } - } catch (error, strace) { - refreshMutex = false; - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - NodeConnectionStatus.disconnected, - walletId, - coin, - ), - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - Logging.instance.log( - "Caught exception in refreshWalletData(): $error\n$strace", - level: LogLevel.Error); - } - } - - @override - Future> prepareSend({ - required String address, - required Amount amount, - Map? args, - }) async { - try { - final feeRateType = args?["feeRate"]; - final customSatsPerVByte = args?["satsPerVByte"] as int?; - final feeRateAmount = args?["feeRateAmount"]; - final utxos = args?["UTXOs"] as Set?; - - if (customSatsPerVByte != null) { - // check for send all - bool isSendAll = false; - if (amount == balance.spendable) { - isSendAll = true; - } - - final bool coinControl = utxos != null; - - final result = await coinSelection( - satoshiAmountToSend: amount.raw.toInt(), - selectedTxFeeRate: -1, - satsPerVByte: customSatsPerVByte, - recipientAddress: address, - isSendAll: isSendAll, - utxos: utxos?.toList(), - coinControl: coinControl, - ); - - Logging.instance - .log("PREPARE SEND RESULT: $result", level: LogLevel.Info); - if (result is int) { - switch (result) { - case 1: - throw Exception("Insufficient balance!"); - case 2: - throw Exception("Insufficient funds to pay for transaction fee!"); - default: - throw Exception("Transaction failed with error code $result"); - } - } else { - final hex = result["hex"]; - if (hex is String) { - final fee = result["fee"] as int; - final vSize = result["vSize"] as int; - - Logging.instance.log("txHex: $hex", level: LogLevel.Info); - Logging.instance.log("fee: $fee", level: LogLevel.Info); - Logging.instance.log("vsize: $vSize", level: LogLevel.Info); - // fee should never be less than vSize sanity check - if (fee < vSize) { - throw Exception( - "Error in fee calculation: Transaction fee cannot be less than vSize"); - } - return result as Map; - } else { - throw Exception("sent hex is not a String!!!"); - } - } - } else if (feeRateType is FeeRateType || feeRateAmount is int) { - late final int rate; - if (feeRateType is FeeRateType) { - int fee = 0; - final feeObject = await fees; - switch (feeRateType) { - case FeeRateType.fast: - fee = feeObject.fast; - break; - case FeeRateType.average: - fee = feeObject.medium; - break; - case FeeRateType.slow: - fee = feeObject.slow; - break; - default: - throw ArgumentError("Invalid use of custom fee"); - } - rate = fee; - } else { - rate = feeRateAmount as int; - } - - // check for send all - bool isSendAll = false; - if (amount == balance.spendable) { - isSendAll = true; - } - - final bool coinControl = utxos != null; - - final txData = await coinSelection( - satoshiAmountToSend: amount.raw.toInt(), - selectedTxFeeRate: rate, - recipientAddress: address, - isSendAll: isSendAll, - utxos: utxos?.toList(), - coinControl: coinControl, - ); - - Logging.instance.log("prepare send: $txData", level: LogLevel.Info); - try { - if (txData is int) { - switch (txData) { - case 1: - throw Exception("Insufficient balance!"); - case 2: - throw Exception( - "Insufficient funds to pay for transaction fee!"); - default: - throw Exception("Transaction failed with error code $txData"); - } - } else { - final hex = txData["hex"]; - - if (hex is String) { - final fee = txData["fee"] as int; - final vSize = txData["vSize"] as int; - - Logging.instance - .log("prepared txHex: $hex", level: LogLevel.Info); - Logging.instance.log("prepared fee: $fee", level: LogLevel.Info); - Logging.instance - .log("prepared vSize: $vSize", level: LogLevel.Info); - - // fee should never be less than vSize sanity check - if (fee < vSize) { - throw Exception( - "Error in fee calculation: Transaction fee cannot be less than vSize"); - } - - return txData as Map; - } else { - throw Exception("prepared hex is not a String!!!"); - } - } - } catch (e, s) { - Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } else { - throw ArgumentError("Invalid fee rate argument provided!"); - } - } catch (e, s) { - Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - @override - Future confirmSend({required Map txData}) async { - try { - Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); - - final hex = txData["hex"] as String; - - final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex); - Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); - - final utxos = txData["usedUTXOs"] as List; - - // mark utxos as used - await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList()); - - return txHash; - } catch (e, s) { - Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - @override - Future testNetworkConnection() async { - try { - final result = await _electrumXClient.ping(); - return result; - } catch (_) { - return false; - } - } - - Timer? _networkAliveTimer; - - 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 testNetworkConnection(); - - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - GlobalEventBus.instance - .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); - - _isConnected = hasNetwork; - if (hasNetwork) { - unawaited(refresh()); - } - } - } - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - bool _isConnected = false; - - @override - bool get isConnected => _isConnected; - - @override - Future initializeNew( - ({String mnemonicPassphrase, int wordCount})? data, - ) async { - Logging.instance - .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); - - if (getCachedId() != null) { - throw Exception( - "Attempted to initialize a new wallet using an existing wallet ID!"); - } - - await _prefs.init(); - try { - await _generateNewWallet(data); - } catch (e, s) { - Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", - level: LogLevel.Fatal); - rethrow; - } - - await Future.wait([ - updateCachedId(walletId), - updateCachedIsFavorite(false), - ]); - } - - @override - Future initializeExisting() async { - Logging.instance.log("initializeExisting() ${coin.prettyName} wallet.", - level: LogLevel.Info); - - if (getCachedId() == null) { - throw Exception( - "Attempted to initialize an existing wallet using an unknown wallet ID!"); - } - await _prefs.init(); - // await _checkCurrentChangeAddressesForTransactions(); - // await _checkCurrentReceivingAddressesForTransactions(); - } - - // hack to add tx to txData before refresh completes - // required based on current app architecture where we don't properly store - // transactions locally in a good way - @override - Future updateSentCachedTxData(Map txData) async { - final transaction = isar_models.Transaction( - walletId: walletId, - txid: txData["txid"] as String, - timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, - type: isar_models.TransactionType.outgoing, - subType: isar_models.TransactionSubType.none, - // precision may be lost here hence the following amountString - amount: (txData["recipientAmt"] as Amount).raw.toInt(), - amountString: (txData["recipientAmt"] as Amount).toJsonString(), - fee: txData["fee"] as int, - height: null, - isCancelled: false, - isLelantus: false, - otherData: null, - slateId: null, - nonce: null, - inputs: [], - outputs: [], - numberOfMessages: null, - ); - - final address = txData["address"] is String - ? await db.getAddress(walletId, txData["address"] as String) - : null; - - await db.addNewTransactionData( - [ - Tuple2(transaction, address), - ], - walletId, - ); - } - - @override - bool validateAddress(String address) { - return Address.validateAddress(address, _network, _network.bech32!); - } - - @override - String get walletId => _walletId; - late final String _walletId; - - @override - String get walletName => _walletName; - late String _walletName; - - // setter for updating on rename - @override - set walletName(String newName) => _walletName = newName; - - late ElectrumXClient _electrumXClient; - - ElectrumXClient get electrumXClient => _electrumXClient; - - late CachedElectrumXClient _cachedElectrumXClient; - - CachedElectrumXClient get cachedElectrumXClient => _cachedElectrumXClient; - - late SecureStorageInterface _secureStore; - - @override - Future updateNode(bool shouldRefresh) async { - final failovers = NodeService(secureStorageInterface: _secureStore) - .failoverNodesFor(coin: coin) - .map((e) => ElectrumXNode( - address: e.host, - port: e.port, - name: e.name, - id: e.id, - useSSL: e.useSSL, - )) - .toList(); - final newNode = await getCurrentNode(); - _electrumXClient = ElectrumXClient.from( - node: newNode, - prefs: _prefs, - failovers: failovers, - ); - _cachedElectrumXClient = CachedElectrumXClient.from( - electrumXClient: _electrumXClient, - ); - - if (shouldRefresh) { - unawaited(refresh()); - } - } - - Future> _getMnemonicList() async { - final _mnemonicString = await mnemonicString; - if (_mnemonicString == null) { - return []; - } - final List data = _mnemonicString.split(' '); - return data; - } - - Future getCurrentNode() async { - final node = NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - - return ElectrumXNode( - address: node.host, - port: node.port, - name: node.name, - useSSL: node.useSSL, - id: node.id, - ); - } - - Future> _fetchAllOwnAddresses() async { - final allAddresses = await db - .getAddresses(walletId) - .filter() - .not() - .typeEqualTo(isar_models.AddressType.nonWallet) - .and() - .group((q) => q - .subTypeEqualTo(isar_models.AddressSubType.receiving) - .or() - .subTypeEqualTo(isar_models.AddressSubType.change)) - .findAll(); - // final List allAddresses = []; - // final receivingAddresses = DB.instance.get( - // boxName: walletId, key: 'receivingAddressesP2WPKH') as List; - // final changeAddresses = DB.instance.get( - // boxName: walletId, key: 'changeAddressesP2WPKH') as List; - // final receivingAddressesP2PKH = DB.instance.get( - // boxName: walletId, key: 'receivingAddressesP2PKH') as List; - // final changeAddressesP2PKH = - // DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') - // as List; - // final receivingAddressesP2SH = DB.instance.get( - // boxName: walletId, key: 'receivingAddressesP2SH') as List; - // final changeAddressesP2SH = - // DB.instance.get(boxName: walletId, key: 'changeAddressesP2SH') - // as List; - // - // for (var i = 0; i < receivingAddresses.length; i++) { - // if (!allAddresses.contains(receivingAddresses[i])) { - // allAddresses.add(receivingAddresses[i] as String); - // } - // } - // for (var i = 0; i < changeAddresses.length; i++) { - // if (!allAddresses.contains(changeAddresses[i])) { - // allAddresses.add(changeAddresses[i] as String); - // } - // } - // for (var i = 0; i < receivingAddressesP2PKH.length; i++) { - // if (!allAddresses.contains(receivingAddressesP2PKH[i])) { - // allAddresses.add(receivingAddressesP2PKH[i] as String); - // } - // } - // for (var i = 0; i < changeAddressesP2PKH.length; i++) { - // if (!allAddresses.contains(changeAddressesP2PKH[i])) { - // allAddresses.add(changeAddressesP2PKH[i] as String); - // } - // } - // for (var i = 0; i < receivingAddressesP2SH.length; i++) { - // if (!allAddresses.contains(receivingAddressesP2SH[i])) { - // allAddresses.add(receivingAddressesP2SH[i] as String); - // } - // } - // for (var i = 0; i < changeAddressesP2SH.length; i++) { - // if (!allAddresses.contains(changeAddressesP2SH[i])) { - // allAddresses.add(changeAddressesP2SH[i] as String); - // } - // } - return allAddresses; - } - - Future _getFees() async { - try { - //TODO adjust numbers for different speeds? - const int f = 1, m = 5, s = 20; - - final fast = await electrumXClient.estimateFee(blocks: f); - final medium = await electrumXClient.estimateFee(blocks: m); - final slow = await electrumXClient.estimateFee(blocks: s); - - final feeObject = FeeObject( - numberOfBlocksFast: f, - numberOfBlocksAverage: m, - numberOfBlocksSlow: s, - fast: Amount.fromDecimal( - fast, - fractionDigits: coin.decimals, - ).raw.toInt(), - medium: Amount.fromDecimal( - medium, - fractionDigits: coin.decimals, - ).raw.toInt(), - slow: Amount.fromDecimal( - slow, - fractionDigits: coin.decimals, - ).raw.toInt(), - ); - - Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); - return feeObject; - } catch (e) { - Logging.instance - .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); - rethrow; - } - } - - Future _generateNewWallet( - ({String mnemonicPassphrase, int wordCount})? data, - ) async { - Logging.instance - .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); - if (!integrationTestFlag) { - try { - final features = await electrumXClient - .getServerFeatures() - .timeout(const Duration(seconds: 3)); - Logging.instance.log("features: $features", level: LogLevel.Info); - switch (coin) { - case Coin.litecoin: - if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { - // print(features['genesis_hash']); - throw Exception("genesis hash does not match main net!"); - } - break; - case Coin.litecoinTestNet: - if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - throw Exception("genesis hash does not match test net!"); - } - break; - default: - throw Exception( - "Attempted to generate a LitecoinWallet using a non litecoin coin type: ${coin.name}"); - } - } catch (e, s) { - Logging.instance.log("$e/n$s", level: LogLevel.Info); - } - } - - // this should never fail - if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { - throw Exception( - "Attempted to overwrite mnemonic on generate new wallet!"); - } - final int strength; - if (data == null || data.wordCount == 12) { - strength = 128; - } else if (data.wordCount == 24) { - strength = 256; - } else { - throw Exception("Invalid word count"); - } - await _secureStore.write( - key: '${_walletId}_mnemonic', - value: bip39.generateMnemonic(strength: strength)); - await _secureStore.write( - key: '${_walletId}_mnemonicPassphrase', - value: data?.mnemonicPassphrase ?? "", - ); - - // Generate and add addresses to relevant arrays - final initialAddresses = await Future.wait([ - // P2WPKH - _generateAddressForChain(0, 0, DerivePathType.bip84), - _generateAddressForChain(1, 0, DerivePathType.bip84), - - // P2PKH - _generateAddressForChain(0, 0, DerivePathType.bip44), - _generateAddressForChain(1, 0, DerivePathType.bip44), - - // P2SH - _generateAddressForChain(0, 0, DerivePathType.bip49), - _generateAddressForChain(1, 0, DerivePathType.bip49), - ]); - - await db.putAddresses(initialAddresses); - - Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); - } - - /// Generates a new internal or external chain address for the wallet using a BIP84, BIP44, or BIP49 derivation path. - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - /// [index] - This can be any integer >= 0 - Future _generateAddressForChain( - int chain, - int index, - DerivePathType derivePathType, - ) async { - final _mnemonic = await mnemonicString; - final _mnemonicPassphrase = await mnemonicPassphrase; - if (_mnemonicPassphrase == null) { - Logging.instance.log( - "Exception in _generateAddressForChain: mnemonic passphrase null, possible migration issue; if using internal builds, delete wallet and restore from seed, if using a release build, please file bug report", - level: LogLevel.Error); - } - - final derivePath = constructDerivePath( - derivePathType: derivePathType, - networkWIF: _network.wif, - chain: chain, - index: index, - ); - final node = await Bip32Utils.getBip32Node( - _mnemonic!, - _mnemonicPassphrase!, - _network, - derivePath, - ); - - final data = PaymentData(pubkey: node.publicKey); - String address; - isar_models.AddressType addrType; - - switch (derivePathType) { - case DerivePathType.bip44: - address = P2PKH(data: data, network: _network).data.address!; - addrType = isar_models.AddressType.p2pkh; - break; - case DerivePathType.bip49: - address = P2SH( - data: PaymentData( - redeem: P2WPKH( - data: data, - network: _network, - overridePrefix: _network.bech32!) - .data), - network: _network) - .data - .address!; - addrType = isar_models.AddressType.p2sh; - break; - case DerivePathType.bip84: - address = P2WPKH( - network: _network, data: data, overridePrefix: _network.bech32!) - .data - .address!; - addrType = isar_models.AddressType.p2wpkh; - break; - default: - throw Exception("DerivePathType unsupported"); - } - - // add generated address & info to derivations - await addDerivation( - chain: chain, - address: address, - pubKey: Format.uint8listToString(node.publicKey), - wif: node.toWIF(), - derivePathType: derivePathType, - ); - - return isar_models.Address( - walletId: walletId, - value: address, - publicKey: node.publicKey, - type: addrType, - derivationIndex: index, - derivationPath: isar_models.DerivationPath()..value = derivePath, - subType: chain == 0 - ? isar_models.AddressSubType.receiving - : isar_models.AddressSubType.change, - ); - } - - /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] - /// and - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future _getCurrentAddressForChain( - int chain, - DerivePathType derivePathType, - ) async { - final subType = chain == 0 // Here, we assume that chain == 1 if it isn't 0 - ? isar_models.AddressSubType.receiving - : isar_models.AddressSubType.change; - - isar_models.AddressType type; - isar_models.Address? address; - switch (derivePathType) { - case DerivePathType.bip44: - type = isar_models.AddressType.p2pkh; - break; - case DerivePathType.bip49: - type = isar_models.AddressType.p2sh; - break; - case DerivePathType.bip84: - type = isar_models.AddressType.p2wpkh; - break; - default: - throw Exception("DerivePathType unsupported"); - } - address = await db - .getAddresses(walletId) - .filter() - .typeEqualTo(type) - .subTypeEqualTo(subType) - .sortByDerivationIndexDesc() - .findFirst(); - return address!.value; - } - - String _buildDerivationStorageKey({ - required int chain, - required DerivePathType derivePathType, - }) { - String key; - String chainId = chain == 0 ? "receive" : "change"; - switch (derivePathType) { - case DerivePathType.bip44: - key = "${walletId}_${chainId}DerivationsP2PKH"; - break; - case DerivePathType.bip49: - key = "${walletId}_${chainId}DerivationsP2SH"; - break; - case DerivePathType.bip84: - key = "${walletId}_${chainId}DerivationsP2WPKH"; - break; - default: - throw Exception("DerivePathType unsupported"); - } - return key; - } - - Future> _fetchDerivations({ - required int chain, - required DerivePathType derivePathType, - }) async { - // build lookup key - final key = _buildDerivationStorageKey( - chain: chain, derivePathType: derivePathType); - - // fetch current derivations - final derivationsString = await _secureStore.read(key: key); - return Map.from( - jsonDecode(derivationsString ?? "{}") as Map); - } - - /// Add a single derivation to the local secure storage for [chain] and - /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. - /// This will overwrite a previous entry where the address of the new derivation - /// matches a derivation currently stored. - Future addDerivation({ - required int chain, - required String address, - required String pubKey, - required String wif, - required DerivePathType derivePathType, - }) async { - // build lookup key - final key = _buildDerivationStorageKey( - chain: chain, derivePathType: derivePathType); - - // fetch current derivations - final derivationsString = await _secureStore.read(key: key); - final derivations = - Map.from(jsonDecode(derivationsString ?? "{}") as Map); - - // add derivation - derivations[address] = { - "pubKey": pubKey, - "wif": wif, - }; - - // save derivations - final newReceiveDerivationsString = jsonEncode(derivations); - await _secureStore.write(key: key, value: newReceiveDerivationsString); - } - - /// Add multiple derivations to the local secure storage for [chain] and - /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. - /// This will overwrite any previous entries where the address of the new derivation - /// matches a derivation currently stored. - /// The [derivationsToAdd] must be in the format of: - /// { - /// addressA : { - /// "pubKey": , - /// "wif": , - /// }, - /// addressB : { - /// "pubKey": , - /// "wif": , - /// }, - /// } - Future addDerivations({ - required int chain, - required DerivePathType derivePathType, - required Map derivationsToAdd, - }) async { - // build lookup key - final key = _buildDerivationStorageKey( - chain: chain, derivePathType: derivePathType); - - // fetch current derivations - final derivationsString = await _secureStore.read(key: key); - final derivations = - Map.from(jsonDecode(derivationsString ?? "{}") as Map); - - // add derivation - derivations.addAll(derivationsToAdd); - - // save derivations - final newReceiveDerivationsString = jsonEncode(derivations); - await _secureStore.write(key: key, value: newReceiveDerivationsString); - } - - Future _updateUTXOs() async { - final allAddresses = await _fetchAllOwnAddresses(); - - try { - final fetchedUtxoList = >>[]; - - final Map>> batches = {}; - const batchSizeMax = 100; - int batchNumber = 0; - for (int i = 0; i < allAddresses.length; i++) { - if (batches[batchNumber] == null) { - batches[batchNumber] = {}; - } - final scripthash = - _convertToScriptHash(allAddresses[i].value, _network); - batches[batchNumber]!.addAll({ - scripthash: [scripthash] - }); - if (i % batchSizeMax == batchSizeMax - 1) { - batchNumber++; - } - } - - for (int i = 0; i < batches.length; i++) { - final response = - await _electrumXClient.getBatchUTXOs(args: batches[i]!); - for (final entry in response.entries) { - if (entry.value.isNotEmpty) { - fetchedUtxoList.add(entry.value); - } - } - } - - final List outputArray = []; - - for (int i = 0; i < fetchedUtxoList.length; i++) { - for (int j = 0; j < fetchedUtxoList[i].length; j++) { - final jsonUTXO = fetchedUtxoList[i][j]; - - final txn = await cachedElectrumXClient.getTransaction( - txHash: jsonUTXO["tx_hash"] as String, - verbose: true, - coin: coin, - ); - - bool shouldBlock = false; - String? blockReason; - String? label; - - final vout = jsonUTXO["tx_pos"] as int; - - final outputs = txn["vout"] as List; - - String? utxoOwnerAddress; - // get UTXO owner address - for (final output in outputs) { - if (output["n"] == vout) { - utxoOwnerAddress = - output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; - } - } - - final utxoAmount = jsonUTXO["value"] as int; - - // TODO check the specific output, not just the address in general - // TODO optimize by freezing output in OrdinalsInterface, so one ordinal API calls is made (or at least many less) - if (utxoOwnerAddress != null) { - if (await inscriptionInAddress(utxoOwnerAddress!)) { - shouldBlock = true; - blockReason = "Ordinal"; - label = "Ordinal detected at address"; - } - } else { - // TODO implement inscriptionInOutput - if (utxoAmount <= 10000) { - shouldBlock = true; - blockReason = "May contain ordinal"; - label = "Possible ordinal"; - } - } - - final utxo = isar_models.UTXO( - walletId: walletId, - txid: txn["txid"] as String, - vout: vout, - value: utxoAmount, - name: label ?? "", - isBlocked: shouldBlock, - blockedReason: blockReason, - isCoinbase: txn["is_coinbase"] as bool? ?? false, - blockHash: txn["blockhash"] as String?, - blockHeight: jsonUTXO["height"] as int?, - blockTime: txn["blocktime"] as int?, - address: utxoOwnerAddress, - ); - - outputArray.add(utxo); - } - } - - Logging.instance.log( - 'Outputs fetched: $outputArray', - level: LogLevel.Info, - ); - - bool inscriptionsRefreshNeeded = - await db.updateUTXOs(walletId, outputArray); - - if (inscriptionsRefreshNeeded) { - await refreshInscriptions(); - } - - // finally update balance - await _updateBalance(); - } catch (e, s) { - Logging.instance.log( - "Output fetch unsuccessful: $e\n$s", - level: LogLevel.Error, - ); - } - } - - Future _updateBalance() async { - await refreshBalance(); - } - - @override - Balance get balance => _balance ??= getCachedBalance(); - Balance? _balance; - - // /// Takes in a list of UtxoObjects and adds a name (dependent on object index within list) - // /// and checks for the txid associated with the utxo being blocked and marks it accordingly. - // /// Now also checks for output labeling. - // Future _sortOutputs(List utxos) async { - // final blockedHashArray = - // DB.instance.get(boxName: walletId, key: 'blocked_tx_hashes') - // as List?; - // final List lst = []; - // if (blockedHashArray != null) { - // for (var hash in blockedHashArray) { - // lst.add(hash as String); - // } - // } - // final labels = - // DB.instance.get(boxName: walletId, key: 'labels') as Map? ?? - // {}; - // - // outputsList = []; - // - // for (var i = 0; i < utxos.length; i++) { - // if (labels[utxos[i].txid] != null) { - // utxos[i].txName = labels[utxos[i].txid] as String? ?? ""; - // } else { - // utxos[i].txName = 'Output #$i'; - // } - // - // if (utxos[i].status.confirmed == false) { - // outputsList.add(utxos[i]); - // } else { - // if (lst.contains(utxos[i].txid)) { - // utxos[i].blocked = true; - // outputsList.add(utxos[i]); - // } else if (!lst.contains(utxos[i].txid)) { - // outputsList.add(utxos[i]); - // } - // } - // } - // } - - Future getTxCount({required String address}) async { - String? scripthash; - try { - scripthash = _convertToScriptHash(address, _network); - final transactions = - await electrumXClient.getHistory(scripthash: scripthash); - return transactions.length; - } catch (e) { - Logging.instance.log( - "Exception rethrown in _getTxCount(address: $address, scripthash: $scripthash): $e", - level: LogLevel.Error); - rethrow; - } - } - - Future> _getBatchTxCount({ - required Map addresses, - }) async { - try { - final Map> args = {}; - for (final entry in addresses.entries) { - args[entry.key] = [_convertToScriptHash(entry.value, _network)]; - } - final response = await electrumXClient.getBatchHistory(args: args); - - final Map result = {}; - for (final entry in response.entries) { - result[entry.key] = entry.value.length; - } - return result; - } catch (e, s) { - Logging.instance.log( - "Exception rethrown in _getBatchTxCount(address: $addresses: $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future _checkReceivingAddressForTransactions() async { - try { - final currentReceiving = await _currentReceivingAddress; - - final int txCount = await getTxCount(address: currentReceiving.value); - Logging.instance.log( - 'Number of txs for current receiving address $currentReceiving: $txCount', - level: LogLevel.Info); - - if (txCount >= 1 || currentReceiving.derivationIndex < 0) { - // First increment the receiving index - final newReceivingIndex = currentReceiving.derivationIndex + 1; - - // Use new index to derive a new receiving address - final newReceivingAddress = await _generateAddressForChain( - 0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin)); - - final existing = await db - .getAddresses(walletId) - .filter() - .valueEqualTo(newReceivingAddress.value) - .findFirst(); - if (existing == null) { - // Add that new change address - await db.putAddress(newReceivingAddress); - } else { - // we need to update the address - await db.updateAddress(existing, newReceivingAddress); - } - // keep checking until address with no tx history is set as current - await _checkReceivingAddressForTransactions(); - } - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future _checkChangeAddressForTransactions() async { - try { - final currentChange = await _currentChangeAddress; - final int txCount = await getTxCount(address: currentChange.value); - Logging.instance.log( - 'Number of txs for current change address $currentChange: $txCount', - level: LogLevel.Info); - - if (txCount >= 1 || currentChange.derivationIndex < 0) { - // First increment the change index - final newChangeIndex = currentChange.derivationIndex + 1; - - // Use new index to derive a new change address - final newChangeAddress = await _generateAddressForChain( - 1, newChangeIndex, DerivePathTypeExt.primaryFor(coin)); - - final existing = await db - .getAddresses(walletId) - .filter() - .valueEqualTo(newChangeAddress.value) - .findFirst(); - if (existing == null) { - // Add that new change address - await db.putAddress(newChangeAddress); - } else { - // we need to update the address - await db.updateAddress(existing, newChangeAddress); - } - // keep checking until address with no tx history is set as current - await _checkChangeAddressForTransactions(); - } - } on SocketException catch (se, s) { - Logging.instance.log( - "SocketException caught in _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $se\n$s", - level: LogLevel.Error); - return; - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future _checkCurrentReceivingAddressesForTransactions() async { - try { - // for (final type in DerivePathType.values) { - await _checkReceivingAddressForTransactions(); - // } - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - /// public wrapper because dart can't test private... - Future checkCurrentReceivingAddressesForTransactions() async { - if (Platform.environment["FLUTTER_TEST"] == "true") { - try { - return _checkCurrentReceivingAddressesForTransactions(); - } catch (_) { - rethrow; - } - } - } - - Future _checkCurrentChangeAddressesForTransactions() async { - try { - // for (final type in DerivePathType.values) { - await _checkChangeAddressForTransactions(); - // } - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - /// public wrapper because dart can't test private... - Future checkCurrentChangeAddressesForTransactions() async { - if (Platform.environment["FLUTTER_TEST"] == "true") { - try { - return _checkCurrentChangeAddressesForTransactions(); - } catch (_) { - rethrow; - } - } - } - - /// attempts to convert a string to a valid scripthash - /// - /// Returns the scripthash or throws an exception on invalid litecoin address - String _convertToScriptHash(String litecoinAddress, NetworkType network) { - try { - final output = Address.addressToOutputScript( - litecoinAddress, network, _network.bech32!); - final hash = sha256.convert(output.toList(growable: false)).toString(); - - final chars = hash.split(""); - final reversedPairs = []; - var i = chars.length - 1; - while (i > 0) { - reversedPairs.add(chars[i - 1]); - reversedPairs.add(chars[i]); - i -= 2; - } - return reversedPairs.join(""); - } catch (e) { - rethrow; - } - } - - Future>> _fetchHistory( - List allAddresses) async { - try { - List> allTxHashes = []; - - final Map>> batches = {}; - final Map requestIdToAddressMap = {}; - const batchSizeMax = 100; - int batchNumber = 0; - for (int i = 0; i < allAddresses.length; i++) { - if (batches[batchNumber] == null) { - batches[batchNumber] = {}; - } - final scripthash = _convertToScriptHash(allAddresses[i], _network); - final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); - requestIdToAddressMap[id] = allAddresses[i]; - batches[batchNumber]!.addAll({ - id: [scripthash] - }); - if (i % batchSizeMax == batchSizeMax - 1) { - batchNumber++; - } - } - - for (int i = 0; i < batches.length; i++) { - final response = - await _electrumXClient.getBatchHistory(args: batches[i]!); - for (final entry in response.entries) { - for (int j = 0; j < entry.value.length; j++) { - entry.value[j]["address"] = requestIdToAddressMap[entry.key]; - if (!allTxHashes.contains(entry.value[j])) { - allTxHashes.add(entry.value[j]); - } - } - } - } - - return allTxHashes; - } catch (e, s) { - Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error); - rethrow; - } - } - - Future>> fastFetch(List allTxHashes) async { - List> allTransactions = []; - - const futureLimit = 30; - List>> transactionFutures = []; - int currentFutureCount = 0; - for (final txHash in allTxHashes) { - Future> transactionFuture = - cachedElectrumXClient.getTransaction( - txHash: txHash, - verbose: true, - coin: coin, - ); - transactionFutures.add(transactionFuture); - currentFutureCount++; - if (currentFutureCount > futureLimit) { - currentFutureCount = 0; - await Future.wait(transactionFutures); - for (final fTx in transactionFutures) { - final tx = await fTx; - - allTransactions.add(tx); - } - } - } - if (currentFutureCount != 0) { - currentFutureCount = 0; - await Future.wait(transactionFutures); - for (final fTx in transactionFutures) { - final tx = await fTx; - - allTransactions.add(tx); - } - } - return allTransactions; - } - - bool _duplicateTxCheck( - List> allTransactions, String txid) { - for (int i = 0; i < allTransactions.length; i++) { - if (allTransactions[i]["txid"] == txid) { - return true; - } - } - return false; - } - - Future _refreshTransactions() async { - final List allAddresses = - await _fetchAllOwnAddresses(); - - final List> allTxHashes = - await _fetchHistory(allAddresses.map((e) => e.value).toList()); - - Set hashes = {}; - for (var element in allTxHashes) { - hashes.add(element['tx_hash'] as String); - } - await fastFetch(hashes.toList()); - - List> allTransactions = []; - final currentHeight = await chainHeight; - - for (final txHash in allTxHashes) { - final storedTx = await db - .getTransactions(walletId) - .filter() - .txidEqualTo(txHash["tx_hash"] as String) - .findFirst(); - - if (storedTx == null || - !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) { - final tx = await cachedElectrumXClient.getTransaction( - txHash: txHash["tx_hash"] as String, - verbose: true, - coin: coin, - ); - - // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}"); - if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { - tx["address"] = await db - .getAddresses(walletId) - .filter() - .valueEqualTo(txHash["address"] as String) - .findFirst(); - tx["height"] = txHash["height"]; - allTransactions.add(tx); - } - } - } - - // prefetch/cache - Set vHashes = {}; - for (final txObject in allTransactions) { - for (int i = 0; i < (txObject["vin"] as List).length; i++) { - final input = txObject["vin"]![i] as Map; - final prevTxid = input["txid"] as String; - vHashes.add(prevTxid); - } - } - await fastFetch(vHashes.toList()); - - final List> txnsData = - []; - - for (final txObject in allTransactions) { - final data = await parseTransaction( - txObject, - cachedElectrumXClient, - allAddresses, - coin, - MINIMUM_CONFIRMATIONS, - walletId, - ); - - txnsData.add(data); - } - await db.addNewTransactionData(txnsData, walletId); - - // quick hack to notify manager to call notifyListeners if - // transactions changed - if (txnsData.isNotEmpty) { - GlobalEventBus.instance.fire( - UpdatedInBackgroundEvent( - "Transactions updated/added for: $walletId $walletName ", - walletId, - ), - ); - } - } - - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); - } - - /// The coinselection algorithm decides whether or not the user is eligible to make the transaction - /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return - /// a map containing the tx hex along with other important information. If not, then it will return - /// an integer (1 or 2) - dynamic coinSelection({ - required int satoshiAmountToSend, - required int selectedTxFeeRate, - required String recipientAddress, - required bool coinControl, - required bool isSendAll, - int? satsPerVByte, - int additionalOutputs = 0, - List? utxos, - }) async { - Logging.instance - .log("Starting coinSelection ----------", level: LogLevel.Info); - final List availableOutputs = utxos ?? await this.utxos; - final currentChainHeight = await chainHeight; - final List spendableOutputs = []; - int spendableSatoshiValue = 0; - - // Build list of spendable outputs and totaling their satoshi amount - for (final utxo in availableOutputs) { - if (utxo.isBlocked == false && - utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) && - utxo.used != true) { - spendableOutputs.add(utxo); - spendableSatoshiValue += utxo.value; - } - } - - if (coinControl) { - if (spendableOutputs.length < availableOutputs.length) { - throw ArgumentError("Attempted to use an unavailable utxo"); - } - } - - // don't care about sorting if using all utxos - if (!coinControl) { - // sort spendable by age (oldest first) - spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); - } - - Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", - level: LogLevel.Info); - Logging.instance - .log("spendableOutputs: $spendableOutputs", level: LogLevel.Info); - Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue", - level: LogLevel.Info); - Logging.instance - .log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info); - // If the amount the user is trying to send is smaller than the amount that they have spendable, - // then return 1, which indicates that they have an insufficient balance. - if (spendableSatoshiValue < satoshiAmountToSend) { - return 1; - // If the amount the user wants to send is exactly equal to the amount they can spend, then return - // 2, which indicates that they are not leaving enough over to pay the transaction fee - } else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) { - return 2; - } - // If neither of these statements pass, we assume that the user has a spendable balance greater - // than the amount they're attempting to send. Note that this value still does not account for - // the added transaction fee, which may require an extra input and will need to be checked for - // later on. - - // Possible situation right here - int satoshisBeingUsed = 0; - int inputsBeingConsumed = 0; - List utxoObjectsToUse = []; - - if (!coinControl) { - for (var i = 0; - satoshisBeingUsed < satoshiAmountToSend && - i < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += spendableOutputs[i].value; - inputsBeingConsumed += 1; - } - for (int i = 0; - i < additionalOutputs && - inputsBeingConsumed < spendableOutputs.length; - i++) { - utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; - inputsBeingConsumed += 1; - } - } else { - satoshisBeingUsed = spendableSatoshiValue; - utxoObjectsToUse = spendableOutputs; - inputsBeingConsumed = spendableOutputs.length; - } - - Logging.instance - .log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info); - Logging.instance - .log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info); - Logging.instance - .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); - - // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray - List recipientsArray = [recipientAddress]; - List recipientsAmtArray = [satoshiAmountToSend]; - - // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); - - if (isSendAll) { - Logging.instance - .log("Attempting to send all $coin", level: LogLevel.Info); - - final int vSizeForOneOutput = (await buildTransaction( - utxoSigningData: utxoSigningData, - recipients: [recipientAddress], - satoshiAmounts: [satoshisBeingUsed - 1], - ))["vSize"] as int; - int feeForOneOutput = satsPerVByte != null - ? (satsPerVByte * vSizeForOneOutput) - : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: selectedTxFeeRate, - ); - - if (satsPerVByte == null) { - final int roughEstimate = roughFeeEstimate( - spendableOutputs.length, - 1, - selectedTxFeeRate, - ).raw.toInt(); - if (feeForOneOutput < roughEstimate) { - feeForOneOutput = roughEstimate; - } - } - - final int amount = satoshiAmountToSend - feeForOneOutput; - dynamic txn = await buildTransaction( - utxoSigningData: utxoSigningData, - recipients: recipientsArray, - satoshiAmounts: [amount], - ); - Map transactionObject = { - "hex": txn["hex"], - "recipient": recipientsArray[0], - "recipientAmt": Amount( - rawValue: BigInt.from(amount), - fractionDigits: coin.decimals, - ), - "fee": feeForOneOutput, - "vSize": txn["vSize"], - "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), - }; - return transactionObject; - } - - final int vSizeForOneOutput = (await buildTransaction( - utxoSigningData: utxoSigningData, - recipients: [recipientAddress], - satoshiAmounts: [satoshisBeingUsed - 1], - ))["vSize"] as int; - final int vSizeForTwoOutPuts = (await buildTransaction( - utxoSigningData: utxoSigningData, - recipients: [ - recipientAddress, - await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)), - ], - satoshiAmounts: [ - satoshiAmountToSend, - satoshisBeingUsed - satoshiAmountToSend - 1 - ], // dust limit is the minimum amount a change output should be - ))["vSize"] as int; - - // Assume 1 output, only for recipient and no change - final feeForOneOutput = satsPerVByte != null - ? (satsPerVByte * vSizeForOneOutput) - : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: selectedTxFeeRate, - ); - // Assume 2 outputs, one for recipient and one for change - final feeForTwoOutputs = satsPerVByte != null - ? (satsPerVByte * vSizeForTwoOutPuts) - : estimateTxFee( - vSize: vSizeForTwoOutPuts, - feeRatePerKB: selectedTxFeeRate, - ); - - Logging.instance - .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); - Logging.instance - .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); - - if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { - if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT.raw.toInt()) { - // Here, we know that theoretically, we may be able to include another output(change) but we first need to - // factor in the value of this output in satoshis. - int changeOutputSize = - satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; - // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and - // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new - // change address. - if (changeOutputSize > DUST_LIMIT.raw.toInt() && - satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == - feeForTwoOutputs) { - // generate new change address if current change address has been used - await _checkChangeAddressForTransactions(); - final String newChangeAddress = await _getCurrentAddressForChain( - 1, DerivePathTypeExt.primaryFor(coin)); - - int feeBeingPaid = - satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; - - recipientsArray.add(newChangeAddress); - recipientsAmtArray.add(changeOutputSize); - // At this point, we have the outputs we're going to use, the amounts to send along with which addresses - // we intend to send these amounts to. We have enough to send instructions to build the transaction. - Logging.instance.log('2 outputs in tx', level: LogLevel.Info); - Logging.instance - .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); - Logging.instance.log('Recipient output size: $satoshiAmountToSend', - level: LogLevel.Info); - Logging.instance.log('Change Output Size: $changeOutputSize', - level: LogLevel.Info); - Logging.instance.log( - 'Difference (fee being paid): $feeBeingPaid sats', - level: LogLevel.Info); - Logging.instance - .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); - dynamic txn = await buildTransaction( - utxoSigningData: utxoSigningData, - recipients: recipientsArray, - satoshiAmounts: recipientsAmtArray, - ); - - // make sure minimum fee is accurate if that is being used - if (txn["vSize"] - feeBeingPaid == 1) { - int changeOutputSize = - satoshisBeingUsed - satoshiAmountToSend - (txn["vSize"] as int); - feeBeingPaid = - satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; - recipientsAmtArray.removeLast(); - recipientsAmtArray.add(changeOutputSize); - Logging.instance.log('Adjusted Input size: $satoshisBeingUsed', - level: LogLevel.Info); - Logging.instance.log( - 'Adjusted Recipient output size: $satoshiAmountToSend', - level: LogLevel.Info); - Logging.instance.log( - 'Adjusted Change Output Size: $changeOutputSize', - level: LogLevel.Info); - Logging.instance.log( - 'Adjusted Difference (fee being paid): $feeBeingPaid sats', - level: LogLevel.Info); - Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', - level: LogLevel.Info); - txn = await buildTransaction( - utxoSigningData: utxoSigningData, - recipients: recipientsArray, - satoshiAmounts: recipientsAmtArray, - ); - } - - Map transactionObject = { - "hex": txn["hex"], - "recipient": recipientsArray[0], - "recipientAmt": Amount( - rawValue: BigInt.from(recipientsAmtArray[0]), - fractionDigits: coin.decimals, - ), - "fee": feeBeingPaid, - "vSize": txn["vSize"], - "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), - }; - return transactionObject; - } else { - // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize - // is smaller than or equal to DUST_LIMIT. Revert to single output transaction. - Logging.instance.log('1 output in tx', level: LogLevel.Info); - Logging.instance - .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); - Logging.instance.log('Recipient output size: $satoshiAmountToSend', - level: LogLevel.Info); - Logging.instance.log( - 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', - level: LogLevel.Info); - Logging.instance - .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); - dynamic txn = await buildTransaction( - utxoSigningData: utxoSigningData, - recipients: recipientsArray, - satoshiAmounts: recipientsAmtArray, - ); - Map transactionObject = { - "hex": txn["hex"], - "recipient": recipientsArray[0], - "recipientAmt": Amount( - rawValue: BigInt.from(recipientsAmtArray[0]), - fractionDigits: coin.decimals, - ), - "fee": satoshisBeingUsed - satoshiAmountToSend, - "vSize": txn["vSize"], - "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), - }; - return transactionObject; - } - } else { - // No additional outputs needed since adding one would mean that it'd be smaller than DUST_LIMIT sats - // which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct - // the wallet to begin crafting the transaction that the user requested. - Logging.instance.log('1 output in tx', level: LogLevel.Info); - Logging.instance - .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); - Logging.instance.log('Recipient output size: $satoshiAmountToSend', - level: LogLevel.Info); - Logging.instance.log( - 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', - level: LogLevel.Info); - Logging.instance - .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); - dynamic txn = await buildTransaction( - utxoSigningData: utxoSigningData, - recipients: recipientsArray, - satoshiAmounts: recipientsAmtArray, - ); - Map transactionObject = { - "hex": txn["hex"], - "recipient": recipientsArray[0], - "recipientAmt": Amount( - rawValue: BigInt.from(recipientsAmtArray[0]), - fractionDigits: coin.decimals, - ), - "fee": satoshisBeingUsed - satoshiAmountToSend, - "vSize": txn["vSize"], - "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), - }; - return transactionObject; - } - } else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) { - // In this scenario, no additional change output is needed since inputs - outputs equal exactly - // what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin - // crafting the transaction that the user requested. - Logging.instance.log('1 output in tx', level: LogLevel.Info); - Logging.instance - .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); - Logging.instance.log('Recipient output size: $satoshiAmountToSend', - level: LogLevel.Info); - Logging.instance.log( - 'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats', - level: LogLevel.Info); - Logging.instance - .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); - dynamic txn = await buildTransaction( - utxoSigningData: utxoSigningData, - recipients: recipientsArray, - satoshiAmounts: recipientsAmtArray, - ); - Map transactionObject = { - "hex": txn["hex"], - "recipient": recipientsArray[0], - "recipientAmt": Amount( - rawValue: BigInt.from(recipientsAmtArray[0]), - fractionDigits: coin.decimals, - ), - "fee": feeForOneOutput, - "vSize": txn["vSize"], - "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), - }; - return transactionObject; - } else { - // Remember that returning 2 indicates that the user does not have a sufficient balance to - // pay for the transaction fee. Ideally, at this stage, we should check if the user has any - // additional outputs they're able to spend and then recalculate fees. - Logging.instance.log( - 'Cannot pay tx fee - checking for more outputs and trying again', - level: LogLevel.Warning); - // try adding more outputs - if (spendableOutputs.length > inputsBeingConsumed) { - return coinSelection( - satoshiAmountToSend: satoshiAmountToSend, - selectedTxFeeRate: selectedTxFeeRate, - satsPerVByte: satsPerVByte, - recipientAddress: recipientAddress, - isSendAll: isSendAll, - additionalOutputs: additionalOutputs + 1, - utxos: utxos, - coinControl: coinControl, - ); - } - return 2; - } - } - - Future> fetchBuildTxData( - List utxosToUse, - ) async { - // return data - List signingData = []; - - try { - // Populating the addresses to check - for (var i = 0; i < utxosToUse.length; i++) { - if (utxosToUse[i].address == null) { - final txid = utxosToUse[i].txid; - final tx = await _cachedElectrumXClient.getTransaction( - txHash: txid, - coin: coin, - ); - for (final output in tx["vout"] as List) { - final n = output["n"]; - if (n != null && n == utxosToUse[i].vout) { - utxosToUse[i] = utxosToUse[i].copyWith( - address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]["address"] as String, - ); - } - } - } - - final derivePathType = addressType(address: utxosToUse[i].address!); - - signingData.add( - SigningData( - derivePathType: derivePathType, - utxo: utxosToUse[i], - ), - ); - } - - Map> receiveDerivations = {}; - Map> changeDerivations = {}; - - for (final sd in signingData) { - String? pubKey; - String? wif; - - // fetch receiving derivations if null - receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( - chain: 0, - derivePathType: sd.derivePathType, - ); - final receiveDerivation = - receiveDerivations[sd.derivePathType]![sd.utxo.address!]; - - if (receiveDerivation != null) { - pubKey = receiveDerivation["pubKey"] as String; - wif = receiveDerivation["wif"] as String; - } else { - // fetch change derivations if null - changeDerivations[sd.derivePathType] ??= await _fetchDerivations( - chain: 1, - derivePathType: sd.derivePathType, - ); - final changeDerivation = - changeDerivations[sd.derivePathType]![sd.utxo.address!]; - if (changeDerivation != null) { - pubKey = changeDerivation["pubKey"] as String; - wif = changeDerivation["wif"] as String; - } - } - - if (wif == null || pubKey == null) { - final address = await db.getAddress(walletId, sd.utxo.address!); - if (address?.derivationPath != null) { - final node = await Bip32Utils.getBip32Node( - (await mnemonicString)!, - (await mnemonicPassphrase)!, - _network, - address!.derivationPath!.value, - ); - - wif = node.toWIF(); - pubKey = Format.uint8listToString(node.publicKey); - } - } - - if (wif != null && pubKey != null) { - final PaymentData data; - final Uint8List? redeemScript; - - switch (sd.derivePathType) { - case DerivePathType.bip44: - data = P2PKH( - data: PaymentData( - pubkey: Format.stringToUint8List(pubKey), - ), - network: _network, - ).data; - redeemScript = null; - break; - - case DerivePathType.bip49: - final p2wpkh = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List(pubKey), - ), - network: _network, - overridePrefix: _network.bech32!, - ).data; - redeemScript = p2wpkh.output; - data = P2SH( - data: PaymentData(redeem: p2wpkh), - network: _network, - ).data; - break; - - case DerivePathType.bip84: - data = P2WPKH( - data: PaymentData( - pubkey: Format.stringToUint8List(pubKey), - ), - network: _network, - overridePrefix: _network.bech32!, - ).data; - redeemScript = null; - break; - - default: - throw Exception("DerivePathType unsupported"); - } - - final keyPair = ECPair.fromWIF( - wif, - network: _network, - ); - - sd.redeemScript = redeemScript; - sd.output = data.output; - sd.keyPair = keyPair; - } - } - - return signingData; - } catch (e, s) { - Logging.instance - .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); - rethrow; - } - } - - /// Builds and signs a transaction - Future> buildTransaction({ - required List utxoSigningData, - required List recipients, - required List satoshiAmounts, - }) async { - Logging.instance - .log("Starting buildTransaction ----------", level: LogLevel.Info); - - final txb = TransactionBuilder(network: _network); - txb.setVersion(1); - - // Add transaction inputs - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; - txb.addInput( - txid, - utxoSigningData[i].utxo.vout, - null, - utxoSigningData[i].output!, - _network.bech32!, - ); - } - - // Add transaction output - for (var i = 0; i < recipients.length; i++) { - txb.addOutput(recipients[i], satoshiAmounts[i], _network.bech32!); - } - - try { - // Sign the transaction accordingly - for (var i = 0; i < utxoSigningData.length; i++) { - txb.sign( - vin: i, - keyPair: utxoSigningData[i].keyPair!, - witnessValue: utxoSigningData[i].utxo.value, - redeemScript: utxoSigningData[i].redeemScript, - overridePrefix: _network.bech32!, - ); - } - } catch (e, s) { - Logging.instance.log("Caught exception while signing transaction: $e\n$s", - level: LogLevel.Error); - rethrow; - } - - final builtTx = txb.build(_network.bech32!); - final vSize = builtTx.virtualSize(); - - return {"hex": builtTx.toHex(), "vSize": vSize}; - } - - @override - Future fullRescan( - int maxUnusedAddressGap, - int maxNumberOfIndexesToCheck, - ) async { - Logging.instance.log("Starting full rescan!", level: LogLevel.Info); - longMutex = true; - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - // clear cache - await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); - - // back up data - // await _rescanBackup(); - - // clear blockchain info - await db.deleteWalletBlockchainData(walletId); - await _deleteDerivations(); - - try { - final _mnemonic = await mnemonicString; - final _mnemonicPassphrase = await mnemonicPassphrase; - if (_mnemonicPassphrase == null) { - Logging.instance.log( - "Exception in fullRescan: mnemonic passphrase null, possible migration issue; if using internal builds, delete wallet and restore from seed, if using a release build, please file bug report", - level: LogLevel.Error); - } - - await _recoverWalletFromBIP32SeedPhrase( - mnemonic: _mnemonic!, - mnemonicPassphrase: _mnemonicPassphrase!, - maxUnusedAddressGap: maxUnusedAddressGap, - maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, - isRescan: true, - ); - - longMutex = false; - await refresh(); - Logging.instance.log("Full rescan complete!", level: LogLevel.Info); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - } catch (e, s) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - - // restore from backup - // await _rescanRestore(); - - longMutex = false; - Logging.instance.log("Exception rethrown from fullRescan(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future _deleteDerivations() async { - // P2PKH derivations - await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH"); - await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH"); - - // P2SH derivations - await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH"); - await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH"); - - // P2WPKH derivations - await _secureStore.delete(key: "${walletId}_receiveDerivationsP2WPKH"); - await _secureStore.delete(key: "${walletId}_changeDerivationsP2WPKH"); - } - - // Future _rescanRestore() async { - // Logging.instance.log("starting rescan restore", level: LogLevel.Info); - // - // // restore from backup - // // p2pkh - // final tempReceivingAddressesP2PKH = DB.instance - // .get(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP'); - // final tempChangeAddressesP2PKH = DB.instance - // .get(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP'); - // final tempReceivingIndexP2PKH = DB.instance - // .get(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP'); - // final tempChangeIndexP2PKH = DB.instance - // .get(boxName: walletId, key: 'changeIndexP2PKH_BACKUP'); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingAddressesP2PKH', - // value: tempReceivingAddressesP2PKH); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeAddressesP2PKH', - // value: tempChangeAddressesP2PKH); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingIndexP2PKH', - // value: tempReceivingIndexP2PKH); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeIndexP2PKH', - // value: tempChangeIndexP2PKH); - // await DB.instance.delete( - // key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId); - // await DB.instance - // .delete(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId); - // await DB.instance - // .delete(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId); - // await DB.instance - // .delete(key: 'changeIndexP2PKH_BACKUP', boxName: walletId); - // - // // p2Sh - // final tempReceivingAddressesP2SH = DB.instance - // .get(boxName: walletId, key: 'receivingAddressesP2SH_BACKUP'); - // final tempChangeAddressesP2SH = DB.instance - // .get(boxName: walletId, key: 'changeAddressesP2SH_BACKUP'); - // final tempReceivingIndexP2SH = DB.instance - // .get(boxName: walletId, key: 'receivingIndexP2SH_BACKUP'); - // final tempChangeIndexP2SH = DB.instance - // .get(boxName: walletId, key: 'changeIndexP2SH_BACKUP'); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingAddressesP2SH', - // value: tempReceivingAddressesP2SH); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeAddressesP2SH', - // value: tempChangeAddressesP2SH); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingIndexP2SH', - // value: tempReceivingIndexP2SH); - // await DB.instance.put( - // boxName: walletId, key: 'changeIndexP2SH', value: tempChangeIndexP2SH); - // await DB.instance.delete( - // key: 'receivingAddressesP2SH_BACKUP', boxName: walletId); - // await DB.instance - // .delete(key: 'changeAddressesP2SH_BACKUP', boxName: walletId); - // await DB.instance - // .delete(key: 'receivingIndexP2SH_BACKUP', boxName: walletId); - // await DB.instance - // .delete(key: 'changeIndexP2SH_BACKUP', boxName: walletId); - // - // // p2wpkh - // final tempReceivingAddressesP2WPKH = DB.instance.get( - // boxName: walletId, key: 'receivingAddressesP2WPKH_BACKUP'); - // final tempChangeAddressesP2WPKH = DB.instance - // .get(boxName: walletId, key: 'changeAddressesP2WPKH_BACKUP'); - // final tempReceivingIndexP2WPKH = DB.instance - // .get(boxName: walletId, key: 'receivingIndexP2WPKH_BACKUP'); - // final tempChangeIndexP2WPKH = DB.instance - // .get(boxName: walletId, key: 'changeIndexP2WPKH_BACKUP'); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingAddressesP2WPKH', - // value: tempReceivingAddressesP2WPKH); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeAddressesP2WPKH', - // value: tempChangeAddressesP2WPKH); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingIndexP2WPKH', - // value: tempReceivingIndexP2WPKH); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeIndexP2WPKH', - // value: tempChangeIndexP2WPKH); - // await DB.instance.delete( - // key: 'receivingAddressesP2WPKH_BACKUP', boxName: walletId); - // await DB.instance.delete( - // key: 'changeAddressesP2WPKH_BACKUP', boxName: walletId); - // await DB.instance - // .delete(key: 'receivingIndexP2WPKH_BACKUP', boxName: walletId); - // await DB.instance - // .delete(key: 'changeIndexP2WPKH_BACKUP', boxName: walletId); - // - // // P2PKH derivations - // final p2pkhReceiveDerivationsString = await _secureStore.read( - // key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); - // final p2pkhChangeDerivationsString = await _secureStore.read( - // key: "${walletId}_changeDerivationsP2PKH_BACKUP"); - // - // await _secureStore.write( - // key: "${walletId}_receiveDerivationsP2PKH", - // value: p2pkhReceiveDerivationsString); - // await _secureStore.write( - // key: "${walletId}_changeDerivationsP2PKH", - // value: p2pkhChangeDerivationsString); - // - // await _secureStore.delete( - // key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); - // await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH_BACKUP"); - // - // // P2SH derivations - // final p2shReceiveDerivationsString = await _secureStore.read( - // key: "${walletId}_receiveDerivationsP2SH_BACKUP"); - // final p2shChangeDerivationsString = await _secureStore.read( - // key: "${walletId}_changeDerivationsP2SH_BACKUP"); - // - // await _secureStore.write( - // key: "${walletId}_receiveDerivationsP2SH", - // value: p2shReceiveDerivationsString); - // await _secureStore.write( - // key: "${walletId}_changeDerivationsP2SH", - // value: p2shChangeDerivationsString); - // - // await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH_BACKUP"); - // await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH_BACKUP"); - // - // // P2WPKH derivations - // final p2wpkhReceiveDerivationsString = await _secureStore.read( - // key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); - // final p2wpkhChangeDerivationsString = await _secureStore.read( - // key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); - // - // await _secureStore.write( - // key: "${walletId}_receiveDerivationsP2WPKH", - // value: p2wpkhReceiveDerivationsString); - // await _secureStore.write( - // key: "${walletId}_changeDerivationsP2WPKH", - // value: p2wpkhChangeDerivationsString); - // - // await _secureStore.delete( - // key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); - // await _secureStore.delete( - // key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); - // - // // UTXOs - // final utxoData = DB.instance - // .get(boxName: walletId, key: 'latest_utxo_model_BACKUP'); - // await DB.instance.put( - // boxName: walletId, key: 'latest_utxo_model', value: utxoData); - // await DB.instance - // .delete(key: 'latest_utxo_model_BACKUP', boxName: walletId); - // - // Logging.instance.log("rescan restore complete", level: LogLevel.Info); - // } - // - // Future _rescanBackup() async { - // Logging.instance.log("starting rescan backup", level: LogLevel.Info); - // - // // backup current and clear data - // // p2pkh - // final tempReceivingAddressesP2PKH = DB.instance - // .get(boxName: walletId, key: 'receivingAddressesP2PKH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingAddressesP2PKH_BACKUP', - // value: tempReceivingAddressesP2PKH); - // await DB.instance - // .delete(key: 'receivingAddressesP2PKH', boxName: walletId); - // - // final tempChangeAddressesP2PKH = DB.instance - // .get(boxName: walletId, key: 'changeAddressesP2PKH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeAddressesP2PKH_BACKUP', - // value: tempChangeAddressesP2PKH); - // await DB.instance - // .delete(key: 'changeAddressesP2PKH', boxName: walletId); - // - // final tempReceivingIndexP2PKH = - // DB.instance.get(boxName: walletId, key: 'receivingIndexP2PKH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingIndexP2PKH_BACKUP', - // value: tempReceivingIndexP2PKH); - // await DB.instance - // .delete(key: 'receivingIndexP2PKH', boxName: walletId); - // - // final tempChangeIndexP2PKH = - // DB.instance.get(boxName: walletId, key: 'changeIndexP2PKH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeIndexP2PKH_BACKUP', - // value: tempChangeIndexP2PKH); - // await DB.instance - // .delete(key: 'changeIndexP2PKH', boxName: walletId); - // - // // p2sh - // final tempReceivingAddressesP2SH = DB.instance - // .get(boxName: walletId, key: 'receivingAddressesP2SH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingAddressesP2SH_BACKUP', - // value: tempReceivingAddressesP2SH); - // await DB.instance - // .delete(key: 'receivingAddressesP2SH', boxName: walletId); - // - // final tempChangeAddressesP2SH = - // DB.instance.get(boxName: walletId, key: 'changeAddressesP2SH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeAddressesP2SH_BACKUP', - // value: tempChangeAddressesP2SH); - // await DB.instance - // .delete(key: 'changeAddressesP2SH', boxName: walletId); - // - // final tempReceivingIndexP2SH = - // DB.instance.get(boxName: walletId, key: 'receivingIndexP2SH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingIndexP2SH_BACKUP', - // value: tempReceivingIndexP2SH); - // await DB.instance - // .delete(key: 'receivingIndexP2SH', boxName: walletId); - // - // final tempChangeIndexP2SH = - // DB.instance.get(boxName: walletId, key: 'changeIndexP2SH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeIndexP2SH_BACKUP', - // value: tempChangeIndexP2SH); - // await DB.instance - // .delete(key: 'changeIndexP2SH', boxName: walletId); - // - // // p2wpkh - // final tempReceivingAddressesP2WPKH = DB.instance - // .get(boxName: walletId, key: 'receivingAddressesP2WPKH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingAddressesP2WPKH_BACKUP', - // value: tempReceivingAddressesP2WPKH); - // await DB.instance - // .delete(key: 'receivingAddressesP2WPKH', boxName: walletId); - // - // final tempChangeAddressesP2WPKH = DB.instance - // .get(boxName: walletId, key: 'changeAddressesP2WPKH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeAddressesP2WPKH_BACKUP', - // value: tempChangeAddressesP2WPKH); - // await DB.instance - // .delete(key: 'changeAddressesP2WPKH', boxName: walletId); - // - // final tempReceivingIndexP2WPKH = DB.instance - // .get(boxName: walletId, key: 'receivingIndexP2WPKH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'receivingIndexP2WPKH_BACKUP', - // value: tempReceivingIndexP2WPKH); - // await DB.instance - // .delete(key: 'receivingIndexP2WPKH', boxName: walletId); - // - // final tempChangeIndexP2WPKH = - // DB.instance.get(boxName: walletId, key: 'changeIndexP2WPKH'); - // await DB.instance.put( - // boxName: walletId, - // key: 'changeIndexP2WPKH_BACKUP', - // value: tempChangeIndexP2WPKH); - // await DB.instance - // .delete(key: 'changeIndexP2WPKH', boxName: walletId); - // - // // P2PKH derivations - // final p2pkhReceiveDerivationsString = - // await _secureStore.read(key: "${walletId}_receiveDerivationsP2PKH"); - // final p2pkhChangeDerivationsString = - // await _secureStore.read(key: "${walletId}_changeDerivationsP2PKH"); - // - // await _secureStore.write( - // key: "${walletId}_receiveDerivationsP2PKH_BACKUP", - // value: p2pkhReceiveDerivationsString); - // await _secureStore.write( - // key: "${walletId}_changeDerivationsP2PKH_BACKUP", - // value: p2pkhChangeDerivationsString); - // - // await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH"); - // await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH"); - // - // // P2SH derivations - // final p2shReceiveDerivationsString = - // await _secureStore.read(key: "${walletId}_receiveDerivationsP2SH"); - // final p2shChangeDerivationsString = - // await _secureStore.read(key: "${walletId}_changeDerivationsP2SH"); - // - // await _secureStore.write( - // key: "${walletId}_receiveDerivationsP2SH_BACKUP", - // value: p2shReceiveDerivationsString); - // await _secureStore.write( - // key: "${walletId}_changeDerivationsP2SH_BACKUP", - // value: p2shChangeDerivationsString); - // - // await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH"); - // await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH"); - // - // // P2WPKH derivations - // final p2wpkhReceiveDerivationsString = - // await _secureStore.read(key: "${walletId}_receiveDerivationsP2WPKH"); - // final p2wpkhChangeDerivationsString = - // await _secureStore.read(key: "${walletId}_changeDerivationsP2WPKH"); - // - // await _secureStore.write( - // key: "${walletId}_receiveDerivationsP2WPKH_BACKUP", - // value: p2wpkhReceiveDerivationsString); - // await _secureStore.write( - // key: "${walletId}_changeDerivationsP2WPKH_BACKUP", - // value: p2wpkhChangeDerivationsString); - // - // await _secureStore.delete(key: "${walletId}_receiveDerivationsP2WPKH"); - // await _secureStore.delete(key: "${walletId}_changeDerivationsP2WPKH"); - // - // // UTXOs - // final utxoData = - // DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); - // await DB.instance.put( - // boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData); - // await DB.instance - // .delete(key: 'latest_utxo_model', boxName: walletId); - // - // Logging.instance.log("rescan backup complete", level: LogLevel.Info); - // } - - bool isActive = false; - - @override - void Function(bool)? get onIsActiveWalletChanged => - (isActive) => this.isActive = isActive; - - @override - Future estimateFeeFor(Amount amount, int feeRate) async { - final available = balance.spendable; - - if (available == amount) { - return amount - (await sweepAllEstimate(feeRate)); - } else if (amount <= Amount.zero || amount > available) { - return roughFeeEstimate(1, 2, feeRate); - } - - Amount runningBalance = Amount( - rawValue: BigInt.zero, - fractionDigits: coin.decimals, - ); - int inputCount = 0; - for (final output in (await utxos)) { - if (!output.isBlocked) { - runningBalance = runningBalance + - Amount( - rawValue: BigInt.from(output.value), - fractionDigits: coin.decimals, - ); - inputCount++; - if (runningBalance > amount) { - break; - } - } - } - - final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); - final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - - if (runningBalance - amount > oneOutPutFee) { - if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - amount - twoOutPutFee; - if (change > DUST_LIMIT && - runningBalance - amount - change == twoOutPutFee) { - return runningBalance - amount - change; - } else { - return runningBalance - amount; - } - } else { - return runningBalance - amount; - } - } else if (runningBalance - amount == oneOutPutFee) { - return oneOutPutFee; - } else { - return twoOutPutFee; - } - } - - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return Amount( - rawValue: BigInt.from( - ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil()), - fractionDigits: coin.decimals, - ); - } - - Future sweepAllEstimate(int feeRate) async { - int available = 0; - int inputCount = 0; - for (final output in (await utxos)) { - if (!output.isBlocked && - output.isConfirmed(storedChainHeight, MINIMUM_CONFIRMATIONS)) { - available += output.value; - inputCount++; - } - } - - // transaction will only have 1 output minus the fee - final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - - return Amount( - rawValue: BigInt.from(available), - fractionDigits: coin.decimals, - ) - - estimatedFee; - } - - @override - Future generateNewAddress() async { - try { - final currentReceiving = await _currentReceivingAddress; - - final newReceivingIndex = currentReceiving.derivationIndex + 1; - - // Use new index to derive a new receiving address - final newReceivingAddress = await _generateAddressForChain( - 0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin)); - - // Add that new receiving address - await db.putAddress(newReceivingAddress); - - return true; - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from generateNewAddress(): $e\n$s", - level: LogLevel.Error); - return false; - } - } - - @override - Future get xpub async { - final node = await Bip32Utils.getBip32Root( - (await mnemonic).join(" "), - await mnemonicPassphrase ?? "", - _network, - ); - - return node.neutered().toBase58(); - } -} - -final litecoin = NetworkType( - messagePrefix: '\x19Litecoin Signed Message:\n', - bech32: 'ltc', - bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), - pubKeyHash: 0x30, - scriptHash: 0x32, - wif: 0xb0); - -final litecointestnet = NetworkType( - messagePrefix: '\x19Litecoin Signed Message:\n', - bech32: 'tltc', - bip32: Bip32Type(public: 0x043587cf, private: 0x04358394), - pubKeyHash: 0x6f, - scriptHash: 0x3a, - wif: 0xef); +// /* +// * This file is part of Stack Wallet. +// * +// * Copyright (c) 2023 Cypher Stack +// * All Rights Reserved. +// * The code is distributed under GPLv3 license, see LICENSE file for details. +// * Generated by Cypher Stack on 2023-05-26 +// * +// */ +// +// import 'dart:async'; +// import 'dart:convert'; +// import 'dart:io'; +// +// import 'package:bech32/bech32.dart'; +// import 'package:bip32/bip32.dart' as bip32; +// import 'package:bip39/bip39.dart' as bip39; +// import 'package:bitcoindart/bitcoindart.dart'; +// import 'package:bs58check/bs58check.dart' as bs58check; +// import 'package:crypto/crypto.dart'; +// import 'package:decimal/decimal.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:isar/isar.dart'; +// import 'package:stackwallet/db/isar/main_db.dart'; +// import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart'; +// import 'package:stackwallet/electrumx_rpc/electrumx_client.dart'; +// import 'package:stackwallet/models/balance.dart'; +// import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; +// import 'package:stackwallet/models/paymint/fee_object_model.dart'; +// import 'package:stackwallet/models/signing_data.dart'; +// import 'package:stackwallet/services/coins/coin_service.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/updated_in_background_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/mixins/coin_control_interface.dart'; +// import 'package:stackwallet/services/mixins/electrum_x_parsing.dart'; +// import 'package:stackwallet/services/mixins/ordinals_interface.dart'; +// import 'package:stackwallet/services/mixins/wallet_cache.dart'; +// import 'package:stackwallet/services/mixins/wallet_db.dart'; +// import 'package:stackwallet/services/mixins/xpubable.dart'; +// import 'package:stackwallet/services/node_service.dart'; +// import 'package:stackwallet/services/transaction_notification_tracker.dart'; +// import 'package:stackwallet/utilities/amount/amount.dart'; +// import 'package:stackwallet/utilities/bip32_utils.dart'; +// import 'package:stackwallet/utilities/constants.dart'; +// import 'package:stackwallet/utilities/default_nodes.dart'; +// import 'package:stackwallet/utilities/enums/coin_enum.dart'; +// import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; +// import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +// import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +// import 'package:stackwallet/utilities/format.dart'; +// import 'package:stackwallet/utilities/logger.dart'; +// import 'package:stackwallet/utilities/prefs.dart'; +// import 'package:stackwallet/widgets/crypto_notifications.dart'; +// import 'package:tuple/tuple.dart'; +// import 'package:uuid/uuid.dart'; +// // +// // const int MINIMUM_CONFIRMATIONS = 1; +// // final Amount DUST_LIMIT = Amount( +// // rawValue: BigInt.from(294), +// // fractionDigits: Coin.particl.decimals, +// // ); +// // final Amount DUST_LIMIT_P2PKH = Amount( +// // rawValue: BigInt.from(546), +// // fractionDigits: Coin.particl.decimals, +// // ); +// // +// // const String GENESIS_HASH_MAINNET = +// // "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2"; +// // const String GENESIS_HASH_TESTNET = +// // "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0"; +// // +// // String constructDerivePath({ +// // required DerivePathType derivePathType, +// // required int networkWIF, +// // int account = 0, +// // required int chain, +// // required int index, +// // }) { +// // String coinType; +// // switch (networkWIF) { +// // case 0xb0: // ltc mainnet wif +// // coinType = "2"; // ltc mainnet +// // break; +// // case 0xef: // ltc testnet wif +// // coinType = "1"; // ltc testnet +// // break; +// // default: +// // throw Exception("Invalid Litecoin network wif used!"); +// // } +// // +// // int purpose; +// // switch (derivePathType) { +// // case DerivePathType.bip44: +// // purpose = 44; +// // break; +// // case DerivePathType.bip49: +// // purpose = 49; +// // break; +// // case DerivePathType.bip84: +// // purpose = 84; +// // break; +// // default: +// // throw Exception("DerivePathType $derivePathType not supported"); +// // } +// // +// // return "m/$purpose'/$coinType'/$account'/$chain/$index"; +// // } +// +// class LitecoinWallet extends CoinServiceAPI +// with +// WalletCache, +// WalletDB, +// ElectrumXParsing, +// CoinControlInterface, +// OrdinalsInterface +// implements XPubAble { +// LitecoinWallet({ +// required String walletId, +// required String walletName, +// required Coin coin, +// required ElectrumXClient client, +// required CachedElectrumXClient cachedClient, +// required TransactionNotificationTracker tracker, +// required SecureStorageInterface secureStore, +// MainDB? mockableOverride, +// }) { +// txTracker = tracker; +// _walletId = walletId; +// _walletName = walletName; +// _coin = coin; +// _electrumXClient = client; +// _cachedElectrumXClient = cachedClient; +// _secureStore = secureStore; +// initCache(walletId, coin); +// initWalletDB(mockableOverride: mockableOverride); +// initOrdinalsInterface(walletId: walletId, coin: coin, db: db); +// initCoinControlInterface( +// walletId: walletId, +// walletName: walletName, +// coin: coin, +// db: db, +// getChainHeight: () => chainHeight, +// refreshedBalanceCallback: (balance) async { +// _balance = balance; +// await updateCachedBalance(_balance!); +// }, +// ); +// } +// // +// // static const integrationTestFlag = +// // bool.fromEnvironment("IS_INTEGRATION_TEST"); +// // +// // final _prefs = Prefs.instance; +// // +// // Timer? timer; +// // late final Coin _coin; +// // +// // late final TransactionNotificationTracker txTracker; +// // +// // NetworkType get _network { +// // switch (coin) { +// // case Coin.litecoin: +// // return litecoin; +// // case Coin.litecoinTestNet: +// // return litecointestnet; +// // default: +// // throw Exception("Invalid network type!"); +// // } +// // } +// // +// // @override +// // set isFavorite(bool markFavorite) { +// // _isFavorite = markFavorite; +// // updateCachedIsFavorite(markFavorite); +// // } +// // +// // @override +// // bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); +// // +// // bool? _isFavorite; +// // +// // @override +// // Coin get coin => _coin; +// // +// // @override +// // Future> get utxos => db.getUTXOs(walletId).findAll(); +// // +// // @override +// // Future> get transactions => +// // db.getTransactions(walletId).sortByTimestampDesc().findAll(); +// // +// // @override +// // Future get currentReceivingAddress async => +// // (await _currentReceivingAddress).value; +// // +// // Future get _currentReceivingAddress async => +// // (await db +// // .getAddresses(walletId) +// // .filter() +// // .typeEqualTo(isar_models.AddressType.p2wpkh) +// // .subTypeEqualTo(isar_models.AddressSubType.receiving) +// // .sortByDerivationIndexDesc() +// // .findFirst()) ?? +// // await _generateAddressForChain(0, 0, DerivePathTypeExt.primaryFor(coin)); +// // +// // Future get currentChangeAddress async => +// // (await _currentChangeAddress).value; +// // +// // Future get _currentChangeAddress async => +// // (await db +// // .getAddresses(walletId) +// // .filter() +// // .typeEqualTo(isar_models.AddressType.p2wpkh) +// // .subTypeEqualTo(isar_models.AddressSubType.change) +// // .sortByDerivationIndexDesc() +// // .findFirst()) ?? +// // await _generateAddressForChain(1, 0, DerivePathTypeExt.primaryFor(coin)); +// // +// // @override +// // Future exit() async { +// // _hasCalledExit = true; +// // timer?.cancel(); +// // timer = null; +// // stopNetworkAlivePinging(); +// // } +// // +// // bool _hasCalledExit = false; +// // +// // @override +// // bool get hasCalledExit => _hasCalledExit; +// // +// // @override +// // Future get fees => _feeObject ??= _getFees(); +// // Future? _feeObject; +// // +// // @override +// // Future get maxFee async { +// // final fee = (await fees).fast as String; +// // final satsFee = Decimal.parse(fee) * +// // Decimal.fromInt(Constants.satsPerCoin(coin).toInt()); +// // return satsFee.floor().toBigInt().toInt(); +// // } +// // +// // @override +// // Future> get mnemonic => _getMnemonicList(); +// // +// // @override +// // Future get mnemonicString => +// // _secureStore.read(key: '${_walletId}_mnemonic'); +// // +// // @override +// // Future get mnemonicPassphrase => _secureStore.read( +// // key: '${_walletId}_mnemonicPassphrase', +// // ); +// // +// // Future get chainHeight async { +// // try { +// // final result = await _electrumXClient.getBlockHeadTip(); +// // final height = result["height"] as int; +// // await updateCachedChainHeight(height); +// // if (height > storedChainHeight) { +// // GlobalEventBus.instance.fire( +// // UpdatedInBackgroundEvent( +// // "Updated current chain height in $walletId $walletName!", +// // walletId, +// // ), +// // ); +// // } +// // return height; +// // } catch (e, s) { +// // Logging.instance.log("Exception caught in chainHeight: $e\n$s", +// // level: LogLevel.Error); +// // return storedChainHeight; +// // } +// // } +// // +// // @override +// // int get storedChainHeight => getCachedChainHeight(); +// // +// // 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] == _network.pubKeyHash) { +// // // P2PKH +// // return DerivePathType.bip44; +// // } +// // if (decodeBase58[0] == _network.scriptHash) { +// // // P2SH +// // return DerivePathType.bip49; +// // } +// // throw ArgumentError('Invalid version or Network mismatch'); +// // } else { +// // try { +// // decodeBech32 = segwit.decode(address, _network.bech32!); +// // } catch (err) { +// // // Bech32 decode fail +// // } +// // if (_network.bech32 != decodeBech32!.hrp) { +// // throw ArgumentError('Invalid prefix or Network mismatch'); +// // } +// // if (decodeBech32.version != 0) { +// // throw ArgumentError('Invalid address version'); +// // } +// // // P2WPKH +// // return DerivePathType.bip84; +// // } +// // } +// // +// // bool longMutex = false; +// // +// // @override +// // Future recoverFromMnemonic({ +// // required String mnemonic, +// // String? mnemonicPassphrase, +// // required int maxUnusedAddressGap, +// // required int maxNumberOfIndexesToCheck, +// // required int height, +// // }) async { +// // longMutex = true; +// // final start = DateTime.now(); +// // try { +// // Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag", +// // level: LogLevel.Info); +// // if (!integrationTestFlag) { +// // final features = await electrumXClient.getServerFeatures(); +// // Logging.instance.log("features: $features", level: LogLevel.Info); +// // switch (coin) { +// // case Coin.litecoin: +// // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { +// // throw Exception("genesis hash does not match main net!"); +// // } +// // break; +// // case Coin.litecoinTestNet: +// // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { +// // throw Exception("genesis hash does not match test net!"); +// // } +// // break; +// // default: +// // throw Exception( +// // "Attempted to generate a LitecoinWallet using a non litecoin coin type: ${coin.name}"); +// // } +// // // if (_networkType == BasicNetworkType.main) { +// // // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { +// // // throw Exception("genesis hash does not match main net!"); +// // // } +// // // } else if (_networkType == BasicNetworkType.test) { +// // // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { +// // // throw Exception("genesis hash does not match test net!"); +// // // } +// // // } +// // } +// // // check to make sure we aren't overwriting a mnemonic +// // // this should never fail +// // if ((await mnemonicString) != null || +// // (await this.mnemonicPassphrase) != null) { +// // longMutex = false; +// // throw Exception("Attempted to overwrite mnemonic on restore!"); +// // } +// // await _secureStore.write( +// // key: '${_walletId}_mnemonic', value: mnemonic.trim()); +// // await _secureStore.write( +// // key: '${_walletId}_mnemonicPassphrase', +// // value: mnemonicPassphrase ?? "", +// // ); +// // await _recoverWalletFromBIP32SeedPhrase( +// // mnemonic: mnemonic.trim(), +// // mnemonicPassphrase: mnemonicPassphrase ?? "", +// // maxUnusedAddressGap: maxUnusedAddressGap, +// // maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, +// // ); +// // } catch (e, s) { +// // Logging.instance.log( +// // "Exception rethrown from recoverFromMnemonic(): $e\n$s", +// // level: LogLevel.Error); +// // longMutex = false; +// // rethrow; +// // } +// // longMutex = false; +// // +// // final end = DateTime.now(); +// // Logging.instance.log( +// // "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", +// // level: LogLevel.Info); +// // } +// // +// // Future> _checkGaps( +// // int maxNumberOfIndexesToCheck, +// // int maxUnusedAddressGap, +// // int txCountBatchSize, +// // bip32.BIP32 root, +// // DerivePathType type, +// // int chain) async { +// // List addressArray = []; +// // int returningIndex = -1; +// // Map> derivations = {}; +// // int gapCounter = 0; +// // for (int index = 0; +// // index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap; +// // index += txCountBatchSize) { +// // List iterationsAddressArray = []; +// // Logging.instance.log( +// // "index: $index, \t GapCounter $chain ${type.name}: $gapCounter", +// // level: LogLevel.Info); +// // +// // final _id = "k_$index"; +// // Map txCountCallArgs = {}; +// // final Map receivingNodes = {}; +// // +// // for (int j = 0; j < txCountBatchSize; j++) { +// // final derivePath = constructDerivePath( +// // derivePathType: type, +// // networkWIF: root.network.wif, +// // chain: chain, +// // index: index + j, +// // ); +// // final node = await Bip32Utils.getBip32NodeFromRoot(root, derivePath); +// // +// // String addressString; +// // final data = PaymentData(pubkey: node.publicKey); +// // isar_models.AddressType addrType; +// // switch (type) { +// // case DerivePathType.bip44: +// // addressString = P2PKH(data: data, network: _network).data.address!; +// // addrType = isar_models.AddressType.p2pkh; +// // break; +// // case DerivePathType.bip49: +// // addressString = P2SH( +// // data: PaymentData( +// // redeem: P2WPKH( +// // data: data, +// // network: _network, +// // overridePrefix: _network.bech32!) +// // .data), +// // network: _network) +// // .data +// // .address!; +// // addrType = isar_models.AddressType.p2sh; +// // break; +// // case DerivePathType.bip84: +// // addressString = P2WPKH( +// // network: _network, +// // data: data, +// // overridePrefix: _network.bech32!) +// // .data +// // .address!; +// // addrType = isar_models.AddressType.p2wpkh; +// // break; +// // default: +// // throw Exception("DerivePathType unsupported"); +// // } +// // +// // final address = isar_models.Address( +// // walletId: walletId, +// // value: addressString, +// // publicKey: node.publicKey, +// // type: addrType, +// // derivationIndex: index + j, +// // derivationPath: isar_models.DerivationPath()..value = derivePath, +// // subType: chain == 0 +// // ? isar_models.AddressSubType.receiving +// // : isar_models.AddressSubType.change, +// // ); +// // +// // receivingNodes.addAll({ +// // "${_id}_$j": { +// // "node": node, +// // "address": address, +// // } +// // }); +// // txCountCallArgs.addAll({ +// // "${_id}_$j": addressString, +// // }); +// // } +// // +// // // get address tx counts +// // final counts = await _getBatchTxCount(addresses: txCountCallArgs); +// // +// // // check and add appropriate addresses +// // for (int k = 0; k < txCountBatchSize; k++) { +// // int count = counts["${_id}_$k"]!; +// // if (count > 0) { +// // final node = receivingNodes["${_id}_$k"]; +// // final address = node["address"] as isar_models.Address; +// // // add address to array +// // addressArray.add(address); +// // iterationsAddressArray.add(address.value); +// // // set current index +// // returningIndex = index + k; +// // // reset counter +// // gapCounter = 0; +// // // add info to derivations +// // derivations[address.value] = { +// // "pubKey": Format.uint8listToString( +// // (node["node"] as bip32.BIP32).publicKey), +// // "wif": (node["node"] as bip32.BIP32).toWIF(), +// // }; +// // } +// // +// // // increase counter when no tx history found +// // if (count == 0) { +// // gapCounter++; +// // } +// // } +// // // cache all the transactions while waiting for the current function to finish. +// // unawaited(getTransactionCacheEarly(iterationsAddressArray)); +// // } +// // return { +// // "addressArray": addressArray, +// // "index": returningIndex, +// // "derivations": derivations +// // }; +// // } +// // +// // Future getTransactionCacheEarly(List allAddresses) async { +// // try { +// // final List> allTxHashes = +// // await _fetchHistory(allAddresses); +// // for (final txHash in allTxHashes) { +// // try { +// // unawaited(cachedElectrumXClient.getTransaction( +// // txHash: txHash["tx_hash"] as String, +// // verbose: true, +// // coin: coin, +// // )); +// // } catch (e) { +// // continue; +// // } +// // } +// // } catch (e) { +// // // +// // } +// // } +// // +// // Future _recoverWalletFromBIP32SeedPhrase({ +// // required String mnemonic, +// // required String mnemonicPassphrase, +// // int maxUnusedAddressGap = 20, +// // int maxNumberOfIndexesToCheck = 1000, +// // bool isRescan = false, +// // }) async { +// // longMutex = true; +// // +// // Map> p2pkhReceiveDerivations = {}; +// // Map> p2shReceiveDerivations = {}; +// // Map> p2wpkhReceiveDerivations = {}; +// // Map> p2pkhChangeDerivations = {}; +// // Map> p2shChangeDerivations = {}; +// // Map> p2wpkhChangeDerivations = {}; +// // +// // final root = await Bip32Utils.getBip32Root( +// // mnemonic, +// // mnemonicPassphrase, +// // _network, +// // ); +// // +// // List p2pkhReceiveAddressArray = []; +// // List p2shReceiveAddressArray = []; +// // List p2wpkhReceiveAddressArray = []; +// // int p2pkhReceiveIndex = -1; +// // int p2shReceiveIndex = -1; +// // int p2wpkhReceiveIndex = -1; +// // +// // List p2pkhChangeAddressArray = []; +// // List p2shChangeAddressArray = []; +// // List p2wpkhChangeAddressArray = []; +// // int p2pkhChangeIndex = -1; +// // int p2shChangeIndex = -1; +// // int p2wpkhChangeIndex = -1; +// // +// // // actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3 +// // const txCountBatchSize = 12; +// // +// // try { +// // // receiving addresses +// // Logging.instance +// // .log("checking receiving addresses...", level: LogLevel.Info); +// // final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck, +// // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0); +// // +// // final resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck, +// // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 0); +// // +// // final resultReceive84 = _checkGaps(maxNumberOfIndexesToCheck, +// // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 0); +// // +// // Logging.instance +// // .log("checking change addresses...", level: LogLevel.Info); +// // // change addresses +// // final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck, +// // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1); +// // +// // final resultChange49 = _checkGaps(maxNumberOfIndexesToCheck, +// // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 1); +// // +// // final resultChange84 = _checkGaps(maxNumberOfIndexesToCheck, +// // maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 1); +// // +// // await Future.wait([ +// // resultReceive44, +// // resultReceive49, +// // resultReceive84, +// // resultChange44, +// // resultChange49, +// // resultChange84 +// // ]); +// // +// // p2pkhReceiveAddressArray = +// // (await resultReceive44)['addressArray'] as List; +// // p2pkhReceiveIndex = (await resultReceive44)['index'] as int; +// // p2pkhReceiveDerivations = (await resultReceive44)['derivations'] +// // as Map>; +// // +// // p2shReceiveAddressArray = +// // (await resultReceive49)['addressArray'] as List; +// // p2shReceiveIndex = (await resultReceive49)['index'] as int; +// // p2shReceiveDerivations = (await resultReceive49)['derivations'] +// // as Map>; +// // +// // p2wpkhReceiveAddressArray = +// // (await resultReceive84)['addressArray'] as List; +// // p2wpkhReceiveIndex = (await resultReceive84)['index'] as int; +// // p2wpkhReceiveDerivations = (await resultReceive84)['derivations'] +// // as Map>; +// // +// // p2pkhChangeAddressArray = +// // (await resultChange44)['addressArray'] as List; +// // p2pkhChangeIndex = (await resultChange44)['index'] as int; +// // p2pkhChangeDerivations = (await resultChange44)['derivations'] +// // as Map>; +// // +// // p2shChangeAddressArray = +// // (await resultChange49)['addressArray'] as List; +// // p2shChangeIndex = (await resultChange49)['index'] as int; +// // p2shChangeDerivations = (await resultChange49)['derivations'] +// // as Map>; +// // +// // p2wpkhChangeAddressArray = +// // (await resultChange84)['addressArray'] as List; +// // p2wpkhChangeIndex = (await resultChange84)['index'] as int; +// // p2wpkhChangeDerivations = (await resultChange84)['derivations'] +// // as Map>; +// // +// // // save the derivations (if any) +// // if (p2pkhReceiveDerivations.isNotEmpty) { +// // await addDerivations( +// // chain: 0, +// // derivePathType: DerivePathType.bip44, +// // derivationsToAdd: p2pkhReceiveDerivations); +// // } +// // if (p2shReceiveDerivations.isNotEmpty) { +// // await addDerivations( +// // chain: 0, +// // derivePathType: DerivePathType.bip49, +// // derivationsToAdd: p2shReceiveDerivations); +// // } +// // if (p2wpkhReceiveDerivations.isNotEmpty) { +// // await addDerivations( +// // chain: 0, +// // derivePathType: DerivePathType.bip84, +// // derivationsToAdd: p2wpkhReceiveDerivations); +// // } +// // if (p2pkhChangeDerivations.isNotEmpty) { +// // await addDerivations( +// // chain: 1, +// // derivePathType: DerivePathType.bip44, +// // derivationsToAdd: p2pkhChangeDerivations); +// // } +// // if (p2shChangeDerivations.isNotEmpty) { +// // await addDerivations( +// // chain: 1, +// // derivePathType: DerivePathType.bip49, +// // derivationsToAdd: p2shChangeDerivations); +// // } +// // if (p2wpkhChangeDerivations.isNotEmpty) { +// // await addDerivations( +// // chain: 1, +// // derivePathType: DerivePathType.bip84, +// // derivationsToAdd: p2wpkhChangeDerivations); +// // } +// // +// // // If restoring a wallet that never received any funds, then set receivingArray manually +// // // If we didn't do this, it'd store an empty array +// // if (p2pkhReceiveIndex == -1) { +// // final address = +// // await _generateAddressForChain(0, 0, DerivePathType.bip44); +// // p2pkhReceiveAddressArray.add(address); +// // } +// // if (p2shReceiveIndex == -1) { +// // final address = +// // await _generateAddressForChain(0, 0, DerivePathType.bip49); +// // p2shReceiveAddressArray.add(address); +// // } +// // if (p2wpkhReceiveIndex == -1) { +// // final address = +// // await _generateAddressForChain(0, 0, DerivePathType.bip84); +// // p2wpkhReceiveAddressArray.add(address); +// // } +// // +// // // If restoring a wallet that never sent any funds with change, then set changeArray +// // // manually. If we didn't do this, it'd store an empty array. +// // if (p2pkhChangeIndex == -1) { +// // final address = +// // await _generateAddressForChain(1, 0, DerivePathType.bip44); +// // p2pkhChangeAddressArray.add(address); +// // } +// // if (p2shChangeIndex == -1) { +// // final address = +// // await _generateAddressForChain(1, 0, DerivePathType.bip49); +// // p2shChangeAddressArray.add(address); +// // } +// // if (p2wpkhChangeIndex == -1) { +// // final address = +// // await _generateAddressForChain(1, 0, DerivePathType.bip84); +// // p2wpkhChangeAddressArray.add(address); +// // } +// // +// // if (isRescan) { +// // await db.updateOrPutAddresses([ +// // ...p2wpkhReceiveAddressArray, +// // ...p2wpkhChangeAddressArray, +// // ...p2pkhReceiveAddressArray, +// // ...p2pkhChangeAddressArray, +// // ...p2shReceiveAddressArray, +// // ...p2shChangeAddressArray, +// // ]); +// // } else { +// // await db.putAddresses([ +// // ...p2wpkhReceiveAddressArray, +// // ...p2wpkhChangeAddressArray, +// // ...p2pkhReceiveAddressArray, +// // ...p2pkhChangeAddressArray, +// // ...p2shReceiveAddressArray, +// // ...p2shChangeAddressArray, +// // ]); +// // } +// // +// // await _updateUTXOs(); +// // +// // await Future.wait([ +// // updateCachedId(walletId), +// // updateCachedIsFavorite(false), +// // ]); +// // +// // longMutex = false; +// // } catch (e, s) { +// // Logging.instance.log( +// // "Exception rethrown from _recoverWalletFromBIP32SeedPhrase(): $e\n$s", +// // level: LogLevel.Error); +// // +// // longMutex = false; +// // rethrow; +// // } +// // } +// // +// // Future refreshIfThereIsNewData() async { +// // if (longMutex) return false; +// // if (_hasCalledExit) return false; +// // Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info); +// // +// // try { +// // bool needsRefresh = false; +// // Set txnsToCheck = {}; +// // +// // for (final String txid in txTracker.pendings) { +// // if (!txTracker.wasNotifiedConfirmed(txid)) { +// // txnsToCheck.add(txid); +// // } +// // } +// // +// // for (String txid in txnsToCheck) { +// // final txn = await electrumXClient.getTransaction(txHash: txid); +// // int confirmations = txn["confirmations"] as int? ?? 0; +// // bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS; +// // if (!isUnconfirmed) { +// // // unconfirmedTxs = {}; +// // needsRefresh = true; +// // break; +// // } +// // } +// // if (!needsRefresh) { +// // var allOwnAddresses = await _fetchAllOwnAddresses(); +// // List> allTxs = await _fetchHistory( +// // allOwnAddresses.map((e) => e.value).toList(growable: false)); +// // for (Map transaction in allTxs) { +// // final txid = transaction['tx_hash'] as String; +// // if ((await db +// // .getTransactions(walletId) +// // .filter() +// // .txidMatches(txid) +// // .findFirst()) == +// // null) { +// // Logging.instance.log( +// // " txid not found in address history already ${transaction['tx_hash']}", +// // level: LogLevel.Info); +// // needsRefresh = true; +// // break; +// // } +// // } +// // } +// // return needsRefresh; +// // } catch (e, s) { +// // Logging.instance.log( +// // "Exception caught in refreshIfThereIsNewData: $e\n$s", +// // level: LogLevel.Error); +// // rethrow; +// // } +// // } +// // +// // Future getAllTxsToWatch() async { +// // if (_hasCalledExit) return; +// // List unconfirmedTxnsToNotifyPending = []; +// // List unconfirmedTxnsToNotifyConfirmed = []; +// // +// // final currentChainHeight = await chainHeight; +// // +// // final txCount = await db.getTransactions(walletId).count(); +// // +// // const paginateLimit = 50; +// // +// // for (int i = 0; i < txCount; i += paginateLimit) { +// // final transactions = await db +// // .getTransactions(walletId) +// // .offset(i) +// // .limit(paginateLimit) +// // .findAll(); +// // for (final tx in transactions) { +// // if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { +// // // get all transactions that were notified as pending but not as confirmed +// // if (txTracker.wasNotifiedPending(tx.txid) && +// // !txTracker.wasNotifiedConfirmed(tx.txid)) { +// // unconfirmedTxnsToNotifyConfirmed.add(tx); +// // } +// // } else { +// // // get all transactions that were not notified as pending yet +// // if (!txTracker.wasNotifiedPending(tx.txid)) { +// // unconfirmedTxnsToNotifyPending.add(tx); +// // } +// // } +// // } +// // } +// // +// // // notify on unconfirmed transactions +// // for (final tx in unconfirmedTxnsToNotifyPending) { +// // final confirmations = tx.getConfirmations(currentChainHeight); +// // +// // if (tx.type == isar_models.TransactionType.incoming) { +// // CryptoNotificationsEventBus.instance.fire( +// // CryptoNotificationEvent( +// // title: "Incoming transaction", +// // walletId: walletId, +// // date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), +// // shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, +// // txid: tx.txid, +// // confirmations: confirmations, +// // requiredConfirmations: MINIMUM_CONFIRMATIONS, +// // walletName: walletName, +// // coin: coin, +// // ), +// // ); +// // await txTracker.addNotifiedPending(tx.txid); +// // } else if (tx.type == isar_models.TransactionType.outgoing) { +// // CryptoNotificationsEventBus.instance.fire( +// // CryptoNotificationEvent( +// // title: "Sending transaction", +// // walletId: walletId, +// // date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), +// // shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, +// // txid: tx.txid, +// // confirmations: confirmations, +// // requiredConfirmations: MINIMUM_CONFIRMATIONS, +// // walletName: walletName, +// // coin: coin, +// // ), +// // ); +// // +// // await txTracker.addNotifiedPending(tx.txid); +// // } +// // } +// // +// // // notify on confirmed +// // for (final tx in unconfirmedTxnsToNotifyConfirmed) { +// // if (tx.type == isar_models.TransactionType.incoming) { +// // CryptoNotificationsEventBus.instance.fire( +// // CryptoNotificationEvent( +// // title: "Incoming transaction confirmed", +// // walletId: walletId, +// // date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), +// // shouldWatchForUpdates: false, +// // txid: tx.txid, +// // requiredConfirmations: MINIMUM_CONFIRMATIONS, +// // walletName: walletName, +// // coin: coin, +// // ), +// // ); +// // +// // await txTracker.addNotifiedConfirmed(tx.txid); +// // } else if (tx.type == isar_models.TransactionType.outgoing) { +// // CryptoNotificationsEventBus.instance.fire( +// // CryptoNotificationEvent( +// // title: "Outgoing transaction confirmed", +// // walletId: walletId, +// // date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), +// // shouldWatchForUpdates: false, +// // txid: tx.txid, +// // requiredConfirmations: MINIMUM_CONFIRMATIONS, +// // walletName: walletName, +// // coin: coin, +// // ), +// // ); +// // await txTracker.addNotifiedConfirmed(tx.txid); +// // } +// // } +// // } +// // +// // bool _shouldAutoSync = false; +// // +// // @override +// // bool get shouldAutoSync => _shouldAutoSync; +// // +// // @override +// // set shouldAutoSync(bool shouldAutoSync) { +// // if (_shouldAutoSync != shouldAutoSync) { +// // _shouldAutoSync = shouldAutoSync; +// // if (!shouldAutoSync) { +// // timer?.cancel(); +// // timer = null; +// // stopNetworkAlivePinging(); +// // } else { +// // startNetworkAlivePinging(); +// // refresh(); +// // } +// // } +// // } +// // +// // @override +// // bool get isRefreshing => refreshMutex; +// // +// // bool refreshMutex = false; +// // +// // //TODO Show percentages properly/more consistently +// // /// Refreshes display data for the wallet +// // @override +// // Future refresh() async { +// // if (refreshMutex) { +// // Logging.instance.log("$walletId $walletName refreshMutex denied", +// // level: LogLevel.Info); +// // return; +// // } else { +// // refreshMutex = true; +// // } +// // +// // try { +// // GlobalEventBus.instance.fire( +// // WalletSyncStatusChangedEvent( +// // WalletSyncStatus.syncing, +// // walletId, +// // coin, +// // ), +// // ); +// // +// // GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); +// // +// // GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); +// // +// // final currentHeight = await chainHeight; +// // const storedHeight = 1; //await storedChainHeight; +// // +// // Logging.instance +// // .log("chain height: $currentHeight", level: LogLevel.Info); +// // Logging.instance +// // .log("cached height: $storedHeight", level: LogLevel.Info); +// // +// // if (currentHeight != storedHeight) { +// // GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); +// // await _checkChangeAddressForTransactions(); +// // +// // GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); +// // await _checkCurrentReceivingAddressesForTransactions(); +// // +// // final fetchFuture = _refreshTransactions(); +// // 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)); +// // } +// // +// // refreshMutex = false; +// // GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); +// // GlobalEventBus.instance.fire( +// // WalletSyncStatusChangedEvent( +// // WalletSyncStatus.synced, +// // walletId, +// // coin, +// // ), +// // ); +// // +// // if (shouldAutoSync) { +// // timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { +// // Logging.instance.log( +// // "Periodic refresh check for $walletId $walletName in object instance: $hashCode", +// // level: LogLevel.Info); +// // // chain height check currently broken +// // // if ((await chainHeight) != (await storedChainHeight)) { +// // if (await refreshIfThereIsNewData()) { +// // await refresh(); +// // GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( +// // "New data found in $walletId $walletName in background!", +// // walletId)); +// // } +// // // } +// // }); +// // } +// // } catch (error, strace) { +// // refreshMutex = false; +// // GlobalEventBus.instance.fire( +// // NodeConnectionStatusChangedEvent( +// // NodeConnectionStatus.disconnected, +// // walletId, +// // coin, +// // ), +// // ); +// // GlobalEventBus.instance.fire( +// // WalletSyncStatusChangedEvent( +// // WalletSyncStatus.unableToSync, +// // walletId, +// // coin, +// // ), +// // ); +// // Logging.instance.log( +// // "Caught exception in refreshWalletData(): $error\n$strace", +// // level: LogLevel.Error); +// // } +// // } +// // +// // @override +// // Future> prepareSend({ +// // required String address, +// // required Amount amount, +// // Map? args, +// // }) async { +// // try { +// // final feeRateType = args?["feeRate"]; +// // final customSatsPerVByte = args?["satsPerVByte"] as int?; +// // final feeRateAmount = args?["feeRateAmount"]; +// // final utxos = args?["UTXOs"] as Set?; +// // +// // if (customSatsPerVByte != null) { +// // // check for send all +// // bool isSendAll = false; +// // if (amount == balance.spendable) { +// // isSendAll = true; +// // } +// // +// // final bool coinControl = utxos != null; +// // +// // final result = await coinSelection( +// // satoshiAmountToSend: amount.raw.toInt(), +// // selectedTxFeeRate: -1, +// // satsPerVByte: customSatsPerVByte, +// // recipientAddress: address, +// // isSendAll: isSendAll, +// // utxos: utxos?.toList(), +// // coinControl: coinControl, +// // ); +// // +// // Logging.instance +// // .log("PREPARE SEND RESULT: $result", level: LogLevel.Info); +// // if (result is int) { +// // switch (result) { +// // case 1: +// // throw Exception("Insufficient balance!"); +// // case 2: +// // throw Exception("Insufficient funds to pay for transaction fee!"); +// // default: +// // throw Exception("Transaction failed with error code $result"); +// // } +// // } else { +// // final hex = result["hex"]; +// // if (hex is String) { +// // final fee = result["fee"] as int; +// // final vSize = result["vSize"] as int; +// // +// // Logging.instance.log("txHex: $hex", level: LogLevel.Info); +// // Logging.instance.log("fee: $fee", level: LogLevel.Info); +// // Logging.instance.log("vsize: $vSize", level: LogLevel.Info); +// // // fee should never be less than vSize sanity check +// // if (fee < vSize) { +// // throw Exception( +// // "Error in fee calculation: Transaction fee cannot be less than vSize"); +// // } +// // return result as Map; +// // } else { +// // throw Exception("sent hex is not a String!!!"); +// // } +// // } +// // } else if (feeRateType is FeeRateType || feeRateAmount is int) { +// // late final int rate; +// // if (feeRateType is FeeRateType) { +// // int fee = 0; +// // final feeObject = await fees; +// // switch (feeRateType) { +// // case FeeRateType.fast: +// // fee = feeObject.fast; +// // break; +// // case FeeRateType.average: +// // fee = feeObject.medium; +// // break; +// // case FeeRateType.slow: +// // fee = feeObject.slow; +// // break; +// // default: +// // throw ArgumentError("Invalid use of custom fee"); +// // } +// // rate = fee; +// // } else { +// // rate = feeRateAmount as int; +// // } +// // +// // // check for send all +// // bool isSendAll = false; +// // if (amount == balance.spendable) { +// // isSendAll = true; +// // } +// // +// // final bool coinControl = utxos != null; +// // +// // final txData = await coinSelection( +// // satoshiAmountToSend: amount.raw.toInt(), +// // selectedTxFeeRate: rate, +// // recipientAddress: address, +// // isSendAll: isSendAll, +// // utxos: utxos?.toList(), +// // coinControl: coinControl, +// // ); +// // +// // Logging.instance.log("prepare send: $txData", level: LogLevel.Info); +// // try { +// // if (txData is int) { +// // switch (txData) { +// // case 1: +// // throw Exception("Insufficient balance!"); +// // case 2: +// // throw Exception( +// // "Insufficient funds to pay for transaction fee!"); +// // default: +// // throw Exception("Transaction failed with error code $txData"); +// // } +// // } else { +// // final hex = txData["hex"]; +// // +// // if (hex is String) { +// // final fee = txData["fee"] as int; +// // final vSize = txData["vSize"] as int; +// // +// // Logging.instance +// // .log("prepared txHex: $hex", level: LogLevel.Info); +// // Logging.instance.log("prepared fee: $fee", level: LogLevel.Info); +// // Logging.instance +// // .log("prepared vSize: $vSize", level: LogLevel.Info); +// // +// // // fee should never be less than vSize sanity check +// // if (fee < vSize) { +// // throw Exception( +// // "Error in fee calculation: Transaction fee cannot be less than vSize"); +// // } +// // +// // return txData as Map; +// // } else { +// // throw Exception("prepared hex is not a String!!!"); +// // } +// // } +// // } catch (e, s) { +// // Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", +// // level: LogLevel.Error); +// // rethrow; +// // } +// // } else { +// // throw ArgumentError("Invalid fee rate argument provided!"); +// // } +// // } catch (e, s) { +// // Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", +// // level: LogLevel.Error); +// // rethrow; +// // } +// // } +// // +// // @override +// // Future confirmSend({required Map txData}) async { +// // try { +// // Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); +// // +// // final hex = txData["hex"] as String; +// // +// // final txHash = await _electrumXClient.broadcastTransaction(rawTx: hex); +// // Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info); +// // +// // final utxos = txData["usedUTXOs"] as List; +// // +// // // mark utxos as used +// // await db.putUTXOs(utxos.map((e) => e.copyWith(used: true)).toList()); +// // +// // return txHash; +// // } catch (e, s) { +// // Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", +// // level: LogLevel.Error); +// // rethrow; +// // } +// // } +// // +// // @override +// // Future testNetworkConnection() async { +// // try { +// // final result = await _electrumXClient.ping(); +// // return result; +// // } catch (_) { +// // return false; +// // } +// // } +// // +// // Timer? _networkAliveTimer; +// // +// // 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 testNetworkConnection(); +// // +// // if (_isConnected != hasNetwork) { +// // NodeConnectionStatus status = hasNetwork +// // ? NodeConnectionStatus.connected +// // : NodeConnectionStatus.disconnected; +// // GlobalEventBus.instance +// // .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); +// // +// // _isConnected = hasNetwork; +// // if (hasNetwork) { +// // unawaited(refresh()); +// // } +// // } +// // } +// // +// // void stopNetworkAlivePinging() { +// // _networkAliveTimer?.cancel(); +// // _networkAliveTimer = null; +// // } +// // +// // bool _isConnected = false; +// // +// // @override +// // bool get isConnected => _isConnected; +// // +// // @override +// // Future initializeNew( +// // ({String mnemonicPassphrase, int wordCount})? data, +// // ) async { +// // Logging.instance +// // .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); +// // +// // if (getCachedId() != null) { +// // throw Exception( +// // "Attempted to initialize a new wallet using an existing wallet ID!"); +// // } +// // +// // await _prefs.init(); +// // try { +// // await _generateNewWallet(data); +// // } catch (e, s) { +// // Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", +// // level: LogLevel.Fatal); +// // rethrow; +// // } +// // +// // await Future.wait([ +// // updateCachedId(walletId), +// // updateCachedIsFavorite(false), +// // ]); +// // } +// // +// // @override +// // Future initializeExisting() async { +// // Logging.instance.log("initializeExisting() ${coin.prettyName} wallet.", +// // level: LogLevel.Info); +// // +// // if (getCachedId() == null) { +// // throw Exception( +// // "Attempted to initialize an existing wallet using an unknown wallet ID!"); +// // } +// // await _prefs.init(); +// // // await _checkCurrentChangeAddressesForTransactions(); +// // // await _checkCurrentReceivingAddressesForTransactions(); +// // } +// +// // hack to add tx to txData before refresh completes +// // required based on current app architecture where we don't properly store +// // transactions locally in a good way +// @override +// Future updateSentCachedTxData(Map txData) async { +// final transaction = isar_models.Transaction( +// walletId: walletId, +// txid: txData["txid"] as String, +// timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, +// type: isar_models.TransactionType.outgoing, +// subType: isar_models.TransactionSubType.none, +// // precision may be lost here hence the following amountString +// amount: (txData["recipientAmt"] as Amount).raw.toInt(), +// amountString: (txData["recipientAmt"] as Amount).toJsonString(), +// fee: txData["fee"] as int, +// height: null, +// isCancelled: false, +// isLelantus: false, +// otherData: null, +// slateId: null, +// nonce: null, +// inputs: [], +// outputs: [], +// numberOfMessages: null, +// ); +// +// final address = txData["address"] is String +// ? await db.getAddress(walletId, txData["address"] as String) +// : null; +// +// await db.addNewTransactionData( +// [ +// Tuple2(transaction, address), +// ], +// walletId, +// ); +// } +// +// @override +// bool validateAddress(String address) { +// return Address.validateAddress(address, _network, _network.bech32!); +// } +// +// @override +// String get walletId => _walletId; +// late final String _walletId; +// +// @override +// String get walletName => _walletName; +// late String _walletName; +// +// // setter for updating on rename +// @override +// set walletName(String newName) => _walletName = newName; +// +// late ElectrumXClient _electrumXClient; +// +// ElectrumXClient get electrumXClient => _electrumXClient; +// +// late CachedElectrumXClient _cachedElectrumXClient; +// +// CachedElectrumXClient get cachedElectrumXClient => _cachedElectrumXClient; +// +// late SecureStorageInterface _secureStore; +// +// @override +// Future updateNode(bool shouldRefresh) async { +// final failovers = NodeService(secureStorageInterface: _secureStore) +// .failoverNodesFor(coin: coin) +// .map((e) => ElectrumXNode( +// address: e.host, +// port: e.port, +// name: e.name, +// id: e.id, +// useSSL: e.useSSL, +// )) +// .toList(); +// final newNode = await getCurrentNode(); +// _electrumXClient = ElectrumXClient.from( +// node: newNode, +// prefs: _prefs, +// failovers: failovers, +// ); +// _cachedElectrumXClient = CachedElectrumXClient.from( +// electrumXClient: _electrumXClient, +// ); +// +// if (shouldRefresh) { +// unawaited(refresh()); +// } +// } +// +// Future> _getMnemonicList() async { +// final _mnemonicString = await mnemonicString; +// if (_mnemonicString == null) { +// return []; +// } +// final List data = _mnemonicString.split(' '); +// return data; +// } +// +// Future getCurrentNode() async { +// final node = NodeService(secureStorageInterface: _secureStore) +// .getPrimaryNodeFor(coin: coin) ?? +// DefaultNodes.getNodeFor(coin); +// +// return ElectrumXNode( +// address: node.host, +// port: node.port, +// name: node.name, +// useSSL: node.useSSL, +// id: node.id, +// ); +// } +// +// Future> _fetchAllOwnAddresses() async { +// final allAddresses = await db +// .getAddresses(walletId) +// .filter() +// .not() +// .typeEqualTo(isar_models.AddressType.nonWallet) +// .and() +// .group((q) => q +// .subTypeEqualTo(isar_models.AddressSubType.receiving) +// .or() +// .subTypeEqualTo(isar_models.AddressSubType.change)) +// .findAll(); +// // final List allAddresses = []; +// // final receivingAddresses = DB.instance.get( +// // boxName: walletId, key: 'receivingAddressesP2WPKH') as List; +// // final changeAddresses = DB.instance.get( +// // boxName: walletId, key: 'changeAddressesP2WPKH') as List; +// // final receivingAddressesP2PKH = DB.instance.get( +// // boxName: walletId, key: 'receivingAddressesP2PKH') as List; +// // final changeAddressesP2PKH = +// // DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') +// // as List; +// // final receivingAddressesP2SH = DB.instance.get( +// // boxName: walletId, key: 'receivingAddressesP2SH') as List; +// // final changeAddressesP2SH = +// // DB.instance.get(boxName: walletId, key: 'changeAddressesP2SH') +// // as List; +// // +// // for (var i = 0; i < receivingAddresses.length; i++) { +// // if (!allAddresses.contains(receivingAddresses[i])) { +// // allAddresses.add(receivingAddresses[i] as String); +// // } +// // } +// // for (var i = 0; i < changeAddresses.length; i++) { +// // if (!allAddresses.contains(changeAddresses[i])) { +// // allAddresses.add(changeAddresses[i] as String); +// // } +// // } +// // for (var i = 0; i < receivingAddressesP2PKH.length; i++) { +// // if (!allAddresses.contains(receivingAddressesP2PKH[i])) { +// // allAddresses.add(receivingAddressesP2PKH[i] as String); +// // } +// // } +// // for (var i = 0; i < changeAddressesP2PKH.length; i++) { +// // if (!allAddresses.contains(changeAddressesP2PKH[i])) { +// // allAddresses.add(changeAddressesP2PKH[i] as String); +// // } +// // } +// // for (var i = 0; i < receivingAddressesP2SH.length; i++) { +// // if (!allAddresses.contains(receivingAddressesP2SH[i])) { +// // allAddresses.add(receivingAddressesP2SH[i] as String); +// // } +// // } +// // for (var i = 0; i < changeAddressesP2SH.length; i++) { +// // if (!allAddresses.contains(changeAddressesP2SH[i])) { +// // allAddresses.add(changeAddressesP2SH[i] as String); +// // } +// // } +// return allAddresses; +// } +// +// Future _getFees() async { +// try { +// //TODO adjust numbers for different speeds? +// const int f = 1, m = 5, s = 20; +// +// final fast = await electrumXClient.estimateFee(blocks: f); +// final medium = await electrumXClient.estimateFee(blocks: m); +// final slow = await electrumXClient.estimateFee(blocks: s); +// +// final feeObject = FeeObject( +// numberOfBlocksFast: f, +// numberOfBlocksAverage: m, +// numberOfBlocksSlow: s, +// fast: Amount.fromDecimal( +// fast, +// fractionDigits: coin.decimals, +// ).raw.toInt(), +// medium: Amount.fromDecimal( +// medium, +// fractionDigits: coin.decimals, +// ).raw.toInt(), +// slow: Amount.fromDecimal( +// slow, +// fractionDigits: coin.decimals, +// ).raw.toInt(), +// ); +// +// Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); +// return feeObject; +// } catch (e) { +// Logging.instance +// .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); +// rethrow; +// } +// } +// +// Future _generateNewWallet( +// ({String mnemonicPassphrase, int wordCount})? data, +// ) async { +// Logging.instance +// .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); +// if (!integrationTestFlag) { +// try { +// final features = await electrumXClient +// .getServerFeatures() +// .timeout(const Duration(seconds: 3)); +// Logging.instance.log("features: $features", level: LogLevel.Info); +// switch (coin) { +// case Coin.litecoin: +// if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { +// // print(features['genesis_hash']); +// throw Exception("genesis hash does not match main net!"); +// } +// break; +// case Coin.litecoinTestNet: +// if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { +// throw Exception("genesis hash does not match test net!"); +// } +// break; +// default: +// throw Exception( +// "Attempted to generate a LitecoinWallet using a non litecoin coin type: ${coin.name}"); +// } +// } catch (e, s) { +// Logging.instance.log("$e/n$s", level: LogLevel.Info); +// } +// } +// +// // this should never fail +// if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { +// throw Exception( +// "Attempted to overwrite mnemonic on generate new wallet!"); +// } +// final int strength; +// if (data == null || data.wordCount == 12) { +// strength = 128; +// } else if (data.wordCount == 24) { +// strength = 256; +// } else { +// throw Exception("Invalid word count"); +// } +// await _secureStore.write( +// key: '${_walletId}_mnemonic', +// value: bip39.generateMnemonic(strength: strength)); +// await _secureStore.write( +// key: '${_walletId}_mnemonicPassphrase', +// value: data?.mnemonicPassphrase ?? "", +// ); +// +// // Generate and add addresses to relevant arrays +// final initialAddresses = await Future.wait([ +// // P2WPKH +// _generateAddressForChain(0, 0, DerivePathType.bip84), +// _generateAddressForChain(1, 0, DerivePathType.bip84), +// +// // P2PKH +// _generateAddressForChain(0, 0, DerivePathType.bip44), +// _generateAddressForChain(1, 0, DerivePathType.bip44), +// +// // P2SH +// _generateAddressForChain(0, 0, DerivePathType.bip49), +// _generateAddressForChain(1, 0, DerivePathType.bip49), +// ]); +// +// await db.putAddresses(initialAddresses); +// +// Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); +// } +// +// /// Generates a new internal or external chain address for the wallet using a BIP84, BIP44, or BIP49 derivation path. +// /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! +// /// [index] - This can be any integer >= 0 +// Future _generateAddressForChain( +// int chain, +// int index, +// DerivePathType derivePathType, +// ) async { +// final _mnemonic = await mnemonicString; +// final _mnemonicPassphrase = await mnemonicPassphrase; +// if (_mnemonicPassphrase == null) { +// Logging.instance.log( +// "Exception in _generateAddressForChain: mnemonic passphrase null, possible migration issue; if using internal builds, delete wallet and restore from seed, if using a release build, please file bug report", +// level: LogLevel.Error); +// } +// +// final derivePath = constructDerivePath( +// derivePathType: derivePathType, +// networkWIF: _network.wif, +// chain: chain, +// index: index, +// ); +// final node = await Bip32Utils.getBip32Node( +// _mnemonic!, +// _mnemonicPassphrase!, +// _network, +// derivePath, +// ); +// +// final data = PaymentData(pubkey: node.publicKey); +// String address; +// isar_models.AddressType addrType; +// +// switch (derivePathType) { +// case DerivePathType.bip44: +// address = P2PKH(data: data, network: _network).data.address!; +// addrType = isar_models.AddressType.p2pkh; +// break; +// case DerivePathType.bip49: +// address = P2SH( +// data: PaymentData( +// redeem: P2WPKH( +// data: data, +// network: _network, +// overridePrefix: _network.bech32!) +// .data), +// network: _network) +// .data +// .address!; +// addrType = isar_models.AddressType.p2sh; +// break; +// case DerivePathType.bip84: +// address = P2WPKH( +// network: _network, data: data, overridePrefix: _network.bech32!) +// .data +// .address!; +// addrType = isar_models.AddressType.p2wpkh; +// break; +// default: +// throw Exception("DerivePathType unsupported"); +// } +// +// // add generated address & info to derivations +// await addDerivation( +// chain: chain, +// address: address, +// pubKey: Format.uint8listToString(node.publicKey), +// wif: node.toWIF(), +// derivePathType: derivePathType, +// ); +// +// return isar_models.Address( +// walletId: walletId, +// value: address, +// publicKey: node.publicKey, +// type: addrType, +// derivationIndex: index, +// derivationPath: isar_models.DerivationPath()..value = derivePath, +// subType: chain == 0 +// ? isar_models.AddressSubType.receiving +// : isar_models.AddressSubType.change, +// ); +// } +// +// /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] +// /// and +// /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! +// Future _getCurrentAddressForChain( +// int chain, +// DerivePathType derivePathType, +// ) async { +// final subType = chain == 0 // Here, we assume that chain == 1 if it isn't 0 +// ? isar_models.AddressSubType.receiving +// : isar_models.AddressSubType.change; +// +// isar_models.AddressType type; +// isar_models.Address? address; +// switch (derivePathType) { +// case DerivePathType.bip44: +// type = isar_models.AddressType.p2pkh; +// break; +// case DerivePathType.bip49: +// type = isar_models.AddressType.p2sh; +// break; +// case DerivePathType.bip84: +// type = isar_models.AddressType.p2wpkh; +// break; +// default: +// throw Exception("DerivePathType unsupported"); +// } +// address = await db +// .getAddresses(walletId) +// .filter() +// .typeEqualTo(type) +// .subTypeEqualTo(subType) +// .sortByDerivationIndexDesc() +// .findFirst(); +// return address!.value; +// } +// +// String _buildDerivationStorageKey({ +// required int chain, +// required DerivePathType derivePathType, +// }) { +// String key; +// String chainId = chain == 0 ? "receive" : "change"; +// switch (derivePathType) { +// case DerivePathType.bip44: +// key = "${walletId}_${chainId}DerivationsP2PKH"; +// break; +// case DerivePathType.bip49: +// key = "${walletId}_${chainId}DerivationsP2SH"; +// break; +// case DerivePathType.bip84: +// key = "${walletId}_${chainId}DerivationsP2WPKH"; +// break; +// default: +// throw Exception("DerivePathType unsupported"); +// } +// return key; +// } +// +// Future> _fetchDerivations({ +// required int chain, +// required DerivePathType derivePathType, +// }) async { +// // build lookup key +// final key = _buildDerivationStorageKey( +// chain: chain, derivePathType: derivePathType); +// +// // fetch current derivations +// final derivationsString = await _secureStore.read(key: key); +// return Map.from( +// jsonDecode(derivationsString ?? "{}") as Map); +// } +// +// /// Add a single derivation to the local secure storage for [chain] and +// /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. +// /// This will overwrite a previous entry where the address of the new derivation +// /// matches a derivation currently stored. +// Future addDerivation({ +// required int chain, +// required String address, +// required String pubKey, +// required String wif, +// required DerivePathType derivePathType, +// }) async { +// // build lookup key +// final key = _buildDerivationStorageKey( +// chain: chain, derivePathType: derivePathType); +// +// // fetch current derivations +// final derivationsString = await _secureStore.read(key: key); +// final derivations = +// Map.from(jsonDecode(derivationsString ?? "{}") as Map); +// +// // add derivation +// derivations[address] = { +// "pubKey": pubKey, +// "wif": wif, +// }; +// +// // save derivations +// final newReceiveDerivationsString = jsonEncode(derivations); +// await _secureStore.write(key: key, value: newReceiveDerivationsString); +// } +// +// /// Add multiple derivations to the local secure storage for [chain] and +// /// [derivePathType] where [chain] must either be 1 for change or 0 for receive. +// /// This will overwrite any previous entries where the address of the new derivation +// /// matches a derivation currently stored. +// /// The [derivationsToAdd] must be in the format of: +// /// { +// /// addressA : { +// /// "pubKey": , +// /// "wif": , +// /// }, +// /// addressB : { +// /// "pubKey": , +// /// "wif": , +// /// }, +// /// } +// Future addDerivations({ +// required int chain, +// required DerivePathType derivePathType, +// required Map derivationsToAdd, +// }) async { +// // build lookup key +// final key = _buildDerivationStorageKey( +// chain: chain, derivePathType: derivePathType); +// +// // fetch current derivations +// final derivationsString = await _secureStore.read(key: key); +// final derivations = +// Map.from(jsonDecode(derivationsString ?? "{}") as Map); +// +// // add derivation +// derivations.addAll(derivationsToAdd); +// +// // save derivations +// final newReceiveDerivationsString = jsonEncode(derivations); +// await _secureStore.write(key: key, value: newReceiveDerivationsString); +// } +// +// Future _updateUTXOs() async { +// final allAddresses = await _fetchAllOwnAddresses(); +// +// try { +// final fetchedUtxoList = >>[]; +// +// final Map>> batches = {}; +// const batchSizeMax = 100; +// int batchNumber = 0; +// for (int i = 0; i < allAddresses.length; i++) { +// if (batches[batchNumber] == null) { +// batches[batchNumber] = {}; +// } +// final scripthash = +// _convertToScriptHash(allAddresses[i].value, _network); +// batches[batchNumber]!.addAll({ +// scripthash: [scripthash] +// }); +// if (i % batchSizeMax == batchSizeMax - 1) { +// batchNumber++; +// } +// } +// +// for (int i = 0; i < batches.length; i++) { +// final response = +// await _electrumXClient.getBatchUTXOs(args: batches[i]!); +// for (final entry in response.entries) { +// if (entry.value.isNotEmpty) { +// fetchedUtxoList.add(entry.value); +// } +// } +// } +// +// final List outputArray = []; +// +// for (int i = 0; i < fetchedUtxoList.length; i++) { +// for (int j = 0; j < fetchedUtxoList[i].length; j++) { +// final jsonUTXO = fetchedUtxoList[i][j]; +// +// final txn = await cachedElectrumXClient.getTransaction( +// txHash: jsonUTXO["tx_hash"] as String, +// verbose: true, +// coin: coin, +// ); +// +// bool shouldBlock = false; +// String? blockReason; +// String? label; +// +// final vout = jsonUTXO["tx_pos"] as int; +// +// final outputs = txn["vout"] as List; +// +// String? utxoOwnerAddress; +// // get UTXO owner address +// for (final output in outputs) { +// if (output["n"] == vout) { +// utxoOwnerAddress = +// output["scriptPubKey"]?["addresses"]?[0] as String? ?? +// output["scriptPubKey"]?["address"] as String?; +// } +// } +// +// final utxoAmount = jsonUTXO["value"] as int; +// +// // TODO check the specific output, not just the address in general +// // TODO optimize by freezing output in OrdinalsInterface, so one ordinal API calls is made (or at least many less) +// if (utxoOwnerAddress != null) { +// if (await inscriptionInAddress(utxoOwnerAddress!)) { +// shouldBlock = true; +// blockReason = "Ordinal"; +// label = "Ordinal detected at address"; +// } +// } else { +// // TODO implement inscriptionInOutput +// if (utxoAmount <= 10000) { +// shouldBlock = true; +// blockReason = "May contain ordinal"; +// label = "Possible ordinal"; +// } +// } +// +// final utxo = isar_models.UTXO( +// walletId: walletId, +// txid: txn["txid"] as String, +// vout: vout, +// value: utxoAmount, +// name: label ?? "", +// isBlocked: shouldBlock, +// blockedReason: blockReason, +// isCoinbase: txn["is_coinbase"] as bool? ?? false, +// blockHash: txn["blockhash"] as String?, +// blockHeight: jsonUTXO["height"] as int?, +// blockTime: txn["blocktime"] as int?, +// address: utxoOwnerAddress, +// ); +// +// outputArray.add(utxo); +// } +// } +// +// Logging.instance.log( +// 'Outputs fetched: $outputArray', +// level: LogLevel.Info, +// ); +// +// bool inscriptionsRefreshNeeded = +// await db.updateUTXOs(walletId, outputArray); +// +// if (inscriptionsRefreshNeeded) { +// await refreshInscriptions(); +// } +// +// // finally update balance +// await _updateBalance(); +// } catch (e, s) { +// Logging.instance.log( +// "Output fetch unsuccessful: $e\n$s", +// level: LogLevel.Error, +// ); +// } +// } +// +// Future _updateBalance() async { +// await refreshBalance(); +// } +// +// @override +// Balance get balance => _balance ??= getCachedBalance(); +// Balance? _balance; +// +// // /// Takes in a list of UtxoObjects and adds a name (dependent on object index within list) +// // /// and checks for the txid associated with the utxo being blocked and marks it accordingly. +// // /// Now also checks for output labeling. +// // Future _sortOutputs(List utxos) async { +// // final blockedHashArray = +// // DB.instance.get(boxName: walletId, key: 'blocked_tx_hashes') +// // as List?; +// // final List lst = []; +// // if (blockedHashArray != null) { +// // for (var hash in blockedHashArray) { +// // lst.add(hash as String); +// // } +// // } +// // final labels = +// // DB.instance.get(boxName: walletId, key: 'labels') as Map? ?? +// // {}; +// // +// // outputsList = []; +// // +// // for (var i = 0; i < utxos.length; i++) { +// // if (labels[utxos[i].txid] != null) { +// // utxos[i].txName = labels[utxos[i].txid] as String? ?? ""; +// // } else { +// // utxos[i].txName = 'Output #$i'; +// // } +// // +// // if (utxos[i].status.confirmed == false) { +// // outputsList.add(utxos[i]); +// // } else { +// // if (lst.contains(utxos[i].txid)) { +// // utxos[i].blocked = true; +// // outputsList.add(utxos[i]); +// // } else if (!lst.contains(utxos[i].txid)) { +// // outputsList.add(utxos[i]); +// // } +// // } +// // } +// // } +// +// Future getTxCount({required String address}) async { +// String? scripthash; +// try { +// scripthash = _convertToScriptHash(address, _network); +// final transactions = +// await electrumXClient.getHistory(scripthash: scripthash); +// return transactions.length; +// } catch (e) { +// Logging.instance.log( +// "Exception rethrown in _getTxCount(address: $address, scripthash: $scripthash): $e", +// level: LogLevel.Error); +// rethrow; +// } +// } +// +// Future> _getBatchTxCount({ +// required Map addresses, +// }) async { +// try { +// final Map> args = {}; +// for (final entry in addresses.entries) { +// args[entry.key] = [_convertToScriptHash(entry.value, _network)]; +// } +// final response = await electrumXClient.getBatchHistory(args: args); +// +// final Map result = {}; +// for (final entry in response.entries) { +// result[entry.key] = entry.value.length; +// } +// return result; +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown in _getBatchTxCount(address: $addresses: $e\n$s", +// level: LogLevel.Error); +// rethrow; +// } +// } +// +// Future _checkReceivingAddressForTransactions() async { +// try { +// final currentReceiving = await _currentReceivingAddress; +// +// final int txCount = await getTxCount(address: currentReceiving.value); +// Logging.instance.log( +// 'Number of txs for current receiving address $currentReceiving: $txCount', +// level: LogLevel.Info); +// +// if (txCount >= 1 || currentReceiving.derivationIndex < 0) { +// // First increment the receiving index +// final newReceivingIndex = currentReceiving.derivationIndex + 1; +// +// // Use new index to derive a new receiving address +// final newReceivingAddress = await _generateAddressForChain( +// 0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin)); +// +// final existing = await db +// .getAddresses(walletId) +// .filter() +// .valueEqualTo(newReceivingAddress.value) +// .findFirst(); +// if (existing == null) { +// // Add that new change address +// await db.putAddress(newReceivingAddress); +// } else { +// // we need to update the address +// await db.updateAddress(existing, newReceivingAddress); +// } +// // keep checking until address with no tx history is set as current +// await _checkReceivingAddressForTransactions(); +// } +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s", +// level: LogLevel.Error); +// rethrow; +// } +// } +// +// Future _checkChangeAddressForTransactions() async { +// try { +// final currentChange = await _currentChangeAddress; +// final int txCount = await getTxCount(address: currentChange.value); +// Logging.instance.log( +// 'Number of txs for current change address $currentChange: $txCount', +// level: LogLevel.Info); +// +// if (txCount >= 1 || currentChange.derivationIndex < 0) { +// // First increment the change index +// final newChangeIndex = currentChange.derivationIndex + 1; +// +// // Use new index to derive a new change address +// final newChangeAddress = await _generateAddressForChain( +// 1, newChangeIndex, DerivePathTypeExt.primaryFor(coin)); +// +// final existing = await db +// .getAddresses(walletId) +// .filter() +// .valueEqualTo(newChangeAddress.value) +// .findFirst(); +// if (existing == null) { +// // Add that new change address +// await db.putAddress(newChangeAddress); +// } else { +// // we need to update the address +// await db.updateAddress(existing, newChangeAddress); +// } +// // keep checking until address with no tx history is set as current +// await _checkChangeAddressForTransactions(); +// } +// } on SocketException catch (se, s) { +// Logging.instance.log( +// "SocketException caught in _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $se\n$s", +// level: LogLevel.Error); +// return; +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathTypeExt.primaryFor(coin)}): $e\n$s", +// level: LogLevel.Error); +// rethrow; +// } +// } +// +// Future _checkCurrentReceivingAddressesForTransactions() async { +// try { +// // for (final type in DerivePathType.values) { +// await _checkReceivingAddressForTransactions(); +// // } +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", +// level: LogLevel.Error); +// rethrow; +// } +// } +// +// /// public wrapper because dart can't test private... +// Future checkCurrentReceivingAddressesForTransactions() async { +// if (Platform.environment["FLUTTER_TEST"] == "true") { +// try { +// return _checkCurrentReceivingAddressesForTransactions(); +// } catch (_) { +// rethrow; +// } +// } +// } +// +// Future _checkCurrentChangeAddressesForTransactions() async { +// try { +// // for (final type in DerivePathType.values) { +// await _checkChangeAddressForTransactions(); +// // } +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s", +// level: LogLevel.Error); +// rethrow; +// } +// } +// +// /// public wrapper because dart can't test private... +// Future checkCurrentChangeAddressesForTransactions() async { +// if (Platform.environment["FLUTTER_TEST"] == "true") { +// try { +// return _checkCurrentChangeAddressesForTransactions(); +// } catch (_) { +// rethrow; +// } +// } +// } +// +// /// attempts to convert a string to a valid scripthash +// /// +// /// Returns the scripthash or throws an exception on invalid litecoin address +// String _convertToScriptHash(String litecoinAddress, NetworkType network) { +// try { +// final output = Address.addressToOutputScript( +// litecoinAddress, network, _network.bech32!); +// final hash = sha256.convert(output.toList(growable: false)).toString(); +// +// final chars = hash.split(""); +// final reversedPairs = []; +// var i = chars.length - 1; +// while (i > 0) { +// reversedPairs.add(chars[i - 1]); +// reversedPairs.add(chars[i]); +// i -= 2; +// } +// return reversedPairs.join(""); +// } catch (e) { +// rethrow; +// } +// } +// +// Future>> _fetchHistory( +// List allAddresses) async { +// try { +// List> allTxHashes = []; +// +// final Map>> batches = {}; +// final Map requestIdToAddressMap = {}; +// const batchSizeMax = 100; +// int batchNumber = 0; +// for (int i = 0; i < allAddresses.length; i++) { +// if (batches[batchNumber] == null) { +// batches[batchNumber] = {}; +// } +// final scripthash = _convertToScriptHash(allAddresses[i], _network); +// final id = Logger.isTestEnv ? "$i" : const Uuid().v1(); +// requestIdToAddressMap[id] = allAddresses[i]; +// batches[batchNumber]!.addAll({ +// id: [scripthash] +// }); +// if (i % batchSizeMax == batchSizeMax - 1) { +// batchNumber++; +// } +// } +// +// for (int i = 0; i < batches.length; i++) { +// final response = +// await _electrumXClient.getBatchHistory(args: batches[i]!); +// for (final entry in response.entries) { +// for (int j = 0; j < entry.value.length; j++) { +// entry.value[j]["address"] = requestIdToAddressMap[entry.key]; +// if (!allTxHashes.contains(entry.value[j])) { +// allTxHashes.add(entry.value[j]); +// } +// } +// } +// } +// +// return allTxHashes; +// } catch (e, s) { +// Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error); +// rethrow; +// } +// } +// +// Future>> fastFetch(List allTxHashes) async { +// List> allTransactions = []; +// +// const futureLimit = 30; +// List>> transactionFutures = []; +// int currentFutureCount = 0; +// for (final txHash in allTxHashes) { +// Future> transactionFuture = +// cachedElectrumXClient.getTransaction( +// txHash: txHash, +// verbose: true, +// coin: coin, +// ); +// transactionFutures.add(transactionFuture); +// currentFutureCount++; +// if (currentFutureCount > futureLimit) { +// currentFutureCount = 0; +// await Future.wait(transactionFutures); +// for (final fTx in transactionFutures) { +// final tx = await fTx; +// +// allTransactions.add(tx); +// } +// } +// } +// if (currentFutureCount != 0) { +// currentFutureCount = 0; +// await Future.wait(transactionFutures); +// for (final fTx in transactionFutures) { +// final tx = await fTx; +// +// allTransactions.add(tx); +// } +// } +// return allTransactions; +// } +// +// bool _duplicateTxCheck( +// List> allTransactions, String txid) { +// for (int i = 0; i < allTransactions.length; i++) { +// if (allTransactions[i]["txid"] == txid) { +// return true; +// } +// } +// return false; +// } +// +// Future _refreshTransactions() async { +// final List allAddresses = +// await _fetchAllOwnAddresses(); +// +// final List> allTxHashes = +// await _fetchHistory(allAddresses.map((e) => e.value).toList()); +// +// Set hashes = {}; +// for (var element in allTxHashes) { +// hashes.add(element['tx_hash'] as String); +// } +// await fastFetch(hashes.toList()); +// +// List> allTransactions = []; +// final currentHeight = await chainHeight; +// +// for (final txHash in allTxHashes) { +// final storedTx = await db +// .getTransactions(walletId) +// .filter() +// .txidEqualTo(txHash["tx_hash"] as String) +// .findFirst(); +// +// if (storedTx == null || +// !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) { +// final tx = await cachedElectrumXClient.getTransaction( +// txHash: txHash["tx_hash"] as String, +// verbose: true, +// coin: coin, +// ); +// +// // Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}"); +// if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { +// tx["address"] = await db +// .getAddresses(walletId) +// .filter() +// .valueEqualTo(txHash["address"] as String) +// .findFirst(); +// tx["height"] = txHash["height"]; +// allTransactions.add(tx); +// } +// } +// } +// +// // prefetch/cache +// Set vHashes = {}; +// for (final txObject in allTransactions) { +// for (int i = 0; i < (txObject["vin"] as List).length; i++) { +// final input = txObject["vin"]![i] as Map; +// final prevTxid = input["txid"] as String; +// vHashes.add(prevTxid); +// } +// } +// await fastFetch(vHashes.toList()); +// +// final List> txnsData = +// []; +// +// for (final txObject in allTransactions) { +// final data = await parseTransaction( +// txObject, +// cachedElectrumXClient, +// allAddresses, +// coin, +// MINIMUM_CONFIRMATIONS, +// walletId, +// ); +// +// txnsData.add(data); +// } +// await db.addNewTransactionData(txnsData, walletId); +// +// // quick hack to notify manager to call notifyListeners if +// // transactions changed +// if (txnsData.isNotEmpty) { +// GlobalEventBus.instance.fire( +// UpdatedInBackgroundEvent( +// "Transactions updated/added for: $walletId $walletName ", +// walletId, +// ), +// ); +// } +// } +// +// int estimateTxFee({required int vSize, required int feeRatePerKB}) { +// return vSize * (feeRatePerKB / 1000).ceil(); +// } +// +// /// The coinselection algorithm decides whether or not the user is eligible to make the transaction +// /// with [satoshiAmountToSend] and [selectedTxFeeRate]. If so, it will call buildTrasaction() and return +// /// a map containing the tx hex along with other important information. If not, then it will return +// /// an integer (1 or 2) +// dynamic coinSelection({ +// required int satoshiAmountToSend, +// required int selectedTxFeeRate, +// required String recipientAddress, +// required bool coinControl, +// required bool isSendAll, +// int? satsPerVByte, +// int additionalOutputs = 0, +// List? utxos, +// }) async { +// Logging.instance +// .log("Starting coinSelection ----------", level: LogLevel.Info); +// final List availableOutputs = utxos ?? await this.utxos; +// final currentChainHeight = await chainHeight; +// final List spendableOutputs = []; +// int spendableSatoshiValue = 0; +// +// // Build list of spendable outputs and totaling their satoshi amount +// for (final utxo in availableOutputs) { +// if (utxo.isBlocked == false && +// utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) && +// utxo.used != true) { +// spendableOutputs.add(utxo); +// spendableSatoshiValue += utxo.value; +// } +// } +// +// if (coinControl) { +// if (spendableOutputs.length < availableOutputs.length) { +// throw ArgumentError("Attempted to use an unavailable utxo"); +// } +// } +// +// // don't care about sorting if using all utxos +// if (!coinControl) { +// // sort spendable by age (oldest first) +// spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!)); +// } +// +// Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}", +// level: LogLevel.Info); +// Logging.instance +// .log("spendableOutputs: $spendableOutputs", level: LogLevel.Info); +// Logging.instance.log("spendableSatoshiValue: $spendableSatoshiValue", +// level: LogLevel.Info); +// Logging.instance +// .log("satoshiAmountToSend: $satoshiAmountToSend", level: LogLevel.Info); +// // If the amount the user is trying to send is smaller than the amount that they have spendable, +// // then return 1, which indicates that they have an insufficient balance. +// if (spendableSatoshiValue < satoshiAmountToSend) { +// return 1; +// // If the amount the user wants to send is exactly equal to the amount they can spend, then return +// // 2, which indicates that they are not leaving enough over to pay the transaction fee +// } else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) { +// return 2; +// } +// // If neither of these statements pass, we assume that the user has a spendable balance greater +// // than the amount they're attempting to send. Note that this value still does not account for +// // the added transaction fee, which may require an extra input and will need to be checked for +// // later on. +// +// // Possible situation right here +// int satoshisBeingUsed = 0; +// int inputsBeingConsumed = 0; +// List utxoObjectsToUse = []; +// +// if (!coinControl) { +// for (var i = 0; +// satoshisBeingUsed < satoshiAmountToSend && +// i < spendableOutputs.length; +// i++) { +// utxoObjectsToUse.add(spendableOutputs[i]); +// satoshisBeingUsed += spendableOutputs[i].value; +// inputsBeingConsumed += 1; +// } +// for (int i = 0; +// i < additionalOutputs && +// inputsBeingConsumed < spendableOutputs.length; +// i++) { +// utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); +// satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; +// inputsBeingConsumed += 1; +// } +// } else { +// satoshisBeingUsed = spendableSatoshiValue; +// utxoObjectsToUse = spendableOutputs; +// inputsBeingConsumed = spendableOutputs.length; +// } +// +// Logging.instance +// .log("satoshisBeingUsed: $satoshisBeingUsed", level: LogLevel.Info); +// Logging.instance +// .log("inputsBeingConsumed: $inputsBeingConsumed", level: LogLevel.Info); +// Logging.instance +// .log('utxoObjectsToUse: $utxoObjectsToUse', level: LogLevel.Info); +// +// // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray +// List recipientsArray = [recipientAddress]; +// List recipientsAmtArray = [satoshiAmountToSend]; +// +// // gather required signing data +// final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); +// +// if (isSendAll) { +// Logging.instance +// .log("Attempting to send all $coin", level: LogLevel.Info); +// +// final int vSizeForOneOutput = (await buildTransaction( +// utxoSigningData: utxoSigningData, +// recipients: [recipientAddress], +// satoshiAmounts: [satoshisBeingUsed - 1], +// ))["vSize"] as int; +// int feeForOneOutput = satsPerVByte != null +// ? (satsPerVByte * vSizeForOneOutput) +// : estimateTxFee( +// vSize: vSizeForOneOutput, +// feeRatePerKB: selectedTxFeeRate, +// ); +// +// if (satsPerVByte == null) { +// final int roughEstimate = roughFeeEstimate( +// spendableOutputs.length, +// 1, +// selectedTxFeeRate, +// ).raw.toInt(); +// if (feeForOneOutput < roughEstimate) { +// feeForOneOutput = roughEstimate; +// } +// } +// +// final int amount = satoshiAmountToSend - feeForOneOutput; +// dynamic txn = await buildTransaction( +// utxoSigningData: utxoSigningData, +// recipients: recipientsArray, +// satoshiAmounts: [amount], +// ); +// Map transactionObject = { +// "hex": txn["hex"], +// "recipient": recipientsArray[0], +// "recipientAmt": Amount( +// rawValue: BigInt.from(amount), +// fractionDigits: coin.decimals, +// ), +// "fee": feeForOneOutput, +// "vSize": txn["vSize"], +// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), +// }; +// return transactionObject; +// } +// +// final int vSizeForOneOutput = (await buildTransaction( +// utxoSigningData: utxoSigningData, +// recipients: [recipientAddress], +// satoshiAmounts: [satoshisBeingUsed - 1], +// ))["vSize"] as int; +// final int vSizeForTwoOutPuts = (await buildTransaction( +// utxoSigningData: utxoSigningData, +// recipients: [ +// recipientAddress, +// await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)), +// ], +// satoshiAmounts: [ +// satoshiAmountToSend, +// satoshisBeingUsed - satoshiAmountToSend - 1 +// ], // dust limit is the minimum amount a change output should be +// ))["vSize"] as int; +// +// // Assume 1 output, only for recipient and no change +// final feeForOneOutput = satsPerVByte != null +// ? (satsPerVByte * vSizeForOneOutput) +// : estimateTxFee( +// vSize: vSizeForOneOutput, +// feeRatePerKB: selectedTxFeeRate, +// ); +// // Assume 2 outputs, one for recipient and one for change +// final feeForTwoOutputs = satsPerVByte != null +// ? (satsPerVByte * vSizeForTwoOutPuts) +// : estimateTxFee( +// vSize: vSizeForTwoOutPuts, +// feeRatePerKB: selectedTxFeeRate, +// ); +// +// Logging.instance +// .log("feeForTwoOutputs: $feeForTwoOutputs", level: LogLevel.Info); +// Logging.instance +// .log("feeForOneOutput: $feeForOneOutput", level: LogLevel.Info); +// +// if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { +// if (satoshisBeingUsed - satoshiAmountToSend > +// feeForOneOutput + DUST_LIMIT.raw.toInt()) { +// // Here, we know that theoretically, we may be able to include another output(change) but we first need to +// // factor in the value of this output in satoshis. +// int changeOutputSize = +// satoshisBeingUsed - satoshiAmountToSend - feeForTwoOutputs; +// // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and +// // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new +// // change address. +// if (changeOutputSize > DUST_LIMIT.raw.toInt() && +// satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == +// feeForTwoOutputs) { +// // generate new change address if current change address has been used +// await _checkChangeAddressForTransactions(); +// final String newChangeAddress = await _getCurrentAddressForChain( +// 1, DerivePathTypeExt.primaryFor(coin)); +// +// int feeBeingPaid = +// satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; +// +// recipientsArray.add(newChangeAddress); +// recipientsAmtArray.add(changeOutputSize); +// // At this point, we have the outputs we're going to use, the amounts to send along with which addresses +// // we intend to send these amounts to. We have enough to send instructions to build the transaction. +// Logging.instance.log('2 outputs in tx', level: LogLevel.Info); +// Logging.instance +// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); +// Logging.instance.log('Recipient output size: $satoshiAmountToSend', +// level: LogLevel.Info); +// Logging.instance.log('Change Output Size: $changeOutputSize', +// level: LogLevel.Info); +// Logging.instance.log( +// 'Difference (fee being paid): $feeBeingPaid sats', +// level: LogLevel.Info); +// Logging.instance +// .log('Estimated fee: $feeForTwoOutputs', level: LogLevel.Info); +// dynamic txn = await buildTransaction( +// utxoSigningData: utxoSigningData, +// recipients: recipientsArray, +// satoshiAmounts: recipientsAmtArray, +// ); +// +// // make sure minimum fee is accurate if that is being used +// if (txn["vSize"] - feeBeingPaid == 1) { +// int changeOutputSize = +// satoshisBeingUsed - satoshiAmountToSend - (txn["vSize"] as int); +// feeBeingPaid = +// satoshisBeingUsed - satoshiAmountToSend - changeOutputSize; +// recipientsAmtArray.removeLast(); +// recipientsAmtArray.add(changeOutputSize); +// Logging.instance.log('Adjusted Input size: $satoshisBeingUsed', +// level: LogLevel.Info); +// Logging.instance.log( +// 'Adjusted Recipient output size: $satoshiAmountToSend', +// level: LogLevel.Info); +// Logging.instance.log( +// 'Adjusted Change Output Size: $changeOutputSize', +// level: LogLevel.Info); +// Logging.instance.log( +// 'Adjusted Difference (fee being paid): $feeBeingPaid sats', +// level: LogLevel.Info); +// Logging.instance.log('Adjusted Estimated fee: $feeForTwoOutputs', +// level: LogLevel.Info); +// txn = await buildTransaction( +// utxoSigningData: utxoSigningData, +// recipients: recipientsArray, +// satoshiAmounts: recipientsAmtArray, +// ); +// } +// +// Map transactionObject = { +// "hex": txn["hex"], +// "recipient": recipientsArray[0], +// "recipientAmt": Amount( +// rawValue: BigInt.from(recipientsAmtArray[0]), +// fractionDigits: coin.decimals, +// ), +// "fee": feeBeingPaid, +// "vSize": txn["vSize"], +// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), +// }; +// return transactionObject; +// } else { +// // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize +// // is smaller than or equal to DUST_LIMIT. Revert to single output transaction. +// Logging.instance.log('1 output in tx', level: LogLevel.Info); +// Logging.instance +// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); +// Logging.instance.log('Recipient output size: $satoshiAmountToSend', +// level: LogLevel.Info); +// Logging.instance.log( +// 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', +// level: LogLevel.Info); +// Logging.instance +// .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); +// dynamic txn = await buildTransaction( +// utxoSigningData: utxoSigningData, +// recipients: recipientsArray, +// satoshiAmounts: recipientsAmtArray, +// ); +// Map transactionObject = { +// "hex": txn["hex"], +// "recipient": recipientsArray[0], +// "recipientAmt": Amount( +// rawValue: BigInt.from(recipientsAmtArray[0]), +// fractionDigits: coin.decimals, +// ), +// "fee": satoshisBeingUsed - satoshiAmountToSend, +// "vSize": txn["vSize"], +// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), +// }; +// return transactionObject; +// } +// } else { +// // No additional outputs needed since adding one would mean that it'd be smaller than DUST_LIMIT sats +// // which makes it uneconomical to add to the transaction. Here, we pass data directly to instruct +// // the wallet to begin crafting the transaction that the user requested. +// Logging.instance.log('1 output in tx', level: LogLevel.Info); +// Logging.instance +// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); +// Logging.instance.log('Recipient output size: $satoshiAmountToSend', +// level: LogLevel.Info); +// Logging.instance.log( +// 'Difference (fee being paid): ${satoshisBeingUsed - satoshiAmountToSend} sats', +// level: LogLevel.Info); +// Logging.instance +// .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); +// dynamic txn = await buildTransaction( +// utxoSigningData: utxoSigningData, +// recipients: recipientsArray, +// satoshiAmounts: recipientsAmtArray, +// ); +// Map transactionObject = { +// "hex": txn["hex"], +// "recipient": recipientsArray[0], +// "recipientAmt": Amount( +// rawValue: BigInt.from(recipientsAmtArray[0]), +// fractionDigits: coin.decimals, +// ), +// "fee": satoshisBeingUsed - satoshiAmountToSend, +// "vSize": txn["vSize"], +// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), +// }; +// return transactionObject; +// } +// } else if (satoshisBeingUsed - satoshiAmountToSend == feeForOneOutput) { +// // In this scenario, no additional change output is needed since inputs - outputs equal exactly +// // what we need to pay for fees. Here, we pass data directly to instruct the wallet to begin +// // crafting the transaction that the user requested. +// Logging.instance.log('1 output in tx', level: LogLevel.Info); +// Logging.instance +// .log('Input size: $satoshisBeingUsed', level: LogLevel.Info); +// Logging.instance.log('Recipient output size: $satoshiAmountToSend', +// level: LogLevel.Info); +// Logging.instance.log( +// 'Fee being paid: ${satoshisBeingUsed - satoshiAmountToSend} sats', +// level: LogLevel.Info); +// Logging.instance +// .log('Estimated fee: $feeForOneOutput', level: LogLevel.Info); +// dynamic txn = await buildTransaction( +// utxoSigningData: utxoSigningData, +// recipients: recipientsArray, +// satoshiAmounts: recipientsAmtArray, +// ); +// Map transactionObject = { +// "hex": txn["hex"], +// "recipient": recipientsArray[0], +// "recipientAmt": Amount( +// rawValue: BigInt.from(recipientsAmtArray[0]), +// fractionDigits: coin.decimals, +// ), +// "fee": feeForOneOutput, +// "vSize": txn["vSize"], +// "usedUTXOs": utxoSigningData.map((e) => e.utxo).toList(), +// }; +// return transactionObject; +// } else { +// // Remember that returning 2 indicates that the user does not have a sufficient balance to +// // pay for the transaction fee. Ideally, at this stage, we should check if the user has any +// // additional outputs they're able to spend and then recalculate fees. +// Logging.instance.log( +// 'Cannot pay tx fee - checking for more outputs and trying again', +// level: LogLevel.Warning); +// // try adding more outputs +// if (spendableOutputs.length > inputsBeingConsumed) { +// return coinSelection( +// satoshiAmountToSend: satoshiAmountToSend, +// selectedTxFeeRate: selectedTxFeeRate, +// satsPerVByte: satsPerVByte, +// recipientAddress: recipientAddress, +// isSendAll: isSendAll, +// additionalOutputs: additionalOutputs + 1, +// utxos: utxos, +// coinControl: coinControl, +// ); +// } +// return 2; +// } +// } +// +// Future> fetchBuildTxData( +// List utxosToUse, +// ) async { +// // return data +// List signingData = []; +// +// try { +// // Populating the addresses to check +// for (var i = 0; i < utxosToUse.length; i++) { +// if (utxosToUse[i].address == null) { +// final txid = utxosToUse[i].txid; +// final tx = await _cachedElectrumXClient.getTransaction( +// txHash: txid, +// coin: coin, +// ); +// for (final output in tx["vout"] as List) { +// final n = output["n"]; +// if (n != null && n == utxosToUse[i].vout) { +// utxosToUse[i] = utxosToUse[i].copyWith( +// address: output["scriptPubKey"]?["addresses"]?[0] as String? ?? +// output["scriptPubKey"]["address"] as String, +// ); +// } +// } +// } +// +// final derivePathType = addressType(address: utxosToUse[i].address!); +// +// signingData.add( +// SigningData( +// derivePathType: derivePathType, +// utxo: utxosToUse[i], +// ), +// ); +// } +// +// Map> receiveDerivations = {}; +// Map> changeDerivations = {}; +// +// for (final sd in signingData) { +// String? pubKey; +// String? wif; +// +// // fetch receiving derivations if null +// receiveDerivations[sd.derivePathType] ??= await _fetchDerivations( +// chain: 0, +// derivePathType: sd.derivePathType, +// ); +// final receiveDerivation = +// receiveDerivations[sd.derivePathType]![sd.utxo.address!]; +// +// if (receiveDerivation != null) { +// pubKey = receiveDerivation["pubKey"] as String; +// wif = receiveDerivation["wif"] as String; +// } else { +// // fetch change derivations if null +// changeDerivations[sd.derivePathType] ??= await _fetchDerivations( +// chain: 1, +// derivePathType: sd.derivePathType, +// ); +// final changeDerivation = +// changeDerivations[sd.derivePathType]![sd.utxo.address!]; +// if (changeDerivation != null) { +// pubKey = changeDerivation["pubKey"] as String; +// wif = changeDerivation["wif"] as String; +// } +// } +// +// if (wif == null || pubKey == null) { +// final address = await db.getAddress(walletId, sd.utxo.address!); +// if (address?.derivationPath != null) { +// final node = await Bip32Utils.getBip32Node( +// (await mnemonicString)!, +// (await mnemonicPassphrase)!, +// _network, +// address!.derivationPath!.value, +// ); +// +// wif = node.toWIF(); +// pubKey = Format.uint8listToString(node.publicKey); +// } +// } +// +// if (wif != null && pubKey != null) { +// final PaymentData data; +// final Uint8List? redeemScript; +// +// switch (sd.derivePathType) { +// case DerivePathType.bip44: +// data = P2PKH( +// data: PaymentData( +// pubkey: Format.stringToUint8List(pubKey), +// ), +// network: _network, +// ).data; +// redeemScript = null; +// break; +// +// case DerivePathType.bip49: +// final p2wpkh = P2WPKH( +// data: PaymentData( +// pubkey: Format.stringToUint8List(pubKey), +// ), +// network: _network, +// overridePrefix: _network.bech32!, +// ).data; +// redeemScript = p2wpkh.output; +// data = P2SH( +// data: PaymentData(redeem: p2wpkh), +// network: _network, +// ).data; +// break; +// +// case DerivePathType.bip84: +// data = P2WPKH( +// data: PaymentData( +// pubkey: Format.stringToUint8List(pubKey), +// ), +// network: _network, +// overridePrefix: _network.bech32!, +// ).data; +// redeemScript = null; +// break; +// +// default: +// throw Exception("DerivePathType unsupported"); +// } +// +// final keyPair = ECPair.fromWIF( +// wif, +// network: _network, +// ); +// +// sd.redeemScript = redeemScript; +// sd.output = data.output; +// sd.keyPair = keyPair; +// } +// } +// +// return signingData; +// } catch (e, s) { +// Logging.instance +// .log("fetchBuildTxData() threw: $e,\n$s", level: LogLevel.Error); +// rethrow; +// } +// } +// +// /// Builds and signs a transaction +// Future> buildTransaction({ +// required List utxoSigningData, +// required List recipients, +// required List satoshiAmounts, +// }) async { +// Logging.instance +// .log("Starting buildTransaction ----------", level: LogLevel.Info); +// +// final txb = TransactionBuilder(network: _network); +// txb.setVersion(1); +// +// // Add transaction inputs +// for (var i = 0; i < utxoSigningData.length; i++) { +// final txid = utxoSigningData[i].utxo.txid; +// txb.addInput( +// txid, +// utxoSigningData[i].utxo.vout, +// null, +// utxoSigningData[i].output!, +// _network.bech32!, +// ); +// } +// +// // Add transaction output +// for (var i = 0; i < recipients.length; i++) { +// txb.addOutput(recipients[i], satoshiAmounts[i], _network.bech32!); +// } +// +// try { +// // Sign the transaction accordingly +// for (var i = 0; i < utxoSigningData.length; i++) { +// txb.sign( +// vin: i, +// keyPair: utxoSigningData[i].keyPair!, +// witnessValue: utxoSigningData[i].utxo.value, +// redeemScript: utxoSigningData[i].redeemScript, +// overridePrefix: _network.bech32!, +// ); +// } +// } catch (e, s) { +// Logging.instance.log("Caught exception while signing transaction: $e\n$s", +// level: LogLevel.Error); +// rethrow; +// } +// +// final builtTx = txb.build(_network.bech32!); +// final vSize = builtTx.virtualSize(); +// +// return {"hex": builtTx.toHex(), "vSize": vSize}; +// } +// +// @override +// Future fullRescan( +// int maxUnusedAddressGap, +// int maxNumberOfIndexesToCheck, +// ) async { +// Logging.instance.log("Starting full rescan!", level: LogLevel.Info); +// longMutex = true; +// GlobalEventBus.instance.fire( +// WalletSyncStatusChangedEvent( +// WalletSyncStatus.syncing, +// walletId, +// coin, +// ), +// ); +// +// // clear cache +// await _cachedElectrumXClient.clearSharedTransactionCache(coin: coin); +// +// // back up data +// // await _rescanBackup(); +// +// // clear blockchain info +// await db.deleteWalletBlockchainData(walletId); +// await _deleteDerivations(); +// +// try { +// final _mnemonic = await mnemonicString; +// final _mnemonicPassphrase = await mnemonicPassphrase; +// if (_mnemonicPassphrase == null) { +// Logging.instance.log( +// "Exception in fullRescan: mnemonic passphrase null, possible migration issue; if using internal builds, delete wallet and restore from seed, if using a release build, please file bug report", +// level: LogLevel.Error); +// } +// +// await _recoverWalletFromBIP32SeedPhrase( +// mnemonic: _mnemonic!, +// mnemonicPassphrase: _mnemonicPassphrase!, +// maxUnusedAddressGap: maxUnusedAddressGap, +// maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck, +// isRescan: true, +// ); +// +// longMutex = false; +// await refresh(); +// Logging.instance.log("Full rescan complete!", level: LogLevel.Info); +// GlobalEventBus.instance.fire( +// WalletSyncStatusChangedEvent( +// WalletSyncStatus.synced, +// walletId, +// coin, +// ), +// ); +// } catch (e, s) { +// GlobalEventBus.instance.fire( +// WalletSyncStatusChangedEvent( +// WalletSyncStatus.unableToSync, +// walletId, +// coin, +// ), +// ); +// +// // restore from backup +// // await _rescanRestore(); +// +// longMutex = false; +// Logging.instance.log("Exception rethrown from fullRescan(): $e\n$s", +// level: LogLevel.Error); +// rethrow; +// } +// } +// +// Future _deleteDerivations() async { +// // P2PKH derivations +// await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH"); +// await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH"); +// +// // P2SH derivations +// await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH"); +// await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH"); +// +// // P2WPKH derivations +// await _secureStore.delete(key: "${walletId}_receiveDerivationsP2WPKH"); +// await _secureStore.delete(key: "${walletId}_changeDerivationsP2WPKH"); +// } +// +// // Future _rescanRestore() async { +// // Logging.instance.log("starting rescan restore", level: LogLevel.Info); +// // +// // // restore from backup +// // // p2pkh +// // final tempReceivingAddressesP2PKH = DB.instance +// // .get(boxName: walletId, key: 'receivingAddressesP2PKH_BACKUP'); +// // final tempChangeAddressesP2PKH = DB.instance +// // .get(boxName: walletId, key: 'changeAddressesP2PKH_BACKUP'); +// // final tempReceivingIndexP2PKH = DB.instance +// // .get(boxName: walletId, key: 'receivingIndexP2PKH_BACKUP'); +// // final tempChangeIndexP2PKH = DB.instance +// // .get(boxName: walletId, key: 'changeIndexP2PKH_BACKUP'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingAddressesP2PKH', +// // value: tempReceivingAddressesP2PKH); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeAddressesP2PKH', +// // value: tempChangeAddressesP2PKH); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingIndexP2PKH', +// // value: tempReceivingIndexP2PKH); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeIndexP2PKH', +// // value: tempChangeIndexP2PKH); +// // await DB.instance.delete( +// // key: 'receivingAddressesP2PKH_BACKUP', boxName: walletId); +// // await DB.instance +// // .delete(key: 'changeAddressesP2PKH_BACKUP', boxName: walletId); +// // await DB.instance +// // .delete(key: 'receivingIndexP2PKH_BACKUP', boxName: walletId); +// // await DB.instance +// // .delete(key: 'changeIndexP2PKH_BACKUP', boxName: walletId); +// // +// // // p2Sh +// // final tempReceivingAddressesP2SH = DB.instance +// // .get(boxName: walletId, key: 'receivingAddressesP2SH_BACKUP'); +// // final tempChangeAddressesP2SH = DB.instance +// // .get(boxName: walletId, key: 'changeAddressesP2SH_BACKUP'); +// // final tempReceivingIndexP2SH = DB.instance +// // .get(boxName: walletId, key: 'receivingIndexP2SH_BACKUP'); +// // final tempChangeIndexP2SH = DB.instance +// // .get(boxName: walletId, key: 'changeIndexP2SH_BACKUP'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingAddressesP2SH', +// // value: tempReceivingAddressesP2SH); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeAddressesP2SH', +// // value: tempChangeAddressesP2SH); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingIndexP2SH', +// // value: tempReceivingIndexP2SH); +// // await DB.instance.put( +// // boxName: walletId, key: 'changeIndexP2SH', value: tempChangeIndexP2SH); +// // await DB.instance.delete( +// // key: 'receivingAddressesP2SH_BACKUP', boxName: walletId); +// // await DB.instance +// // .delete(key: 'changeAddressesP2SH_BACKUP', boxName: walletId); +// // await DB.instance +// // .delete(key: 'receivingIndexP2SH_BACKUP', boxName: walletId); +// // await DB.instance +// // .delete(key: 'changeIndexP2SH_BACKUP', boxName: walletId); +// // +// // // p2wpkh +// // final tempReceivingAddressesP2WPKH = DB.instance.get( +// // boxName: walletId, key: 'receivingAddressesP2WPKH_BACKUP'); +// // final tempChangeAddressesP2WPKH = DB.instance +// // .get(boxName: walletId, key: 'changeAddressesP2WPKH_BACKUP'); +// // final tempReceivingIndexP2WPKH = DB.instance +// // .get(boxName: walletId, key: 'receivingIndexP2WPKH_BACKUP'); +// // final tempChangeIndexP2WPKH = DB.instance +// // .get(boxName: walletId, key: 'changeIndexP2WPKH_BACKUP'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingAddressesP2WPKH', +// // value: tempReceivingAddressesP2WPKH); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeAddressesP2WPKH', +// // value: tempChangeAddressesP2WPKH); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingIndexP2WPKH', +// // value: tempReceivingIndexP2WPKH); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeIndexP2WPKH', +// // value: tempChangeIndexP2WPKH); +// // await DB.instance.delete( +// // key: 'receivingAddressesP2WPKH_BACKUP', boxName: walletId); +// // await DB.instance.delete( +// // key: 'changeAddressesP2WPKH_BACKUP', boxName: walletId); +// // await DB.instance +// // .delete(key: 'receivingIndexP2WPKH_BACKUP', boxName: walletId); +// // await DB.instance +// // .delete(key: 'changeIndexP2WPKH_BACKUP', boxName: walletId); +// // +// // // P2PKH derivations +// // final p2pkhReceiveDerivationsString = await _secureStore.read( +// // key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); +// // final p2pkhChangeDerivationsString = await _secureStore.read( +// // key: "${walletId}_changeDerivationsP2PKH_BACKUP"); +// // +// // await _secureStore.write( +// // key: "${walletId}_receiveDerivationsP2PKH", +// // value: p2pkhReceiveDerivationsString); +// // await _secureStore.write( +// // key: "${walletId}_changeDerivationsP2PKH", +// // value: p2pkhChangeDerivationsString); +// // +// // await _secureStore.delete( +// // key: "${walletId}_receiveDerivationsP2PKH_BACKUP"); +// // await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH_BACKUP"); +// // +// // // P2SH derivations +// // final p2shReceiveDerivationsString = await _secureStore.read( +// // key: "${walletId}_receiveDerivationsP2SH_BACKUP"); +// // final p2shChangeDerivationsString = await _secureStore.read( +// // key: "${walletId}_changeDerivationsP2SH_BACKUP"); +// // +// // await _secureStore.write( +// // key: "${walletId}_receiveDerivationsP2SH", +// // value: p2shReceiveDerivationsString); +// // await _secureStore.write( +// // key: "${walletId}_changeDerivationsP2SH", +// // value: p2shChangeDerivationsString); +// // +// // await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH_BACKUP"); +// // await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH_BACKUP"); +// // +// // // P2WPKH derivations +// // final p2wpkhReceiveDerivationsString = await _secureStore.read( +// // key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); +// // final p2wpkhChangeDerivationsString = await _secureStore.read( +// // key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); +// // +// // await _secureStore.write( +// // key: "${walletId}_receiveDerivationsP2WPKH", +// // value: p2wpkhReceiveDerivationsString); +// // await _secureStore.write( +// // key: "${walletId}_changeDerivationsP2WPKH", +// // value: p2wpkhChangeDerivationsString); +// // +// // await _secureStore.delete( +// // key: "${walletId}_receiveDerivationsP2WPKH_BACKUP"); +// // await _secureStore.delete( +// // key: "${walletId}_changeDerivationsP2WPKH_BACKUP"); +// // +// // // UTXOs +// // final utxoData = DB.instance +// // .get(boxName: walletId, key: 'latest_utxo_model_BACKUP'); +// // await DB.instance.put( +// // boxName: walletId, key: 'latest_utxo_model', value: utxoData); +// // await DB.instance +// // .delete(key: 'latest_utxo_model_BACKUP', boxName: walletId); +// // +// // Logging.instance.log("rescan restore complete", level: LogLevel.Info); +// // } +// // +// // Future _rescanBackup() async { +// // Logging.instance.log("starting rescan backup", level: LogLevel.Info); +// // +// // // backup current and clear data +// // // p2pkh +// // final tempReceivingAddressesP2PKH = DB.instance +// // .get(boxName: walletId, key: 'receivingAddressesP2PKH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingAddressesP2PKH_BACKUP', +// // value: tempReceivingAddressesP2PKH); +// // await DB.instance +// // .delete(key: 'receivingAddressesP2PKH', boxName: walletId); +// // +// // final tempChangeAddressesP2PKH = DB.instance +// // .get(boxName: walletId, key: 'changeAddressesP2PKH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeAddressesP2PKH_BACKUP', +// // value: tempChangeAddressesP2PKH); +// // await DB.instance +// // .delete(key: 'changeAddressesP2PKH', boxName: walletId); +// // +// // final tempReceivingIndexP2PKH = +// // DB.instance.get(boxName: walletId, key: 'receivingIndexP2PKH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingIndexP2PKH_BACKUP', +// // value: tempReceivingIndexP2PKH); +// // await DB.instance +// // .delete(key: 'receivingIndexP2PKH', boxName: walletId); +// // +// // final tempChangeIndexP2PKH = +// // DB.instance.get(boxName: walletId, key: 'changeIndexP2PKH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeIndexP2PKH_BACKUP', +// // value: tempChangeIndexP2PKH); +// // await DB.instance +// // .delete(key: 'changeIndexP2PKH', boxName: walletId); +// // +// // // p2sh +// // final tempReceivingAddressesP2SH = DB.instance +// // .get(boxName: walletId, key: 'receivingAddressesP2SH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingAddressesP2SH_BACKUP', +// // value: tempReceivingAddressesP2SH); +// // await DB.instance +// // .delete(key: 'receivingAddressesP2SH', boxName: walletId); +// // +// // final tempChangeAddressesP2SH = +// // DB.instance.get(boxName: walletId, key: 'changeAddressesP2SH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeAddressesP2SH_BACKUP', +// // value: tempChangeAddressesP2SH); +// // await DB.instance +// // .delete(key: 'changeAddressesP2SH', boxName: walletId); +// // +// // final tempReceivingIndexP2SH = +// // DB.instance.get(boxName: walletId, key: 'receivingIndexP2SH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingIndexP2SH_BACKUP', +// // value: tempReceivingIndexP2SH); +// // await DB.instance +// // .delete(key: 'receivingIndexP2SH', boxName: walletId); +// // +// // final tempChangeIndexP2SH = +// // DB.instance.get(boxName: walletId, key: 'changeIndexP2SH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeIndexP2SH_BACKUP', +// // value: tempChangeIndexP2SH); +// // await DB.instance +// // .delete(key: 'changeIndexP2SH', boxName: walletId); +// // +// // // p2wpkh +// // final tempReceivingAddressesP2WPKH = DB.instance +// // .get(boxName: walletId, key: 'receivingAddressesP2WPKH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingAddressesP2WPKH_BACKUP', +// // value: tempReceivingAddressesP2WPKH); +// // await DB.instance +// // .delete(key: 'receivingAddressesP2WPKH', boxName: walletId); +// // +// // final tempChangeAddressesP2WPKH = DB.instance +// // .get(boxName: walletId, key: 'changeAddressesP2WPKH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeAddressesP2WPKH_BACKUP', +// // value: tempChangeAddressesP2WPKH); +// // await DB.instance +// // .delete(key: 'changeAddressesP2WPKH', boxName: walletId); +// // +// // final tempReceivingIndexP2WPKH = DB.instance +// // .get(boxName: walletId, key: 'receivingIndexP2WPKH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'receivingIndexP2WPKH_BACKUP', +// // value: tempReceivingIndexP2WPKH); +// // await DB.instance +// // .delete(key: 'receivingIndexP2WPKH', boxName: walletId); +// // +// // final tempChangeIndexP2WPKH = +// // DB.instance.get(boxName: walletId, key: 'changeIndexP2WPKH'); +// // await DB.instance.put( +// // boxName: walletId, +// // key: 'changeIndexP2WPKH_BACKUP', +// // value: tempChangeIndexP2WPKH); +// // await DB.instance +// // .delete(key: 'changeIndexP2WPKH', boxName: walletId); +// // +// // // P2PKH derivations +// // final p2pkhReceiveDerivationsString = +// // await _secureStore.read(key: "${walletId}_receiveDerivationsP2PKH"); +// // final p2pkhChangeDerivationsString = +// // await _secureStore.read(key: "${walletId}_changeDerivationsP2PKH"); +// // +// // await _secureStore.write( +// // key: "${walletId}_receiveDerivationsP2PKH_BACKUP", +// // value: p2pkhReceiveDerivationsString); +// // await _secureStore.write( +// // key: "${walletId}_changeDerivationsP2PKH_BACKUP", +// // value: p2pkhChangeDerivationsString); +// // +// // await _secureStore.delete(key: "${walletId}_receiveDerivationsP2PKH"); +// // await _secureStore.delete(key: "${walletId}_changeDerivationsP2PKH"); +// // +// // // P2SH derivations +// // final p2shReceiveDerivationsString = +// // await _secureStore.read(key: "${walletId}_receiveDerivationsP2SH"); +// // final p2shChangeDerivationsString = +// // await _secureStore.read(key: "${walletId}_changeDerivationsP2SH"); +// // +// // await _secureStore.write( +// // key: "${walletId}_receiveDerivationsP2SH_BACKUP", +// // value: p2shReceiveDerivationsString); +// // await _secureStore.write( +// // key: "${walletId}_changeDerivationsP2SH_BACKUP", +// // value: p2shChangeDerivationsString); +// // +// // await _secureStore.delete(key: "${walletId}_receiveDerivationsP2SH"); +// // await _secureStore.delete(key: "${walletId}_changeDerivationsP2SH"); +// // +// // // P2WPKH derivations +// // final p2wpkhReceiveDerivationsString = +// // await _secureStore.read(key: "${walletId}_receiveDerivationsP2WPKH"); +// // final p2wpkhChangeDerivationsString = +// // await _secureStore.read(key: "${walletId}_changeDerivationsP2WPKH"); +// // +// // await _secureStore.write( +// // key: "${walletId}_receiveDerivationsP2WPKH_BACKUP", +// // value: p2wpkhReceiveDerivationsString); +// // await _secureStore.write( +// // key: "${walletId}_changeDerivationsP2WPKH_BACKUP", +// // value: p2wpkhChangeDerivationsString); +// // +// // await _secureStore.delete(key: "${walletId}_receiveDerivationsP2WPKH"); +// // await _secureStore.delete(key: "${walletId}_changeDerivationsP2WPKH"); +// // +// // // UTXOs +// // final utxoData = +// // DB.instance.get(boxName: walletId, key: 'latest_utxo_model'); +// // await DB.instance.put( +// // boxName: walletId, key: 'latest_utxo_model_BACKUP', value: utxoData); +// // await DB.instance +// // .delete(key: 'latest_utxo_model', boxName: walletId); +// // +// // Logging.instance.log("rescan backup complete", level: LogLevel.Info); +// // } +// +// bool isActive = false; +// +// @override +// void Function(bool)? get onIsActiveWalletChanged => +// (isActive) => this.isActive = isActive; +// +// @override +// Future estimateFeeFor(Amount amount, int feeRate) async { +// final available = balance.spendable; +// +// if (available == amount) { +// return amount - (await sweepAllEstimate(feeRate)); +// } else if (amount <= Amount.zero || amount > available) { +// return roughFeeEstimate(1, 2, feeRate); +// } +// +// Amount runningBalance = Amount( +// rawValue: BigInt.zero, +// fractionDigits: coin.decimals, +// ); +// int inputCount = 0; +// for (final output in (await utxos)) { +// if (!output.isBlocked) { +// runningBalance = runningBalance + +// Amount( +// rawValue: BigInt.from(output.value), +// fractionDigits: coin.decimals, +// ); +// inputCount++; +// if (runningBalance > amount) { +// break; +// } +// } +// } +// +// final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); +// final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); +// +// if (runningBalance - amount > oneOutPutFee) { +// if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { +// final change = runningBalance - amount - twoOutPutFee; +// if (change > DUST_LIMIT && +// runningBalance - amount - change == twoOutPutFee) { +// return runningBalance - amount - change; +// } else { +// return runningBalance - amount; +// } +// } else { +// return runningBalance - amount; +// } +// } else if (runningBalance - amount == oneOutPutFee) { +// return oneOutPutFee; +// } else { +// return twoOutPutFee; +// } +// } +// +// Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { +// return Amount( +// rawValue: BigInt.from( +// ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * +// (feeRatePerKB / 1000).ceil()), +// fractionDigits: coin.decimals, +// ); +// } +// +// Future sweepAllEstimate(int feeRate) async { +// int available = 0; +// int inputCount = 0; +// for (final output in (await utxos)) { +// if (!output.isBlocked && +// output.isConfirmed(storedChainHeight, MINIMUM_CONFIRMATIONS)) { +// available += output.value; +// inputCount++; +// } +// } +// +// // transaction will only have 1 output minus the fee +// final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); +// +// return Amount( +// rawValue: BigInt.from(available), +// fractionDigits: coin.decimals, +// ) - +// estimatedFee; +// } +// +// @override +// Future generateNewAddress() async { +// try { +// final currentReceiving = await _currentReceivingAddress; +// +// final newReceivingIndex = currentReceiving.derivationIndex + 1; +// +// // Use new index to derive a new receiving address +// final newReceivingAddress = await _generateAddressForChain( +// 0, newReceivingIndex, DerivePathTypeExt.primaryFor(coin)); +// +// // Add that new receiving address +// await db.putAddress(newReceivingAddress); +// +// return true; +// } catch (e, s) { +// Logging.instance.log( +// "Exception rethrown from generateNewAddress(): $e\n$s", +// level: LogLevel.Error); +// return false; +// } +// } +// +// @override +// Future get xpub async { +// final node = await Bip32Utils.getBip32Root( +// (await mnemonic).join(" "), +// await mnemonicPassphrase ?? "", +// _network, +// ); +// +// return node.neutered().toBase58(); +// } +// } +// +// final litecoin = NetworkType( +// messagePrefix: '\x19Litecoin Signed Message:\n', +// bech32: 'ltc', +// bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), +// pubKeyHash: 0x30, +// scriptHash: 0x32, +// wif: 0xb0); +// +// final litecointestnet = NetworkType( +// messagePrefix: '\x19Litecoin Signed Message:\n', +// bech32: 'tltc', +// bip32: Bip32Type(public: 0x043587cf, private: 0x04358394), +// pubKeyHash: 0x6f, +// scriptHash: 0x3a, +// wif: 0xef); diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 5b17f3b48..397bf04df 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -1,94 +1,48 @@ -import 'dart:async'; - -import 'package:isar/isar.dart'; -import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:stackwallet/dto/ordinals/inscription_data.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; -import 'package:stackwallet/models/isar/ordinal.dart'; -import 'package:stackwallet/services/litescribe_api.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; - mixin OrdinalsInterface { - late final String _walletId; - late final Coin _coin; - late final MainDB _db; - - void initOrdinalsInterface({ - required String walletId, - required Coin coin, - required MainDB db, - }) { - _walletId = walletId; - _coin = coin; - _db = db; - } - - final LitescribeAPI litescribeAPI = - LitescribeAPI(baseUrl: 'https://litescribe.io/api'); - - Future refreshInscriptions() async { - final uniqueAddresses = await _db - .getUTXOs(_walletId) - .filter() - .addressIsNotNull() - .distinctByAddress() - .addressProperty() - .findAll(); - final inscriptions = - await _getInscriptionDataFromAddresses(uniqueAddresses.cast()); - - final ords = inscriptions - .map((e) => Ordinal.fromInscriptionData(e, _walletId)) - .toList(); - - await _db.isar.writeTxn(() async { - await _db.isar.ordinals - .where() - .filter() - .walletIdEqualTo(_walletId) - .deleteAll(); - await _db.isar.ordinals.putAll(ords); - }); - } - - Future> _getInscriptionDataFromAddresses( - List addresses) async { - List allInscriptions = []; - for (String address in addresses) { - try { - var inscriptions = - await litescribeAPI.getInscriptionsByAddress(address); - allInscriptions.addAll(inscriptions); - } catch (e) { - throw Exception("Error fetching inscriptions for address $address: $e"); - } - } - return allInscriptions; - } - - // check if an inscription is in a given output - Future inscriptionInOutput(UTXO output) async { - if (output.address != null) { - var inscriptions = - await litescribeAPI.getInscriptionsByAddress("${output.address}"); - if (inscriptions.isNotEmpty) { - return true; - } else { - return false; - } - } else { - throw UnimplementedError( - 'TODO look up utxo without address. utxo->txid:output->address'); - } - } - - // check if an inscription is in a given output - Future inscriptionInAddress(String address) async { - var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); - if (inscriptions.isNotEmpty) { - return true; - } else { - return false; - } - } + // late final String _walletId; + // late final Coin _coin; + // late final MainDB _db; + // + // void initOrdinalsInterface({ + // required String walletId, + // required Coin coin, + // required MainDB db, + // }) { + // _walletId = walletId; + // _coin = coin; + // _db = db; + // } + // + // final LitescribeAPI litescribeAPI = + // LitescribeAPI(baseUrl: 'https://litescribe.io/api'); + // + // + // + // + // + // // // check if an inscription is in a given output + // // Future inscriptionInOutput(UTXO output) async { + // // if (output.address != null) { + // // var inscriptions = + // // await litescribeAPI.getInscriptionsByAddress("${output.address}"); + // // if (inscriptions.isNotEmpty) { + // // return true; + // // } else { + // // return false; + // // } + // // } else { + // // throw UnimplementedError( + // // 'TODO look up utxo without address. utxo->txid:output->address'); + // // } + // // } + // + // // check if an inscription is in a given output + // Future inscriptionInAddress(String address) async { + // var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + // if (inscriptions.isNotEmpty) { + // return true; + // } else { + // return false; + // } + // } } diff --git a/lib/wallets/wallet/impl/bitcoin_wallet.dart b/lib/wallets/wallet/impl/bitcoin_wallet.dart index d458d3736..e12e3fec3 100644 --- a/lib/wallets/wallet/impl/bitcoin_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_wallet.dart @@ -67,11 +67,13 @@ class BitcoinWallet extends Bip39HDWallet } @override - ({String? blockedReason, bool blocked}) checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map? jsonTX, - ) { + String? utxoOwnerAddress, + ) async { bool blocked = false; String? blockedReason; @@ -97,7 +99,7 @@ class BitcoinWallet extends Bip39HDWallet } } - return (blockedReason: blockedReason, blocked: blocked); + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); } @override diff --git a/lib/wallets/wallet/impl/bitcoincash_wallet.dart b/lib/wallets/wallet/impl/bitcoincash_wallet.dart index 0edf16cb1..71711c347 100644 --- a/lib/wallets/wallet/impl/bitcoincash_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoincash_wallet.dart @@ -303,11 +303,13 @@ class BitcoincashWallet extends Bip39HDWallet } @override - ({String? blockedReason, bool blocked}) checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, - ) { + String? utxoOwnerAddress, + ) async { bool blocked = false; String? blockedReason; @@ -337,7 +339,7 @@ class BitcoincashWallet extends Bip39HDWallet } } - return (blockedReason: blockedReason, blocked: blocked); + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); } // TODO: correct formula for bch? diff --git a/lib/wallets/wallet/impl/dogecoin_wallet.dart b/lib/wallets/wallet/impl/dogecoin_wallet.dart index 611b80d29..ec5de124d 100644 --- a/lib/wallets/wallet/impl/dogecoin_wallet.dart +++ b/lib/wallets/wallet/impl/dogecoin_wallet.dart @@ -63,11 +63,13 @@ class DogecoinWallet extends Bip39HDWallet } @override - ({String? blockedReason, bool blocked}) checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, - ) { + String? utxoOwnerAddress, + ) async { bool blocked = false; String? blockedReason; @@ -91,7 +93,7 @@ class DogecoinWallet extends Bip39HDWallet } } - return (blockedReason: blockedReason, blocked: blocked); + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); } @override diff --git a/lib/wallets/wallet/impl/ecash_wallet.dart b/lib/wallets/wallet/impl/ecash_wallet.dart index 802c21b77..e49baa3f3 100644 --- a/lib/wallets/wallet/impl/ecash_wallet.dart +++ b/lib/wallets/wallet/impl/ecash_wallet.dart @@ -298,11 +298,13 @@ class EcashWallet extends Bip39HDWallet } @override - ({String? blockedReason, bool blocked}) checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, - ) { + String? utxoOwnerAddress, + ) async { bool blocked = false; String? blockedReason; @@ -332,7 +334,7 @@ class EcashWallet extends Bip39HDWallet } } - return (blockedReason: blockedReason, blocked: blocked); + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); } // TODO: correct formula for ecash? diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 4d842c8e6..19f949ec4 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -57,13 +57,13 @@ class EpiccashWallet extends Bip39Wallet { } @override - Future updateUTXOs() { + Future updateUTXOs() { // TODO: implement updateUTXOs throw UnimplementedError(); } @override - Future updateNode() { + Future updateNode() { // TODO: implement updateNode throw UnimplementedError(); } diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 97f1b192a..e77d1d3e9 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -494,11 +494,13 @@ class FiroWallet extends Bip39HDWallet } @override - ({String? blockedReason, bool blocked}) checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map? jsonTX, - ) { + String? utxoOwnerAddress, + ) async { bool blocked = false; String? blockedReason; // @@ -524,7 +526,7 @@ class FiroWallet extends Bip39HDWallet // } // } // - return (blockedReason: blockedReason, blocked: blocked); + return (blockedReason: blockedReason, blocked: blocked, utxoLabel: null); } @override diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart new file mode 100644 index 000000000..b475c4011 --- /dev/null +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -0,0 +1,143 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/litecoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; +import 'package:tuple/tuple.dart'; + +class LitecoinWallet extends Bip39HDWallet + with ElectrumXInterface, CoinControlInterface, OrdinalsInterface { + @override + int get isarTransactionVersion => 1; // TODO actually set this to 2 + + LitecoinWallet(CryptoCurrencyNetwork network) : super(Litecoin(network)); + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + // =========================================================================== + + @override + Future> fetchAddressesForElectrumXScan() async { + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); + return allAddresses; + } + + // =========================================================================== + + @override + Future updateTransactions() async { + final currentChainHeight = await fetchChainHeight(); + + // TODO: [prio=med] switch to V2 transactions + final data = await fetchTransactionsV1( + addresses: await fetchAddressesForElectrumXScan(), + currentChainHeight: currentChainHeight, + ); + + await mainDB.addNewTransactionData( + data + .map((e) => Tuple2( + e.transaction, + e.address, + )) + .toList(), + walletId, + ); + } + + @override + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + @override + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } +// +// @override +// Future coinSelection({required TxData txData}) async { +// final isCoinControl = txData.utxos != null; +// final isSendAll = txData.amount == info.cachedBalance.spendable; +// +// final utxos = +// txData.utxos?.toList() ?? await mainDB.getUTXOs(walletId).findAll(); +// +// final currentChainHeight = await chainHeight; +// final List spendableOutputs = []; +// int spendableSatoshiValue = 0; +// +// // Build list of spendable outputs and totaling their satoshi amount +// for (final utxo in utxos) { +// if (utxo.isBlocked == false && +// utxo.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms) && +// utxo.used != true) { +// spendableOutputs.add(utxo); +// spendableSatoshiValue += utxo.value; +// } +// } +// +// if (isCoinControl && spendableOutputs.length < utxos.length) { +// throw ArgumentError("Attempted to use an unavailable utxo"); +// } +// +// if (spendableSatoshiValue < txData.amount!.raw.toInt()) { +// throw Exception("Insufficient balance"); +// } else if (spendableSatoshiValue == txData.amount!.raw.toInt() && +// !isSendAll) { +// throw Exception("Insufficient balance to pay transaction fee"); +// } +// +// if (isCoinControl) { +// } else { +// final selection = cs.coinSelection( +// spendableOutputs +// .map((e) => cs.InputModel( +// i: e.vout, +// txid: e.txid, +// value: e.value, +// address: e.address, +// )) +// .toList(), +// txData.recipients! +// .map((e) => cs.OutputModel( +// address: e.address, +// value: e.amount.raw.toInt(), +// )) +// .toList(), +// txData.feeRateAmount!, +// 10, // TODO: ??????????????????????????????? +// ); +// +// // .inputs and .outputs will be null if no solution was found +// if (selection.inputs!.isEmpty || selection.outputs!.isEmpty) { +// throw Exception("coin selection failed"); +// } +// } +// } +} diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index 64c178932..794c40006 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -599,7 +599,8 @@ class TezosWallet extends Bip39Wallet { } @override - Future updateUTXOs() async { + Future updateUTXOs() async { // do nothing. Not used in tezos + return false; } } diff --git a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart index 12fafb18c..ab988eb23 100644 --- a/lib/wallets/wallet/intermediate/cryptonote_wallet.dart +++ b/lib/wallets/wallet/intermediate/cryptonote_wallet.dart @@ -1,7 +1,7 @@ import 'package:stackwallet/wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; -import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import 'package:stackwallet/wallets/wallet/wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; abstract class CryptonoteWallet extends Wallet with MnemonicInterface { @@ -28,7 +28,8 @@ abstract class CryptonoteWallet extends Wallet } @override - Future updateUTXOs() async { + Future updateUTXOs() async { // do nothing for now + return false; } } diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 54a1b0aa5..32e84838f 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -29,6 +29,7 @@ import 'package:stackwallet/wallets/wallet/impl/dogecoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/ecash_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/litecoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/tezos_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart'; @@ -273,6 +274,11 @@ abstract class Wallet { case Coin.firoTestNet: return FiroWallet(CryptoCurrencyNetwork.test); + case Coin.litecoin: + return LitecoinWallet(CryptoCurrencyNetwork.main); + case Coin.litecoinTestNet: + return LitecoinWallet(CryptoCurrencyNetwork.test); + case Coin.nano: return NanoWallet(CryptoCurrencyNetwork.main); @@ -360,9 +366,11 @@ abstract class Wallet { Future updateNode(); Future updateTransactions(); - Future updateUTXOs(); Future updateBalance(); + // returns true if new utxos were added to local db + Future updateUTXOs(); + /// updates the wallet info's cachedChainHeight Future updateChainHeight(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index cd7e590ec..9dcb2b8e5 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -647,6 +647,7 @@ mixin ElectrumXInterface on Bip39HDWallet { utxoSigningData[i].utxo.vout, null, utxoSigningData[i].output!, + cryptoCurrency.networkParams.bech32Hrp, ); } @@ -655,6 +656,7 @@ mixin ElectrumXInterface on Bip39HDWallet { txb.addOutput( txData.recipients![i].address, txData.recipients![i].amount.raw.toInt(), + cryptoCurrency.networkParams.bech32Hrp, ); } @@ -666,6 +668,7 @@ mixin ElectrumXInterface on Bip39HDWallet { keyPair: utxoSigningData[i].keyPair!, witnessValue: utxoSigningData[i].utxo.value, redeemScript: utxoSigningData[i].redeemScript, + overridePrefix: cryptoCurrency.networkParams.bech32Hrp, ); } } catch (e, s) { @@ -674,7 +677,7 @@ mixin ElectrumXInterface on Bip39HDWallet { rethrow; } - final builtTx = txb.build(); + final builtTx = txb.build(cryptoCurrency.networkParams.bech32Hrp); final vSize = builtTx.virtualSize(); return txData.copyWith( @@ -1051,14 +1054,19 @@ mixin ElectrumXInterface on Bip39HDWallet { } } - final checkBlockResult = checkBlockUTXO(jsonUTXO, scriptPubKey, txn); + final checkBlockResult = await checkBlockUTXO( + jsonUTXO, + scriptPubKey, + txn, + utxoOwnerAddress, + ); final utxo = UTXO( walletId: walletId, txid: txn["txid"] as String, vout: vout, value: jsonUTXO["value"] as int, - name: "", + name: checkBlockResult.utxoLabel ?? "", isBlocked: checkBlockResult.blocked, blockedReason: checkBlockResult.blockedReason, isCoinbase: txn["is_coinbase"] as bool? ?? false, @@ -1650,7 +1658,7 @@ mixin ElectrumXInterface on Bip39HDWallet { } @override - Future updateUTXOs() async { + Future updateUTXOs() async { final allAddresses = await fetchAddressesForElectrumXScan(); try { @@ -1710,12 +1718,13 @@ mixin ElectrumXInterface on Bip39HDWallet { } } - await mainDB.updateUTXOs(walletId, outputArray); + return await mainDB.updateUTXOs(walletId, outputArray); } catch (e, s) { Logging.instance.log( "Output fetch unsuccessful: $e\n$s", level: LogLevel.Error, ); + return false; } } @@ -1870,10 +1879,12 @@ mixin ElectrumXInterface on Bip39HDWallet { /// Certain coins need to check if the utxo should be marked /// as blocked as well as give a reason. - ({String? blockedReason, bool blocked}) checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, + String? utxoOwnerAddress, ); // =========================================================================== diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart index 09e5543b9..ab1ea55fc 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart @@ -651,8 +651,9 @@ mixin NanoInterface on Bip39Wallet { FilterGroup.and(standardReceivingAddressFilters); @override - Future updateUTXOs() async { + Future updateUTXOs() async { // do nothing for nano based coins + return false; } @override diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart new file mode 100644 index 000000000..d0a741513 --- /dev/null +++ b/lib/wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart @@ -0,0 +1,109 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/dto/ordinals/inscription_data.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/models/isar/ordinal.dart'; +import 'package:stackwallet/services/litescribe_api.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; + +mixin OrdinalsInterface on ElectrumXInterface { + final LitescribeAPI litescribeAPI = + LitescribeAPI(baseUrl: 'https://litescribe.io/api'); + + // check if an inscription is in a given output + Future _inscriptionInAddress(String address) async { + var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + if (inscriptions.isNotEmpty) { + return true; + } else { + return false; + } + } + + Future refreshInscriptions() async { + final uniqueAddresses = await mainDB + .getUTXOs(walletId) + .filter() + .addressIsNotNull() + .distinctByAddress() + .addressProperty() + .findAll(); + final inscriptions = + await _getInscriptionDataFromAddresses(uniqueAddresses.cast()); + + final ords = inscriptions + .map((e) => Ordinal.fromInscriptionData(e, walletId)) + .toList(); + + await mainDB.isar.writeTxn(() async { + await mainDB.isar.ordinals + .where() + .filter() + .walletIdEqualTo(walletId) + .deleteAll(); + await mainDB.isar.ordinals.putAll(ords); + }); + } + // =================== Overrides ============================================= + + @override + Future<({bool blocked, String? blockedReason, String? utxoLabel})> + checkBlockUTXO( + Map jsonUTXO, + String? scriptPubKeyHex, + Map jsonTX, + String? utxoOwnerAddress, + ) async { + bool shouldBlock = false; + String? blockReason; + String? label; + + final utxoAmount = jsonUTXO["value"] as int; + + // TODO: [prio=med] check following 3 todos + + // TODO check the specific output, not just the address in general + // TODO optimize by freezing output in OrdinalsInterface, so one ordinal API calls is made (or at least many less) + if (utxoOwnerAddress != null) { + if (await _inscriptionInAddress(utxoOwnerAddress)) { + shouldBlock = true; + blockReason = "Ordinal"; + label = "Ordinal detected at address"; + } + } else { + // TODO implement inscriptionInOutput + if (utxoAmount <= 10000) { + shouldBlock = true; + blockReason = "May contain ordinal"; + label = "Possible ordinal"; + } + } + + return (blockedReason: blockReason, blocked: shouldBlock, utxoLabel: label); + } + + @override + Future updateUTXOs() async { + final newUtxosAdded = await super.updateUTXOs(); + if (newUtxosAdded) { + await refreshInscriptions(); + } + + return newUtxosAdded; + } + + // ===================== Private ============================================= + Future> _getInscriptionDataFromAddresses( + List addresses) async { + List allInscriptions = []; + for (String address in addresses) { + try { + var inscriptions = + await litescribeAPI.getInscriptionsByAddress(address); + allInscriptions.addAll(inscriptions); + } catch (e) { + throw Exception("Error fetching inscriptions for address $address: $e"); + } + } + return allInscriptions; + } +} diff --git a/pubspec.lock b/pubspec.lock index 33f80fe24..4df3339c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -280,7 +280,7 @@ packages: source: git version: "1.1.0" coinlib_flutter: - dependency: "direct overridden" + dependency: "direct dev" description: path: coinlib_flutter ref: "4f549b8b511a63fdc1f44796ab43b10f586635cd" diff --git a/pubspec.yaml b/pubspec.yaml index f162490ab..d8c428e3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -186,6 +186,11 @@ dev_dependencies: import_sorter: ^4.6.0 flutter_lints: ^2.0.1 isar_generator: 3.0.5 + coinlib_flutter: + git: + url: https://github.com/cypherstack/coinlib.git + path: coinlib_flutter + ref: 4f549b8b511a63fdc1f44796ab43b10f586635cd flutter_launcher_icons: android: true