Skip to content

Commit 8637b3e

Browse files
feat(packages): add localStorage utilities for pending deposit tracking (#548)
1 parent 4457620 commit 8637b3e

File tree

4 files changed

+196
-194
lines changed

4 files changed

+196
-194
lines changed

services/vault/src/components/Overview/Deposits/DepositSignModal/index.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,8 @@ export function CollateralDepositSignModal({
5151
if (depositorEthAddress) {
5252
addPendingPegin(depositorEthAddress, {
5353
id: ethTxHash,
54-
btcTxHash: btcTxid,
5554
amount: amount.toString(),
56-
providers: selectedProviders,
57-
ethAddress: depositorEthAddress,
58-
btcAddress: "", // Will be populated when needed
55+
providerId: selectedProviders[0], // Use first selected provider
5956
});
6057
}
6158

services/vault/src/hooks/useVaultActivityActions.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
/**
22
* Custom hook for managing vault activity actions
3-
*
4-
* Extracts business logic for broadcasting and signing from VaultActivityCard
5-
* to improve separation of concerns and testability.
63
*/
74

85
import { useChainConnector } from "@babylonlabs-io/wallet-connector";
@@ -83,7 +80,6 @@ export function useVaultActivityActions(): UseVaultActivityActionsReturn {
8380
activityId,
8481
activityAmount,
8582
activityProviders,
86-
connectedAddress,
8783
pendingPegin,
8884
updatePendingPeginStatus,
8985
addPendingPegin,
@@ -148,15 +144,10 @@ export function useVaultActivityActions(): UseVaultActivityActionsReturn {
148144
updatePendingPeginStatus(activityId, nextStatus, txId);
149145
} else if (addPendingPegin && nextStatus) {
150146
// Case 2: NO localStorage entry (cross-device) - create full peg-in entry
151-
const btcAddress = btcConnector?.connectedWallet?.account?.address;
152-
153147
addPendingPegin({
154148
id: activityId,
155149
amount: activityAmount,
156-
providers: activityProviders.map((p) => p.id),
157-
ethAddress: connectedAddress,
158-
btcAddress: btcAddress || "",
159-
btcTxHash: txId,
150+
providerId: activityProviders[0]?.id, // Use first provider ID
160151
status: nextStatus,
161152
});
162153
}
Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
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

1015
export 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

2123
const 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
*/
88134
export 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
*/
119169
export 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

Comments
 (0)