mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-24 20:39:21 +00:00
aba37faacb
TODO fix `pubKey`s, `amount`s, etc passed in as 0000 or String? (whereas parameter is String for example, ie null awareness/safety) Can instantiate a cached electrum client and call eg txid for info missing from the transaction blockchain model
541 lines
16 KiB
Dart
541 lines
16 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:stackwallet/exceptions/main_db/main_db_exception.dart';
|
|
import 'package:stackwallet/models/isar/models/block_explorer.dart';
|
|
import 'package:stackwallet/models/isar/models/contact_entry.dart';
|
|
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
|
import 'package:stackwallet/models/isar/ordinal.dart';
|
|
import 'package:stackwallet/models/isar/stack_theme.dart';
|
|
import 'package:stackwallet/utilities/amount/amount.dart';
|
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
|
import 'package:stackwallet/utilities/stack_file_system.dart';
|
|
import 'package:tuple/tuple.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,
|
|
],
|
|
directory: (await StackFileSystem.applicationIsarDirectory()).path,
|
|
// inspector: kDebugMode,
|
|
inspector: false,
|
|
name: "wallet_data",
|
|
maxSizeMiB: 512,
|
|
);
|
|
return true;
|
|
}
|
|
|
|
// 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 Coin coin}) {
|
|
return isar.transactionBlockExplorers
|
|
.where()
|
|
.tickerEqualTo(coin.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 {
|
|
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 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);
|
|
}
|
|
|
|
// 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
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ========== 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();
|
|
}
|
|
}
|