Skip to content

Commit

Permalink
feat: WalletConnectError is added, fixed error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
dianasavvatina committed Dec 18, 2024
1 parent 1a7ddfc commit 941e9eb
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const useSignWithWalletConnect = ({
return openWith(<SuccessStep hash={opHash} />);
},
error => ({
description: `Failed to confirm Beacon operation: ${error.message}`,
description: `Failed to confirm WalletConnect operation: ${error.message}`,
})
);

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/SendFlow/common/BatchSignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { useSignWithBeacon } from "../Beacon/useSignWithBeacon";
import { SignButton } from "../SignButton";
import { SignPageFee } from "../SignPageFee";
import { type SdkSignPageProps } from "../utils";
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc";
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWalletConnect";

export const BatchSignPage = (
signProps: SdkSignPageProps,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/SendFlow/common/SingleSignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { TezSignPage } from "./TezSignPage";
import { UndelegationSignPage } from "./UndelegationSignPage";
import { UnstakeSignPage } from "./UnstakeSignPage";
import { useSignWithBeacon } from "../Beacon/useSignWithBeacon";
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc";
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWalletConnect";

export const SingleSignPage = (signProps: SdkSignPageProps) => {
const operationType = signProps.operation.operations[0].type;
Expand Down
31 changes: 13 additions & 18 deletions apps/web/src/components/WalletConnect/WalletConnectProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
walletKit,
} from "@umami/state";
import { type Network } from "@umami/tezos";
import { CustomError } from "@umami/utils";
import { CustomError, WalletConnectError } from "@umami/utils";
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
import { type SessionTypes } from "@walletconnect/types";
import { getSdkError } from "@walletconnect/utils";
import { type SdkErrorKey, getSdkError } from "@walletconnect/utils";
import { type PropsWithChildren, useCallback, useEffect, useRef } from "react";

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

const session = activeSessions[event.topic];

toast({
description: `Session request from dApp ${session.peer.metadata.name}`,
status: "info",
});
await handleWcRequest(event, session);
}).catch(async error => {
const { id, topic } = event;
const activeSessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
console.error("WalletConnect session request failed", event, error);
if (event.topic in activeSessions) {
const session = activeSessions[event.topic];
toast({
description: `Session request for dApp ${session.peer.metadata.name} failed. It was rejected.`,
status: "error",
});
let sdkErrorKey: SdkErrorKey =
error instanceof WalletConnectError ? error.sdkError : "SESSION_SETTLEMENT_FAILED";
if (sdkErrorKey === "USER_REJECTED") {
console.info("WC request rejected", sdkErrorKey, event, error);
} else {
toast({
description: `Session request for dApp ${topic} failed. It was rejected. Peer not found by topic.`,
status: "error",
});
if (error.message.includes("delegate.unchanged")) {
sdkErrorKey = "INVALID_EVENT";
}
console.warn("WC request failed", sdkErrorKey, event, error);
}
// dApp is waiting so we need to notify it
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
const sdkErrorMessage = getSdkError(sdkErrorKey).message;
const response = formatJsonRpcError(id, sdkErrorMessage);
await walletKit.respondSessionRequest({ topic, response });
}),
[handleAsyncActionUnsafe, handleWcRequest, toast]
Expand Down
150 changes: 72 additions & 78 deletions apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { useToast } from "@chakra-ui/react";
import { useDynamicModalContext } from "@umami/components";
import { type ImplicitAccount, estimate, toAccountOperations } from "@umami/core";
import {
useAsyncActionHandler,
useFindNetwork,
useGetOwnedAccountSafe,
walletKit,
} from "@umami/state";
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
import { useAsyncActionHandler, useFindNetwork, useGetOwnedAccountSafe } from "@umami/state";
import { WalletConnectError } from "@umami/utils";
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";
import { getSdkError } from "@walletconnect/utils";

import { BatchSignPage } from "../SendFlow/common/BatchSignPage";
import { SingleSignPage } from "../SendFlow/common/SingleSignPage";
Expand All @@ -26,7 +19,6 @@ export const useHandleWcRequest = () => {
const { handleAsyncActionUnsafe } = useAsyncActionHandler();
const getAccount = useGetOwnedAccountSafe();
const findNetwork = useFindNetwork();
const toast = useToast();

return async (
event: {
Expand All @@ -41,82 +33,84 @@ export const useHandleWcRequest = () => {
}>,
session: SessionTypes.Struct
) => {
await handleAsyncActionUnsafe(
async () => {
const { id, topic, params } = event;
const { request, chainId } = params;
await handleAsyncActionUnsafe(async () => {
const { id, topic, params } = event;
const { request, chainId } = params;

let modal;
let onClose;
let modal;
let onClose;

switch (request.method) {
case "tezos_getAccounts": {
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
await walletKit.respondSessionRequest({ topic, response });
return;
}
switch (request.method) {
case "tezos_getAccounts": {
throw new WalletConnectError(
"Getting accounts is not supported yet",
"WC_METHOD_UNSUPPORTED",
session
);
}

case "tezos_sign": {
// onClose = async () => {
// const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
// await walletKit.respondSessionRequest({ topic, response });
// };
// return openWith(<SignPayloadRequestModal request={"FIXME"} />, { onClose });
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
await walletKit.respondSessionRequest({ topic, response });
return;
}
case "tezos_sign": {
throw new WalletConnectError(
"Sign is not supported yet",
"WC_METHOD_UNSUPPORTED",
session
);
}

case "tezos_send": {
if (!request.params.account) {
throw new Error("Missing account in request");
}
const signer = getAccount(request.params.account);
if (!signer) {
throw new Error(`Unknown account, no signer: ${request.params.account}`);
}
const operation = toAccountOperations(
request.params.operations,
signer as ImplicitAccount
case "tezos_send": {
if (!request.params.account) {
throw new WalletConnectError("Missing account in request", "INVALID_EVENT", session);
}
const signer = getAccount(request.params.account);
if (!signer) {
throw new WalletConnectError(
`Unknown account, no signer: ${request.params.account}`,
"UNAUTHORIZED_EVENT",
session
);
const network = findNetwork(chainId.split(":")[1]);
if (!network) {
const response = formatJsonRpcError(id, getSdkError("INVALID_EVENT").message);
await walletKit.respondSessionRequest({ topic, response });
toast({ description: `Unsupported network: ${chainId}`, status: "error" });
return;
}
const estimatedOperations = await estimate(operation, network);
const headerProps: SignHeaderProps = {
network,
appName: session.peer.metadata.name,
appIcon: session.peer.metadata.icons[0],
};
const signProps: SdkSignPageProps = {
headerProps: headerProps,
operation: estimatedOperations,
requestId: { sdkType: "walletconnect", id: id, topic },
};

if (operation.operations.length === 1) {
modal = <SingleSignPage {...signProps} />;
} else {
modal = <BatchSignPage {...signProps} {...event.params.request.params} />;
}
onClose = async () => {
const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
await walletKit.respondSessionRequest({ topic, response });
};
}
const operation = toAccountOperations(
request.params.operations,
signer as ImplicitAccount
);
const network = findNetwork(chainId.split(":")[1]);
if (!network) {
throw new WalletConnectError(
`Unsupported network ${chainId}`,
"UNSUPPORTED_CHAINS",
session
);
}
const estimatedOperations = await estimate(operation, network);
const headerProps: SignHeaderProps = {
network,
appName: session.peer.metadata.name,
appIcon: session.peer.metadata.icons[0],
};
const signProps: SdkSignPageProps = {
headerProps: headerProps,
operation: estimatedOperations,
requestId: { sdkType: "walletconnect", id: id, topic },
};

return openWith(modal, { onClose });
if (operation.operations.length === 1) {
modal = <SingleSignPage {...signProps} />;
} else {
modal = <BatchSignPage {...signProps} {...event.params.request.params} />;
}
default:
throw new Error(`Unsupported method ${request.method}`);
onClose = () => {
throw new WalletConnectError("Rejected by user", "USER_REJECTED", session);
};

return openWith(modal, { onClose });
}
default:
throw new WalletConnectError(
`Unsupported method ${request.method}`,
"WC_METHOD_UNSUPPORTED",
session
);
}
// error => ({
// description: `Error while processing WalletConnect request: ${error.message}`,
// })
);
});
};
};
17 changes: 16 additions & 1 deletion packages/utils/src/ErrorContext.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CustomError, getErrorContext, handleTezError } from "./ErrorContext";
import { CustomError, WalletConnectError, getErrorContext, handleTezError } from "./ErrorContext";

describe("getErrorContext", () => {
it("should handle error object with message and stack", () => {
Expand Down Expand Up @@ -53,6 +53,16 @@ describe("getErrorContext", () => {
expect(context.stacktrace).toBeDefined();
expect(context.timestamp).toBeDefined();
});
it("should handle WalletConnectError instances", () => {
const error = new WalletConnectError("Custom WC error message", "UNSUPPORTED_EVENTS", null);

const context = getErrorContext(error);

expect(context.technicalDetails).toBe("");
expect(context.description).toBe("Custom WC error message");
expect(context.stacktrace).toBeDefined();
expect(context.timestamp).toBeDefined();
});
});

describe("handleTezError", () => {
Expand All @@ -78,6 +88,11 @@ describe("handleTezError", () => {
);
});

it("catches delegate.unchanged", () => {
const res = handleTezError(new Error("delegate.unchanged"));
expect(res).toBe("The delegate is unchanged. Delegation to this address is already done.");
});

it("returns undefined for unknown errors", () => {
const err = new Error("unknown error");
expect(handleTezError(err)).toBeUndefined();
Expand Down
16 changes: 15 additions & 1 deletion packages/utils/src/ErrorContext.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { type SessionTypes } from "@walletconnect/types";
import { type SdkErrorKey } from "@walletconnect/utils";
export type ErrorContext = {
timestamp: string;
description: string;
Expand All @@ -12,6 +14,16 @@ export class CustomError extends Error {
}
}

export class WalletConnectError extends CustomError {
sdkError: SdkErrorKey;
constructor(message: string, sdkError: SdkErrorKey, session: SessionTypes.Struct | null) {
const dappName = session?.peer.metadata.name ?? "unknown";
super(session ? `Request from ${dappName} is rejected. ${message}` : message);
this.name = "WalletConnectError";
this.sdkError = sdkError;
}
}

// Converts a known L1 error message to a more user-friendly one
export const handleTezError = (err: Error): string | undefined => {
if (err.message.includes("subtraction_underflow")) {
Expand All @@ -22,6 +34,8 @@ export const handleTezError = (err: Error): string | undefined => {
return "The baker you are trying to stake to does not accept external staking.";
} else if (err.message.includes("empty_implicit_delegated_contract")) {
return "Emptying an implicit delegated account is not allowed. End delegation before trying again.";
} else if (err.message.includes("delegate.unchanged")) {
return "The delegate is unchanged. Delegation to this address is already done.";
}
};

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

if (error.name === "CustomError") {
if (error instanceof CustomError) {
description = error.message;
technicalDetails = "";
} else if (error instanceof Error) {
Expand Down

1 comment on commit 941e9eb

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
apps/web Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
packages/components Coverage: 97%
97.51% (196/201) 95.91% (94/98) 88.13% (52/59)
packages/core Coverage: 81%
82.47% (207/251) 72.72% (88/121) 81.35% (48/59)
packages/crypto Coverage: 100%
100% (43/43) 90.9% (10/11) 100% (7/7)
packages/data-polling Coverage: 97%
95.27% (141/148) 87.5% (21/24) 92.85% (39/42)
packages/multisig Coverage: 98%
98.47% (129/131) 85.71% (18/21) 100% (36/36)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 85%
84.76% (818/965) 80.86% (186/230) 78.59% (301/383)
packages/tezos Coverage: 89%
88.72% (118/133) 94.59% (35/37) 86.84% (33/38)
packages/tzkt Coverage: 89%
87.32% (62/71) 87.5% (14/16) 80.48% (33/41)

Please sign in to comment.