Skip to content

Commit a654fb7

Browse files
feat: WalletConnect: error handling
1 parent 6eb4ad8 commit a654fb7

File tree

6 files changed

+223
-71
lines changed

6 files changed

+223
-71
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

+15-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type EventEmitter from "events";
33
import { type NetworkType } from "@airgap/beacon-wallet";
44
import { useToast } from "@chakra-ui/react";
55
import { type WalletKitTypes } from "@reown/walletkit";
6+
import { TezosOperationError } from "@taquito/taquito";
67
import { useDynamicModalContext } from "@umami/components";
78
import {
89
createWalletKit,
@@ -12,10 +13,10 @@ import {
1213
walletKit,
1314
} from "@umami/state";
1415
import { type Network } from "@umami/tezos";
15-
import { CustomError, WalletConnectError } from "@umami/utils";
16+
import { CustomError, WalletConnectError, type WcErrorKey, getWcErrorResponse } from "@umami/utils";
1617
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
1718
import { type SessionTypes } from "@walletconnect/types";
18-
import { type SdkErrorKey, getSdkError } from "@walletconnect/utils";
19+
import { getSdkError } from "@walletconnect/utils";
1920
import { type PropsWithChildren, useCallback, useEffect, useRef } from "react";
2021

2122
import { SessionProposalModal } from "./SessionProposalModal";
@@ -94,7 +95,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
9495
handleAsyncActionUnsafe(async () => {
9596
const activeSessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
9697
if (!(event.topic in activeSessions)) {
97-
throw new WalletConnectError("Session not found", "INVALID_EVENT", null);
98+
throw new WalletConnectError("Session not found", "SESSION_NOT_FOUND", null);
9899
}
99100

100101
const session = activeSessions[event.topic];
@@ -105,19 +106,20 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
105106
await handleWcRequest(event, session);
106107
}).catch(async error => {
107108
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);
109+
let wcErrorKey: WcErrorKey = "UNKNOWN_ERROR";
110+
111+
if (error instanceof WalletConnectError) {
112+
wcErrorKey = error.wcError;
113+
} else if (error instanceof TezosOperationError) {
114+
wcErrorKey = "REJECTED_BY_CHAIN";
115+
}
116+
const response = formatJsonRpcError(id, getWcErrorResponse(error));
117+
if (wcErrorKey === "USER_REJECTED") {
118+
console.info("WC request rejected", wcErrorKey, event, error);
112119
} else {
113-
if (error.message.includes("delegate.unchanged")) {
114-
sdkErrorKey = "INVALID_EVENT";
115-
}
116-
console.warn("WC request failed", sdkErrorKey, event, error);
120+
console.warn("WC request failed", wcErrorKey, event, error, response);
117121
}
118122
// dApp is waiting so we need to notify it
119-
const sdkErrorMessage = getSdkError(sdkErrorKey).message;
120-
const response = formatJsonRpcError(id, sdkErrorMessage);
121123
await walletKit.respondSessionRequest({ topic, response });
122124
}),
123125
[handleAsyncActionUnsafe, handleWcRequest, toast]

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

+29-16
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ import {
1515
useGetOwnedAccountSafe,
1616
walletKit,
1717
} from "@umami/state";
18-
import { WalletConnectError } from "@umami/utils";
18+
import { WC_ERRORS, WalletConnectError } from "@umami/utils";
1919
import { formatJsonRpcError, formatJsonRpcResult } from "@walletconnect/jsonrpc-utils";
2020
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";
21-
import { type SdkErrorKey, getSdkError } from "@walletconnect/utils";
2221

2322
import { SignPayloadRequestModal } from "../common/SignPayloadRequestModal";
2423
import { BatchSignPage } from "../SendFlow/common/BatchSignPage";
@@ -62,11 +61,18 @@ export const useHandleWcRequest = () => {
6261
let modal;
6362
let onClose;
6463

64+
const handleUserRejected = () => {
65+
// dApp is waiting so we need to notify it
66+
const response = formatJsonRpcError(id, WC_ERRORS.USER_REJECTED);
67+
console.info("WC request rejected by user", event, response);
68+
void walletKit.respondSessionRequest({ topic, response });
69+
};
70+
6571
switch (request.method) {
6672
case "tezos_getAccounts": {
6773
const wcPeers = walletKit.getActiveSessions();
6874
if (!(topic in wcPeers)) {
69-
throw new WalletConnectError(`Unknown session ${topic}`, "UNAUTHORIZED_EVENT", null);
75+
throw new WalletConnectError(`Unknown session ${topic}`, "SESSION_NOT_FOUND", null);
7076
}
7177
const session = wcPeers[topic];
7278
const accountPkh = session.namespaces.tezos.accounts[0].split(":")[2];
@@ -77,7 +83,8 @@ export const useHandleWcRequest = () => {
7783
throw new WalletConnectError(
7884
`Unsupported network ${networkName}`,
7985
"UNSUPPORTED_CHAINS",
80-
session
86+
session,
87+
networkName
8188
);
8289
}
8390
const { publicKey, curve } = await getPublicKeyAndCurve(
@@ -104,15 +111,20 @@ export const useHandleWcRequest = () => {
104111

105112
case "tezos_sign": {
106113
if (!request.params.account) {
107-
throw new WalletConnectError("Missing account in request", "INVALID_EVENT", session);
114+
throw new WalletConnectError(
115+
"Missing account in request",
116+
"MISSING_ACCOUNT_IN_REQUEST",
117+
session
118+
);
108119
}
109120
const signer = getImplicitAccount(request.params.account);
110121
const network = findNetwork(chainId.split(":")[1]);
111122
if (!network) {
112123
throw new WalletConnectError(
113124
`Unsupported network ${chainId}`,
114125
"UNSUPPORTED_CHAINS",
115-
session
126+
session,
127+
chainId
116128
);
117129
}
118130

@@ -130,24 +142,24 @@ export const useHandleWcRequest = () => {
130142

131143
modal = <SignPayloadRequestModal opts={signPayloadProps} />;
132144
onClose = () => {
133-
const sdkErrorKey: SdkErrorKey = "USER_REJECTED";
134-
console.info("WC request rejected by user", sdkErrorKey, event);
135-
// dApp is waiting so we need to notify it
136-
const response = formatJsonRpcError(id, getSdkError(sdkErrorKey).message);
137-
void walletKit.respondSessionRequest({ topic, response });
145+
handleUserRejected();
138146
};
139147
return openWith(modal, { onClose });
140148
}
141149

142150
case "tezos_send": {
143151
if (!request.params.account) {
144-
throw new WalletConnectError("Missing account in request", "INVALID_EVENT", session);
152+
throw new WalletConnectError(
153+
"Missing account in request",
154+
"MISSING_ACCOUNT_IN_REQUEST",
155+
session
156+
);
145157
}
146158
const signer = getAccount(request.params.account);
147159
if (!signer) {
148160
throw new WalletConnectError(
149161
`Unknown account, no signer: ${request.params.account}`,
150-
"UNAUTHORIZED_EVENT",
162+
"INTERNAL_SIGNER_IS_MISSING",
151163
session
152164
);
153165
}
@@ -183,16 +195,17 @@ export const useHandleWcRequest = () => {
183195
modal = <BatchSignPage {...signProps} {...event.params.request.params} />;
184196
}
185197
onClose = () => {
186-
throw new WalletConnectError("Rejected by user", "USER_REJECTED", session);
198+
handleUserRejected();
187199
};
188200

189201
return openWith(modal, { onClose });
190202
}
191203
default:
192204
throw new WalletConnectError(
193205
`Unsupported method ${request.method}`,
194-
"WC_METHOD_UNSUPPORTED",
195-
session
206+
"METHOD_UNSUPPORTED",
207+
session,
208+
request.method
196209
);
197210
}
198211
});

packages/core/src/getPublicKeyAndCurve.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const getPublicKeyAndCurve = async (
2929
if (!keyResponse) {
3030
throw new WalletConnectError(
3131
`Signer address is not revealed on the ${network.name}. To reveal it, send any amount, e.g. 0.000001ꜩ, from that address to yourself. Wait several minutes and try again.`,
32-
"UNSUPPORTED_ACCOUNTS",
32+
"SIGNER_ADDRESS_NOT_REVEALED",
3333
session || null
3434
);
3535
}
@@ -44,8 +44,9 @@ export const getPublicKeyAndCurve = async (
4444
if (!curve) {
4545
throw new WalletConnectError(
4646
`Unknown curve for the public key: ${publicKey}`,
47-
"UNSUPPORTED_ACCOUNTS",
48-
session || null
47+
"UNKNOWN_CURVE_FOR_PUBLIC_KEY",
48+
session || null,
49+
publicKey
4950
);
5051
}
5152
return { publicKey, curve };

packages/utils/src/ErrorContext.test.ts

+76-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { CustomError, WalletConnectError, getErrorContext, handleTezError } from "./ErrorContext";
1+
import { TezosOperationError, type TezosOperationErrorWithMessage } from "@taquito/taquito";
2+
3+
import {
4+
CustomError,
5+
WalletConnectError,
6+
explainTezError,
7+
getErrorContext,
8+
getWcErrorResponse,
9+
} from "./ErrorContext";
210

311
describe("getErrorContext", () => {
412
it("should handle error object with message and stack", () => {
@@ -12,7 +20,7 @@ describe("getErrorContext", () => {
1220
expect(context.technicalDetails).toBe("some error message");
1321
expect(context.stacktrace).toBe("some stacktrace");
1422
expect(context.description).toBe(
15-
"Something went wrong. Please try again or contact support if the issue persists."
23+
"Something went wrong. Please try again. Contact support if the issue persists. Details: some error message"
1624
);
1725
expect(context.timestamp).toBeDefined();
1826
});
@@ -25,7 +33,7 @@ describe("getErrorContext", () => {
2533
expect(context.technicalDetails).toBe("string error message");
2634
expect(context.stacktrace).toBe("");
2735
expect(context.description).toBe(
28-
"Something went wrong. Please try again or contact support if the issue persists."
36+
"Something went wrong. Please try again. Contact support if the issue persists."
2937
);
3038
expect(context.timestamp).toBeDefined();
3139
});
@@ -48,53 +56,107 @@ describe("getErrorContext", () => {
4856

4957
const context = getErrorContext(error);
5058

51-
expect(context.technicalDetails).toBe("");
59+
expect(context.technicalDetails).toBeUndefined();
5260
expect(context.description).toBe("Custom error message");
5361
expect(context.stacktrace).toBeDefined();
5462
expect(context.timestamp).toBeDefined();
5563
});
5664
it("should handle WalletConnectError instances", () => {
57-
const error = new WalletConnectError("Custom WC error message", "UNSUPPORTED_EVENTS", null);
65+
const error = new WalletConnectError("Custom WC error message", "INTERNAL_ERROR", null);
5866

5967
const context = getErrorContext(error);
6068

61-
expect(context.technicalDetails).toBe("");
69+
expect(context.technicalDetails).toBeUndefined();
6270
expect(context.description).toBe("Custom WC error message");
6371
expect(context.stacktrace).toBeDefined();
6472
expect(context.timestamp).toBeDefined();
6573
});
6674
});
6775

68-
describe("handleTezError", () => {
76+
describe("explainTezError", () => {
6977
it("catches subtraction_underflow", () => {
70-
const res = handleTezError(new Error("subtraction_underflow"));
78+
const res = explainTezError("subtraction_underflow");
7179
expect(res).toBe("Insufficient balance, please make sure you have enough funds.");
7280
});
7381

7482
it("catches non_existing_contract", () => {
75-
const res = handleTezError(new Error("contract.non_existing_contract"));
83+
const res = explainTezError("contract.non_existing_contract");
7684
expect(res).toBe("Contract does not exist, please check if the correct network is selected.");
7785
});
7886

7987
it("catches staking_to_delegate_that_refuses_external_staking", () => {
80-
const res = handleTezError(new Error("staking_to_delegate_that_refuses_external_staking"));
88+
const res = explainTezError("staking_to_delegate_that_refuses_external_staking");
8189
expect(res).toBe("The baker you are trying to stake to does not accept external staking.");
8290
});
8391

8492
it("catches empty_implicit_delegated_contract", () => {
85-
const res = handleTezError(new Error("empty_implicit_delegated_contract"));
93+
const res = explainTezError("empty_implicit_delegated_contract");
8694
expect(res).toBe(
8795
"Emptying an implicit delegated account is not allowed. End delegation before trying again."
8896
);
8997
});
9098

9199
it("catches delegate.unchanged", () => {
92-
const res = handleTezError(new Error("delegate.unchanged"));
100+
const res = explainTezError("delegate.unchanged");
93101
expect(res).toBe("The delegate is unchanged. Delegation to this address is already done.");
94102
});
95103

96104
it("returns undefined for unknown errors", () => {
97-
const err = new Error("unknown error");
98-
expect(handleTezError(err)).toBeUndefined();
105+
const err = "unknown error";
106+
expect(explainTezError(err)).toBeUndefined();
107+
});
108+
109+
it("should return default error message for unknown error", () => {
110+
const error = new Error("Unknown error");
111+
const context = getErrorContext(error);
112+
expect(context.description).toBe(
113+
"Something went wrong. Please try again. Contact support if the issue persists. Details: Unknown error"
114+
);
115+
});
116+
117+
it("should return custom error message for CustomError", () => {
118+
const error = new CustomError("Custom error message");
119+
const context = getErrorContext(error);
120+
expect(context.description).toBe("Custom error message");
121+
});
122+
123+
it("should return WalletConnectError message", () => {
124+
const error = new WalletConnectError("WC error custom text", "INTERNAL_ERROR", null);
125+
const context = getErrorContext(error);
126+
expect(context.description).toBe("WC error custom text");
127+
expect(context.code).toBe(4011);
128+
expect(context.technicalDetails).toBeUndefined();
129+
});
130+
131+
it("should return TezosOperationError message", () => {
132+
// const error = new TezosOperationError(errors:[], lastError: { id: 'michelson_v1.script_rejected', with: { prim: 'Unit' } });
133+
const mockError: TezosOperationErrorWithMessage = {
134+
kind: "temporary",
135+
id: "proto.020-PsParisC.michelson_v1.script_rejected",
136+
with: { string: "Fail entrypoint" }, // Include the `with` field for testing
137+
};
138+
const error = new TezosOperationError(
139+
[mockError],
140+
"Operation failed due to a rejected script.",
141+
[]
142+
);
143+
const context = getErrorContext(error);
144+
expect(context.description).toContain(
145+
"Rejected by chain. The contract code failed to run. Please check the contract. Details: Fail entrypoint"
146+
);
147+
expect(context.technicalDetails).toEqual([
148+
"proto.020-PsParisC.michelson_v1.script_rejected",
149+
{ with: { string: "Fail entrypoint" } },
150+
]);
151+
});
152+
153+
it("should return error response for getWcErrorResponse", () => {
154+
const error = new Error("Unknown error");
155+
const response = getWcErrorResponse(error);
156+
expect(response.message).toBe(
157+
"Something went wrong. Please try again. Contact support if the issue persists. Details: Unknown error"
158+
);
159+
expect(response.code).toBe(4011);
160+
expect(response.data).toBe("Unknown error");
99161
});
100162
});

0 commit comments

Comments
 (0)