forked from CredenceOrg/Credence-Frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpendingTransactionsStorage.ts
More file actions
133 lines (115 loc) · 3.93 KB
/
Copy pathpendingTransactionsStorage.ts
File metadata and controls
133 lines (115 loc) · 3.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import type { Transaction } from '../api/types'
import { safeReadJson, safeWriteJson } from './storageJson'
import { logInfo, logWarn } from './log'
/**
* Legacy format (v0):
* - key: credence:pendingTransactions
* - value: Transaction[]
*
* Versioned format (v1):
* - key: credence:pendingTransactions:v1
* - value: { schemaVersion: 1, items: Transaction[], migratedFrom?: 'v0' }
*
* Compatibility policy mirrors `recentLookupsStorage.ts`.
*/
export const PENDING_TXS_LEGACY_KEY = 'credence:pendingTransactions'
export const PENDING_TXS_V1_KEY = 'credence:pendingTransactions:v1'
type PendingTxsV1 = {
schemaVersion: 1
items: Transaction[]
migratedFrom?: 'v0'
migratedAt?: string
}
// A pending entry that remains pending "forever" is misleading. If the API never
// confirms it (offline, wallet crash, etc.), we surface it as failed after a
// generous time window so the user sees a resolvable state.
const STALE_PENDING_MS = 24 * 60 * 60 * 1000
function isTransaction(value: unknown): value is Transaction {
if (!value || typeof value !== 'object') return false
const v = value as Partial<Transaction>
return (
typeof v.id === 'string' &&
typeof v.type === 'string' &&
typeof v.timestamp === 'string' &&
typeof v.status === 'string' &&
typeof v.hash === 'string'
)
}
function coerceTransactions(raw: unknown): Transaction[] {
if (!Array.isArray(raw)) return []
return raw.filter(isTransaction)
}
function readV1(): Transaction[] | null {
const res = safeReadJson<PendingTxsV1>(PENDING_TXS_V1_KEY)
if (!res.ok) return []
if (!res.value) return null
if (res.value.schemaVersion !== 1) return []
return coerceTransactions(res.value.items)
}
function readLegacy(): Transaction[] | null {
const res = safeReadJson<unknown>(PENDING_TXS_LEGACY_KEY)
if (!res.ok) return []
if (!res.value) return null
return coerceTransactions(res.value)
}
function migrateFromLegacy(items: Transaction[]): void {
const payload: PendingTxsV1 = {
schemaVersion: 1,
items,
migratedFrom: 'v0',
migratedAt: new Date().toISOString(),
}
const write = safeWriteJson(PENDING_TXS_V1_KEY, payload)
if (write.ok) {
logInfo('pending_txs_migrated', { from: 'v0', to: 'v1', count: items.length })
} else {
logWarn('pending_txs_migration_failed', { message: write.error.message })
}
}
export function readPendingTransactions(): Transaction[] {
const v1 = readV1()
if (v1 !== null) {
return normalizeStalePending(v1)
}
const legacy = readLegacy()
if (legacy === null) return []
migrateFromLegacy(legacy)
return normalizeStalePending(legacy)
}
export function writePendingTransactions(items: Transaction[]): void {
const sanitized = coerceTransactions(items)
safeWriteJson<PendingTxsV1>(PENDING_TXS_V1_KEY, {
schemaVersion: 1,
items: sanitized,
})
// Rollback compatibility
safeWriteJson(PENDING_TXS_LEGACY_KEY, sanitized)
}
function normalizeStalePending(items: Transaction[]): Transaction[] {
const now = Date.now()
let changed = false
const next = items.map((tx) => {
if (tx.status !== 'pending') return tx
const ts = Date.parse(tx.timestamp)
if (Number.isNaN(ts)) return tx
if (now - ts <= STALE_PENDING_MS) return tx
changed = true
return { ...tx, status: 'failed' as const }
})
if (changed) {
logWarn('pending_txs_marked_stale', { count: next.filter((t) => t.status === 'failed').length })
// Best-effort: persist the normalized view so subsequent reads are deterministic.
writePendingTransactions(next)
}
return next
}
export function addPendingTransaction(tx: Transaction): void {
const pending = readPendingTransactions()
// Avoid duplicates by hash.
if (pending.some((existing) => existing.hash === tx.hash)) return
writePendingTransactions([tx, ...pending])
}
export function removePendingTransaction(hash: string): void {
const pending = readPendingTransactions()
writePendingTransactions(pending.filter((tx) => tx.hash !== hash))
}