From bcc0291f7ea217cf9fe8214a75f7afc559f69606 Mon Sep 17 00:00:00 2001 From: Diana Savatina Date: Tue, 22 Oct 2024 14:45:44 +0100 Subject: [PATCH 1/2] feat: WalletConnect integration, part 6, request requests are supported. Tested: - send tez - delegate / undelegate - originate / call contract - stake / unstake / finalize unstake --- .../SendFlow/WalletConnect/useSignWithWc.tsx | 53 ++++++++ .../SendFlow/common/BatchSignPage.tsx | 5 +- .../common/OriginationOperationSignPage.tsx | 1 - .../SendFlow/common/SingleSignPage.tsx | 5 +- .../WalletConnect/WalletConnectProvider.tsx | 16 ++- .../WalletConnect/useHandleWcRequest.tsx | 122 ++++++++++++++++++ 6 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx create mode 100644 apps/web/src/components/WalletConnect/useHandleWcRequest.tsx diff --git a/apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx b/apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx new file mode 100644 index 0000000000..e3f688526c --- /dev/null +++ b/apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx @@ -0,0 +1,53 @@ +import { type TezosToolkit } from "@taquito/taquito"; +import { useDynamicModalContext } from "@umami/components"; +import { executeOperations, totalFee } from "@umami/core"; +import { useAsyncActionHandler, walletKit } from "@umami/state"; +import { formatJsonRpcResult } from "@walletconnect/jsonrpc-utils"; +import { useForm } from "react-hook-form"; + +import { SuccessStep } from "../SuccessStep"; +import { type CalculatedSignProps, type SdkSignPageProps } from "../utils"; + +export const useSignWithWalletConnect = ({ + operation, + headerProps, + requestId, +}: SdkSignPageProps): CalculatedSignProps => { + const { isLoading: isSigning, handleAsyncAction } = useAsyncActionHandler(); + const { openWith } = useDynamicModalContext(); + + const form = useForm({ defaultValues: { executeParams: operation.estimates } }); + + if (requestId.sdkType !== "walletconnect") { + return { + fee: 0, + isSigning: false, + onSign: async () => {}, + network: null, + }; + } + + const onSign = async (tezosToolkit: TezosToolkit) => + handleAsyncAction( + async () => { + const { opHash } = await executeOperations( + { ...operation, estimates: form.watch("executeParams") }, + tezosToolkit + ); + + const response = formatJsonRpcResult(requestId.id, { hash: opHash }); + await walletKit.respondSessionRequest({ topic: requestId.topic, response }); + return openWith(); + }, + error => ({ + description: `Failed to confirm Beacon operation: ${error.message}`, + }) + ); + + return { + fee: totalFee(form.watch("executeParams")), + isSigning, + onSign, + network: headerProps.network, + }; +}; diff --git a/apps/web/src/components/SendFlow/common/BatchSignPage.tsx b/apps/web/src/components/SendFlow/common/BatchSignPage.tsx index aec74b54cc..fd451cfe9b 100644 --- a/apps/web/src/components/SendFlow/common/BatchSignPage.tsx +++ b/apps/web/src/components/SendFlow/common/BatchSignPage.tsx @@ -23,6 +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"; export const BatchSignPage = ( signProps: SdkSignPageProps, @@ -31,7 +32,9 @@ export const BatchSignPage = ( const color = useColor(); const beaconCalculatedProps = useSignWithBeacon({ ...signProps }); - const calculatedProps = beaconCalculatedProps; + const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps }); + const calculatedProps = + signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps; const { isSigning, onSign, network, fee } = calculatedProps; const { signer, operations } = signProps.operation; diff --git a/apps/web/src/components/SendFlow/common/OriginationOperationSignPage.tsx b/apps/web/src/components/SendFlow/common/OriginationOperationSignPage.tsx index 87a978c168..77d5bea397 100644 --- a/apps/web/src/components/SendFlow/common/OriginationOperationSignPage.tsx +++ b/apps/web/src/components/SendFlow/common/OriginationOperationSignPage.tsx @@ -37,7 +37,6 @@ export const OriginationOperationSignPage = ({ }: SdkSignPageProps & CalculatedSignProps) => { const color = useColor(); const { code, storage } = operation.operations[0] as ContractOrigination; - const form = useForm({ defaultValues: { executeParams: operation.estimates } }); return ( diff --git a/apps/web/src/components/SendFlow/common/SingleSignPage.tsx b/apps/web/src/components/SendFlow/common/SingleSignPage.tsx index 4f70346847..11250ce2f5 100644 --- a/apps/web/src/components/SendFlow/common/SingleSignPage.tsx +++ b/apps/web/src/components/SendFlow/common/SingleSignPage.tsx @@ -10,12 +10,15 @@ import { TezSignPage } from "./TezSignPage"; import { UndelegationSignPage } from "./UndelegationSignPage"; import { UnstakeSignPage } from "./UnstakeSignPage"; import { useSignWithBeacon } from "../Beacon/useSignWithBeacon"; +import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc"; export const SingleSignPage = (signProps: SdkSignPageProps) => { const operationType = signProps.operation.operations[0].type; const beaconCalculatedProps = useSignWithBeacon({ ...signProps }); - const calculatedProps = beaconCalculatedProps; + const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps }); + const calculatedProps = + signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps; switch (operationType) { case "tez": { diff --git a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx index dfcb2ae58a..9627069e5b 100644 --- a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx +++ b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx @@ -19,6 +19,7 @@ import { getSdkError } from "@walletconnect/utils"; import { type PropsWithChildren, useCallback, useEffect, useRef } from "react"; import { SessionProposalModal } from "./SessionProposalModal"; +import { useHandleWcRequest } from "./useHandleWcRequest"; enum WalletKitState { NOT_INITIALIZED, @@ -36,6 +37,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { const availableNetworks: Network[] = useAvailableNetworks(); + const handleWcRequest = useHandleWcRequest(); + const onSessionProposal = useCallback( (proposal: WalletKitTypes.SessionProposal) => handleAsyncActionUnsafe(async () => { @@ -87,8 +90,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { ); const onSessionRequest = useCallback( - async (event: WalletKitTypes.SessionRequest) => { - try { + async (event: WalletKitTypes.SessionRequest) => + handleAsyncActionUnsafe(async () => { const activeSessions: Record = walletKit.getActiveSessions(); if (!(event.topic in activeSessions)) { console.error("WalletConnect session request failed. Session not found", event); @@ -101,8 +104,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { description: `Session request from dApp ${session.peer.metadata.name}`, status: "info", }); - throw new CustomError("Not implemented"); - } catch (error) { + await handleWcRequest(event, session); + }).catch(async error => { const { id, topic } = event; const activeSessions: Record = walletKit.getActiveSessions(); console.error("WalletConnect session request failed", event, error); @@ -121,9 +124,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { // dApp is waiting so we need to notify it const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message); await walletKit.respondSessionRequest({ topic, response }); - } - }, - [toast] + }), + [handleAsyncActionUnsafe, handleWcRequest, toast] ); useEffect(() => { diff --git a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx new file mode 100644 index 0000000000..cb1403277f --- /dev/null +++ b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx @@ -0,0 +1,122 @@ +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 { 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"; +import { type SdkSignPageProps, type SignHeaderProps } from "../SendFlow/utils"; + +/** + * @returns a function that handles a beacon message and opens a modal with the appropriate content + * + * For operation requests it will also try to convert the operation(s) to our {@link Operation} format, + * estimate the fee and open the BeaconSignPage only if it succeeds + */ +export const useHandleWcRequest = () => { + const { openWith } = useDynamicModalContext(); + const { handleAsyncActionUnsafe } = useAsyncActionHandler(); + const getAccount = useGetOwnedAccountSafe(); + const findNetwork = useFindNetwork(); + const toast = useToast(); + + return async ( + event: { + verifyContext: Verify.Context; + } & SignClientTypes.BaseEventArgs<{ + request: { + method: string; + params: any; + expiryTimestamp?: number; + }; + chainId: string; + }>, + session: SessionTypes.Struct + ) => { + await handleAsyncActionUnsafe( + async () => { + const { id, topic, params } = event; + const { request, chainId } = params; + + let modal; + let onClose; + + switch (request.method) { + case "tezos_getAccounts": { + const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message); + await walletKit.respondSessionRequest({ topic, response }); + return; + } + + case "tezos_sign": { + // onClose = async () => { + // const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message); + // await walletKit.respondSessionRequest({ topic, response }); + // }; + // return openWith(, { onClose }); + const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message); + await walletKit.respondSessionRequest({ topic, response }); + return; + } + + 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 + ); + 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 = ; + } else { + modal = ; + } + onClose = async () => { + const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message); + await walletKit.respondSessionRequest({ topic, response }); + }; + + return openWith(modal, { onClose }); + } + default: + throw new Error(`Unsupported method ${request.method}`); + } + } + // error => ({ + // description: `Error while processing WalletConnect request: ${error.message}`, + // }) + ); + }; +}; From e6cee09fdcb6b9eaca572fb7b4fb55b7b7eba040 Mon Sep 17 00:00:00 2001 From: Diana Savatina Date: Fri, 13 Dec 2024 13:36:15 +0000 Subject: [PATCH 2/2] feat: WalletConnectError is added, fixed error handling --- ...ithWc.tsx => useSignWithWalletConnect.tsx} | 2 +- .../SendFlow/common/BatchSignPage.tsx | 2 +- .../SendFlow/common/SingleSignPage.tsx | 2 +- .../WalletConnect/WalletConnectProvider.tsx | 31 ++-- .../WalletConnect/useHandleWcRequest.tsx | 150 +++++++++--------- packages/utils/src/ErrorContext.test.ts | 17 +- packages/utils/src/ErrorContext.ts | 16 +- 7 files changed, 119 insertions(+), 101 deletions(-) rename apps/web/src/components/SendFlow/WalletConnect/{useSignWithWc.tsx => useSignWithWalletConnect.tsx} (95%) diff --git a/apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx b/apps/web/src/components/SendFlow/WalletConnect/useSignWithWalletConnect.tsx similarity index 95% rename from apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx rename to apps/web/src/components/SendFlow/WalletConnect/useSignWithWalletConnect.tsx index e3f688526c..0711d4c72a 100644 --- a/apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx +++ b/apps/web/src/components/SendFlow/WalletConnect/useSignWithWalletConnect.tsx @@ -40,7 +40,7 @@ export const useSignWithWalletConnect = ({ return openWith(); }, error => ({ - description: `Failed to confirm Beacon operation: ${error.message}`, + description: `Failed to confirm WalletConnect operation: ${error.message}`, }) ); diff --git a/apps/web/src/components/SendFlow/common/BatchSignPage.tsx b/apps/web/src/components/SendFlow/common/BatchSignPage.tsx index fd451cfe9b..eee5749c08 100644 --- a/apps/web/src/components/SendFlow/common/BatchSignPage.tsx +++ b/apps/web/src/components/SendFlow/common/BatchSignPage.tsx @@ -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, diff --git a/apps/web/src/components/SendFlow/common/SingleSignPage.tsx b/apps/web/src/components/SendFlow/common/SingleSignPage.tsx index 11250ce2f5..5c35f7addf 100644 --- a/apps/web/src/components/SendFlow/common/SingleSignPage.tsx +++ b/apps/web/src/components/SendFlow/common/SingleSignPage.tsx @@ -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; diff --git a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx index 9627069e5b..af17e4cad1 100644 --- a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx +++ b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx @@ -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"; @@ -94,12 +94,10 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { handleAsyncActionUnsafe(async () => { const activeSessions: Record = 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", @@ -107,22 +105,19 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { await handleWcRequest(event, session); }).catch(async error => { const { id, topic } = event; - const activeSessions: Record = 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] diff --git a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx index cb1403277f..c1a4b68da8 100644 --- a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx +++ b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx @@ -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"; @@ -26,7 +19,6 @@ export const useHandleWcRequest = () => { const { handleAsyncActionUnsafe } = useAsyncActionHandler(); const getAccount = useGetOwnedAccountSafe(); const findNetwork = useFindNetwork(); - const toast = useToast(); return async ( event: { @@ -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(, { 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 = ; - } else { - modal = ; - } - 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 = ; + } else { + modal = ; } - 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}`, - // }) - ); + }); }; }; diff --git a/packages/utils/src/ErrorContext.test.ts b/packages/utils/src/ErrorContext.test.ts index 28fc72059d..fd3ef71609 100644 --- a/packages/utils/src/ErrorContext.test.ts +++ b/packages/utils/src/ErrorContext.test.ts @@ -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", () => { @@ -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", () => { @@ -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(); diff --git a/packages/utils/src/ErrorContext.ts b/packages/utils/src/ErrorContext.ts index 1766f220f7..6e1a9bad6d 100644 --- a/packages/utils/src/ErrorContext.ts +++ b/packages/utils/src/ErrorContext.ts @@ -1,3 +1,5 @@ +import { type SessionTypes } from "@walletconnect/types"; +import { type SdkErrorKey } from "@walletconnect/utils"; export type ErrorContext = { timestamp: string; description: string; @@ -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")) { @@ -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."; } }; @@ -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) {