// 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> maps = await db!.query('trade_statistics'); // Convert the list of maps to a list of PaymentMethod objects List tradeStatistics = maps.map((map) { final String tradeStatisticsJson = map['data']; return TradeStatistics3.create() ..mergeFromProto3Json(jsonDecode(tradeStatisticsJson)); }).toList(); //print("Found ${tradeStatistics.length} trade statistics entries in the database."); return tradeStatistics; } // Batch delete offers by id or isMyOffer Future deleteOffers(List? offers, {bool? isMyOffer}) async { final db = await instance.database; final batch = db!.batch(); if (offers != null && offers.isNotEmpty) { // Delete by offer id for the provided offers list for (var offer in offers) { batch.delete( 'offers', where: 'offerId = ?', whereArgs: [offer.id], ); } } else if (isMyOffer != null) { // Delete by isMyOffer flag if offers are not provided batch.delete( 'offers', where: 'isMyOffer = ?', whereArgs: [isMyOffer ? 1 : 0], // 1 for true, 0 for false ); } // Commit the batch await batch.commit(noResult: true); } // Get all payment methods Future> getAllPaymentMethods() async { final db = await instance.database; final List> maps = await db!.query('payment_methods'); // Convert the list of maps to a list of PaymentMethod objects List paymentMethods = maps.map((map) { final String paymentMethodJson = map['data']; return PaymentMethod.create() ..mergeFromProto3Json(jsonDecode(paymentMethodJson)); }).toList(); //print("Found ${paymentMethods.length} payment methods in the database."); return paymentMethods; } // Get all trades Future> getAllTrades() async { final db = await instance.database; final List> maps = await db!.query('trades'); // Convert the list of maps to a list of PaymentMethod objects List trades = maps.map((map) { final String tradesJson = map['data']; return TradeInfo.create()..mergeFromProto3Json(jsonDecode(tradesJson)); }).toList(); return trades; } // Get disputes Future> getAllDisputes() async { final db = await instance.database; final List> maps = await db!.query('disputes'); // Convert the list of maps to a list of PaymentMethod objects List disputes = maps.map((map) { final String disputesJson = map['data']; return Dispute.create()..mergeFromProto3Json(jsonDecode(disputesJson)); }).toList(); return disputes; } Future> getOffers( {String? paymentMethodId, String? direction, bool? isMyOffer}) async { final db = await instance.database; // Build the where clause dynamically based on the provided criteria final List whereClauses = []; final List whereArgs = []; if (paymentMethodId != null) { whereClauses.add('paymentMethodId = ?'); whereArgs.add(paymentMethodId); } if (direction != null) { whereClauses.add('direction = ?'); whereArgs.add(direction); } if (isMyOffer != null) { whereClauses.add('isMyOffer = ?'); whereArgs.add(isMyOffer ? 1 : 0); // Assuming isMyOffer is stored as 1 for true, 0 for false } // Combine the where clauses into a single string final whereClause = whereClauses.isNotEmpty ? whereClauses.join(' AND ') : null; // Query the database with the dynamic where clause final List> maps = await db!.query( 'offers', where: whereClause, whereArgs: whereArgs, ); // Convert the list of maps to a list of OfferInfo objects final List offers = maps.map((map) { final String offerJson = map['data']; // Assuming 'data' contains the serialized OfferInfo JSON return OfferInfo.create()..mergeFromProto3Json(jsonDecode(offerJson)); }).toList(); //print("Found ${offers.length} offers in the database."); return offers; } // Get payment method by ID Future getPaymentMethodById(String paymentMethodId) async { final db = await instance.database; final List> maps = await db!.query( 'payment_methods', where: 'paymentMethodId = ?', whereArgs: [paymentMethodId], limit: 1, ); if (maps.isNotEmpty) { // The data column contains the JSON string of the PaymentMethod final String paymentMethodJson = maps.first['data']; // Deserialize JSON back into a PaymentMethod object final paymentMethod = PaymentMethod.create() ..mergeFromProto3Json(jsonDecode(paymentMethodJson)); return paymentMethod; } throw Exception( "Could not find payment method with ID $paymentMethodId in database, local state or fetched from remote!"); } // Get trade by ID Future getTradeById(String tradeId) async { final db = await instance.database; final List> maps = await db!.query( 'trades', where: 'tradeId = ?', whereArgs: [tradeId], limit: 1, ); if (maps.isNotEmpty) { // The data column contains the JSON string of the PaymentMethod final String tradeJson = maps.first['data']; // Deserialize JSON back into a PaymentMethod object final trade = TradeInfo.create() ..mergeFromProto3Json(jsonDecode(tradeJson)); return trade; } throw Exception("Could not find trade with ID $tradeId in database..."); } // Get payment method by ID Future getDisputeByTradeId(String paymentMethodId) async { final db = await instance.database; final List> maps = await db!.query( 'payment_methods', where: 'paymentMethodId = ?', whereArgs: [paymentMethodId], limit: 1, ); if (maps.isNotEmpty) { // The data column contains the JSON string of the PaymentMethod final String paymentMethodJson = maps.first['data']; // Deserialize JSON back into a PaymentMethod object final paymentMethod = PaymentMethod.create() ..mergeFromProto3Json(jsonDecode(paymentMethodJson)); return paymentMethod; } throw Exception( "Could not find payment method with ID $paymentMethodId in database, local state or fetched from remote!"); } // Get all payment accounts Future> getAllPaymentAccounts() async { final db = await instance.database; final List> maps = await db!.query('payment_accounts'); // Convert the list of maps to a list of PaymentMethod objects List paymentAccounts = maps.map((map) { final String paymentAccountsJson = map['data']; return PaymentAccount.create() ..mergeFromProto3Json(jsonDecode(paymentAccountsJson)); }).toList(); return paymentAccounts; } // Get payment account form by payment method ID Future getPaymentAccountFormByPaymentMethodId( String paymentMethodId) async { final db = await instance.database; final List> maps = await db!.query( 'payment_account_forms', where: 'paymentMethodId = ?', whereArgs: [paymentMethodId], limit: 1, ); if (maps.isNotEmpty) { // The data column contains the JSON string of the PaymentMethod final String paymentAccountFormJson = maps.first['data']; // Deserialize JSON back into a PaymentMethod object final paymentAccountForm = PaymentAccountForm.create() ..mergeFromProto3Json(jsonDecode(paymentAccountFormJson)); return paymentAccountForm; } else { return null; } } // Get all payment methods Future?> getAllPaymentAccountForms() async { final db = await instance.database; final List> maps = await db!.query('payment_account_forms'); // Convert the list of maps to a list of PaymentMethod objects List? paymentAccountForms = maps.map((map) { final String paymentAccountFormsJson = map['data']; return PaymentAccountForm.create() ..mergeFromProto3Json(jsonDecode(paymentAccountFormsJson)); }).toList(); //print("Found ${paymentAccountForms.length} payment account forms in the database."); return paymentAccountForms; } // Insert a payment account into the database Future insertPaymentAccountForm( String paymentMethodId, PaymentAccountForm paymentAccountForm) async { final db = await instance.database; final paymentAccountFormJson = jsonEncode(paymentAccountForm.toProto3Json()); await db!.insert( 'payment_account_forms', { 'paymentAccountFormId': paymentAccountForm.id.name, 'paymentMethodId': paymentMethodId, 'data': paymentAccountFormJson, }, conflictAlgorithm: ConflictAlgorithm.replace, ); } // Check if a trade is new Future isTradeNew(String tradeId) async { final db = await instance.database; final result = await db!.query('trades', where: 'tradeId = ?', whereArgs: [tradeId]); return result.isEmpty; } // Check if a trade chat message is new Future isTradeChatMessageNew(String messageId) async { final db = await instance.database; final result = await db!.query('trade_chat_messages', where: 'messageId = ?', whereArgs: [messageId]); return result.isEmpty; } // Check if a dispute is new Future isDisputeNew(String disputeId) async { final db = await instance.database; final result = await db! .query('disputes', where: 'disputeId = ?', whereArgs: [disputeId]); return result.isEmpty; } // Check if a dispute chat message is new Future isDisputeChatMessageNew(String messageId) async { final db = await instance.database; final result = await db!.query('dispute_chat_messages', where: 'messageId = ?', whereArgs: [messageId]); return result.isEmpty; } }