Fix transactions history

This commit is contained in:
minibits-cash
2024-11-08 15:41:02 +01:00
parent 4e12afc807
commit def26b28d7
10 changed files with 481 additions and 520 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
"version": "0.1.9-beta.29",
"version": "0.1.9-beta.30",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
+1 -1
View File
@@ -11,7 +11,7 @@ import {WalletStoreModel} from './WalletStore'
import {NwcStoreModel} from './NwcStore'
import { log } from '../services'
export const rootStoreModelVersion = 28 // Update this if model changes require migrations defined in setupRootStore.ts
export const rootStoreModelVersion = 29 // Update this if model changes require migrations defined in setupRootStore.ts
/**
* A RootStore model.
+2 -2
View File
@@ -54,8 +54,8 @@ export enum TransactionStatus {
}
export const TransactionModel = types
.model('Transaction', {
id: types.number,
.model('Transaction', {
id: types.identifierNumber,
type: types.frozen<TransactionType>(),
amount: types.integer,
fee: types.optional(types.integer, 0),
+209 -166
View File
@@ -19,9 +19,7 @@ import { formatDistance } from 'date-fns'
import { MintUnit } from '../services/wallet/currency'
import { Mint } from './Mint'
export const maxTransactionsInModel = 10
export const maxTransactionsByMint = 10
export const maxTransactionsByHostname = 3
export const maxTransactionsInHistory = 10
export const maxTransactionsByUnit = 3
export type GroupedByTimeAgo = {
@@ -30,191 +28,222 @@ export type GroupedByTimeAgo = {
export const TransactionsStoreModel = types
.model('TransactionsStore', {
transactions: types.array(TransactionModel),
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 all() {
return self.transactions
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) => {
// Sort by createdAt timestamp
if (a.createdAt && b.createdAt) {
return b.createdAt.getTime() - a.createdAt.getTime()
}
}) as Transaction[]
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
.filter(t => t.unit === unit)
},
get count() {
return self.transactions.length
},
get recent() {
return this.all.slice(0, 3) // Return the first 3 transactions
},
get pending() {
return this.all.filter(t => t.status === TransactionStatus.PENDING)
},
get groupedByTimeAgo() {
return this.all.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 groupedPendingByTimeAgo() {
return this.pending.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
}, {})
},
recentByHostname(mintHostname: string) {
return this.all.filter(t => getHostname(t.mint as string) === mintHostname).slice(0, maxTransactionsByHostname)
},
recentByUnit(unit: MintUnit, count?: number) {
if (!count || count > maxTransactionsByUnit) {
count = maxTransactionsByUnit
}
return this.all.filter(t => t.unit === unit).slice(0, count)
},
recentByHostnameGroupedByTimeAgo(mintHostname: string) {
const recentByHostname = this.recentByHostname(mintHostname)
return recentByHostname.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
}, {})
},
getByMint(mintUrl: string) {
return this.all.filter(t => t.mint === mintUrl)
},
countByMint(mintUrl: string) {
return this.getByMint(mintUrl).length
}
}))
countRecentByUnit(unit: MintUnit) {
return this.getRecentByUnit(unit).length
}
}))
.actions(self => ({
findById(id: number) {
let tx = self.transactions.find(tx => tx.id === id)
findById(id: number) {
let transaction = self.transactionsMap.get(id)
// Search the db and add if tx is not in the state
if(!tx) {
if(!transaction) {
const dbTransaction = Database.getTransactionById(id)
if(dbTransaction) {
const createdAt = new Date(dbTransaction.createdAt)
const createdAt = new Date(dbTransaction.createdAt)
const inStoreTransaction = {...dbTransaction, createdAt}
tx = TransactionModel.create(inStoreTransaction)
self.transactions.push(tx)
const {id} = dbTransaction
self.transactionsMap.set(id, inStoreTransaction)
transaction = self.transactionsMap.get(id)
}
}
return tx
return transaction
},
removeOldTransactions: () => { // not used
const numTransactions = self.count
/*pruneTransactionsMap() {
// Clean up transactionMap: remove transactions not in history or recentByUnit
self.transactionsMap.forEach((_, transactionId) => {
const isInHistory = self.history.some(t => t.id === transactionId)
const isInRecentByUnit = self.recentByUnit.some(t => t.id === transactionId)
// If there are more than 10 transactions, delete the older ones
if (numTransactions > maxTransactionsInModel) {
self.transactions
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
.splice(maxTransactionsInModel) // Remove transactions beyond the desired number to keep
log.debug('[removeOldTransactions]', `${
numTransactions - maxTransactionsInModel
} transaction(s) removed from TransactionsStore`,
)
}
},
removeOldByMint: (mintUrl: string) => {
const numByMint = self.countByMint(mintUrl)
// If the transaction is not in history or recentByUnit, remove it from transactionMap
if (!isInHistory && !isInRecentByUnit) {
self.transactionsMap.delete(transactionId as string)
log.trace(`[pruneTransactionsMap] Transaction ${transactionId} pruned from the map`)
} else {
log.trace(`[pruneTransactionsMap] Transaction ${transactionId} kept in the map`)
}
})
},*/
pruneRecentByUnit(unit: MintUnit) {
const unitCount = self.countRecentByUnit(unit)
if (numByMint > maxTransactionsByMint) {
const transactionsToRemove = self.getByMint(mintUrl)
.slice()
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
.slice(maxTransactionsByMint)
transactionsToRemove.map((t) => {
detach(t)
})
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.transactions.replace(self.transactions.filter(t => !transactionsToRemove.some(removed => removed.id === t.id)))
self.recentByUnit.replace(self.recentByUnit.filter(t => !transactionsToRemove.some(removed => removed.id === t.id)))
const txByMintAfterDelete = self.countByMint(mintUrl)
const txTotalAfterDelete = self.count
log.trace('[removeOldByMint]', {txByMintAfterDelete, txTotalAfterDelete})
log.trace('[pruneRecentByUnit]', `${transactionsToRemove.length} pruned from recentByUnit`)
}
},
removeAllWithoutCurrentMint: () => {
pruneRecentWithoutCurrentMint() {
const rootStore = getRootStore(self)
const {mintsStore} = rootStore
const transactionsToRemove = self.transactions.filter(transaction => {
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.transactions.replace(self.transactions.filter(t => !transactionsToRemove.some(removed => removed.id === t.id)))
self.recentByUnit.replace(self.recentByUnit.filter(t => !transactionsToRemove.some(removed => removed.id === t.id)))
const txTotalAfterDelete = self.count
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)
log.trace('[removeAllWithoutCurrentMint]', {deleted: transactionsToRemove.length, txTotalAfterDelete})
}
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: TransactionRecord = yield Database.addTransactionAsync(newTransaction)
const dbTransaction: TransactionRecord = yield Database.addTransactionAsync(newTransaction)
// Add the new transaction to the transactions store
const createdAt = new Date(dbTransaction.createdAt)
const createdAt = new Date(dbTransaction.createdAt)
const inStoreTransaction = {...dbTransaction, createdAt}
const {id} = dbTransaction
const transactionInstance = TransactionModel.create(inStoreTransaction)
self.transactions.push(transactionInstance)
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.debug('[addTransaction]', 'New transaction added to the TransactionsStore')
// Purge the oldest transaction from cache, but keep some for each mint
self.removeOldByMint(newTransaction.mint)
// Purge the oldest references from cache, but keep some for each mint
self.pruneRecentByUnit(newTransaction.unit)
self.pruneHistory()
return transactionInstance as Transaction
return reference as Transaction
}),
addTransactionsToModel: (dbTransactions: TransactionRecord[]) => {
// This adds to model only. Used to have observable UI in tx history loaded from database.
const inStoreTransactions: Transaction[] = []
addToHistory(limit: number, offset: number, onlyPending: boolean){
// Appends transaction to the map and adds reference to history from database.
const result = Database.getTransactions(limit, offset, onlyPending)
log.trace('[addToHistory] dbResult ids', {ids: result?._array.map(t => t.id)})
for (const dbTransaction of dbTransactions) {
const createdAt = new Date(dbTransaction.createdAt)
const inStoreTransaction = {...dbTransaction, createdAt}
if (result && result.length > 0) {
for (const dbTransaction of result._array) {
const createdAt = new Date(dbTransaction.createdAt)
const inStoreTransaction = {...dbTransaction, createdAt}
const {id} = dbTransaction
if(self.findById(inStoreTransaction.id as number)) {
log.trace('[addTransactionsToModel] Transaction already exists in the model, skipping...')
continue
}
const transactionInstance = TransactionModel.create(inStoreTransaction)
inStoreTransactions.push(transactionInstance as Transaction)
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 dbTransactions = Database.getRecentTransactionsByUnit(maxTransactionsByUnit)
self.transactions.push(...inStoreTransactions)
log.debug('[addTransactionsToModel]', `${inStoreTransactions.length} new transactions added to TransactionsStore`)
if (dbTransactions && dbTransactions.length > 0) {
for (const dbTransaction of dbTransactions) {
const createdAt = new Date(dbTransaction.createdAt)
const inStoreTransaction = {...dbTransaction, createdAt} as Transaction
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[],
@@ -226,7 +255,7 @@ export const TransactionsStoreModel = types
// Update the model status and amend related tx data
for (const id of ids) {
const transactionInstance = self.findById(id)
const transactionInstance = self.transactionsMap.get(id)
if (transactionInstance) {
transactionInstance.status = status
@@ -235,36 +264,50 @@ export const TransactionsStoreModel = types
const updatedData = JSON.parse(transactionInstance.data)
updatedData.push(JSON.parse(data))
transactionInstance.data = JSON.stringify(updatedData)
}
log.debug('[updateStatuses]', 'Transaction statuses and data updated in TransactionsStore', {ids, status})
}
log.trace('[updateStatuses]', 'Transaction statuses and data updated in TransactionsStore', {ids, status})
}
}),
expireAllAfterRecovery: flow(function* expireAllAfterRecovery() {
// Update status in database
yield Database.expireAllAfterRecovery()
}),
deleteByStatus(status: TransactionStatus){
// Update the model statuses
for (const t of self.all) {
t.setIsExpired()
}
}),
deleteByStatus: (status: TransactionStatus) => {
for (const transaction of self.transactions) {
if(transaction.status === status) {
detach(transaction)
self.transactionsMap.forEach((transaction, transactionId) => {
if (transaction.status === status) {
self.transactionsMap.delete(transactionId as string)
}
}
self.transactions.replace(self.transactions.filter(t => t.status !== status))
})
return Database.deleteTransactionsByStatus(status)
},
removeAllTransactions() {
self.transactions.clear()
log.debug('[removeAllTransactions]', 'Removed all transactions from TransactionsStore')
},
}))
}
})).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
})
const getHostname = function (mintUrl: string) {
try {
+14 -123
View File
@@ -55,14 +55,13 @@ export async function setupRootStore(rootStore: RootStore) {
// load the last known state from storage
const start = performance.now()
restoredState = MMKVStorage.load(ROOT_STORAGE_KEY) || {}
const mmkvLoaded = performance.now()
restoredState = MMKVStorage.load(ROOT_STORAGE_KEY) || {}
const mmkvLoaded = performance.now()
const dataSize = Buffer.byteLength(JSON.stringify(restoredState), 'utf8')
log.trace('[setupRootStore]', `Loading ${dataSize.toLocaleString()} bytes of state from MMKV took ${(mmkvLoaded - start).toLocaleString()} ms.`)
log.trace({restoredState})
log.trace('[setupRootStore]', `Loading ${dataSize.toLocaleString()} bytes of state from MMKV took ${(mmkvLoaded - start).toLocaleString()} ms.`)
// log.trace({restoredState})
applySnapshot(rootStore, restoredState)
const stateHydrated = performance.now()
@@ -129,123 +128,7 @@ async function _runMigrations(rootStore: RootStore) {
let currentVersion = rootStore.version
try {
if(currentVersion < 6) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v6`)
userSettingsStore.setLogLevel(LogLevel.ERROR)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
if(currentVersion < 7) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v7`)
for (const mint of mintsStore.allMints) {
mint.setStatus(MintStatus.ONLINE)
}
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
if(currentVersion < 8) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v8`)
const seedHash = await KeyChain.loadSeedHash()
if(seedHash && walletProfileStore.pubkey) {
await MinibitsClient.migrateSeedHash(
walletProfileStore.pubkey,
{
seedHash
}
)
walletProfileStore.setSeedHash(seedHash)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
}
if(currentVersion < 9) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v9`)
for (const mint of mintsStore.allMints) {
try {
await mint.setShortname()
} catch (e: any) {
continue
}
}
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
if(currentVersion < 10) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v10`)
for (const contact of contactsStore.all) {
if(contact.isExternalDomain === false) {
contact.setLud16(contact.nip05 as string)
}
}
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
rootStore.setVersion(rootStoreModelVersion)
}
if(currentVersion < 11) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v11`)
// userSettingsStore.setIsStorageMigrated(true)
rootStore.setVersion(rootStoreModelVersion)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
}
if(currentVersion < 12) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v12`)
for (const mint of mintsStore.allMints) {
try {
mint.addUnit('sat')
mint.resetCounters()
} catch (e: any) {
continue
}
}
rootStore.setVersion(rootStoreModelVersion)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
}
if(currentVersion < 13) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v13`)
userSettingsStore.setPreferredUnit('sat')
for (const proof of proofsStore.allProofs) {
try {
if(!proof.unit) {
proof.setUnit('sat')
}
} catch (e: any) {
continue
}
}
for (const tx of transactionsStore.all) {
try {
if(!tx.unit) {
tx.setUnit('sat')
}
} catch (e: any) {
continue
}
}
rootStore.setVersion(rootStoreModelVersion)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
}
try {
if(currentVersion < 14) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v14`)
@@ -295,7 +178,7 @@ async function _runMigrations(rootStore: RootStore) {
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
}
if(currentVersion < 28) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v27`)
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v28`)
userSettingsStore.setExchangeCurrency(CurrencyCode.USD)
userSettingsStore.setTheme(ThemeCode.DEFAULT)
@@ -315,6 +198,14 @@ async function _runMigrations(rootStore: RootStore) {
}
}
rootStore.setVersion(rootStoreModelVersion)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
}
if(currentVersion < 29) {
log.trace(`Starting rootStore migrations from version v${currentVersion} -> v29`)
transactionsStore.addRecentByUnit()
rootStore.setVersion(rootStoreModelVersion)
log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`)
}
+24 -16
View File
@@ -32,12 +32,12 @@ import {translate} from '../i18n'
import AppError from '../utils/AppError'
import {Database, KeyChain, NostrClient, log} from '../services'
import {MMKVStorage} from '../services'
import {maxTransactionsInModel} from '../models/TransactionsStore'
import { LogLevel } from '../services/log/logTypes'
import { getSnapshot } from 'mobx-state-tree'
import { delay } from '../utils/delay'
import RNExitApp from 'react-native-exit-app'
import { TransactionStatus } from '../models/Transaction'
import { maxTransactionsInHistory } from '../models/TransactionsStore'
// refresh
@@ -77,11 +77,11 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
}, [])
// Reset of transaction model state and reload from DB
const syncTransactionsFromDb = async function () {
const syncTransactionsFromDb = function () {
setIsLoading(true)
try {
const result = await Database.getTransactionsAsync(
maxTransactionsInModel,
const result = Database.getTransactions(
maxTransactionsInHistory,
0,
)
@@ -89,8 +89,10 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
// remove all from the transactionsStore model
transactionsStore.removeAllTransactions()
// Add last 10 from database
transactionsStore.addTransactionsToModel(result._array)
// Add last 10 to history
transactionsStore.addToHistory(maxTransactionsInHistory, 0, false)
// Add recent by unit
transactionsStore.addRecentByUnit()
setIsLoading(false)
setInfo(translate('resetCompletedDetail', { transCount: result.length }))
@@ -131,7 +133,7 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
proofsStore.removeProofs(pending, true, false)
}
await syncTransactionsFromDb()
syncTransactionsFromDb()
setIsLoading(false)
setInfo(`Removed pending transactions from the database and ${pendingCount} proofs from the wallet state`)
@@ -142,8 +144,7 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
},
},
],
)
)
}
@@ -254,8 +255,7 @@ Sentry id: ${userSettingsStore.userSettings.walletId}
subText={userSettingsStore.logLevel.toUpperCase()}
leftIcon='faListUl'
leftIconColor={colors.palette.iconMagenta200}
leftIconInverse={true}
RightComponent={<View style={$rightContainer} />}
leftIconInverse={true}
style={$item}
bottomSeparator={true}
onPress={toggleLogLevelSelector}
@@ -263,14 +263,22 @@ Sentry id: ${userSettingsStore.userSettings.walletId}
<ListItem
tx="showOnboarding"
subTx="showOnboardingDesc"
leftIcon='faRotate'
leftIcon='faInfoCircle'
leftIconColor={colors.light.tint}
leftIconInverse={true}
RightComponent={<View style={$rightContainer} />}
style={$item}
leftIconInverse={true}
style={$item}
bottomSeparator={true}
onPress={() => userSettingsStore.setIsOnboarded(false)}
/>
<ListItem
text="Resync transactions"
subText="Refresh recent transactions history from database."
leftIcon='faRotate'
leftIconColor={colors.palette.blue200}
leftIconInverse={true}
style={$item}
onPress={syncTransactionsFromDb}
/>
</>
}
/>
-3
View File
@@ -75,9 +75,6 @@ export const TranDetailScreen: FC<TransactionsStackScreenProps<'TranDetail'>> =
const noteInputRef = useRef<TextInput>(null)
const [transaction, setTransaction] = useState<Transaction>()
/* const [proofsByStatus, setProofsByStatus] = useState<
ProofsByStatus | undefined
>(undefined)*/
const [error, setError] = useState<AppError | undefined>()
const [isNoteEditing, setIsNoteEditing] = useState(transaction?.noteToSelf ? false : true)
const [isDataParsable, setIsDataParsable] = useState<boolean>(true)
+89 -121
View File
@@ -27,24 +27,24 @@ import {
import {TransactionsStackScreenProps} from '../navigation'
import {useHeader} from '../utils/useHeader'
import {useStores} from '../models'
import {GroupedByTimeAgo, maxTransactionsInModel} from '../models/TransactionsStore'
import {Database, log} from '../services'
import AppError from '../utils/AppError'
import {TransactionListItem} from './Transactions/TransactionListItem'
import { Transaction, TransactionStatus } from '../models/Transaction'
import { height } from '@fortawesome/free-solid-svg-icons/faWallet'
import { translate } from '../i18n'
import { maxTransactionsInHistory } from '../models/TransactionsStore'
if (Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true)
}
// Number of transactions held in TransactionsStore model
const limit = maxTransactionsInModel
const limit = maxTransactionsInHistory
export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>> = observer(function TranHistoryScreen(_props) {
const {navigation} = _props
const {transactionsStore, proofsStore, mintsStore} = useStores()
const {transactionsStore, mintsStore} = useStores()
useHeader({
leftIcon: 'faArrowLeft',
onLeftPress: () => navigation.goBack(),
@@ -58,9 +58,7 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
const [isLoading, setIsLoading] = useState(false)
const [isHeaderVisible, setIsHeaderVisible] = useState(true)
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false)
const [offset, setOffset] = useState<number>(transactionsStore.count) // load from db those that are not already displayed
const [pendingOffset, setPendingOffset] = useState<number>(transactionsStore.pending.length) // load from db those that are not already displayed
const [dbCount, setDbCount] = useState<number>(0)
const [totalDbCount, setTotalDbCount] = useState<number>(0)
const [pendingDbCount, setPendingDbCount] = useState<number>(0)
const [expiredDbCount, setExpiredDbCount] = useState<number>(0)
const [erroredDbCount, setErroredDbCount] = useState<number>(0)
@@ -71,106 +69,111 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
useEffect(() => {
const init = async () => {
setIsLoading(true)
const count = Database.getTransactionsCount() // all
const pendingCount = Database.getTransactionsCount(TransactionStatus.PENDING)
const expiredCount = Database.getTransactionsCount(TransactionStatus.EXPIRED)
const erroredCount = Database.getTransactionsCount(TransactionStatus.ERROR)
const revertedCount = Database.getTransactionsCount(TransactionStatus.REVERTED)
const countByStatus = Database.getTransactionsCount()
log.trace('transaction counts', {count, pendingCount, erroredCount, revertedCount})
log.trace('Database transaction counts', {countByStatus})
setPendingDbCount(countByStatus[TransactionStatus.PENDING] || 0)
setExpiredDbCount(countByStatus[TransactionStatus.EXPIRED] || 0)
setErroredDbCount(countByStatus[TransactionStatus.ERROR] || 0)
setRevertedDbCount(countByStatus[TransactionStatus.REVERTED] || 0)
setTotalDbCount(countByStatus.total)
setDbCount(count)
setPendingDbCount(pendingCount)
setExpiredDbCount(expiredCount)
setErroredDbCount(erroredCount)
setRevertedDbCount(revertedCount)
// Preload transactions to model in case they are not there
if(countByStatus.total > 0) {
if(transactionsStore.historyCount === 0) {
transactionsStore.addToHistory(limit, 0, false)
}
if(transactionsStore.recentByUnit.length === 0) {
transactionsStore.addRecentByUnit()
}
}
setIsLoading(false)
if (count <= limit) {
log.trace('setAll true')
if (countByStatus.total <= limit) {
log.trace('[init] setAll true')
setIsAll(true)
}
if (pendingCount <= limit) {
/* if (countByStatus[TransactionStatus.PENDING] <= limit) {
log.trace('setPendingAll true')
setPendingIsAll(true)
}
// Run on component unmount (cleanup)
return () => {
/* When leaving screen we remove all transactions over maxTransactionsByMint
* from the transactionsStore that might have been sourced from sqlite db while browsing older records
*/
for (const mint of mintsStore.allMints) {
transactionsStore.removeOldByMint(mint.mintUrl)
transactionsStore.removeAllWithoutCurrentMint() // avoid that tx from deleted mints remain in model forever
}
}
}*/
}
init()
return () => {}
return () => {
if(showPendingOnly) {
// Full clean if filtered, next visit will reload from db
transactionsStore.removeAllHistory()
} else {
// Keep recent in history to load fast on next visit
transactionsStore.pruneHistory()
}
// general cleanup - avoid that tx from deleted mints remain in state forever
transactionsStore.pruneRecentWithoutCurrentMint()
}
}, [])
const toggleDeleteModal = () => {
setIsDeleteModalVisible(previousState => !previousState)
}
// TODO debug
const getTransactionsList = async function () {
const addTransactionsToList = function () {
setIsLoading(true)
try {
const result = await Database.getTransactionsAsync(limit, offset)
transactionsStore.addToHistory(limit, transactionsStore.historyCount, false)
if (result && result.length > 0) {
// Add new transaction to the transactions store so mobx refreshes UI
transactionsStore.addTransactionsToModel(result._array)
log.trace('[addTransactionsToList]', {
currentOffset: transactionsStore.historyCount,
totalDbCount
})
setOffset(offset + result.length)
log.trace({storeCount: transactionsStore.count, dbCount})
if (transactionsStore.count >= dbCount) {
log.trace('[getTransactionsList] setAll true')
setIsAll(true)
}
}
if (transactionsStore.historyCount >= totalDbCount) {
log.trace('[getTransactionsList] setAll true')
setIsAll(true)
}
setIsLoading(false)
} catch (e: any) {
handleError(e)
}
}
}
// TODO debug
const getPendingTransactionsList = async function () {
const addPendingTransactionsToList = function () {
setIsLoading(true)
try {
const result = await Database.getTransactionsAsync(limit, pendingOffset, true) // pending
transactionsStore.addToHistory(limit, transactionsStore.historyCount, true)
if (result && result.length > 0) {
// Add new transaction to the transactions store so that mobx refreshes UI
transactionsStore.addTransactionsToModel(result._array)
log.trace('[addTransactionsToList] onlyPending', {
currentOffset: transactionsStore.historyCount,
pendingDbCount
})
setOffset(pendingOffset + result.length)
if (transactionsStore.pending.length === pendingDbCount) {
log.trace('[getTransactionsList] setAll true')
setPendingIsAll(true)
}
}
if (transactionsStore.historyCount >= pendingDbCount) {
log.trace('[getTransactionsList] onlyPending setAll true')
setPendingIsAll(true)
}
setIsLoading(false)
} catch (e: any) {
handleError(e)
}
}
}
const toggleShowPendingOnly = async function () {
if (showPendingOnly) {
const toggleShowPendingOnly = function () {
if (showPendingOnly) {
transactionsStore.removeAllHistory()
addTransactionsToList()
setShowPendingOnly(false)
} else {
await getPendingTransactionsList()
transactionsStore.removeAllHistory()
addPendingTransactionsToList() // hydrate with onlyPending = true
setShowPendingOnly(true)
}
}
@@ -192,59 +195,24 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
}
const onDeleteExpired = function () {
const onDelete = function (status: TransactionStatus) {
try {
toggleDeleteModal()
setIsLoading(true)
transactionsStore.deleteByStatus(TransactionStatus.EXPIRED)
const count = Database.getTransactionsCount() // all
const expiredCount = Database.getTransactionsCount(TransactionStatus.EXPIRED)
setDbCount(count)
setExpiredDbCount(expiredCount)
transactionsStore.deleteByStatus(status)
const countByStatus = Database.getTransactionsCount()
setPendingDbCount(countByStatus[TransactionStatus.PENDING] || 0)
setExpiredDbCount(countByStatus[TransactionStatus.EXPIRED] || 0)
setErroredDbCount(countByStatus[TransactionStatus.ERROR] || 0)
setRevertedDbCount(countByStatus[TransactionStatus.REVERTED] || 0)
setTotalDbCount(countByStatus.total)
setIsLoading(false)
} catch (e: any) {
handleError(e)
}
}
const onDeleteErrored = function () {
try {
toggleDeleteModal()
setIsLoading(true)
transactionsStore.deleteByStatus(TransactionStatus.ERROR)
const count = Database.getTransactionsCount() // all
const erroredCount = Database.getTransactionsCount(TransactionStatus.ERROR)
setDbCount(count)
setErroredDbCount(erroredCount)
setIsLoading(false)
} catch (e: any) {
handleError(e)
}
}
const onDeleteReverted = function () {
try {
toggleDeleteModal()
setIsLoading(true)
transactionsStore.deleteByStatus(TransactionStatus.REVERTED)
const count = Database.getTransactionsCount() // all
const revertedCount = Database.getTransactionsCount(TransactionStatus.REVERTED)
setDbCount(count)
setRevertedDbCount(revertedCount)
setIsLoading(false)
} catch (e: any) {
handleError(e)
}
}
const handleError = function (e: AppError): void {
setIsLoading(false)
setError(e)
@@ -255,12 +223,12 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
const activeIconColor = useThemeColor('button')
const headerTitle = useThemeColor('headerTitle')
const sections = showPendingOnly ? Object.keys(transactionsStore.groupedPendingByTimeAgo).map((timeAgo) => ({
const sections = showPendingOnly ? Object.keys(transactionsStore.historyPendingByTimeAgo).map((timeAgo) => ({
title: timeAgo,
data: transactionsStore.groupedPendingByTimeAgo[timeAgo],
})) : Object.keys(transactionsStore.groupedByTimeAgo).map((timeAgo) => ({
data: transactionsStore.historyPendingByTimeAgo[timeAgo],
})) : Object.keys(transactionsStore.historyByTimeAgo).map((timeAgo) => ({
title: timeAgo,
data: transactionsStore.groupedByTimeAgo[timeAgo],
data: transactionsStore.historyByTimeAgo[timeAgo],
}))
return (
@@ -277,12 +245,12 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
<ListItem
text={showPendingOnly
? translate("tranHistory.showingPaginationPending", {
amount: transactionsStore.pending.length,
amount: transactionsStore.pendingHistoryCount,
total: pendingDbCount
})
: translate("tranHistory.showingPaginationTotal", {
amount: transactionsStore.count,
total: dbCount
amount: transactionsStore.historyCount,
total: totalDbCount
})
}
LeftComponent={
@@ -299,7 +267,7 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
/>
<ListItem
text={translate("tranHistory.pendingParam", {
param: transactionsStore.pending.length
param: transactionsStore.pendingHistoryCount
})}
LeftComponent={
<Icon
@@ -350,7 +318,7 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
) : (
<Button
preset="secondary"
onPress={getTransactionsList}
onPress={addTransactionsToList}
tx="tranHistory.viewMore"
style={{minHeight: 25, paddingVertical: spacing.tiny}}
textStyle={{fontSize: 14}}
@@ -389,7 +357,7 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
count: expiredDbCount
})}
leftIcon='faRotate'
onPress={onDeleteExpired}
onPress={() => onDelete(TransactionStatus.EXPIRED)}
bottomSeparator={true}
/>
<ListItem
@@ -398,7 +366,7 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
count: erroredDbCount
})}
leftIcon='faBug'
onPress={onDeleteErrored}
onPress={() => onDelete(TransactionStatus.ERROR)}
bottomSeparator={true}
/>
<ListItem
@@ -407,7 +375,7 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
count: revertedDbCount
})}
leftIcon='faBan'
onPress={onDeleteReverted}
onPress={() => onDelete(TransactionStatus.REVERTED)}
/>
</>
}
+5 -4
View File
@@ -146,8 +146,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
}
// On app start
useEffect(() => {
// get deeplink if any
useEffect(() => {
const getInitialData = async () => {
// get deeplink data if any
const url = await Linking.getInitialURL()
@@ -465,6 +464,8 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
const renderUnitTabs = function ({ route }: { route: { key: string } }) {
const unitMints = groupedMints.find((mintUnit) => mintUnit.unit === route.key)
// log.trace('[renderUnitTabs]', {recentByUnit: transactionsStore.getRecentByUnit(unitMints!.unit), unit: unitMints!.unit})
if (unitMints) {
return (
<>
@@ -487,11 +488,11 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
</Pressable>
</View>
<View style={$tabContainer}>
{transactionsStore.recentByUnit(unitMints.unit).length > 0 ? (
{transactionsStore.getRecentByUnit(unitMints.unit).length > 0 ? (
<Card
ContentComponent={
<FlatList
data={transactionsStore.recentByUnit(unitMints.unit) as Transaction[]}
data={transactionsStore.getRecentByUnit(unitMints.unit) as Transaction[]}
renderItem={({item, index}) => {
return (
<TransactionListItem
+136 -83
View File
@@ -16,7 +16,7 @@ import {log} from './logService'
import {LogLevel} from './log/logTypes'
import {BackupProof} from '../models/Proof'
import { CashuUtils } from './cashu/cashuUtils'
import { CurrencyCode } from './wallet/currency'
import { CurrencyCode, MintUnit } from './wallet/currency'
import { ThemeCode } from '../theme'
let _db: QuickSQLiteConnection
@@ -517,32 +517,141 @@ const updateUserSettings = function (settings: UserSettings): UserSettings {
* Transactions
*/
const getTransactionsCount = function (status?: TransactionStatus) {
let query: string = ''
let params
try {
query = `
SELECT COUNT(*) FROM transactions
`
const getTransactions = function (limit: number, offset: number, onlyPending: boolean = false) {
let query: string = ''
try {
query = `
SELECT *
FROM transactions
ORDER BY id DESC
LIMIT ? OFFSET ?
`
if(status) {
query = `
SELECT COUNT(*)
FROM transactions
WHERE status = ?
`
params = [status]
}
if(onlyPending) {
query = `
SELECT *
FROM transactions
WHERE status = 'PENDING'
ORDER BY id DESC
LIMIT ? OFFSET ?
`
}
const db = getInstance()
const {rows} = db.execute(query, params)
return rows?.item(0)['COUNT(*)'] as number
} catch (e: any) {
throw new AppError(Err.DATABASE_ERROR, 'Transaction count error', e.message)
}
const params = [limit, offset]
// log.trace(query, params)
const db = getInstance()
const {rows} = db.execute(query, params)
log.trace(`[getTransactions], Returned ${rows?.length} rows`)
return rows
} 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 getRecentTransactionsByUnit = (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 } = db.execute(query, params)
return rows?._array
} 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 = `
@@ -1002,63 +1111,6 @@ const updateZapRequest = function (id: number, zapRequest: string) {
}
} */
const getTransactionsAsync = async function (limit: number, offset: number, isPending: boolean = false) {
let query: string = ''
try {
query = `
SELECT *
FROM transactions
ORDER BY id DESC
LIMIT ? OFFSET ?
`
if(isPending) {
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', rows?.length)
return rows
} catch (e: any) {
throw new AppError(
Err.DATABASE_ERROR,
'Transactions could not be retrieved from the database',
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 deleteTransactionsByStatus = function (status: TransactionStatus) {
try {
@@ -1381,7 +1433,9 @@ export const Database = {
updateUserSettings,
getTransactionsCount,
getTransactionById,
addTransactionAsync,
getRecentTransactionsByUnit,
getTransactions,
addTransactionAsync,
updateStatus,
expireAllAfterRecovery,
updateStatusesAsync,
@@ -1395,8 +1449,7 @@ export const Database = {
updateZapRequest,
updateInputToken,
updateOutputToken,
updateProof,
getTransactionsAsync,
updateProof,
deleteTransactionsByStatus,
getPendingAmount,
addOrUpdateProof,