Skip to content

Commit 0d12263

Browse files
committed
add error handling
1 parent 365497d commit 0d12263

File tree

7 files changed

+328
-42
lines changed

7 files changed

+328
-42
lines changed

src/App.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { VaultList, VaultInfo } from './Vaults';
1616
import { ProductsList, ProductInfo } from './Products';
1717
import { AccountPanel } from './Account';
1818
import { EulerSwapBrowse, EulerSwapShowInstance } from './EulerSwap';
19+
import { ContractErrorMessage } from "./ErrorBoundary";
1920

2021

2122
const wagmiConfig = getDefaultConfig({

src/Ctx.jsx

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import fromExponential from 'from-exponential';
66

77
import * as Lens from "./Lens";
88
import * as Utils from './Utils';
9+
import { ContractErrorMessage } from "./ErrorBoundary";
910

1011

1112
export class GlobalContext {
@@ -26,38 +27,57 @@ export class GlobalContext {
2627
this.walletClient = walletClient;
2728
this.queryClient = useQueryClient();
2829

29-
let { data: currChain } = Lens.useEulerChain();
30-
let { data: labels } = Lens.useLabels();
31-
let { data: prices } = Lens.usePrices();
30+
let { data: currChain, error: currChainError, isError: currChainIsError } = Lens.useEulerChain();
31+
let { data: labels, error: labelsError, isError: labelsIsError } = Lens.useLabels();
32+
let { data: prices, error: pricesError, isError: pricesIsError } = Lens.usePrices();
3233

3334
let vaultAddrsLabels = labels ? Object.keys(labels.vaults) : undefined;
3435
let vaultAddrsExtra = Object.keys(this.args.extraVaultAddrs[currChain?.chainId] || {});
3536
let mySubaccountMask = (1n << BigInt(this.args.numSubAccounts)) - 1n;
3637

37-
let { data: vaultsStaticLabels } = Lens.useVaultsStaticInfo(vaultAddrsLabels);
38-
let { data: vaultsStaticExtra } = Lens.useVaultsStaticInfo(vaultAddrsExtra);
38+
let { data: vaultsStaticLabels, error: vaultsStaticLabelsError, isError: vaultsStaticLabelsIsError } = Lens.useVaultsStaticInfo(vaultAddrsLabels);
39+
let { data: vaultsStaticExtra, error: vaultsStaticExtraError, isError: vaultsStaticExtraIsError } = Lens.useVaultsStaticInfo(vaultAddrsExtra);
3940
if (vaultsStaticLabels && vaultsStaticExtra) this.vaultsStatic = { ...vaultsStaticLabels, ... vaultsStaticExtra, };
4041

41-
let { data: vaultsGlobalLabels } = Lens.useVaultsGlobal(vaultAddrsLabels);
42-
let { data: vaultsGlobalExtra } = Lens.useVaultsGlobal(vaultAddrsExtra);
42+
let { data: vaultsGlobalLabels, error: vaultsGlobalLabelsError, isError: vaultsGlobalLabelsIsError } = Lens.useVaultsGlobal(vaultAddrsLabels);
43+
let { data: vaultsGlobalExtra, error: vaultsGlobalExtraError, isError: vaultsGlobalExtraIsError } = Lens.useVaultsGlobal(vaultAddrsExtra);
4344
if (vaultsGlobalLabels && vaultsGlobalExtra) this.vaultsGlobal = { ...vaultsGlobalLabels, ...vaultsGlobalExtra, };
4445

45-
let { data: vaultsPersonalLabels } = Lens.useVaultsPersonalInfoMulti(this.myPrimaryAddr, mySubaccountMask, vaultAddrsLabels);
46-
let { data: vaultsPersonalExtra } = Lens.useVaultsPersonalInfoMulti(this.myPrimaryAddr, mySubaccountMask, vaultAddrsExtra);
46+
let { data: vaultsPersonalLabels, error: vaultsPersonalLabelsError, isError: vaultsPersonalLabelsIsError } = Lens.useVaultsPersonalInfoMulti(this.myPrimaryAddr, mySubaccountMask, vaultAddrsLabels);
47+
let { data: vaultsPersonalExtra, error: vaultsPersonalExtraError, isError: vaultsPersonalExtraIsError } = Lens.useVaultsPersonalInfoMulti(this.myPrimaryAddr, mySubaccountMask, vaultAddrsExtra);
4748
if (vaultsPersonalLabels && vaultsPersonalExtra) {
4849
this.vaultsPersonal = {};
4950
for (let k of Object.keys(vaultsPersonalLabels)) {
5051
this.vaultsPersonal[k] = { ...vaultsPersonalLabels[k], ...vaultsPersonalExtra[k], };
5152
}
5253
}
5354

55+
// Store error states for later use
56+
this.hasDataErrors = currChainIsError || labelsIsError || pricesIsError ||
57+
vaultsStaticLabelsIsError || vaultsStaticExtraIsError ||
58+
vaultsGlobalLabelsIsError || vaultsGlobalExtraIsError ||
59+
(this.connected && (vaultsPersonalLabelsIsError || vaultsPersonalExtraIsError));
60+
61+
this.dataErrors = {
62+
currChain: currChainError,
63+
labels: labelsError,
64+
prices: pricesError,
65+
vaultsStaticLabels: vaultsStaticLabelsError,
66+
vaultsStaticExtra: vaultsStaticExtraError,
67+
vaultsGlobalLabels: vaultsGlobalLabelsError,
68+
vaultsGlobalExtra: vaultsGlobalExtraError,
69+
vaultsPersonalLabels: vaultsPersonalLabelsError,
70+
vaultsPersonalExtra: vaultsPersonalExtraError,
71+
};
72+
5473
this.currChain = currChain;
5574
this.labels = labels;
5675
this.prices = prices;
5776

5877
this.chainConfigs = {};
5978

6079
this.maglevSupportsChain = !!currChain?.addresses.maglevAddrs;
80+
this.eulerSwapSupportsChain = !!currChain?.addresses.eulerSwapAddrs;
6181

6282
//console.log(!!this.maglevSupportsChain, !!this.labels, !!this.prices, !!this.vaultsStatic, !!this.vaultsGlobal, !!this.vaultsPersonal);
6383
this.ready = this.maglevSupportsChain && this.labels && this.prices && this.vaultsStatic && this.vaultsGlobal && (!this.connected || this.vaultsPersonal);
@@ -70,6 +90,15 @@ export class GlobalContext {
7090
}
7191

7292
loading() {
93+
if (this.hasDataErrors) {
94+
// Find the first error to display
95+
for (let [key, error] of Object.entries(this.dataErrors)) {
96+
if (error) {
97+
return <ContractErrorMessage error={error} componentName={`GlobalContext-${key}`} />;
98+
}
99+
}
100+
}
101+
73102
if (!this.maglevSupportsChain) return "Maglev not currently supported on this chain.";
74103

75104
return "Loading...";

src/ErrorBoundary.jsx

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { useState } from 'react';
2+
import { Button } from 'primereact/button';
3+
import { Message } from 'primereact/message';
4+
import { Card } from 'primereact/card';
5+
6+
export function ContractErrorMessage({ error, onRetry, componentName }) {
7+
const [showDetails, setShowDetails] = useState(false);
8+
9+
const isContractError = error?.message?.includes('ContractFunctionExecutionError') ||
10+
error?.message?.includes('ContractFunctionRevertedError') ||
11+
error?.cause?.name === 'ContractFunctionRevertedError';
12+
13+
const errorTitle = isContractError
14+
? 'Contract Call Failed'
15+
: 'Network Error';
16+
17+
const errorMessage = isContractError
18+
? `A contract function call failed. This could be due to network issues, contract state changes, or temporary blockchain congestion.`
19+
: `Unable to load data. Please check your network connection.`;
20+
21+
const handleRefresh = () => {
22+
if (onRetry) {
23+
onRetry();
24+
} else {
25+
window.location.reload();
26+
}
27+
};
28+
29+
return (
30+
<Card className="mt-4">
31+
<div className="text-center p-4">
32+
<div className="mb-3">
33+
<i className="pi pi-exclamation-triangle" style={{ fontSize: '3rem', color: '#f59e0b' }}></i>
34+
</div>
35+
36+
<h3 className="mb-3 text-orange-500">{errorTitle}</h3>
37+
38+
<p className="mb-4 text-gray-300">
39+
{errorMessage}
40+
{componentName && ` (${componentName})`}
41+
</p>
42+
43+
<div className="flex justify-content-center gap-3 mb-3">
44+
<Button
45+
label="Refresh"
46+
icon="pi pi-refresh"
47+
onClick={handleRefresh}
48+
className="p-button-warning"
49+
/>
50+
<Button
51+
label={showDetails ? "Hide Details" : "Show Details"}
52+
icon={showDetails ? "pi pi-eye-slash" : "pi pi-eye"}
53+
onClick={() => setShowDetails(!showDetails)}
54+
className="p-button-secondary"
55+
/>
56+
</div>
57+
58+
{showDetails && (
59+
<div className="mt-3 p-3 bg-gray-800 border-round text-left">
60+
<h5 className="mb-2">Error Details:</h5>
61+
<pre className="text-sm overflow-auto max-h-20rem">
62+
{error?.message || String(error)}
63+
</pre>
64+
</div>
65+
)}
66+
67+
<div className="mt-3">
68+
<Message
69+
severity="info"
70+
text="If this problem persists, try switching networks or refreshing your browser."
71+
className="w-full"
72+
/>
73+
</div>
74+
</div>
75+
</Card>
76+
);
77+
}
78+
79+
export function useErrorHandler(componentName) {
80+
return {
81+
onError: (error) => {
82+
console.warn(`Error in ${componentName}:`, error);
83+
// Don't throw - let the component handle the error state
84+
return false;
85+
},
86+
throwOnError: false, // Disable throwing to prevent black screens
87+
retry: false, // Disable automatic retries that might spam
88+
};
89+
}
90+
91+
// Hook to provide consistent error handling options for React Query
92+
export function getErrorHandlingOptions(componentName) {
93+
return {
94+
throwOnError: false,
95+
retry: (failureCount, error) => {
96+
// Only retry network errors, not contract errors
97+
const isContractError = error?.message?.includes('ContractFunctionExecutionError') ||
98+
error?.message?.includes('ContractFunctionRevertedError');
99+
100+
if (isContractError) return false;
101+
102+
// Retry network errors up to 2 times with exponential backoff
103+
return failureCount < 2;
104+
},
105+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
106+
onError: (error) => {
107+
console.warn(`Query error in ${componentName}:`, error);
108+
}
109+
};
110+
}
111+
112+
// Component wrapper that shows error state instead of crashing
113+
export function withErrorHandling(Component, componentName) {
114+
return function ErrorHandledComponent(props) {
115+
const [hasError, setHasError] = useState(false);
116+
const [error, setError] = useState(null);
117+
118+
const handleRetry = () => {
119+
setHasError(false);
120+
setError(null);
121+
// Force re-render
122+
window.location.reload();
123+
};
124+
125+
if (hasError) {
126+
return <ContractErrorMessage
127+
error={error}
128+
onRetry={handleRetry}
129+
componentName={componentName}
130+
/>;
131+
}
132+
133+
try {
134+
return <Component {...props} />;
135+
} catch (err) {
136+
setHasError(true);
137+
setError(err);
138+
return <ContractErrorMessage
139+
error={err}
140+
onRetry={handleRetry}
141+
componentName={componentName}
142+
/>;
143+
}
144+
};
145+
}

0 commit comments

Comments
 (0)