11/**
22 * Local Storage utilities for pending peg-in transactions
33 *
4- * Similar to simple-staking's delegation storage pattern:
5- * - Store pending peg-ins in localStorage
6- * - Merge with API data when available
7- * - Remove from localStorage when confirmed on-chain
4+ * Purpose:
5+ * - Store pending deposits temporarily until they appear on-chain
6+ * - Show immediate feedback to users after deposit submission
7+ * - Auto-cleanup once transaction is confirmed on blockchain
8+ *
9+ * Cleanup Strategy:
10+ * - Remove when transaction exists in contract (contract = source of truth)
11+ * - Remove when older than 24 hours (stale data)
12+ * - Keep only transactions not yet on blockchain
813 */
914
1015export interface PendingPeginRequest {
11- id : string ; // Unique identifier (BTC tx hash or temporary ID)
12- btcTxHash ?: string ; // BTC transaction hash (once available)
13- amount : string ; // BTC amount as string to avoid BigInt serialization issues
14- providers : string [ ] ; // Selected vault provider IDs
15- ethAddress : string ; // ETH address that initiated the peg-in
16- btcAddress : string ; // BTC address used
16+ id : string ; // Peg-in ID (pegin tx hash)
1717 timestamp : number ; // When the peg-in was initiated
1818 status : "pending" | "payout_signed" | "confirming" | "confirmed" ;
19+ amount ?: string ; // Amount in BTC (formatted for display)
20+ providerId ?: string ; // Vault provider's Ethereum address
1921}
2022
2123const STORAGE_KEY_PREFIX = "vault-pending-pegins" ;
22- const MAX_PENDING_DURATION = 24 * 60 * 60 * 1000 ; // 24 hours
24+ const MAX_PENDING_DURATION = 24 * 60 * 60 * 1000 ; // 24 hours - cleanup stale items
2325
2426/**
2527 * Get storage key for a specific address
@@ -28,6 +30,14 @@ function getStorageKey(ethAddress: string): string {
2830 return `${ STORAGE_KEY_PREFIX } -${ ethAddress } ` ;
2931}
3032
33+ /**
34+ * Normalize transaction ID to ensure it has 0x prefix
35+ * This handles legacy data that might not have the prefix
36+ */
37+ function normalizeTransactionId ( id : string ) : string {
38+ return id . startsWith ( "0x" ) ? id : `0x${ id } ` ;
39+ }
40+
3141/**
3242 * Get all pending peg-ins from localStorage for an address
3343 */
@@ -40,8 +50,26 @@ export function getPendingPegins(ethAddress: string): PendingPeginRequest[] {
4050 if ( ! stored ) return [ ] ;
4151
4252 const parsed : PendingPeginRequest [ ] = JSON . parse ( stored ) ;
43- return parsed ;
44- } catch {
53+
54+ // Normalize IDs to ensure they all have 0x prefix (handles legacy data)
55+ const normalized = parsed . map ( ( pegin ) => ( {
56+ ...pegin ,
57+ id : normalizeTransactionId ( pegin . id ) ,
58+ } ) ) ;
59+
60+ // Check if any IDs were normalized (legacy data)
61+ const hasLegacyData = parsed . some (
62+ ( pegin , index ) => pegin . id !== normalized [ index ] . id ,
63+ ) ;
64+
65+ // If we normalized any legacy data, save it back to localStorage
66+ if ( hasLegacyData ) {
67+ localStorage . setItem ( key , JSON . stringify ( normalized ) ) ;
68+ }
69+
70+ return normalized ;
71+ } catch ( error ) {
72+ console . error ( "[peginStorage] Failed to parse pending pegins:" , error ) ;
4573 return [ ] ;
4674 }
4775}
@@ -58,8 +86,8 @@ export function savePendingPegins(
5886 try {
5987 const key = getStorageKey ( ethAddress ) ;
6088 localStorage . setItem ( key , JSON . stringify ( pegins ) ) ;
61- } catch {
62- // Silent failure - localStorage might be unavailable
89+ } catch ( error ) {
90+ console . error ( "[peginStorage] Failed to save pending pegins:" , error ) ;
6391 }
6492}
6593
@@ -72,13 +100,31 @@ export function addPendingPegin(
72100) : void {
73101 const existingPegins = getPendingPegins ( ethAddress ) ;
74102
103+ // Normalize the ID to ensure it has 0x prefix
104+ const normalizedId = normalizeTransactionId ( pegin . id ) ;
105+
106+ // Check if this pegin already exists
107+ const existingPeginIndex = existingPegins . findIndex (
108+ ( p ) => p . id === normalizedId ,
109+ ) ;
110+
75111 const newPegin : PendingPeginRequest = {
76112 ...pegin ,
113+ id : normalizedId , // Use normalized ID
77114 timestamp : Date . now ( ) ,
78115 status : "pending" ,
79116 } ;
80117
81- const updatedPegins = [ ...existingPegins , newPegin ] ;
118+ let updatedPegins : PendingPeginRequest [ ] ;
119+ if ( existingPeginIndex >= 0 ) {
120+ // Update existing pegin
121+ updatedPegins = [ ...existingPegins ] ;
122+ updatedPegins [ existingPeginIndex ] = newPegin ;
123+ } else {
124+ // Add new pegin
125+ updatedPegins = [ ...existingPegins , newPegin ] ;
126+ }
127+
82128 savePendingPegins ( ethAddress , updatedPegins ) ;
83129}
84130
@@ -87,7 +133,8 @@ export function addPendingPegin(
87133 */
88134export function removePendingPegin ( ethAddress : string , peginId : string ) : void {
89135 const existingPegins = getPendingPegins ( ethAddress ) ;
90- const updatedPegins = existingPegins . filter ( ( p ) => p . id !== peginId ) ;
136+ const normalizedId = normalizeTransactionId ( peginId ) ;
137+ const updatedPegins = existingPegins . filter ( ( p ) => p . id !== normalizedId ) ;
91138 savePendingPegins ( ethAddress , updatedPegins ) ;
92139}
93140
@@ -101,44 +148,58 @@ export function updatePeginStatus(
101148 btcTxHash ?: string ,
102149) : void {
103150 const existingPegins = getPendingPegins ( ethAddress ) ;
151+ const normalizedId = normalizeTransactionId ( peginId ) ;
104152 const updatedPegins = existingPegins . map ( ( p ) =>
105- p . id === peginId ? { ...p , status, ...( btcTxHash && { btcTxHash } ) } : p ,
153+ p . id === normalizedId
154+ ? { ...p , status, ...( btcTxHash && { btcTxHash } ) }
155+ : p ,
106156 ) ;
107157 savePendingPegins ( ethAddress , updatedPegins ) ;
108158}
109159
110160/**
111- * Filter and clean up old pending peg-ins.
112- * Removes peg-ins that exist on blockchain OR exceeded max duration.
161+ * Filter and clean up old pending peg-ins
162+ *
163+ * Removes items from localStorage if:
164+ * 1. Transaction exists on blockchain (any status) - contract is source of truth
165+ * 2. Older than 24 hours - cleanup stale items
113166 *
114- * IMPORTANT: localStorage is a temporary placeholder until blockchain confirms the transaction.
115- * - NOT on blockchain yet: Keep in localStorage (show pending state to user)
116- * - ON blockchain (any presence): Remove from localStorage (blockchain is source of truth)
117- * - Older than 24 hours: Remove from localStorage (cleanup stale data)
167+ * Keeps items only if they're not yet on blockchain (still pending confirmation)
118168 */
119169export function filterPendingPegins (
120170 pendingPegins : PendingPeginRequest [ ] ,
121- confirmedPegins : Array < { id : string } > ,
171+ confirmedPegins : Array < { id : string ; status : number } > ,
122172) : PendingPeginRequest [ ] {
123173 const now = Date . now ( ) ;
124174
125- // Create a set of confirmed pegin IDs for quick lookup
126- const confirmedPeginIds = new Set ( confirmedPegins . map ( ( p ) => p . id ) ) ;
175+ // Normalize confirmed pegin IDs to ensure they have 0x prefix
176+ const normalizedConfirmedPegins = confirmedPegins . map ( ( p ) => ( {
177+ id : normalizeTransactionId ( p . id ) ,
178+ status : p . status ,
179+ } ) ) ;
127180
128181 return pendingPegins . filter ( ( pegin ) => {
129- // If this pegin exists on blockchain (any status), remove from localStorage
130- // Blockchain data is now the source of truth
131- if ( confirmedPeginIds . has ( pegin . id ) ) {
132- return false ;
133- }
182+ // Normalize the pending pegin ID as well (should already be normalized, but just in case)
183+ const normalizedPeginId = normalizeTransactionId ( pegin . id ) ;
134184
135- // Remove if exceeded max duration
185+ // Remove if exceeded max duration (24 hours)
136186 const age = now - pegin . timestamp ;
137187 if ( age > MAX_PENDING_DURATION ) {
138188 return false ;
139189 }
140190
141- // Keep in localStorage (not yet on blockchain)
191+ // Check if pegin exists on blockchain (using normalized IDs)
192+ const confirmedPegin = normalizedConfirmedPegins . find (
193+ ( p ) => p . id === normalizedPeginId ,
194+ ) ;
195+
196+ // If it exists on blockchain, remove from localStorage
197+ // The contract is now the source of truth - no need to keep in localStorage anymore
198+ if ( confirmedPegin ) {
199+ return false ;
200+ }
201+
202+ // Keep in localStorage only if not yet on blockchain
142203 return true ;
143204 } ) ;
144205}
@@ -152,7 +213,7 @@ export function clearPendingPegins(ethAddress: string): void {
152213 try {
153214 const key = getStorageKey ( ethAddress ) ;
154215 localStorage . removeItem ( key ) ;
155- } catch {
156- // Silent failure - localStorage might be unavailable
216+ } catch ( error ) {
217+ console . error ( "[peginStorage] Failed to clear pending pegins:" , error ) ;
157218 }
158219}
0 commit comments