From 275e3ab4cb2df0b70e1e9e1e61b55f99c94848c7 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 11 Jan 2023 20:09:08 -0600 Subject: [PATCH] WIP migrate particl_wallet.dart to isar transactions, addresses, and utxos, as well as the cleaner balance model --- .../coins/particl/particl_wallet.dart | 1307 ++++++++--------- 1 file changed, 578 insertions(+), 729 deletions(-) diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index fe535dbf0..b95c6d567 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -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; @@ -10,16 +9,14 @@ import 'package:bitcoindart/bitcoindart.dart'; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; -import 'package:devicelocale/devicelocale.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/models.dart' as models; +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/paymint/transactions_model.dart'; -import 'package:stackwallet/models/paymint/utxo_model.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'; @@ -28,7 +25,6 @@ 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/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -39,6 +35,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; @@ -153,8 +150,6 @@ class ParticlWallet extends CoinServiceAPI { } } - List outputsList = []; - @override set isFavorite(bool markFavorite) { DB.instance.put( @@ -177,63 +172,34 @@ class ParticlWallet extends CoinServiceAPI { Coin get coin => _coin; @override - Future> get allOwnAddresses => - _allOwnAddresses ??= _fetchAllOwnAddresses(); - Future>? _allOwnAddresses; - - Future? _utxoData; - Future get utxoData => _utxoData ??= _fetchUtxoData(); + Future> get utxos => isar.utxos.where().findAll(); @override - Future> get unspentOutputs async => - (await utxoData).unspentOutputArray; + Future> get transactions => + isar.transactions.where().sortByTimestampDesc().findAll(); @override - Future get availableBalance async { - final data = await utxoData; - return Format.satoshisToAmount( - data.satoshiBalance - data.satoshiBalanceUnconfirmed, - coin: coin); - } + Future get currentReceivingAddress async => + (await _currentReceivingAddress).value; - @override - Future get pendingBalance async { - final data = await utxoData; - return Format.satoshisToAmount(data.satoshiBalanceUnconfirmed, coin: coin); - } + Future get _currentReceivingAddress async => + (await isar.addresses + .filter() + .typeEqualTo(isar_models.AddressType.p2wpkh) + .subTypeEqualTo(isar_models.AddressSubType.receiving) + .sortByDerivationIndexDesc() + .findFirst())!; - @override - Future get balanceMinusMaxFee async => - (await availableBalance) - - (Decimal.fromInt((await maxFee)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(); + Future get currentChangeAddress async => + (await _currentChangeAddress).value; - @override - Future get totalBalance async { - if (!isActive) { - final totalBalance = DB.instance - .get(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 get currentReceivingAddress => _currentReceivingAddress ??= - _getCurrentAddressForChain(0, DerivePathType.bip84); - Future? _currentReceivingAddress; - - Future get currentLegacyReceivingAddress => - _currentReceivingAddressP2PKH ??= - _getCurrentAddressForChain(0, DerivePathType.bip44); - Future? _currentReceivingAddressP2PKH; + Future get _currentChangeAddress async => + (await isar.addresses + .filter() + .typeEqualTo(isar_models.AddressType.p2wpkh) + .subTypeEqualTo(isar_models.AddressSubType.change) + .sortByDerivationIndexDesc() + .findFirst())!; @override Future exit() async { @@ -241,6 +207,7 @@ class ParticlWallet extends CoinServiceAPI { timer?.cancel(); timer = null; stopNetworkAlivePinging(); + await isar.close(); } bool _hasCalledExit = false; @@ -274,6 +241,7 @@ class ParticlWallet extends CoinServiceAPI { } } + @override int get storedChainHeight { final storedHeight = DB.instance .get(boxName: walletId, key: "storedChainHeight") as int?; @@ -389,8 +357,8 @@ class ParticlWallet extends CoinServiceAPI { int txCountBatchSize, bip32.BIP32 root, DerivePathType type, - int account) async { - List addressArray = []; + int chain) async { + List addressArray = []; int returningIndex = -1; Map> derivations = {}; int gapCounter = 0; @@ -399,7 +367,7 @@ class ParticlWallet extends CoinServiceAPI { index += txCountBatchSize) { List iterationsAddressArray = []; Logging.instance.log( - "index: $index, \t GapCounter $account ${type.name}: $gapCounter", + "index: $index, \t GapCounter $chain ${type.name}: $gapCounter", level: LogLevel.Info); final _id = "k_$index"; @@ -410,31 +378,44 @@ class ParticlWallet extends CoinServiceAPI { final node = await compute( getBip32NodeFromRootWrapper, Tuple4( - account, + chain, index + j, root, type, ), ); - String? address; + String addressString; + isar_models.AddressType addrType; switch (type) { case DerivePathType.bip44: - address = P2PKH( + addressString = P2PKH( data: PaymentData(pubkey: node.publicKey), network: _network) .data .address!; + addrType = isar_models.AddressType.p2pkh; break; case DerivePathType.bip84: - address = P2WPKH( + addressString = P2WPKH( network: _network, data: PaymentData(pubkey: node.publicKey)) .data .address!; + addrType = isar_models.AddressType.p2wpkh; break; default: throw Exception("No Path type $type exists"); } + + final address = isar_models.Address() + ..subType = chain == 0 + ? isar_models.AddressSubType.receiving + : isar_models.AddressSubType.change + ..type = addrType + ..publicKey = node.publicKey + ..value = addressString + ..derivationIndex = index + j; + receivingNodes.addAll({ "${_id}_$j": { "node": node, @@ -442,7 +423,7 @@ class ParticlWallet extends CoinServiceAPI { } }); txCountCallArgs.addAll({ - "${_id}_$j": address, + "${_id}_$j": addressString, }); } @@ -454,9 +435,10 @@ class ParticlWallet extends CoinServiceAPI { int count = counts["${_id}_$k"]!; if (count > 0) { final node = receivingNodes["${_id}_$k"]; + final address = node["address"] as isar_models.Address; // add address to array - addressArray.add(node["address"] as String); - iterationsAddressArray.add(node["address"] as String); + addressArray.add(address); + iterationsAddressArray.add(address.value); // set current index returningIndex = index + k; // reset counter @@ -518,13 +500,13 @@ class ParticlWallet extends CoinServiceAPI { final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network)); - List p2pkhReceiveAddressArray = []; - List p2wpkhReceiveAddressArray = []; + List p2pkhReceiveAddressArray = []; + List p2wpkhReceiveAddressArray = []; int p2pkhReceiveIndex = -1; int p2wpkhReceiveIndex = -1; - List p2pkhChangeAddressArray = []; - List p2wpkhChangeAddressArray = []; + List p2pkhChangeAddressArray = []; + List p2wpkhChangeAddressArray = []; int p2pkhChangeIndex = -1; int p2wpkhChangeIndex = -1; @@ -554,25 +536,25 @@ class ParticlWallet extends CoinServiceAPI { [resultReceive44, resultReceive84, resultChange44, resultChange84]); p2pkhReceiveAddressArray = - (await resultReceive44)['addressArray'] as List; + (await resultReceive44)['addressArray'] as List; p2pkhReceiveIndex = (await resultReceive44)['index'] as int; p2pkhReceiveDerivations = (await resultReceive44)['derivations'] as Map>; p2wpkhReceiveAddressArray = - (await resultReceive84)['addressArray'] as List; + (await resultReceive84)['addressArray'] as List; p2wpkhReceiveIndex = (await resultReceive84)['index'] as int; p2wpkhReceiveDerivations = (await resultReceive84)['derivations'] as Map>; p2pkhChangeAddressArray = - (await resultChange44)['addressArray'] as List; + (await resultChange44)['addressArray'] as List; p2pkhChangeIndex = (await resultChange44)['index'] as int; p2pkhChangeDerivations = (await resultChange44)['derivations'] as Map>; p2wpkhChangeAddressArray = - (await resultChange84)['addressArray'] as List; + (await resultChange84)['addressArray'] as List; p2wpkhChangeIndex = (await resultChange84)['index'] as int; p2wpkhChangeDerivations = (await resultChange84)['derivations'] as Map>; @@ -611,14 +593,12 @@ class ParticlWallet extends CoinServiceAPI { final address = await _generateAddressForChain(0, 0, DerivePathType.bip44); p2pkhReceiveAddressArray.add(address); - p2pkhReceiveIndex = 0; } if (p2wpkhReceiveIndex == -1) { final address = await _generateAddressForChain(0, 0, DerivePathType.bip84); p2wpkhReceiveAddressArray.add(address); - p2wpkhReceiveIndex = 0; } // If restoring a wallet that never sent any funds with change, then set changeArray @@ -627,46 +607,25 @@ class ParticlWallet extends CoinServiceAPI { final address = await _generateAddressForChain(1, 0, DerivePathType.bip44); p2pkhChangeAddressArray.add(address); - p2pkhChangeIndex = 0; } if (p2wpkhChangeIndex == -1) { final address = await _generateAddressForChain(1, 0, DerivePathType.bip84); p2wpkhChangeAddressArray.add(address); - p2wpkhChangeIndex = 0; } - await DB.instance.put( - boxName: walletId, - key: 'receivingAddressesP2WPKH', - value: p2wpkhReceiveAddressArray); - await DB.instance.put( - boxName: walletId, - key: 'changeAddressesP2WPKH', - value: p2wpkhChangeAddressArray); - await DB.instance.put( - boxName: walletId, - key: 'receivingAddressesP2PKH', - value: p2pkhReceiveAddressArray); - await DB.instance.put( - boxName: walletId, - key: 'changeAddressesP2PKH', - value: p2pkhChangeAddressArray); - await DB.instance.put( - boxName: walletId, - key: 'receivingIndexP2WPKH', - value: p2wpkhReceiveIndex); - await DB.instance.put( - boxName: walletId, - key: 'changeIndexP2WPKH', - value: p2wpkhChangeIndex); - await DB.instance.put( - boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex); - await DB.instance.put( - boxName: walletId, - key: 'receivingIndexP2PKH', - value: p2pkhReceiveIndex); + await _isarInit(); + + await isar.writeTxn(() async { + await isar.addresses.putAll(p2wpkhReceiveAddressArray); + await isar.addresses.putAll(p2wpkhChangeAddressArray); + await isar.addresses.putAll(p2pkhReceiveAddressArray); + await isar.addresses.putAll(p2pkhChangeAddressArray); + }); + + await _updateUTXOs(); + await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance @@ -710,11 +669,14 @@ class ParticlWallet extends CoinServiceAPI { } if (!needsRefresh) { var allOwnAddresses = await _fetchAllOwnAddresses(); - List> allTxs = - await _fetchHistory(allOwnAddresses); - final txData = await transactionData; + List> allTxs = await _fetchHistory( + allOwnAddresses.map((e) => e.value).toList(growable: false)); for (Map transaction in allTxs) { - if (txData.findTransaction(transaction['tx_hash'] as String) == + final txid = transaction['tx_hash'] as String; + if ((await isar.transactions + .filter() + .txidMatches(txid) + .findFirst()) == null) { Logging.instance.log( " txid not found in address history already ${transaction['tx_hash']}", @@ -733,16 +695,25 @@ class ParticlWallet extends CoinServiceAPI { } } - Future getAllTxsToWatch( - TransactionData txData, - ) async { + Future getAllTxsToWatch() async { if (_hasCalledExit) return; - List unconfirmedTxnsToNotifyPending = []; - List unconfirmedTxnsToNotifyConfirmed = []; + List unconfirmedTxnsToNotifyPending = []; + List unconfirmedTxnsToNotifyConfirmed = []; - for (final chunk in txData.txChunks) { - for (final tx in chunk.transactions) { - if (tx.confirmedStatus) { + final currentChainHeight = await chainHeight; + + final txCount = await isar.transactions.count(); + + const paginateLimit = 50; + + for (int i = 0; i < txCount; i += paginateLimit) { + final transactions = await isar.transactions + .where() + .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)) { @@ -759,31 +730,33 @@ class ParticlWallet extends CoinServiceAPI { // notify on unconfirmed transactions for (final tx in unconfirmedTxnsToNotifyPending) { - if (tx.txType == "Received") { + final confirmations = tx.getConfirmations(currentChainHeight); + + if (tx.type == isar_models.TransactionType.incoming) { unawaited(NotificationApi.showNotification( title: "Incoming transaction", body: walletName, walletId: walletId, iconAssetName: Assets.svg.iconFor(coin: coin), date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, coinName: coin.name, txid: tx.txid, - confirmations: tx.confirmations, + confirmations: confirmations, requiredConfirmations: MINIMUM_CONFIRMATIONS, )); await txTracker.addNotifiedPending(tx.txid); - } else if (tx.txType == "Sent") { + } else if (tx.type == isar_models.TransactionType.outgoing) { unawaited(NotificationApi.showNotification( title: "Sending transaction", body: walletName, walletId: walletId, iconAssetName: Assets.svg.iconFor(coin: coin), date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, coinName: coin.name, txid: tx.txid, - confirmations: tx.confirmations, + confirmations: confirmations, requiredConfirmations: MINIMUM_CONFIRMATIONS, )); await txTracker.addNotifiedPending(tx.txid); @@ -792,7 +765,7 @@ class ParticlWallet extends CoinServiceAPI { // notify on confirmed for (final tx in unconfirmedTxnsToNotifyConfirmed) { - if (tx.txType == "Received") { + if (tx.type == isar_models.TransactionType.incoming) { unawaited(NotificationApi.showNotification( title: "Incoming transaction confirmed", body: walletName, @@ -803,7 +776,7 @@ class ParticlWallet extends CoinServiceAPI { coinName: coin.name, )); await txTracker.addNotifiedConfirmed(tx.txid); - } else if (tx.txType == "Sent") { + } else if (tx.type == isar_models.TransactionType.outgoing) { unawaited(NotificationApi.showNotification( title: "Outgoing transaction confirmed", body: walletName, @@ -883,40 +856,30 @@ class ParticlWallet extends CoinServiceAPI { } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); - final changeAddressForTransactions = - _checkChangeAddressForTransactions(DerivePathType.bip84); + await _checkChangeAddressForTransactions(); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); - final currentReceivingAddressesForTransactions = - _checkCurrentReceivingAddressesForTransactions(); + await _checkCurrentReceivingAddressesForTransactions(); - final newTxData = _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)); - _transactionData = Future(() => newTxData); - GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.70, walletId)); _feeObject = Future(() => feeObj); - _utxoData = Future(() => newUtxoData); + + await utxosRefreshFuture; GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.80, walletId)); - final allTxsToWatch = getAllTxsToWatch(await newTxData); - await Future.wait([ - newTxData, - changeAddressForTransactions, - currentReceivingAddressesForTransactions, - newUtxoData, - feeObj, - allTxsToWatch, - ]); + await fetchFuture; + await getAllTxsToWatch(); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.90, walletId)); } @@ -998,9 +961,7 @@ class ParticlWallet 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; } @@ -1076,24 +1037,6 @@ class ParticlWallet extends CoinServiceAPI { } } - @override - Future send({ - required String toAddress, - required int amount, - Map 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 testNetworkConnection() async { try { @@ -1166,6 +1109,22 @@ class ParticlWallet extends CoinServiceAPI { ]); } + Future _isarInit() async { + isar = await Isar.open( + [ + isar_models.TransactionSchema, + isar_models.TransactionNoteSchema, + isar_models.InputSchema, + isar_models.OutputSchema, + isar_models.UTXOSchema, + isar_models.AddressSchema, + ], + directory: (await StackFileSystem.applicationIsarDirectory()).path, + inspector: false, + name: walletId, + ); + } + @override Future initializeExisting() async { Logging.instance.log("Opening existing ${coin.prettyName} wallet.", @@ -1176,66 +1135,55 @@ class ParticlWallet extends CoinServiceAPI { "Attempted to initialize an existing wallet using an unknown wallet ID!"); } await _prefs.init(); - final data = - DB.instance.get(boxName: walletId, key: "latest_tx_model") - as TransactionData?; - if (data != null) { - _transactionData = Future(() => data); - } + await _isarInit(); } - @override - Future get transactionData => - _transactionData ??= _fetchTransactionData(); - Future? _transactionData; - - TransactionData? cachedTxData; - // TODO make sure this copied implementation from bitcoin_wallet.dart applies for particl just as well--or import it // hack to add tx to txData before refresh completes // required based on current app architecture where we don't properly store // transactions locally in a good way @override Future updateSentCachedTxData(Map txData) async { - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; - final String worthNow = Format.localizedStringAsFixed( - value: - ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(scaleOnInfinitePrecision: 2), - decimalPlaces: 2, - locale: locale!); - - final tx = models.Transaction( - txid: txData["txid"] as String, - confirmedStatus: false, - timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, - txType: "Sent", - amount: txData["recipientAmt"] as int, - worthNow: worthNow, - worthAtBlockTimestamp: worthNow, - fees: txData["fee"] as int, - inputSize: 0, - outputSize: 0, - inputs: [], - outputs: [], - address: txData["address"] as String, - height: -1, - confirmations: 0, - ); - - if (cachedTxData == null) { - final data = await _fetchTransactionData(); - _transactionData = Future(() => data); - } else { - final transactions = cachedTxData!.getAllTransactions(); - transactions[tx.txid] = tx; - cachedTxData = models.TransactionData.fromMap(transactions); - _transactionData = Future(() => cachedTxData!); - } + // final priceData = + // await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + // Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + // final locale = + // Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; + // final String worthNow = Format.localizedStringAsFixed( + // value: + // ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + // Decimal.fromInt(Constants.satsPerCoin(coin))) + // .toDecimal(scaleOnInfinitePrecision: 2), + // decimalPlaces: 2, + // locale: locale!); + // + // final tx = models.Transaction( + // txid: txData["txid"] as String, + // confirmedStatus: false, + // timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + // txType: "Sent", + // amount: txData["recipientAmt"] as int, + // worthNow: worthNow, + // worthAtBlockTimestamp: worthNow, + // fees: txData["fee"] as int, + // inputSize: 0, + // outputSize: 0, + // inputs: [], + // outputs: [], + // address: txData["address"] as String, + // height: -1, + // confirmations: 0, + // ); + // + // if (cachedTxData == null) { + // final data = await _refreshTransactions(); + // _transactionData = Future(() => data); + // } else { + // final transactions = cachedTxData!.getAllTransactions(); + // transactions[tx.txid] = tx; + // cachedTxData = models.TransactionData.fromMap(transactions); + // _transactionData = Future(() => cachedTxData!); + // } } @override @@ -1265,7 +1213,7 @@ class ParticlWallet extends CoinServiceAPI { late SecureStorageInterface _secureStore; - late PriceAPI _priceAPI; + late Isar isar; ParticlWallet({ required String walletId, @@ -1274,7 +1222,6 @@ class ParticlWallet extends CoinServiceAPI { required ElectrumX client, required CachedElectrumX cachedClient, required TransactionNotificationTracker tracker, - PriceAPI? priceAPI, required SecureStorageInterface secureStore, }) { txTracker = tracker; @@ -1283,8 +1230,6 @@ class ParticlWallet extends CoinServiceAPI { _coin = coin; _electrumXClient = client; _cachedElectrumXClient = cachedClient; - - _priceAPI = priceAPI ?? PriceAPI(Client()); _secureStore = secureStore; } @@ -1341,38 +1286,44 @@ class ParticlWallet extends CoinServiceAPI { ); } - Future> _fetchAllOwnAddresses() async { - final List allAddresses = []; - final receivingAddresses = DB.instance.get( - boxName: walletId, key: 'receivingAddressesP2WPKH') as List; - final changeAddresses = DB.instance.get( - boxName: walletId, key: 'changeAddressesP2WPKH') as List; - final receivingAddressesP2PKH = DB.instance.get( - boxName: walletId, key: 'receivingAddressesP2PKH') as List; - final changeAddressesP2PKH = - DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') - as List; - - for (var i = 0; i < receivingAddresses.length; i++) { - if (!allAddresses.contains(receivingAddresses[i])) { - allAddresses.add(receivingAddresses[i] as String); - } - } - for (var i = 0; i < changeAddresses.length; i++) { - if (!allAddresses.contains(changeAddresses[i])) { - allAddresses.add(changeAddresses[i] as String); - } - } - for (var i = 0; i < receivingAddressesP2PKH.length; i++) { - if (!allAddresses.contains(receivingAddressesP2PKH[i])) { - allAddresses.add(receivingAddressesP2PKH[i] as String); - } - } - for (var i = 0; i < changeAddressesP2PKH.length; i++) { - if (!allAddresses.contains(changeAddressesP2PKH[i])) { - allAddresses.add(changeAddressesP2PKH[i] as String); - } - } + Future> _fetchAllOwnAddresses() async { + final allAddresses = await isar.addresses + .filter() + .subTypeEqualTo(isar_models.AddressSubType.receiving) + .or() + .subTypeEqualTo(isar_models.AddressSubType.change) + .findAll(); + // final List allAddresses = []; + // final receivingAddresses = DB.instance.get( + // boxName: walletId, key: 'receivingAddressesP2WPKH') as List; + // final changeAddresses = DB.instance.get( + // boxName: walletId, key: 'changeAddressesP2WPKH') as List; + // final receivingAddressesP2PKH = DB.instance.get( + // boxName: walletId, key: 'receivingAddressesP2PKH') as List; + // final changeAddressesP2PKH = + // DB.instance.get(boxName: walletId, key: 'changeAddressesP2PKH') + // as List; + // + // for (var i = 0; i < receivingAddresses.length; i++) { + // if (!allAddresses.contains(receivingAddresses[i])) { + // allAddresses.add(receivingAddresses[i] as String); + // } + // } + // for (var i = 0; i < changeAddresses.length; i++) { + // if (!allAddresses.contains(changeAddresses[i])) { + // allAddresses.add(changeAddresses[i] as String); + // } + // } + // for (var i = 0; i < receivingAddressesP2PKH.length; i++) { + // if (!allAddresses.contains(receivingAddressesP2PKH[i])) { + // allAddresses.add(receivingAddressesP2PKH[i] as String); + // } + // } + // for (var i = 0; i < changeAddressesP2PKH.length; i++) { + // if (!allAddresses.contains(changeAddressesP2PKH[i])) { + // allAddresses.add(changeAddressesP2PKH[i] as String); + // } + // } return allAddresses; } @@ -1431,20 +1382,6 @@ class ParticlWallet extends CoinServiceAPI { key: '${_walletId}_mnemonic', value: bip39.generateMnemonic(strength: 256)); - // Set relevant indexes - await DB.instance - .put(boxName: walletId, key: "receivingIndexP2WPKH", value: 0); - await DB.instance - .put(boxName: walletId, key: "changeIndexP2WPKH", value: 0); - await DB.instance - .put(boxName: walletId, key: "receivingIndexP2PKH", value: 0); - await DB.instance - .put(boxName: walletId, key: "changeIndexP2PKH", value: 0); - await DB.instance.put( - 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( boxName: walletId, @@ -1452,49 +1389,29 @@ class ParticlWallet extends CoinServiceAPI { value: {}); // Generate and add addresses to relevant arrays - await Future.wait([ + final initialAddresses = await Future.wait([ // P2WPKH - _generateAddressForChain(0, 0, DerivePathType.bip84).then( - (initialReceivingAddressP2WPKH) { - _addToAddressesArrayForChain( - initialReceivingAddressP2WPKH, 0, DerivePathType.bip84); - _currentReceivingAddress = - Future(() => initialReceivingAddressP2WPKH); - }, - ), - _generateAddressForChain(1, 0, DerivePathType.bip84).then( - (initialChangeAddressP2WPKH) => _addToAddressesArrayForChain( - initialChangeAddressP2WPKH, - 1, - DerivePathType.bip84, - ), - ), + _generateAddressForChain(0, 0, DerivePathType.bip84), + _generateAddressForChain(1, 0, DerivePathType.bip84), // P2PKH - _generateAddressForChain(0, 0, DerivePathType.bip44).then( - (initialReceivingAddressP2PKH) { - _addToAddressesArrayForChain( - initialReceivingAddressP2PKH, 0, DerivePathType.bip44); - _currentReceivingAddressP2PKH = - Future(() => initialReceivingAddressP2PKH); - }, - ), - _generateAddressForChain(1, 0, DerivePathType.bip44).then( - (initialChangeAddressP2PKH) => _addToAddressesArrayForChain( - initialChangeAddressP2PKH, - 1, - DerivePathType.bip44, - ), - ), + _generateAddressForChain(0, 0, DerivePathType.bip44), + _generateAddressForChain(1, 0, DerivePathType.bip44), ]); + await _isarInit(); + + await isar.writeTxn(() async { + await isar.addresses.putAll(initialAddresses); + }); + Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); } /// Generates a new internal or external chain address for the wallet using a BIP84, BIP44, or BIP49 derivation path. /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! /// [index] - This can be any integer >= 0 - Future _generateAddressForChain( + Future _generateAddressForChain( int chain, int index, DerivePathType derivePathType, @@ -1512,13 +1429,16 @@ class ParticlWallet extends CoinServiceAPI { ); final data = PaymentData(pubkey: node.publicKey); String address; + isar_models.AddressType addrType; switch (derivePathType) { case DerivePathType.bip44: address = P2PKH(data: data, network: _network).data.address!; + addrType = isar_models.AddressType.p2pkh; break; case DerivePathType.bip84: address = P2WPKH(network: _network, data: data).data.address!; + addrType = isar_models.AddressType.p2wpkh; break; } @@ -1531,88 +1451,44 @@ class ParticlWallet extends CoinServiceAPI { derivePathType: derivePathType, ); - return address; - } - - /// Increases the index for either the internal or external chain, depending on [chain]. - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future _incrementAddressIndexForChain( - int chain, DerivePathType derivePathType) async { - // Here we assume chain == 1 if it isn't 0 - String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; - switch (derivePathType) { - case DerivePathType.bip44: - indexKey += "P2PKH"; - break; - case DerivePathType.bip84: - indexKey += "P2WPKH"; - break; - } - final newIndex = - (DB.instance.get(boxName: walletId, key: indexKey)) + 1; - await DB.instance - .put(boxName: walletId, key: indexKey, value: newIndex); - } - - /// Adds [address] to the relevant chain's address array, which is determined by [chain]. - /// [address] - Expects a standard native segwit address - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future _addToAddressesArrayForChain( - String address, int chain, DerivePathType derivePathType) async { - String chainArray = ''; - if (chain == 0) { - chainArray = 'receivingAddresses'; - } else { - chainArray = 'changeAddresses'; - } - switch (derivePathType) { - case DerivePathType.bip44: - chainArray += "P2PKH"; - break; - case DerivePathType.bip84: - chainArray += "P2WPKH"; - break; - } - - final addressArray = - DB.instance.get(boxName: walletId, key: chainArray); - if (addressArray == null) { - Logging.instance.log( - 'Attempting to add the following to $chainArray array for chain $chain:${[ - address - ]}', - level: LogLevel.Info); - await DB.instance - .put(boxName: walletId, key: chainArray, value: [address]); - } else { - // Make a deep copy of the existing list - final List newArray = []; - addressArray - .forEach((dynamic _address) => newArray.add(_address as String)); - newArray.add(address); // Add the address passed into the method - await DB.instance - .put(boxName: walletId, key: chainArray, value: newArray); - } + return isar_models.Address() + ..derivationIndex = index + ..value = address + ..publicKey = node.publicKey + ..type = addrType + ..subType = chain == 0 + ? isar_models.AddressSubType.receiving + : isar_models.AddressSubType.change; } /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] /// and /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! Future _getCurrentAddressForChain( - int chain, DerivePathType derivePathType) async { - // Here, we assume that chain == 1 if it isn't 0 - String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; + int chain, + DerivePathType derivePathType, + ) async { + final subType = chain == 0 // Here, we assume that chain == 1 if it isn't 0 + ? isar_models.AddressSubType.receiving + : isar_models.AddressSubType.change; + + isar_models.AddressType type; + isar_models.Address? address; switch (derivePathType) { case DerivePathType.bip44: - arrayKey += "P2PKH"; + type = isar_models.AddressType.p2pkh; break; case DerivePathType.bip84: - arrayKey += "P2WPKH"; + type = isar_models.AddressType.p2wpkh; break; } - final internalChainArray = - DB.instance.get(boxName: walletId, key: arrayKey); - return internalChainArray.last as String; + address = await isar.addresses + .filter() + .typeEqualTo(type) + .subTypeEqualTo(subType) + .sortByDerivationIndexDesc() + .findFirst(); + return address!.value; } String _buildDerivationStorageKey({ @@ -1715,8 +1591,8 @@ class ParticlWallet extends CoinServiceAPI { await _secureStore.write(key: key, value: newReceiveDerivationsString); } - Future _fetchUtxoData() async { - final List allAddresses = await _fetchAllOwnAddresses(); + Future _updateUTXOs() async { + final allAddresses = await _fetchAllOwnAddresses(); try { final fetchedUtxoList = >>[]; @@ -1728,7 +1604,8 @@ class ParticlWallet extends CoinServiceAPI { if (batches[batchNumber] == null) { batches[batchNumber] = {}; } - final scripthash = _convertToScriptHash(allAddresses[i], _network); + final scripthash = + _convertToScriptHash(allAddresses[i].value, _network); batches[batchNumber]!.addAll({ scripthash: [scripthash] }); @@ -1746,142 +1623,119 @@ class ParticlWallet extends CoinServiceAPI { } } } - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final List> outputArray = []; - int satoshiBalance = 0; + + final currentChainHeight = await chainHeight; + + final List 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 utxo = {}; - final int confirmations = txn["confirmations"] as int? ?? 0; - final bool confirmed = confirmations >= 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"] = {}; - 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 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 allOutputs = dataModel.unspentOutputArray; Logging.instance - .log('Outputs fetched: $allOutputs', level: LogLevel.Info); - await _sortOutputs(allOutputs); - await DB.instance.put( - boxName: walletId, key: 'latest_utxo_model', value: dataModel); - await DB.instance.put( - 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(boxName: walletId, key: 'latest_utxo_model') - as models.UtxoData?; - - if (latestTxModel == null) { - final emptyModel = { - "total_user_currency": "0.00", - "total_sats": 0, - "total_btc": "0", - "outputArray": [] - }; - return UtxoData.fromJson(emptyModel); - } else { - Logging.instance - .log("Old output model located", level: LogLevel.Warning); - return latestTxModel; - } } } - /// Takes in a list of UtxoObjects and adds a name (dependent on object index within list) - /// and checks for the txid associated with the utxo being blocked and marks it accordingly. - /// Now also checks for output labeling. - Future _sortOutputs(List utxos) async { - final blockedHashArray = - DB.instance.get(boxName: walletId, key: 'blocked_tx_hashes') - as List?; - final List lst = []; - if (blockedHashArray != null) { - for (var hash in blockedHashArray) { - lst.add(hash as String); - } - } - final labels = - DB.instance.get(boxName: walletId, key: 'labels') as Map? ?? - {}; + @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 _sortOutputs(List utxos) async { + // final blockedHashArray = + // DB.instance.get(boxName: walletId, key: 'blocked_tx_hashes') + // as List?; + // final List lst = []; + // if (blockedHashArray != null) { + // for (var hash in blockedHashArray) { + // lst.add(hash as String); + // } + // } + // final labels = + // DB.instance.get(boxName: walletId, key: 'labels') as Map? ?? + // {}; + // + // outputsList = []; + // + // for (var i = 0; i < utxos.length; i++) { + // if (labels[utxos[i].txid] != null) { + // utxos[i].txName = labels[utxos[i].txid] as String? ?? ""; + // } else { + // utxos[i].txName = 'Output #$i'; + // } + // + // if (utxos[i].status.confirmed == false) { + // outputsList.add(utxos[i]); + // } else { + // if (lst.contains(utxos[i].txid)) { + // utxos[i].blocked = true; + // outputsList.add(utxos[i]); + // } else if (!lst.contains(utxos[i].txid)) { + // outputsList.add(utxos[i]); + // } + // } + // } + // } Future getTxCount({required String address}) async { String? scripthash; @@ -1921,102 +1775,65 @@ class ParticlWallet extends CoinServiceAPI { } } - Future _checkReceivingAddressForTransactions( - DerivePathType derivePathType) async { + Future _checkReceivingAddressForTransactions() async { try { - final String currentExternalAddr = - await _getCurrentAddressForChain(0, derivePathType); - final int txCount = await getTxCount(address: currentExternalAddr); + final currentReceiving = await _currentReceivingAddress; + + final int txCount = await getTxCount(address: currentReceiving.value); Logging.instance.log( - 'Number of txs for current receiving address $currentExternalAddr: $txCount', + 'Number of txs for current receiving address $currentReceiving: $txCount', level: LogLevel.Info); if (txCount >= 1) { // First increment the receiving index - await _incrementAddressIndexForChain(0, derivePathType); - - // Check the new receiving index - String indexKey = "receivingIndex"; - switch (derivePathType) { - case DerivePathType.bip44: - indexKey += "P2PKH"; - break; - case DerivePathType.bip84: - indexKey += "P2WPKH"; - break; - } - final newReceivingIndex = - DB.instance.get(boxName: walletId, key: indexKey) as int; + final newReceivingIndex = currentReceiving.derivationIndex + 1; // Use new index to derive a new receiving address final newReceivingAddress = await _generateAddressForChain( - 0, newReceivingIndex, derivePathType); + 0, newReceivingIndex, DerivePathType.bip84); - // Add that new receiving address to the array of receiving addresses - await _addToAddressesArrayForChain( - newReceivingAddress, 0, derivePathType); - - // Set the new receiving address that the service - - switch (derivePathType) { - case DerivePathType.bip44: - _currentReceivingAddressP2PKH = Future(() => newReceivingAddress); - break; - case DerivePathType.bip84: - _currentReceivingAddress = Future(() => newReceivingAddress); - break; - } + // Add that new receiving address + await isar.writeTxn(() async { + await isar.addresses.put(newReceivingAddress); + }); } } catch (e, s) { Logging.instance.log( - "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", + "Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s", level: LogLevel.Error); rethrow; } } - Future _checkChangeAddressForTransactions( - DerivePathType derivePathType) async { + Future _checkChangeAddressForTransactions() async { try { - final String currentExternalAddr = - await _getCurrentAddressForChain(1, derivePathType); - final int txCount = await getTxCount(address: currentExternalAddr); + final currentChange = await _currentChangeAddress; + final int txCount = await getTxCount(address: currentChange.value); Logging.instance.log( - 'Number of txs for current change address $currentExternalAddr: $txCount', + 'Number of txs for current change address $currentChange: $txCount', level: LogLevel.Info); if (txCount >= 1) { // First increment the change index - await _incrementAddressIndexForChain(1, derivePathType); - - // Check the new change index - String indexKey = "changeIndex"; - switch (derivePathType) { - case DerivePathType.bip44: - indexKey += "P2PKH"; - break; - case DerivePathType.bip84: - indexKey += "P2WPKH"; - break; - } - final newChangeIndex = - DB.instance.get(boxName: walletId, key: indexKey) as int; + final newChangeIndex = currentChange.derivationIndex + 1; // Use new index to derive a new change address - final newChangeAddress = - await _generateAddressForChain(1, newChangeIndex, derivePathType); + final newChangeAddress = await _generateAddressForChain( + 1, newChangeIndex, DerivePathType.bip84); - // Add that new receiving address to the array of change addresses - await _addToAddressesArrayForChain(newChangeAddress, 1, derivePathType); + // Add that new change address + await isar.writeTxn(() async { + await isar.addresses.put(newChangeAddress); + }); } } on SocketException catch (se, s) { Logging.instance.log( - "SocketException caught in _checkReceivingAddressForTransactions($derivePathType): $se\n$s", + "SocketException caught in _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $se\n$s", level: LogLevel.Error); return; } catch (e, s) { Logging.instance.log( - "Exception rethrown from _checkReceivingAddressForTransactions($derivePathType): $e\n$s", + "Exception rethrown from _checkReceivingAddressForTransactions(${DerivePathType.bip84}): $e\n$s", level: LogLevel.Error); rethrow; } @@ -2024,9 +1841,9 @@ class ParticlWallet extends CoinServiceAPI { Future _checkCurrentReceivingAddressesForTransactions() async { try { - for (final type in DerivePathType.values) { - await _checkReceivingAddressForTransactions(type); - } + // for (final type in DerivePathType.values) { + await _checkReceivingAddressForTransactions(); + // } } catch (e, s) { Logging.instance.log( "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", @@ -2048,9 +1865,9 @@ class ParticlWallet extends CoinServiceAPI { Future _checkCurrentChangeAddressesForTransactions() async { try { - for (final type in DerivePathType.values) { - await _checkChangeAddressForTransactions(type); - } + // for (final type in DerivePathType.values) { + await _checkChangeAddressForTransactions(); + // } } catch (e, s) { Logging.instance.log( "Exception rethrown from _checkCurrentChangeAddressesForTransactions(): $e\n$s", @@ -2184,39 +2001,44 @@ class ParticlWallet extends CoinServiceAPI { return allTransactions; } - Future _fetchTransactionData() async { - final List allAddresses = await _fetchAllOwnAddresses(); + Future _refreshTransactions() async { + final allAddresses = await _fetchAllOwnAddresses(); - final changeAddresses = DB.instance.get( - boxName: walletId, key: 'changeAddressesP2WPKH') as List; + // final changeAddresses = DB.instance.get( + // boxName: walletId, key: 'changeAddressesP2WPKH') as List; - final List> allTxHashes = - await _fetchHistory(allAddresses); + List changeAddresses = allAddresses + .where((e) => e.subType == isar_models.AddressSubType.change) + .map((e) => e.value) + .toList(); - final cachedTransactions = - DB.instance.get(boxName: walletId, key: 'latest_tx_model') - as TransactionData?; - int latestTxnBlockHeight = - DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") - as int? ?? - 0; + final List> allTxHashes = await _fetchHistory( + allAddresses.map((e) => e.value).toList(growable: false)); - final unconfirmedCachedTransactions = - cachedTransactions?.getAllTransactions() ?? {}; - unconfirmedCachedTransactions - .removeWhere((key, value) => value.confirmedStatus); - - if (cachedTransactions != null) { - for (final tx in allTxHashes.toList(growable: false)) { - final txHeight = tx["height"] as int; - if (txHeight > 0 && - txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { - if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { - allTxHashes.remove(tx); - } - } - } - } + // final cachedTransactions = + // DB.instance.get(boxName: walletId, key: 'latest_tx_model') + // as TransactionData?; + // int latestTxnBlockHeight = + // DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") + // as int? ?? + // 0; + // + // final unconfirmedCachedTransactions = + // cachedTransactions?.getAllTransactions() ?? {}; + // unconfirmedCachedTransactions + // .removeWhere((key, value) => value.confirmedStatus); + // + // if (cachedTransactions != null) { + // for (final tx in allTxHashes.toList(growable: false)) { + // final txHeight = tx["height"] as int; + // if (txHeight > 0 && + // txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { + // if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { + // allTxHashes.remove(tx); + // } + // } + // } + // } Set hashes = {}; for (var element in allTxHashes) { @@ -2249,10 +2071,7 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log("allTransactions length: ${allTransactions.length}", level: LogLevel.Info); - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final List> midSortedArray = []; + // final List> midSortedArray = []; Set vHashes = {}; for (final txObject in allTransactions) { @@ -2264,6 +2083,8 @@ class ParticlWallet extends CoinServiceAPI { } await fastFetch(vHashes.toList()); + final List txns = []; + for (final txObject in allTransactions) { List sendersArray = []; List recipientsArray = []; @@ -2315,7 +2136,7 @@ class ParticlWallet extends CoinServiceAPI { .log("output is private (RingCT)", level: LogLevel.Info); } else { // TODO detect staking - Logging.instance.log("output type not detected; output: ${output}", + Logging.instance.log("output type not detected; output: $output", level: LogLevel.Info); } } @@ -2324,7 +2145,7 @@ class ParticlWallet extends CoinServiceAPI { .log("recipientsArray: $recipientsArray", level: LogLevel.Info); final foundInSenders = - allAddresses.any((element) => sendersArray.contains(element)); + allAddresses.any((element) => sendersArray.contains(element.value)); Logging.instance .log("foundInSenders: $foundInSenders", level: LogLevel.Info); @@ -2353,7 +2174,7 @@ class ParticlWallet extends CoinServiceAPI { totalInput = inputAmtSentFromWallet; int totalOutput = 0; - Logging.instance.log("txObject: ${txObject}", level: LogLevel.Info); + Logging.instance.log("txObject: $txObject", level: LogLevel.Info); for (final output in txObject["vout"] as List) { // Particl has different tx types that need to be detected and handled here @@ -2373,7 +2194,7 @@ class ParticlWallet extends CoinServiceAPI { // change address from 'sent from' to the 'sent to' address txObject["address"] = address; } - } catch (s, e) { + } catch (s) { Logging.instance.log(s.toString(), level: LogLevel.Warning); } // Logging.instance.log("output is transparent", level: LogLevel.Info); @@ -2384,15 +2205,15 @@ class ParticlWallet extends CoinServiceAPI { Logging.instance.log( "output is blinded (CT); cannot parse output values", level: LogLevel.Info); - final ct_fee = output["ct_fee"]!; - final fee_value = (Decimal.parse(ct_fee.toString()) * + final ctFee = output["ct_fee"]!; + final feeValue = (Decimal.parse(ctFee.toString()) * Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); Logging.instance.log( - "ct_fee ${ct_fee} subtracted from inputAmtSentFromWallet ${inputAmtSentFromWallet}", + "ct_fee $ctFee subtracted from inputAmtSentFromWallet $inputAmtSentFromWallet", level: LogLevel.Info); - inputAmtSentFromWallet += fee_value; + inputAmtSentFromWallet += feeValue; } else if (output.containsKey('rangeproof') as bool) { // or valueCommitment or type: anon // TODO handle RingCT tx @@ -2401,7 +2222,7 @@ class ParticlWallet extends CoinServiceAPI { level: LogLevel.Info); } else { // TODO detect staking - Logging.instance.log("output type not detected; output: ${output}", + Logging.instance.log("output type not detected; output: $output", level: LogLevel.Info); } } @@ -2428,7 +2249,7 @@ class ParticlWallet extends CoinServiceAPI { outputAmtAddressedToWallet += value; } } - } catch (s, e) { + } catch (s) { Logging.instance.log(s.toString(), level: LogLevel.Info); } } @@ -2457,112 +2278,141 @@ class ParticlWallet extends CoinServiceAPI { // create final tx map midSortedTx["txid"] = txObject["txid"]; - midSortedTx["confirmed_status"] = (txObject["confirmations"] != null) && - (txObject["confirmations"] as int >= MINIMUM_CONFIRMATIONS); - midSortedTx["confirmations"] = txObject["confirmations"] ?? 0; + midSortedTx["timestamp"] = txObject["blocktime"] ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000); - if (foundInSenders) { - midSortedTx["txType"] = "Sent"; - midSortedTx["amount"] = inputAmtSentFromWallet; - final String worthNow = - ((currentPrice * Decimal.fromInt(inputAmtSentFromWallet)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(scaleOnInfinitePrecision: 2) - .toStringAsFixed(2); - midSortedTx["worthNow"] = worthNow; - midSortedTx["worthAtBlockTimestamp"] = worthNow; - } else { - midSortedTx["txType"] = "Received"; - midSortedTx["amount"] = outputAmtAddressedToWallet; - final worthNow = - ((currentPrice * Decimal.fromInt(outputAmtAddressedToWallet)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(scaleOnInfinitePrecision: 2) - .toStringAsFixed(2); - midSortedTx["worthNow"] = worthNow; - } - midSortedTx["aliens"] = []; - midSortedTx["fees"] = fee; midSortedTx["address"] = txObject["address"]; - midSortedTx["inputSize"] = txObject["vin"].length; - midSortedTx["outputSize"] = txObject["vout"].length; midSortedTx["inputs"] = txObject["vin"]; midSortedTx["outputs"] = txObject["vout"]; final int height = txObject["height"] as int; - midSortedTx["height"] = height; - if (height >= latestTxnBlockHeight) { - latestTxnBlockHeight = height; - } + // midSortedArray.add(midSortedTx); + final tx = isar_models.Transaction(); + tx.txid = midSortedTx["txid"] as String; + tx.timestamp = midSortedTx["timestamp"] as int; - midSortedArray.add(midSortedTx); - } - - // sort by date ---- //TODO not sure if needed - // shouldn't be any issues with a null timestamp but I got one at some point? - midSortedArray - .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); - // { - // final aT = a["timestamp"]; - // final bT = b["timestamp"]; - // - // if (aT == null && bT == null) { - // return 0; - // } else if (aT == null) { - // return -1; - // } else if (bT == null) { - // return 1; - // } else { - // return bT - aT; - // } - // }); - - // buildDateTimeChunks - final Map result = {"dateTimeChunks": []}; - final dateArray = []; - - for (int i = 0; i < midSortedArray.length; i++) { - final txObject = midSortedArray[i]; - final date = extractDateFromTimestamp(txObject["timestamp"] as int); - final txTimeArray = [txObject["timestamp"], date]; - - if (dateArray.contains(txTimeArray[1])) { - result["dateTimeChunks"].forEach((dynamic chunk) { - if (extractDateFromTimestamp(chunk["timestamp"] as int) == - txTimeArray[1]) { - if (chunk["transactions"] == null) { - chunk["transactions"] = >[]; - } - chunk["transactions"].add(txObject); - } - }); + if (foundInSenders) { + tx.type = isar_models.TransactionType.outgoing; + tx.amount = inputAmtSentFromWallet; } else { - dateArray.add(txTimeArray[1]); - final chunk = { - "timestamp": txTimeArray[0], - "transactions": [txObject], - }; - result["dateTimeChunks"].add(chunk); + tx.type = isar_models.TransactionType.incoming; + tx.amount = outputAmtAddressedToWallet; } + + // TODO: other subtypes + tx.subType = isar_models.TransactionSubType.none; + + tx.fee = fee; + tx.address = midSortedTx["address"] as String; + + for (final json in midSortedTx["vin"] as List) { + bool isCoinBase = json['coinbase'] != null; + final input = isar_models.Input(); + input.txid = json['txid'] as String? ?? ""; + input.vout = json['vout'] as int? ?? -1; + input.scriptSig = json['scriptSig']?['hex'] as String?; + input.scriptSigAsm = json['scriptSig']?['asm'] as String?; + input.isCoinbase = + isCoinBase ? isCoinBase : json['is_coinbase'] as bool?; + input.sequence = json['sequence'] as int?; + input.innerRedeemScriptAsm = json['innerRedeemscriptAsm'] as String?; + tx.inputs.add(input); + } + + for (final json in midSortedTx["vout"] as List) { + final output = isar_models.Output(); + output.scriptPubKey = json['scriptPubKey']?['hex'] as String?; + output.scriptPubKeyAsm = json['scriptPubKey']?['asm'] as String?; + output.scriptPubKeyType = json['scriptPubKey']?['type'] as String?; + output.scriptPubKeyAddress = + json["scriptPubKey"]?["addresses"]?[0] as String? ?? + json['scriptPubKey']?['type'] as String? ?? + ""; + output.value = Format.decimalAmountToSatoshis( + Decimal.tryParse(json["value"].toString()) ?? Decimal.zero, + coin, + ); + tx.outputs.add(output); + } + + tx.height = height; + + tx.isCancelled = false; + tx.slateId = null; + tx.otherData = null; + + txns.add(tx); } - final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; - transactionsMap - .addAll(TransactionData.fromJson(result).getAllTransactions()); + await isar.writeTxn(() async { + await isar.transactions.putAll(txns); + }); - final txModel = TransactionData.fromMap(transactionsMap); - - await DB.instance.put( - boxName: walletId, - key: 'storedTxnDataHeight', - value: latestTxnBlockHeight); - await DB.instance.put( - boxName: walletId, key: 'latest_tx_model', value: txModel); - - return txModel; + // + // // sort by date ---- //TODO not sure if needed + // // shouldn't be any issues with a null timestamp but I got one at some point? + // midSortedArray + // .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + // // { + // // final aT = a["timestamp"]; + // // final bT = b["timestamp"]; + // // + // // if (aT == null && bT == null) { + // // return 0; + // // } else if (aT == null) { + // // return -1; + // // } else if (bT == null) { + // // return 1; + // // } else { + // // return bT - aT; + // // } + // // }); + // + // // buildDateTimeChunks + // final Map result = {"dateTimeChunks": []}; + // final dateArray = []; + // + // for (int i = 0; i < midSortedArray.length; i++) { + // final txObject = midSortedArray[i]; + // final date = extractDateFromTimestamp(txObject["timestamp"] as int); + // final txTimeArray = [txObject["timestamp"], date]; + // + // if (dateArray.contains(txTimeArray[1])) { + // result["dateTimeChunks"].forEach((dynamic chunk) { + // if (extractDateFromTimestamp(chunk["timestamp"] as int) == + // txTimeArray[1]) { + // if (chunk["transactions"] == null) { + // chunk["transactions"] = >[]; + // } + // chunk["transactions"].add(txObject); + // } + // }); + // } else { + // dateArray.add(txTimeArray[1]); + // final chunk = { + // "timestamp": txTimeArray[0], + // "transactions": [txObject], + // }; + // result["dateTimeChunks"].add(chunk); + // } + // } + // + // final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + // transactionsMap + // .addAll(TransactionData.fromJson(result).getAllTransactions()); + // + // final txModel = TransactionData.fromMap(transactionsMap); + // + // await DB.instance.put( + // boxName: walletId, + // key: 'storedTxnDataHeight', + // value: latestTxnBlockHeight); + // await DB.instance.put( + // boxName: walletId, key: 'latest_tx_model', value: txModel); + // + // return txModel; } int estimateTxFee({required int vSize, required int feeRatePerKB}) { @@ -2579,25 +2429,27 @@ class ParticlWallet extends CoinServiceAPI { String _recipientAddress, bool isSendAll, { int additionalOutputs = 0, - List? utxos, + List? utxos, }) async { Logging.instance .log("Starting coinSelection ----------", level: LogLevel.Info); - final List availableOutputs = utxos ?? outputsList; - final List spendableOutputs = []; + final List availableOutputs = utxos ?? await this.utxos; + final currentChainHeight = await chainHeight; + final List spendableOutputs = []; int spendableSatoshiValue = 0; // Build list of spendable outputs and totaling their satoshi amount for (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); @@ -2624,7 +2476,7 @@ class ParticlWallet extends CoinServiceAPI { // Possible situation right here int satoshisBeingUsed = 0; int inputsBeingConsumed = 0; - List utxoObjectsToUse = []; + List utxoObjectsToUse = []; for (var i = 0; satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; @@ -2742,7 +2594,7 @@ class ParticlWallet extends CoinServiceAPI { satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used - await _checkChangeAddressForTransactions(DerivePathType.bip84); + await _checkChangeAddressForTransactions(); final String newChangeAddress = await _getCurrentAddressForChain(1, DerivePathType.bip84); @@ -2912,7 +2764,7 @@ class ParticlWallet extends CoinServiceAPI { } Future> fetchBuildTxData( - List utxosToUse, + List utxosToUse, ) async { // return data Map results = {}; @@ -3079,7 +2931,7 @@ class ParticlWallet extends CoinServiceAPI { /// Builds and signs a transaction Future> buildTransaction({ - required List utxosToUse, + required List utxosToUse, required Map utxoSigningData, required List recipients, required List satoshiAmounts, @@ -3444,22 +3296,23 @@ class ParticlWallet extends CoinServiceAPI { @override Future 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; + } } } @@ -3490,11 +3343,12 @@ class ParticlWallet extends CoinServiceAPI { (feeRatePerKB / 1000).ceil(); } - int sweepAllEstimate(int feeRate) { + Future 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++; } @@ -3509,23 +3363,18 @@ class ParticlWallet extends CoinServiceAPI { @override Future generateNewAddress() async { try { - await _incrementAddressIndexForChain( - 0, DerivePathType.bip84); // First increment the receiving index - final newReceivingIndex = DB.instance.get( - boxName: walletId, - key: 'receivingIndexP2WPKH') as int; // Check the new receiving index + final currentReceiving = await _currentReceivingAddress; + + final newReceivingIndex = currentReceiving.derivationIndex + 1; + + // Use new index to derive a new receiving address final newReceivingAddress = await _generateAddressForChain( - 0, - newReceivingIndex, - DerivePathType - .bip84); // Use new index to derive a new receiving address - await _addToAddressesArrayForChain( - newReceivingAddress, - 0, - DerivePathType - .bip84); // Add that new receiving address to the array of receiving addresses - _currentReceivingAddress = Future(() => - newReceivingAddress); // Set the new receiving address that the service + 0, newReceivingIndex, DerivePathType.bip84); + + // Add that new receiving address + await isar.writeTxn(() async { + await isar.addresses.put(newReceivingAddress); + }); return true; } catch (e, s) {