diff --git a/apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.tsx b/apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.tsx
index 983fc21b95..3cc761f8e1 100644
--- a/apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.tsx
+++ b/apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.tsx
@@ -1,6 +1,5 @@
import { Box, Button, Divider, Flex, Heading, Link, Text, VStack } from "@chakra-ui/react";
import { errorsActions, useAppDispatch, useAppSelector } from "@umami/state";
-import { handleTezError } from "@umami/utils";
import { useColor } from "../../../styles/useColor";
import { EmptyMessage } from "../../EmptyMessage";
@@ -61,8 +60,7 @@ export const ErrorLogsMenu = () => {
{errorLog.technicalDetails && (
- {handleTezError({ name: "unknown", message: errorLog.technicalDetails }) ??
- ""}
+ {JSON.stringify(errorLog.technicalDetails)}
)}
diff --git a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx
index af17e4cad1..d212325ae1 100644
--- a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx
+++ b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx
@@ -3,6 +3,7 @@ import type EventEmitter from "events";
import { type NetworkType } from "@airgap/beacon-wallet";
import { useToast } from "@chakra-ui/react";
import { type WalletKitTypes } from "@reown/walletkit";
+import { TezosOperationError } from "@taquito/taquito";
import { useDynamicModalContext } from "@umami/components";
import {
createWalletKit,
@@ -12,10 +13,10 @@ import {
walletKit,
} from "@umami/state";
import { type Network } from "@umami/tezos";
-import { CustomError, WalletConnectError } from "@umami/utils";
+import { CustomError, WalletConnectError, type WcErrorKey, getWcErrorResponse } from "@umami/utils";
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
import { type SessionTypes } from "@walletconnect/types";
-import { type SdkErrorKey, getSdkError } from "@walletconnect/utils";
+import { getSdkError } from "@walletconnect/utils";
import { type PropsWithChildren, useCallback, useEffect, useRef } from "react";
import { SessionProposalModal } from "./SessionProposalModal";
@@ -94,7 +95,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
handleAsyncActionUnsafe(async () => {
const activeSessions: Record = walletKit.getActiveSessions();
if (!(event.topic in activeSessions)) {
- throw new WalletConnectError("Session not found", "INVALID_EVENT", null);
+ throw new WalletConnectError("Session not found", "SESSION_NOT_FOUND", null);
}
const session = activeSessions[event.topic];
@@ -105,19 +106,20 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
await handleWcRequest(event, session);
}).catch(async error => {
const { id, topic } = event;
- let sdkErrorKey: SdkErrorKey =
- error instanceof WalletConnectError ? error.sdkError : "SESSION_SETTLEMENT_FAILED";
- if (sdkErrorKey === "USER_REJECTED") {
- console.info("WC request rejected", sdkErrorKey, event, error);
+ let wcErrorKey: WcErrorKey = "UNKNOWN_ERROR";
+
+ if (error instanceof WalletConnectError) {
+ wcErrorKey = error.wcError;
+ } else if (error instanceof TezosOperationError) {
+ wcErrorKey = "REJECTED_BY_CHAIN";
+ }
+ const response = formatJsonRpcError(id, getWcErrorResponse(error));
+ if (wcErrorKey === "USER_REJECTED") {
+ console.info("WC request rejected", wcErrorKey, event, error);
} else {
- if (error.message.includes("delegate.unchanged")) {
- sdkErrorKey = "INVALID_EVENT";
- }
- console.warn("WC request failed", sdkErrorKey, event, error);
+ console.warn("WC request failed", wcErrorKey, event, error, response);
}
// dApp is waiting so we need to notify it
- 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 502361d530..55172746ae 100644
--- a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
+++ b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
@@ -14,10 +14,9 @@ import {
useGetOwnedAccountSafe,
walletKit,
} from "@umami/state";
-import { WalletConnectError } from "@umami/utils";
+import { WC_ERRORS, WalletConnectError } from "@umami/utils";
import { formatJsonRpcError, formatJsonRpcResult } from "@walletconnect/jsonrpc-utils";
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";
-import { type SdkErrorKey, getSdkError } from "@walletconnect/utils";
import { SignPayloadRequestModal } from "../common/SignPayloadRequestModal";
import { BatchSignPage } from "../SendFlow/common/BatchSignPage";
@@ -61,11 +60,18 @@ export const useHandleWcRequest = () => {
let modal;
let onClose;
+ const handleUserRejected = () => {
+ // dApp is waiting so we need to notify it
+ const response = formatJsonRpcError(id, WC_ERRORS.USER_REJECTED);
+ console.info("WC request rejected by user", event, response);
+ void walletKit.respondSessionRequest({ topic, response });
+ };
+
switch (request.method) {
case "tezos_getAccounts": {
const wcPeers = walletKit.getActiveSessions();
if (!(topic in wcPeers)) {
- throw new WalletConnectError(`Unknown session ${topic}`, "UNAUTHORIZED_EVENT", null);
+ throw new WalletConnectError(`Unknown session ${topic}`, "SESSION_NOT_FOUND", null);
}
const session = wcPeers[topic];
const accountPkh = session.namespaces.tezos.accounts[0].split(":")[2];
@@ -89,7 +95,11 @@ export const useHandleWcRequest = () => {
case "tezos_sign": {
if (!request.params.account) {
- throw new WalletConnectError("Missing account in request", "INVALID_EVENT", session);
+ throw new WalletConnectError(
+ "Missing account in request",
+ "MISSING_ACCOUNT_IN_REQUEST",
+ session
+ );
}
const signer = getImplicitAccount(request.params.account);
const network = findNetwork(chainId.split(":")[1]);
@@ -97,7 +107,8 @@ export const useHandleWcRequest = () => {
throw new WalletConnectError(
`Unsupported network ${chainId}`,
"UNSUPPORTED_CHAINS",
- session
+ session,
+ chainId
);
}
@@ -115,24 +126,24 @@ export const useHandleWcRequest = () => {
modal = ;
onClose = () => {
- const sdkErrorKey: SdkErrorKey = "USER_REJECTED";
- console.info("WC request rejected by user", sdkErrorKey, event);
- // dApp is waiting so we need to notify it
- const response = formatJsonRpcError(id, getSdkError(sdkErrorKey).message);
- void walletKit.respondSessionRequest({ topic, response });
+ handleUserRejected();
};
return openWith(modal, { onClose });
}
case "tezos_send": {
if (!request.params.account) {
- throw new WalletConnectError("Missing account in request", "INVALID_EVENT", session);
+ throw new WalletConnectError(
+ "Missing account in request",
+ "MISSING_ACCOUNT_IN_REQUEST",
+ session
+ );
}
const signer = getAccount(request.params.account);
if (!signer) {
throw new WalletConnectError(
`Unknown account, no signer: ${request.params.account}`,
- "UNAUTHORIZED_EVENT",
+ "INTERNAL_SIGNER_IS_MISSING",
session
);
}
@@ -168,7 +179,7 @@ export const useHandleWcRequest = () => {
modal = ;
}
onClose = () => {
- throw new WalletConnectError("Rejected by user", "USER_REJECTED", session);
+ handleUserRejected();
};
return openWith(modal, { onClose });
@@ -176,8 +187,9 @@ export const useHandleWcRequest = () => {
default:
throw new WalletConnectError(
`Unsupported method ${request.method}`,
- "WC_METHOD_UNSUPPORTED",
- session
+ "METHOD_UNSUPPORTED",
+ session,
+ request.method
);
}
});
diff --git a/packages/state/src/slices/errors.test.ts b/packages/state/src/slices/errors.test.ts
index d5e29e1bd1..b50cc36b75 100644
--- a/packages/state/src/slices/errors.test.ts
+++ b/packages/state/src/slices/errors.test.ts
@@ -29,6 +29,7 @@ describe("Errors reducer", () => {
description: `error ${i}`,
stacktrace: "stacktrace",
technicalDetails: "technicalDetails",
+ code: i,
})
);
}
diff --git a/packages/test-utils/src/errorContext.ts b/packages/test-utils/src/errorContext.ts
index 9e3369b971..7970eef22b 100644
--- a/packages/test-utils/src/errorContext.ts
+++ b/packages/test-utils/src/errorContext.ts
@@ -2,6 +2,7 @@ export const errorContext1 = {
timestamp: "2023-08-03T19:27:43.735Z",
description: "error1",
stacktrace: "stacktrace",
+ code: 100,
technicalDetails: "technicalDetails",
};
@@ -9,5 +10,6 @@ export const errorContext2 = {
timestamp: "2023-08-03T20:21:58.395Z",
description: "error1",
stacktrace: "stacktrace",
+ code: 200,
technicalDetails: "technicalDetails",
};
diff --git a/packages/utils/src/ErrorContext.test.ts b/packages/utils/src/ErrorContext.test.ts
index fd3ef71609..3b1609a98b 100644
--- a/packages/utils/src/ErrorContext.test.ts
+++ b/packages/utils/src/ErrorContext.test.ts
@@ -1,4 +1,12 @@
-import { CustomError, WalletConnectError, getErrorContext, handleTezError } from "./ErrorContext";
+import { TezosOperationError, type TezosOperationErrorWithMessage } from "@taquito/taquito";
+
+import {
+ CustomError,
+ WalletConnectError,
+ getTezErrorMessage,
+ getErrorContext,
+ getWcErrorResponse,
+} from "./ErrorContext";
describe("getErrorContext", () => {
it("should handle error object with message and stack", () => {
@@ -12,7 +20,7 @@ describe("getErrorContext", () => {
expect(context.technicalDetails).toBe("some error message");
expect(context.stacktrace).toBe("some stacktrace");
expect(context.description).toBe(
- "Something went wrong. Please try again or contact support if the issue persists."
+ "Something went wrong. Please try again. Contact support if the issue persists. Details: some error message"
);
expect(context.timestamp).toBeDefined();
});
@@ -25,7 +33,7 @@ describe("getErrorContext", () => {
expect(context.technicalDetails).toBe("string error message");
expect(context.stacktrace).toBe("");
expect(context.description).toBe(
- "Something went wrong. Please try again or contact support if the issue persists."
+ "Something went wrong. Please try again. Contact support if the issue persists."
);
expect(context.timestamp).toBeDefined();
});
@@ -48,53 +56,114 @@ describe("getErrorContext", () => {
const context = getErrorContext(error);
- expect(context.technicalDetails).toBe("");
+ expect(context.technicalDetails).toBeUndefined();
expect(context.description).toBe("Custom error message");
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 error = new WalletConnectError("Custom WC error message", "INTERNAL_ERROR", null);
const context = getErrorContext(error);
- expect(context.technicalDetails).toBe("");
+ expect(context.technicalDetails).toBeUndefined();
expect(context.description).toBe("Custom WC error message");
expect(context.stacktrace).toBeDefined();
expect(context.timestamp).toBeDefined();
});
});
-describe("handleTezError", () => {
+describe("getTezErrorMessage", () => {
it("catches subtraction_underflow", () => {
- const res = handleTezError(new Error("subtraction_underflow"));
+ const res = getTezErrorMessage("subtraction_underflow");
expect(res).toBe("Insufficient balance, please make sure you have enough funds.");
});
it("catches non_existing_contract", () => {
- const res = handleTezError(new Error("contract.non_existing_contract"));
+ const res = getTezErrorMessage("contract.non_existing_contract");
expect(res).toBe("Contract does not exist, please check if the correct network is selected.");
});
it("catches staking_to_delegate_that_refuses_external_staking", () => {
- const res = handleTezError(new Error("staking_to_delegate_that_refuses_external_staking"));
+ const res = getTezErrorMessage("staking_to_delegate_that_refuses_external_staking");
expect(res).toBe("The baker you are trying to stake to does not accept external staking.");
});
it("catches empty_implicit_delegated_contract", () => {
- const res = handleTezError(new Error("empty_implicit_delegated_contract"));
+ const res = getTezErrorMessage("empty_implicit_delegated_contract");
expect(res).toBe(
"Emptying an implicit delegated account is not allowed. End delegation before trying again."
);
});
it("catches delegate.unchanged", () => {
- const res = handleTezError(new Error("delegate.unchanged"));
+ const res = getTezErrorMessage("delegate.unchanged");
expect(res).toBe("The delegate is unchanged. Delegation to this address is already done.");
});
+ it("catches contract.manager.unregistered_delegate", () => {
+ const res = getTezErrorMessage("contract.manager.unregistered_delegate");
+ expect(res).toBe(
+ "The provided delegate address is not registered as a delegate. Verify the delegate address and ensure it is active."
+ );
+ });
+
it("returns undefined for unknown errors", () => {
- const err = new Error("unknown error");
- expect(handleTezError(err)).toBeUndefined();
+ const err = "unknown error";
+ expect(getTezErrorMessage(err)).toBeUndefined();
+ });
+
+ it("should return default error message for unknown error", () => {
+ const error = new Error("Unknown error");
+ const context = getErrorContext(error);
+ expect(context.description).toBe(
+ "Something went wrong. Please try again. Contact support if the issue persists. Details: Unknown error"
+ );
+ });
+
+ it("should return custom error message for CustomError", () => {
+ const error = new CustomError("Custom error message");
+ const context = getErrorContext(error);
+ expect(context.description).toBe("Custom error message");
+ });
+
+ it("should return WalletConnectError message", () => {
+ const error = new WalletConnectError("WC error custom text", "INTERNAL_ERROR", null);
+ const context = getErrorContext(error);
+ expect(context.description).toBe("WC error custom text");
+ expect(context.code).toBe(4011);
+ expect(context.technicalDetails).toBeUndefined();
+ });
+
+ it("should return TezosOperationError message", () => {
+ // const error = new TezosOperationError(errors:[], lastError: { id: 'michelson_v1.script_rejected', with: { prim: 'Unit' } });
+ const mockError: TezosOperationErrorWithMessage = {
+ kind: "temporary",
+ id: "proto.020-PsParisC.michelson_v1.script_rejected",
+ with: { string: "Fail entrypoint" }, // Include the `with` field for testing
+ };
+ const error = new TezosOperationError(
+ [mockError],
+ "Operation failed due to a rejected script.",
+ []
+ );
+ const context = getErrorContext(error);
+ expect(context.description).toContain(
+ "Rejected by chain. The contract code failed to run. Please check the contract. Details: Fail entrypoint"
+ );
+ expect(context.technicalDetails).toEqual([
+ "proto.020-PsParisC.michelson_v1.script_rejected",
+ { with: { string: "Fail entrypoint" } },
+ ]);
+ });
+
+ it("should return error response for getWcErrorResponse", () => {
+ const error = new Error("Unknown error");
+ const response = getWcErrorResponse(error);
+ expect(response.message).toBe(
+ "Something went wrong. Please try again. Contact support if the issue persists. Details: Unknown error"
+ );
+ expect(response.code).toBe(4011);
+ expect(response.data).toBe("Unknown error");
});
});
diff --git a/packages/utils/src/ErrorContext.ts b/packages/utils/src/ErrorContext.ts
index 6e1a9bad6d..31e4cb35cc 100644
--- a/packages/utils/src/ErrorContext.ts
+++ b/packages/utils/src/ErrorContext.ts
@@ -1,10 +1,15 @@
+import { type MichelsonV1ExpressionBase, type TezosGenericOperationError } from "@taquito/rpc";
+import { TezosOperationError, type TezosOperationErrorWithMessage } from "@taquito/taquito";
+import { type ErrorResponse } from "@walletconnect/jsonrpc-utils";
import { type SessionTypes } from "@walletconnect/types";
-import { type SdkErrorKey } from "@walletconnect/utils";
+
export type ErrorContext = {
timestamp: string;
description: string;
stacktrace: string;
- technicalDetails: string;
+ technicalDetails: any;
+ code: number;
+ data?: any;
};
export class CustomError extends Error {
@@ -15,51 +20,116 @@ export class CustomError extends Error {
}
export class WalletConnectError extends CustomError {
- sdkError: SdkErrorKey;
- constructor(message: string, sdkError: SdkErrorKey, session: SessionTypes.Struct | null) {
+ wcError: WcErrorKey;
+ context?: string | number;
+ constructor(
+ message: string,
+ wcError: WcErrorKey,
+ session: SessionTypes.Struct | null,
+ context?: string | number
+ ) {
const dappName = session?.peer.metadata.name ?? "unknown";
super(session ? `Request from ${dappName} is rejected. ${message}` : message);
this.name = "WalletConnectError";
- this.sdkError = sdkError;
+ this.wcError = wcError;
+ this.context = context;
}
}
+export type WcErrorKey = keyof typeof WC_ERRORS;
+export const WC_ERRORS = {
+ // JSON-RPC reserved error codes
+ PARSE_ERROR: { code: -32700, message: "Invalid JSON received by the server." },
+ INVALID_REQUEST: { code: -32600, message: "The JSON sent is not a valid request object." },
+ METHOD_NOT_FOUND: { code: -32601, message: "The method does not exist or is not available." },
+ INVALID_PARAMS: { code: -32602, message: "Invalid method parameters." },
+ INTERNAL_ERROR: { code: -32603, message: "Internal JSON-RPC error." },
+
+ // Application-specific errors (codes >= 0 for clarity)
+ USER_REJECTED: { code: 4001, message: "User rejected the request." },
+ UNSUPPORTED_CHAINS: { code: 4002, message: "Unsupported chains." },
+ METHOD_UNSUPPORTED: { code: 4003, message: "Method unsupported." },
+ SESSION_NOT_FOUND: { code: 4004, message: "Session not found." },
+ MISSING_ACCOUNT_IN_REQUEST: { code: 4005, message: "Missing account in request." },
+ INTERNAL_SIGNER_IS_MISSING: { code: 4006, message: "Internal signer is missing." },
+ SIGNER_ADDRESS_NOT_REVEALED: {
+ code: 4007,
+ message:
+ "Signer address is not revealed on the chain. To reveal it, send any amount, e.g., 0.000001ęś©, from that address to yourself. Wait several minutes and try again.",
+ },
+ UNKNOWN_CURVE_FOR_PUBLIC_KEY: { code: 4008, message: "Unknown curve for the public key." },
+ REJECTED_BY_CHAIN: { code: 4009, message: "Request rejected by chain." },
+ DELEGATE_UNCHANGED: { code: 4010, message: "The delegate is unchanged." },
+ UNKNOWN_ERROR: { code: 4011, message: "Unknown error." },
+};
+
// 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")) {
+export const getTezErrorMessage = (err: string): string | undefined => {
+ if (err.includes("subtraction_underflow")) {
return "Insufficient balance, please make sure you have enough funds.";
- } else if (err.message.includes("contract.non_existing_contract")) {
+ } else if (err.includes("contract.non_existing_contract")) {
return "Contract does not exist, please check if the correct network is selected.";
- } else if (err.message.includes("staking_to_delegate_that_refuses_external_staking")) {
+ } else if (err.includes("staking_to_delegate_that_refuses_external_staking")) {
return "The baker you are trying to stake to does not accept external staking.";
- } else if (err.message.includes("empty_implicit_delegated_contract")) {
+ } else if (err.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")) {
+ } else if (err.includes("delegate.unchanged")) {
return "The delegate is unchanged. Delegation to this address is already done.";
+ } else if (err.includes("contract.manager.unregistered_delegate")) {
+ return "The provided delegate address is not registered as a delegate. Verify the delegate address and ensure it is active.";
+ } else if (err.includes("michelson_v1.script_rejected")) {
+ return "The contract code failed to run. Please check the contract.";
}
};
+const isTezosOperationErrorWithMessage = (
+ error: TezosGenericOperationError
+): error is TezosOperationErrorWithMessage => "with" in error;
+
export const getErrorContext = (error: any): ErrorContext => {
- let description =
- "Something went wrong. Please try again or contact support if the issue persists.";
- let technicalDetails;
+ const defaultDescription =
+ "Something went wrong. Please try again. Contact support if the issue persists.";
+ let description = defaultDescription;
+ let technicalDetails: any = undefined;
+ let code = WC_ERRORS.UNKNOWN_ERROR.code;
+ const errorMessage = typeof error === "string" ? error : error.message;
let stacktrace = "";
if (typeof error === "object" && "stack" in error) {
stacktrace = error.stack;
- }
-
- if (typeof error === "object" && "message" in error) {
- technicalDetails = error.message;
} else if (typeof error === "string") {
technicalDetails = error;
}
if (error instanceof CustomError) {
- description = error.message;
- technicalDetails = "";
- } else if (error instanceof Error) {
- description = handleTezError(error) ?? description;
+ description = errorMessage;
+ } else if (error instanceof WalletConnectError) {
+ const message = WC_ERRORS[error.wcError].message;
+ code = WC_ERRORS[error.wcError].code;
+ description = message + errorMessage;
+ technicalDetails = error.context;
+ } else if (error instanceof TezosOperationError) {
+ code = WC_ERRORS.REJECTED_BY_CHAIN.code;
+ const lastError = error.lastError;
+ description =
+ "Rejected by chain. " +
+ (getTezErrorMessage(lastError.id) ?? "") +
+ " Details: " +
+ errorMessage;
+ if (isTezosOperationErrorWithMessage(lastError)) {
+ const failswith: MichelsonV1ExpressionBase = lastError.with;
+ technicalDetails = [lastError.id, { with: failswith }];
+ } else {
+ technicalDetails = [lastError.id];
+ }
+ } else if (error instanceof Error || Object.prototype.hasOwnProperty.call(error, "message")) {
+ const explanation = getTezErrorMessage(errorMessage);
+ if (explanation) {
+ description = explanation;
+ } else {
+ description = `${defaultDescription} Details: ${errorMessage}`;
+ }
+ technicalDetails = errorMessage;
}
return {
@@ -67,5 +137,16 @@ export const getErrorContext = (error: any): ErrorContext => {
description,
stacktrace,
technicalDetails,
+ code,
+ };
+};
+
+export const getWcErrorResponse = (error: any): ErrorResponse => {
+ const context = getErrorContext(error);
+ const response: ErrorResponse = {
+ code: context.code,
+ message: context.description,
+ data: context.technicalDetails,
};
+ return response;
};