Handling pending transactions

This commit is contained in:
minibits-cash
2025-06-21 00:47:52 +02:00
parent 73b1986ce1
commit d03a17e6ff
15 changed files with 319 additions and 256 deletions
+2
View File
@@ -513,6 +513,8 @@
"from": "From %{sender}",
"paidFrom": "Paid from",
"paidTo": "Paid to %{receiver}",
"payTo": "Pay to %{receiver}",
"payNow": "Pay now",
"receivedFrom": "Received from %{sender}",
"receivedTo": "Received to",
"receiveFailed": "Receive failed",
+2 -2
View File
@@ -3,10 +3,10 @@ import {withSetPropAction} from './helpers/withSetPropAction'
import { MintUnit } from '../services/wallet/currency'
/**
* Proof type saved in database backup
* Proof db record
*/
export type BackupProof = Proof & {
export type ProofRecord = Proof & {
isPending: boolean
isSpent: boolean
updatedAt: Date
+4 -4
View File
@@ -7,7 +7,7 @@ import {
flow,
} from 'mobx-state-tree'
import {withSetPropAction} from './helpers/withSetPropAction'
import {ProofModel, Proof, BackupProof} from './Proof'
import {ProofModel, Proof, ProofRecord} from './Proof'
import {log} from '../services/logService'
import {getRootStore} from './helpers/getRootStore'
import AppError, {Err} from '../utils/AppError'
@@ -110,7 +110,7 @@ export const ProofsStoreModel = types
// Group into isUnspent and isPending
const groupedProofs = unspentAndPendingProofs.reduce(
(acc: {isPending: BackupProof[], isUnspent: BackupProof[]}, proof: BackupProof) => {
(acc: {isPending: ProofRecord[], isUnspent: ProofRecord[]}, proof: ProofRecord) => {
if (proof.isPending) {
acc.isPending.push(proof);
} else {
@@ -121,7 +121,7 @@ export const ProofsStoreModel = types
{ isUnspent: [], isPending: [] } // Initialize the groups
)
const cleanProofs = (proofs: BackupProof[]) => {
const cleanProofs = (proofs: ProofRecord[]) => {
return proofs.map(p => {
// Destructure the unwanted properties and collect the rest in `cleaned`
const { isPending, isSpent, updatedAt, ...cleaned } = p
@@ -165,7 +165,7 @@ export const ProofsStoreModel = types
throw new AppError(Err.STORAGE_ERROR, 'Could not find mint', {mintUrl: newProofs[0].mintUrl})
}
for (const [keysetId, keysetProofs] of proofsByKeysetId.entries()) {
for (const [keysetId, keysetProofs] of Array.from(proofsByKeysetId.entries())) {
for (const proof of keysetProofs) {
if(self.alreadyExists(proof)) {
+5 -36
View File
@@ -3,37 +3,6 @@ import { log } from '../services/logService'
import { MintUnit } from '../services/wallet/currency'
import { Database } from '../services'
/**
* Transaction model with MST actions for database updates.
* Note: Individual setter actions (setStatus, setFee, etc.) are deprecated.
* Use the generic update() action instead for all mutations.
*/
export interface TransactionRecord {
id: number
type: TransactionType
amount: number
fee: number
unit: MintUnit
data: string
sentFrom?: string | null
sentTo?: string | null
profile?: string | null
memo?: string | null
paymentId?: string | null
quote?: string | null
paymentRequest?: string | null
zapRequest?: string | null
inputToken?: string | null
outputToken?: string | null
proof?: string | null
mint: string
balanceAfter?: number | null
noteToSelf?: string | null
tags?: Array<string> | null
status: TransactionStatus
expiresAt?: Date | null
createdAt: Date
}
export type TransactionData = {
status: TransactionStatus
@@ -100,16 +69,16 @@ export const TransactionModel = types
self.outputToken = outputToken.slice(0, 40)
log.trace('[pruneOutputToken]', 'Transaction outputToken pruned in store', { id: self.id })
},
update(fields: Partial<TransactionRecord>) {
update(fields: Partial<Transaction>) {
// Update multiple fields in database with a single query
Database.updateTransaction(self.id!, fields)
const updatedTransaction = Database.updateTransaction(self.id, fields)
// Update the model to keep store in sync
Object.keys(fields).forEach(key => {
;(self as any)[key] = (fields as any)[key]
Object.keys(updatedTransaction).forEach(key => {
;(self as any)[key] = (updatedTransaction as any)[key]
})
log.trace('[update]', 'Transaction updated', { id: self.id, fields: Object.keys(fields) })
log.trace('[update]', 'Transaction updated', { id: self.id, updatedData: fields})
},
}))
+39 -45
View File
@@ -3,14 +3,12 @@ import {
SnapshotOut,
types,
flow,
detach,
} from 'mobx-state-tree'
import {withSetPropAction} from './helpers/withSetPropAction'
import {
TransactionModel,
Transaction,
TransactionStatus,
TransactionRecord,
} from './Transaction'
import {Database} from '../services'
import {log} from '../services/logService'
@@ -76,16 +74,14 @@ import {
.filter(t => t.unit === unit)
},
getPendingTopups() {
const dbTopups: TransactionRecord[] = Database.getPendingTopups()
const dbTopups: Transaction[] = Database.getPendingTopups()
if(dbTopups.length > 0) {
const pendingTopups: Transaction[] = []
for(const topup of dbTopups) {
const createdAt = new Date(topup.createdAt)
const expiresAt = topup.expiresAt ? new Date(topup.expiresAt) : null
const inStoreTransaction = {...topup, createdAt, expiresAt}
const inStoreTransaction = {...topup}
const topupInstance = TransactionModel.create(inStoreTransaction)
pendingTopups.push(topupInstance)
@@ -98,8 +94,26 @@ import {
return []
},
getPendingTopupsCount() {
return Database.getPendingTopupsCount()
getPendingTransfers() {
const dbTransfers: Transaction[] = Database.getPendingTopups()
if(dbTransfers.length > 0) {
const pendingTransfers: Transaction[] = []
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
@@ -115,9 +129,7 @@ import {
const dbTransaction = Database.getTransactionById(id)
if(dbTransaction) {
const createdAt = new Date(dbTransaction.createdAt)
const expiresAt = dbTransaction.expiresAt ? new Date(dbTransaction.expiresAt) : null
const inStoreTransaction = {...dbTransaction, createdAt}
const inStoreTransaction = {...dbTransaction}
const {id} = dbTransaction
if(!loadTokens) {
@@ -143,9 +155,7 @@ import {
const dbTransaction = Database.getTransactionByPaymentId(paymentId)
if(dbTransaction) {
const createdAt = new Date(dbTransaction.createdAt)
const expiresAt = dbTransaction.expiresAt ? new Date(dbTransaction.expiresAt) : null
const inStoreTransaction = {...dbTransaction, createdAt, expiresAt}
const inStoreTransaction = {...dbTransaction}
const {id} = dbTransaction
// Shorten for performance reasons
@@ -169,9 +179,7 @@ import {
const dbTransaction = Database.getTransactionByQuote(quote)
if(dbTransaction) {
const createdAt = new Date(dbTransaction.createdAt)
const expiresAt = dbTransaction.expiresAt ? new Date(dbTransaction.expiresAt) : null
const inStoreTransaction = {...dbTransaction, createdAt, expiresAt}
const inStoreTransaction = {...dbTransaction}
const {id} = dbTransaction
// Shorten for performance reasons
@@ -195,9 +203,7 @@ import {
const dbTransaction = Database.getTransactionByPaymentRequest(pr)
if(dbTransaction) {
const createdAt = new Date(dbTransaction.createdAt)
const expiresAt = dbTransaction.expiresAt ? new Date(dbTransaction.expiresAt) : null
const inStoreTransaction = {...dbTransaction, createdAt, expiresAt}
const inStoreTransaction = {...dbTransaction}
const {id} = dbTransaction
// Shorten for performance reasons
@@ -276,11 +282,10 @@ import {
.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: Transaction = yield Database.addTransactionAsync(newTransaction)
// Add the new transaction to the transactions store
const createdAt = new Date(dbTransaction.createdAt)
const inStoreTransaction = {...dbTransaction, createdAt}
const inStoreTransaction = {...dbTransaction}
const {id} = dbTransaction
if (!self.transactionsMap.has(id)) {
@@ -301,14 +306,12 @@ import {
}),
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)})
const transactions = Database.getTransactions(limit, offset, onlyPending)
log.trace('[addToHistory] transactions ids', {ids: transactions ? transactions.map(t => t.id) : []})
if (result && result.length > 0) {
for (const dbTransaction of result._array) {
const createdAt = new Date(dbTransaction.createdAt)
const expiresAt = dbTransaction.expiresAt ? new Date(dbTransaction.expiresAt) : null
const inStoreTransaction = {...dbTransaction, createdAt, expiresAt}
if (transactions && transactions.length > 0) {
for (const dbTransaction of transactions) {
const inStoreTransaction = {...dbTransaction}
// Shorten for performance reasons
if(inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) {
@@ -337,13 +340,11 @@ import {
},
addRecentByUnit() {
// Rehydrates recent from database.
const dbTransactions = Database.getRecentTransactionsByUnit(maxTransactionsByUnit)
const transactions = Database.getRecentTransactionsByUnit(maxTransactionsByUnit)
if (dbTransactions && dbTransactions.length > 0) {
for (const dbTransaction of dbTransactions) {
const createdAt = new Date(dbTransaction.createdAt)
const expiresAt = dbTransaction.expiresAt ? new Date(dbTransaction.expiresAt) : null
const inStoreTransaction = {...dbTransaction, createdAt, expiresAt}
if (transactions && transactions.length > 0) {
for (const dbTransaction of transactions) {
const inStoreTransaction = {...dbTransaction}
// Shorten for performance reasons
if(inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) {
@@ -431,17 +432,10 @@ import {
return prunedSnapshot
})
const getHostname = function (mintUrl: string) {
try {
return new URL(mintUrl).hostname
} catch (e) {
return false
}
}
// refresh
export interface TransactionsStore
extends Instance<typeof TransactionsStoreModel> {}
export interface TransactionsStoreSnapshot
extends SnapshotOut<typeof TransactionsStoreModel> {}
extends SnapshotOut<typeof TransactionsStoreModel> {}
+25 -11
View File
@@ -102,7 +102,7 @@ export const TranDetailScreen = observer(function TranDetailScreen({ route }: Pr
useFocusEffect(useCallback(() => {
try {
const tx = transactionsStore.findById(id, true) // load full tokens
log.trace('Transaction loaded', {id: tx?.id, unit: tx?.unit, note: tx?.noteToSelf, inputToken: tx?.inputToken, outputToken: tx?.inputToken})
log.trace('Transaction loaded', {tx})
if (!tx) {
throw new AppError(
@@ -1385,10 +1385,12 @@ const TopupInfoBlock = function (props: {
useFocusEffect(useCallback(() => {
log.trace('[TopupInfoBlock] useFocusEffect start')
if (transaction.status === TransactionStatus.ERROR ||
transaction.status === TransactionStatus.PENDING ||
transaction.status === TransactionStatus.EXPIRED) {
setIsPendingTopupRetriable(true)
if ([
TransactionStatus.ERROR,
TransactionStatus.PENDING,
TransactionStatus.EXPIRED
].includes(transaction.status)) {
setIsPendingTopupRetriable(true)
}
}, []))
@@ -1530,7 +1532,7 @@ const TopupInfoBlock = function (props: {
label="tranDetailScreen.createdAt"
value={(transaction.createdAt as Date).toLocaleString()}
/>
{transaction.expiresAt && (
{transaction.expiresAt && transaction.status !== TransactionStatus.COMPLETED && (
<TranItem
label="tranDetailScreen.expiresAt"
value={(new Date(transaction.expiresAt!)).toLocaleString()}
@@ -1675,11 +1677,19 @@ const TransferInfoBlock = function (props: {
// Pay invoice received over Nostr
const onPayDraftTransfer = async function () {
if(!transaction.paymentRequest || !transaction.unit || transaction.mint) {
log.trace('[onPayDraftTransfer] start', {tId: transaction.id})
const {paymentRequest, unit, mint} = transaction
if(!paymentRequest || !unit || !mint) {
log.error('[onPayDraftTransfer] Missing params', {paymentRequest, unit, mint})
setResultModalInfo({
status: TransactionStatus.ERROR,
message: 'This transaction is missing data needed to be paid.'
})
toggleResultModal()
return
}
@@ -1758,8 +1768,7 @@ const TransferInfoBlock = function (props: {
/>
<Button
style={{marginTop: spacing.medium}}
preset="secondary"
text="Pay"
tx="transactionCommon.payNow"
onPress={onPayDraftTransfer}
/>
</View>
@@ -1817,7 +1826,7 @@ const TransferInfoBlock = function (props: {
value={transaction.status as string}
/>
)}
{transaction.status !== TransactionStatus.ERROR && (
{transaction.status === TransactionStatus.COMPLETED && (
<TranItem
label="tranDetailScreen.balanceAfter"
value={transaction.balanceAfter || 0}
@@ -1825,11 +1834,16 @@ const TransferInfoBlock = function (props: {
isCurrency={true}
/>
)}
<TranItem
label="tranDetailScreen.createdAt"
value={(transaction.createdAt as Date).toLocaleString()}
/>
{transaction.expiresAt && transaction.status !== TransactionStatus.COMPLETED && (
<TranItem
label="tranDetailScreen.expiresAt"
value={(new Date(transaction.expiresAt!)).toLocaleString()}
/>
)}
{transaction.paymentId && (
<TranItem
label="tranDetailScreen.paymentId"
+21 -14
View File
@@ -1,22 +1,17 @@
import {observer} from 'mobx-react-lite'
import React, {FC, useState, useEffect, useRef, useMemo} from 'react'
import React, {useState, useEffect, useCallback} from 'react'
import {
TextStyle,
ViewStyle,
View,
FlatList,
SectionList,
Platform,
UIManager,
LayoutAnimation,
} from 'react-native'
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated'
import {formatDistance, toDate} from 'date-fns'
import {useThemeColor, spacing, colors, typography} from '../theme'
import {useThemeColor, spacing, typography} from '../theme'
import {
Button,
Icon,
@@ -37,12 +32,14 @@ import {TransactionListItem} from './Transactions/TransactionListItem'
import { Transaction, TransactionStatus } from '../models/Transaction'
import { translate } from '../i18n'
import { maxTransactionsInHistory } from '../models/TransactionsStore'
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
import { StaticScreenProps, useFocusEffect, useNavigation } from '@react-navigation/native'
// Number of transactions held in TransactionsStore model
const limit = maxTransactionsInHistory
type Props = StaticScreenProps<undefined>
type Props = StaticScreenProps<{
showPending?: boolean
}>
export const TranHistoryScreen = observer(function TranHistoryScreen({ route }: Props) {
const navigation = useNavigation()
@@ -98,11 +95,6 @@ export const TranHistoryScreen = observer(function TranHistoryScreen({ route }:
log.trace('[init] setAll true')
setIsAll(true)
}
/* if (countByStatus[TransactionStatus.PENDING] <= limit) {
log.trace('setPendingAll true')
setPendingIsAll(true)
}*/
}
init()
@@ -119,6 +111,21 @@ export const TranHistoryScreen = observer(function TranHistoryScreen({ route }:
}
}, [])
const initFilter = useCallback(() => {
log.trace('[initFilter] start')
if(route.params && route.params.showPending) {
log.trace('[initFilter] toggleShowPendingOnly ')
toggleShowPendingOnly()
//@ts-ignore
/* navigation.setParams({
showPending: undefined,
})*/
}
}, [])
useFocusEffect(initFilter)
const toggleDeleteModal = () => {
setIsDeleteModalVisible(previousState => !previousState)
}
@@ -94,7 +94,9 @@ export const TransactionListItem = observer(function (props: TransactionListProp
return (tx.memo && tx.memo !== 'LNbits'
? tx.memo
: tx.sentTo
? translate('transactionCommon.paidTo', {receiver: getProfileName(tx.sentTo)})
? tx.status === TransactionStatus.COMPLETED ?
translate('transactionCommon.paidTo', {receiver: getProfileName(tx.sentTo)})
: translate('transactionCommon.payTo', {receiver: getProfileName(tx.sentTo)})
: translate('transactionCommon.youPaid'))
default:
return translate('transactionCommon.unknown')
+22 -24
View File
@@ -36,8 +36,8 @@ import EventEmitter from '../utils/eventEmitter'
import {useStores} from '../models'
import {Mint, UnitBalance} from '../models/Mint'
import {MintsByUnit} from '../models/MintsStore'
import {HANDLE_CLAIM_TASK, HANDLE_RECEIVED_EVENT_TASK, log, NostrClient, WalletTaskResult} from '../services'
import {Transaction} from '../models/Transaction'
import {Database, HANDLE_CLAIM_TASK, HANDLE_RECEIVED_EVENT_TASK, log, NostrClient, WalletTaskResult} from '../services'
import {Transaction, TransactionStatus} from '../models/Transaction'
import {TransactionListItem} from './Transactions/TransactionListItem'
import {WalletTask} from '../services'
import {translate} from '../i18n'
@@ -92,7 +92,7 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
const [info, setInfo] = useState<string>('')
const [defaultMintUrl, setDefaultMintUrl] = useState<string>(MINIBITS_MINT_URL)
const [pendingTopupsCount, setPendingTopupsCount] = useState<number>(0)
const [pendingCount, setPendingCount] = useState<number>(0)
const [error, setError] = useState<AppError | undefined>()
const [isLoading, setIsLoading] = useState<boolean>(false)
const [isMintsModalVisible, setIsMintsModalVisible] = useState<boolean>(false)
@@ -171,17 +171,6 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
await addMint()
}
/*
TODO rethink
const pending = transactionsStore.getPendingTopupsCount()
if(pending > 0) {
setPendingTopupsCount(pending)
} */
// const nostrKeyPair = await KeyChain.loadNostrKeyPair()
// log.trace('[getInitialData] KEYS CHECK', {keychainPubkey: nostrKeyPair?.publicKey, profilePubkey: walletProfileStore.pubkey})
// Only once on startup - Create websocket subscriptions to receive tokens or payment requests by NOSTR DMs
WalletTask.receiveEventsFromRelaysQueue()
@@ -299,11 +288,17 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
WalletTask.handleInFlightQueue()
WalletTask.handleClaimQueue().catch(e => setInfo(e.message))
WalletTask.syncStateWithAllMintsQueue({isPending: true})
WalletTask.handlePendingTopupsQueue()
WalletTask.handlePendingQueue()
if(userSettingsStore.exchangeCurrency) {
walletStore.refreshExchangeRate(userSettingsStore.exchangeCurrency)
}
// TODO rethink
const countByStatus = Database.getTransactionsCount()
if(countByStatus[TransactionStatus.PENDING] && countByStatus[TransactionStatus.PENDING] > 0) {
setPendingCount(countByStatus[TransactionStatus.PENDING] || 0)
}
} else {
log.trace('[performChecks] Skipping mint server checks...')
}
@@ -399,9 +394,14 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
navigation.navigate('TokenReceive', {unit: currentUnit})
}
const gotoPendingTopups = function () {
const gotoPendingTransactions = function () {
// @ts-ignore
navigation.navigate('Transactions')
navigation.navigate('TransactionsNavigator', {
screen: 'TranHistory',
params: {
showPending: true
}
})
}
const gotoProfile = function () {
@@ -632,17 +632,15 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
TitleActionComponent={headerTitle.length > 0 ? <HeaderTitle /> : undefined}
RightActionComponent={
<>
{/* TODO this should be unpaid payment requests
pendingTopupsCount > 0 && (
{pendingCount > 0 && (
<Pressable
style={{flexDirection: 'row', alignItems:'center', marginRight: spacing.medium}}
onPress={() => gotoPendingTopups()}
onPress={gotoPendingTransactions}
>
<Icon icon='faPaperPlane' color={headerTitleColor}/>
<Text text={`${pendingTopupsCount}`} style={{color: headerTitleColor}} />
<Icon icon='faClock' color={headerTitleColor}/>
<Text text={`${pendingCount}`} style={{color: headerTitleColor}} />
</Pressable>
)*/ }
)}
</>
}
/>
-1
View File
@@ -12,7 +12,6 @@ export * from './SendScreen'
export * from './ScanScreen'
export * from './TranDetailScreen'
export * from './TranHistoryScreen'
export * from './PaymentRequestsScreen'
export * from './TransferScreen'
export * from './TopupScreen'
export * from './LightningPayScreen'
+143 -84
View File
@@ -6,12 +6,23 @@ import {
import {Proof} from '../models/Proof'
import {
Transaction,
TransactionRecord,
TransactionStatus,
} from '../models/Transaction'
import AppError, {Err} from '../utils/AppError'
import {log} from './logService'
import {BackupProof} from '../models/Proof'
import {ProofRecord} from '../models/Proof'
import { isDate } from 'date-fns'
// 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
@@ -255,43 +266,57 @@ const getDatabaseVersion = function (db: QuickSQLiteConnection): {version: numbe
*/
const updateTransaction = function (id: number, fields: {[key:string]: any}): TransactionRecord {
const updateTransaction = function (id: number, fields: Partial<Transaction>): Transaction {
const allowedColumns = ['amount','fee','unit','data','sentFrom','sentTo','profile','memo','paymentId','quote','paymentRequest','zapRequest','inputToken','outputToken','proof','balanceAfter','noteToSelf','tags','status','expiresAt'];
try {
// Filter keys against allowed columns to prevent SQL injection
const validKeys = Object.keys(fields).filter(key => allowedColumns.includes(key));
// Normalize data types for sqlite
for (const key in fields) {
const value = fields[key]
if (value === '') {
fields[key] = null
}
if (isDate(value)) {
fields[key] = value.toISOString()
}
}
// 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);
return getTransactionById(id)
}
// Build SET clauses and parameters
const setClauses = validKeys.map(key => `${key} = ?`);
const params = validKeys.map(key => fields[key]);
params.push(id); // Add id at the end for WHERE clause
const setClauses = validKeys.map(key => `${key} = ?`)
const params = validKeys.map(key => fields[key])
params.push(id) // Add id at the end for WHERE clause
const query = `
UPDATE transactions
SET ${setClauses.join(', ')}
WHERE id = ?
`;
`
const db = getInstance();
db.execute(query, params);
const db = getInstance()
const result = db.execute(query, params)
log.debug('[updateTransaction]', `Transaction updated in the database`, {id, fields: validKeys});
const updated = getTransactionById(id) // already normalized
const updatedTx = getTransactionById(id);
return updatedTx as TransactionRecord;
log.trace('[updateTransaction] Transaction updated in the database', {id: updated.id})
return updated as Transaction
} catch (e: any) {
throw new AppError(
Err.DATABASE_ERROR,
'Could not update transaction in database',
e.message,
);
)
}
}
@@ -324,7 +349,9 @@ const getTransactions = function (limit: number, offset: number, onlyPending: bo
const {rows} = db.execute(query, params)
log.trace(`[getTransactions], Returned ${rows?.length} rows`)
return rows
return normalizeTransactionRows(rows)
} catch (e: any) {
throw new AppError(
Err.DATABASE_ERROR,
@@ -335,9 +362,8 @@ const getTransactions = function (limit: number, offset: number, onlyPending: bo
}
const getPendingTopups = function () {
let query: string = ''
try {
query = `
const query = `
SELECT *
FROM transactions
WHERE status = 'PENDING'
@@ -350,7 +376,34 @@ const getPendingTopups = function () {
log.trace(`[getPendingTopups], Returned ${rows?.length} rows`)
return rows?._array
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(
@@ -390,6 +443,34 @@ const getPendingTopupsCount = function () {
}
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[] = []
@@ -458,7 +539,7 @@ const getRecentTransactionsByUnit = (countRecent: number) => {
const db = getInstance()
const { rows } = db.execute(query, params)
return rows?._array
return normalizeTransactionRows(rows)
} catch (e: any) {
throw new AppError(Err.DATABASE_ERROR, 'Error retrieving last 3 transactions by unit', e.message)
@@ -498,7 +579,7 @@ const getTransactionById = function (id: number) {
const db = getInstance()
const {rows} = db.execute(query, params)
return rows?.item(0) as TransactionRecord
return normalizeTransactionRecord(rows?.item(0))
} catch (e: any) {
throw new AppError(Err.DATABASE_ERROR, 'Transaction not found', e.message)
}
@@ -516,7 +597,7 @@ const getTransactionByPaymentId = function (id: string) {
const db = getInstance()
const {rows} = db.execute(query, params)
return rows?.item(0) as TransactionRecord
return normalizeTransactionRecord(rows?.item(0))
} catch (e: any) {
throw new AppError(Err.DATABASE_ERROR, 'Transaction not found', e.message)
}
@@ -534,7 +615,7 @@ const getTransactionByQuote = function (quote: string) {
const db = getInstance()
const {rows} = db.execute(query, params)
return rows?.item(0) as TransactionRecord
return normalizeTransactionRecord(rows?.item(0))
} catch (e: any) {
throw new AppError(Err.DATABASE_ERROR, 'Transaction not found', e.message)
}
@@ -552,14 +633,14 @@ const getTransactionByPaymentRequest = function (pr: string) {
const db = getInstance()
const {rows} = db.execute(query, params)
return rows?.item(0) as TransactionRecord
return normalizeTransactionRecord(rows?.item(0))
} catch (e: any) {
throw new AppError(Err.DATABASE_ERROR, 'Transaction not found', e.message)
}
}
const addTransactionAsync = async function (tx: Transaction): Promise<TransactionRecord> {
const addTransactionAsync = async function (tx: Transaction): Promise<Transaction> {
try {
const {type, amount, fee, unit, data, memo, mint, status} = tx
const now = new Date()
@@ -575,9 +656,8 @@ const addTransactionAsync = async function (tx: Transaction): Promise<Transactio
log.info('[addTransactionAsync]', 'New transaction added to the database', {id: result.insertId, type, mint, status})
const newTx = getTransactionById(result.insertId as number)
return getTransactionById(result.insertId as number) // already normalized
return newTx as TransactionRecord
} catch (e: any) {
throw new AppError(
Err.DATABASE_ERROR,
@@ -653,55 +733,6 @@ const expireAllAfterRecovery = async function () {
}
/* const cleanTransactionData = async function (transactionIds: number[]) {
try {
const transactionIdsString = transactionIds.join(',')
const query = `
UPDATE transactions
SET data = ?
WHERE id IN (${transactionIdsString})
`
const params = ['[]']
const _db = getInstance()
const result = await _db.executeAsync(query, params)
log.info('cleanTransactionData executed in the database')
} catch (e: any) {
throw new AppError(Err.DATABASE_ERROR, 'Could not cleanTransactionData in database', e.message)
}
} */
const deleteTransactionsByStatus = function (status: TransactionStatus) {
try {
const query = `
@@ -720,12 +751,37 @@ const deleteTransactionsByStatus = function (status: TransactionStatus) {
} catch (e: any) {
throw new AppError(
Err.DATABASE_ERROR,
'Could not delete transactions',
'Could not delete transactions.',
e.message,
)
}
}
const deleteTransactionById = function (id: number) {
try {
const query = `
DELETE FROM transactions
WHERE id = ?
`
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 - backup of store model
*/
@@ -762,7 +818,7 @@ const addOrUpdateProof = function (
const newProof = getProofById(result.insertId as number)
return newProof as BackupProof
return newProof as ProofRecord
} catch (e: any) {
throw new AppError(
Err.DATABASE_ERROR,
@@ -907,7 +963,7 @@ const getProofById = function (id: number) {
const db = getInstance()
const {rows} = db.execute(query, params)
return rows?.item(0) as BackupProof
return rows?.item(0) as ProofRecord
} catch (e: any) {
throw new AppError(Err.DATABASE_ERROR, 'proof not found', e.message)
}
@@ -917,7 +973,7 @@ const getProofs = async function (
isUnspent: boolean,
isPending: boolean,
isSpent: boolean,
): Promise<BackupProof[]> {
): Promise<ProofRecord[]> {
let query: string = ''
try {
@@ -961,7 +1017,7 @@ const getProofs = async function (
const db = getInstance()
const {rows} = await db.executeAsync(query)
return rows?._array as BackupProof[]
return rows?._array as ProofRecord[]
} catch (e: any) {
throw new AppError(
@@ -972,7 +1028,7 @@ const getProofs = async function (
}
}
const getProofsByTransaction = function (transactionId: number): BackupProof[] {
const getProofsByTransaction = function (transactionId: number): ProofRecord[] {
try {
const query = `
SELECT *
@@ -984,7 +1040,7 @@ const getProofsByTransaction = function (transactionId: number): BackupProof[] {
const db = getInstance()
const {rows} = db.execute(query, params)
return rows?._array as BackupProof[]
return rows?._array as ProofRecord[]
} catch (e: any) {
throw new AppError(
Err.DATABASE_ERROR,
@@ -1028,11 +1084,14 @@ export const Database = {
getTransactions,
getPendingTopups,
getPendingTopupsCount,
getPendingTransfers,
getPendingTransfersCount,
addTransactionAsync,
updateTransaction,
expireAllAfterRecovery,
updateStatusesAsync,
deleteTransactionsByStatus,
deleteTransactionById,
getPendingAmount,
addOrUpdateProof,
addOrUpdateProofs,
+3 -11
View File
@@ -1,20 +1,12 @@
import {rootStoreInstance} from '../../models'
import { TransactionTaskResult, WalletTask } from '../walletService'
import { TransactionTaskResult } from '../walletService'
import { MintBalance } from '../../models/Mint'
import { poller } from '../../utils/poller'
import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../../models/Transaction'
import { Transaction, TransactionData, TransactionStatus, TransactionType } from '../../models/Transaction'
import { log } from '../logService'
import { Contact } from '../../models/Contact'
import { LightningUtils } from '../lightning/lightningUtils'
import { getSnapshot, isStateTreeNode } from 'mobx-state-tree'
import { WalletUtils } from './utils'
import { MintUnit } from './currency'
import { NostrClient, NostrEvent } from '../nostrService'
import { Err } from '../../utils/AppError'
import { NostrClient } from '../nostrService'
import {
CashuMint,
CashuWallet,
MintQuoteResponse,
PaymentRequest as CashuPaymentRequest,
PaymentRequestTransport,
PaymentRequestTransportType
+5 -2
View File
@@ -105,16 +105,19 @@ export const topupTask = async function (
const expiresAtDate = addSeconds(new Date(timestamp * 1000), expiry)
const sentFromValue = contactTo?.nip05 || contactTo?.name || ''
const sentToValue = walletProfileStore.nip05 || ''
log.trace('[topupTask]', expiresAtDate)
transaction.update({
quote: mintQuote,
paymentId: paymentHash,
paymentRequest: encodedInvoice,
expiresAt: expiresAtDate,
expiresAt: new Date(expiresAtDate),
sentFrom: sentFromValue,
sentTo: sentToValue
})
log.trace('[topupTask] invoice', {amount, paymentHash, expiry, timestamp})
log.trace('[topupTask] invoice', {amount, paymentHash, expiry, timestamp, expiresAtDate})
transactionData.push({
status: TransactionStatus.PENDING,
+2 -2
View File
@@ -3,9 +3,9 @@ import AppError, {Err} from '../../utils/AppError'
import {MeltProofsResponse, MeltQuoteResponse, MeltQuoteState, getEncodedToken} from '@cashu/cashu-ts'
import {rootStoreInstance} from '../../models'
import { TransactionTaskResult, WalletTask } from '../walletService'
import { MintBalance, MintProofsCounter } from '../../models/Mint'
import { MintBalance } from '../../models/Mint'
import { Proof } from '../../models/Proof'
import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../../models/Transaction'
import { Transaction, TransactionData, TransactionStatus, TransactionType } from '../../models/Transaction'
import { log } from '../logService'
import { WalletUtils } from './utils'
import {isBefore} from 'date-fns'
+43 -19
View File
@@ -41,6 +41,7 @@ import { UnsignedEvent } from 'nostr-tools'
import { Platform } from 'react-native'
import { cashuPaymentRequestTask } from './wallet/cashuPaymentRequestTask'
import { decodePaymentRequest, sumBlindSignatures } from '@cashu/cashu-ts/src/utils'
import { Database } from './sqlite'
/**
* The default number of proofs per denomination to keep in a wallet.
@@ -81,7 +82,7 @@ type WalletTaskService = {
}
) => Promise<SyncStateTaskResult | void>
handleInFlightQueue: () => Promise<void>
handlePendingTopupsQueue: () => Promise<void>
handlePendingQueue: () => Promise<void>
handlePendingTopupQueue: (params: {
transaction: Transaction
}) => Promise<void>
@@ -1504,20 +1505,42 @@ const handleInFlightByMintTask = async function (mint: Mint): Promise<WalletTask
const handlePendingTopupsQueue = async function (): Promise<void> {
const handlePendingQueue = async function (): Promise<void> {
const pendingTopups: Transaction[] = transactionsStore.getPendingTopups()
const pendingTransfers: Transaction[] = transactionsStore.getPendingTransfers()
log.trace('[handlePendingTopupsQueue] start', {pendingTopups})
log.trace('[handlePendingQueue] start', {pendingTopups, pendingTransfers})
// we just expire expired transfers, no ecash ops so no need of queue
if(pendingTransfers.length > 0) {
for (const transfer of pendingTransfers) {
if (isBefore(transfer.expiresAt as Date, new Date())) {
log.debug('[handlePendingQueue]', `Transfer invoice expired: ${transfer.paymentId} ${transfer.quote}`)
const transactionData = JSON.parse(transfer.data)
transactionData.push({
status: TransactionStatus.EXPIRED,
message: 'Invoice expired',
createdAt: new Date(),
})
transfer.update({
status: TransactionStatus.EXPIRED,
data: JSON.stringify(transactionData)
})
}
}
}
if (pendingTopups.length === 0) {
log.trace('[handlePendingTopupsQueue]', 'No pending topups - skipping task send to the queue...')
log.trace('[handlePendingQueue]', 'No pending topups - skipping task send to the queue...')
return
}
for (const topup of pendingTopups) {
// skip pr if active poller exists
if(pollerExists(`handlePendingTopupPoller-${topup.paymentId}`)) {
log.trace('[handlePendingTopupsQueue] Skipping check of pendingTopup, poller exists', {paymentHash: topup.paymentId})
log.trace('[handlePendingQueue] Skipping check of pendingTopup, poller exists', {paymentHash: topup.paymentId})
continue
}
@@ -1558,12 +1581,15 @@ const handlePendingTopupTask = async function (params: {transaction: Transaction
expiresAt
} = transaction
log.trace('[handlePendingTopupTask] start', transaction)
const transactionData = JSON.parse(transaction.data)
const mintInstance = mintsStore.findByUrl(mintUrl as string)
try {
if(!mintInstance || !mintQuote || !unit || !amount) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing mint, mint quote, unit, amount', {transactionId})
// Database.deleteTransactionById(transactionId)
throw new AppError(Err.VALIDATION_ERROR, 'Missing mint, mint quote, unit, amount', {transactionId, caller: 'handlePendingTopupTask'})
}
// check is quote has been paid
@@ -1576,19 +1602,17 @@ const handlePendingTopupTask = async function (params: {transaction: Transaction
if (isBefore(expiresAt as Date, new Date())) {
log.debug('[handlePendingTopupTask]', `Invoice expired, removing: ${paymentHash} ${mintQuote}`)
// expire related tx - but only if it has not been completed before this check
if(transaction.status !== TransactionStatus.COMPLETED) {
transactionData.push({
status: TransactionStatus.EXPIRED,
message: 'Invoice expired',
createdAt: new Date(),
})
transactionData.push({
status: TransactionStatus.EXPIRED,
message: 'Invoice expired',
createdAt: new Date(),
})
transaction.update({
status: TransactionStatus.EXPIRED,
data: JSON.stringify(transactionData)
})
}
transaction.update({
status: TransactionStatus.EXPIRED,
data: JSON.stringify(transactionData)
})
stopPolling(`handlePendingTopupPoller-${paymentHash}`)
}
@@ -2678,7 +2702,7 @@ export const WalletTask: WalletTaskService = {
syncStateWithMintQueue,
syncStateWithMintTask,
handleInFlightQueue,
handlePendingTopupsQueue,
handlePendingQueue,
handlePendingTopupQueue,
handleClaimQueue,
handleNwcRequestQueue,