/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. * Generated by Cypher Stack on 2023-05-26 * */ import 'package:decimal/decimal.dart'; import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; import '../../exceptions/main_db/main_db_exception.dart'; import '../../models/isar/models/block_explorer.dart'; import '../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../models/isar/models/contact_entry.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/isar/ordinal.dart'; import '../../models/isar/stack_theme.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/extensions/extensions.dart'; import '../../utilities/stack_file_system.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/models/frost_wallet_info.dart'; import '../../wallets/isar/models/spark_coin.dart'; import '../../wallets/isar/models/token_wallet_info.dart'; import '../../wallets/isar/models/wallet_info.dart'; import '../../wallets/isar/models/wallet_info_meta.dart'; part '../queries/queries.dart'; /* * This file includes the functions that are used to interact with the main database. * To add a new function, add it in the class MainDB. */ class MainDB { MainDB._(); static MainDB? _instance; static MainDB get instance => _instance ??= MainDB._(); Isar? _isar; Isar get isar => _isar!; Future initMainDB({Isar? mock}) async { if (mock != null) { _isar = mock; return true; } if (_isar != null && isar.isOpen) return false; _isar = await Isar.open( [ TransactionSchema, TransactionNoteSchema, UTXOSchema, AddressSchema, AddressLabelSchema, EthContractSchema, TransactionBlockExplorerSchema, StackThemeSchema, ContactEntrySchema, OrdinalSchema, LelantusCoinSchema, WalletInfoSchema, TransactionV2Schema, SparkCoinSchema, WalletInfoMetaSchema, TokenWalletInfoSchema, FrostWalletInfoSchema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, inspector: false, name: "wallet_data", maxSizeMiB: 512, ); return true; } Future putWalletInfo(WalletInfo walletInfo) async { try { await isar.writeTxn(() async { await isar.walletInfo.put(walletInfo); }); } catch (e) { throw MainDBException("failed putWalletInfo()", e); } } Future updateWalletInfo(WalletInfo walletInfo) async { try { await isar.writeTxn(() async { final info = await isar.walletInfo .where() .walletIdEqualTo(walletInfo.walletId) .findFirst(); if (info == null) { throw Exception("updateWalletInfo() called with new WalletInfo." " Use putWalletInfo()"); } await isar.walletInfo.deleteByWalletId(walletInfo.walletId); await isar.walletInfo.put(walletInfo); }); } catch (e) { throw MainDBException("failed updateWalletInfo()", e); } } // contact entries List getContactEntries() { return isar.contactEntrys.where().sortByName().findAllSync(); } Future deleteContactEntry({required String id}) { try { return isar.writeTxn(() async { await isar.contactEntrys.deleteByCustomId(id); return true; }); } catch (e) { throw MainDBException("failed deleteContactEntry: $id", e); } } Future isContactEntryExists({required String id}) async { return isar.contactEntrys .where() .customIdEqualTo(id) .count() .then((value) => value > 0); } ContactEntry? getContactEntry({required String id}) { return isar.contactEntrys.where().customIdEqualTo(id).findFirstSync(); } Future putContactEntry({required ContactEntry contactEntry}) async { try { return await isar.writeTxn(() async { await isar.contactEntrys.put(contactEntry); return true; }); } catch (e) { throw MainDBException("failed putContactEntry: $contactEntry", e); } } // tx block explorers TransactionBlockExplorer? getTransactionBlockExplorer({ required CryptoCurrency cryptoCurrency, }) { return isar.transactionBlockExplorers .where() .tickerEqualTo(cryptoCurrency.ticker) .findFirstSync(); } Future putTransactionBlockExplorer( TransactionBlockExplorer explorer, ) async { try { return await isar.writeTxn(() async { return await isar.transactionBlockExplorers.put(explorer); }); } catch (e) { throw MainDBException("failed putTransactionBlockExplorer: $explorer", e); } } // addresses QueryBuilder getAddresses( String walletId, ) => isar.addresses.where().walletIdEqualTo(walletId); Future putAddress(Address address) async { try { return await isar.writeTxn(() async { return await isar.addresses.put(address); }); } catch (e) { throw MainDBException("failed putAddress: $address", e); } } Future> putAddresses(List
addresses) async { try { return await isar.writeTxn(() async { return await isar.addresses.putAll(addresses); }); } catch (e) { throw MainDBException("failed putAddresses: $addresses", e); } } Future> updateOrPutAddresses(List
addresses) async { try { final List ids = []; await isar.writeTxn(() async { for (final address in addresses) { final storedAddress = await isar.addresses .getByValueWalletId(address.value, address.walletId); int id; if (storedAddress == null) { id = await isar.addresses.put(address); } else { address.id = storedAddress.id; await storedAddress.transactions.load(); final txns = storedAddress.transactions.toList(); await isar.addresses.delete(storedAddress.id); id = await isar.addresses.put(address); address.transactions.addAll(txns); await address.transactions.save(); } ids.add(id); } }); return ids; } catch (e) { throw MainDBException("failed updateOrPutAddresses: $addresses", e); } } Future getAddress(String walletId, String address) async { return isar.addresses.getByValueWalletId(address, walletId); } Future updateAddress(Address oldAddress, Address newAddress) async { try { return await isar.writeTxn(() async { newAddress.id = oldAddress.id; await oldAddress.transactions.load(); final txns = oldAddress.transactions.toList(); await isar.addresses.delete(oldAddress.id); final id = await isar.addresses.put(newAddress); newAddress.transactions.addAll(txns); await newAddress.transactions.save(); return id; }); } catch (e) { throw MainDBException( "failed updateAddress: from=$oldAddress to=$newAddress", e, ); } } // transactions QueryBuilder getTransactions( String walletId, ) => isar.transactions.where().walletIdEqualTo(walletId); Future putTransaction(Transaction transaction) async { try { return await isar.writeTxn(() async { return await isar.transactions.put(transaction); }); } catch (e) { throw MainDBException("failed putTransaction: $transaction", e); } } Future> putTransactions(List transactions) async { try { return await isar.writeTxn(() async { return await isar.transactions.putAll(transactions); }); } catch (e) { throw MainDBException("failed putTransactions: $transactions", e); } } Future getTransaction(String walletId, String txid) async { return isar.transactions.getByTxidWalletId(txid, walletId); } Stream watchTransaction({ required Id id, bool fireImmediately = false, }) { return isar.transactions.watchObject(id, fireImmediately: fireImmediately); } // utxos QueryBuilder getUTXOs(String walletId) => isar.utxos.where().walletIdEqualTo(walletId); QueryBuilder getUTXOsByAddress( String walletId, String address, ) => isar.utxos .where() .walletIdEqualTo(walletId) .filter() .addressEqualTo(address); Future putUTXO(UTXO utxo) => isar.writeTxn(() async { await isar.utxos.put(utxo); }); Future putUTXOs(List utxos) => isar.writeTxn(() async { await isar.utxos.putAll(utxos); }); Future updateUTXOs(String walletId, List utxos) async { bool newUTXO = false; await isar.writeTxn(() async { final set = utxos.toSet(); for (final utxo in utxos) { // check if utxo exists in db and update accordingly final storedUtxo = await isar.utxos .where() .txidWalletIdVoutEqualTo(utxo.txid, utxo.walletId, utxo.vout) .findFirst(); if (storedUtxo != null) { // update set.remove(utxo); set.add( storedUtxo.copyWith( value: utxo.value, address: utxo.address, blockTime: utxo.blockTime, blockHeight: utxo.blockHeight, blockHash: utxo.blockHash, ), ); } else { newUTXO = true; } } await isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); await isar.utxos.putAll(set.toList()); }); return newUTXO; } Stream watchUTXO({ required Id id, bool fireImmediately = false, }) { return isar.utxos.watchObject(id, fireImmediately: fireImmediately); } // transaction notes QueryBuilder getTransactionNotes(String walletId) => isar.transactionNotes.where().walletIdEqualTo(walletId); Future putTransactionNote(TransactionNote transactionNote) => isar.writeTxn(() async { await isar.transactionNotes.put(transactionNote); }); Future putTransactionNotes(List transactionNotes) => isar.writeTxn(() async { await isar.transactionNotes.putAll(transactionNotes); }); Future getTransactionNote( String walletId, String txid, ) async { return isar.transactionNotes.getByTxidWalletId( txid, walletId, ); } Stream watchTransactionNote({ required Id id, bool fireImmediately = false, }) { return isar.transactionNotes .watchObject(id, fireImmediately: fireImmediately); } // address labels QueryBuilder getAddressLabels( String walletId, ) => isar.addressLabels.where().walletIdEqualTo(walletId); Future putAddressLabel(AddressLabel addressLabel) => isar.writeTxn(() async { return await isar.addressLabels.put(addressLabel); }); int putAddressLabelSync(AddressLabel addressLabel) => isar.writeTxnSync(() { return isar.addressLabels.putSync(addressLabel); }); Future putAddressLabels(List addressLabels) => isar.writeTxn(() async { await isar.addressLabels.putAll(addressLabels); }); Future getAddressLabel( String walletId, String addressString, ) async { return isar.addressLabels.getByAddressStringWalletId( addressString, walletId, ); } AddressLabel? getAddressLabelSync(String walletId, String addressString) { return isar.addressLabels.getByAddressStringWalletIdSync( addressString, walletId, ); } Stream watchAddressLabel({ required Id id, bool fireImmediately = false, }) { return isar.addressLabels.watchObject(id, fireImmediately: fireImmediately); } Future updateAddressLabel(AddressLabel addressLabel) async { try { return await isar.writeTxn(() async { return await isar.addressLabels.put(addressLabel); }); } catch (e) { throw MainDBException("failed updateAddressLabel", e); } } // Future deleteWalletBlockchainData(String walletId) async { final transactionCount = await getTransactions(walletId).count(); final transactionCountV2 = await isar.transactionV2s.where().walletIdEqualTo(walletId).count(); final addressCount = await getAddresses(walletId).count(); final utxoCount = await getUTXOs(walletId).count(); // final lelantusCoinCount = // await isar.lelantusCoins.where().walletIdEqualTo(walletId).count(); await isar.writeTxn(() async { const paginateLimit = 50; // transactions for (int i = 0; i < transactionCount; i += paginateLimit) { final txnIds = await getTransactions(walletId) .offset(i) .limit(paginateLimit) .idProperty() .findAll(); await isar.transactions.deleteAll(txnIds); } // transactions V2 for (int i = 0; i < transactionCountV2; i += paginateLimit) { final txnIds = await isar.transactionV2s .where() .walletIdEqualTo(walletId) .offset(i) .limit(paginateLimit) .idProperty() .findAll(); await isar.transactionV2s.deleteAll(txnIds); } // addresses for (int i = 0; i < addressCount; i += paginateLimit) { final addressIds = await getAddresses(walletId) .offset(i) .limit(paginateLimit) .idProperty() .findAll(); await isar.addresses.deleteAll(addressIds); } // utxos for (int i = 0; i < utxoCount; i += paginateLimit) { final utxoIds = await getUTXOs(walletId) .offset(i) .limit(paginateLimit) .idProperty() .findAll(); await isar.utxos.deleteAll(utxoIds); } // lelantusCoins await isar.lelantusCoins.where().walletIdEqualTo(walletId).deleteAll(); // for (int i = 0; i < lelantusCoinCount; i += paginateLimit) { // final lelantusCoinIds = await isar.lelantusCoins // .where() // .walletIdEqualTo(walletId) // .offset(i) // .limit(paginateLimit) // .idProperty() // .findAll(); // await isar.lelantusCoins.deleteAll(lelantusCoinIds); // } // spark coins await isar.sparkCoins .where() .walletIdEqualToAnyLTagHash(walletId) .deleteAll(); }); } Future deleteAddressLabels(String walletId) async { final addressLabelCount = await getAddressLabels(walletId).count(); await isar.writeTxn(() async { const paginateLimit = 50; for (int i = 0; i < addressLabelCount; i += paginateLimit) { final labelIds = await getAddressLabels(walletId) .offset(i) .limit(paginateLimit) .idProperty() .findAll(); await isar.addressLabels.deleteAll(labelIds); } }); } Future deleteTransactionNotes(String walletId) async { final noteCount = await getTransactionNotes(walletId).count(); await isar.writeTxn(() async { const paginateLimit = 50; for (int i = 0; i < noteCount; i += paginateLimit) { final labelIds = await getTransactionNotes(walletId) .offset(i) .limit(paginateLimit) .idProperty() .findAll(); await isar.transactionNotes.deleteAll(labelIds); } }); } Future addNewTransactionData( List> transactionsData, String walletId, ) async { try { await isar.writeTxn(() async { for (final data in transactionsData) { final tx = data.item1; final potentiallyUnconfirmedTx = await getTransaction( walletId, tx.txid, ); if (potentiallyUnconfirmedTx != null) { // update use id to replace tx tx.id = potentiallyUnconfirmedTx.id; await isar.transactions.delete(potentiallyUnconfirmedTx.id); } // save transaction await isar.transactions.put(tx); if (data.item2 != null) { final address = await getAddress(walletId, data.item2!.value); // check if address exists in db and add if it does not if (address == null) { await isar.addresses.put(data.item2!); } // link and save address tx.address.value = address ?? data.item2!; await tx.address.save(); } } }); } catch (e) { throw MainDBException("failed addNewTransactionData", e); } } Future> updateOrPutTransactionV2s( List transactions, ) async { try { final List ids = []; await isar.writeTxn(() async { for (final tx in transactions) { final storedTx = await isar.transactionV2s .where() .txidWalletIdEqualTo(tx.txid, tx.walletId) .findFirst(); Id id; if (storedTx == null) { id = await isar.transactionV2s.put(tx); } else { tx.id = storedTx.id; await isar.transactionV2s.delete(storedTx.id); id = await isar.transactionV2s.put(tx); } ids.add(id); } }); return ids; } catch (e) { throw MainDBException( "failed updateOrPutTransactionV2s: $transactions", e, ); } } // ========== Ethereum ======================================================= // eth contracts QueryBuilder getEthContracts() => isar.ethContracts.where(); Future getEthContract(String contractAddress) => isar.ethContracts.where().addressEqualTo(contractAddress).findFirst(); EthContract? getEthContractSync(String contractAddress) => isar.ethContracts.where().addressEqualTo(contractAddress).findFirstSync(); Future putEthContract(EthContract contract) => isar.writeTxn(() async { return await isar.ethContracts.put(contract); }); Future putEthContracts(List contracts) => isar.writeTxn(() async { await isar.ethContracts.putAll(contracts); }); // ========== Lelantus ======================================================= Future getHighestUsedMintIndex({required String walletId}) async { return await isar.lelantusCoins .where() .walletIdEqualTo(walletId) .sortByMintIndexDesc() .mintIndexProperty() .findFirst(); } }