mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 11:18:24 +00:00
- Add deleteIncomingPending action to TransactionsStore - Add getIncomingPendingCount and deleteIncomingPending to sqlite - Update TranHistoryScreen with UI for deleting pending incoming - Add i18n translations for the new feature Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1160 lines
28 KiB
TypeScript
1160 lines
28 KiB
TypeScript
import {
|
|
QuickSQLiteConnection,
|
|
open,
|
|
SQLBatchTuple,
|
|
} from 'react-native-quick-sqlite'
|
|
import {Proof} from '../models/Proof'
|
|
import {
|
|
Transaction,
|
|
TransactionStatus,
|
|
} from '../models/Transaction'
|
|
import AppError, {Err} from '../utils/AppError'
|
|
import {log} from './logService'
|
|
import {ProofRecord} from '../models/Proof'
|
|
import { isDate } from 'date-fns'
|
|
import { isAlive } from 'mobx-state-tree'
|
|
|
|
// Helper functions to normalize transaction records with Date objects
|
|
const normalizeTransactionRecord = function (r: any) {
|
|
if (r.createdAt) r.createdAt = new Date(r.createdAt);
|
|
if (r.expiresAt) r.expiresAt = r.expiresAt ? new Date(r.expiresAt) : null;
|
|
return r as Transaction;
|
|
}
|
|
|
|
const normalizeTransactionRows = function(rows: any) {
|
|
return rows?._array.map(normalizeTransactionRecord) as Transaction[];
|
|
}
|
|
|
|
let _db: QuickSQLiteConnection
|
|
|
|
const _dbVersion = 24 // Update this if db changes require migrations
|
|
|
|
const getInstance = function () {
|
|
if (!_db) {
|
|
// 1. creates database
|
|
_db = _createDatabaseInstance() as QuickSQLiteConnection
|
|
|
|
// 2. Runs possible migrations and sets version
|
|
_createOrUpdateSchema(_db)
|
|
}
|
|
|
|
return _db
|
|
}
|
|
|
|
const _createDatabaseInstance = function () {
|
|
try {
|
|
const instance = open({name: 'minibits.db'})
|
|
return instance as QuickSQLiteConnection
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not create or open database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
const _createOrUpdateSchema = function (db: QuickSQLiteConnection) {
|
|
const creationQueries = [
|
|
[
|
|
`CREATE TABLE IF NOT EXISTS transactions (
|
|
id INTEGER PRIMARY KEY NOT NULL,
|
|
paymentId TEXT,
|
|
type TEXT,
|
|
amount INTEGER,
|
|
unit TEXT,
|
|
fee INTEGER,
|
|
data TEXT,
|
|
keysetId TEXT,
|
|
sentFrom TEXT,
|
|
sentTo TEXT,
|
|
profile TEXT,
|
|
memo TEXT,
|
|
mint TEXT,
|
|
quote TEXT,
|
|
paymentRequest TEXT,
|
|
zapRequest TEXT,
|
|
inputToken TEXT,
|
|
outputToken TEXT,
|
|
proof TEXT,
|
|
balanceAfter INTEGER,
|
|
noteToSelf TEXT,
|
|
tags TEXT,
|
|
status TEXT,
|
|
expiresAt TEXT,
|
|
createdAt TEXT
|
|
)`,
|
|
],
|
|
[
|
|
`CREATE TABLE IF NOT EXISTS proofs (
|
|
id TEXT NOT NULL,
|
|
amount INTEGER NOT NULL,
|
|
secret TEXT PRIMARY KEY NOT NULL,
|
|
C TEXT NOT NULL,
|
|
dleq_r TEXT,
|
|
dleq_s TEXT,
|
|
dleq_e TEXT,
|
|
unit TEXT,
|
|
tId INTEGER,
|
|
mintUrl TEXT,
|
|
isPending BOOLEAN,
|
|
isSpent BOOLEAN,
|
|
updatedAt TEXT
|
|
)`,
|
|
],
|
|
[
|
|
`CREATE TABLE IF NOT EXISTS dbversion (
|
|
id INTEGER PRIMARY KEY NOT NULL,
|
|
version INTEGER,
|
|
createdAt TEXT
|
|
)`,
|
|
],
|
|
] as SQLBatchTuple[]
|
|
|
|
try {
|
|
const {rowsAffected} = db.executeBatch(creationQueries)
|
|
|
|
if (rowsAffected && rowsAffected > 0) {
|
|
log.info('[_createOrUpdateSchema] New database schema created')
|
|
}
|
|
|
|
const {version} = getDatabaseVersion(db)
|
|
log.info('[_createOrUpdateSchema]', `Device database version: ${version}`)
|
|
|
|
// Trigger migrations if there is versions mismatch
|
|
if (version < _dbVersion) {
|
|
_runMigrations(db)
|
|
}
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not create or update database schema',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Run database migrations in case on device version of schema is not yet set or outdated
|
|
|
|
const _runMigrations = function (db: QuickSQLiteConnection) {
|
|
const now = new Date()
|
|
const {version} = getDatabaseVersion(db)
|
|
|
|
let currentVersion = version
|
|
let migrationQueries: SQLBatchTuple[] = []
|
|
|
|
// Database migrations sequence based on local version numbers
|
|
if (currentVersion < 19) {
|
|
migrationQueries.push([
|
|
`DROP TABLE usersettings`,
|
|
])
|
|
|
|
log.info(`Prepared database migrations from ${currentVersion} -> 19`)
|
|
}
|
|
|
|
if (currentVersion < 20) {
|
|
migrationQueries.push([
|
|
`ALTER TABLE transactions
|
|
ADD COLUMN paymentId TEXT`,
|
|
])
|
|
|
|
log.info(`Prepared database migrations from ${currentVersion} -> 20`)
|
|
}
|
|
|
|
if (currentVersion < 21) {
|
|
migrationQueries.push([
|
|
`ALTER TABLE transactions
|
|
ADD COLUMN quote TEXT`,
|
|
])
|
|
|
|
log.info(`Prepared database migrations from ${currentVersion} -> 21`)
|
|
}
|
|
|
|
if (currentVersion < 22) {
|
|
migrationQueries.push([
|
|
`ALTER TABLE transactions
|
|
ADD COLUMN paymentRequest TEXT`
|
|
])
|
|
|
|
migrationQueries.push([
|
|
`ALTER TABLE transactions
|
|
ADD COLUMN expiresAt TEXT`
|
|
])
|
|
|
|
log.info(`Prepared database migrations from ${currentVersion} -> 22`)
|
|
}
|
|
|
|
if (currentVersion < 23) {
|
|
migrationQueries.push([
|
|
`ALTER TABLE proofs
|
|
ADD COLUMN dleq_r TEXT`
|
|
])
|
|
|
|
migrationQueries.push([
|
|
`ALTER TABLE proofs
|
|
ADD COLUMN dleq_s TEXT`
|
|
])
|
|
|
|
migrationQueries.push([
|
|
`ALTER TABLE proofs
|
|
ADD COLUMN dleq_e TEXT`
|
|
])
|
|
|
|
log.info(`Prepared database migrations from ${currentVersion} -> 23`)
|
|
}
|
|
|
|
if (currentVersion < 24) {
|
|
migrationQueries.push([
|
|
`ALTER TABLE transactions
|
|
ADD COLUMN keysetId TEXT`
|
|
])
|
|
log.info(`Prepared database migrations from ${currentVersion} -> 24`)
|
|
}
|
|
|
|
// Update db version as a part of migration sqls
|
|
migrationQueries.push([
|
|
`INSERT OR REPLACE INTO dbversion (id, version, createdAt)
|
|
VALUES (?, ?, ?)`,
|
|
[1, _dbVersion, now.toISOString()],
|
|
])
|
|
|
|
try {
|
|
const {rowsAffected} = db.executeBatch(migrationQueries)
|
|
|
|
if (rowsAffected && rowsAffected > 0) {
|
|
log.info(`Completed database migrations to version ${_dbVersion}`)
|
|
}
|
|
} catch (e: any) {
|
|
// silent
|
|
log.info(
|
|
Err.DATABASE_ERROR,
|
|
'Database migrations error: ' + JSON.stringify(e),
|
|
)
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Exported functions
|
|
*/
|
|
|
|
const cleanAll = function () {
|
|
const dropQueries = [
|
|
['DROP TABLE transactions'],
|
|
['DROP TABLE proofs'],
|
|
['DROP TABLE dbversion'],
|
|
] as SQLBatchTuple[]
|
|
|
|
try {
|
|
const db = getInstance()
|
|
const {rowsAffected} = db.executeBatch(dropQueries)
|
|
|
|
if (rowsAffected && rowsAffected > 0) {
|
|
log.info('[cleanAll]', 'Database tables were deleted')
|
|
}
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not delete database schema',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const getDatabaseVersion = function (db: QuickSQLiteConnection): {version: number} {
|
|
try {
|
|
const query = `
|
|
SELECT version FROM dbVersion
|
|
`
|
|
|
|
const {rows} = db.execute(query)
|
|
|
|
if (!rows?.item(0)) {
|
|
// On first run, insert current version record
|
|
const now = new Date()
|
|
const insertQuery = `
|
|
INSERT OR REPLACE INTO dbversion (id, version, createdAt)
|
|
VALUES (?, ?, ?)
|
|
`
|
|
const params = [1, _dbVersion, now.toISOString()]
|
|
db.execute(insertQuery, params)
|
|
|
|
return {version: _dbVersion}
|
|
}
|
|
|
|
return rows?.item(0)
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not get database version',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
/*
|
|
* Transactions
|
|
*/
|
|
|
|
|
|
const updateTransaction = function (id: number, fields: Partial<Transaction>): Transaction {
|
|
|
|
const allowedColumns = ['amount','fee','unit','data', 'keysetId', 'sentFrom','sentTo','profile','memo','paymentId','quote','paymentRequest','zapRequest','inputToken','outputToken','proof','balanceAfter','noteToSelf','tags','status','expiresAt'];
|
|
|
|
try {
|
|
// Filter keys against allowed columns
|
|
const validKeys = Object.keys(fields).filter(key => allowedColumns.includes(key))
|
|
|
|
if (validKeys.length === 0) {
|
|
// No valid keys to update, return existing transaction
|
|
return getTransactionById(id)
|
|
}
|
|
|
|
// Build SET clauses and parameters
|
|
const setClauses = validKeys.map(key => `${key} = ?`).join(', ')
|
|
const params = validKeys.map(key => {
|
|
const value = fields[key as keyof Transaction]
|
|
if (key === 'expiresAt' && value instanceof Date) {
|
|
return value.toISOString()
|
|
}
|
|
return value;
|
|
})
|
|
params.push(id) // Add id at the end for WHERE clause
|
|
|
|
const query = `
|
|
UPDATE transactions
|
|
SET ${setClauses}
|
|
WHERE id = ?
|
|
`
|
|
|
|
const db = getInstance()
|
|
const result = db.execute(query, params)
|
|
|
|
const updated = getTransactionById(id) // already normalized
|
|
|
|
log.trace('[updateTransaction] Transaction updated in the database', {id: updated.id, status: updated.status})
|
|
|
|
return updated as Transaction
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not update transaction in database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const getTransactionsAsync = async function (limit: number, offset: number, onlyPending: boolean = false) {
|
|
let query: string = ''
|
|
try {
|
|
query = `
|
|
SELECT *
|
|
FROM transactions
|
|
ORDER BY id DESC
|
|
LIMIT ? OFFSET ?
|
|
`
|
|
|
|
if(onlyPending) {
|
|
query = `
|
|
SELECT *
|
|
FROM transactions
|
|
WHERE status = 'PENDING'
|
|
ORDER BY id DESC
|
|
LIMIT ? OFFSET ?
|
|
`
|
|
}
|
|
|
|
const params = [limit, offset]
|
|
|
|
// log.trace(query, params)
|
|
|
|
const db = getInstance()
|
|
const {rows} = await db.executeAsync(query, params)
|
|
|
|
log.trace(`[getTransactionsAsync], Returned ${rows?.length} rows`)
|
|
|
|
return normalizeTransactionRows(rows)
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Transactions could not be retrieved from the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
const getPendingTopups = function () {
|
|
try {
|
|
const query = `
|
|
SELECT *
|
|
FROM transactions
|
|
WHERE status = 'PENDING'
|
|
AND type = 'TOPUP'
|
|
ORDER BY id DESC
|
|
`
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query)
|
|
|
|
log.trace(`[getPendingTopups], Returned ${rows?.length} rows`)
|
|
|
|
return normalizeTransactionRows(rows)
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Transactions could not be retrieved from the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const getPendingTransfers = function () {
|
|
try {
|
|
const query = `
|
|
SELECT *
|
|
FROM transactions
|
|
WHERE status = 'PENDING'
|
|
AND type = 'TRANSFER'
|
|
ORDER BY id DESC
|
|
`
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query)
|
|
|
|
log.trace(`[getPendingTransfers], Returned ${rows?.length} rows`)
|
|
|
|
return normalizeTransactionRows(rows)
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Transactions could not be retrieved from the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const getPendingTopupsCount = function () {
|
|
let query: string = ''
|
|
try {
|
|
query = `
|
|
SELECT COUNT(*)
|
|
AS total
|
|
FROM transactions
|
|
WHERE status = 'PENDING'
|
|
AND type = 'TOPUP'
|
|
`
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query)
|
|
|
|
log.trace(`[getPendingTopupsCount], Returned ${rows?.item(0)}`)
|
|
|
|
return rows?.item(0)['total'] as number
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Transactions could not be retrieved from the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const getPendingTransfersCount = function () {
|
|
let query: string = ''
|
|
try {
|
|
query = `
|
|
SELECT COUNT(*)
|
|
AS total
|
|
FROM transactions
|
|
WHERE status = 'PENDING'
|
|
AND type = 'TRANSFER'
|
|
`
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query)
|
|
|
|
log.trace(`[getPendingTransfersCount], Returned ${rows?.item(0)}`)
|
|
|
|
return rows?.item(0)['total'] as number
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Transactions could not be retrieved from the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const getTransactionsCount = function (status?: TransactionStatus) {
|
|
let query: string
|
|
let params: any[] = []
|
|
|
|
try {
|
|
if (status) {
|
|
// Query to get the count for a specific status along with the total
|
|
query = `
|
|
WITH total_count AS (
|
|
SELECT COUNT(*) AS total FROM transactions
|
|
)
|
|
SELECT status, COUNT(*) AS count, (SELECT total FROM total_count) AS total
|
|
FROM transactions
|
|
WHERE status = ?
|
|
GROUP BY status
|
|
`
|
|
params = [status]
|
|
} else {
|
|
// Query to get the count per status along with the total
|
|
query = `
|
|
WITH total_count AS (
|
|
SELECT COUNT(*) AS total FROM transactions
|
|
)
|
|
SELECT status, COUNT(*) AS count, (SELECT total FROM total_count) AS total
|
|
FROM transactions
|
|
GROUP BY status
|
|
`
|
|
}
|
|
|
|
const db = getInstance()
|
|
const { rows } = db.execute(query, params)
|
|
|
|
// Convert rows to an object with status counts and a total count
|
|
if(rows) {
|
|
const counts: Record<string, number> = { total: 0 }
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const row = rows.item(i)
|
|
counts[row.status] = row.count
|
|
counts.total = row.total
|
|
}
|
|
return counts
|
|
} else {
|
|
return {total: 0}
|
|
}
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(Err.DATABASE_ERROR, 'Transaction count error', e.message)
|
|
}
|
|
}
|
|
|
|
|
|
const getRecentTransactionsByUnitAsync = async (countRecent: number) => {
|
|
try {
|
|
const query = `
|
|
SELECT *
|
|
FROM (
|
|
SELECT *,
|
|
ROW_NUMBER() OVER (PARTITION BY unit ORDER BY createdAt DESC) as row_num
|
|
FROM transactions
|
|
)
|
|
WHERE row_num <= ?
|
|
ORDER BY unit, createdAt DESC
|
|
`
|
|
|
|
const params = [countRecent]
|
|
const db = getInstance()
|
|
const { rows } = await db.executeAsync(query, params)
|
|
|
|
return normalizeTransactionRows(rows)
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(Err.DATABASE_ERROR, 'Error retrieving last 3 transactions by unit', e.message)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const getPendingAmount = function () {
|
|
try {
|
|
const query = `
|
|
SELECT
|
|
SUM(amount)
|
|
FROM transactions
|
|
WHERE status = ?
|
|
`
|
|
const params = [TransactionStatus.PENDING]
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query, params)
|
|
|
|
return rows?.item(0)['SUM(amount)']
|
|
} catch (e: any) {
|
|
throw new AppError(Err.DATABASE_ERROR, 'Transaction not found', e.message)
|
|
}
|
|
}
|
|
|
|
|
|
const getTransactionById = function (id: number) {
|
|
try {
|
|
const query = `
|
|
SELECT * FROM transactions WHERE id = ?
|
|
`
|
|
|
|
const params = [id]
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query, params)
|
|
|
|
return normalizeTransactionRecord(rows?.item(0))
|
|
} catch (e: any) {
|
|
throw new AppError(Err.DATABASE_ERROR, 'Transaction not found', e.message)
|
|
}
|
|
}
|
|
|
|
|
|
const getLastTransactionBy = function (
|
|
criteria: { paymentId?: string; quote?: string; paymentRequest?: string }
|
|
): Transaction {
|
|
try {
|
|
// === 1. Validate exactly one search criterion ===
|
|
const provided = Object.values(criteria).filter(v => v != null)
|
|
if (provided.length !== 1) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Exactly one of paymentId, quote, or paymentRequest must be provided',
|
|
)
|
|
}
|
|
|
|
// === 2. Build query with ORDER BY createdAt DESC + LIMIT 1 ===
|
|
let query: string
|
|
let params: string[]
|
|
|
|
if (criteria.paymentId != null) {
|
|
query = `
|
|
SELECT * FROM transactions
|
|
WHERE paymentId = ?
|
|
ORDER BY createdAt DESC
|
|
LIMIT 1
|
|
`
|
|
params = [criteria.paymentId]
|
|
} else if (criteria.quote != null) {
|
|
query = `
|
|
SELECT * FROM transactions
|
|
WHERE quote = ?
|
|
ORDER BY createdAt DESC
|
|
LIMIT 1
|
|
`
|
|
params = [criteria.quote]
|
|
} else if (criteria.paymentRequest != null
|
|
|
|
) {
|
|
query = `
|
|
SELECT * FROM transactions
|
|
WHERE paymentRequest = ?
|
|
ORDER BY createdAt DESC
|
|
LIMIT 1
|
|
`
|
|
params = [criteria.paymentRequest]
|
|
} else {
|
|
// This should never happen due to validation above
|
|
throw new AppError(Err.DATABASE_ERROR, 'No valid criterion provided')
|
|
}
|
|
|
|
// === 3. Execute ===
|
|
const db = getInstance()
|
|
const result = db.execute(query, params) // assuming this returns { rows: Row[] }
|
|
|
|
if (!result.rows || result.rows.length === 0) {
|
|
throw new AppError(Err.NOTFOUND_ERROR, `No transaction found for given criteria`)
|
|
}
|
|
|
|
const row = result.rows.item(0) // now guaranteed to be the LATEST one
|
|
|
|
log.trace('[getLastTransactionBy]', {
|
|
criteria,
|
|
foundTransactionId: row.id,
|
|
createdAt: row.createdAt,
|
|
})
|
|
|
|
return normalizeTransactionRecord(row)
|
|
} catch (e: any) {
|
|
if (e instanceof AppError) {
|
|
throw e // rethrow known app errors
|
|
}
|
|
|
|
log.error('[getLastTransactionBy] Database error', e)
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Failed to fetch transaction',
|
|
e.message || String(e)
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const addTransactionAsync = async function (tx: Partial<Transaction>): Promise<Transaction> {
|
|
try {
|
|
const {type, amount, fee, unit, data, memo, mint, status} = tx
|
|
const now = new Date()
|
|
|
|
const query = `
|
|
INSERT INTO transactions (type, amount, fee, unit, data, memo, mint, status, createdAt)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
const params = [type, amount, fee, unit, data, memo, mint, status, now.toISOString()]
|
|
|
|
const db = getInstance()
|
|
const result = await db.executeAsync(query, params)
|
|
|
|
log.info('[addTransactionAsync]', 'New transaction added to the database', {id: result.insertId, type, mint, status})
|
|
|
|
return getTransactionById(result.insertId as number) // already normalized
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not store transaction in the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
// This updates status and appends data to the existing transaction data
|
|
const updateStatusesAsync = async function (
|
|
transactionIds: number[],
|
|
status: TransactionStatus,
|
|
data: string,
|
|
) {
|
|
const transactionIdsString = transactionIds.join(',')
|
|
|
|
const selectQuery = `
|
|
SELECT data
|
|
FROM transactions
|
|
WHERE id IN (${transactionIdsString})
|
|
`
|
|
|
|
try {
|
|
const db = getInstance()
|
|
const result1 = await db.executeAsync(selectQuery)
|
|
|
|
if (!result1.rows) {
|
|
return
|
|
}
|
|
|
|
const updatedDataArray = []
|
|
|
|
// We prepare appended transaction data for each transaction retrieved into array
|
|
for (const row of result1.rows?._array) {
|
|
const currentData = row.data
|
|
const updatedData = currentData.slice(0, -1) + ', ' + data + ']'
|
|
updatedDataArray.push(updatedData)
|
|
}
|
|
|
|
const updateQuery = `
|
|
UPDATE transactions
|
|
SET status = ?, data = ?
|
|
WHERE id IN (${transactionIdsString})
|
|
`
|
|
// We update one by one from the array
|
|
const params = [status, updatedDataArray.join(',')]
|
|
|
|
const result2 = await _db.executeAsync(updateQuery, params)
|
|
|
|
log.info('[updateStatusesAsync]', `Transactions statuses updated in the database`, {numUpdates: result2.rowsAffected, status})
|
|
|
|
return result2
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not update transaction statuses in the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
const expireAllAfterRecovery = async function () {
|
|
const updateQuery = `
|
|
UPDATE transactions
|
|
SET status = ?
|
|
`
|
|
const params = [TransactionStatus.EXPIRED]
|
|
const result = await _db.executeAsync(updateQuery, params)
|
|
log.info('[expireAllAfterRecovery]', `Transactions statuses set to EXPIRED.`)
|
|
return result
|
|
}
|
|
|
|
|
|
const deleteTransactionsByStatus = function (status: TransactionStatus) {
|
|
try {
|
|
const query = `
|
|
DELETE FROM transactions
|
|
WHERE status = ?
|
|
`
|
|
const params = [status]
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query, params)
|
|
|
|
log.debug('[deleteTransactionsByStatus]', 'Transactions were deleted', {status})
|
|
|
|
return rows
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not delete transactions.',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const getIncomingPendingCount = function () {
|
|
try {
|
|
const query = `
|
|
SELECT COUNT(*) AS total
|
|
FROM transactions
|
|
WHERE status = 'PENDING' AND type IN ('RECEIVE', 'RECEIVE_BY_PAYMENT_REQUEST')
|
|
`
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query)
|
|
|
|
return rows?.item(0)['total'] as number
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not get incoming pending count.',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const deleteIncomingPending = function () {
|
|
try {
|
|
const query = `
|
|
DELETE FROM transactions
|
|
WHERE status = 'PENDING' AND type IN ('RECEIVE', 'RECEIVE_BY_PAYMENT_REQUEST')
|
|
`
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query)
|
|
|
|
log.debug('[deleteIncomingPending]', 'Pending incoming transactions were deleted')
|
|
|
|
return rows
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not delete incoming pending transactions.',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const deleteTransactionById = function (id: number) {
|
|
try {
|
|
const query = `
|
|
DELETE FROM transactions
|
|
WHERE id = ?
|
|
LIMIT 1
|
|
`
|
|
const params = [id]
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query, params)
|
|
|
|
log.debug('[deleteTransactionById]', 'Transaction has been deleted', {id})
|
|
|
|
return rows
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not delete transaction.',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Proofs
|
|
*/
|
|
const addOrUpdateProof = function (
|
|
proof: Proof,
|
|
isPending: boolean = false,
|
|
isSpent: boolean = false,
|
|
) {
|
|
try {
|
|
const now = new Date()
|
|
|
|
const query = `
|
|
INSERT OR REPLACE INTO proofs (id, amount, secret, C, dleq_r, dleq_s, dleq_e, unit, tId, mintUrl, isPending, isSpent, updatedAt)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
const params = [
|
|
proof.id,
|
|
proof.amount,
|
|
proof.secret,
|
|
proof.C,
|
|
proof.dleq ? proof.dleq.r : null,
|
|
proof.dleq ? proof.dleq.s : null,
|
|
proof.dleq ? proof.dleq.e : null,
|
|
proof.unit,
|
|
proof.tId,
|
|
proof.mintUrl,
|
|
isPending,
|
|
isSpent,
|
|
now.toISOString(),
|
|
]
|
|
|
|
const db = getInstance()
|
|
const result = db.execute(query, params)
|
|
// DO NOT log proof secrets to Sentry
|
|
log.info('[addOrUpdateProof]', `${isPending ? ' Pending' : ''} proof added or updated in the database`,
|
|
{id: result.insertId, tId: proof.tId, isPending, isSpent},
|
|
)
|
|
|
|
const newProof = getProofById(result.insertId as number)
|
|
|
|
return newProof as ProofRecord
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not store proof into the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
const addOrUpdateProofs = function (
|
|
proofs: Proof[],
|
|
isPending: boolean = false,
|
|
isSpent: boolean = false,
|
|
): number | undefined {
|
|
try {
|
|
const now = new Date()
|
|
let insertQueries: SQLBatchTuple[] = []
|
|
|
|
if(isPending && isSpent) {
|
|
throw new Error('Conflicting proof states')
|
|
}
|
|
|
|
if(proofs.length === 0) {
|
|
log.error('[addOrUpdateProofs] Empty proof array passed')
|
|
return 0
|
|
}
|
|
|
|
|
|
for (const proof of proofs) {
|
|
if(!isAlive(proof)) {
|
|
log.error('[addOrUpdateProofs] Proof is not alive', {id: proof.id})
|
|
continue
|
|
}
|
|
|
|
insertQueries.push([
|
|
`
|
|
INSERT OR REPLACE INTO proofs (id, amount, secret, C, dleq_r, dleq_s, dleq_e, unit, tId, mintUrl, isPending, isSpent, updatedAt)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`,
|
|
[
|
|
proof.id,
|
|
proof.amount,
|
|
proof.secret,
|
|
proof.C,
|
|
proof.dleq ? proof.dleq.r : null,
|
|
proof.dleq ? proof.dleq.s : null,
|
|
proof.dleq ? proof.dleq.e : null,
|
|
proof.unit,
|
|
proof.tId,
|
|
proof.mintUrl,
|
|
isPending,
|
|
isSpent,
|
|
now.toISOString(),
|
|
],
|
|
])
|
|
}
|
|
|
|
// Execute the batch of SQL statements
|
|
const db = getInstance()
|
|
const {rowsAffected} = db.executeBatch(insertQueries)
|
|
|
|
// DO NOT log proof secrets to Sentry
|
|
log.info('[addOrUpdateProofs]',
|
|
`${rowsAffected}${isPending ? ' pending' : ''} ${isSpent ? ' spent' : ''} proofs were added or updated in the database`,
|
|
{isPending, isSpent}
|
|
)
|
|
|
|
return rowsAffected
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not insert or update proofs into the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
const updateProofsMintUrl = function (currentMintUrl: string, updatedMintUrl: string) {
|
|
try {
|
|
const query = `
|
|
UPDATE proofs
|
|
SET mintUrl = ?
|
|
WHERE mintUrl = ?
|
|
`
|
|
const params = [updatedMintUrl, currentMintUrl]
|
|
|
|
const db = getInstance()
|
|
db.execute(query, params)
|
|
|
|
log.debug('[updateMintUrl]', 'Proof mintUrl updated', {currentMintUrl, updatedMintUrl})
|
|
|
|
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not update proof mintUrl in database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
const removeAllProofs = async function () {
|
|
try {
|
|
const query = `
|
|
DELETE FROM proofs
|
|
`
|
|
const db = getInstance()
|
|
db.execute(query)
|
|
|
|
log.info('[removeAllProofs]', 'All proofs were removed from the database.')
|
|
|
|
return true
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Could not remove proofs from the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
const getProofById = function (id: number) {
|
|
try {
|
|
const query = `
|
|
SELECT * FROM proofs WHERE id = ?
|
|
`
|
|
const params = [id]
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query, params)
|
|
|
|
return rows?.item(0) as ProofRecord
|
|
} catch (e: any) {
|
|
throw new AppError(Err.DATABASE_ERROR, 'proof not found', e.message)
|
|
}
|
|
}
|
|
|
|
const getProofs = async (
|
|
includeUnspent: boolean,
|
|
includePending: boolean,
|
|
includeSpent: boolean,
|
|
): Promise<ProofRecord[]> => {
|
|
// If nothing is requested, return empty array early
|
|
if (!includeUnspent && !includePending && !includeSpent) {
|
|
return [];
|
|
}
|
|
|
|
const conditions: string[] = [];
|
|
|
|
if (includeUnspent) {
|
|
conditions.push('(isPending = 0 AND isSpent = 0)');
|
|
}
|
|
if (includePending) {
|
|
conditions.push('(isPending = 1 AND isSpent = 0)');
|
|
}
|
|
if (includeSpent) {
|
|
conditions.push('isSpent = 1');
|
|
}
|
|
|
|
const whereClause = conditions.join(' OR ');
|
|
|
|
const query = `
|
|
SELECT *
|
|
FROM proofs
|
|
WHERE ${whereClause}
|
|
ORDER BY id DESC
|
|
`;
|
|
|
|
try {
|
|
const db = getInstance();
|
|
const { rows } = await db.executeAsync(query);
|
|
return (rows?._array ?? []) as ProofRecord[];
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Proofs could not be retrieved from the database',
|
|
e.message,
|
|
);
|
|
}
|
|
};
|
|
|
|
const getProofsByTransaction = function (transactionId: number): ProofRecord[] {
|
|
try {
|
|
const query = `
|
|
SELECT *
|
|
FROM proofs
|
|
WHERE tId = ?
|
|
`
|
|
const params = [transactionId]
|
|
|
|
const db = getInstance()
|
|
const {rows} = db.execute(query, params)
|
|
|
|
return rows?._array as ProofRecord[]
|
|
} catch (e: any) {
|
|
throw new AppError(
|
|
Err.DATABASE_ERROR,
|
|
'Proofs could not be retrieved from the database',
|
|
e.message,
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
export const Database = {
|
|
getInstance,
|
|
getDatabaseVersion,
|
|
cleanAll,
|
|
getTransactionsCount,
|
|
getTransactionById,
|
|
getLastTransactionBy,
|
|
getRecentTransactionsByUnitAsync,
|
|
getTransactionsAsync,
|
|
getPendingTopups,
|
|
getPendingTopupsCount,
|
|
getPendingTransfers,
|
|
getPendingTransfersCount,
|
|
addTransactionAsync,
|
|
updateTransaction,
|
|
expireAllAfterRecovery,
|
|
updateStatusesAsync,
|
|
deleteTransactionsByStatus,
|
|
deleteTransactionById,
|
|
getIncomingPendingCount,
|
|
deleteIncomingPending,
|
|
getPendingAmount,
|
|
addOrUpdateProof,
|
|
addOrUpdateProofs,
|
|
updateProofsMintUrl,
|
|
removeAllProofs,
|
|
getProofById,
|
|
getProofs,
|
|
getProofsByTransaction,
|
|
}
|