Skip to content

Commit 941e9eb

Browse files
feat: WalletConnectError is added, fixed error handling
1 parent 1a7ddfc commit 941e9eb

File tree

7 files changed

+119
-101
lines changed

7 files changed

+119
-101
lines changed

apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx renamed to apps/web/src/components/SendFlow/WalletConnect/useSignWithWalletConnect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const useSignWithWalletConnect = ({
4040
return openWith(<SuccessStep hash={opHash} />);
4141
},
4242
error => ({
43-
description: `Failed to confirm Beacon operation: ${error.message}`,
43+
description: `Failed to confirm WalletConnect operation: ${error.message}`,
4444
})
4545
);
4646

apps/web/src/components/SendFlow/common/BatchSignPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { useSignWithBeacon } from "../Beacon/useSignWithBeacon";
2323
import { SignButton } from "../SignButton";
2424
import { SignPageFee } from "../SignPageFee";
2525
import { type SdkSignPageProps } from "../utils";
26-
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc";
26+
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWalletConnect";
2727

2828
export const BatchSignPage = (
2929
signProps: SdkSignPageProps,

apps/web/src/components/SendFlow/common/SingleSignPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { TezSignPage } from "./TezSignPage";
1010
import { UndelegationSignPage } from "./UndelegationSignPage";
1111
import { UnstakeSignPage } from "./UnstakeSignPage";
1212
import { useSignWithBeacon } from "../Beacon/useSignWithBeacon";
13-
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc";
13+
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWalletConnect";
1414

1515
export const SingleSignPage = (signProps: SdkSignPageProps) => {
1616
const operationType = signProps.operation.operations[0].type;

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

Lines changed: 13 additions & 18 deletions
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 } from "@umami/utils";
15+
import { CustomError, WalletConnectError } from "@umami/utils";
1616
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
1717
import { type SessionTypes } from "@walletconnect/types";
18-
import { getSdkError } from "@walletconnect/utils";
18+
import { type SdkErrorKey, getSdkError } from "@walletconnect/utils";
1919
import { type PropsWithChildren, useCallback, useEffect, useRef } from "react";
2020

2121
import { SessionProposalModal } from "./SessionProposalModal";
@@ -94,35 +94,30 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
9494
handleAsyncActionUnsafe(async () => {
9595
const activeSessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
9696
if (!(event.topic in activeSessions)) {
97-
console.error("WalletConnect session request failed. Session not found", event);
98-
throw new CustomError("WalletConnect session request failed. Session not found");
97+
throw new WalletConnectError("Session not found", "INVALID_EVENT", null);
9998
}
10099

101100
const session = activeSessions[event.topic];
102-
103101
toast({
104102
description: `Session request from dApp ${session.peer.metadata.name}`,
105103
status: "info",
106104
});
107105
await handleWcRequest(event, session);
108106
}).catch(async error => {
109107
const { id, topic } = event;
110-
const activeSessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
111-
console.error("WalletConnect session request failed", event, error);
112-
if (event.topic in activeSessions) {
113-
const session = activeSessions[event.topic];
114-
toast({
115-
description: `Session request for dApp ${session.peer.metadata.name} failed. It was rejected.`,
116-
status: "error",
117-
});
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);
118112
} else {
119-
toast({
120-
description: `Session request for dApp ${topic} failed. It was rejected. Peer not found by topic.`,
121-
status: "error",
122-
});
113+
if (error.message.includes("delegate.unchanged")) {
114+
sdkErrorKey = "INVALID_EVENT";
115+
}
116+
console.warn("WC request failed", sdkErrorKey, event, error);
123117
}
124118
// dApp is waiting so we need to notify it
125-
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
119+
const sdkErrorMessage = getSdkError(sdkErrorKey).message;
120+
const response = formatJsonRpcError(id, sdkErrorMessage);
126121
await walletKit.respondSessionRequest({ topic, response });
127122
}),
128123
[handleAsyncActionUnsafe, handleWcRequest, toast]
Lines changed: 72 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
1-
import { useToast } from "@chakra-ui/react";
21
import { useDynamicModalContext } from "@umami/components";
32
import { type ImplicitAccount, estimate, toAccountOperations } from "@umami/core";
4-
import {
5-
useAsyncActionHandler,
6-
useFindNetwork,
7-
useGetOwnedAccountSafe,
8-
walletKit,
9-
} from "@umami/state";
10-
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
3+
import { useAsyncActionHandler, useFindNetwork, useGetOwnedAccountSafe } from "@umami/state";
4+
import { WalletConnectError } from "@umami/utils";
115
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";
12-
import { getSdkError } from "@walletconnect/utils";
136

147
import { BatchSignPage } from "../SendFlow/common/BatchSignPage";
158
import { SingleSignPage } from "../SendFlow/common/SingleSignPage";
@@ -26,7 +19,6 @@ export const useHandleWcRequest = () => {
2619
const { handleAsyncActionUnsafe } = useAsyncActionHandler();
2720
const getAccount = useGetOwnedAccountSafe();
2821
const findNetwork = useFindNetwork();
29-
const toast = useToast();
3022

3123
return async (
3224
event: {
@@ -41,82 +33,84 @@ export const useHandleWcRequest = () => {
4133
}>,
4234
session: SessionTypes.Struct
4335
) => {
44-
await handleAsyncActionUnsafe(
45-
async () => {
46-
const { id, topic, params } = event;
47-
const { request, chainId } = params;
36+
await handleAsyncActionUnsafe(async () => {
37+
const { id, topic, params } = event;
38+
const { request, chainId } = params;
4839

49-
let modal;
50-
let onClose;
40+
let modal;
41+
let onClose;
5142

52-
switch (request.method) {
53-
case "tezos_getAccounts": {
54-
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
55-
await walletKit.respondSessionRequest({ topic, response });
56-
return;
57-
}
43+
switch (request.method) {
44+
case "tezos_getAccounts": {
45+
throw new WalletConnectError(
46+
"Getting accounts is not supported yet",
47+
"WC_METHOD_UNSUPPORTED",
48+
session
49+
);
50+
}
5851

59-
case "tezos_sign": {
60-
// onClose = async () => {
61-
// const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
62-
// await walletKit.respondSessionRequest({ topic, response });
63-
// };
64-
// return openWith(<SignPayloadRequestModal request={"FIXME"} />, { onClose });
65-
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
66-
await walletKit.respondSessionRequest({ topic, response });
67-
return;
68-
}
52+
case "tezos_sign": {
53+
throw new WalletConnectError(
54+
"Sign is not supported yet",
55+
"WC_METHOD_UNSUPPORTED",
56+
session
57+
);
58+
}
6959

70-
case "tezos_send": {
71-
if (!request.params.account) {
72-
throw new Error("Missing account in request");
73-
}
74-
const signer = getAccount(request.params.account);
75-
if (!signer) {
76-
throw new Error(`Unknown account, no signer: ${request.params.account}`);
77-
}
78-
const operation = toAccountOperations(
79-
request.params.operations,
80-
signer as ImplicitAccount
60+
case "tezos_send": {
61+
if (!request.params.account) {
62+
throw new WalletConnectError("Missing account in request", "INVALID_EVENT", session);
63+
}
64+
const signer = getAccount(request.params.account);
65+
if (!signer) {
66+
throw new WalletConnectError(
67+
`Unknown account, no signer: ${request.params.account}`,
68+
"UNAUTHORIZED_EVENT",
69+
session
8170
);
82-
const network = findNetwork(chainId.split(":")[1]);
83-
if (!network) {
84-
const response = formatJsonRpcError(id, getSdkError("INVALID_EVENT").message);
85-
await walletKit.respondSessionRequest({ topic, response });
86-
toast({ description: `Unsupported network: ${chainId}`, status: "error" });
87-
return;
88-
}
89-
const estimatedOperations = await estimate(operation, network);
90-
const headerProps: SignHeaderProps = {
91-
network,
92-
appName: session.peer.metadata.name,
93-
appIcon: session.peer.metadata.icons[0],
94-
};
95-
const signProps: SdkSignPageProps = {
96-
headerProps: headerProps,
97-
operation: estimatedOperations,
98-
requestId: { sdkType: "walletconnect", id: id, topic },
99-
};
100-
101-
if (operation.operations.length === 1) {
102-
modal = <SingleSignPage {...signProps} />;
103-
} else {
104-
modal = <BatchSignPage {...signProps} {...event.params.request.params} />;
105-
}
106-
onClose = async () => {
107-
const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
108-
await walletKit.respondSessionRequest({ topic, response });
109-
};
71+
}
72+
const operation = toAccountOperations(
73+
request.params.operations,
74+
signer as ImplicitAccount
75+
);
76+
const network = findNetwork(chainId.split(":")[1]);
77+
if (!network) {
78+
throw new WalletConnectError(
79+
`Unsupported network ${chainId}`,
80+
"UNSUPPORTED_CHAINS",
81+
session
82+
);
83+
}
84+
const estimatedOperations = await estimate(operation, network);
85+
const headerProps: SignHeaderProps = {
86+
network,
87+
appName: session.peer.metadata.name,
88+
appIcon: session.peer.metadata.icons[0],
89+
};
90+
const signProps: SdkSignPageProps = {
91+
headerProps: headerProps,
92+
operation: estimatedOperations,
93+
requestId: { sdkType: "walletconnect", id: id, topic },
94+
};
11095

111-
return openWith(modal, { onClose });
96+
if (operation.operations.length === 1) {
97+
modal = <SingleSignPage {...signProps} />;
98+
} else {
99+
modal = <BatchSignPage {...signProps} {...event.params.request.params} />;
112100
}
113-
default:
114-
throw new Error(`Unsupported method ${request.method}`);
101+
onClose = () => {
102+
throw new WalletConnectError("Rejected by user", "USER_REJECTED", session);
103+
};
104+
105+
return openWith(modal, { onClose });
115106
}
107+
default:
108+
throw new WalletConnectError(
109+
`Unsupported method ${request.method}`,
110+
"WC_METHOD_UNSUPPORTED",
111+
session
112+
);
116113
}
117-
// error => ({
118-
// description: `Error while processing WalletConnect request: ${error.message}`,
119-
// })
120-
);
114+
});
121115
};
122116
};

packages/utils/src/ErrorContext.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CustomError, getErrorContext, handleTezError } from "./ErrorContext";
1+
import { CustomError, WalletConnectError, getErrorContext, handleTezError } from "./ErrorContext";
22

33
describe("getErrorContext", () => {
44
it("should handle error object with message and stack", () => {
@@ -53,6 +53,16 @@ describe("getErrorContext", () => {
5353
expect(context.stacktrace).toBeDefined();
5454
expect(context.timestamp).toBeDefined();
5555
});
56+
it("should handle WalletConnectError instances", () => {
57+
const error = new WalletConnectError("Custom WC error message", "UNSUPPORTED_EVENTS", null);
58+
59+
const context = getErrorContext(error);
60+
61+
expect(context.technicalDetails).toBe("");
62+
expect(context.description).toBe("Custom WC error message");
63+
expect(context.stacktrace).toBeDefined();
64+
expect(context.timestamp).toBeDefined();
65+
});
5666
});
5767

5868
describe("handleTezError", () => {
@@ -78,6 +88,11 @@ describe("handleTezError", () => {
7888
);
7989
});
8090

91+
it("catches delegate.unchanged", () => {
92+
const res = handleTezError(new Error("delegate.unchanged"));
93+
expect(res).toBe("The delegate is unchanged. Delegation to this address is already done.");
94+
});
95+
8196
it("returns undefined for unknown errors", () => {
8297
const err = new Error("unknown error");
8398
expect(handleTezError(err)).toBeUndefined();

packages/utils/src/ErrorContext.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { type SessionTypes } from "@walletconnect/types";
2+
import { type SdkErrorKey } from "@walletconnect/utils";
13
export type ErrorContext = {
24
timestamp: string;
35
description: string;
@@ -12,6 +14,16 @@ export class CustomError extends Error {
1214
}
1315
}
1416

17+
export class WalletConnectError extends CustomError {
18+
sdkError: SdkErrorKey;
19+
constructor(message: string, sdkError: SdkErrorKey, session: SessionTypes.Struct | null) {
20+
const dappName = session?.peer.metadata.name ?? "unknown";
21+
super(session ? `Request from ${dappName} is rejected. ${message}` : message);
22+
this.name = "WalletConnectError";
23+
this.sdkError = sdkError;
24+
}
25+
}
26+
1527
// Converts a known L1 error message to a more user-friendly one
1628
export const handleTezError = (err: Error): string | undefined => {
1729
if (err.message.includes("subtraction_underflow")) {
@@ -22,6 +34,8 @@ export const handleTezError = (err: Error): string | undefined => {
2234
return "The baker you are trying to stake to does not accept external staking.";
2335
} else if (err.message.includes("empty_implicit_delegated_contract")) {
2436
return "Emptying an implicit delegated account is not allowed. End delegation before trying again.";
37+
} else if (err.message.includes("delegate.unchanged")) {
38+
return "The delegate is unchanged. Delegation to this address is already done.";
2539
}
2640
};
2741

@@ -41,7 +55,7 @@ export const getErrorContext = (error: any): ErrorContext => {
4155
technicalDetails = error;
4256
}
4357

44-
if (error.name === "CustomError") {
58+
if (error instanceof CustomError) {
4559
description = error.message;
4660
technicalDetails = "";
4761
} else if (error instanceof Error) {

0 commit comments

Comments
 (0)