stack_wallet/lib/db/isar/main_db.dart
2024-05-27 18:01:41 -06:00

651 lines
19 KiB
Dart

/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:decimal/decimal.dart';
import 'package:flutter_native_splash/cli_commands.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/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<bool> 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<void> putWalletInfo(WalletInfo walletInfo) async {
try {
await isar.writeTxn(() async {
await isar.walletInfo.put(walletInfo);
});
} catch (e) {
throw MainDBException("failed putWalletInfo()", e);
}
}
Future<void> 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<ContactEntry> getContactEntries() {
return isar.contactEntrys.where().sortByName().findAllSync();
}
Future<bool> 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<bool> 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<bool> 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<int> 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<Address, Address, QAfterWhereClause> getAddresses(
String walletId,
) =>
isar.addresses.where().walletIdEqualTo(walletId);
Future<int> 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<List<int>> putAddresses(List<Address> addresses) async {
try {
return await isar.writeTxn(() async {
return await isar.addresses.putAll(addresses);
});
} catch (e) {
throw MainDBException("failed putAddresses: $addresses", e);
}
}
Future<List<int>> updateOrPutAddresses(List<Address> addresses) async {
try {
final List<int> 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<Address?> getAddress(String walletId, String address) async {
return isar.addresses.getByValueWalletId(address, walletId);
}
Future<int> 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<Transaction, Transaction, QAfterWhereClause> getTransactions(
String walletId,
) =>
isar.transactions.where().walletIdEqualTo(walletId);
Future<int> 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<List<int>> putTransactions(List<Transaction> transactions) async {
try {
return await isar.writeTxn(() async {
return await isar.transactions.putAll(transactions);
});
} catch (e) {
throw MainDBException("failed putTransactions: $transactions", e);
}
}
Future<Transaction?> getTransaction(String walletId, String txid) async {
return isar.transactions.getByTxidWalletId(txid, walletId);
}
Stream<Transaction?> watchTransaction({
required Id id,
bool fireImmediately = false,
}) {
return isar.transactions.watchObject(id, fireImmediately: fireImmediately);
}
// utxos
QueryBuilder<UTXO, UTXO, QAfterWhereClause> getUTXOs(String walletId) =>
isar.utxos.where().walletIdEqualTo(walletId);
QueryBuilder<UTXO, UTXO, QAfterFilterCondition> getUTXOsByAddress(
String walletId,
String address,
) =>
isar.utxos
.where()
.walletIdEqualTo(walletId)
.filter()
.addressEqualTo(address);
Future<void> putUTXO(UTXO utxo) => isar.writeTxn(() async {
await isar.utxos.put(utxo);
});
Future<void> putUTXOs(List<UTXO> utxos) => isar.writeTxn(() async {
await isar.utxos.putAll(utxos);
});
Future<bool> updateUTXOs(String walletId, List<UTXO> 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<UTXO?> watchUTXO({
required Id id,
bool fireImmediately = false,
}) {
return isar.utxos.watchObject(id, fireImmediately: fireImmediately);
}
// transaction notes
QueryBuilder<TransactionNote, TransactionNote, QAfterWhereClause>
getTransactionNotes(String walletId) =>
isar.transactionNotes.where().walletIdEqualTo(walletId);
Future<void> putTransactionNote(TransactionNote transactionNote) =>
isar.writeTxn(() async {
await isar.transactionNotes.put(transactionNote);
});
Future<void> putTransactionNotes(List<TransactionNote> transactionNotes) =>
isar.writeTxn(() async {
await isar.transactionNotes.putAll(transactionNotes);
});
Future<TransactionNote?> getTransactionNote(
String walletId,
String txid,
) async {
return isar.transactionNotes.getByTxidWalletId(
txid,
walletId,
);
}
Stream<TransactionNote?> watchTransactionNote({
required Id id,
bool fireImmediately = false,
}) {
return isar.transactionNotes
.watchObject(id, fireImmediately: fireImmediately);
}
// address labels
QueryBuilder<AddressLabel, AddressLabel, QAfterWhereClause> getAddressLabels(
String walletId,
) =>
isar.addressLabels.where().walletIdEqualTo(walletId);
Future<int> putAddressLabel(AddressLabel addressLabel) =>
isar.writeTxn(() async {
return await isar.addressLabels.put(addressLabel);
});
int putAddressLabelSync(AddressLabel addressLabel) => isar.writeTxnSync(() {
return isar.addressLabels.putSync(addressLabel);
});
Future<void> putAddressLabels(List<AddressLabel> addressLabels) =>
isar.writeTxn(() async {
await isar.addressLabels.putAll(addressLabels);
});
Future<AddressLabel?> 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<AddressLabel?> watchAddressLabel({
required Id id,
bool fireImmediately = false,
}) {
return isar.addressLabels.watchObject(id, fireImmediately: fireImmediately);
}
Future<int> updateAddressLabel(AddressLabel addressLabel) async {
try {
return await isar.writeTxn(() async {
return await isar.addressLabels.put(addressLabel);
});
} catch (e) {
throw MainDBException("failed updateAddressLabel", e);
}
}
//
Future<void> 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<void> 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<void> 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<void> addNewTransactionData(
List<Tuple2<Transaction, Address?>> 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<List<int>> updateOrPutTransactionV2s(
List<TransactionV2> transactions,
) async {
try {
final List<int> 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<EthContract, EthContract, QWhere> getEthContracts() =>
isar.ethContracts.where();
Future<EthContract?> getEthContract(String contractAddress) =>
isar.ethContracts.where().addressEqualTo(contractAddress).findFirst();
EthContract? getEthContractSync(String contractAddress) =>
isar.ethContracts.where().addressEqualTo(contractAddress).findFirstSync();
Future<int> putEthContract(EthContract contract) => isar.writeTxn(() async {
return await isar.ethContracts.put(contract);
});
Future<void> putEthContracts(List<EthContract> contracts) =>
isar.writeTxn(() async {
await isar.ethContracts.putAll(contracts);
});
// ========== Lelantus =======================================================
Future<int?> getHighestUsedMintIndex({required String walletId}) async {
return await isar.lelantusCoins
.where()
.walletIdEqualTo(walletId)
.sortByMintIndexDesc()
.mintIndexProperty()
.findFirst();
}
}