diff --git a/dist/BUNDLE_ID b/dist/BUNDLE_ID index c0421e84..e298d3e1 100644 --- a/dist/BUNDLE_ID +++ b/dist/BUNDLE_ID @@ -1 +1 @@ -01977574-53f2-73c3-9f00-0f58244617c5 \ No newline at end of file +01978f90-d6d8-77d0-b0dd-18186d4c07cc \ No newline at end of file diff --git a/package.json b/package.json index b40dd48e..5a99fbaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.2.2-beta.20", + "version": "0.2.2-beta.21", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", diff --git a/src/models/ProofsStore.ts b/src/models/ProofsStore.ts index 6d686d51..cb6e1fd5 100644 --- a/src/models/ProofsStore.ts +++ b/src/models/ProofsStore.ts @@ -299,7 +299,7 @@ export const ProofsStoreModel = types }, })) .views(self => ({ - getBalances() { + get balances() { const mintBalancesMap: Map = new Map() const unitBalancesMap: Map = new Map() const mintPendingBalancesMap: Map = new Map() @@ -393,7 +393,7 @@ export const ProofsStoreModel = types })) .views(self => ({ // Move to MintsStore? getMintBalance: (mintUrl: string) => { - const balances = self.getBalances().mintBalances + const balances = self.balances.mintBalances const mintBalance = balances .find((balance: MintBalance) => balance.mintUrl === mintUrl) @@ -401,7 +401,7 @@ export const ProofsStoreModel = types return mintBalance }, getMintBalancesWithEnoughBalance: (amount: number, unit: MintUnit) => { - const balances = self.getBalances().mintBalances + const balances = self.balances.mintBalances const filteredMintBalances = balances .filter((balance: MintBalance) => { @@ -416,7 +416,7 @@ export const ProofsStoreModel = types return filteredMintBalances }, getMintBalancesWithUnit: (unit: MintUnit) => { - const balances = self.getBalances().mintBalances + const balances = self.balances.mintBalances const filteredMintBalances = balances .filter((balance: MintBalance) => { @@ -431,7 +431,7 @@ export const ProofsStoreModel = types return filteredMintBalances }, getMintBalanceWithMaxBalance: (unit: MintUnit) => { - const balances = self.getBalances().mintBalances + const balances = self.balances.mintBalances let maxBalance = undefined; let maxAmount = -Infinity; @@ -447,7 +447,7 @@ export const ProofsStoreModel = types return maxBalance; }, getUnitBalance: (unit: MintUnit) => { - const balances = self.getBalances().unitBalances + const balances = self.balances.unitBalances const unitBalance = balances .find((balance: UnitBalance) => balance.unit === unit) @@ -455,7 +455,9 @@ export const ProofsStoreModel = types return unitBalance }, getProofsSubset: (proofs: Proof[], proofsToRemove: Proof[]) => { - return proofs.filter(proof => !proofsToRemove.includes(proof)) + // return proofs.filter(proof => !proofsToRemove.includes(proof)) + const secrets = new Set(proofsToRemove.map(p => p.secret)); + return proofs.filter(p => !secrets.has(p.secret)); }, })).postProcessSnapshot((snapshot) => { // NOT persisted to storage except last pendingByMintSecrets! return { diff --git a/src/models/TransactionsStore.ts b/src/models/TransactionsStore.ts index 1503c598..01e120a0 100644 --- a/src/models/TransactionsStore.ts +++ b/src/models/TransactionsStore.ts @@ -3,439 +3,398 @@ import { SnapshotOut, types, flow, - } from 'mobx-state-tree' - import {withSetPropAction} from './helpers/withSetPropAction' - import { +} from 'mobx-state-tree' +import {withSetPropAction} from './helpers/withSetPropAction' +import { TransactionModel, Transaction, TransactionStatus, - } from './Transaction' - import {Database} from '../services' - import {log} from '../services/logService' - import { getRootStore } from './helpers/getRootStore' - import { formatDistance } from 'date-fns' - import { MintUnit } from '../services/wallet/currency' - import { Mint } from './Mint' - - export const maxTransactionsInHistory = 10 - export const maxTransactionsByUnit = 3 - - export type GroupedByTimeAgo = { - [timeAgo: string]: Transaction[]; - } - - export const TransactionsStoreModel = types - .model('TransactionsStore', { - transactionsMap: types.map(TransactionModel), - history: types.array(types.safeReference(TransactionModel, { acceptsUndefined: false })), - recentByUnit: types.array(types.safeReference(TransactionModel, { acceptsUndefined: false })), - }) - .actions(withSetPropAction) - .views(self => ({ - get pendingHistory() { - return self.history - .slice() - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) - .filter(t => t.status === TransactionStatus.PENDING) - }, - get historyByTimeAgo() { - return self.history - .slice() - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) - .reduce((groups: GroupedByTimeAgo, transaction: Transaction) => { - const timeAgo = formatDistance(transaction.createdAt as Date, new Date(), {addSuffix: true}) - if (!groups[timeAgo]) { - groups[timeAgo] = [] - } - groups[timeAgo].push(transaction) - return groups - }, {}) - }, - get historyPendingByTimeAgo() { - return this.pendingHistory.reduce((groups: GroupedByTimeAgo, transaction: Transaction) => { - const timeAgo = formatDistance(transaction.createdAt as Date, new Date(), {addSuffix: true}) - if (!groups[timeAgo]) { - groups[timeAgo] = [] - } - groups[timeAgo].push(transaction) - return groups - }, {}) - }, - get historyCount() { - return self.history.length - }, - get pendingHistoryCount() { - return this.pendingHistory.length - }, - getRecentByUnit(unit: MintUnit) { - return self.recentByUnit - .slice() - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) - .filter(t => t.unit === unit) - }, - getPendingTopups() { +} from './Transaction' +import {Database} from '../services' +import {log} from '../services/logService' +import { getRootStore } from './helpers/getRootStore' +import { formatDistance } from 'date-fns' +import { MintUnit } from '../services/wallet/currency' +import { Mint } from './Mint' + +export const maxTransactionsInHistory = 10 +export const maxTransactionsByUnit = 3 + +export type GroupedByTimeAgo = { + [timeAgo: string]: Transaction[]; +} + +export const TransactionsStoreModel = types + .model('TransactionsStore', { + transactionsMap: types.map(TransactionModel), + history: types.array(types.safeReference(TransactionModel, { acceptsUndefined: false })), + recentByUnit: types.array(types.safeReference(TransactionModel, { acceptsUndefined: false })), + }) + .actions(withSetPropAction) + .views(self => ({ + get pendingHistory() { + return self.history + .slice() + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + .filter(t => t.status === TransactionStatus.PENDING) + }, + get historyByTimeAgo() { + return self.history + .slice() + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + .reduce((groups: GroupedByTimeAgo, transaction: Transaction) => { + const timeAgo = formatDistance(transaction.createdAt as Date, new Date(), { addSuffix: true }) + if (!groups[timeAgo]) { + groups[timeAgo] = [] + } + groups[timeAgo].push(transaction) + return groups + }, {}) + }, + get historyPendingByTimeAgo() { + return this.pendingHistory.reduce((groups: GroupedByTimeAgo, transaction: Transaction) => { + const timeAgo = formatDistance(transaction.createdAt as Date, new Date(), { addSuffix: true }) + if (!groups[timeAgo]) { + groups[timeAgo] = [] + } + groups[timeAgo].push(transaction) + return groups + }, {}) + }, + get historyCount() { + return self.history.length + }, + get pendingHistoryCount() { + return this.pendingHistory.length + }, + getRecentByUnit(unit: MintUnit) { + return self.recentByUnit + .slice() + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + .filter(t => t.unit === unit) + }, + getPendingTopups() { const dbTopups: Transaction[] = Database.getPendingTopups() - if(dbTopups.length > 0) { - + if (dbTopups.length > 0) { const pendingTopups: Transaction[] = [] - - for(const topup of dbTopups) { - const inStoreTransaction = {...topup} - + for (const topup of dbTopups) { + const inStoreTransaction = { ...topup } const topupInstance = TransactionModel.create(inStoreTransaction) pendingTopups.push(topupInstance) } - return pendingTopups - } - return [] - - }, - getPendingTransfers() { - const dbTransfers: Transaction[] = Database.getPendingTopups() - - if(dbTransfers.length > 0) { + }, + getPendingTransfers() { + const dbTransfers: Transaction[] = Database.getPendingTransfers() + if (dbTransfers.length > 0) { const pendingTransfers: Transaction[] = [] - - for(const transfer of dbTransfers) { - const inStoreTransaction = {...transfer} - + for (const transfer of dbTransfers) { + const inStoreTransaction = { ...transfer } const transferInstance = TransactionModel.create(inStoreTransaction) pendingTransfers.push(transferInstance) } - return pendingTransfers + } + return [] + }, + countRecentByUnit(unit: MintUnit) { + return this.getRecentByUnit(unit).length + } + })) + .actions(self => ({ + findById(id: number, loadTokens?: boolean) { + let transaction = self.transactionsMap.get(id) + // Search the db and add if tx is not in the state + // Search always to retrieve full tokens in tx detail screen + if (!transaction || loadTokens === true) { + const dbTransaction = Database.getTransactionById(id) + + if (dbTransaction) { + const inStoreTransaction = { ...dbTransaction } + const { id } = dbTransaction + + if (!loadTokens) { + // Shorten for performance reasons + if (inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { + inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40) + } + if (inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { + inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40) + } + } + + self.transactionsMap.set(id, inStoreTransaction) + transaction = self.transactionsMap.get(id) + } } - return [] - - }, - countRecentByUnit(unit: MintUnit) { - return this.getRecentByUnit(unit).length - } - })) - .actions(self => ({ - findById(id: number, loadTokens?: boolean) { - let transaction = self.transactionsMap.get(id) - - // Search the db and add if tx is not in the state - // Search always to retrieve full tokens in tx detail screen - if(!transaction || loadTokens === true) { - const dbTransaction = Database.getTransactionById(id) - - if(dbTransaction) { - const inStoreTransaction = {...dbTransaction} - const {id} = dbTransaction - - if(!loadTokens) { - // Shorten for performance reasons - if(inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { - inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40) - } - - if(inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { - inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40) - } - } - - self.transactionsMap.set(id, inStoreTransaction) - transaction = self.transactionsMap.get(id) - } - } - - return transaction - }, - findByPaymentId(paymentId: string) { + return transaction + }, + findBy(criteria: { paymentId?: string; quote?: string; paymentRequest?: string }) { + let dbTransaction: Transaction + let key: string, value: string - const dbTransaction = Database.getTransactionByPaymentId(paymentId) - - if(dbTransaction) { - const inStoreTransaction = {...dbTransaction} - const {id} = dbTransaction - - // Shorten for performance reasons - if(inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { - inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40) + try { + dbTransaction = Database.getTransactionBy(criteria) + } catch (e: any) { + const entry = Object.entries(criteria).find(([_, v]) => v != null) + if (entry) { + [key, value] = entry + log.warn('[findBy]', `Transaction with ${key} ${value} not found in database`) + } else { + log.warn('[findBy]', 'Invalid search criteria', { criteria }) } - - if(inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { - inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40) - } - - self.transactionsMap.set(id, inStoreTransaction) - return self.transactionsMap.get(id) - } else { - log.warn('[findByPaymentId]', `Transaction with paymentId ${paymentId} not found in database`) return undefined - } - }, - findByQuote(quote: string) { + } - const dbTransaction = Database.getTransactionByQuote(quote) + const inStoreTransaction = { ...dbTransaction } + const { id } = dbTransaction - if(dbTransaction) { - const inStoreTransaction = {...dbTransaction} - const {id} = dbTransaction - - // Shorten for performance reasons - if(inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { - inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40) - } + // Shorten for performance reasons + if (inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { + inStoreTransaction.inputToken = inStoreTransaction.inputToken.slice(0, 40) + } + if (inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { + inStoreTransaction.outputToken = inStoreTransaction.outputToken.slice(0, 40) + } - if(inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { - inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40) - } - + self.transactionsMap.set(id, inStoreTransaction) + return self.transactionsMap.get(id) + }, + pruneRecentByUnit(unit: MintUnit) { + const unitCount = self.countRecentByUnit(unit) + log.trace('[pruneRecentByUnit]', { unit, unitCount }) + + if (unitCount > maxTransactionsByUnit) { + const transactionsToRemove = self.getRecentByUnit(unit) + .slice() + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + .splice(maxTransactionsByUnit) + + self.recentByUnit.replace( + self.recentByUnit.filter( + t => !transactionsToRemove.some(removed => removed.id === t.id) + ) + ) + log.trace('[pruneRecentByUnit]', `${transactionsToRemove.length} pruned from recentByUnit`) + } + }, + pruneRecentWithoutCurrentMint() { + const rootStore = getRootStore(self) + const { mintsStore } = rootStore + + const transactionsToRemove = self.recentByUnit.filter(transaction => { + // Check if the mint property of the transaction does not exist in the mints array + return !mintsStore.allMints.some((mint: Mint) => mint.mintUrl === transaction.mint) + }) + + self.recentByUnit.replace( + self.recentByUnit.filter( + t => !transactionsToRemove.some(removed => removed.id === t.id) + ) + ) + log.trace( + '[pruneRecentWithoutCurrentMint]', + `${transactionsToRemove.length} pruned from recentByUnit` + ) + }, + pruneHistory() { + // Step 1: Trim history to keep only the MAX_HISTORY_TRANSACTIONS most recent + if (self.history.length > maxTransactionsInHistory) { + const transactionsToRemove = self.history + .slice() + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + .splice(maxTransactionsInHistory) + + self.history.replace( + self.history.filter( + t => !transactionsToRemove.some(removed => removed.id === t.id) + ) + ) + log.trace('[pruneHistory]', `${transactionsToRemove.length} pruned from history`) + } + }, + removeAllHistory() { + self.history.clear() + log.debug('[removeAllHistory]', 'Removed all transactions from history') + }, + removeAllRecentByUnit() { + self.recentByUnit.clear() + log.debug('[removeAllRecentByUnit]', 'Removed all transactions from recentByUnit') + }, + removeAllTransactions() { + self.recentByUnit.clear() + self.history.clear() + self.transactionsMap.clear() + log.debug('[removeAllTransactions]', 'Removed all transactions from TransactionsStore') + }, + })) + .actions(self => ({ + addTransaction: flow(function* addTransaction(newTransaction) { + // First let's store the transaction into the database + const dbTransaction: Transaction = yield Database.addTransactionAsync(newTransaction) + + // Add the new transaction to the transactions store + const inStoreTransaction = { ...dbTransaction } + const { id } = dbTransaction + + if (!self.transactionsMap.has(id)) { self.transactionsMap.set(id, inStoreTransaction) - return self.transactionsMap.get(id) - } else { - log.warn('[findByQuote]', `Transaction with quote ${quote} not found in database`) - return undefined - } - }, - findByPaymentRequest(pr: string) { + } - const dbTransaction = Database.getTransactionByPaymentRequest(pr) + const reference = self.transactionsMap.get(id) + self.history.unshift(reference!) + self.recentByUnit.unshift(reference!) - if(dbTransaction) { - const inStoreTransaction = {...dbTransaction} - const {id} = dbTransaction - - // Shorten for performance reasons - if(inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { - inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40) + // Purge the oldest references from cache, but keep some for each mint + self.pruneRecentByUnit(newTransaction.unit) + self.pruneHistory() + + return reference as Transaction + }), + addToHistory(limit: number, offset: number, onlyPending: boolean) { + // Appends transaction to the map and adds reference to history from database. + const transactions = Database.getTransactions(limit, offset, onlyPending) + log.trace('[addToHistory] transactions ids', { + ids: transactions ? transactions.map(t => t.id) : [] + }) + + if (transactions && transactions.length > 0) { + for (const dbTransaction of transactions) { + const inStoreTransaction = { ...dbTransaction } + + // Shorten for performance reasons + if (inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { + inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40) + } + if (inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { + inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40) + } + + const { id } = dbTransaction + + if (!self.transactionsMap.has(id)) { + self.transactionsMap.set(id, inStoreTransaction) + log.trace('[addToHistory]', `${id} added to transactionsMap`) + } + + const reference = self.transactionsMap.get(id) + + if (!self.history.find(t => t.id === id)) { + self.history.push(reference!) + log.trace( + '[addToHistory]', + `${onlyPending ? 'Pending reference' : 'Reference'} ${id} added to history` + ) + } + } + } + }, + addRecentByUnit() { + // Rehydrates recent from database. + const transactions = Database.getRecentTransactionsByUnit(maxTransactionsByUnit) + + if (transactions && transactions.length > 0) { + for (const dbTransaction of transactions) { + const inStoreTransaction = { ...dbTransaction } + + // Shorten for performance reasons + if (inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { + inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40) + } + if (inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { + inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40) + } + + const { id } = dbTransaction + + if (!self.transactionsMap.has(id)) { + self.transactionsMap.set(id, inStoreTransaction) + } + + const reference = self.transactionsMap.get(id) + + if (!self.recentByUnit.find(t => t.id === id)) { + self.recentByUnit.push(reference!) + log.trace( + '[addRecentByUnit]', + `Transaction ${inStoreTransaction.id} added to recentByUnit` + ) + } + } + } + }, + updateStatuses: flow(function* updateStatuses( + ids: number[], + status: TransactionStatus, + data: string, + ) { + // Update status and amend to existing data in database + yield Database.updateStatusesAsync(ids, status, data) + + // Update the model status and amend related tx data + for (const id of ids) { + const transactionInstance = self.transactionsMap.get(id) + + if (transactionInstance) { + transactionInstance.status = status + + // Awkward but I want to keep function signature aligned with single status update + const updatedData = JSON.parse(transactionInstance.data) + updatedData.push(JSON.parse(data)) + transactionInstance.data = JSON.stringify(updatedData) } - if(inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { - inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40) + log.trace( + '[updateStatuses]', + 'Transaction statuses and data updated in TransactionsStore', + { ids, status } + ) + } + }), + deleteByStatus(status: TransactionStatus) { + self.transactionsMap.forEach((transaction, transactionId) => { + if (transaction.status === status) { + self.transactionsMap.delete(transactionId as string) } - - self.transactionsMap.set(id, inStoreTransaction) - return self.transactionsMap.get(id) - } else { - log.warn('[findByQuote]', `Transaction with paymentRequest ${pr} not found in database`) - return undefined - } - }, - pruneRecentByUnit(unit: MintUnit) { - const unitCount = self.countRecentByUnit(unit) - - log.trace('[pruneRecentByUnit]', {unit, unitCount}) - - if (unitCount > maxTransactionsByUnit) { - const transactionsToRemove = self.getRecentByUnit(unit) - .slice() - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) - .splice(maxTransactionsByUnit) - - self.recentByUnit.replace(self.recentByUnit.filter(t => !transactionsToRemove.some(removed => removed.id === t.id))) - - log.trace('[pruneRecentByUnit]', `${transactionsToRemove.length} pruned from recentByUnit`) - } - }, - pruneRecentWithoutCurrentMint() { - const rootStore = getRootStore(self) - const {mintsStore} = rootStore - - const transactionsToRemove = self.recentByUnit.filter(transaction => { - // Check if the mint property of the transaction does not exist in the mints array - return !mintsStore.allMints.some((mint: Mint) => mint.mintUrl === transaction.mint); - }) - - self.recentByUnit.replace(self.recentByUnit.filter(t => !transactionsToRemove.some(removed => removed.id === t.id))) - - log.trace('[pruneRecentWithoutCurrentMint]', `${transactionsToRemove.length} pruned from recentByUnit`) - }, - pruneHistory() { - // Step 1: Trim history to keep only the MAX_HISTORY_TRANSACTIONS most recent - if (self.history.length > maxTransactionsInHistory) { - const transactionsToRemove = self.history - .slice() - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) - .splice(maxTransactionsInHistory) - - self.history.replace(self.history.filter(t => !transactionsToRemove.some(removed => removed.id === t.id))) - - log.trace('[pruneHistory]', `${transactionsToRemove.length} pruned from history`) - } - }, - removeAllHistory() { - self.history.clear() - log.debug('[removeAllHistory]', 'Removed all transactions from history') - }, - removeAllRecentByUnit() { - self.recentByUnit.clear() - log.debug('[removeAllRecentByUnit]', 'Removed all transactions from recentByUnit') - }, - removeAllTransactions() { - self.recentByUnit.clear() - self.history.clear() - self.transactionsMap.clear() - log.debug('[removeAllTransactions]', 'Removed all transactions from TransactionsStore') - }, - })) - .actions(self => ({ - addTransaction: flow(function* addTransaction(newTransaction){ - // First let's store the transaction into the database - const dbTransaction: Transaction = yield Database.addTransactionAsync(newTransaction) - - // Add the new transaction to the transactions store - const inStoreTransaction = {...dbTransaction} - const {id} = dbTransaction - - if (!self.transactionsMap.has(id)) { - self.transactionsMap.set(id, inStoreTransaction) - } - - const reference = self.transactionsMap.get(id) - self.history.unshift(reference!) - self.recentByUnit.unshift(reference!) - - // log.trace('[addTransaction]', 'New transaction added to the TransactionsStore', {id}) - - // Purge the oldest references from cache, but keep some for each mint - self.pruneRecentByUnit(newTransaction.unit) - self.pruneHistory() - - return reference as Transaction - }), - addToHistory(limit: number, offset: number, onlyPending: boolean){ - // Appends transaction to the map and adds reference to history from database. - const transactions = Database.getTransactions(limit, offset, onlyPending) - log.trace('[addToHistory] transactions ids', {ids: transactions ? transactions.map(t => t.id) : []}) - - if (transactions && transactions.length > 0) { - for (const dbTransaction of transactions) { - const inStoreTransaction = {...dbTransaction} - - // Shorten for performance reasons - if(inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { - inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40) - } - - if(inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { - inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40) - } - - const {id} = dbTransaction - - if (!self.transactionsMap.has(id)) { - self.transactionsMap.set(id, inStoreTransaction) - log.trace('[addToHistory]', `${id} added to transactionsMap`) - } - - const reference = self.transactionsMap.get(id) - - if (!self.history.find(t => t.id === id)) { - self.history.push(reference!) - log.trace('[addToHistory]', `${onlyPending ? 'Pending reference' : 'Reference'} ${id} added to history`) - } - } - } - }, - addRecentByUnit() { - // Rehydrates recent from database. - const transactions = Database.getRecentTransactionsByUnit(maxTransactionsByUnit) - - if (transactions && transactions.length > 0) { - for (const dbTransaction of transactions) { - const inStoreTransaction = {...dbTransaction} - - // Shorten for performance reasons - if(inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) { - inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40) - } - if(inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) { - inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40) - } - - const {id} = dbTransaction - - if (!self.transactionsMap.has(id)) { - self.transactionsMap.set(id, inStoreTransaction) - } - - const reference = self.transactionsMap.get(id) - - if (!self.recentByUnit.find(t => t.id === id)) { - self.recentByUnit.push(reference!) - log.trace('[addRecentByUnit]', `Transaction ${inStoreTransaction.id} added to recentByUnit`) - } - } - } - }, - updateStatuses: flow(function* updateStatuses( - ids: number[], - status: TransactionStatus, - data: string, - ) { - // Update status and amend to existing data in database - yield Database.updateStatusesAsync(ids, status, data) - - // Update the model status and amend related tx data - for (const id of ids) { - const transactionInstance = self.transactionsMap.get(id) - - if (transactionInstance) { - transactionInstance.status = status - - // Awkward but I want to keep function signature aligned with single status update - const updatedData = JSON.parse(transactionInstance.data) - updatedData.push(JSON.parse(data)) - transactionInstance.data = JSON.stringify(updatedData) - } - - log.trace('[updateStatuses]', 'Transaction statuses and data updated in TransactionsStore', {ids, status}) - } - }), - deleteByStatus(status: TransactionStatus){ - - self.transactionsMap.forEach((transaction, transactionId) => { - if (transaction.status === status) { - self.transactionsMap.delete(transactionId as string) - } - }) - - return Database.deleteTransactionsByStatus(status) - } - })).postProcessSnapshot((snapshot) => { - // Trim history if it exceeds the limit - let prunedHistory = snapshot.history - - if (snapshot.history.length > maxTransactionsInHistory) { - // Keep only the most recent transactions within the limit - const orderedHistory = [...snapshot.history].sort((a, b) => (b as number) - (a as number)) - prunedHistory = orderedHistory.slice(0, maxTransactionsInHistory) - } - - // Clean up transactionMap: remove transactions not in history or recentByUnit - const prunedTransactionsMap = Object.fromEntries( - Object.entries(snapshot.transactionsMap).filter(([transactionId]) => - prunedHistory.includes(parseInt(transactionId)) || - snapshot.recentByUnit.includes(parseInt(transactionId)) - ) - ) - - // Return the new snapshot with the trimmed history and filtered transactionMap - const prunedSnapshot = { - ...snapshot, - history: prunedHistory, - transactionsMap: prunedTransactionsMap - } - - // console.log('[postProcessSnapshot]', {prunedSnapshot}) - - return prunedSnapshot - }) + }) + return Database.deleteTransactionsByStatus(status) + } + })).postProcessSnapshot((snapshot) => { + // Trim history if it exceeds the limit + let prunedHistory = snapshot.history - - // refresh - export interface TransactionsStore + if (snapshot.history.length > maxTransactionsInHistory) { + // Keep only the most recent transactions within the limit + const orderedHistory = [...snapshot.history].sort((a, b) => (b as number) - (a as number)) + prunedHistory = orderedHistory.slice(0, maxTransactionsInHistory) + } + + // Clean up transactionMap: remove transactions not in history or recentByUnit + const prunedTransactionsMap = Object.fromEntries( + Object.entries(snapshot.transactionsMap).filter(([transactionId]) => + prunedHistory.includes(parseInt(transactionId)) || + snapshot.recentByUnit.includes(parseInt(transactionId)) + ) + ) + + // Return the new snapshot with the trimmed history and filtered transactionMap + const prunedSnapshot = { + ...snapshot, + history: prunedHistory, + transactionsMap: prunedTransactionsMap + } + + return prunedSnapshot + }) + +// refresh +export interface TransactionsStore extends Instance {} - export interface TransactionsStoreSnapshot - extends SnapshotOut {} +export interface TransactionsStoreSnapshot + extends SnapshotOut {} \ No newline at end of file diff --git a/src/models/helpers/setupRootStore.ts b/src/models/helpers/setupRootStore.ts index 00de3251..c7b68b7f 100644 --- a/src/models/helpers/setupRootStore.ts +++ b/src/models/helpers/setupRootStore.ts @@ -14,7 +14,6 @@ import { IDisposer, onSnapshot, } from 'mobx-state-tree' -import { debounce } from "lodash" import * as Sentry from '@sentry/react-native' import type {RootStore} from '../RootStore' import {KeyChain, MMKVStorage} from '../../services' @@ -35,9 +34,10 @@ export const ROOT_STORAGE_KEY = 'minibits-root-storage' /** * Setup the root state. */ -let _disposer: IDisposer + export async function setupRootStore(rootStore: RootStore) { let restoredState: any + let _disposer: IDisposer // let latestSnapshot: any try { @@ -75,17 +75,9 @@ export async function setupRootStore(rootStore: RootStore) { // stop tracking state changes if we've already setup if (_disposer) { _disposer() - } + } - // track changes & save snapshot to the storage not more then once per second - /* const saveSnapshot = debounce((snapshot) => { - MMKVStorage.save(ROOT_STORAGE_KEY, snapshot) - }, 1000)*/ - - _disposer = onSnapshot(rootStore, snapshot => { - // log.trace('[setupRootStore] onSnapshot *** MMKV SHOULD SAVE ***') - // saveSnapshot(snapshot) - // log.trace('[setupRootStore] saved', {walletStore: snapshot.walletStore}) + _disposer = onSnapshot(rootStore, snapshot => { MMKVStorage.save(ROOT_STORAGE_KEY, snapshot) }) diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx index bf772ffd..ad553ae6 100644 --- a/src/screens/WalletScreen.tsx +++ b/src/screens/WalletScreen.tsx @@ -612,7 +612,7 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) { } const headerBg = useThemeColor('header') - const balances = proofsStore.getBalances() + const balances = proofsStore.balances const screenBg = useThemeColor('background') const mainButtonIcon = useThemeColor('mainButtonIcon') const mainButtonColor = useThemeColor('card') diff --git a/src/services/sqlite.ts b/src/services/sqlite.ts index f6cd288b..4876b37c 100644 --- a/src/services/sqlite.ts +++ b/src/services/sqlite.ts @@ -586,49 +586,28 @@ const getTransactionById = function (id: number) { } -const getTransactionByPaymentId = function (id: string) { +const getTransactionBy = function (criteria: { paymentId?: string; quote?: string; paymentRequest?: string }) { try { - const query = ` - SELECT * FROM transactions WHERE paymentId = ? - ` + // Input validation: ensure exactly one search criterion is provided + const providedCriteria = [criteria.paymentId != null, criteria.quote != null, criteria.paymentRequest != null].filter(Boolean).length + if (providedCriteria !== 1) { + throw new AppError(Err.DATABASE_ERROR, 'Exactly one search criterion must be provided to getTransactionBy', 'Invalid criteria object') + } - const params = [id] + // Dynamic query building based on the provided criterion + let query: string + let params: string[] - 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 getTransactionByQuote = function (quote: string) { - try { - const query = ` - SELECT * FROM transactions WHERE quote = ? - ` - - const params = [quote] - - 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 getTransactionByPaymentRequest = function (pr: string) { - try { - const query = ` - SELECT * FROM transactions WHERE paymentRequest = ? - ` - - const params = [pr] + if (criteria.paymentId != null) { + query = `SELECT * FROM transactions WHERE paymentId = ?` + params = [criteria.paymentId] + } else if (criteria.quote != null) { + query = `SELECT * FROM transactions WHERE quote = ?` + params = [criteria.quote] + } else { + query = `SELECT * FROM transactions WHERE paymentRequest = ?` + params = [criteria.paymentRequest!] + } const db = getInstance() const {rows} = db.execute(query, params) @@ -762,7 +741,8 @@ const deleteTransactionById = function (id: number) { try { const query = ` DELETE FROM transactions - WHERE id = ? + WHERE id = ? + LIMIT 1 ` const params = [id] @@ -1077,9 +1057,7 @@ export const Database = { cleanAll, getTransactionsCount, getTransactionById, - getTransactionByPaymentId, - getTransactionByQuote, - getTransactionByPaymentRequest, + getTransactionBy, getRecentTransactionsByUnit, getTransactions, getPendingTopups,