mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-08-12 01:17:43 +00:00
337 lines
12 KiB
TypeScript
337 lines
12 KiB
TypeScript
import {
|
|
Instance,
|
|
SnapshotOut,
|
|
types,
|
|
flow,
|
|
getSnapshot,
|
|
} from 'mobx-state-tree'
|
|
import { withSetPropAction } from './helpers/withSetPropAction'
|
|
import {
|
|
TransactionModel,
|
|
Transaction,
|
|
TransactionStatus,
|
|
TransactionType,
|
|
} 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(): GroupedByTimeAgo {
|
|
return self.history
|
|
.slice()
|
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
.reduce((groups, tx) => {
|
|
const key = formatDistance(tx.createdAt, new Date(), { addSuffix: true })
|
|
;(groups[key] ??= []).push(tx)
|
|
return groups
|
|
}, {} as GroupedByTimeAgo)
|
|
},
|
|
|
|
get historyPendingByTimeAgo(): GroupedByTimeAgo {
|
|
return this.pendingHistory.reduce((groups, tx) => {
|
|
const key = formatDistance(tx.createdAt, new Date(), { addSuffix: true })
|
|
;(groups[key] ??= []).push(tx)
|
|
return groups
|
|
}, {} as GroupedByTimeAgo)
|
|
},
|
|
|
|
get historyCount() { return self.history.length },
|
|
get pendingHistoryCount() { return this.pendingHistory.length },
|
|
|
|
getRecentByUnit(unit: MintUnit): Transaction[] {
|
|
return self.recentByUnit
|
|
.slice()
|
|
.filter(t => t.unit === unit)
|
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
},
|
|
|
|
countRecentByUnit(unit: MintUnit): number {
|
|
return this.getRecentByUnit(unit).length
|
|
},
|
|
|
|
getPendingTopups(): Transaction[] {
|
|
const dbTopups = Database.getPendingTopups()
|
|
return dbTopups.map(t => TransactionModel.create({ ...t }))
|
|
},
|
|
|
|
getPendingTransfers(): Transaction[] {
|
|
const dbTransfers = Database.getPendingTransfers()
|
|
return dbTransfers.map(t => TransactionModel.create({ ...t }))
|
|
},
|
|
}))
|
|
|
|
|
|
.actions(self => ({
|
|
findById(id: number, loadTokens = false): Transaction | undefined {
|
|
let tx = self.transactionsMap.get(String(id))
|
|
|
|
if (!tx || loadTokens) {
|
|
const dbTx = Database.getTransactionById(id)
|
|
if (!dbTx) return undefined
|
|
|
|
const inStoreTx = { ...dbTx }
|
|
if (!loadTokens) {
|
|
inStoreTx.inputToken = inStoreTx.inputToken?.slice(0, 40) || ''
|
|
inStoreTx.outputToken = inStoreTx.outputToken?.slice(0, 40) || ''
|
|
}
|
|
|
|
self.transactionsMap.set(String(id), inStoreTx)
|
|
tx = self.transactionsMap.get(String(id))
|
|
}
|
|
return tx
|
|
},
|
|
|
|
findLastBy(criteria: { paymentId?: string; quote?: string; paymentRequest?: string }): Transaction | undefined {
|
|
let dbTx: Transaction
|
|
try {
|
|
dbTx = Database.getLastTransactionBy(criteria)
|
|
} catch {
|
|
return undefined
|
|
}
|
|
|
|
const inStoreTx = {
|
|
...dbTx,
|
|
inputToken: dbTx.inputToken?.slice(0, 40) || '',
|
|
outputToken: dbTx.outputToken?.slice(0, 40) || '',
|
|
}
|
|
|
|
self.transactionsMap.set(String(dbTx.id), inStoreTx)
|
|
return self.transactionsMap.get(String(dbTx.id))
|
|
},
|
|
|
|
pruneHistory() {
|
|
if (self.history.length <= maxTransactionsInHistory) return
|
|
|
|
const keep = self.history
|
|
.slice()
|
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
.slice(0, maxTransactionsInHistory)
|
|
|
|
self.history.replace(keep)
|
|
log.trace('[pruneHistory]', `${self.history.length} kept in history`)
|
|
},
|
|
|
|
pruneRecentByUnit(unit: MintUnit) {
|
|
const recent = self.getRecentByUnit(unit)
|
|
if (recent.length <= maxTransactionsByUnit) return
|
|
|
|
const keepIds = new Set(recent.slice(0, maxTransactionsByUnit).map(t => t.id))
|
|
const newList = self.recentByUnit.filter(t => t.unit !== unit || keepIds.has(t.id))
|
|
|
|
self.recentByUnit.replace(newList)
|
|
log.trace('[pruneRecentByUnit]', `${recent.length - keepIds.size} pruned for unit ${unit}`)
|
|
},
|
|
|
|
pruneRecentWithoutCurrentMint() {
|
|
const { mintsStore } = getRootStore(self)
|
|
const validUrls = new Set(mintsStore.allMints.map(m => m.mintUrl))
|
|
|
|
const newList = self.recentByUnit.filter(tx => validUrls.has(tx.mint))
|
|
if (newList.length !== self.recentByUnit.length) {
|
|
self.recentByUnit.replace(newList)
|
|
log.trace('[pruneRecentWithoutCurrentMint]', `${self.recentByUnit.length - newList.length} removed`)
|
|
}
|
|
},
|
|
|
|
removeAllHistory() { self.history.clear() },
|
|
removeAllRecentByUnit() { self.recentByUnit.clear() },
|
|
removeAllTransactions() {
|
|
self.history.clear()
|
|
self.recentByUnit.clear()
|
|
self.transactionsMap.clear()
|
|
},
|
|
}))
|
|
|
|
.actions(self => ({
|
|
// ── Main add (push + safe prune) ──
|
|
addTransaction: flow(function* addTransaction(newTxData: Partial<Transaction>) {
|
|
const dbTx: Transaction = yield Database.addTransactionAsync(newTxData)
|
|
const tx = TransactionModel.create(dbTx)
|
|
|
|
self.transactionsMap.set(String(dbTx.id), tx)
|
|
self.history.push(tx)
|
|
self.recentByUnit.push(tx)
|
|
|
|
self.pruneHistory()
|
|
self.pruneRecentByUnit(dbTx.unit)
|
|
|
|
return tx
|
|
}),
|
|
|
|
// ── Lazy load more history ──
|
|
addToHistory: flow(function* addToHistory(limit: number, offset: number, onlyPending: boolean) {
|
|
// Appends transaction to the map and adds reference to history from database.
|
|
const transactions = yield Database.getTransactionsAsync(limit, offset, onlyPending)
|
|
|
|
for (const dbTx of transactions) {
|
|
const { id } = dbTx
|
|
|
|
if (!self.transactionsMap.has(id)) {
|
|
self.transactionsMap.set(id, {
|
|
...dbTx,
|
|
inputToken: dbTx.inputToken?.slice(0, 40) || '',
|
|
outputToken: dbTx.outputToken?.slice(0, 40) || '',
|
|
})
|
|
log.trace('[addToHistory]', `${id} added to transactionsMap`)
|
|
}
|
|
|
|
const ref = self.transactionsMap.get(id)
|
|
|
|
if (!self.history.find(t => t.id === id)) {
|
|
if(ref) self.history.push(ref)
|
|
}
|
|
}
|
|
|
|
}),
|
|
|
|
// ── Search-driven history page load ──
|
|
searchHistory: flow(function* searchHistory(
|
|
term: string,
|
|
filters: {amount: boolean; incoming: boolean; outgoing: boolean; pending: boolean},
|
|
limit: number,
|
|
offset: number,
|
|
) {
|
|
const transactions = yield Database.searchTransactionsAsync(term, filters, limit, offset)
|
|
|
|
for (const dbTx of transactions) {
|
|
const { id } = dbTx
|
|
|
|
if (!self.transactionsMap.has(id)) {
|
|
self.transactionsMap.set(id, {
|
|
...dbTx,
|
|
inputToken: dbTx.inputToken?.slice(0, 40) || '',
|
|
outputToken: dbTx.outputToken?.slice(0, 40) || '',
|
|
})
|
|
}
|
|
|
|
const ref = self.transactionsMap.get(id)
|
|
|
|
if (!self.history.find(t => t.id === id)) {
|
|
if(ref) self.history.push(ref)
|
|
}
|
|
}
|
|
}),
|
|
|
|
// ── Rehydrate recent on app start ──
|
|
addRecentByUnit: flow(function* addRecentByUnit() {
|
|
const dbTxs = yield Database.getRecentTransactionsByUnitAsync(maxTransactionsByUnit)
|
|
if (!dbTxs?.length) return
|
|
|
|
for (const dbTx of dbTxs) {
|
|
if (self.transactionsMap.has(String(dbTx.id))) continue
|
|
|
|
const tx = TransactionModel.create({
|
|
...dbTx,
|
|
inputToken: dbTx.inputToken?.slice(0, 40) || '',
|
|
outputToken: dbTx.outputToken?.slice(0, 40) || '',
|
|
})
|
|
|
|
self.transactionsMap.set(String(dbTx.id), tx)
|
|
if (!self.recentByUnit.some(t => t.id === dbTx.id)) {
|
|
self.recentByUnit.push(tx)
|
|
}
|
|
}
|
|
}),
|
|
|
|
updateStatuses: flow(function* updateStatuses(ids: number[], status: TransactionStatus, data: string) {
|
|
yield Database.updateStatusesAsync(ids, status, data)
|
|
|
|
for (const id of ids) {
|
|
const tx = self.transactionsMap.get(String(id))
|
|
if (tx) {
|
|
tx.status = status
|
|
const parsed = JSON.parse(tx.data || '[]')
|
|
parsed.push(JSON.parse(data))
|
|
tx.data = JSON.stringify(parsed)
|
|
}
|
|
}
|
|
}),
|
|
|
|
deleteByStatus(status: TransactionStatus) {
|
|
const toDelete: number[] = []
|
|
|
|
self.transactionsMap.forEach((tx, key) => {
|
|
if (tx.status === status) {
|
|
toDelete.push(tx.id)
|
|
self.transactionsMap.delete(key as string)
|
|
}
|
|
})
|
|
|
|
if (toDelete.length > 0) {
|
|
self.history.replace(self.history.filter(t => !toDelete.includes(t.id)))
|
|
self.recentByUnit.replace(self.recentByUnit.filter(t => !toDelete.includes(t.id)))
|
|
}
|
|
|
|
return Database.deleteTransactionsByStatus(status)
|
|
},
|
|
|
|
deleteIncomingPending() {
|
|
const toDelete: number[] = []
|
|
|
|
self.transactionsMap.forEach((tx, key) => {
|
|
const isPendingIncoming = tx.status === TransactionStatus.PENDING &&
|
|
(tx.type === TransactionType.RECEIVE || tx.type === TransactionType.RECEIVE_BY_PAYMENT_REQUEST)
|
|
|
|
if (isPendingIncoming) {
|
|
toDelete.push(tx.id)
|
|
self.transactionsMap.delete(key as string)
|
|
}
|
|
})
|
|
|
|
if (toDelete.length > 0) {
|
|
self.history.replace(self.history.filter(t => !toDelete.includes(t.id)))
|
|
self.recentByUnit.replace(self.recentByUnit.filter(t => !toDelete.includes(t.id)))
|
|
}
|
|
|
|
return Database.deleteIncomingPending()
|
|
},
|
|
|
|
}))
|
|
|
|
.actions(self => ({
|
|
|
|
loadRecentFromDatabase: flow(function* loadRecentFromDatabase() {
|
|
if (self.history.length > 0 || self.recentByUnit.length > 0) return // already loaded
|
|
|
|
yield self.addRecentByUnit() // wallet screen
|
|
yield self.addToHistory(maxTransactionsInHistory, 0, false) // tx screen
|
|
|
|
log.trace('[TransactionsStore] Rehydrated from database')
|
|
}),
|
|
|
|
}))
|
|
|
|
.postProcessSnapshot(() => ({
|
|
transactionsMap: {},
|
|
history: [],
|
|
recentByUnit: [],
|
|
}))
|
|
|
|
export interface TransactionsStore extends Instance<typeof TransactionsStoreModel> {}
|
|
export interface TransactionsStoreSnapshot extends SnapshotOut<typeof TransactionsStoreModel> {} |