Skip to content

Commit dbf5277

Browse files
authored
WalletConnect stability fixes (#2897)
* initial commit * removePairs fixes * add debugging * catch unhandled rejection * fix error spam * re-add duplicate check * re-imple Bram's lost error handling from elsewhere * remove accidental file * change logging on no ke error * fix incomplete singleton init * info not debug * remove dead store
1 parent 6d61dfa commit dbf5277

5 files changed

Lines changed: 135 additions & 60 deletions

File tree

packages/gui/src/hooks/useWalletConnect.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,21 @@ export default function useWalletConnect(config: UseWalletConnectConfig) {
4040
const processRef = useRef(process);
4141
processRef.current = process;
4242

43+
const pairsRef = useRef(pairs);
44+
pairsRef.current = pairs;
45+
4346
const isLoadingData = isLoading || isLoadingWalletConnectCommand;
4447

4548
useEffect(() => {
4649
if (!client) {
4750
return undefined;
4851
}
4952

50-
cleanupPairings(client, pairs);
53+
const currentPairs = pairsRef.current;
54+
cleanupPairings(client, currentPairs);
5155

52-
return bindEvents(client, pairs, () => processRef.current);
53-
}, [client, pairs]);
56+
return bindEvents(client, currentPairs, () => processRef.current);
57+
}, [client]);
5458

5559
const handlePair = useCallback(
5660
async (uri: string, fingerprints: number[], mainnet = false) => {
@@ -63,7 +67,7 @@ export default function useWalletConnect(config: UseWalletConnectConfig) {
6367
throw new Error('Pairing failed');
6468
}
6569

66-
pairs.addPair({
70+
pairsRef.current.addPair({
6771
topic,
6872
fingerprints,
6973
mainnet,
@@ -72,7 +76,7 @@ export default function useWalletConnect(config: UseWalletConnectConfig) {
7276

7377
return topic;
7478
},
75-
[client, pairs],
79+
[client],
7680
);
7781

7882
const handleDisconnect = useCallback(
@@ -81,9 +85,9 @@ export default function useWalletConnect(config: UseWalletConnectConfig) {
8185
throw new Error('Client is not defined');
8286
}
8387

84-
return disconnectPair(client, pairs, topic);
88+
return disconnectPair(client, pairsRef.current, topic);
8589
},
86-
[client, pairs],
90+
[client],
8791
);
8892

8993
return {

packages/gui/src/hooks/useWalletConnectClient.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
33

44
import useWalletConnectPreferences from './useWalletConnectPreferences';
55

6+
function clearWalletConnectStorage(): void {
7+
if (typeof indexedDB === 'undefined') return;
8+
try {
9+
indexedDB.deleteDatabase('WALLET_CONNECT_V2_INDEXED_DB');
10+
} catch {
11+
/* best-effort */
12+
}
13+
}
14+
615
const defaultMetadata = {
716
name: 'Chia Blockchain',
817
description: 'GUI for Chia Blockchain',
@@ -24,6 +33,13 @@ export type UseWalletConnectConfig = {
2433

2534
let clientId = 1;
2635

36+
type WalletConnectSingleton = {
37+
initPromise?: Promise<Client>;
38+
configKey?: string;
39+
};
40+
41+
const singleton: WalletConnectSingleton = {};
42+
2743
export default function useWalletConnectClient(config: UseWalletConnectConfig) {
2844
const { projectId, relayUrl = 'wss://relay.walletconnect.com', metadata = defaultMetadata, debug = false } = config;
2945

@@ -48,13 +64,29 @@ export default function useWalletConnectClient(config: UseWalletConnectConfig) {
4864
return;
4965
}
5066

51-
const newClient = await Client.init({
52-
logger: debug ? 'debug' : undefined,
53-
projectId,
54-
relayUrl,
55-
metadata: memoizedMetadata,
56-
});
67+
const configKey = JSON.stringify({ projectId, relayUrl, metadata: memoizedMetadata, debug });
68+
69+
if (singleton.configKey !== undefined && singleton.configKey !== configKey) {
70+
singleton.initPromise = undefined;
71+
}
72+
73+
if (!singleton.initPromise) {
74+
singleton.configKey = configKey;
75+
singleton.initPromise = Client.init({
76+
logger: debug ? 'debug' : 'error',
77+
projectId,
78+
relayUrl,
79+
metadata: memoizedMetadata,
80+
}).catch((initError) => {
81+
console.error('[WC] Client.init() failed, clearing storage', initError);
82+
clearWalletConnectStorage();
83+
singleton.initPromise = undefined;
84+
singleton.configKey = undefined;
85+
throw initError;
86+
});
87+
}
5788

89+
const newClient = await singleton.initPromise;
5890
if (currentClientId === clientId) {
5991
setClient(newClient);
6092
}

packages/gui/src/hooks/useWalletConnectPairs.ts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,50 @@ export default function useWalletConnectPairs(): Pairs {
3434
pairsRef.current = localStorageData;
3535

3636
const updatePair = useCallback((topic: string, data: Partial<Omit<Pair, 'topic'>> | ((pair: Pair) => Pair)) => {
37-
const [, setPairs] = pairsRef.current;
37+
const [latestPairs, setPairs] = pairsRef.current;
38+
39+
const index = latestPairs.findIndex((item) => item.topic === topic);
40+
if (index !== -1) {
41+
const oldPair = latestPairs[index];
42+
const newPairing = typeof data === 'function' ? data(oldPair) : { ...oldPair, ...data };
43+
const newPairings = [...latestPairs];
44+
newPairings[index] = newPairing;
45+
pairsRef.current = [newPairings, setPairs];
46+
}
47+
3848
setPairs((pairs: Pair[]) => {
39-
const index = pairs.findIndex((item) => item.topic === topic);
40-
if (index === -1) {
49+
const idx = pairs.findIndex((item) => item.topic === topic);
50+
if (idx === -1) {
4151
return pairs;
4252
}
4353

44-
const oldPair = pairs[index];
54+
const oldPair = pairs[idx];
4555
const newPairing = typeof data === 'function' ? data(oldPair) : { ...oldPair, ...data };
4656
const newPairings = [...pairs];
47-
newPairings[index] = newPairing;
57+
newPairings[idx] = newPairing;
4858

4959
return newPairings;
5060
});
5161
}, []);
5262

5363
const removePair = useCallback((topic: string) => {
54-
const [, setPairs] = pairsRef.current;
64+
const [latestPairs, setPairs] = pairsRef.current;
65+
if (!latestPairs.some((item) => item.topic === topic)) {
66+
return;
67+
}
68+
69+
pairsRef.current = [latestPairs.filter((item) => item.topic !== topic), setPairs];
70+
5571
setPairs((pairs: Pair[]) => pairs.filter((item) => item.topic !== topic));
5672
}, []);
5773

5874
const removePairBySession = useCallback((sessionTopic: string) => {
59-
const [, setPairs] = pairsRef.current;
75+
const [latestPairs, setPairs] = pairsRef.current;
76+
pairsRef.current = [
77+
latestPairs.filter((item) => !item.sessions.find((session) => session.topic === sessionTopic)),
78+
setPairs,
79+
];
80+
6081
setPairs((pairs: Pair[]) =>
6182
pairs.filter((item) => !item.sessions.find((session) => session.topic === sessionTopic)),
6283
);
@@ -78,19 +99,31 @@ export default function useWalletConnectPairs(): Pairs {
7899
}, []);
79100

80101
const addPair = useCallback((pair: Pair) => {
81-
const [, setPairs] = pairsRef.current;
102+
const [latestPairs, setPairs] = pairsRef.current;
103+
if (latestPairs.findIndex((item) => item.topic === pair.topic) !== -1) {
104+
throw new Error('Pair already exists');
105+
}
106+
107+
pairsRef.current = [[...latestPairs, pair], setPairs];
108+
82109
setPairs((pairs: Pair[]) => {
83-
const index = pairs.findIndex((item) => item.topic === pair.topic);
84-
if (index !== -1) {
85-
throw new Error('Pair already exists');
110+
if (pairs.some((item) => item.topic === pair.topic)) {
111+
return pairs;
86112
}
87-
88113
return [...pairs, pair];
89114
});
90115
}, []);
91116

92117
const removeSessionFromPair = useCallback((sessionTopic: string) => {
93-
const [, setPairs] = pairsRef.current;
118+
const [latestPairs, setPairs] = pairsRef.current;
119+
pairsRef.current = [
120+
latestPairs.map((pair) => ({
121+
...pair,
122+
sessions: pair.sessions.filter((item) => item.topic !== sessionTopic),
123+
})),
124+
setPairs,
125+
];
126+
94127
setPairs((pairs: Pair[]) =>
95128
pairs.map((pair) => ({
96129
...pair,

packages/gui/src/index.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,31 @@ import { createRoot } from 'react-dom/client';
66
import App from './components/app/App';
77
import initPrefs from './init-prefs';
88

9+
// The WalletConnect SDK can reject internal promises with "No matching key"
10+
// during benign stale-pairing races (e.g. reconnecting after storage cleanup)
11+
// from async paths outside our try/catch sites, producing noisy errors in
12+
// DevTools. Suppress those specific rejections but leave a visible breadcrumb
13+
// so they are not completely silent, and scope the suppression to
14+
// WalletConnect-originated errors so unrelated code paths are not swallowed.
15+
window.addEventListener('unhandledrejection', (event) => {
16+
const { reason } = event;
17+
const message = reason?.message ?? String(reason);
18+
const stack: string = reason?.stack ?? '';
19+
20+
if (!message.includes('No matching key')) {
21+
return;
22+
}
23+
24+
const isWalletConnectOrigin = /walletconnect/i.test(stack);
25+
if (!isWalletConnectOrigin) {
26+
console.warn('[chia-gui] "No matching key" rejection from non-WalletConnect origin; not suppressing:', reason);
27+
return;
28+
}
29+
30+
console.info('[chia-gui] Suppressing WalletConnect stale-key rejection:', message);
31+
event.preventDefault();
32+
});
33+
934
// we need to use additional root for hot reloading
1035
function Root() {
1136
return <App />;

packages/gui/src/util/walletConnect.ts

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const STANDARD_ERROR_MAP = {
2222

2323
export function processError(error: Error) {
2424
if (error.message.includes('No matching key')) {
25-
log('Pairing not found');
25+
console.info('[chia-gui:walletConnect] Pairing not found (stale key, safe to ignore):', error.message);
2626
return;
2727
}
2828

@@ -48,12 +48,14 @@ export async function processSessionProposal(
4848
chia?: {
4949
chains: string[];
5050
methods: string[];
51+
events?: string[];
5152
};
5253
};
5354
optionalNamespaces?: {
5455
chia?: {
5556
chains: string[];
5657
methods: string[];
58+
events?: string[];
5759
};
5860
};
5961
};
@@ -90,6 +92,7 @@ export async function processSessionProposal(
9092

9193
const chains = [...new Set([...(requiredChia?.chains ?? []), ...(optionalChia?.chains ?? [])])];
9294
const methods = [...new Set([...(requiredChia?.methods ?? []), ...(optionalChia?.methods ?? [])])];
95+
const events = [...new Set([...(requiredChia?.events ?? []), ...(optionalChia?.events ?? [])])];
9396
const chain = chains.find((item) => ['chia:testnet', 'chia:mainnet'].includes(item));
9497
if (!chain) {
9598
throw new Error('Chain not supported');
@@ -119,7 +122,7 @@ export async function processSessionProposal(
119122
chia: {
120123
accounts,
121124
methods,
122-
events: [],
125+
events,
123126
},
124127
};
125128

@@ -148,22 +151,13 @@ export async function processSessionProposal(
148151
} catch (error) {
149152
try {
150153
log('Session proposal error', error);
154+
console.error('WC session proposal REJECTED due to error:', error);
151155

152-
const {
153-
id,
154-
params: { pairingTopic },
155-
} = event;
156+
const { id } = event;
156157

157-
await client?.respond({
158-
topic: pairingTopic,
159-
response: {
160-
id,
161-
jsonrpc: '2.0',
162-
error: {
163-
code: -32_600,
164-
message: (error as Error).message ?? 'Invalid Session Proposal',
165-
},
166-
},
158+
await client?.reject({
159+
id,
160+
reason: getSdkError('USER_REJECTED'),
167161
});
168162
} catch (e) {
169163
processError(e as Error);
@@ -282,40 +276,27 @@ export async function disconnectPair(client: Client, pairs: Pairs, topic: string
282276
const pairings = await client.core.pairing.getPairings();
283277
const pairing = pairings.find((p) => p.topic === topic);
284278
if (pairing) {
285-
// disconnect all sessions
286279
const sessions = pairs.getPair(topic)?.sessions ?? [];
287280
await Promise.all(
288281
sessions.map(async (session) => {
289282
try {
290283
await client.disconnect({ topic: session.topic, reason: getSdkError('USER_DISCONNECTED') });
291284
} catch (e) {
292-
if (e instanceof Error && e.message.includes('No matching key')) {
293-
log(`Session was already disconnected ${session.topic}`);
294-
// we can ignore this error because it means that session was already disconnected
295-
return;
296-
}
297-
298-
throw e;
285+
log(`Failed to disconnect session ${session.topic}:`, e);
299286
}
300287
}),
301288
);
302289

303-
// then disconnect pairing
304290
try {
305291
await client.core.pairing.disconnect({ topic });
306292
} catch (e) {
307-
if (e instanceof Error && e.message.includes('No matching key')) {
308-
log(`Pairing was already disconnected ${topic}`);
309-
// we can ignore this error because it means that session was already disconnected
310-
return;
311-
}
312-
throw e;
293+
log(`Failed to disconnect pairing ${topic}:`, e);
313294
}
314295
}
315-
316-
pairs.removePair(topic);
317296
} catch (e) {
318-
processError(e as Error);
297+
log('Error during pair disconnect, removing pair anyway:', e);
298+
} finally {
299+
pairs.removePair(topic);
319300
}
320301
}
321302

0 commit comments

Comments
 (0)