Skip to content

Commit 30f1084

Browse files
Merge pull request #2315 from trilitech/wc_errh
feat: WalletConnect - advanced error handling
2 parents 0224ca5 + 3a33a40 commit 30f1084

File tree

10 files changed

+3099
-74
lines changed

10 files changed

+3099
-74
lines changed

apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Box, Button, Divider, Flex, Heading, Link, Text, VStack } from "@chakra-ui/react";
22
import { errorsActions, useAppDispatch, useAppSelector } from "@umami/state";
3-
import { handleTezError } from "@umami/utils";
43

54
import { useColor } from "../../../styles/useColor";
65
import { EmptyMessage } from "../../EmptyMessage";
@@ -61,8 +60,7 @@ export const ErrorLogsMenu = () => {
6160
</Text>
6261
{errorLog.technicalDetails && (
6362
<Text marginTop="12px" color={color("700")} size="sm">
64-
{handleTezError({ name: "unknown", message: errorLog.technicalDetails }) ??
65-
""}
63+
{JSON.stringify(errorLog.technicalDetails)}
6664
</Text>
6765
)}
6866
</Flex>

apps/web/src/components/WalletConnect/WalletConnectProvider.tsx

+4-15
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import {
1212
walletKit,
1313
} from "@umami/state";
1414
import { type Network } from "@umami/tezos";
15-
import { CustomError, WalletConnectError } from "@umami/utils";
15+
import { CustomError, WalletConnectError, WcErrorCode, getWcErrorResponse } from "@umami/utils";
1616
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
1717
import { type SessionTypes } from "@walletconnect/types";
18-
import { type SdkErrorKey, getSdkError } from "@walletconnect/utils";
18+
import { getSdkError } from "@walletconnect/utils";
1919
import { type PropsWithChildren, useCallback, useEffect, useRef } from "react";
2020

2121
import { SessionProposalModal } from "./SessionProposalModal";
@@ -94,7 +94,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
9494
handleAsyncActionUnsafe(async () => {
9595
const activeSessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
9696
if (!(event.topic in activeSessions)) {
97-
throw new WalletConnectError("Session not found", "INVALID_EVENT", null);
97+
throw new WalletConnectError("Session not found", WcErrorCode.SESSION_NOT_FOUND, null);
9898
}
9999

100100
const session = activeSessions[event.topic];
@@ -105,19 +105,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
105105
await handleWcRequest(event, session);
106106
}).catch(async error => {
107107
const { id, topic } = event;
108-
let sdkErrorKey: SdkErrorKey =
109-
error instanceof WalletConnectError ? error.sdkError : "SESSION_SETTLEMENT_FAILED";
110-
if (sdkErrorKey === "USER_REJECTED") {
111-
console.info("WC request rejected", sdkErrorKey, event, error);
112-
} else {
113-
if (error.message.includes("delegate.unchanged")) {
114-
sdkErrorKey = "INVALID_EVENT";
115-
}
116-
console.warn("WC request failed", sdkErrorKey, event, error);
117-
}
108+
const response = formatJsonRpcError(id, getWcErrorResponse(error));
118109
// dApp is waiting so we need to notify it
119-
const sdkErrorMessage = getSdkError(sdkErrorKey).message;
120-
const response = formatJsonRpcError(id, sdkErrorMessage);
121110
await walletKit.respondSessionRequest({ topic, response });
122111
}),
123112
[handleAsyncActionUnsafe, handleWcRequest, toast]

apps/web/src/components/WalletConnect/useHandleWcRequest.tsx

+36-17
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ import {
1414
useGetOwnedAccountSafe,
1515
walletKit,
1616
} from "@umami/state";
17-
import { WalletConnectError } from "@umami/utils";
17+
import { WalletConnectError, WcErrorCode } from "@umami/utils";
1818
import { formatJsonRpcError, formatJsonRpcResult } from "@walletconnect/jsonrpc-utils";
1919
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";
20-
import { type SdkErrorKey, getSdkError } from "@walletconnect/utils";
2120

2221
import { SignPayloadRequestModal } from "../common/SignPayloadRequestModal";
2322
import { BatchSignPage } from "../SendFlow/common/BatchSignPage";
@@ -61,11 +60,25 @@ export const useHandleWcRequest = () => {
6160
let modal;
6261
let onClose;
6362

63+
const handleUserRejected = () => {
64+
// dApp is waiting so we need to notify it
65+
const response = formatJsonRpcError(id, {
66+
code: WcErrorCode.USER_REJECTED,
67+
message: "User rejected the request",
68+
});
69+
console.info("WC request rejected by user", event, response);
70+
void walletKit.respondSessionRequest({ topic, response });
71+
};
72+
6473
switch (request.method) {
6574
case "tezos_getAccounts": {
6675
const wcPeers = walletKit.getActiveSessions();
6776
if (!(topic in wcPeers)) {
68-
throw new WalletConnectError(`Unknown session ${topic}`, "UNAUTHORIZED_EVENT", null);
77+
throw new WalletConnectError(
78+
`Unknown session ${topic}`,
79+
WcErrorCode.SESSION_NOT_FOUND,
80+
null
81+
);
6982
}
7083
const session = wcPeers[topic];
7184
const accountPkh = session.namespaces.tezos.accounts[0].split(":")[2];
@@ -89,15 +102,20 @@ export const useHandleWcRequest = () => {
89102

90103
case "tezos_sign": {
91104
if (!request.params.account) {
92-
throw new WalletConnectError("Missing account in request", "INVALID_EVENT", session);
105+
throw new WalletConnectError(
106+
"Missing account in request",
107+
WcErrorCode.MISSING_ACCOUNT_IN_REQUEST,
108+
session
109+
);
93110
}
94111
const signer = getImplicitAccount(request.params.account);
95112
const network = findNetwork(chainId.split(":")[1]);
96113
if (!network) {
97114
throw new WalletConnectError(
98115
`Unsupported network ${chainId}`,
99-
"UNSUPPORTED_CHAINS",
100-
session
116+
WcErrorCode.UNSUPPORTED_CHAINS,
117+
session,
118+
chainId
101119
);
102120
}
103121

@@ -115,24 +133,24 @@ export const useHandleWcRequest = () => {
115133

116134
modal = <SignPayloadRequestModal opts={signPayloadProps} />;
117135
onClose = () => {
118-
const sdkErrorKey: SdkErrorKey = "USER_REJECTED";
119-
console.info("WC request rejected by user", sdkErrorKey, event);
120-
// dApp is waiting so we need to notify it
121-
const response = formatJsonRpcError(id, getSdkError(sdkErrorKey).message);
122-
void walletKit.respondSessionRequest({ topic, response });
136+
handleUserRejected();
123137
};
124138
return openWith(modal, { onClose });
125139
}
126140

127141
case "tezos_send": {
128142
if (!request.params.account) {
129-
throw new WalletConnectError("Missing account in request", "INVALID_EVENT", session);
143+
throw new WalletConnectError(
144+
"Missing account in request",
145+
WcErrorCode.MISSING_ACCOUNT_IN_REQUEST,
146+
session
147+
);
130148
}
131149
const signer = getAccount(request.params.account);
132150
if (!signer) {
133151
throw new WalletConnectError(
134152
`Unknown account, no signer: ${request.params.account}`,
135-
"UNAUTHORIZED_EVENT",
153+
WcErrorCode.INTERNAL_SIGNER_IS_MISSING,
136154
session
137155
);
138156
}
@@ -144,7 +162,7 @@ export const useHandleWcRequest = () => {
144162
if (!network) {
145163
throw new WalletConnectError(
146164
`Unsupported network ${chainId}`,
147-
"UNSUPPORTED_CHAINS",
165+
WcErrorCode.UNSUPPORTED_CHAINS,
148166
session
149167
);
150168
}
@@ -168,16 +186,17 @@ export const useHandleWcRequest = () => {
168186
modal = <BatchSignPage {...signProps} {...event.params.request.params} />;
169187
}
170188
onClose = () => {
171-
throw new WalletConnectError("Rejected by user", "USER_REJECTED", session);
189+
handleUserRejected();
172190
};
173191

174192
return openWith(modal, { onClose });
175193
}
176194
default:
177195
throw new WalletConnectError(
178196
`Unsupported method ${request.method}`,
179-
"WC_METHOD_UNSUPPORTED",
180-
session
197+
WcErrorCode.METHOD_UNSUPPORTED,
198+
session,
199+
request.method
181200
);
182201
}
183202
});

packages/data-polling/src/useReactQueryErrorHandler.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ export const useReactQueryErrorHandler = () => {
1414
return;
1515
}
1616
dispatch(errorsActions.add(getErrorContext(error)));
17+
const context = getErrorContext(error);
1718

1819
if (!toast.isActive(toastId)) {
1920
toast({
2021
id: toastId,
21-
description: `Data fetching error: ${error.message}`,
22+
description: `Data fetching error: ${context.description}`,
2223
status: "error",
2324
isClosable: true,
2425
duration: 10000,

packages/state/src/hooks/useAsyncActionHandler.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const useAsyncActionHandler = () => {
3838
try {
3939
return await fn();
4040
} catch (error: any) {
41-
const errorContext = getErrorContext(error);
41+
const errorContext = getErrorContext(error, true);
4242

4343
// Prevent double toast and record of the same error if case of nested handleAsyncActionUnsafe calls.
4444
// Still we need to re-throw the error to propagate it to the caller.

packages/state/src/slices/errors.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe("Errors reducer", () => {
2929
description: `error ${i}`,
3030
stacktrace: "stacktrace",
3131
technicalDetails: "technicalDetails",
32+
code: i,
3233
})
3334
);
3435
}

packages/test-utils/src/errorContext.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ export const errorContext1 = {
22
timestamp: "2023-08-03T19:27:43.735Z",
33
description: "error1",
44
stacktrace: "stacktrace",
5+
code: 100,
56
technicalDetails: "technicalDetails",
67
};
78

89
export const errorContext2 = {
910
timestamp: "2023-08-03T20:21:58.395Z",
1011
description: "error1",
1112
stacktrace: "stacktrace",
13+
code: 200,
1214
technicalDetails: "technicalDetails",
1315
};

packages/utils/src/ErrorContext.test.ts

+88-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { CustomError, WalletConnectError, getErrorContext, handleTezError } from "./ErrorContext";
1+
import { TezosOperationError, type TezosOperationErrorWithMessage } from "@taquito/taquito";
2+
3+
import {
4+
CustomError,
5+
WalletConnectError,
6+
WcErrorCode,
7+
getErrorContext,
8+
getTezErrorMessage,
9+
getWcErrorResponse,
10+
} from "./ErrorContext";
211

312
describe("getErrorContext", () => {
413
it("should handle error object with message and stack", () => {
@@ -12,7 +21,7 @@ describe("getErrorContext", () => {
1221
expect(context.technicalDetails).toBe("some error message");
1322
expect(context.stacktrace).toBe("some stacktrace");
1423
expect(context.description).toBe(
15-
"Something went wrong. Please try again or contact support if the issue persists."
24+
"Something went wrong. Please try again. Contact support if the issue persists."
1625
);
1726
expect(context.timestamp).toBeDefined();
1827
});
@@ -25,7 +34,7 @@ describe("getErrorContext", () => {
2534
expect(context.technicalDetails).toBe("string error message");
2635
expect(context.stacktrace).toBe("");
2736
expect(context.description).toBe(
28-
"Something went wrong. Please try again or contact support if the issue persists."
37+
"Something went wrong. Please try again. Contact support if the issue persists."
2938
);
3039
expect(context.timestamp).toBeDefined();
3140
});
@@ -48,53 +57,118 @@ describe("getErrorContext", () => {
4857

4958
const context = getErrorContext(error);
5059

51-
expect(context.technicalDetails).toBe("");
60+
expect(context.technicalDetails).toBeUndefined();
5261
expect(context.description).toBe("Custom error message");
5362
expect(context.stacktrace).toBeDefined();
5463
expect(context.timestamp).toBeDefined();
5564
});
5665
it("should handle WalletConnectError instances", () => {
57-
const error = new WalletConnectError("Custom WC error message", "UNSUPPORTED_EVENTS", null);
66+
const error = new WalletConnectError(
67+
"Custom WC error message",
68+
WcErrorCode.INTERNAL_ERROR,
69+
null
70+
);
5871

5972
const context = getErrorContext(error);
6073

61-
expect(context.technicalDetails).toBe("");
74+
expect(context.technicalDetails).toBeUndefined();
6275
expect(context.description).toBe("Custom WC error message");
6376
expect(context.stacktrace).toBeDefined();
6477
expect(context.timestamp).toBeDefined();
6578
});
6679
});
6780

68-
describe("handleTezError", () => {
81+
describe("getTezErrorMessage", () => {
6982
it("catches subtraction_underflow", () => {
70-
const res = handleTezError(new Error("subtraction_underflow"));
83+
const res = getTezErrorMessage("subtraction_underflow");
7184
expect(res).toBe("Insufficient balance, please make sure you have enough funds.");
7285
});
7386

7487
it("catches non_existing_contract", () => {
75-
const res = handleTezError(new Error("contract.non_existing_contract"));
88+
const res = getTezErrorMessage("contract.non_existing_contract");
7689
expect(res).toBe("Contract does not exist, please check if the correct network is selected.");
7790
});
7891

7992
it("catches staking_to_delegate_that_refuses_external_staking", () => {
80-
const res = handleTezError(new Error("staking_to_delegate_that_refuses_external_staking"));
93+
const res = getTezErrorMessage("staking_to_delegate_that_refuses_external_staking");
8194
expect(res).toBe("The baker you are trying to stake to does not accept external staking.");
8295
});
8396

8497
it("catches empty_implicit_delegated_contract", () => {
85-
const res = handleTezError(new Error("empty_implicit_delegated_contract"));
98+
const res = getTezErrorMessage("empty_implicit_delegated_contract");
8699
expect(res).toBe(
87100
"Emptying an implicit delegated account is not allowed. End delegation before trying again."
88101
);
89102
});
90103

91104
it("catches delegate.unchanged", () => {
92-
const res = handleTezError(new Error("delegate.unchanged"));
105+
const res = getTezErrorMessage("delegate.unchanged");
93106
expect(res).toBe("The delegate is unchanged. Delegation to this address is already done.");
94107
});
95108

109+
it("catches contract.manager.unregistered_delegate", () => {
110+
const res = getTezErrorMessage("contract.manager.unregistered_delegate");
111+
expect(res).toBe(
112+
"The provided delegate address is not registered as a delegate. Verify the delegate address and ensure it is active."
113+
);
114+
});
115+
96116
it("returns undefined for unknown errors", () => {
97-
const err = new Error("unknown error");
98-
expect(handleTezError(err)).toBeUndefined();
117+
const err = "unknown error";
118+
expect(getTezErrorMessage(err)).toBeUndefined();
119+
});
120+
121+
it("should return default error message for unknown error", () => {
122+
const error = new Error("Unknown error");
123+
const context = getErrorContext(error);
124+
expect(context.description).toBe(
125+
"Something went wrong. Please try again. Contact support if the issue persists."
126+
);
127+
});
128+
129+
it("should return custom error message for CustomError", () => {
130+
const error = new CustomError("Custom error message");
131+
const context = getErrorContext(error);
132+
expect(context.description).toBe("Custom error message");
133+
});
134+
135+
it("should return WalletConnectError message", () => {
136+
const error = new WalletConnectError("WC error custom text", WcErrorCode.INTERNAL_ERROR, null);
137+
const context = getErrorContext(error);
138+
expect(context.description).toBe("WC error custom text");
139+
expect(context.code).toBe(WcErrorCode.INTERNAL_ERROR);
140+
expect(context.technicalDetails).toBeUndefined();
141+
});
142+
143+
it("should return TezosOperationError message", () => {
144+
// const error = new TezosOperationError(errors:[], lastError: { id: 'michelson_v1.script_rejected', with: { prim: 'Unit' } });
145+
const mockError: TezosOperationErrorWithMessage = {
146+
kind: "temporary",
147+
id: "proto.020-PsParisC.michelson_v1.script_rejected",
148+
with: { string: "Fail entrypoint" }, // Include the `with` field for testing
149+
};
150+
const error = new TezosOperationError(
151+
[mockError],
152+
"Operation failed due to a rejected script.",
153+
[]
154+
);
155+
const context = getErrorContext(error);
156+
expect(context.description).toContain(
157+
"Rejected by chain. The contract code failed to run. Please check the contract. Details: Fail entrypoint"
158+
);
159+
expect(context.technicalDetails).toEqual([
160+
"proto.020-PsParisC.michelson_v1.script_rejected",
161+
{ with: { string: "Fail entrypoint" } },
162+
]);
163+
});
164+
165+
it("should return error response for getWcErrorResponse", () => {
166+
const error = new Error("Unknown error");
167+
const response = getWcErrorResponse(error);
168+
expect(response.message).toBe(
169+
"Something went wrong. Please try again. Contact support if the issue persists."
170+
);
171+
expect(response.code).toBe(WcErrorCode.INTERNAL_ERROR);
172+
expect(response.data).toBe("Unknown error");
99173
});
100174
});

0 commit comments

Comments
 (0)