From 816dfa0dbbd900918f2315ce79771df7566ec9d7 Mon Sep 17 00:00:00 2001 From: Oleg Chendighelean Date: Thu, 5 Dec 2024 16:27:56 +0000 Subject: [PATCH] Move error handling logic to @umami/utils; Use CustomError for custom error message --- apps/desktop-e2e/package.json | 1 + apps/desktop-e2e/src/helpers/AccountGroup.ts | 11 ++- apps/desktop-e2e/src/steps/onboarding.ts | 3 +- apps/desktop-e2e/src/steps/utils.ts | 5 +- apps/desktop/package.json | 1 + .../MultisigDecodedOperation.tsx | 3 +- .../MultisigSignerTile.tsx | 3 +- .../CSVFileUploader/CSVFileUploadForm.tsx | 7 +- .../src/components/CSVFileUploader/utils.ts | 13 +-- .../src/components/Onboarding/FakeAccount.tsx | 3 +- .../masterPassword/MasterPassword.tsx | 3 +- .../restoreMnemonic/RestoreMnemonic.tsx | 5 +- .../SendFlow/Beacon/BeaconSignPage.tsx | 4 +- .../SignTransactionFormPage.tsx | 3 +- .../src/components/UpsertContactModal.tsx | 3 +- apps/desktop/src/index.tsx | 2 +- .../utils/beacon/useHandleBeaconMessage.tsx | 7 +- apps/desktop/src/views/batch/BatchView.tsx | 3 +- .../desktop/src/views/batch/OperationView.tsx | 3 +- apps/desktop/src/views/home/AccountGroup.tsx | 3 +- .../views/settings/ErrorLogsDrawerCard.tsx | 2 +- apps/embed-iframe/package.json | 1 + apps/embed-iframe/src/operationModalHooks.tsx | 3 +- apps/web/package.json | 1 + .../AddContactModal/AddContactModal.tsx | 3 +- .../Menu/AddressBookMenu/EditContactMenu.tsx | 3 +- .../Menu/ErrorLogsMenu/ErrorLogsMenu.test.tsx | 2 +- .../Onboarding/ImportWallet/SeedPhraseTab.tsx | 7 +- .../SendFlow/Beacon/BeaconSignPage.tsx | 4 +- .../WalletConnect/WalletConnectProvider.tsx | 9 +- .../beacon/useHandleBeaconMessage.tsx | 7 +- apps/web/src/main.tsx | 2 +- packages/core/package.json | 1 + packages/core/src/AccountOperations.ts | 3 +- packages/core/src/ErrorContext.test.ts | 55 ----------- packages/core/src/ErrorContext.ts | 36 ------- packages/core/src/Operation.ts | 6 +- packages/core/src/beaconUtils.ts | 5 +- packages/core/src/decodeBeaconPayload.ts | 6 +- packages/core/src/estimate.test.ts | 31 +----- packages/core/src/estimate.ts | 16 +-- packages/core/src/index.ts | 1 - packages/crypto/package.json | 1 + packages/crypto/src/AES.ts | 6 +- packages/data-polling/package.json | 1 + .../src/useReactQueryErrorHandler.ts | 2 +- packages/multisig/package.json | 1 + packages/multisig/src/helpers.ts | 3 +- packages/state/package.json | 1 + packages/state/src/hooks/backup.ts | 5 +- packages/state/src/hooks/getAccountData.ts | 9 +- packages/state/src/hooks/setAccountData.ts | 3 +- .../state/src/hooks/useAsyncActionHandler.ts | 3 +- .../state/src/slices/accounts/accounts.ts | 7 +- packages/state/src/slices/errors.ts | 2 +- .../src/thunks/changeMnemonicPassword.ts | 9 +- packages/state/src/thunks/secretKeyAccount.ts | 3 +- packages/tezos/package.json | 1 + packages/tezos/src/Address.ts | 9 +- packages/tezos/src/helpers.ts | 7 +- packages/tzkt/package.json | 1 + packages/tzkt/src/withRateLimit.test.ts | 6 +- packages/tzkt/src/withRateLimit.ts | 5 +- packages/utils/.depcheckrc | 2 + packages/utils/.eslintrc.cjs | 9 ++ packages/utils/babel.config.json | 3 + packages/utils/jest.config.ts | 9 ++ packages/utils/package.json | 65 +++++++++++++ packages/utils/src/ErrorContext.test.ts | 83 ++++++++++++++++ packages/utils/src/ErrorContext.ts | 57 +++++++++++ packages/utils/src/index.ts | 1 + packages/utils/tsconfig.json | 4 + pnpm-lock.yaml | 97 +++++++++++++++++++ 73 files changed, 482 insertions(+), 223 deletions(-) delete mode 100644 packages/core/src/ErrorContext.test.ts delete mode 100644 packages/core/src/ErrorContext.ts create mode 100644 packages/utils/.depcheckrc create mode 100644 packages/utils/.eslintrc.cjs create mode 100644 packages/utils/babel.config.json create mode 100644 packages/utils/jest.config.ts create mode 100644 packages/utils/package.json create mode 100644 packages/utils/src/ErrorContext.test.ts create mode 100644 packages/utils/src/ErrorContext.ts create mode 100644 packages/utils/src/index.ts create mode 100644 packages/utils/tsconfig.json diff --git a/apps/desktop-e2e/package.json b/apps/desktop-e2e/package.json index cc98e44cf7..00455c626c 100644 --- a/apps/desktop-e2e/package.json +++ b/apps/desktop-e2e/package.json @@ -34,6 +34,7 @@ "@umami/tezos": "workspace:^", "@umami/typescript-config": "workspace:^", "@umami/tzkt": "workspace:^", + "@umami/utils": "workspace:^", "date-fns": "^4.1.0", "lodash": "^4.17.21", "react": "^18.3.1", diff --git a/apps/desktop-e2e/src/helpers/AccountGroup.ts b/apps/desktop-e2e/src/helpers/AccountGroup.ts index 7f8baaf8ae..3091c6210a 100644 --- a/apps/desktop-e2e/src/helpers/AccountGroup.ts +++ b/apps/desktop-e2e/src/helpers/AccountGroup.ts @@ -1,5 +1,6 @@ import { InMemorySigner } from "@taquito/signer"; import { type RawPkh, derivePublicKeyPair, makeDerivationPath } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import lodash from "lodash"; export type AccountGroup = { @@ -41,14 +42,14 @@ export class AccountGroupBuilder { setDerivationPathTemplate(derivationPathTemplate: string): void { if (this.accountGroup.type !== "mnemonic") { - throw new Error(`Derivation path is not used for ${this.accountGroup.type} accounts}`); + throw new CustomError(`Derivation path is not used for ${this.accountGroup.type} accounts}`); } this.derivationPathTemplate = derivationPathTemplate; } setSeedPhrase(seedPhrase: string[]): void { if (this.accountGroup.type !== "mnemonic") { - throw new Error(`Seed phrase is not used for ${this.accountGroup.type} accounts}`); + throw new CustomError(`Seed phrase is not used for ${this.accountGroup.type} accounts}`); } this.seedPhrase = seedPhrase; } @@ -57,7 +58,7 @@ export class AccountGroupBuilder { async setSecretKey(secretKey: string, accountIndex = 0): Promise { if (this.accountGroup.type !== "secret_key") { - throw new Error(`Secret key is not used for ${this.accountGroup.type} accounts}`); + throw new CustomError(`Secret key is not used for ${this.accountGroup.type} accounts}`); } this.accountGroup.accounts[accountIndex].pkh = await ( await InMemorySigner.fromSecretKey(secretKey) @@ -84,10 +85,10 @@ export class AccountGroupBuilder { private async setMnemonicPkhs() { if (this.seedPhrase.length === 0) { - throw new Error("Seed phrase is not set"); + throw new CustomError("Seed phrase is not set"); } if (this.derivationPathTemplate === "") { - throw new Error("Derivation path is not set"); + throw new CustomError("Derivation path is not set"); } for (let i = 0; i < this.accountGroup.accounts.length; i++) { const keyPair = await derivePublicKeyPair( diff --git a/apps/desktop-e2e/src/steps/onboarding.ts b/apps/desktop-e2e/src/steps/onboarding.ts index a6fc496c34..711aa5c27e 100644 --- a/apps/desktop-e2e/src/steps/onboarding.ts +++ b/apps/desktop-e2e/src/steps/onboarding.ts @@ -4,6 +4,7 @@ import { Given, Then, When } from "@cucumber/cucumber"; import { expect } from "@playwright/test"; import { mnemonic1 as existingSeedphrase } from "@umami/test-utils"; import { DEFAULT_DERIVATION_PATH_TEMPLATE } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { type CustomWorld } from "./world"; import { type AccountGroup, AccountGroupBuilder } from "../helpers/AccountGroup"; @@ -119,7 +120,7 @@ Then( if (backupFileName === "V2Backup.json") { expectedGroups = await v2BackedupAccountGroups(); } else { - throw new Error(`Unknown backup file: ${backupFileName}`); + throw new CustomError(`Unknown backup file: ${backupFileName}`); } // TODO: check for groups amount once all type of groups are supported by the tests diff --git a/apps/desktop-e2e/src/steps/utils.ts b/apps/desktop-e2e/src/steps/utils.ts index fbca4377de..463c961843 100644 --- a/apps/desktop-e2e/src/steps/utils.ts +++ b/apps/desktop-e2e/src/steps/utils.ts @@ -4,6 +4,7 @@ import { type Account } from "@umami/core"; import { type AccountsState, makeSecretKeyAccount } from "@umami/state"; import { BLOCK_TIME } from "@umami/tezos"; import { getOperationsByHash } from "@umami/tzkt"; +import { CustomError } from "@umami/utils"; import { minutesToMilliseconds } from "date-fns"; import { some } from "lodash"; @@ -28,7 +29,7 @@ Given(/I have accounts?/, async function (this: CustomWorld, table: DataTable) { accounts.items.push(account); accounts.secretKeys[account.address.pkh] = encryptedSecretKey; } else { - throw new Error(`${data.type} account is not supported yet`); + throw new CustomError(`${data.type} account is not supported yet`); } } this.setReduxState({ accounts }); @@ -105,7 +106,7 @@ When( return matches[1]; } } - throw new Error("TZKT sync last applied block not found"); + throw new CustomError("TZKT sync last applied block not found"); }; for (let i = 0; i < 2; i++) { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 5edeae99e1..4d36881677 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -93,6 +93,7 @@ "@umami/state": "workspace:^", "@umami/test-utils": "workspace:^", "@umami/tezos": "workspace:^", + "@umami/utils": "workspace:^", "@umami/typescript-config": "workspace:^", "@umami/tzkt": "workspace:^", "@vitejs/plugin-react": "^4.3.4", diff --git a/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigDecodedOperation.tsx b/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigDecodedOperation.tsx index dbbcae4e8d..ff56d7eb67 100644 --- a/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigDecodedOperation.tsx +++ b/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigDecodedOperation.tsx @@ -2,6 +2,7 @@ import { Box, Flex, Text } from "@chakra-ui/react"; import { type Operation, tokenNameSafe, tokenPrettyAmount } from "@umami/core"; import { useGetToken } from "@umami/state"; import { prettyTezAmount } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { OutgoingArrow } from "../../../../assets/icons"; import colors from "../../../../style/colors"; @@ -45,7 +46,7 @@ export const MultisigDecodedOperation = ({ operation }: { operation: Operation } case "stake": case "unstake": case "finalize_unstake": - throw new Error(`${operation.type} is not supported yet`); + throw new CustomError(`${operation.type} is not supported yet`); } }; diff --git a/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigSignerTile.tsx b/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigSignerTile.tsx index 4db48f6212..6ab92217e5 100644 --- a/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigSignerTile.tsx +++ b/apps/desktop/src/components/AccountDrawer/AssetsPanel/MultisigPendingOperations/MultisigSignerTile.tsx @@ -9,6 +9,7 @@ import { import { type MultisigOperation, parseRawMichelson } from "@umami/multisig"; import { useAsyncActionHandler, useGetImplicitAccountSafe, useSelectedNetwork } from "@umami/state"; import { type ImplicitAddress } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { MultisigActionButton, type MultisigSignerState } from "./MultisigActionButton"; import colors from "../../../../style/colors"; @@ -40,7 +41,7 @@ export const MultisigSignerTile = ({ const approveOrExecute = () => handleAsyncAction(async () => { if (!signer) { - throw new Error("Can't approve or execute with an account you don't own"); + throw new CustomError("Can't approve or execute with an account you don't own"); } const actionType = operationIsExecutable ? "execute" : "approve"; diff --git a/apps/desktop/src/components/CSVFileUploader/CSVFileUploadForm.tsx b/apps/desktop/src/components/CSVFileUploader/CSVFileUploadForm.tsx index 39b152fd17..508c0c687a 100644 --- a/apps/desktop/src/components/CSVFileUploader/CSVFileUploadForm.tsx +++ b/apps/desktop/src/components/CSVFileUploader/CSVFileUploadForm.tsx @@ -25,6 +25,7 @@ import { useSelectedNetwork, } from "@umami/state"; import { type RawPkh } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import Papa, { type ParseResult } from "papaparse"; import { FormProvider, useForm } from "react-hook-form"; @@ -63,7 +64,9 @@ export const CSVFileUploadForm = () => { Papa.parse(file[0], { skipEmptyLines: true, complete: resolve }); }); if (rows.errors.length > 0) { - throw new Error("Error loading csv file: " + rows.errors.map(e => e.message).join(", ")); + throw new CustomError( + "Error loading csv file: " + rows.errors.map(e => e.message).join(", ") + ); } const operations: Operation[] = []; @@ -72,7 +75,7 @@ export const CSVFileUploadForm = () => { try { operations.push(parseOperation(senderAccount.address, row, getToken)); } catch (error: any) { - throw new Error(`Error at row #${i + 1}: ${error?.message}`); + throw new CustomError(`Error at row #${i + 1}: ${error?.message}`); } } diff --git a/apps/desktop/src/components/CSVFileUploader/utils.ts b/apps/desktop/src/components/CSVFileUploader/utils.ts index 6496530179..3148c75b01 100644 --- a/apps/desktop/src/components/CSVFileUploader/utils.ts +++ b/apps/desktop/src/components/CSVFileUploader/utils.ts @@ -8,6 +8,7 @@ import { parsePkh, tezToMutez, } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { validateNonNegativeNumber } from "../../utils/helpers"; @@ -19,16 +20,16 @@ export const parseOperation = ( const filteredRow = row.filter(col => col.length > 0); const len = filteredRow.length; if (len < 2 || 4 < len) { - throw new Error("Invalid csv format"); + throw new CustomError("Invalid csv format"); } const [recipientPkh, prettyAmount, contractPkh] = filteredRow; if (!isAddressValid(recipientPkh)) { - throw new Error("Invalid csv value: recipient"); + throw new CustomError("Invalid csv value: recipient"); } const recipient = parsePkh(recipientPkh); if (validateNonNegativeNumber(prettyAmount) === null) { - throw new Error("Invalid csv value: amount"); + throw new CustomError("Invalid csv value: amount"); } if (len === 2) { @@ -40,18 +41,18 @@ export const parseOperation = ( } if (!isValidContractPkh(contractPkh)) { - throw new Error("Invalid csv value: contract address"); + throw new CustomError("Invalid csv value: contract address"); } const contract = parseContractPkh(contractPkh); const tokenId = filteredRow[3] || "0"; if (validateNonNegativeNumber(tokenId) === null) { - throw new Error("Invalid csv value: tokenId"); + throw new CustomError("Invalid csv value: tokenId"); } const token = getToken(contractPkh, tokenId); if (!token) { - throw new Error(`Unknown token ${contractPkh} ${tokenId}`); + throw new CustomError(`Unknown token ${contractPkh} ${tokenId}`); } const amount = getRealAmount(token, prettyAmount); diff --git a/apps/desktop/src/components/Onboarding/FakeAccount.tsx b/apps/desktop/src/components/Onboarding/FakeAccount.tsx index fb6a7b7629..031f21dcb6 100644 --- a/apps/desktop/src/components/Onboarding/FakeAccount.tsx +++ b/apps/desktop/src/components/Onboarding/FakeAccount.tsx @@ -9,6 +9,7 @@ import { makeDerivationPath, parseImplicitPkh, } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { useForm } from "react-hook-form"; import { ModalContentWrapper } from "./ModalContentWrapper"; @@ -26,7 +27,7 @@ export const FakeAccount = ({ onClose }: { onClose: () => void }) => { const onSubmit = async ({ pkh, name, idp }: { pkh: string; name: string; idp?: IDP }) => { if (idp && idp.length > 0) { if (!["google", "facebook", "twitter", "reddit", "email"].includes(idp)) { - throw new Error("Invalid IDP"); + throw new CustomError("Invalid IDP"); } } const rpc = new RpcClient(GHOSTNET.rpcUrl); diff --git a/apps/desktop/src/components/Onboarding/masterPassword/MasterPassword.tsx b/apps/desktop/src/components/Onboarding/masterPassword/MasterPassword.tsx index 1e8d5a7234..0b875ac09a 100644 --- a/apps/desktop/src/components/Onboarding/masterPassword/MasterPassword.tsx +++ b/apps/desktop/src/components/Onboarding/masterPassword/MasterPassword.tsx @@ -6,6 +6,7 @@ import { useRestoreFromSecretKey, useValidateMasterPassword, } from "@umami/state"; +import { CustomError } from "@umami/utils"; import { type MasterPasswordStep } from "../OnboardingStep"; import { EnterAndConfirmPassword } from "./password/EnterAndConfirmPassword"; @@ -37,7 +38,7 @@ export const MasterPassword = ({ } if (!account) { - throw new Error("No account data provided."); + throw new CustomError("No account data provided."); } switch (account.type) { diff --git a/apps/desktop/src/components/Onboarding/restoreMnemonic/RestoreMnemonic.tsx b/apps/desktop/src/components/Onboarding/restoreMnemonic/RestoreMnemonic.tsx index 146e865ca9..9606fd23a1 100644 --- a/apps/desktop/src/components/Onboarding/restoreMnemonic/RestoreMnemonic.tsx +++ b/apps/desktop/src/components/Onboarding/restoreMnemonic/RestoreMnemonic.tsx @@ -3,6 +3,7 @@ import { Box, Button, Grid, GridItem, Heading, Select, VStack } from "@chakra-ui import { MnemonicAutocomplete } from "@umami/components"; import { useAsyncActionHandler } from "@umami/state"; import { mnemonic1 } from "@umami/test-utils"; +import { CustomError } from "@umami/utils"; import { validateMnemonic } from "bip39"; import { range } from "lodash"; import { useState } from "react"; @@ -53,7 +54,7 @@ export const RestoreMnemonic = ({ goToStep }: { goToStep: (step: OnboardingStep) handleAsyncAction(async () => { const words = mnemonic.split(" "); if (!mnemonicSizes.includes(words.length)) { - throw new Error(`the mnemonic must be ${mnemonicSizes.join(", ")} words long`); + throw new CustomError(`the mnemonic must be ${mnemonicSizes.join(", ")} words long`); } words.slice(0, mnemonicSize).forEach((word, i) => { setValue(`word${i}`, word); @@ -65,7 +66,7 @@ export const RestoreMnemonic = ({ goToStep }: { goToStep: (step: OnboardingStep) handleAsyncAction(async () => { const mnemonic = Object.values(data).join(" ").trim(); if (!validateMnemonic(mnemonic)) { - throw new Error("Invalid Mnemonic"); + throw new CustomError("Invalid Mnemonic"); } goToStep({ type: "nameAccount", diff --git a/apps/desktop/src/components/SendFlow/Beacon/BeaconSignPage.tsx b/apps/desktop/src/components/SendFlow/Beacon/BeaconSignPage.tsx index cfe8817ee3..8d4049d32f 100644 --- a/apps/desktop/src/components/SendFlow/Beacon/BeaconSignPage.tsx +++ b/apps/desktop/src/components/SendFlow/Beacon/BeaconSignPage.tsx @@ -1,3 +1,5 @@ +import { CustomError } from "@umami/utils"; + import { type BeaconSignPageProps } from "./BeaconSignPageProps"; import { ContractCallSignPage } from "./ContractCallSignPage"; import { DelegationSignPage } from "./DelegationSignPage"; @@ -40,6 +42,6 @@ export const BeaconSignPage = ({ operation, message }: BeaconSignPageProps) => { */ case "fa1.2": case "fa2": - throw new Error("Unsupported operation type"); + throw new CustomError("Unsupported operation type"); } }; diff --git a/apps/desktop/src/components/SendFlow/MultisigAccount/SignTransactionFormPage.tsx b/apps/desktop/src/components/SendFlow/MultisigAccount/SignTransactionFormPage.tsx index f7321ba569..dea10a4eee 100644 --- a/apps/desktop/src/components/SendFlow/MultisigAccount/SignTransactionFormPage.tsx +++ b/apps/desktop/src/components/SendFlow/MultisigAccount/SignTransactionFormPage.tsx @@ -12,6 +12,7 @@ import { import { type TezosToolkit } from "@taquito/taquito"; import { multisigsActions, useAppDispatch, useAsyncActionHandler } from "@umami/state"; import { parsePkh } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { FormProvider } from "react-hook-form"; import { type FormValues } from "./FormValues"; @@ -63,7 +64,7 @@ export const SignTransactionFormPage = (props: SignPageProps) => { * fetch the contract address, we won't assign the provided label and * the contract will appear with a default label */ - throw new Error("An error occurred during contract origination"); + throw new CustomError("An error occurred during contract origination"); } const pkh = (await operation.getOriginatedContractAddresses())[0]; diff --git a/apps/desktop/src/components/UpsertContactModal.tsx b/apps/desktop/src/components/UpsertContactModal.tsx index fb67261140..389cdd0c68 100644 --- a/apps/desktop/src/components/UpsertContactModal.tsx +++ b/apps/desktop/src/components/UpsertContactModal.tsx @@ -22,6 +22,7 @@ import { useValidateNewContactPkh, } from "@umami/state"; import { isValidContractPkh } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { type FC, useEffect, useRef } from "react"; import { useForm } from "react-hook-form"; @@ -52,7 +53,7 @@ export const UpsertContactModal: FC<{ newContact.pkh, ]); if (!contractsWithNetworks.has(newContact.pkh)) { - throw new Error(`Network not found for contract ${newContact.pkh}`); + throw new CustomError(`Network not found for contract ${newContact.pkh}`); } dispatch( contactsActions.upsert({ diff --git a/apps/desktop/src/index.tsx b/apps/desktop/src/index.tsx index 0c3f732c66..78e58110b5 100644 --- a/apps/desktop/src/index.tsx +++ b/apps/desktop/src/index.tsx @@ -1,8 +1,8 @@ /* istanbul ignore file */ import "./index.css"; -import { getErrorContext } from "@umami/core"; import { errorsActions } from "@umami/state"; +import { getErrorContext } from "@umami/utils"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { ErrorBoundary } from "react-error-boundary"; diff --git a/apps/desktop/src/utils/beacon/useHandleBeaconMessage.tsx b/apps/desktop/src/utils/beacon/useHandleBeaconMessage.tsx index df0048bb52..510c027efe 100644 --- a/apps/desktop/src/utils/beacon/useHandleBeaconMessage.tsx +++ b/apps/desktop/src/utils/beacon/useHandleBeaconMessage.tsx @@ -14,6 +14,7 @@ import { useRemoveBeaconPeerBySenderId, } from "@umami/state"; import { type Network } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { PermissionRequestModal } from "./PermissionRequestModal"; import { SignPayloadRequestModal } from "./SignPayloadRequestModal"; @@ -49,7 +50,7 @@ export const useHandleBeaconMessage = () => { type: BeaconMessageType.Error, errorType: BeaconErrorType.NETWORK_NOT_SUPPORTED, }); - throw new Error( + throw new CustomError( `Got Beacon request from an unknown network: ${JSON.stringify( beaconNetwork )}. Please add it to the networks list and retry.` @@ -101,7 +102,7 @@ export const useHandleBeaconMessage = () => { type: BeaconMessageType.Error, errorType: BeaconErrorType.NO_PRIVATE_KEY_FOUND_ERROR, }); - throw new Error(`Unknown account: ${message.sourceAddress}`); + throw new CustomError(`Unknown account: ${message.sourceAddress}`); } const operation = toAccountOperations( message.operationDetails, @@ -132,7 +133,7 @@ export const useHandleBeaconMessage = () => { errorType: BeaconErrorType.UNKNOWN_ERROR, }); - throw new Error(`Unknown Beacon message type: ${message.type}`); + throw new CustomError(`Unknown Beacon message type: ${message.type}`); } } diff --git a/apps/desktop/src/views/batch/BatchView.tsx b/apps/desktop/src/views/batch/BatchView.tsx index 8d0820d209..fc9867f2c0 100644 --- a/apps/desktop/src/views/batch/BatchView.tsx +++ b/apps/desktop/src/views/batch/BatchView.tsx @@ -11,6 +11,7 @@ import { useSelectedNetwork, } from "@umami/state"; import { TEZ } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import pluralize from "pluralize"; import { useEffect, useState } from "react"; @@ -76,7 +77,7 @@ const prettyOperationType = (operation: Operation) => { return "Finalize Unstake"; case "contract_origination": case "contract_call": - throw new Error(`${operation.type} is not supported yet`); + throw new CustomError(`${operation.type} is not supported yet`); } }; diff --git a/apps/desktop/src/views/batch/OperationView.tsx b/apps/desktop/src/views/batch/OperationView.tsx index c2363ce57d..305fdfd0b7 100644 --- a/apps/desktop/src/views/batch/OperationView.tsx +++ b/apps/desktop/src/views/batch/OperationView.tsx @@ -2,6 +2,7 @@ import { AspectRatio, Flex, Heading, Image, Link, Tooltip } from "@chakra-ui/rea import { type Operation, thumbnailUri, tokenNameSafe, tokenUri } from "@umami/core"; import { useGetToken, useSelectedNetwork } from "@umami/state"; import { getIPFSurl, prettyTezAmount } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { tokenTitle } from "./tokenTitle"; import { BakerIcon, OutgoingArrow } from "../../assets/icons"; @@ -105,6 +106,6 @@ export const OperationView = ({ operation }: { operation: Operation }) => { ); case "contract_origination": case "contract_call": - throw new Error(`${operation.type} is not supported yet`); + throw new CustomError(`${operation.type} is not supported yet`); } }; diff --git a/apps/desktop/src/views/home/AccountGroup.tsx b/apps/desktop/src/views/home/AccountGroup.tsx index b24541927a..10bcdadb6d 100644 --- a/apps/desktop/src/views/home/AccountGroup.tsx +++ b/apps/desktop/src/views/home/AccountGroup.tsx @@ -2,6 +2,7 @@ import { Box, Center, Heading } from "@chakra-ui/react"; import { useDynamicModalContext } from "@umami/components"; import { type Account, getAccountGroupLabel } from "@umami/core"; import { useImplicitAccounts, useRemoveMnemonic, useRemoveNonMnemonic } from "@umami/state"; +import { CustomError } from "@umami/utils"; import { AccountGroupPopover } from "./AccountGroupPopover"; import { DeriveMnemonicAccountModal } from "./DeriveMnemonicAccountModal"; @@ -59,7 +60,7 @@ export const AccountGroup = ({ const onDerive = () => { if (!isMnemonic) { - throw new Error("Can't derive a non mnemonic account!"); + throw new CustomError("Can't derive a non mnemonic account!"); } return openWith( diff --git a/apps/desktop/src/views/settings/ErrorLogsDrawerCard.tsx b/apps/desktop/src/views/settings/ErrorLogsDrawerCard.tsx index 815ed9a431..887b6874d6 100644 --- a/apps/desktop/src/views/settings/ErrorLogsDrawerCard.tsx +++ b/apps/desktop/src/views/settings/ErrorLogsDrawerCard.tsx @@ -12,8 +12,8 @@ import { useDisclosure, } from "@chakra-ui/react"; import { nanoid } from "@reduxjs/toolkit"; -import { type ErrorContext } from "@umami/core"; import { errorsActions, useAppSelector } from "@umami/state"; +import { type ErrorContext } from "@umami/utils"; import { useDispatch } from "react-redux"; import { OutlineExclamationCircleIcon } from "../../assets/icons"; diff --git a/apps/embed-iframe/package.json b/apps/embed-iframe/package.json index 2d35344dc4..d9c0df9c2c 100644 --- a/apps/embed-iframe/package.json +++ b/apps/embed-iframe/package.json @@ -47,6 +47,7 @@ "devDependencies": { "@umami/eslint-config": "workspace:^", "@umami/typescript-config": "workspace:^", + "@umami/utils": "workspace:^", "@vitejs/plugin-react": "^4.3.4", "depcheck": "^1.4.7", "eslint": "^8.57.0", diff --git a/apps/embed-iframe/src/operationModalHooks.tsx b/apps/embed-iframe/src/operationModalHooks.tsx index 2cdf2d5a51..ac747e6a96 100644 --- a/apps/embed-iframe/src/operationModalHooks.tsx +++ b/apps/embed-iframe/src/operationModalHooks.tsx @@ -11,7 +11,8 @@ import { } from "./utils"; import { useOperationModalContext } from "./OperationModalContext"; import { ModalLoadingOverlay } from "./ModalLoadingOverlay"; -import { estimate, getErrorContext, toAccountOperations } from "@umami/core"; +import { estimate, toAccountOperations } from "@umami/core"; +import { getErrorContext } from "@umami/utils"; import { useEmbedApp } from "./EmbedAppContext"; export const useOperationModal = () => { diff --git a/apps/web/package.json b/apps/web/package.json index b7473aa0e2..6e7896fe18 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -50,6 +50,7 @@ "@umami/social-auth": "workspace:^", "@umami/state": "workspace:^", "@umami/tezos": "workspace:^", + "@umami/utils": "workspace:^", "@umami/tzkt": "workspace:^", "@walletconnect/jsonrpc-utils": "^1.0.8", "@walletconnect/types": "^2.16.2", diff --git a/apps/web/src/components/AddContactModal/AddContactModal.tsx b/apps/web/src/components/AddContactModal/AddContactModal.tsx index 4892937514..af835611b0 100644 --- a/apps/web/src/components/AddContactModal/AddContactModal.tsx +++ b/apps/web/src/components/AddContactModal/AddContactModal.tsx @@ -22,6 +22,7 @@ import { useValidateNewContactPkh, } from "@umami/state"; import { isValidContractPkh } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { type FC } from "react"; import { useForm } from "react-hook-form"; @@ -42,7 +43,7 @@ export const AddContactModal: FC<{ newContact.pkh, ]); if (!contractsWithNetworks.has(newContact.pkh)) { - throw new Error(`Network not found for contract ${newContact.pkh}`); + throw new CustomError(`Network not found for contract ${newContact.pkh}`); } dispatch( contactsActions.upsert({ diff --git a/apps/web/src/components/Menu/AddressBookMenu/EditContactMenu.tsx b/apps/web/src/components/Menu/AddressBookMenu/EditContactMenu.tsx index fe43c3d22b..9c7c611ae2 100644 --- a/apps/web/src/components/Menu/AddressBookMenu/EditContactMenu.tsx +++ b/apps/web/src/components/Menu/AddressBookMenu/EditContactMenu.tsx @@ -11,6 +11,7 @@ import { useValidateNewContactPkh, } from "@umami/state"; import { isValidContractPkh } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { type FC } from "react"; import { useForm } from "react-hook-form"; @@ -46,7 +47,7 @@ export const EditContactMenu: FC<{ network = contractsWithNetworks.get(newContact.pkh); if (!network) { - throw new Error(`Network not found for contract ${newContact.pkh}`); + throw new CustomError(`Network not found for contract ${newContact.pkh}`); } } diff --git a/apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.test.tsx b/apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.test.tsx index c25122081d..b1fd1ab522 100644 --- a/apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.test.tsx +++ b/apps/web/src/components/Menu/ErrorLogsMenu/ErrorLogsMenu.test.tsx @@ -1,5 +1,5 @@ -import { type ErrorContext } from "@umami/core"; import { type UmamiStore, errorsActions, makeStore } from "@umami/state"; +import { type ErrorContext } from "@umami/utils"; import { ErrorLogsMenu } from "./ErrorLogsMenu"; import { renderInDrawer, screen, userEvent } from "../../../testUtils"; diff --git a/apps/web/src/components/Onboarding/ImportWallet/SeedPhraseTab.tsx b/apps/web/src/components/Onboarding/ImportWallet/SeedPhraseTab.tsx index 0e478ee7ff..0fa66fee9a 100644 --- a/apps/web/src/components/Onboarding/ImportWallet/SeedPhraseTab.tsx +++ b/apps/web/src/components/Onboarding/ImportWallet/SeedPhraseTab.tsx @@ -16,6 +16,7 @@ import { } from "@chakra-ui/react"; import { useDynamicModalContext, useMultiForm } from "@umami/components"; import { useAsyncActionHandler } from "@umami/state"; +import { CustomError } from "@umami/utils"; import { validateMnemonic } from "bip39"; import { range } from "lodash"; import { useState } from "react"; @@ -74,7 +75,9 @@ export const SeedPhraseTab = () => { handleAsyncAction(async () => { const words = mnemonic.split(" "); if (!MNEMONIC_SIZE_OPTIONS.includes(words.length)) { - throw new Error(`the mnemonic must be ${MNEMONIC_SIZE_OPTIONS.join(", ")} words long`); + throw new CustomError( + `the mnemonic must be ${MNEMONIC_SIZE_OPTIONS.join(", ")} words long` + ); } words.slice(0, mnemonicSize).forEach((word, i) => update(i, { val: word })); return Promise.resolve(); @@ -83,7 +86,7 @@ export const SeedPhraseTab = () => { const onSubmit = ({ mnemonic }: FormValues) => handleAsyncAction(async () => { if (!validateMnemonic(mnemonic.map(({ val }) => val).join(" "))) { - throw new Error("Invalid Mnemonic"); + throw new CustomError("Invalid Mnemonic"); } return openWith(); }); diff --git a/apps/web/src/components/SendFlow/Beacon/BeaconSignPage.tsx b/apps/web/src/components/SendFlow/Beacon/BeaconSignPage.tsx index cfe8817ee3..8d4049d32f 100644 --- a/apps/web/src/components/SendFlow/Beacon/BeaconSignPage.tsx +++ b/apps/web/src/components/SendFlow/Beacon/BeaconSignPage.tsx @@ -1,3 +1,5 @@ +import { CustomError } from "@umami/utils"; + import { type BeaconSignPageProps } from "./BeaconSignPageProps"; import { ContractCallSignPage } from "./ContractCallSignPage"; import { DelegationSignPage } from "./DelegationSignPage"; @@ -40,6 +42,6 @@ export const BeaconSignPage = ({ operation, message }: BeaconSignPageProps) => { */ case "fa1.2": case "fa2": - throw new Error("Unsupported operation type"); + throw new CustomError("Unsupported operation type"); } }; diff --git a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx index e41f54f7c9..dfcb2ae58a 100644 --- a/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx +++ b/apps/web/src/components/WalletConnect/WalletConnectProvider.tsx @@ -12,6 +12,7 @@ import { walletKit, } from "@umami/state"; import { type Network } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils"; import { type SessionTypes } from "@walletconnect/types"; import { getSdkError } from "@walletconnect/utils"; @@ -47,7 +48,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { .filter(Boolean); if (requiredNetworks.length !== 1) { - throw new Error( + throw new CustomError( `Umami supports only one network per request, got required networks: ${requiredNetworks}` ); } @@ -55,7 +56,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { const availablenetworks = availableNetworks.map(network => network.name); // the network contains a namespace, e.g. tezos:mainnet if (!availablenetworks.includes(network.split(":")[1])) { - throw new Error( + throw new CustomError( `The requested required network "${network}" is not supported. Available: ${availablenetworks}` ); } @@ -91,7 +92,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { const activeSessions: Record = walletKit.getActiveSessions(); if (!(event.topic in activeSessions)) { console.error("WalletConnect session request failed. Session not found", event); - throw new Error("WalletConnect session request failed. Session not found"); + throw new CustomError("WalletConnect session request failed. Session not found"); } const session = activeSessions[event.topic]; @@ -100,7 +101,7 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => { description: `Session request from dApp ${session.peer.metadata.name}`, status: "info", }); - throw new Error("Not implemented"); + throw new CustomError("Not implemented"); } catch (error) { const { id, topic } = event; const activeSessions: Record = walletKit.getActiveSessions(); diff --git a/apps/web/src/components/beacon/useHandleBeaconMessage.tsx b/apps/web/src/components/beacon/useHandleBeaconMessage.tsx index be96589275..7820156381 100644 --- a/apps/web/src/components/beacon/useHandleBeaconMessage.tsx +++ b/apps/web/src/components/beacon/useHandleBeaconMessage.tsx @@ -14,6 +14,7 @@ import { useRemoveBeaconPeerBySenderId, } from "@umami/state"; import { type Network } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { PermissionRequestModal } from "./PermissionRequestModal"; import { SignPayloadRequestModal } from "./SignPayloadRequestModal"; @@ -49,7 +50,7 @@ export const useHandleBeaconMessage = () => { type: BeaconMessageType.Error, errorType: BeaconErrorType.NETWORK_NOT_SUPPORTED, }); - throw new Error( + throw new CustomError( `Got Beacon request from an unknown network: ${JSON.stringify( beaconNetwork )}. Please add it to the networks list and retry.` @@ -101,7 +102,7 @@ export const useHandleBeaconMessage = () => { type: BeaconMessageType.Error, errorType: BeaconErrorType.NO_PRIVATE_KEY_FOUND_ERROR, }); - throw new Error(`Unknown account: ${message.sourceAddress}`); + throw new CustomError(`Unknown account: ${message.sourceAddress}`); } const operation = toAccountOperations( message.operationDetails, @@ -132,7 +133,7 @@ export const useHandleBeaconMessage = () => { errorType: BeaconErrorType.UNKNOWN_ERROR, }); - throw new Error(`Unknown Beacon message type: ${message.type}`); + throw new CustomError(`Unknown Beacon message type: ${message.type}`); } } diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 5dec8affde..bc4d435242 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,6 +1,6 @@ import { DynamicDisclosureProvider } from "@umami/components"; -import { getErrorContext } from "@umami/core"; import { errorsActions } from "@umami/state"; +import { getErrorContext } from "@umami/utils"; import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { ErrorBoundary } from "react-error-boundary"; diff --git a/packages/core/package.json b/packages/core/package.json index 8df75dbf40..15fb47b4e2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,6 +70,7 @@ "@tzkt/sdk-api": "^2.2.1", "@umami/social-auth": "workspace:^", "@umami/tezos": "workspace:^", + "@umami/utils": "workspace:^", "@umami/tzkt": "workspace:^", "bignumber.js": "^9.1.2", "lodash": "^4.17.21" diff --git a/packages/core/src/AccountOperations.ts b/packages/core/src/AccountOperations.ts index 91081eaadc..775fbe62f3 100644 --- a/packages/core/src/AccountOperations.ts +++ b/packages/core/src/AccountOperations.ts @@ -3,6 +3,7 @@ import { BigNumber } from "bignumber.js"; import { type Account, type ImplicitAccount, type MultisigAccount } from "./Account"; import { type Operation } from "./Operation"; +import { CustomError } from "../../utils/src/ErrorContext"; type ProposalOperations = { type: "proposal"; @@ -43,7 +44,7 @@ export const makeAccountOperations = ( case "social": case "secret_key": if (sender.address.pkh !== signer.address.pkh) { - throw new Error("Sender and Signer must be the same"); + throw new CustomError("Sender and Signer must be the same"); } return { type: "implicit", diff --git a/packages/core/src/ErrorContext.test.ts b/packages/core/src/ErrorContext.test.ts deleted file mode 100644 index c6ea72ec4c..0000000000 --- a/packages/core/src/ErrorContext.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getErrorContext } from "./ErrorContext"; -import { handleTezError } from "./estimate"; - -jest.mock("./estimate", () => ({ - handleTezError: jest.fn(), -})); - -describe("getErrorContext", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("should handle error object with message and stack", () => { - jest.mocked(handleTezError).mockReturnValue(undefined); - const error = { - message: "some error message", - stack: "some stacktrace", - }; - - const context = getErrorContext(error); - - 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." - ); - expect(context.timestamp).toBeDefined(); - }); - - it("should handle string errors", () => { - jest.mocked(handleTezError).mockReturnValue(undefined); - const error = "string error message"; - - const context = getErrorContext(error); - - 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." - ); - expect(context.timestamp).toBeDefined(); - }); - - it("should handle Error instances with Tezos-specific errors", () => { - jest.mocked(handleTezError).mockReturnValue("Handled tez error message"); - const error = new Error("test error"); - - const context = getErrorContext(error); - - expect(context.technicalDetails).toBe("test error"); - expect(context.description).toBe("Handled tez error message"); - expect(context.stacktrace).toBeDefined(); - expect(context.timestamp).toBeDefined(); - }); -}); diff --git a/packages/core/src/ErrorContext.ts b/packages/core/src/ErrorContext.ts deleted file mode 100644 index f6a28298d0..0000000000 --- a/packages/core/src/ErrorContext.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { handleTezError } from "./estimate"; - -export type ErrorContext = { - timestamp: string; - description: string; - stacktrace: string; - technicalDetails: string; -}; - -export const getErrorContext = (error: any): ErrorContext => { - let description = - "Something went wrong. Please try again or contact support if the issue persists."; - let technicalDetails; - - let stacktrace = ""; - if (typeof error === "object" && "stack" in error) { - stacktrace = error.stack; - } - - if (error instanceof Error) { - description = handleTezError(error) ?? description; - } - - if (typeof error === "object" && "message" in error) { - technicalDetails = error.message; - } else if (typeof error === "string") { - technicalDetails = error; - } - - return { - timestamp: new Date().toISOString(), - description, - stacktrace, - technicalDetails, - }; -}; diff --git a/packages/core/src/Operation.ts b/packages/core/src/Operation.ts index 26b068d612..709221082c 100644 --- a/packages/core/src/Operation.ts +++ b/packages/core/src/Operation.ts @@ -3,6 +3,8 @@ import { MANAGER_LAMBDA } from "@taquito/taquito"; import { type Address, type ContractAddress, type ImplicitAddress } from "@umami/tezos"; import { isEqual } from "lodash"; +import { CustomError } from "../../utils/src/ErrorContext"; + export type TezTransfer = { type: "tez"; recipient: Address; @@ -139,7 +141,7 @@ export const toLambda = (operation: Operation): MichelsonV1Expression[] => { Number(operation.amount) ); default: - throw new Error(`${operation.recipient.type} is not supported yet`); + throw new CustomError(`${operation.recipient.type} is not supported yet`); } // eslint-disable-next-line no-fallthrough case "fa1.2": @@ -164,7 +166,7 @@ export const toLambda = (operation: Operation): MichelsonV1Expression[] => { case "stake": case "unstake": case "finalize_unstake": - throw new Error(`${operation.type} is not supported yet`); + throw new CustomError(`${operation.type} is not supported yet`); } }; diff --git a/packages/core/src/beaconUtils.ts b/packages/core/src/beaconUtils.ts index 917d9cecef..28510a45da 100644 --- a/packages/core/src/beaconUtils.ts +++ b/packages/core/src/beaconUtils.ts @@ -4,6 +4,7 @@ import { isValidImplicitPkh, parseImplicitPkh, parsePkh } from "@umami/tezos"; import { type ImplicitAccount } from "./Account"; import { type ImplicitOperations } from "./AccountOperations"; import { type ContractOrigination, type Operation } from "./Operation"; +import { CustomError } from "../../utils/src/ErrorContext"; /** * takes a list of {@link PartialTezosOperation} which come from Beacon @@ -18,7 +19,7 @@ export const toAccountOperations = ( signer: ImplicitAccount ): ImplicitOperations => { if (operationDetails.length === 0) { - throw new Error("Empty operation details!"); + throw new CustomError("Empty operation details!"); } const operations = operationDetails.map(operation => @@ -105,6 +106,6 @@ export const partialOperationToOperation = ( }; } default: - throw new Error(`Unsupported operation kind: ${partialOperation.kind}`); + throw new CustomError(`Unsupported operation kind: ${partialOperation.kind}`); } }; diff --git a/packages/core/src/decodeBeaconPayload.ts b/packages/core/src/decodeBeaconPayload.ts index 9dc620cb2f..a2e63f0827 100644 --- a/packages/core/src/decodeBeaconPayload.ts +++ b/packages/core/src/decodeBeaconPayload.ts @@ -3,6 +3,8 @@ import { CODEC, type ProtocolsHash, Uint8ArrayConsumer, getCodec } from "@taquit import { DefaultProtocol, unpackData } from "@taquito/michel-codec"; import { hex2buf } from "@taquito/utils"; +import { CustomError } from "../../utils/src/ErrorContext"; + /** * Decodes a sign request payload string. * @@ -43,12 +45,12 @@ export const decodeBeaconPayload = ( break; } default: { - throw new Error(`Unsupported signing type: ${signingType}`); + throw new CustomError(`Unsupported signing type: ${signingType}`); } } if (!isValidASCII(result)) { - throw new Error("Invalid payload. Only ASCII characters are supported."); + throw new CustomError("Invalid payload. Only ASCII characters are supported."); } return { result }; diff --git a/packages/core/src/estimate.test.ts b/packages/core/src/estimate.test.ts index e42f408f44..f42ef98f40 100644 --- a/packages/core/src/estimate.test.ts +++ b/packages/core/src/estimate.test.ts @@ -1,7 +1,7 @@ import { GHOSTNET, isAccountRevealed, makeToolkit } from "@umami/tezos"; import { makeAccountOperations } from "./AccountOperations"; -import { estimate, handleTezError } from "./estimate"; +import { estimate } from "./estimate"; import { mockImplicitAccount, mockTezOperation } from "./testUtils"; import { executeParams } from "../../test-utils/src/executeParams"; @@ -33,35 +33,6 @@ describe("estimate", () => { "Signer address is not revealed on the ghostnet." ); }); - - describe("handleTezError", () => { - it("catches subtraction_underflow", () => { - const res = handleTezError(new Error("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")); - 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")); - 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")); - expect(res).toBe("Emptying an implicit delegated account is not allowed."); - }); - - it("returns undefined for unknown errors", () => { - const err = new Error("unknown error"); - expect(handleTezError(err)).toBeUndefined(); - }); - }); }); it("returns estimated operations", async () => { diff --git a/packages/core/src/estimate.ts b/packages/core/src/estimate.ts index b21208958e..99df4f9d89 100644 --- a/packages/core/src/estimate.ts +++ b/packages/core/src/estimate.ts @@ -3,6 +3,7 @@ import { type Estimation, type Network, isAccountRevealed, makeToolkit } from "@ import { type AccountOperations, type EstimatedAccountOperations } from "./AccountOperations"; import { operationsToBatchParams } from "./helpers"; +import { CustomError } from "../../utils/src/ErrorContext"; /** * Estimates (and simulates the execution of) the operations. @@ -39,7 +40,7 @@ export const estimate = async ( const isRevealed = await isAccountRevealed(operations.signer.address.pkh, network); if (!isRevealed) { - throw new Error(`Signer address is not revealed on the ${network.name}.`); + throw new CustomError(`Signer address is not revealed on the ${network.name}.`); } throw err; @@ -55,16 +56,3 @@ const estimateToEstimation = (estimate: Estimate): Estimation => ({ // though totalCost doesn't work well with simple tez transfers and suggestedFeeMutez is more accurate fee: Math.max(estimate.suggestedFeeMutez, estimate.totalCost), }); - -// 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")) { - return "Insufficient balance, please make sure you have enough funds."; - } else if (err.message.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")) { - 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."; - } -}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2b22f31236..86c65c256f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,7 +3,6 @@ export * from "./AccountOperations"; export * from "./Contact"; export * from "./decodeBeaconPayload"; export * from "./Delegate"; -export * from "./ErrorContext"; export * from "./estimate"; export * from "./execute"; export * from "./helpers"; diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 6320f7f4b8..2d2ed49be8 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -23,6 +23,7 @@ "@umami/test-utils": "workspace:^", "@umami/typescript-config": "workspace:^", "babel-jest": "^29.7.0", + "@umami/utils": "workspace:^", "depcheck": "^1.4.7", "eslint": "^8.57.0", "jest": "^29.7.0", diff --git a/packages/crypto/src/AES.ts b/packages/crypto/src/AES.ts index 20eedcce98..ca6e438fe9 100644 --- a/packages/crypto/src/AES.ts +++ b/packages/crypto/src/AES.ts @@ -1,10 +1,10 @@ import { buf2hex, hex2Bytes } from "@taquito/utils"; +import { CustomError } from "@umami/utils"; import { differenceInMinutes } from "date-fns"; import { AES_MODE } from "./AES_MODE"; import { derivePasswordBasedKeyV1, derivePasswordBasedKeyV2 } from "./KDF"; import { type EncryptedData } from "./types"; - // NIST recommends a salt size of at least 128 bits (16 bytes) // https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf const SALT_SIZE = 32; @@ -50,7 +50,7 @@ export const decrypt = async ( new Date(localStorage.getItem("failedDecryptTime")!) ); if (minutesSinceLastAttempt < 5) { - throw new Error(TOO_MANY_ATTEMPTS_ERROR); + throw new CustomError(TOO_MANY_ATTEMPTS_ERROR); } } const derivedKey = @@ -74,7 +74,7 @@ export const decrypt = async ( } setAttemptsCount(getAttemptsCount() + 1); localStorage.setItem("failedDecryptTime", new Date().toISOString()); - throw new Error("Error decrypting data: Invalid password"); + throw new CustomError("Error decrypting data: Invalid password"); } }; diff --git a/packages/data-polling/package.json b/packages/data-polling/package.json index 79bdce35e4..4e6ba6295a 100644 --- a/packages/data-polling/package.json +++ b/packages/data-polling/package.json @@ -24,6 +24,7 @@ "@types/react": "18.3.12", "@types/react-dom": "18.3.1", "@umami/eslint-config": "workspace:^", + "@umami/utils": "workspace:^", "@umami/jest-config": "workspace:^", "@umami/typescript-config": "workspace:^", "babel-jest": "^29.7.0", diff --git a/packages/data-polling/src/useReactQueryErrorHandler.ts b/packages/data-polling/src/useReactQueryErrorHandler.ts index 75711227c0..05351c558b 100644 --- a/packages/data-polling/src/useReactQueryErrorHandler.ts +++ b/packages/data-polling/src/useReactQueryErrorHandler.ts @@ -1,6 +1,6 @@ import { useToast } from "@chakra-ui/react"; -import { getErrorContext } from "@umami/core"; import { errorsActions, useAppDispatch } from "@umami/state"; +import { getErrorContext } from "@umami/utils"; import { useCallback } from "react"; export const useReactQueryErrorHandler = () => { diff --git a/packages/multisig/package.json b/packages/multisig/package.json index 446d3d143d..54872f9534 100644 --- a/packages/multisig/package.json +++ b/packages/multisig/package.json @@ -19,6 +19,7 @@ "@types/eslint": "^8", "@types/jest": "^29.5.14", "@types/lodash": "^4", + "@umami/utils": "workspace:^", "@umami/core": "workspace:^", "@umami/eslint-config": "workspace:^", "@umami/jest-config": "workspace:^", diff --git a/packages/multisig/src/helpers.ts b/packages/multisig/src/helpers.ts index afd4a7a3db..0e8d32a6a2 100644 --- a/packages/multisig/src/helpers.ts +++ b/packages/multisig/src/helpers.ts @@ -5,6 +5,7 @@ import { parseContractPkh, parseImplicitPkh, } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { compact, every } from "lodash"; import { @@ -75,7 +76,7 @@ export const getNetworksForContracts = async ( const parseMultisigOperation = (raw: RawTzktMultisigBigMap): MultisigOperation => { const { bigmap, key, value } = raw; if (key === null || value === null) { - throw new Error("parseMultisigOperation failed"); + throw new CustomError("parseMultisigOperation failed"); } return { diff --git a/packages/state/package.json b/packages/state/package.json index 8ada31cf4c..032e65611f 100644 --- a/packages/state/package.json +++ b/packages/state/package.json @@ -24,6 +24,7 @@ "@types/react-dom": "18.3.1", "@umami/eslint-config": "workspace:^", "@umami/jest-config": "workspace:^", + "@umami/utils": "workspace:^", "@umami/test-utils": "workspace:^", "@umami/typescript-config": "workspace:^", "babel-jest": "^29.7.0", diff --git a/packages/state/src/hooks/backup.ts b/packages/state/src/hooks/backup.ts index d0eec8542a..5b7b77e20b 100644 --- a/packages/state/src/hooks/backup.ts +++ b/packages/state/src/hooks/backup.ts @@ -1,5 +1,6 @@ import { DEFAULT_ACCOUNT_LABEL } from "@umami/core"; import { type EncryptedData, decrypt, encrypt } from "@umami/crypto"; +import { CustomError } from "@umami/utils"; import { type Persistor } from "redux-persist"; import { useValidateMasterPassword } from "./getAccountData"; @@ -24,7 +25,7 @@ export const useRestoreBackup = () => { if (isV21Backup(backup)) { return restoreV21BackupFile(backup, password, persistor); } - throw new Error("Invalid backup file."); + throw new CustomError("Invalid backup file."); }; }; @@ -65,7 +66,7 @@ export const restoreV2BackupFile = async ( ) => { const accountsInString: string = backup["persist:accounts"]; if (!accountsInString) { - throw new Error("Invalid backup file."); + throw new CustomError("Invalid backup file."); } const accounts: { seedPhrases: string } = JSON.parse(accountsInString); diff --git a/packages/state/src/hooks/getAccountData.ts b/packages/state/src/hooks/getAccountData.ts index 899fde7f67..722386a0d8 100644 --- a/packages/state/src/hooks/getAccountData.ts +++ b/packages/state/src/hooks/getAccountData.ts @@ -7,6 +7,7 @@ import { } from "@umami/core"; import { decrypt } from "@umami/crypto"; import { type RawPkh, deriveSecretKey } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { maxBy } from "lodash"; import { useEffect } from "react"; import { useDispatch } from "react-redux"; @@ -35,7 +36,7 @@ export const useGetImplicitAccount = () => { return (pkh: RawPkh) => { const account = getAccount(pkh); if (!account) { - throw new Error(`Unknown account: ${pkh}`); + throw new CustomError(`Unknown account: ${pkh}`); } return account; }; @@ -76,7 +77,7 @@ export const useGetOwnedAccount = () => { return (pkh: string): Account => { const account = getOwnedAccount(pkh); if (!account) { - throw new Error(`Unknown account: ${pkh}`); + throw new CustomError(`Unknown account: ${pkh}`); } return account; }; @@ -172,7 +173,7 @@ export const useGetSecretKey = () => { if (account.type === "secret_key") { const encryptedSecretKey = encryptedSecretKeys[account.address.pkh]; if (!encryptedSecretKey) { - throw new Error(`Missing secret key for account ${account.address.pkh}`); + throw new CustomError(`Missing secret key for account ${account.address.pkh}`); } return decrypt(encryptedSecretKey, password); @@ -208,7 +209,7 @@ export const useGetDecryptedMnemonic = () => { const encryptedMnemonic = seedPhrases[account.seedFingerPrint]; if (!encryptedMnemonic) { - throw new Error(`Missing seedphrase for account ${account.address.pkh}`); + throw new CustomError(`Missing seedphrase for account ${account.address.pkh}`); } return decrypt(encryptedMnemonic, password); diff --git a/packages/state/src/hooks/setAccountData.ts b/packages/state/src/hooks/setAccountData.ts index f8d69d6fa6..f30e8c3878 100644 --- a/packages/state/src/hooks/setAccountData.ts +++ b/packages/state/src/hooks/setAccountData.ts @@ -9,6 +9,7 @@ import { import { decrypt, encrypt } from "@umami/crypto"; import { type IDP } from "@umami/social-auth"; import { derivePublicKeyPair, makeDerivationPath } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { useCallback } from "react"; import { useDispatch } from "react-redux"; @@ -113,7 +114,7 @@ export const useDeriveMnemonicAccount = () => { }) => { const encryptedSeedphrase = encryptedMnemonics[fingerPrint]; if (!encryptedSeedphrase) { - throw new Error(`No seedphrase found with fingerprint: ${fingerPrint}`); + throw new CustomError(`No seedphrase found with fingerprint: ${fingerPrint}`); } const seedphrase = await decrypt(encryptedSeedphrase, password); diff --git a/packages/state/src/hooks/useAsyncActionHandler.ts b/packages/state/src/hooks/useAsyncActionHandler.ts index e0381dc5ab..f6e8f4584d 100644 --- a/packages/state/src/hooks/useAsyncActionHandler.ts +++ b/packages/state/src/hooks/useAsyncActionHandler.ts @@ -1,5 +1,5 @@ import { type UseToastOptions, useToast } from "@chakra-ui/react"; -import { getErrorContext } from "@umami/core"; +import { getErrorContext } from "@umami/utils"; import { useCallback, useRef, useState } from "react"; import { useAppDispatch } from "./useAppDispatch"; @@ -38,6 +38,7 @@ export const useAsyncActionHandler = () => { try { return await fn(); } catch (error: any) { + console.log("error", error); const errorContext = getErrorContext(error); toast({ diff --git a/packages/state/src/slices/accounts/accounts.ts b/packages/state/src/slices/accounts/accounts.ts index 7445f68f6e..91bfb72354 100644 --- a/packages/state/src/slices/accounts/accounts.ts +++ b/packages/state/src/slices/accounts/accounts.ts @@ -9,6 +9,7 @@ import { } from "@umami/core"; import { type EncryptedData } from "@umami/crypto"; import { type RawPkh } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { remove } from "lodash"; import { type AccountsState } from "./State"; @@ -81,10 +82,10 @@ export const accountsSlice = createSlice({ ) => { const { account, newName } = payload; if (newName.length === 0) { - throw new Error("Cannot rename account to an empty name."); + throw new CustomError("Cannot rename account to an empty name."); } if (state.items.find(a => a.label === newName)) { - throw new Error( + throw new CustomError( `Cannot rename account ${account.address.pkh} to ${newName} since the name already exists.` ); } @@ -153,7 +154,7 @@ const concatUnique = (existingAccounts: ImplicitAccount[], newAccounts: Implicit existingAccount => existingAccount.address.pkh === newAccount.address.pkh ) ) { - throw new Error( + throw new CustomError( `Can't add account with address ${newAccount.address.pkh} because it already exists.` ); } diff --git a/packages/state/src/slices/errors.ts b/packages/state/src/slices/errors.ts index 4f548bcfc4..3bd58e2d55 100644 --- a/packages/state/src/slices/errors.ts +++ b/packages/state/src/slices/errors.ts @@ -1,5 +1,5 @@ import { createSlice } from "@reduxjs/toolkit"; -import { type ErrorContext } from "@umami/core"; +import { type ErrorContext } from "@umami/utils"; type State = ErrorContext[]; diff --git a/packages/state/src/thunks/changeMnemonicPassword.ts b/packages/state/src/thunks/changeMnemonicPassword.ts index f5ff6117ee..6496924906 100644 --- a/packages/state/src/thunks/changeMnemonicPassword.ts +++ b/packages/state/src/thunks/changeMnemonicPassword.ts @@ -1,5 +1,6 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import { type EncryptedData, decrypt, encrypt } from "@umami/crypto"; +import { CustomError } from "@umami/utils"; import { fromPairs } from "lodash"; import { type AccountsState } from "../slices/accounts/State"; @@ -15,19 +16,19 @@ export const changeMnemonicPassword = createAsyncThunk< { state: { accounts: AccountsState } } >("accounts/changeMnemonicPassword", async ({ currentPassword, newPassword }, { getState }) => { if (currentPassword === newPassword) { - throw new Error("New password must be different from the current password"); + throw new CustomError("New password must be different from the current password"); } const { items: accounts, seedPhrases } = getState().accounts; if (accounts.filter(account => account.type === "mnemonic").length === 0) { - throw new Error("No mnemonic accounts found"); + throw new CustomError("No mnemonic accounts found"); } const newEncryptedMnemonics = await Promise.all( Object.entries(seedPhrases).map(async ([fingerprint, currentEncryptedMnemonic]) => { if (!currentEncryptedMnemonic) { - throw new Error("No encrypted mnemonic found"); + throw new CustomError("No encrypted mnemonic found"); } try { // Re-encrypt mnemonic with new password @@ -36,7 +37,7 @@ export const changeMnemonicPassword = createAsyncThunk< return [fingerprint, newEncryptedMnemonic]; } catch (err: any) { - throw new Error(err.message); + throw new CustomError(err.message); } }) ); diff --git a/packages/state/src/thunks/secretKeyAccount.ts b/packages/state/src/thunks/secretKeyAccount.ts index a1ac7774f8..c4997ea5fa 100644 --- a/packages/state/src/thunks/secretKeyAccount.ts +++ b/packages/state/src/thunks/secretKeyAccount.ts @@ -2,6 +2,7 @@ import { type Curves } from "@taquito/signer"; import { Prefix } from "@taquito/utils"; import { encrypt } from "@umami/crypto"; import { getPublicKeyPairFromSk, parseImplicitPkh } from "@umami/tezos"; +import { CustomError } from "@umami/utils"; import { accountsActions } from "../slices/accounts"; import { type AppDispatch } from "../store"; @@ -16,7 +17,7 @@ export const getCurve = (secretKey: string): Curves => { if (secretKey.startsWith(Prefix.P2ESK) || secretKey.startsWith(Prefix.P2SK)) { return "p256"; } - throw new Error("Invalid secret key"); + throw new CustomError("Invalid secret key"); }; export const isEncryptedSecretKeyPrefix = (secretKeyPrefix: string) => diff --git a/packages/tezos/package.json b/packages/tezos/package.json index 561acfb599..7c9cb7a801 100644 --- a/packages/tezos/package.json +++ b/packages/tezos/package.json @@ -59,6 +59,7 @@ ] }, "dependencies": { + "@umami/utils": "workspace:^", "@ledgerhq/hw-transport-webusb": "^6.29.4", "@taquito/ledger-signer": "^20.1.0", "@taquito/michel-codec": "^20.1.0", diff --git a/packages/tezos/src/Address.ts b/packages/tezos/src/Address.ts index aa12381adb..444f620408 100644 --- a/packages/tezos/src/Address.ts +++ b/packages/tezos/src/Address.ts @@ -1,4 +1,5 @@ import { ValidationResult, validateAddress } from "@taquito/utils"; +import { CustomError } from "@umami/utils"; import { type Address, @@ -17,7 +18,7 @@ export const parsePkh = (pkh: string): Address => { if (isValidSmartRollupPkh(pkh)) { return parseSmartRollupPkh(pkh); } - throw new Error(`Cannot parse address type: ${pkh}`); + throw new CustomError(`Cannot parse address type: ${pkh}`); }; export const isAddressValid = (pkh: string) => validateAddress(pkh) === ValidationResult.VALID; @@ -33,19 +34,19 @@ export const parseContractPkh = (pkh: string): ContractAddress => { if (isValidContractPkh(pkh)) { return { type: "contract", pkh }; } - throw new Error(`Invalid contract address: ${pkh}`); + throw new CustomError(`Invalid contract address: ${pkh}`); }; export const parseImplicitPkh = (pkh: string): ImplicitAddress => { if (isValidImplicitPkh(pkh)) { return { type: "implicit", pkh }; } - throw new Error(`Invalid implicit address: ${pkh}`); + throw new CustomError(`Invalid implicit address: ${pkh}`); }; export const parseSmartRollupPkh = (pkh: string): SmartRollupAddress => { if (isValidSmartRollupPkh(pkh)) { return { type: "smart_rollup", pkh }; } - throw new Error(`Invalid smart rollup address: ${pkh}`); + throw new CustomError(`Invalid smart rollup address: ${pkh}`); }; diff --git a/packages/tezos/src/helpers.ts b/packages/tezos/src/helpers.ts index f9d5a1cbcc..ae873ab577 100644 --- a/packages/tezos/src/helpers.ts +++ b/packages/tezos/src/helpers.ts @@ -3,6 +3,7 @@ import { DerivationType, LedgerSigner } from "@taquito/ledger-signer"; import { Parser } from "@taquito/michel-codec"; import { type Curves, InMemorySigner } from "@taquito/signer"; import { TezosToolkit } from "@taquito/taquito"; +import { CustomError } from "@umami/utils"; import { FakeSigner } from "./fakeSigner"; import { type PublicKeyPair, type SignerConfig } from "./types"; @@ -27,7 +28,7 @@ export const curveToDerivationType = (curve: Curves): DerivationType => { case "p256": return DerivationType.P256; case "bip25519": - throw new Error("bip25519 is not supported in Tezos"); + throw new CustomError("bip25519 is not supported in Tezos"); } }; @@ -122,11 +123,11 @@ export const decryptSecretKey = async (secretKey: string, password: string) => { // if the password doesn't match taquito throws this error if (message.includes("Cannot read properties of null")) { - throw new Error("Key-password pair is invalid"); + throw new CustomError("Key-password pair is invalid"); } if (message.includes("Invalid checksum")) { - throw new Error("Invalid secret key: checksum doesn't match"); + throw new CustomError("Invalid secret key: checksum doesn't match"); } throw error; diff --git a/packages/tzkt/package.json b/packages/tzkt/package.json index 086d3d59be..730fbbded0 100644 --- a/packages/tzkt/package.json +++ b/packages/tzkt/package.json @@ -26,6 +26,7 @@ "@types/babel__core": "^7.20.5", "@types/eslint": "^8", "@types/jest": "^29.5.14", + "@umami/utils": "workspace:^", "@types/lodash": "^4", "@types/promise-retry": "^1.1.6", "@umami/eslint-config": "workspace:^", diff --git a/packages/tzkt/src/withRateLimit.test.ts b/packages/tzkt/src/withRateLimit.test.ts index 4b4601d9a7..c31f889725 100644 --- a/packages/tzkt/src/withRateLimit.test.ts +++ b/packages/tzkt/src/withRateLimit.test.ts @@ -1,3 +1,5 @@ +import { CustomError } from "@umami/utils"; + import { withRateLimit } from "./withRateLimit"; class HTTPErrorMock extends Error { @@ -17,7 +19,7 @@ describe("withRateLimit", () => { const fn = async () => { counter++; if (counter < 4) { - throw new Error("Some error"); + throw new CustomError("Some error"); } return Promise.resolve("success"); }; @@ -30,7 +32,7 @@ describe("withRateLimit", () => { const fn = async () => { counter++; if (counter < 5) { - throw new Error("Some error"); + throw new CustomError("Some error"); } return Promise.resolve("success"); }; diff --git a/packages/tzkt/src/withRateLimit.ts b/packages/tzkt/src/withRateLimit.ts index 4c31d35b68..8e3120d75d 100644 --- a/packages/tzkt/src/withRateLimit.ts +++ b/packages/tzkt/src/withRateLimit.ts @@ -1,4 +1,5 @@ import Semaphore from "@chriscdn/promise-semaphore"; +import { CustomError } from "@umami/utils"; import promiseRetry from "promise-retry"; const tzktRateLimiter = new Semaphore(10); @@ -11,7 +12,9 @@ export const withRateLimit = (fn: () => Promise) => // tzkt throws HttpError, but doesn't export it // default behaviour just shows Error: 504 which isn't very helpful for the user if ("status" in error && "data" in error) { - throw new Error(`Fetching data from tzkt failed with: ${error.status}, ${error.data}`); + throw new CustomError( + `Fetching data from tzkt failed with: ${error.status}, ${error.data}` + ); } throw error; }) diff --git a/packages/utils/.depcheckrc b/packages/utils/.depcheckrc new file mode 100644 index 0000000000..5236e62cbf --- /dev/null +++ b/packages/utils/.depcheckrc @@ -0,0 +1,2 @@ +ignores: ["@types/*", "rimraf", "depcheck", "madge"] +quiet: true diff --git a/packages/utils/.eslintrc.cjs b/packages/utils/.eslintrc.cjs new file mode 100644 index 0000000000..effe41d41f --- /dev/null +++ b/packages/utils/.eslintrc.cjs @@ -0,0 +1,9 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ["@umami/eslint-config"], + parserOptions: { + project: "tsconfig.json", + parser: "@typescript-eslint/parser", + tsconfigRootDir: __dirname, + }, +}; diff --git a/packages/utils/babel.config.json b/packages/utils/babel.config.json new file mode 100644 index 0000000000..ac08da0a4a --- /dev/null +++ b/packages/utils/babel.config.json @@ -0,0 +1,3 @@ +{ + "extends": "../../babel.config.json" +} diff --git a/packages/utils/jest.config.ts b/packages/utils/jest.config.ts new file mode 100644 index 0000000000..a9c8609dd4 --- /dev/null +++ b/packages/utils/jest.config.ts @@ -0,0 +1,9 @@ +import baseConfig from "@umami/jest-config"; +import type { Config } from "jest"; + +const config: Config = { + ...baseConfig, + testEnvironment: "node", + rootDir: "./", +}; +export default config; diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000000..b83a2226c1 --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,65 @@ +{ + "name": "@umami/utils", + "packageManager": "pnpm@9.9.0", + "type": "module", + "module": "./dist/index.js", + "main": "./dist/index.cjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "@umami/source": "./src/index.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "devDependencies": { + "@babel/core": "^7.26.0", + "@types/babel__core": "^7.20.5", + "@types/eslint": "^8", + "@types/jest": "^29.5.14", + "@types/lodash": "^4", + "@umami/eslint-config": "workspace:^", + "@umami/jest-config": "workspace:^", + "@umami/test-utils": "workspace:^", + "@umami/typescript-config": "workspace:^", + "babel-jest": "^29.7.0", + "depcheck": "^1.4.7", + "eslint": "^8.57.0", + "jest": "^29.7.0", + "madge": "^8.0.0", + "prettier": "^3.3.3", + "rimraf": "^6.0.1", + "tsup": "^8.3.5", + "typescript": "^5.7.2" + }, + "scripts": { + "build": "tsup-node --dts", + "build:quick": "tsup-node", + "clean": "rimraf build dist .turbo", + "check-circular-deps": "madge --circular src/index.ts", + "check-types:watch": "pnpm check-types --watch", + "check-types": "tsc", + "dev": "tsup-node --watch", + "format:ci": "prettier --ignore-path ../../.gitignore --check .", + "format": "prettier --ignore-path ../../.gitignore --write .", + "lint:ci": "eslint src --ext .ts --max-warnings=0", + "lint": "eslint src --ext .ts --fix", + "test:watch": "jest --watch", + "test": "jest" + }, + "tsup": { + "entry": [ + "src/index.ts" + ], + "clean": true, + "format": [ + "cjs", + "esm" + ] + }, + "dependencies": { + "bignumber.js": "^9.1.2", + "lodash": "^4.17.21" + } +} diff --git a/packages/utils/src/ErrorContext.test.ts b/packages/utils/src/ErrorContext.test.ts new file mode 100644 index 0000000000..293ac0c266 --- /dev/null +++ b/packages/utils/src/ErrorContext.test.ts @@ -0,0 +1,83 @@ +import { CustomError, getErrorContext, handleTezError } from "./ErrorContext"; + +describe("getErrorContext", () => { + it("should handle error object with message and stack", () => { + const error = { + message: "some error message", + stack: "some stacktrace", + }; + + const context = getErrorContext(error); + + 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." + ); + expect(context.timestamp).toBeDefined(); + }); + + it("should handle string errors", () => { + const error = "string error message"; + + const context = getErrorContext(error); + + 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." + ); + expect(context.timestamp).toBeDefined(); + }); + + it("should handle Error instances with Tezos-specific errors", () => { + const error = new Error("subtraction_underflow"); + + const context = getErrorContext(error); + + expect(context.technicalDetails).toBe("subtraction_underflow"); + expect(context.description).toBe( + "Insufficient balance, please make sure you have enough funds." + ); + expect(context.stacktrace).toBeDefined(); + expect(context.timestamp).toBeDefined(); + }); + + it("should handle CustomError instances", () => { + const error = new CustomError("Custom error message"); + + const context = getErrorContext(error); + + expect(context.technicalDetails).toBe(""); + expect(context.description).toBe("Custom error message"); + expect(context.stacktrace).toBeDefined(); + expect(context.timestamp).toBeDefined(); + }); +}); + +describe("handleTezError", () => { + it("catches subtraction_underflow", () => { + const res = handleTezError(new Error("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")); + 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")); + 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")); + expect(res).toBe("Emptying an implicit delegated account is not allowed."); + }); + + 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 new file mode 100644 index 0000000000..ad5d79786a --- /dev/null +++ b/packages/utils/src/ErrorContext.ts @@ -0,0 +1,57 @@ +export type ErrorContext = { + timestamp: string; + description: string; + stacktrace: string; + technicalDetails: string; +}; + +export class CustomError extends Error { + constructor(message: string) { + super(message); + this.name = "CustomError"; + } +} + +// 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")) { + return "Insufficient balance, please make sure you have enough funds."; + } else if (err.message.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")) { + 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."; + } +}; + +export const getErrorContext = (error: any): ErrorContext => { + let description = + "Something went wrong. Please try again or contact support if the issue persists."; + let technicalDetails; + + 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.name === "CustomError") { + description = error.message; + technicalDetails = ""; + } else if (error instanceof Error) { + description = handleTezError(error) ?? description; + } + + return { + timestamp: new Date().toISOString(), + description, + stacktrace, + technicalDetails, + }; +}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 0000000000..38cf1d1381 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1 @@ +export * from "./ErrorContext"; diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000000..42161d97f7 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@umami/typescript-config/tsconfig.json", + "include": ["src", "jest.config.ts", ".eslintrc.cjs"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be9eb78a43..3ab87045b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -214,6 +214,9 @@ importers: '@umami/tzkt': specifier: workspace:^ version: link:../../packages/tzkt + '@umami/utils': + specifier: workspace:^ + version: link:../../packages/utils '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.3.4(vite@5.4.11(@types/node@22.10.1)(lightningcss@1.22.0)(sass@1.80.7)(terser@5.36.0)) @@ -412,6 +415,9 @@ importers: '@umami/tzkt': specifier: workspace:^ version: link:../../packages/tzkt + '@umami/utils': + specifier: workspace:^ + version: link:../../packages/utils date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -558,6 +564,9 @@ importers: '@umami/typescript-config': specifier: workspace:^ version: link:../../packages/typescript-config + '@umami/utils': + specifier: workspace:^ + version: link:../../packages/utils '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.3.4(vite@5.4.11(@types/node@22.10.1)(lightningcss@1.22.0)(sass@1.80.7)(terser@5.36.0)) @@ -878,6 +887,9 @@ importers: '@umami/tzkt': specifier: workspace:^ version: link:../../packages/tzkt + '@umami/utils': + specifier: workspace:^ + version: link:../../packages/utils '@walletconnect/jsonrpc-utils': specifier: ^1.0.8 version: 1.0.8 @@ -1291,6 +1303,9 @@ importers: '@umami/tzkt': specifier: workspace:^ version: link:../tzkt + '@umami/utils': + specifier: workspace:^ + version: link:../utils bignumber.js: specifier: ^9.1.2 version: 9.1.2 @@ -1386,6 +1401,9 @@ importers: '@umami/typescript-config': specifier: workspace:^ version: link:../typescript-config + '@umami/utils': + specifier: workspace:^ + version: link:../utils babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.26.0) @@ -1501,6 +1519,9 @@ importers: '@umami/typescript-config': specifier: workspace:^ version: link:../typescript-config + '@umami/utils': + specifier: workspace:^ + version: link:../utils babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.26.0) @@ -1658,6 +1679,9 @@ importers: '@umami/typescript-config': specifier: workspace:^ version: link:../typescript-config + '@umami/utils': + specifier: workspace:^ + version: link:../utils babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.26.0) @@ -1876,6 +1900,9 @@ importers: '@umami/typescript-config': specifier: workspace:^ version: link:../typescript-config + '@umami/utils': + specifier: workspace:^ + version: link:../utils babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.26.0) @@ -1994,6 +2021,9 @@ importers: '@taquito/utils': specifier: ^20.1.0 version: 20.1.0 + '@umami/utils': + specifier: workspace:^ + version: link:../utils bignumber.js: specifier: ^9.1.2 version: 9.1.2 @@ -2103,6 +2133,73 @@ importers: '@umami/typescript-config': specifier: workspace:^ version: link:../typescript-config + '@umami/utils': + specifier: workspace:^ + version: link:../utils + babel-jest: + specifier: ^29.7.0 + version: 29.7.0(@babel/core@7.26.0) + depcheck: + specifier: ^1.4.7 + version: 1.4.7 + eslint: + specifier: ^8.57.0 + version: 8.57.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.10.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.36(@swc/helpers@0.5.13))(@types/node@22.10.1)(typescript@5.7.2)) + madge: + specifier: ^8.0.0 + version: 8.0.0(typescript@5.7.2) + prettier: + specifier: ^3.3.3 + version: 3.3.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + tsup: + specifier: ^8.3.5 + version: 8.3.5(@swc/core@1.7.36(@swc/helpers@0.5.13))(jiti@1.21.6)(postcss@8.4.47)(typescript@5.7.2)(yaml@2.5.1) + typescript: + specifier: ^5.7.2 + version: 5.7.2 + + packages/utils: + dependencies: + bignumber.js: + specifier: ^9.1.2 + version: 9.1.2 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + devDependencies: + '@babel/core': + specifier: ^7.26.0 + version: 7.26.0 + '@types/babel__core': + specifier: ^7.20.5 + version: 7.20.5 + '@types/eslint': + specifier: ^8 + version: 8.56.11 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/lodash': + specifier: ^4 + version: 4.17.7 + '@umami/eslint-config': + specifier: workspace:^ + version: link:../eslint-config + '@umami/jest-config': + specifier: workspace:^ + version: link:../jest-config + '@umami/test-utils': + specifier: workspace:^ + version: link:../test-utils + '@umami/typescript-config': + specifier: workspace:^ + version: link:../typescript-config babel-jest: specifier: ^29.7.0 version: 29.7.0(@babel/core@7.26.0)