Skip to content

Commit e77f788

Browse files
[gdpatchagent] fix(core,citizen-claim-widget): add switch-chain timeout, fix custodial misconfig status, use shared chain constant
- provider.tsx: some WalletConnect sessions never resolve or reject wallet_switchEthereumChain, so the request could hang switchChain forever and never reach the integrator override fallback. Races it against a 10s timeout, treated like any other rejection. - adapter.ts: when custodial execution has no configured client for any supported chain, this was reported as 'unsupported_chain', driving a "switch network" narrative the user has no way to act on (custodial mode has no wallet chain to switch). Now reports 'error' with a clear message instead, reserving 'unsupported_chain' for the real switchable case. - CitizenClaimWidget.tsx: replaced a hard-coded 42220 fallback chain id with the existing SupportedChains.CELO constant already used elsewhere. On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
1 parent 0ed69d5 commit e77f788

3 files changed

Lines changed: 58 additions & 12 deletions

File tree

packages/citizen-claim-widget/src/CitizenClaimWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ function CitizenClaimShell({
621621
]}
622622
activeTab={activeTab}
623623
onTabChange={(tabId: string) => setActiveTab(tabId as CitizenClaimTab)}
624-
chainId={chainId ?? fallbackChainId ?? 42220}
624+
chainId={chainId ?? fallbackChainId ?? SupportedChains.CELO}
625625
/>
626626
{activeTab === 'claim' ? (
627627
<>

packages/citizen-claim-widget/src/adapter.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -550,13 +550,29 @@ export function useCitizenClaimAdapter(
550550
null
551551
: chainId
552552

553+
if (isCustodialExecution && statusChainId === null) {
554+
await auxiliaryReads
555+
// Custodial execution has no wallet chain to switch, so a missing
556+
// client for every supported chain is an integrator configuration
557+
// problem rather than something the "switch network" narrative below
558+
// could ever resolve — route it to a plain error instead.
559+
setAmount(null)
560+
setNextClaimTime(null)
561+
setStatus('error')
562+
setError(
563+
humanReadableError(
564+
new CitizenClaimAdapterError('Claim execution is not configured for any supported chain.'),
565+
),
566+
)
567+
return
568+
}
569+
553570
if (statusChainId === null || !isSupportedChain(statusChainId)) {
554571
await auxiliaryReads
555-
// Chain is known but unsupported (or custodial has no chain configured
556-
// at all) — a distinct status from not_connected so the UI can show
557-
// "switch chain" copy instead of misleadingly asking an already-connected
558-
// wallet to connect. Clear personalized entitlement from whatever chain
559-
// was previously active.
572+
// Chain is known but unsupported — a distinct status from not_connected
573+
// so the UI can show "switch chain" copy instead of misleadingly asking
574+
// an already-connected wallet to connect. Clear personalized entitlement
575+
// from whatever chain was previously active.
560576
setAmount(null)
561577
setNextClaimTime(null)
562578
setStatus('unsupported_chain')

packages/core/src/provider.tsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,36 @@ export interface GoodWidgetContextValue extends GoodWidgetState {
4242
}
4343

4444
const SWITCH_CHAIN_UNAVAILABLE_ERROR = 'No wallet provider available to switch chains'
45+
const SWITCH_CHAIN_TIMEOUT_ERROR = 'Timed out waiting for the wallet to respond to the network switch request'
46+
const SWITCH_CHAIN_REQUEST_TIMEOUT_MS = 10_000
4547

4648
const noopSwitchChain = async () => {
4749
throw new Error(SWITCH_CHAIN_UNAVAILABLE_ERROR)
4850
}
4951

52+
/**
53+
* Races a promise against a timeout so a request the wallet never settles
54+
* (some WalletConnect sessions never resolve or reject
55+
* wallet_switchEthereumChain at all) doesn't hang the caller forever — a
56+
* timeout is treated the same as any other rejection, so switchChain's
57+
* existing override fallback below still applies.
58+
*/
59+
function raceWithTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> {
60+
return new Promise((resolve, reject) => {
61+
const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs)
62+
promise.then(
63+
(value) => {
64+
clearTimeout(timer)
65+
resolve(value)
66+
},
67+
(err) => {
68+
clearTimeout(timer)
69+
reject(err)
70+
},
71+
)
72+
})
73+
}
74+
5075
/**
5176
* EIP-1193's standard user-rejection code (4001), plus ethers v6's
5277
* ACTION_REJECTED — both mean the wallet's own switch-chain prompt was
@@ -206,16 +231,21 @@ export function GoodWidgetProvider({
206231

207232
// Tries the standard EIP-3326 request first; falls back to the
208233
// integrator's own switch/network-modal flow (e.g. AppKit) when the
209-
// active connector rejects or does not support it — some WalletConnect
210-
// sessions never resolve wallet_switchEthereumChain at all.
234+
// active connector rejects, does not support it, or never settles the
235+
// request at all (some WalletConnect sessions do this, hence the timeout
236+
// race below rather than a bare await).
211237
const switchChain = useCallback(
212238
async (targetChainId: number) => {
213239
if (resolvedProvider) {
214240
try {
215-
await resolvedProvider.request({
216-
method: 'wallet_switchEthereumChain',
217-
params: [{ chainId: `0x${targetChainId.toString(16)}` }],
218-
})
241+
await raceWithTimeout(
242+
resolvedProvider.request({
243+
method: 'wallet_switchEthereumChain',
244+
params: [{ chainId: `0x${targetChainId.toString(16)}` }],
245+
}),
246+
SWITCH_CHAIN_REQUEST_TIMEOUT_MS,
247+
SWITCH_CHAIN_TIMEOUT_ERROR,
248+
)
219249
return
220250
} catch (err) {
221251
if (!switchChainOverride || isUserRejectedSwitchChain(err)) throw err

0 commit comments

Comments
 (0)