mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-04-14 18:22:54 +00:00
migrate dogecoin_wallet.dart to isar transactions, addresses, and utxos, as well as the cleaner balance model
This commit is contained in:
parent
78db152ff4
commit
90964b83c6
1 changed files with 196 additions and 364 deletions
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bech32/bech32.dart';
|
||||
import 'package:bip32/bip32.dart' as bip32;
|
||||
|
@ -9,20 +8,15 @@ import 'package:bip39/bip39.dart' as bip39;
|
|||
import 'package:bitcoindart/bitcoindart.dart';
|
||||
import 'package:bitcoindart/bitcoindart.dart' as btc_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:http/http.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart';
|
||||
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
|
||||
import 'package:stackwallet/hive/db.dart';
|
||||
import 'package:stackwallet/models/isar/models/address/address.dart';
|
||||
import 'package:stackwallet/models/balance.dart';
|
||||
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
|
||||
import 'package:stackwallet/models/models.dart' as models;
|
||||
import 'package:stackwallet/models/paymint/fee_object_model.dart';
|
||||
import 'package:stackwallet/models/paymint/transactions_model.dart';
|
||||
import 'package:stackwallet/models/paymint/utxo_model.dart';
|
||||
import 'package:stackwallet/services/coins/coin_paynym_extension.dart';
|
||||
import 'package:stackwallet/services/coins/coin_service.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
|
||||
|
@ -32,8 +26,8 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_
|
|||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||
import 'package:stackwallet/services/node_service.dart';
|
||||
import 'package:stackwallet/services/notifications_api.dart';
|
||||
import 'package:stackwallet/services/price.dart';
|
||||
import 'package:stackwallet/services/transaction_notification_tracker.dart';
|
||||
import 'package:stackwallet/utilities/address_utils.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/default_nodes.dart';
|
||||
|
@ -151,70 +145,25 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
}
|
||||
}
|
||||
|
||||
List<UtxoObject> outputsList = [];
|
||||
@override
|
||||
Future<List<isar_models.UTXO>> get utxos => isar.utxos.where().findAll();
|
||||
|
||||
@override
|
||||
Future<List<isar_models.Transaction>> get transactions =>
|
||||
isar.transactions.where().sortByTimestampDesc().findAll();
|
||||
|
||||
@override
|
||||
Coin get coin => _coin;
|
||||
|
||||
@override
|
||||
Future<List<String>> get allOwnAddresses =>
|
||||
throw Exception("doeg all own address exception!!");
|
||||
// _allOwnAddresses ??= _fetchAllOwnAddresses();
|
||||
// Future<List<String>>? _allOwnAddresses;
|
||||
|
||||
Future<UtxoData>? _utxoData;
|
||||
Future<UtxoData> get utxoData => _utxoData ??= _fetchUtxoData();
|
||||
|
||||
@override
|
||||
Future<List<UtxoObject>> get unspentOutputs async =>
|
||||
(await utxoData).unspentOutputArray;
|
||||
|
||||
@override
|
||||
Future<Decimal> get availableBalance async {
|
||||
final data = await utxoData;
|
||||
return Format.satoshisToAmount(
|
||||
data.satoshiBalance - data.satoshiBalanceUnconfirmed,
|
||||
coin: coin);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Decimal> get pendingBalance async {
|
||||
final data = await utxoData;
|
||||
return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Decimal> get balanceMinusMaxFee async =>
|
||||
(await availableBalance) -
|
||||
(Decimal.fromInt((await maxFee)) /
|
||||
Decimal.fromInt(Constants.satsPerCoin(coin)))
|
||||
.toDecimal();
|
||||
|
||||
@override
|
||||
Future<Decimal> get totalBalance async {
|
||||
if (!isActive) {
|
||||
final totalBalance = DB.instance
|
||||
.get<dynamic>(boxName: walletId, key: 'totalBalance') as int?;
|
||||
if (totalBalance == null) {
|
||||
final data = await utxoData;
|
||||
return Format.satoshisToAmount(data.satoshiBalance, coin: coin);
|
||||
} else {
|
||||
return Format.satoshisToAmount(totalBalance, coin: coin);
|
||||
}
|
||||
}
|
||||
final data = await utxoData;
|
||||
return Format.satoshisToAmount(data.satoshiBalance, coin: coin);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> get currentReceivingAddress async =>
|
||||
(await _currentReceivingAddress).value;
|
||||
|
||||
Future<isar_models.Address> get _currentReceivingAddress async =>
|
||||
(await isar.address
|
||||
(await isar.addresses
|
||||
.filter()
|
||||
.typeEqualTo(isar_models.AddressType.p2pkh)
|
||||
.subTypeEqualTo(AddressSubType.receiving)
|
||||
.subTypeEqualTo(isar_models.AddressSubType.receiving)
|
||||
.sortByDerivationIndexDesc()
|
||||
.findFirst())!;
|
||||
|
||||
|
@ -223,10 +172,10 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
(await _currentChangeAddress).value;
|
||||
|
||||
Future<isar_models.Address> get _currentChangeAddress async =>
|
||||
(await isar.address
|
||||
(await isar.addresses
|
||||
.filter()
|
||||
.typeEqualTo(isar_models.AddressType.p2pkh)
|
||||
.subTypeEqualTo(AddressSubType.change)
|
||||
.subTypeEqualTo(isar_models.AddressSubType.change)
|
||||
.sortByDerivationIndexDesc()
|
||||
.findFirst())!;
|
||||
|
||||
|
@ -566,7 +515,6 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
final address =
|
||||
await _generateAddressForChain(0, 0, DerivePathType.bip44);
|
||||
p2pkhReceiveAddressArray.add(address);
|
||||
p2pkhReceiveIndex = 0;
|
||||
}
|
||||
|
||||
// If restoring a wallet that never sent any funds with change, then set changeArray
|
||||
|
@ -575,16 +523,17 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
final address =
|
||||
await _generateAddressForChain(1, 0, DerivePathType.bip44);
|
||||
p2pkhChangeAddressArray.add(address);
|
||||
p2pkhChangeIndex = 0;
|
||||
}
|
||||
|
||||
await _isarInit();
|
||||
|
||||
await isar.writeTxn(() async {
|
||||
await isar.address.putAll(p2pkhChangeAddressArray);
|
||||
await isar.address.putAll(p2pkhReceiveAddressArray);
|
||||
await isar.addresses.putAll(p2pkhChangeAddressArray);
|
||||
await isar.addresses.putAll(p2pkhReceiveAddressArray);
|
||||
});
|
||||
|
||||
await _updateUTXOs();
|
||||
|
||||
await DB.instance
|
||||
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
|
||||
await DB.instance
|
||||
|
@ -621,8 +570,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
|
||||
for (String txid in txnsToCheck) {
|
||||
final txn = await electrumXClient.getTransaction(txHash: txid);
|
||||
var confirmations = txn["confirmations"];
|
||||
if (confirmations is! int) continue;
|
||||
int confirmations = txn["confirmations"] as int? ?? 0;
|
||||
bool isUnconfirmed = confirmations < MINIMUM_CONFIRMATIONS;
|
||||
if (!isUnconfirmed) {
|
||||
// unconfirmedTxs = {};
|
||||
|
@ -631,10 +579,9 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
}
|
||||
}
|
||||
if (!needsRefresh) {
|
||||
var allOwnAddresses = await _fetchAllOwnAddresses();
|
||||
final allOwnAddresses = await _fetchAllOwnAddresses();
|
||||
List<Map<String, dynamic>> allTxs = await _fetchHistory(
|
||||
allOwnAddresses.map((e) => e.value).toList(growable: false));
|
||||
// final txData = await transactionData;
|
||||
for (Map<String, dynamic> transaction in allTxs) {
|
||||
final txid = transaction['tx_hash'] as String;
|
||||
if ((await isar.transactions
|
||||
|
@ -664,8 +611,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
List<isar_models.Transaction> unconfirmedTxnsToNotifyPending = [];
|
||||
List<isar_models.Transaction> unconfirmedTxnsToNotifyConfirmed = [];
|
||||
|
||||
final currentChainHeight =
|
||||
(await electrumXClient.getBlockHeadTip())["height"] as int;
|
||||
final currentChainHeight = await chainHeight;
|
||||
|
||||
final txCount = await isar.transactions.count();
|
||||
|
||||
|
@ -691,22 +637,6 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
}
|
||||
}
|
||||
|
||||
// // Get all unconfirmed incoming transactions
|
||||
// for (final chunk in txData.txChunks) {
|
||||
// for (final tx in chunk.transactions) {
|
||||
// if (tx.confirmedStatus) {
|
||||
// if (txTracker.wasNotifiedPending(tx.txid) &&
|
||||
// !txTracker.wasNotifiedConfirmed(tx.txid)) {
|
||||
// unconfirmedTxnsToNotifyConfirmed.add(tx);
|
||||
// }
|
||||
// } else {
|
||||
// if (!txTracker.wasNotifiedPending(tx.txid)) {
|
||||
// unconfirmedTxnsToNotifyPending.add(tx);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// notify on new incoming transaction
|
||||
for (final tx in unconfirmedTxnsToNotifyPending) {
|
||||
final confirmations = tx.getConfirmations(currentChainHeight);
|
||||
|
@ -838,11 +768,11 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
|
||||
await _checkCurrentReceivingAddressesForTransactions();
|
||||
|
||||
final fetchFuture = _fetchTransactionData();
|
||||
final fetchFuture = _refreshTransactions();
|
||||
final utxosRefreshFuture = _updateUTXOs();
|
||||
GlobalEventBus.instance
|
||||
.fire(RefreshPercentChangedEvent(0.50, walletId));
|
||||
|
||||
final newUtxoData = _fetchUtxoData();
|
||||
final feeObj = _getFees();
|
||||
GlobalEventBus.instance
|
||||
.fire(RefreshPercentChangedEvent(0.60, walletId));
|
||||
|
@ -850,7 +780,8 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
GlobalEventBus.instance
|
||||
.fire(RefreshPercentChangedEvent(0.70, walletId));
|
||||
_feeObject = Future(() => feeObj);
|
||||
_utxoData = Future(() => newUtxoData);
|
||||
|
||||
await utxosRefreshFuture;
|
||||
GlobalEventBus.instance
|
||||
.fire(RefreshPercentChangedEvent(0.80, walletId));
|
||||
|
||||
|
@ -936,9 +867,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
}
|
||||
// check for send all
|
||||
bool isSendAll = false;
|
||||
final balance =
|
||||
Format.decimalAmountToSatoshis(await availableBalance, coin);
|
||||
if (satoshiAmount == balance) {
|
||||
if (satoshiAmount == balance.spendable) {
|
||||
isSendAll = true;
|
||||
}
|
||||
|
||||
|
@ -998,24 +927,6 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> send({
|
||||
required String toAddress,
|
||||
required int amount,
|
||||
Map<String, String> args = const {},
|
||||
}) async {
|
||||
try {
|
||||
final txData = await prepareSend(
|
||||
address: toAddress, satoshiAmount: amount, args: args);
|
||||
final txHash = await confirmSend(txData: txData);
|
||||
return txHash;
|
||||
} catch (e, s) {
|
||||
Logging.instance
|
||||
.log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> testNetworkConnection() async {
|
||||
try {
|
||||
|
@ -1108,18 +1019,15 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
Logging.instance.log("Opening existing ${coin.prettyName} wallet.",
|
||||
level: LogLevel.Info);
|
||||
|
||||
if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) == null) {
|
||||
throw Exception(
|
||||
"Attempted to initialize an existing wallet using an unknown wallet ID!");
|
||||
}
|
||||
|
||||
await _prefs.init();
|
||||
await _isarInit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TransactionData> get transactionData =>
|
||||
throw Exception("dogecoin transactionData attempt");
|
||||
// _transactionData ??= _fetchTransactionData();
|
||||
// Future<TransactionData>? _transactionData;
|
||||
|
||||
TransactionData? cachedTxData;
|
||||
|
||||
// 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
|
||||
|
@ -1194,8 +1102,6 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
|
||||
late SecureStorageInterface _secureStore;
|
||||
|
||||
late PriceAPI _priceAPI;
|
||||
|
||||
late Isar isar;
|
||||
|
||||
DogecoinWallet({
|
||||
|
@ -1205,7 +1111,6 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
required ElectrumX client,
|
||||
required CachedElectrumX cachedClient,
|
||||
required TransactionNotificationTracker tracker,
|
||||
PriceAPI? priceAPI,
|
||||
required SecureStorageInterface secureStore,
|
||||
}) {
|
||||
txTracker = tracker;
|
||||
|
@ -1214,8 +1119,6 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
_coin = coin;
|
||||
_electrumXClient = client;
|
||||
_cachedElectrumXClient = cachedClient;
|
||||
|
||||
_priceAPI = priceAPI ?? PriceAPI(Client());
|
||||
_secureStore = secureStore;
|
||||
}
|
||||
|
||||
|
@ -1273,40 +1176,12 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
}
|
||||
|
||||
Future<List<isar_models.Address>> _fetchAllOwnAddresses() async {
|
||||
final allAddresses = await isar.address
|
||||
final allAddresses = await isar.addresses
|
||||
.filter()
|
||||
.subTypeEqualTo(isar_models.AddressSubType.receiving)
|
||||
.or()
|
||||
.subTypeEqualTo(isar_models.AddressSubType.change)
|
||||
.findAll();
|
||||
// final List<String> allAddresses = [];
|
||||
//
|
||||
// final receivingAddressesP2PKH = DB.instance.get<dynamic>(
|
||||
// boxName: walletId, key: 'receivingAddressesP2PKH') as List<dynamic>;
|
||||
// final changeAddressesP2PKH =
|
||||
// DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddressesP2PKH')
|
||||
// as List<dynamic>;
|
||||
//
|
||||
// // for (var i = 0; i < receivingAddresses.length; i++) {
|
||||
// // if (!allAddresses.contains(receivingAddresses[i])) {
|
||||
// // allAddresses.add(receivingAddresses[i]);
|
||||
// // }
|
||||
// // }
|
||||
// // for (var i = 0; i < changeAddresses.length; i++) {
|
||||
// // if (!allAddresses.contains(changeAddresses[i])) {
|
||||
// // allAddresses.add(changeAddresses[i]);
|
||||
// // }
|
||||
// // }
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
return allAddresses;
|
||||
}
|
||||
|
||||
|
@ -1375,16 +1250,6 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
key: '${_walletId}_mnemonic',
|
||||
value: bip39.generateMnemonic(strength: 256));
|
||||
|
||||
// // Set relevant indexes
|
||||
// await DB.instance
|
||||
// .put<dynamic>(boxName: walletId, key: "receivingIndexP2PKH", value: 0);
|
||||
// await DB.instance
|
||||
// .put<dynamic>(boxName: walletId, key: "changeIndexP2PKH", value: 0);
|
||||
// await DB.instance.put<dynamic>(
|
||||
// boxName: walletId,
|
||||
// key: 'blocked_tx_hashes',
|
||||
// value: ["0xdefault"],
|
||||
// ); // A list of transaction hashes to represent frozen utxos in wallet
|
||||
// initialize address book entries
|
||||
await DB.instance.put<dynamic>(
|
||||
boxName: walletId,
|
||||
|
@ -1400,7 +1265,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
await _isarInit();
|
||||
|
||||
await isar.writeTxn(() async {
|
||||
await isar.address.putAll([
|
||||
await isar.addresses.putAll([
|
||||
initialReceivingAddressP2PKH,
|
||||
initialChangeAddressP2PKH,
|
||||
]);
|
||||
|
@ -1462,17 +1327,21 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
/// 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<String> getCurrentAddressForChain(
|
||||
int chain, DerivePathType derivePathType) async {
|
||||
Future<String> _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.Address? address;
|
||||
switch (derivePathType) {
|
||||
case DerivePathType.bip44:
|
||||
address = await isar.address
|
||||
address = await isar.addresses
|
||||
.filter()
|
||||
.subTypeEqualTo(
|
||||
chain == 0 // Here, we assume that chain == 1 if it isn't 0
|
||||
? isar_models.AddressSubType.receiving
|
||||
: isar_models.AddressSubType.change)
|
||||
.typeEqualTo(isar_models.AddressType.p2pkh)
|
||||
.subTypeEqualTo(subType)
|
||||
.sortByDerivationIndexDesc()
|
||||
.findFirst();
|
||||
break;
|
||||
|
@ -1609,7 +1478,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
return allTransactions;
|
||||
}
|
||||
|
||||
Future<UtxoData> _fetchUtxoData() async {
|
||||
Future<void> _updateUTXOs() async {
|
||||
final allAddresses = await _fetchAllOwnAddresses();
|
||||
|
||||
try {
|
||||
|
@ -1622,7 +1491,8 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
if (batches[batchNumber] == null) {
|
||||
batches[batchNumber] = {};
|
||||
}
|
||||
final scripthash = _convertToScriptHash(allAddresses[i].value, network);
|
||||
final scripthash =
|
||||
AddressUtils.convertToScriptHash(allAddresses[i].value, network);
|
||||
batches[batchNumber]!.addAll({
|
||||
scripthash: [scripthash]
|
||||
});
|
||||
|
@ -1641,148 +1511,123 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
}
|
||||
}
|
||||
|
||||
final priceData =
|
||||
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
|
||||
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
|
||||
final List<Map<String, dynamic>> outputArray = [];
|
||||
int satoshiBalance = 0;
|
||||
final currentChainHeight = await chainHeight;
|
||||
|
||||
final List<isar_models.UTXO> outputArray = [];
|
||||
int satoshiBalanceTotal = 0;
|
||||
int satoshiBalancePending = 0;
|
||||
int satoshiBalanceSpendable = 0;
|
||||
int satoshiBalanceBlocked = 0;
|
||||
|
||||
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
||||
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
||||
int value = fetchedUtxoList[i][j]["value"] as int;
|
||||
satoshiBalance += value;
|
||||
|
||||
final txn = await cachedElectrumXClient.getTransaction(
|
||||
txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
|
||||
verbose: true,
|
||||
coin: coin,
|
||||
);
|
||||
|
||||
final Map<String, dynamic> utxo = {};
|
||||
final int confirmations = txn["confirmations"] as int? ?? 0;
|
||||
final bool confirmed = txn["confirmations"] == null
|
||||
? false
|
||||
: txn["confirmations"] as int >= MINIMUM_CONFIRMATIONS;
|
||||
if (!confirmed) {
|
||||
satoshiBalancePending += value;
|
||||
final utxo = isar_models.UTXO();
|
||||
|
||||
utxo.txid = txn["txid"] as String;
|
||||
utxo.vout = fetchedUtxoList[i][j]["tx_pos"] as int;
|
||||
utxo.value = fetchedUtxoList[i][j]["value"] as int;
|
||||
utxo.name = "";
|
||||
|
||||
// todo check here if we should mark as blocked
|
||||
utxo.isBlocked = false;
|
||||
utxo.blockedReason = null;
|
||||
|
||||
utxo.isCoinbase = txn["is_coinbase"] as bool? ?? false;
|
||||
utxo.blockHash = txn["blockhash"] as String?;
|
||||
utxo.blockHeight = fetchedUtxoList[i][j]["height"] as int?;
|
||||
utxo.blockTime = txn["blocktime"] as int?;
|
||||
|
||||
satoshiBalanceTotal += utxo.value;
|
||||
|
||||
if (utxo.isBlocked) {
|
||||
satoshiBalanceBlocked += utxo.value;
|
||||
} else {
|
||||
if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
|
||||
satoshiBalanceSpendable += utxo.value;
|
||||
} else {
|
||||
satoshiBalancePending += utxo.value;
|
||||
}
|
||||
}
|
||||
|
||||
utxo["txid"] = txn["txid"];
|
||||
utxo["vout"] = fetchedUtxoList[i][j]["tx_pos"];
|
||||
utxo["value"] = value;
|
||||
|
||||
utxo["status"] = <String, dynamic>{};
|
||||
utxo["status"]["confirmed"] = confirmed;
|
||||
utxo["status"]["confirmations"] = confirmations;
|
||||
utxo["status"]["block_height"] = fetchedUtxoList[i][j]["height"];
|
||||
utxo["status"]["block_hash"] = txn["blockhash"];
|
||||
utxo["status"]["block_time"] = txn["blocktime"];
|
||||
|
||||
final fiatValue = ((Decimal.fromInt(value) * currentPrice) /
|
||||
Decimal.fromInt(Constants.satsPerCoin(coin)))
|
||||
.toDecimal(scaleOnInfinitePrecision: 2);
|
||||
utxo["rawWorth"] = fiatValue;
|
||||
utxo["fiatWorth"] = fiatValue.toString();
|
||||
outputArray.add(utxo);
|
||||
}
|
||||
}
|
||||
|
||||
Decimal currencyBalanceRaw =
|
||||
((Decimal.fromInt(satoshiBalance) * currentPrice) /
|
||||
Decimal.fromInt(Constants.satsPerCoin(coin)))
|
||||
.toDecimal(scaleOnInfinitePrecision: 2);
|
||||
|
||||
final Map<String, dynamic> result = {
|
||||
"total_user_currency": currencyBalanceRaw.toString(),
|
||||
"total_sats": satoshiBalance,
|
||||
"total_btc": (Decimal.fromInt(satoshiBalance) /
|
||||
Decimal.fromInt(Constants.satsPerCoin(coin)))
|
||||
.toDecimal(
|
||||
scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin))
|
||||
.toString(),
|
||||
"outputArray": outputArray,
|
||||
"unconfirmed": satoshiBalancePending,
|
||||
};
|
||||
|
||||
final dataModel = UtxoData.fromJson(result);
|
||||
|
||||
final List<UtxoObject> allOutputs = dataModel.unspentOutputArray;
|
||||
Logging.instance
|
||||
.log('Outputs fetched: $allOutputs', level: LogLevel.Info);
|
||||
await _sortOutputs(allOutputs);
|
||||
await DB.instance.put<dynamic>(
|
||||
boxName: walletId, key: 'latest_utxo_model', value: dataModel);
|
||||
await DB.instance.put<dynamic>(
|
||||
boxName: walletId,
|
||||
key: 'totalBalance',
|
||||
value: dataModel.satoshiBalance);
|
||||
return dataModel;
|
||||
.log('Outputs fetched: $outputArray', level: LogLevel.Info);
|
||||
|
||||
await isar.writeTxn(() async {
|
||||
await isar.utxos.clear();
|
||||
await isar.utxos.putAll(outputArray);
|
||||
});
|
||||
|
||||
// finally update balance
|
||||
_balance = Balance(
|
||||
coin: coin,
|
||||
total: satoshiBalanceTotal,
|
||||
spendable: satoshiBalanceSpendable,
|
||||
blockedTotal: satoshiBalanceBlocked,
|
||||
pendingSpendable: satoshiBalancePending,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance
|
||||
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
|
||||
final latestTxModel =
|
||||
DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model');
|
||||
|
||||
if (latestTxModel == null) {
|
||||
final emptyModel = {
|
||||
"total_user_currency": "0.00",
|
||||
"total_sats": 0,
|
||||
"total_btc": "0",
|
||||
"outputArray": <dynamic>[]
|
||||
};
|
||||
return UtxoData.fromJson(emptyModel);
|
||||
} else {
|
||||
Logging.instance
|
||||
.log("Old output model located", level: LogLevel.Warning);
|
||||
return latestTxModel as models.UtxoData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<void> _sortOutputs(List<UtxoObject> utxos) async {
|
||||
final blockedHashArray =
|
||||
DB.instance.get<dynamic>(boxName: walletId, key: 'blocked_tx_hashes')
|
||||
as List<dynamic>?;
|
||||
final List<String> lst = [];
|
||||
if (blockedHashArray != null) {
|
||||
for (var hash in blockedHashArray) {
|
||||
lst.add(hash as String);
|
||||
}
|
||||
}
|
||||
final labels =
|
||||
DB.instance.get<dynamic>(boxName: walletId, key: 'labels') as Map? ??
|
||||
{};
|
||||
@override
|
||||
Balance get balance => _balance!;
|
||||
Balance? _balance;
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// /// 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<void> _sortOutputs(List<UtxoObject> utxos) async {
|
||||
// final blockedHashArray =
|
||||
// DB.instance.get<dynamic>(boxName: walletId, key: 'blocked_tx_hashes')
|
||||
// as List<dynamic>?;
|
||||
// final List<String> lst = [];
|
||||
// if (blockedHashArray != null) {
|
||||
// for (var hash in blockedHashArray) {
|
||||
// lst.add(hash as String);
|
||||
// }
|
||||
// }
|
||||
// final labels =
|
||||
// DB.instance.get<dynamic>(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<int> getTxCount({required String address}) async {
|
||||
String? scripthash;
|
||||
try {
|
||||
scripthash = _convertToScriptHash(address, network);
|
||||
scripthash = AddressUtils.convertToScriptHash(address, network);
|
||||
final transactions =
|
||||
await electrumXClient.getHistory(scripthash: scripthash);
|
||||
return transactions.length;
|
||||
|
@ -1800,7 +1645,9 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
try {
|
||||
final Map<String, List<dynamic>> args = {};
|
||||
for (final entry in addresses.entries) {
|
||||
args[entry.key] = [_convertToScriptHash(entry.value, network)];
|
||||
args[entry.key] = [
|
||||
AddressUtils.convertToScriptHash(entry.value, network)
|
||||
];
|
||||
}
|
||||
final response = await electrumXClient.getBatchHistory(args: args);
|
||||
|
||||
|
@ -1827,6 +1674,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
level: LogLevel.Info);
|
||||
|
||||
if (txCount >= 1) {
|
||||
// First increment the receiving index
|
||||
final newReceivingIndex = currentReceiving.derivationIndex + 1;
|
||||
|
||||
// Use new index to derive a new receiving address
|
||||
|
@ -1835,7 +1683,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
|
||||
// Add that new receiving address
|
||||
await isar.writeTxn(() async {
|
||||
await isar.address.put(newReceivingAddress);
|
||||
await isar.addresses.put(newReceivingAddress);
|
||||
});
|
||||
}
|
||||
} on SocketException catch (se, s) {
|
||||
|
@ -1869,12 +1717,12 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
|
||||
// Add that new change address
|
||||
await isar.writeTxn(() async {
|
||||
await isar.address.put(newChangeAddress);
|
||||
await isar.addresses.put(newChangeAddress);
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Exception rethrown from _checkChangeAddressForTransactions($DerivePathType.bip44): $e\n$s",
|
||||
"Exception rethrown from _checkChangeAddressForTransactions(${DerivePathType.bip44}): $e\n$s",
|
||||
level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
|
@ -1882,9 +1730,9 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
|
||||
Future<void> _checkCurrentReceivingAddressesForTransactions() async {
|
||||
try {
|
||||
for (final type in DerivePathType.values) {
|
||||
await _checkReceivingAddressForTransactions();
|
||||
}
|
||||
// for (final type in DerivePathType.values) {
|
||||
await _checkReceivingAddressForTransactions();
|
||||
// }
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s",
|
||||
|
@ -1906,9 +1754,9 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
|
||||
Future<void> _checkCurrentChangeAddressesForTransactions() async {
|
||||
try {
|
||||
for (final type in DerivePathType.values) {
|
||||
await checkChangeAddressForTransactions();
|
||||
}
|
||||
// for (final type in DerivePathType.values) {
|
||||
await checkChangeAddressForTransactions();
|
||||
// }
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s",
|
||||
|
@ -1928,31 +1776,6 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
}
|
||||
}
|
||||
|
||||
/// attempts to convert a string to a valid scripthash
|
||||
///
|
||||
/// Returns the scripthash or throws an exception on invalid dogecoin address
|
||||
String _convertToScriptHash(String dogecoinAddress, NetworkType network) {
|
||||
try {
|
||||
final output = btc_dart.Address.addressToOutputScript(
|
||||
dogecoinAddress,
|
||||
network,
|
||||
);
|
||||
final hash = sha256.convert(output.toList(growable: false)).toString();
|
||||
|
||||
final chars = hash.split("");
|
||||
final reversedPairs = <String>[];
|
||||
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<List<Map<String, dynamic>>> _fetchHistory(
|
||||
List<String> allAddresses) async {
|
||||
try {
|
||||
|
@ -1966,7 +1789,8 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
if (batches[batchNumber] == null) {
|
||||
batches[batchNumber] = {};
|
||||
}
|
||||
final scripthash = _convertToScriptHash(allAddresses[i], network);
|
||||
final scripthash =
|
||||
AddressUtils.convertToScriptHash(allAddresses[i], network);
|
||||
final id = Logger.isTestEnv ? "$i" : const Uuid().v1();
|
||||
requestIdToAddressMap[id] = allAddresses[i];
|
||||
batches[batchNumber]!.addAll({
|
||||
|
@ -2044,7 +1868,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
// boxName: walletId, key: "changeAddressesP2PKH", value: []);
|
||||
// }
|
||||
|
||||
Future<void> _fetchTransactionData() async {
|
||||
Future<void> _refreshTransactions() async {
|
||||
final List<isar_models.Address> allAddresses =
|
||||
await _fetchAllOwnAddresses();
|
||||
|
||||
|
@ -2064,7 +1888,6 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
coin: coin,
|
||||
);
|
||||
|
||||
// Logging.instance.log("TRANSACTION: ${jsonEncode(tx)}");
|
||||
if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
|
||||
tx["address"] = txHash["address"];
|
||||
tx["height"] = txHash["height"];
|
||||
|
@ -2083,7 +1906,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
await fastFetch(vHashes.toList());
|
||||
|
||||
for (final txObject in allTransactions) {
|
||||
final midSortedTx = await parseTransaction(
|
||||
final txn = await parseTransaction(
|
||||
txObject,
|
||||
cachedElectrumXClient,
|
||||
allAddresses,
|
||||
|
@ -2091,16 +1914,16 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
MINIMUM_CONFIRMATIONS,
|
||||
);
|
||||
|
||||
final tx = await isar.transactions
|
||||
.filter()
|
||||
.txidMatches(midSortedTx.txid)
|
||||
.findFirst();
|
||||
// we don't need to check this but it saves a write tx instead of overwriting the transaction in Isar
|
||||
if (tx == null) {
|
||||
await isar.writeTxn(() async {
|
||||
await isar.transactions.put(midSortedTx);
|
||||
});
|
||||
}
|
||||
// final tx = await isar.transactions
|
||||
// .filter()
|
||||
// .txidMatches(midSortedTx.txid)
|
||||
// .findFirst();
|
||||
// // we don't need to check this but it saves a write tx instead of overwriting the transaction in Isar
|
||||
// if (tx == null) {
|
||||
await isar.writeTxn(() async {
|
||||
await isar.transactions.put(txn);
|
||||
});
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2112,27 +1935,34 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
/// 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(int satoshiAmountToSend, int selectedTxFeeRate,
|
||||
String _recipientAddress, bool isSendAll,
|
||||
{int additionalOutputs = 0, List<UtxoObject>? utxos}) async {
|
||||
dynamic coinSelection(
|
||||
int satoshiAmountToSend,
|
||||
int selectedTxFeeRate,
|
||||
String _recipientAddress,
|
||||
bool isSendAll, {
|
||||
int additionalOutputs = 0,
|
||||
List<isar_models.UTXO>? utxos,
|
||||
}) async {
|
||||
Logging.instance
|
||||
.log("Starting coinSelection ----------", level: LogLevel.Info);
|
||||
final List<UtxoObject> availableOutputs = utxos ?? outputsList;
|
||||
final List<UtxoObject> spendableOutputs = [];
|
||||
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 (var i = 0; i < availableOutputs.length; i++) {
|
||||
if (availableOutputs[i].blocked == false &&
|
||||
availableOutputs[i].status.confirmed == true) {
|
||||
if (availableOutputs[i].isBlocked == false &&
|
||||
availableOutputs[i]
|
||||
.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) ==
|
||||
true) {
|
||||
spendableOutputs.add(availableOutputs[i]);
|
||||
spendableSatoshiValue += availableOutputs[i].value;
|
||||
}
|
||||
}
|
||||
|
||||
// sort spendable by age (oldest first)
|
||||
spendableOutputs.sort(
|
||||
(a, b) => b.status.confirmations.compareTo(a.status.confirmations));
|
||||
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
||||
|
||||
Logging.instance.log("spendableOutputs.length: ${spendableOutputs.length}",
|
||||
level: LogLevel.Info);
|
||||
|
@ -2159,7 +1989,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
// Possible situation right here
|
||||
int satoshisBeingUsed = 0;
|
||||
int inputsBeingConsumed = 0;
|
||||
List<UtxoObject> utxoObjectsToUse = [];
|
||||
List<isar_models.UTXO> utxoObjectsToUse = [];
|
||||
|
||||
for (var i = 0;
|
||||
satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length;
|
||||
|
@ -2238,7 +2068,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
utxoSigningData: utxoSigningData,
|
||||
recipients: [
|
||||
_recipientAddress,
|
||||
await getCurrentAddressForChain(1, DerivePathType.bip44),
|
||||
await _getCurrentAddressForChain(1, DerivePathType.bip44),
|
||||
],
|
||||
satoshiAmounts: [
|
||||
satoshiAmountToSend,
|
||||
|
@ -2292,7 +2122,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
// generate new change address if current change address has been used
|
||||
await checkChangeAddressForTransactions();
|
||||
final String newChangeAddress =
|
||||
await getCurrentAddressForChain(1, DerivePathType.bip44);
|
||||
await _getCurrentAddressForChain(1, DerivePathType.bip44);
|
||||
|
||||
int feeBeingPaid =
|
||||
satoshisBeingUsed - satoshiAmountToSend - changeOutputSize;
|
||||
|
@ -2460,7 +2290,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
}
|
||||
|
||||
Future<Map<String, dynamic>> fetchBuildTxData(
|
||||
List<UtxoObject> utxosToUse,
|
||||
List<isar_models.UTXO> utxosToUse,
|
||||
) async {
|
||||
// return data
|
||||
Map<String, dynamic> results = {};
|
||||
|
@ -2563,7 +2393,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
|
||||
/// Builds and signs a transaction
|
||||
Future<Map<String, dynamic>> buildTransaction({
|
||||
required List<UtxoObject> utxosToUse,
|
||||
required List<isar_models.UTXO> utxosToUse,
|
||||
required Map<String, dynamic> utxoSigningData,
|
||||
required List<String> recipients,
|
||||
required List<int> satoshiAmounts,
|
||||
|
@ -2830,22 +2660,23 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
|
||||
@override
|
||||
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
|
||||
final available =
|
||||
Format.decimalAmountToSatoshis(await availableBalance, coin);
|
||||
final available = balance.spendable;
|
||||
|
||||
if (available == satoshiAmount) {
|
||||
return satoshiAmount - sweepAllEstimate(feeRate);
|
||||
return satoshiAmount - (await sweepAllEstimate(feeRate));
|
||||
} else if (satoshiAmount <= 0 || satoshiAmount > available) {
|
||||
return roughFeeEstimate(1, 2, feeRate);
|
||||
}
|
||||
|
||||
int runningBalance = 0;
|
||||
int inputCount = 0;
|
||||
for (final output in outputsList) {
|
||||
runningBalance += output.value;
|
||||
inputCount++;
|
||||
if (runningBalance > satoshiAmount) {
|
||||
break;
|
||||
for (final output in (await utxos)) {
|
||||
if (!output.isBlocked) {
|
||||
runningBalance += output.value;
|
||||
inputCount++;
|
||||
if (runningBalance > satoshiAmount) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2877,11 +2708,12 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
(feeRatePerKB / 1000).ceil();
|
||||
}
|
||||
|
||||
int sweepAllEstimate(int feeRate) {
|
||||
Future<int> sweepAllEstimate(int feeRate) async {
|
||||
int available = 0;
|
||||
int inputCount = 0;
|
||||
for (final output in outputsList) {
|
||||
if (output.status.confirmed) {
|
||||
for (final output in (await utxos)) {
|
||||
if (!output.isBlocked &&
|
||||
output.isConfirmed(storedChainHeight, MINIMUM_CONFIRMATIONS)) {
|
||||
available += output.value;
|
||||
inputCount++;
|
||||
}
|
||||
|
@ -2906,7 +2738,7 @@ class DogecoinWallet extends CoinServiceAPI {
|
|||
|
||||
// Add that new receiving address
|
||||
await isar.writeTxn(() async {
|
||||
await isar.address.put(newReceivingAddress);
|
||||
await isar.addresses.put(newReceivingAddress);
|
||||
});
|
||||
|
||||
return true;
|
||||
|
|
Loading…
Reference in a new issue