mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-18 02:07:43 +00:00
1931 lines
70 KiB
Dart
1931 lines
70 KiB
Dart
// /*
|
|
// * 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 'dart:math';
|
|
//
|
|
// 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: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.dart';
|
|
// import 'package:stackwallet/electrumx_rpc/electrumx.dart';
|
|
// import 'package:stackwallet/exceptions/electrumx/no_such_transaction.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/paynym_wallet_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/address_utils.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/paynym_is_api.dart';
|
|
// import 'package:stackwallet/utilities/prefs.dart';
|
|
// import 'package:stackwallet/widgets/crypto_notifications.dart';
|
|
// import 'package:tuple/tuple.dart';
|
|
// import 'package:uuid/uuid.dart';
|
|
//
|
|
// class BitcoinWallet extends CoinServiceAPI
|
|
// with
|
|
// WalletCache,
|
|
// WalletDB,
|
|
// ElectrumXParsing,
|
|
// PaynymWalletInterface,
|
|
// CoinControlInterface
|
|
// implements XPubAble {
|
|
// BitcoinWallet({
|
|
// required String walletId,
|
|
// required String walletName,
|
|
// required Coin coin,
|
|
// required ElectrumX client,
|
|
// required CachedElectrumX 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);
|
|
// initCoinControlInterface(
|
|
// walletId: walletId,
|
|
// walletName: walletName,
|
|
// coin: coin,
|
|
// db: db,
|
|
// getChainHeight: () => chainHeight,
|
|
// refreshedBalanceCallback: (balance) async {
|
|
// _balance = balance;
|
|
// await updateCachedBalance(_balance!);
|
|
// },
|
|
// );
|
|
// initPaynymWalletInterface(
|
|
// walletId: walletId,
|
|
// walletName: walletName,
|
|
// network: _network,
|
|
// coin: coin,
|
|
// db: db,
|
|
// electrumXClient: electrumXClient,
|
|
// secureStorage: secureStore,
|
|
// getMnemonicString: () => mnemonicString,
|
|
// getMnemonicPassphrase: () => mnemonicPassphrase,
|
|
// getChainHeight: () => chainHeight,
|
|
// // getCurrentChangeAddress: () => currentChangeAddressP2PKH,
|
|
// getCurrentChangeAddress: () => currentChangeAddress,
|
|
// estimateTxFee: estimateTxFee,
|
|
// prepareSend: prepareSend,
|
|
// getTxCount: getTxCount,
|
|
// fetchBuildTxData: fetchBuildTxData,
|
|
// refresh: refresh,
|
|
// checkChangeAddressForTransactions: _checkChangeAddressForTransactions,
|
|
// // checkChangeAddressForTransactions:
|
|
// // _checkP2PKHChangeAddressForTransactions,
|
|
// dustLimitP2PKH: DUST_LIMIT_P2PKH.raw.toInt(),
|
|
// minConfirms: MINIMUM_CONFIRMATIONS,
|
|
// dustLimit: DUST_LIMIT.raw.toInt(),
|
|
// );
|
|
// }
|
|
//
|
|
// 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.bitcoin:
|
|
// return bitcoin;
|
|
// case Coin.bitcoinTestNet:
|
|
// return testnet;
|
|
// 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<List<isar_models.UTXO>> get utxos => db.getUTXOs(walletId).findAll();
|
|
//
|
|
// @override
|
|
// Future<List<isar_models.Transaction>> get transactions => db
|
|
// .getTransactions(walletId)
|
|
// .filter()
|
|
// .not()
|
|
// .group((q) => q
|
|
// .subTypeEqualTo(isar_models.TransactionSubType.bip47Notification)
|
|
// .and()
|
|
// .typeEqualTo(isar_models.TransactionType.incoming))
|
|
// .sortByTimestampDesc()
|
|
// .findAll();
|
|
//
|
|
// @override
|
|
// Future<String> get currentReceivingAddress async =>
|
|
// (await _currentReceivingAddress).value;
|
|
//
|
|
// Future<isar_models.Address> 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<String> get currentChangeAddress async =>
|
|
// (await _currentChangeAddress).value;
|
|
//
|
|
// Future<isar_models.Address> 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));
|
|
//
|
|
// Future<String> get currentChangeAddressP2PKH async =>
|
|
// (await _currentChangeAddressP2PKH).value;
|
|
//
|
|
// Future<isar_models.Address> get _currentChangeAddressP2PKH async =>
|
|
// (await db
|
|
// .getAddresses(walletId)
|
|
// .filter()
|
|
// .typeEqualTo(isar_models.AddressType.p2pkh)
|
|
// .subTypeEqualTo(isar_models.AddressSubType.change)
|
|
// .sortByDerivationIndexDesc()
|
|
// .findFirst()) ??
|
|
// await _generateAddressForChain(1, 0, DerivePathType.bip44);
|
|
//
|
|
// @override
|
|
// Future<void> exit() async {
|
|
// _hasCalledExit = true;
|
|
// timer?.cancel();
|
|
// timer = null;
|
|
// stopNetworkAlivePinging();
|
|
// }
|
|
//
|
|
//
|
|
// bool longMutex = false;
|
|
//
|
|
// @override
|
|
// Future<void> 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.bitcoin:
|
|
// if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
|
|
// throw Exception("genesis hash does not match main net!");
|
|
// }
|
|
// break;
|
|
// case Coin.bitcoinTestNet:
|
|
// if (features['genesis_hash'] != GENESIS_HASH_TESTNET) {
|
|
// throw Exception("genesis hash does not match test net!");
|
|
// }
|
|
// break;
|
|
// default:
|
|
// throw Exception(
|
|
// "Attempted to generate a BitcoinWallet using a non bitcoin coin type: ${coin.name}");
|
|
// }
|
|
// }
|
|
// // 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<Tuple3<List<isar_models.Address>, DerivePathType, int>> _checkGaps(
|
|
// int maxNumberOfIndexesToCheck,
|
|
// int maxUnusedAddressGap,
|
|
// int txCountBatchSize,
|
|
// bip32.BIP32 root,
|
|
// DerivePathType type,
|
|
// int chain,
|
|
// ) async {
|
|
// List<isar_models.Address> addressArray = [];
|
|
// int gapCounter = 0;
|
|
// int highestIndexWithHistory = 0;
|
|
//
|
|
// for (int index = 0;
|
|
// index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap;
|
|
// index += txCountBatchSize) {
|
|
// List<String> iterationsAddressArray = [];
|
|
// Logging.instance.log(
|
|
// "index: $index, \t GapCounter $chain ${type.name}: $gapCounter",
|
|
// level: LogLevel.Info);
|
|
//
|
|
// final _id = "k_$index";
|
|
// Map<String, String> txCountCallArgs = {};
|
|
//
|
|
// 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).data),
|
|
// network: _network)
|
|
// .data
|
|
// .address!;
|
|
// addrType = isar_models.AddressType.p2sh;
|
|
// break;
|
|
// case DerivePathType.bip84:
|
|
// addressString = P2WPKH(network: _network, data: data).data.address!;
|
|
// addrType = isar_models.AddressType.p2wpkh;
|
|
// break;
|
|
// default:
|
|
// throw Exception("DerivePathType $type not supported");
|
|
// }
|
|
//
|
|
// 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,
|
|
// );
|
|
//
|
|
// addressArray.add(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) {
|
|
// iterationsAddressArray.add(txCountCallArgs["${_id}_$k"]!);
|
|
//
|
|
// // update highest
|
|
// highestIndexWithHistory = index + k;
|
|
//
|
|
// // reset counter
|
|
// gapCounter = 0;
|
|
// }
|
|
//
|
|
// // 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 Tuple3(addressArray, type, highestIndexWithHistory);
|
|
// }
|
|
//
|
|
// Future<void> getTransactionCacheEarly(List<String> allAddresses) async {
|
|
// try {
|
|
// final List<Map<String, dynamic>> 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<void> _recoverWalletFromBIP32SeedPhrase({
|
|
// required String mnemonic,
|
|
// required String mnemonicPassphrase,
|
|
// int maxUnusedAddressGap = 20,
|
|
// int maxNumberOfIndexesToCheck = 1000,
|
|
// bool isRescan = false,
|
|
// }) async {
|
|
// longMutex = true;
|
|
//
|
|
// final root = await Bip32Utils.getBip32Root(
|
|
// mnemonic,
|
|
// mnemonicPassphrase,
|
|
// _network,
|
|
// );
|
|
//
|
|
// final deriveTypes = [
|
|
// DerivePathType.bip44,
|
|
// DerivePathType.bip49,
|
|
// DerivePathType.bip84,
|
|
// ];
|
|
//
|
|
// final List<Future<Tuple3<List<isar_models.Address>, DerivePathType, int>>>
|
|
// receiveFutures = [];
|
|
// final List<Future<Tuple3<List<isar_models.Address>, DerivePathType, int>>>
|
|
// changeFutures = [];
|
|
//
|
|
// const receiveChain = 0;
|
|
// const changeChain = 1;
|
|
// const indexZero = 0;
|
|
//
|
|
// // 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,
|
|
// );
|
|
//
|
|
// for (final type in deriveTypes) {
|
|
// receiveFutures.add(
|
|
// _checkGaps(
|
|
// maxNumberOfIndexesToCheck,
|
|
// maxUnusedAddressGap,
|
|
// txCountBatchSize,
|
|
// root,
|
|
// type,
|
|
// receiveChain,
|
|
// ),
|
|
// );
|
|
// }
|
|
//
|
|
// // change addresses
|
|
// Logging.instance.log(
|
|
// "checking change addresses...",
|
|
// level: LogLevel.Info,
|
|
// );
|
|
// for (final type in deriveTypes) {
|
|
// changeFutures.add(
|
|
// _checkGaps(
|
|
// maxNumberOfIndexesToCheck,
|
|
// maxUnusedAddressGap,
|
|
// txCountBatchSize,
|
|
// root,
|
|
// type,
|
|
// changeChain,
|
|
// ),
|
|
// );
|
|
// }
|
|
//
|
|
// // io limitations may require running these linearly instead
|
|
// final futuresResult = await Future.wait([
|
|
// Future.wait(receiveFutures),
|
|
// Future.wait(changeFutures),
|
|
// ]);
|
|
//
|
|
// final receiveResults = futuresResult[0];
|
|
// final changeResults = futuresResult[1];
|
|
//
|
|
// final List<isar_models.Address> addressesToStore = [];
|
|
//
|
|
// int highestReceivingIndexWithHistory = 0;
|
|
// // 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
|
|
// for (final tuple in receiveResults) {
|
|
// if (tuple.item1.isEmpty) {
|
|
// final address = await _generateAddressForChain(
|
|
// receiveChain,
|
|
// indexZero,
|
|
// tuple.item2,
|
|
// );
|
|
// addressesToStore.add(address);
|
|
// } else {
|
|
// highestReceivingIndexWithHistory =
|
|
// max(tuple.item3, highestReceivingIndexWithHistory);
|
|
// addressesToStore.addAll(tuple.item1);
|
|
// }
|
|
// }
|
|
//
|
|
// int highestChangeIndexWithHistory = 0;
|
|
// // 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.
|
|
// for (final tuple in changeResults) {
|
|
// if (tuple.item1.isEmpty) {
|
|
// final address = await _generateAddressForChain(
|
|
// changeChain,
|
|
// indexZero,
|
|
// tuple.item2,
|
|
// );
|
|
// addressesToStore.add(address);
|
|
// } else {
|
|
// highestChangeIndexWithHistory =
|
|
// max(tuple.item3, highestChangeIndexWithHistory);
|
|
// addressesToStore.addAll(tuple.item1);
|
|
// }
|
|
// }
|
|
//
|
|
// // remove extra addresses to help minimize risk of creating a large gap
|
|
// addressesToStore.removeWhere((e) =>
|
|
// e.subType == isar_models.AddressSubType.change &&
|
|
// e.derivationIndex > highestChangeIndexWithHistory);
|
|
// addressesToStore.removeWhere((e) =>
|
|
// e.subType == isar_models.AddressSubType.receiving &&
|
|
// e.derivationIndex > highestReceivingIndexWithHistory);
|
|
//
|
|
// if (isRescan) {
|
|
// await db.updateOrPutAddresses(addressesToStore);
|
|
// } else {
|
|
// await db.putAddresses(addressesToStore);
|
|
// }
|
|
//
|
|
// // get own payment code
|
|
// // isSegwit does not matter here at all
|
|
// final myCode = await getPaymentCode(isSegwit: false);
|
|
//
|
|
// // refresh transactions to pick up any received notification transactions
|
|
// await _refreshNotificationAddressTransactions();
|
|
//
|
|
// try {
|
|
// final Set<String> codesToCheck = {};
|
|
// final nym = await PaynymIsApi().nym(myCode.toString());
|
|
// if (nym.value != null) {
|
|
// for (final follower in nym.value!.followers) {
|
|
// codesToCheck.add(follower.code);
|
|
// }
|
|
// for (final following in nym.value!.following) {
|
|
// codesToCheck.add(following.code);
|
|
// }
|
|
// }
|
|
//
|
|
// // restore paynym transactions
|
|
// await restoreAllHistory(
|
|
// maxUnusedAddressGap: maxUnusedAddressGap,
|
|
// maxNumberOfIndexesToCheck: maxNumberOfIndexesToCheck,
|
|
// paymentCodeStrings: codesToCheck,
|
|
// );
|
|
// } catch (e, s) {
|
|
// Logging.instance.log(
|
|
// "Failed to check paynym.is followers/following for history during "
|
|
// "bitcoin wallet ($walletId $walletName) "
|
|
// "_recoverWalletFromBIP32SeedPhrase: $e/n$s",
|
|
// level: LogLevel.Error,
|
|
// );
|
|
// }
|
|
//
|
|
// await Future.wait([
|
|
// _refreshTransactions(),
|
|
// _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<bool> refreshIfThereIsNewData() async {
|
|
// if (longMutex) return false;
|
|
// if (_hasCalledExit) return false;
|
|
// Logging.instance.log("refreshIfThereIsNewData", level: LogLevel.Info);
|
|
//
|
|
// try {
|
|
// bool needsRefresh = false;
|
|
// Set<String> 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) {
|
|
// final allOwnAddresses = await _fetchAllOwnAddresses();
|
|
// List<Map<String, dynamic>> allTxs = await _fetchHistory(
|
|
// allOwnAddresses.map((e) => e.value).toList(growable: false));
|
|
// for (Map<String, dynamic> 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;
|
|
// } on NoSuchTransactionException catch (e) {
|
|
// // TODO: move direct transactions elsewhere
|
|
// await db.isar.writeTxn(() async {
|
|
// await db.isar.transactions.deleteByTxidWalletId(e.txid, walletId);
|
|
// });
|
|
// await txTracker.deleteTransaction(e.txid);
|
|
// return true;
|
|
// } catch (e, s) {
|
|
// Logging.instance.log(
|
|
// "Exception caught in refreshIfThereIsNewData: $e\n$s",
|
|
// level: LogLevel.Error);
|
|
// rethrow;
|
|
// }
|
|
// }
|
|
//
|
|
// Future<void> getAllTxsToWatch() async {
|
|
// if (_hasCalledExit) return;
|
|
// List<isar_models.Transaction> unconfirmedTxnsToNotifyPending = [];
|
|
// List<isar_models.Transaction> 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,
|
|
// walletName: walletName,
|
|
// date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
|
|
// shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS,
|
|
// coin: coin,
|
|
// txid: tx.txid,
|
|
// confirmations: confirmations,
|
|
// requiredConfirmations: MINIMUM_CONFIRMATIONS,
|
|
// ),
|
|
// );
|
|
// 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);
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
//
|
|
// @override
|
|
// bool get isRefreshing => refreshMutex;
|
|
//
|
|
// bool refreshMutex = false;
|
|
//
|
|
// //TODO Show percentages properly/more consistently
|
|
// /// Refreshes display data for the wallet
|
|
// @override
|
|
// Future<void> 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));
|
|
// // isSegwit does not matter here at all
|
|
// final myCode = await getPaymentCode(isSegwit: false);
|
|
// final Set<String> codesToCheck = {};
|
|
// final nym = await PaynymIsApi().nym(myCode.toString());
|
|
// if (nym.value != null) {
|
|
// for (final follower in nym.value!.followers) {
|
|
// codesToCheck.add(follower.code);
|
|
// }
|
|
// for (final following in nym.value!.following) {
|
|
// codesToCheck.add(following.code);
|
|
// }
|
|
// }
|
|
//
|
|
// 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();
|
|
// await _checkP2PKHChangeAddressForTransactions();
|
|
//
|
|
// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
|
|
// await _checkCurrentReceivingAddressesForTransactions();
|
|
//
|
|
// GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.4, walletId));
|
|
// await checkAllCurrentReceivingPaynymAddressesForTransactions();
|
|
//
|
|
// final fetchFuture = _refreshTransactions();
|
|
// 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 fetchFuture;
|
|
// GlobalEventBus.instance
|
|
// .fire(RefreshPercentChangedEvent(0.80, walletId));
|
|
//
|
|
// await checkForNotificationTransactionsTo(codesToCheck);
|
|
// await _updateUTXOs();
|
|
// 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<void> initializeExisting() async {
|
|
// // this will add the notification address to the db if it isn't
|
|
// // already there for older wallets
|
|
// await getMyNotificationAddress();
|
|
//
|
|
// // 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<void> updateSentCachedTxData(Map<String, dynamic> 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,
|
|
// );
|
|
// }
|
|
//
|
|
// Future<void> _refreshNotificationAddressTransactions() async {
|
|
// final address = await getMyNotificationAddress();
|
|
// final hashes = await _fetchHistory([address.value]);
|
|
//
|
|
// List<Map<String, dynamic>> allTransactions = [];
|
|
//
|
|
// final currentHeight = await chainHeight;
|
|
//
|
|
// for (final txHash in hashes) {
|
|
// final storedTx = await db
|
|
// .getTransactions(walletId)
|
|
// .filter()
|
|
// .txidEqualTo(txHash["tx_hash"] as String)
|
|
// .findFirst();
|
|
//
|
|
// // TODO: remove bip47Notification type check sometime after Q2 2023
|
|
// if (storedTx == null ||
|
|
// storedTx.subType ==
|
|
// isar_models.TransactionSubType.bip47Notification ||
|
|
// !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) {
|
|
// final tx = await cachedElectrumXClient.getTransaction(
|
|
// txHash: txHash["tx_hash"] as String,
|
|
// verbose: true,
|
|
// coin: coin,
|
|
// );
|
|
//
|
|
// tx["address"] = await db
|
|
// .getAddresses(walletId)
|
|
// .filter()
|
|
// .valueEqualTo(txHash["address"] as String)
|
|
// .findFirst();
|
|
// tx["height"] = txHash["height"];
|
|
// allTransactions.add(tx);
|
|
// }
|
|
// }
|
|
//
|
|
// final List<Tuple2<isar_models.Transaction, isar_models.Address?>> txnsData =
|
|
// [];
|
|
//
|
|
// for (final txObject in allTransactions) {
|
|
// final data = await parseTransaction(
|
|
// txObject,
|
|
// cachedElectrumXClient,
|
|
// [address],
|
|
// coin,
|
|
// MINIMUM_CONFIRMATIONS,
|
|
// walletId,
|
|
// );
|
|
//
|
|
// txnsData.add(data);
|
|
// }
|
|
// await db.addNewTransactionData(txnsData, walletId);
|
|
// }
|
|
//
|
|
// Future<void> _refreshTransactions() async {
|
|
// final List<isar_models.Address> allAddresses =
|
|
// await _fetchAllOwnAddresses();
|
|
//
|
|
// final Set<Map<String, dynamic>> allTxHashes =
|
|
// (await _fetchHistory(allAddresses.map((e) => e.value).toList()))
|
|
// .toSet();
|
|
//
|
|
// // // prefetch/cache
|
|
// // Set<String> hashes = {};
|
|
// // for (var element in allReceivingTxHashes) {
|
|
// // hashes.add(element['tx_hash'] as String);
|
|
// // }
|
|
// // await fastFetch(hashes.toList());
|
|
//
|
|
// List<Map<String, dynamic>> allTransactions = [];
|
|
//
|
|
// final currentHeight = await chainHeight;
|
|
//
|
|
// for (final txHash in allTxHashes) {
|
|
// final storedTx = await db
|
|
// .getTransactions(walletId)
|
|
// .filter()
|
|
// .txidEqualTo(txHash["tx_hash"] as String)
|
|
// .findFirst();
|
|
//
|
|
// // TODO: remove bip47Notification type check sometime after Q2 2023
|
|
// if (storedTx == null ||
|
|
// storedTx.subType ==
|
|
// isar_models.TransactionSubType.bip47Notification ||
|
|
// !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)) {
|
|
// final tx = await cachedElectrumXClient.getTransaction(
|
|
// txHash: txHash["tx_hash"] as String,
|
|
// verbose: true,
|
|
// coin: coin,
|
|
// );
|
|
//
|
|
// 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<String> 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<Tuple2<isar_models.Transaction, isar_models.Address?>> 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<isar_models.UTXO>? utxos,
|
|
// }) async {
|
|
// Logging.instance
|
|
// .log("Starting coinSelection ----------", level: LogLevel.Info);
|
|
// final List<isar_models.UTXO> availableOutputs = utxos ?? await this.utxos;
|
|
// final currentChainHeight = await chainHeight;
|
|
// final List<isar_models.UTXO> 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("availableOutputs.length: ${availableOutputs.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<isar_models.UTXO> 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<String> recipientsArray = [recipientAddress];
|
|
// List<int> 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<String, dynamic> 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;
|
|
// try {
|
|
// vSizeForOneOutput = (await buildTransaction(
|
|
// utxoSigningData: utxoSigningData,
|
|
// recipients: [recipientAddress],
|
|
// satoshiAmounts: [satoshisBeingUsed - 1],
|
|
// ))["vSize"] as int;
|
|
// } catch (e) {
|
|
// Logging.instance.log("vSizeForOneOutput: $e", level: LogLevel.Error);
|
|
// rethrow;
|
|
// }
|
|
//
|
|
// final int vSizeForTwoOutPuts;
|
|
// try {
|
|
// vSizeForTwoOutPuts = (await buildTransaction(
|
|
// utxoSigningData: utxoSigningData,
|
|
// recipients: [
|
|
// recipientAddress,
|
|
// await currentChangeAddress,
|
|
// ],
|
|
// satoshiAmounts: [
|
|
// satoshiAmountToSend,
|
|
// max(0, satoshisBeingUsed - satoshiAmountToSend - 1),
|
|
// ],
|
|
// ))["vSize"] as int;
|
|
// } catch (e) {
|
|
// Logging.instance.log("vSizeForTwoOutPuts: $e", level: LogLevel.Error);
|
|
// rethrow;
|
|
// }
|
|
//
|
|
// // 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 currentChangeAddress;
|
|
//
|
|
// 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<List<SigningData>> fetchBuildTxData(
|
|
// List<isar_models.UTXO> utxosToUse,
|
|
// ) async {
|
|
// // return data
|
|
// List<SigningData> 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<DerivePathType, Map<String, dynamic>> receiveDerivations = {};
|
|
// Map<DerivePathType, Map<String, dynamic>> changeDerivations = {};
|
|
//
|
|
// for (final sd in signingData) {
|
|
// String? pubKey;
|
|
// String? wif;
|
|
//
|
|
// final address = await db.getAddress(walletId, sd.utxo.address!);
|
|
// if (address?.derivationPath != null) {
|
|
// final bip32.BIP32 node;
|
|
// if (address!.subType == isar_models.AddressSubType.paynymReceive) {
|
|
// final code = await paymentCodeStringByKey(address.otherData!);
|
|
//
|
|
// final bip47base = await getBip47BaseNode();
|
|
//
|
|
// final privateKey = await getPrivateKeyForPaynymReceivingAddress(
|
|
// paymentCodeString: code!,
|
|
// index: address.derivationIndex,
|
|
// );
|
|
//
|
|
// node = bip32.BIP32.fromPrivateKey(
|
|
// privateKey,
|
|
// bip47base.chainCode,
|
|
// bip32.NetworkType(
|
|
// wif: _network.wif,
|
|
// bip32: bip32.Bip32Type(
|
|
// public: _network.bip32.public,
|
|
// private: _network.bip32.private,
|
|
// ),
|
|
// ),
|
|
// );
|
|
// } else {
|
|
// 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) {
|
|
// // 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 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,
|
|
// ).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,
|
|
// ).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<Map<String, dynamic>> buildTransaction({
|
|
// required List<SigningData> utxoSigningData,
|
|
// required List<String> recipients,
|
|
// required List<int> 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!,
|
|
// );
|
|
// }
|
|
//
|
|
// // Add transaction output
|
|
// for (var i = 0; i < recipients.length; i++) {
|
|
// txb.addOutput(recipients[i], satoshiAmounts[i]);
|
|
// }
|
|
//
|
|
// 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,
|
|
// );
|
|
// }
|
|
// } catch (e, s) {
|
|
// Logging.instance.log("Caught exception while signing transaction: $e\n$s",
|
|
// level: LogLevel.Error);
|
|
// rethrow;
|
|
// }
|
|
//
|
|
// final builtTx = txb.build();
|
|
// final vSize = builtTx.virtualSize();
|
|
//
|
|
// return {"hex": builtTx.toHex(), "vSize": vSize};
|
|
// }
|
|
//
|
|
// @override
|
|
// Future<void> 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();
|
|
//
|
|
// 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<void> _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");
|
|
// }
|
|
//
|
|
// bool isActive = false;
|
|
//
|
|
// @override
|
|
// void Function(bool)? get onIsActiveWalletChanged =>
|
|
// (isActive) => this.isActive = isActive;
|
|
//
|
|
// @override
|
|
// Future<Amount> 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 += 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<Amount> 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<bool> 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<String> get xpub async {
|
|
// final node = await Bip32Utils.getBip32Root(
|
|
// (await mnemonic).join(" "),
|
|
// await mnemonicPassphrase ?? "",
|
|
// _network,
|
|
// );
|
|
//
|
|
// return node.neutered().toBase58();
|
|
// }
|
|
// }
|