Skip to content

Commit

Permalink
feat: WalletConnect: error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
dianasavvatina committed Jan 13, 2025
1 parent d1d1a3c commit ca14aab
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 67 deletions.
4 changes: 1 addition & 3 deletions apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -61,8 +60,7 @@ export const ErrorLogsMenu = () => {
</Text>
{errorLog.technicalDetails && (
<Text marginTop="12px" color={color("700")} size="sm">
{handleTezError({ name: "unknown", message: errorLog.technicalDetails }) ??
""}
{JSON.stringify(errorLog.technicalDetails)}
</Text>
)}
</Flex>
Expand Down
28 changes: 15 additions & 13 deletions apps/web/src/components/WalletConnect/WalletConnectProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand Down Expand Up @@ -94,7 +95,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
handleAsyncActionUnsafe(async () => {
const activeSessions: Record<string, SessionTypes.Struct> = 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];
Expand All @@ -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]
Expand Down
42 changes: 27 additions & 15 deletions apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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];
Expand All @@ -89,15 +95,20 @@ 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]);
if (!network) {
throw new WalletConnectError(
`Unsupported network ${chainId}`,
"UNSUPPORTED_CHAINS",
session
session,
chainId
);
}

Expand All @@ -115,24 +126,24 @@ export const useHandleWcRequest = () => {

modal = <SignPayloadRequestModal opts={signPayloadProps} />;
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
);
}
Expand Down Expand Up @@ -168,16 +179,17 @@ export const useHandleWcRequest = () => {
modal = <BatchSignPage {...signProps} {...event.params.request.params} />;
}
onClose = () => {
throw new WalletConnectError("Rejected by user", "USER_REJECTED", session);
handleUserRejected();
};

return openWith(modal, { onClose });
}
default:
throw new WalletConnectError(
`Unsupported method ${request.method}`,
"WC_METHOD_UNSUPPORTED",
session
"METHOD_UNSUPPORTED",
session,
request.method
);
}
});
Expand Down
1 change: 1 addition & 0 deletions packages/state/src/slices/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe("Errors reducer", () => {
description: `error ${i}`,
stacktrace: "stacktrace",
technicalDetails: "technicalDetails",
code: i,
})
);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/test-utils/src/errorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ export const errorContext1 = {
timestamp: "2023-08-03T19:27:43.735Z",
description: "error1",
stacktrace: "stacktrace",
code: 100,
technicalDetails: "technicalDetails",
};

export const errorContext2 = {
timestamp: "2023-08-03T20:21:58.395Z",
description: "error1",
stacktrace: "stacktrace",
code: 200,
technicalDetails: "technicalDetails",
};
97 changes: 83 additions & 14 deletions packages/utils/src/ErrorContext.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { CustomError, WalletConnectError, getErrorContext, handleTezError } from "./ErrorContext";
import { TezosOperationError, type TezosOperationErrorWithMessage } from "@taquito/taquito";

import {
CustomError,
WalletConnectError,
explainTezError,
getErrorContext,
getWcErrorResponse,
} from "./ErrorContext";

describe("getErrorContext", () => {
it("should handle error object with message and stack", () => {
Expand All @@ -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();
});
Expand All @@ -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();
});
Expand All @@ -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("explainTezError", () => {
it("catches subtraction_underflow", () => {
const res = handleTezError(new Error("subtraction_underflow"));
const res = explainTezError("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 = explainTezError("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 = explainTezError("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 = explainTezError("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 = explainTezError("delegate.unchanged");
expect(res).toBe("The delegate is unchanged. Delegation to this address is already done.");
});

it("catches contract.manager.unregistered_delegate", () => {
const res = explainTezError("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(explainTezError(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");
});
});
Loading

1 comment on commit ca14aab

@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% (1787/2132) 79.58% (850/1068) 78.27% (454/580)
apps/web Coverage: 83%
83.81% (1787/2132) 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.37% (215/261) 72.51% (95/131) 81.66% (49/60)
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.39% (822/974) 81.03% (188/232) 77.77% (301/387)
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.