// Haveno App extends the features of Haveno, supporting mobile devices and more.
// Copyright (C) 2024 Kewbit (https://kewbit.org)
// Source Code: https://git.haveno.com/haveno/haveno-app.git
//
// Author: Kewbit
// Website: https://kewbit.org
// Contact Email: me@kewbit.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
import 'package:haveno/grpc_models.dart';
import 'package:haveno/profobuf_models.dart';
import 'package:path/path.dart' as path;
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path_provider/path_provider.dart';
class DatabaseHelper {
// Private constructor
DatabaseHelper._privateConstructor();
// Single instance of DatabaseHelper
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _database;
static bool _isResetting = false;
// Initialize the sqflite ffi loader
Future _initFfiLoader() async {
sqfliteFfiInit(); // Initialize FFI loader
}
Future get database async {
if (_isResetting) {
return null;
}
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
// Initialize the database with FFI support
Future _initDatabase() async {
await _initFfiLoader(); // Initialize FFI
// Get application directory for cross-platform support
final io.Directory appDocumentsDir = await getApplicationSupportDirectory();
// Define the database path
final String databasePath =
path.join(appDocumentsDir.path, 'databases', 'haveno.db');
// Use the FFI database factory
var databaseFactory = databaseFactoryFfi;
return await databaseFactory.openDatabase(databasePath,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
await db.execute('PRAGMA foreign_keys = ON');
// Create trades table
await db.execute('''
CREATE TABLE IF NOT EXISTS trades(
tradeId TEXT PRIMARY KEY,
data TEXT
)
''');
// Create disputes table
await db.execute('''
CREATE TABLE IF NOT EXISTS disputes(
disputeId TEXT PRIMARY KEY,
tradeId TEXT UNIQUE,
data TEXT,
FOREIGN KEY(tradeId) REFERENCES trades(tradeId) ON DELETE CASCADE
)
''');
// Create trade_chat_messages table
await db.execute('''
CREATE TABLE IF NOT EXISTS trade_chat_messages(
messageId TEXT PRIMARY KEY,
tradeId TEXT,
data TEXT,
FOREIGN KEY(tradeId) REFERENCES trades(tradeId) ON DELETE CASCADE
)
''');
// Create dispute_chat_messages table
await db.execute('''
CREATE TABLE IF NOT EXISTS dispute_chat_messages(
messageId TEXT PRIMARY KEY,
disputeId TEXT,
data TEXT,
FOREIGN KEY(disputeId) REFERENCES disputes(disputeId) ON DELETE CASCADE
)
''');
// Create payment_methods table
await db.execute('''
CREATE TABLE IF NOT EXISTS payment_methods(
paymentMethodId TEXT PRIMARY KEY,
data TEXT
)
''');
// Create payment_accounts table
await db.execute('''
CREATE TABLE IF NOT EXISTS payment_accounts(
paymentAccountId TEXT PRIMARY KEY,
paymentMethodId TEXT,
accountName TEXT,
creationDate INT,
data TEXT,
FOREIGN KEY(paymentMethodId) REFERENCES payment_methods(paymentMethodId) ON DELETE CASCADE
)
''');
// Create payment_account_forms table
await db.execute('''
CREATE TABLE IF NOT EXISTS payment_account_forms(
paymentAccountFormId TEXT PRIMARY KEY,
paymentMethodId TEXT UNIQUE,
data TEXT
)
''');
// Create trade_statistics table
await db.execute('''
CREATE TABLE IF NOT EXISTS trade_statistics(
hash BLOB PRIMARY KEY,
hashcode INTEGER,
amount INTEGER,
paymentMethodId TEXT,
date INTEGER,
arbitrator TEXT,
price INTEGER,
currency TEXT,
makerDepositTxnId TEXT,
takerDepositTxnId TEXT,
extraData TEXT,
data TEXT
)
''');
await db.execute('''
CREATE TABLE IF NOT EXISTS offers(
offerId TEXT PRIMARY KEY,
paymentMethodId TEXT,
direction TEXT,
isMyOffer INTEGER,
ownerNodeAddress TEXT,
baseCurrencyCode TEXT,
counterCurrencyCode TEXT,
date INTEGER,
data TEXT,
hashcode INTEGER
)
''');
print("It's run the db init script");
// Create indexes for tradeId and disputeId
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_tradeId ON trade_chat_messages(tradeId)');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_disputeId ON dispute_chat_messages(disputeId)');
},
));
}
// Close the database
Future closeDatabase() async {
final db = await instance.database;
if (db!.isOpen) {
await db.close();
}
}
Future destroyDatabase({bool reinitialize = true}) async {
try {
final db = await instance.database;
// Set the reset flag and initialize the completer if needed
_isResetting = true;
// Close and delete the database
final databasePath = await getApplicationSupportDirectory();
final databaseFilePath = path.join(databasePath.path, 'haveno.db');
if (db!.isOpen) {
print("Closing database...");
await db.close();
print("Database closed.");
}
print("Deleting database file...");
await deleteDatabase(databaseFilePath);
print("Database file deleted.");
// Nullify the current instance of the database
_database = null;
// Delay to ensure file deletion is completed
await Future.delayed(const Duration(milliseconds: 500));
// Optionally reinitialize the database
if (reinitialize) {
print("Reinitializing the database...");
_database = await _initDatabase();
print("Database reinitialized.");
}
} catch (e) {
print("Error during database destruction: $e");
} finally {
// Mark the database as reset and complete the completer
_isResetting = false;
}
}
// Insert a trade into the database
Future insertTrade(TradeInfo trade) async {
final db = await instance.database;
final tradeJson = jsonEncode(trade.toProto3Json());
await db!.insert(
'trades',
{
'tradeId': trade.tradeId,
'data': tradeJson,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future insertTrades(List trades) async {
final db = await instance.database;
final batch = db!.batch();
for (var trade in trades) {
final tradeJson = jsonEncode(trade.toProto3Json());
batch.insert(
'trades',
{
'tradeId': trade.tradeId, // string
'data': tradeJson, // json string of proto3 object
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Commit the batch
await batch.commit(noResult: true);
}
// Insert a trade chat message into the database
Future insertTradeChatMessage(
ChatMessage message, String tradeId) async {
final db = await instance.database;
final messageJson = jsonEncode(message.toProto3Json());
await db!.insert(
'trade_chat_messages',
{
'messageId': message.uid,
'tradeId': tradeId,
'data': messageJson,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Insert a dispute into the database
Future insertDispute(Dispute dispute) async {
final db = await instance.database;
final disputeJson = jsonEncode(dispute.toProto3Json());
await db!.insert(
'disputes',
{
'disputeId': dispute.id,
'tradeId': dispute.tradeId,
'data': disputeJson,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Insert a dispute chat message into the database
Future insertDisputeChatMessage(
ChatMessage message, String disputeId) async {
final db = await instance.database;
final messageJson = jsonEncode(message.toProto3Json());
await db!.insert(
'dispute_chat_messages',
{
'messageId': message.uid,
'disputeId': disputeId,
'data': messageJson,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Insert a payment method into the database
Future insertPaymentMethod(PaymentMethod paymentMethod) async {
final db = await instance.database;
final paymentMethodJson = jsonEncode(paymentMethod.toProto3Json());
await db!.insert(
'payment_methods',
{
'paymentMethodId': paymentMethod.id,
'data': paymentMethodJson,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Batch insert payment methods
Future insertPaymentMethods(List paymentMethods) async {
final db = await instance.database;
final batch = db!.batch();
for (var paymentMethod in paymentMethods) {
final paymentMethodJson = jsonEncode(paymentMethod.toProto3Json());
batch.insert(
'payment_methods',
{
'paymentMethodId': paymentMethod.id, // string
'data': paymentMethodJson, // json string of proto3 object
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Commit the batch
await batch.commit(noResult: true);
}
Future insertOffer(OfferInfo offer) async {
final db = await instance.database;
final offerJson = jsonEncode(offer.toProto3Json());
await db!.insert(
'offers',
{
'offerId': offer.id,
'paymentMethodId': offer.paymentMethodId,
'direction': offer.direction,
'ownerNodeAddress': offer.ownerNodeAddress,
'baseCurrencyCode': offer.baseCurrencyCode,
'counterCurrencyCode': offer.counterCurrencyCode,
'isMyOffer': offer.isMyOffer ? 1 : 0,
'date': offer.date.toInt(),
'data': offerJson,
'hashcode': offer
.hashCode // should store this canse then we know if the object changed or not
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Batch insert payment accounts
Future insertOffers(List offers) async {
final db = await instance.database;
final batch = db!.batch();
for (var offer in offers) {
final offerJson = jsonEncode(offer.toProto3Json());
batch.insert(
'offers',
{
'offerId': offer.id,
'paymentMethodId': offer.paymentMethodId,
'direction': offer.direction,
'ownerNodeAddress': offer.ownerNodeAddress,
'baseCurrencyCode': offer.baseCurrencyCode,
'counterCurrencyCode': offer.counterCurrencyCode,
'isMyOffer': offer.isMyOffer ? 1 : 0,
'date': offer.date.toInt(),
'data': offerJson,
'hashcode': offer.hashCode
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Commit the batch
await batch.commit(noResult: true);
}
Future insertPaymentAccount(PaymentAccount paymentAccount) async {
final db = await instance.database;
final paymentAccountJson = jsonEncode(paymentAccount.toProto3Json());
await db!.insert(
'payment_accounts',
{
'paymentAccountId': paymentAccount.id,
'paymentMethodId': paymentAccount.paymentMethod.id, //string
'accountName': paymentAccount.accountName,
'creationDate':
paymentAccount.creationDate.toInt(), //int64 but convtered to int
'data': paymentAccountJson, //json string of proto3 object
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Batch insert payment accounts
Future insertPaymentAccounts(
List paymentAccounts) async {
final db = await instance.database;
final batch = db!.batch();
for (var paymentAccount in paymentAccounts) {
final paymentAccountJson = jsonEncode(paymentAccount.toProto3Json());
batch.insert(
'payment_accounts',
{
'paymentAccountId': paymentAccount.id,
'paymentMethodId': paymentAccount.paymentMethod.id, // string
'accountName': paymentAccount.accountName,
'creationDate':
paymentAccount.creationDate.toInt(), // int64 but converted to int
'data': paymentAccountJson, // json string of proto3 object
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Commit the batch
await batch.commit(noResult: true);
}
// Insert trade statistic
Future insertTradeStatistic(TradeStatistics3 tradeStatistic) async {
final db = await instance.database;
final tradeStatisticJson = jsonEncode(tradeStatistic.toProto3Json());
await db!.insert(
'trade_statistics',
{
'hashcode': tradeStatistic.hashCode, //int
'hash': tradeStatistic.hash, //list
'amount': tradeStatistic.amount.toInt(), //int64 but converted to int
'paymentMethodId': tradeStatistic.paymentMethod, //string
'date': tradeStatistic.date.toInt(), //int64 but convtered to int
'arbitrator': tradeStatistic.arbitrator, //string
'price': tradeStatistic.price.toInt(), //int64 but converted to int
'currency': tradeStatistic.currency, //string
'makerDepositTxnId': tradeStatistic.makerDepositTxId, //string
'takerDepositTxnId': tradeStatistic.takerDepositTxId, //string
'extraData': jsonEncode(tradeStatistic
.extraData), //Map but json encoded to stirng...
'data': tradeStatisticJson, //json string of proto3 object
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// Get trade statistic groups by a specific peroid
Future> getTradeStatistics(String? period) async {
final db = await instance.database;
final List