From daabf50c01bf3c8ea46287b43c922da3d65987da Mon Sep 17 00:00:00 2001 From: Chid Gilovitz Date: Tue, 7 Nov 2023 13:25:23 +0800 Subject: [PATCH 1/8] fix: add missing analytics call in onboarding success page (#1141) --- apps/extension/src/ui/apps/onboard/routes/Success.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/extension/src/ui/apps/onboard/routes/Success.tsx b/apps/extension/src/ui/apps/onboard/routes/Success.tsx index 485889445d..6365ef54f7 100644 --- a/apps/extension/src/ui/apps/onboard/routes/Success.tsx +++ b/apps/extension/src/ui/apps/onboard/routes/Success.tsx @@ -1,5 +1,6 @@ import imgHandOrb from "@talisman/theme/images/onboard_hand_orb.png" import { AnalyticsPage } from "@ui/api/analytics" +import { useAnalyticsPageView } from "@ui/hooks/useAnalyticsPageView" import { Button } from "talisman-ui" import { useOnboard } from "../context" @@ -13,6 +14,7 @@ const SUCCESS_PAGE: AnalyticsPage = { } export const SuccessPage = () => { + useAnalyticsPageView(SUCCESS_PAGE) const { completeOnboarding } = useOnboard() return ( From a1fc0eaa598a2fdf1d5dad5ab7c9b3cbf71dbc66 Mon Sep 17 00:00:00 2001 From: Kheops <26880866+0xKheops@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:52:56 +0900 Subject: [PATCH 2/8] fix: disable google translate for mnemonics (#1144) --- .../src/ui/domains/Account/Mnemonic.tsx | 66 ------------------- .../src/ui/domains/Mnemonic/Mnemonic.tsx | 2 +- 2 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 apps/extension/src/ui/domains/Account/Mnemonic.tsx diff --git a/apps/extension/src/ui/domains/Account/Mnemonic.tsx b/apps/extension/src/ui/domains/Account/Mnemonic.tsx deleted file mode 100644 index a43db19fb1..0000000000 --- a/apps/extension/src/ui/domains/Account/Mnemonic.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { notify } from "@talisman/components/Notifications" -import { CopyIcon } from "@talismn/icons" -import { classNames } from "@talismn/util" -import { MouseEventHandler, useCallback, useState } from "react" -import { useTranslation } from "react-i18next" - -type MnemonicProps = { - onMouseEnter?: MouseEventHandler - mnemonic: string -} - -export const Mnemonic = ({ onMouseEnter, mnemonic }: MnemonicProps) => { - const { t } = useTranslation() - const [hasHovered, setHasHovered] = useState(false) - - const handleCopy = useCallback(async () => { - try { - await window.navigator.clipboard.writeText(mnemonic) - notify({ - title: t("Copied to clipboard"), - type: "success", - }) - } catch (err) { - notify({ - title: t("Failed to copy"), - type: "error", - }) - } - }, [mnemonic, t]) - - return ( - <> -
- -
- -
{ - setHasHovered(true) - onMouseEnter && onMouseEnter(e) - }} - > -
- {mnemonic.split(" ").map((word, i) => ( - - {word} - - ))} -
-
-
-
-
- - ) -} diff --git a/apps/extension/src/ui/domains/Mnemonic/Mnemonic.tsx b/apps/extension/src/ui/domains/Mnemonic/Mnemonic.tsx index 72451bbf0a..67e8bc961b 100644 --- a/apps/extension/src/ui/domains/Mnemonic/Mnemonic.tsx +++ b/apps/extension/src/ui/domains/Mnemonic/Mnemonic.tsx @@ -75,7 +75,7 @@ export const Mnemonic: FC = ({ onReveal, mnemonic, topRight }) => mnemonic.split(" ").map((word, i) => ( {i + 1}. - {word} + {word} ))} - diff --git a/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Transaction.tsx b/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Transaction.tsx index 4963ef7729..bdad2cd6c9 100644 --- a/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Transaction.tsx +++ b/apps/extension/src/ui/apps/popup/pages/Sign/ethereum/Transaction.tsx @@ -1,6 +1,8 @@ +import { EvmAddress } from "@core/domains/ethereum/types" import { EthPriorityOptionName } from "@core/domains/signing/types" import { AppPill } from "@talisman/components/AppPill" import { WithTooltip } from "@talisman/components/Tooltip" +import { EvmNetworkId, TokenId } from "@talismn/chaindata-provider" import { InfoIcon } from "@talismn/icons" import { PopupContent, @@ -11,7 +13,7 @@ import { import { TokensAndFiat } from "@ui/domains/Asset/TokensAndFiat" import { EthFeeSelect } from "@ui/domains/Ethereum/GasSettings/EthFeeSelect" import { useEthBalance } from "@ui/domains/Ethereum/useEthBalance" -import { useEthereumProvider } from "@ui/domains/Ethereum/useEthereumProvider" +import { usePublicClient } from "@ui/domains/Ethereum/usePublicClient" import { EthSignBody } from "@ui/domains/Sign/Ethereum/EthSignBody" import { SignAlertMessage } from "@ui/domains/Sign/SignAlertMessage" import { SignHardwareEthereum } from "@ui/domains/Sign/SignHardwareEthereum" @@ -22,9 +24,9 @@ import { Button, Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui" import { SignAccountAvatar } from "../SignAccountAvatar" -const useEvmBalance = (address: string, evmNetworkId: string | undefined) => { - const provider = useEthereumProvider(evmNetworkId) - return useEthBalance(provider, address) +const useEvmBalance = (address: EvmAddress, evmNetworkId: EvmNetworkId | undefined) => { + const publicClient = usePublicClient(evmNetworkId) + return useEthBalance(publicClient, address) } const FeeTooltip = ({ @@ -33,10 +35,10 @@ const FeeTooltip = ({ tokenId, balance, }: { - estimatedFee: string | bigint | undefined - maxFee: string | bigint | undefined - tokenId: string | undefined - balance: string | bigint | null | undefined + estimatedFee: bigint | undefined + maxFee: bigint | undefined + tokenId: TokenId | undefined + balance: bigint | null | undefined }) => { const { t } = useTranslation("request") @@ -101,14 +103,14 @@ export const EthSignTransactionRequest = () => { approveHardware, isPayloadLocked, setIsPayloadLocked, - transactionInfo, + decodedTx, gasSettingsByPriority, setCustomSettings, setReady, isValid, networkUsage, } = useEthSignTransactionRequest() - const { balance } = useEvmBalance(account?.address, network?.id) + const { balance } = useEvmBalance(account?.address as EvmAddress, network?.id) const { processing, errorMessage } = useMemo(() => { return { @@ -137,7 +139,7 @@ export const EthSignTransactionRequest = () => {
- +
{!isLoading && ( @@ -164,14 +166,14 @@ export const EthSignTransactionRequest = () => { -
{transaction?.type === 2 && t("Priority")}
+
{transaction?.type === "eip1559" && t("Priority")}
@@ -182,7 +184,7 @@ export const EthSignTransactionRequest = () => {
{ ) : null} {account && request && account.isHardware ? ( { {t("Cancel")}
diff --git a/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormLegacy.tsx b/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormLegacy.tsx index c4fd1cc04f..b820c54168 100644 --- a/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormLegacy.tsx +++ b/apps/extension/src/ui/domains/Ethereum/GasSettings/CustomGasSettingsFormLegacy.tsx @@ -1,4 +1,5 @@ -import { EthGasSettingsLegacy } from "@core/domains/ethereum/types" +import { getHumanReadableErrorMessage } from "@core/domains/ethereum/errors" +import { EthGasSettingsLegacy, EvmNetworkId } from "@core/domains/ethereum/types" import { EthTransactionDetails, GasSettingsByPriorityLegacy } from "@core/domains/signing/types" import { log } from "@core/log" import { yupResolver } from "@hookform/resolvers/yup" @@ -8,17 +9,17 @@ import { ArrowRightIcon, InfoIcon, LoaderIcon } from "@talismn/icons" import { formatDecimals } from "@talismn/util" import { TokensAndFiat } from "@ui/domains/Asset/TokensAndFiat" import { useAnalytics } from "@ui/hooks/useAnalytics" -import { BigNumber, ethers } from "ethers" import { FC, FormEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import { useDebounce } from "react-use" -import { IconButton } from "talisman-ui" +import { IconButton, Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui" import { Button, FormFieldContainer, FormFieldInputText } from "talisman-ui" +import { TransactionRequest, formatGwei, parseGwei } from "viem" import * as yup from "yup" -import { useEthereumProvider } from "../useEthereumProvider" import { useIsValidEthTransaction } from "../useIsValidEthTransaction" +import { usePublicClient } from "../usePublicClient" import { Indicator, MessageRow } from "./common" const INPUT_PROPS = { @@ -31,9 +32,9 @@ type FormData = { } const gasSettingsFromFormData = (formData: FormData): EthGasSettingsLegacy => ({ - type: 0, - gasPrice: BigNumber.from(Math.round(formData.gasPrice * Math.pow(10, 9))), - gasLimit: BigNumber.from(formData.gasLimit), + type: "legacy", + gasPrice: BigInt(Math.round(formData.gasPrice * Math.pow(10, 9))), + gas: BigInt(formData.gasLimit), }) const schema = yup @@ -44,7 +45,8 @@ const schema = yup .required() const useIsValidGasSettings = ( - tx: ethers.providers.TransactionRequest, + evmNetworkId: EvmNetworkId, + tx: TransactionRequest, gasPrice: number, gasLimit: number ) => { @@ -70,7 +72,7 @@ const useIsValidGasSettings = ( [gasPrice, gasLimit] ) - const provider = useEthereumProvider(tx.chainId?.toString()) + const provider = usePublicClient(evmNetworkId) const txPrepared = useMemo(() => { try { @@ -78,7 +80,7 @@ const useIsValidGasSettings = ( return { ...tx, ...gasSettingsFromFormData(debouncedFormData), - } as ethers.providers.TransactionRequest + } as TransactionRequest } catch (err) { // any bad input throws here, ignore return undefined @@ -99,7 +101,7 @@ const useIsValidGasSettings = ( type CustomGasSettingsFormLegacyProps = { networkUsage?: number - tx: ethers.providers.TransactionRequest + tx: TransactionRequest tokenId: string txDetails: EthTransactionDetails gasSettingsByPriority: GasSettingsByPriorityLegacy @@ -121,16 +123,16 @@ export const CustomGasSettingsFormLegacy: FC = useEffect(() => { genericEvent("open custom gas settings", { - network: tx.chainId, + network: Number(txDetails.evmNetworkId), gasType: gasSettingsByPriority?.type, }) - }, [gasSettingsByPriority?.type, genericEvent, tx.chainId]) + }, [gasSettingsByPriority?.type, genericEvent, txDetails.evmNetworkId]) const customSettings = gasSettingsByPriority.custom const networkGasPrice = useMemo( () => - formatDecimals(ethers.utils.formatUnits(txDetails.gasPrice as string, "gwei"), undefined, { + formatDecimals(formatGwei(txDetails.gasPrice), undefined, { notation: "standard", }), [txDetails.gasPrice] @@ -139,13 +141,13 @@ export const CustomGasSettingsFormLegacy: FC = const defaultValues: FormData = useMemo( () => ({ gasPrice: Number( - formatDecimals(ethers.utils.formatUnits(customSettings.gasPrice, "gwei"), undefined, { + formatDecimals(formatGwei(customSettings.gasPrice), undefined, { notation: "standard", }) ), - gasLimit: BigNumber.from(customSettings.gasLimit).toNumber(), + gasLimit: Number(customSettings.gas), }), - [customSettings.gasLimit, customSettings.gasPrice] + [customSettings.gas, customSettings.gasPrice] ) const { @@ -174,7 +176,7 @@ export const CustomGasSettingsFormLegacy: FC = const totalMaxFee = useMemo(() => { try { - return BigNumber.from(ethers.utils.parseUnits(String(gasPrice), "gwei")).mul(gasLimit) + return parseGwei(String(gasPrice)) * BigInt(gasLimit) } catch (err) { return null } @@ -185,22 +187,14 @@ export const CustomGasSettingsFormLegacy: FC = let errorGasLimit = "" if (errors.gasPrice) warningFee = t("Gas price is invalid") - else if ( - gasPrice && - BigNumber.from(ethers.utils.parseUnits(String(gasPrice), "gwei")).lt(txDetails.gasPrice) - ) + else if (gasPrice && parseGwei(String(gasPrice)) < txDetails.gasPrice) warningFee = t("Gas price seems too low for current network conditions") - else if ( - gasPrice && - BigNumber.from(ethers.utils.parseUnits(String(gasPrice), "gwei")).gt( - BigNumber.from(txDetails.gasPrice).mul(2) - ) - ) + else if (gasPrice && parseGwei(String(gasPrice)) > txDetails.gasPrice * 2n) warningFee = t("Gas price seems higher than required") if (errors.gasLimit?.type === "min") errorGasLimit = t("Gas Limit minimum value is 21000") else if (errors.gasLimit) errorGasLimit = t("Gas Limit is invalid") - else if (BigNumber.from(txDetails.estimatedGas).gt(gasLimit)) + else if (txDetails.estimatedGas > BigInt(gasLimit)) errorGasLimit = t("Gas Limit too low, transaction likely to fail") return { @@ -223,7 +217,7 @@ export const CustomGasSettingsFormLegacy: FC = const gasSettings = gasSettingsFromFormData(formData) genericEvent("set custom gas settings", { - network: tx.chainId, + network: Number(txDetails.evmNetworkId), gasType: gasSettings.type, }) @@ -233,11 +227,14 @@ export const CustomGasSettingsFormLegacy: FC = notify({ title: "Error", subtitle: (err as Error).message, type: "error" }) } }, - [genericEvent, onConfirm, tx.chainId] + [genericEvent, onConfirm, txDetails.evmNetworkId] ) - const { isValid: isGasSettingsValid, isLoading: isLoadingGasSettingsValid } = - useIsValidGasSettings(tx, gasPrice, gasLimit) + const { + isValid: isGasSettingsValid, + isLoading: isLoadingGasSettingsValid, + error: gasSettingsError, + } = useIsValidGasSettings(txDetails.evmNetworkId, tx, gasPrice, gasLimit) const showMaxFeeTotal = isFormValid && isGasSettingsValid && !isLoadingGasSettingsValid @@ -344,7 +341,14 @@ export const CustomGasSettingsFormLegacy: FC = ) : isLoadingGasSettingsValid ? ( ) : ( - {t("Invalid settings")} + + + {t("Invalid transaction")} + + {!!gasSettingsError && ( + {getHumanReadableErrorMessage(gasSettingsError)} + )} + )} diff --git a/apps/extension/src/ui/domains/Ethereum/GasSettings/EthFeeSelect.tsx b/apps/extension/src/ui/domains/Ethereum/GasSettings/EthFeeSelect.tsx index 825f6c552b..aedea16437 100644 --- a/apps/extension/src/ui/domains/Ethereum/GasSettings/EthFeeSelect.tsx +++ b/apps/extension/src/ui/domains/Ethereum/GasSettings/EthFeeSelect.tsx @@ -8,9 +8,9 @@ import { TokenId } from "@core/domains/tokens/types" import { useOpenClose } from "@talisman/hooks/useOpenClose" import { classNames } from "@talismn/util" import { useAnalytics } from "@ui/hooks/useAnalytics" -import { ethers } from "ethers" import { FC, useCallback, useEffect, useState } from "react" import { Drawer, PillButton } from "talisman-ui" +import { TransactionRequest } from "viem" import { useFeePriorityOptionsUI } from "./common" import { CustomGasSettingsFormEip1559 } from "./CustomGasSettingsFormEip1559" @@ -28,7 +28,7 @@ const OpenFeeSelectTracker = () => { } type EthFeeSelectProps = { - tx: ethers.providers.TransactionRequest + tx: TransactionRequest tokenId: TokenId disabled?: boolean txDetails: EthTransactionDetails diff --git a/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx b/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx index 49cfa02a59..20b25e30fe 100644 --- a/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx +++ b/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx @@ -7,11 +7,11 @@ import { GasSettingsByPriority, } from "@core/domains/signing/types" import { BalanceFormatter } from "@talismn/balances" +import { TokenId } from "@talismn/chaindata-provider" import { ChevronRightIcon } from "@talismn/icons" import { classNames } from "@talismn/util" import { TokensAndFiat } from "@ui/domains/Asset/TokensAndFiat" import useToken from "@ui/hooks/useToken" -import { BigNumber } from "ethers" import { FC, useCallback, useMemo } from "react" import { Trans, useTranslation } from "react-i18next" import { Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui" @@ -34,9 +34,9 @@ const getGasSettings = ( } const Eip1559FeeTooltip: FC<{ - estimatedFee: BigNumber - maxFee: BigNumber - tokenId: string + estimatedFee: bigint + maxFee: bigint + tokenId: TokenId }> = ({ estimatedFee, maxFee, tokenId }) => { const { t } = useTranslation("request") const token = useToken(tokenId) diff --git a/apps/extension/src/ui/domains/Ethereum/NetworkDetailsButton.tsx b/apps/extension/src/ui/domains/Ethereum/NetworkDetailsButton.tsx index dd93999a94..d59129d091 100644 --- a/apps/extension/src/ui/domains/Ethereum/NetworkDetailsButton.tsx +++ b/apps/extension/src/ui/domains/Ethereum/NetworkDetailsButton.tsx @@ -1,8 +1,8 @@ -import { AddEthereumChainParameter } from "@core/domains/ethereum/types" import { useOpenClose } from "@talisman/hooks/useOpenClose" import { FC, useCallback, useMemo } from "react" import { useTranslation } from "react-i18next" import { Button, Drawer, PillButton } from "talisman-ui" +import { AddEthereumChainParameter } from "viem" import { ViewDetailsField } from "../Sign/ViewDetails/ViewDetailsField" @@ -25,11 +25,11 @@ export const NetworksDetailsButton: FC<{ const { name, rpcs, chainId, tokenSymbol, blockExplorers } = useMemo(() => { return { - name: network?.chainName || "N/A", - rpcs: network?.rpcUrls?.join("\n") || "N/A", - chainId: tryParseIntFromHex(network?.chainId), - tokenSymbol: network?.nativeCurrency?.symbol || "N/A", - blockExplorers: network?.blockExplorerUrls?.join("\n"), + name: network.chainName || "N/A", + rpcs: network.rpcUrls?.join("\n") || "N/A", + chainId: tryParseIntFromHex(network.chainId), + tokenSymbol: network.nativeCurrency?.symbol || "N/A", + blockExplorers: network.blockExplorerUrls?.join("\n"), } }, [network, tryParseIntFromHex]) diff --git a/apps/extension/src/ui/domains/Ethereum/getExtensionEthereumProvider.ts b/apps/extension/src/ui/domains/Ethereum/getExtensionEthereumProvider.ts deleted file mode 100644 index 9ad98d2048..0000000000 --- a/apps/extension/src/ui/domains/Ethereum/getExtensionEthereumProvider.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - ETH_ERROR_EIP1474_INTERNAL_ERROR, - EthProviderRpcError, -} from "@core/injectEth/EthProviderRpcError" -import { EthRequestSignatures, EthRequestTypes } from "@core/injectEth/types" -import { log } from "@core/log" -import { EvmNetworkId } from "@talismn/chaindata-provider" -import { api } from "@ui/api" -import { ethers } from "ethers" - -const ethereumRequest = - (chainId: EvmNetworkId): ethers.providers.JsonRpcFetchFunc => - async (method: string, params?: unknown[]) => { - try { - return await api.ethRequest({ - chainId, - method: method as keyof EthRequestSignatures, - params: params as EthRequestSignatures[EthRequestTypes][0], - }) - } catch (err) { - log.error("[provider.request] error on %s", method, { err }) - - const { message, code, data } = err as EthProviderRpcError - throw new EthProviderRpcError(message, code ?? ETH_ERROR_EIP1474_INTERNAL_ERROR, data) - } - } - -export const getExtensionEthereumProvider = (evmNetworkId: EvmNetworkId) => { - return new ethers.providers.Web3Provider(ethereumRequest(evmNetworkId)) -} diff --git a/apps/extension/src/ui/domains/Ethereum/useEthBalance.ts b/apps/extension/src/ui/domains/Ethereum/useEthBalance.ts index 7881876b16..fe1d583b43 100644 --- a/apps/extension/src/ui/domains/Ethereum/useEthBalance.ts +++ b/apps/extension/src/ui/domains/Ethereum/useEthBalance.ts @@ -1,21 +1,23 @@ +import { EvmAddress } from "@core/domains/ethereum/types" +import { isEthereumAddress } from "@talismn/util" import { useQuery } from "@tanstack/react-query" -import { ethers } from "ethers" +import { PublicClient } from "viem" export const useEthBalance = ( - provider: ethers.providers.JsonRpcProvider | undefined, - address: string | undefined + publicClient: PublicClient | undefined, + address: EvmAddress | undefined ) => { const { data: balance, ...rest } = useQuery({ - queryKey: ["useEthBalance", provider?.network?.chainId, address], + queryKey: ["useEthBalance", publicClient?.chain?.id, address], queryFn: () => { - if (!provider || !address) return null - return provider.getBalance(address) + if (!publicClient || !isEthereumAddress(address)) return null + return publicClient.getBalance({ address }) }, refetchInterval: 12_000, refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, - enabled: !!provider?.network && !!address, + enabled: !!publicClient?.chain?.id && isEthereumAddress(address), }) return { balance, ...rest } diff --git a/apps/extension/src/ui/domains/Ethereum/useEthReplaceTransaction.ts b/apps/extension/src/ui/domains/Ethereum/useEthReplaceTransaction.ts index 59bb4ab4da..bb9cfa05bc 100644 --- a/apps/extension/src/ui/domains/Ethereum/useEthReplaceTransaction.ts +++ b/apps/extension/src/ui/domains/Ethereum/useEthReplaceTransaction.ts @@ -1,32 +1,40 @@ -import { rebuildTransactionRequestNumbers } from "@core/domains/ethereum/helpers" -import { ethers } from "ethers" +import { parseTransactionRequest } from "@core/domains/ethereum/helpers" +import { EvmNetworkId } from "@talismn/chaindata-provider" import { useMemo } from "react" +import { TransactionRequest } from "viem" import { TxReplaceType } from "../Transactions" import { useEthTransaction } from "./useEthTransaction" export const useEthReplaceTransaction = ( - tx: ethers.providers.TransactionRequest, + txToReplace: TransactionRequest, + evmNetworkId: EvmNetworkId, type: TxReplaceType, lock?: boolean ) => { - const transaction: ethers.providers.TransactionRequest = useMemo( - () => - rebuildTransactionRequestNumbers({ - chainId: tx.chainId, - from: tx.from, - to: type === "cancel" ? tx.from : tx.to, - value: type === "cancel" ? "0" : tx.value, - data: type === "cancel" ? undefined : tx.data, - nonce: tx.nonce, + const replaceTx = useMemo(() => { + const parsed = parseTransactionRequest(txToReplace) - // pass previous tx gas data - type: tx.type, - gasPrice: tx.gasPrice, - maxPriorityFeePerGas: tx.maxPriorityFeePerGas, - }), - [tx, type] - ) + const newTx: TransactionRequest = + txToReplace.type === "eip1559" + ? { + type: "eip1559", + from: parsed.from, + maxPriorityFeePerGas: parsed.maxPriorityFeePerGas, + } + : { + type: "legacy", + from: parsed.from, + gasPrice: parsed.gasPrice, + } - return useEthTransaction(transaction, lock, true) + newTx.nonce = parsed.nonce + newTx.to = type === "cancel" ? parsed.from : parsed.to + newTx.value = type === "cancel" ? 0n : parsed.value + if (type === "speed-up") newTx.data = parsed.data + + return newTx + }, [txToReplace, type]) + + return useEthTransaction(replaceTx, evmNetworkId, lock, true) } diff --git a/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts b/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts index 1fd35054b3..8dd1e4e41d 100644 --- a/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts +++ b/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts @@ -1,9 +1,10 @@ -import { getEthersErrorLabelFromCode } from "@core/domains/ethereum/errors" +import { getHumanReadableErrorMessage } from "@core/domains/ethereum/errors" import { getGasLimit, getGasSettingsEip1559, getTotalFeesFromGasSettings, prepareTransaction, + serializeTransactionRequest, } from "@core/domains/ethereum/helpers" import { EthGasSettings, @@ -19,14 +20,15 @@ import { GasSettingsByPriority, } from "@core/domains/signing/types" import { ETH_ERROR_EIP1474_METHOD_NOT_FOUND } from "@core/injectEth/EthProviderRpcError" -import { getEthTransactionInfo } from "@core/util/getEthTransactionInfo" +import { decodeEvmTransaction } from "@core/util/decodeEvmTransaction" import { FeeHistoryAnalysis, getFeeHistoryAnalysis } from "@core/util/getFeeHistoryAnalysis" +import { isBigInt } from "@talismn/util" import { useQuery } from "@tanstack/react-query" import { api } from "@ui/api" -import { useEthereumProvider } from "@ui/domains/Ethereum/useEthereumProvider" -import { BigNumber, ethers } from "ethers" +import { usePublicClient } from "@ui/domains/Ethereum/usePublicClient" import { useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" +import { PublicClient, TransactionRequest } from "viem" import { useIsValidEthTransaction } from "./useIsValidEthTransaction" @@ -34,12 +36,12 @@ import { useIsValidEthTransaction } from "./useIsValidEthTransaction" const UNRELIABLE_GASPRICE_NETWORK_IDS = [137, 80001] const useNonce = ( - address: string | undefined, + address: `0x${string}` | undefined, evmNetworkId: EvmNetworkId | undefined, forcedValue?: number ) => { const { data, ...rest } = useQuery({ - queryKey: ["nonce", address, evmNetworkId, forcedValue], + queryKey: ["useNonce", address, evmNetworkId, forcedValue], queryFn: () => { if (forcedValue !== undefined) return forcedValue return address && evmNetworkId ? api.ethGetTransactionsCount(address, evmNetworkId) : null @@ -50,21 +52,20 @@ const useNonce = ( } // TODO : could be skipped for networks that we know already support it, but need to keep checking for legacy network in case they upgrade -const useHasEip1559Support = (provider: ethers.providers.JsonRpcProvider | undefined) => { +const useHasEip1559Support = (publicClient: PublicClient | undefined) => { const { data, ...rest } = useQuery({ - queryKey: ["hasEip1559Support", provider?.network?.chainId], + queryKey: ["useHasEip1559Support", publicClient?.chain?.id], queryFn: async () => { - if (!provider) return null + if (!publicClient) return null try { - const [{ baseFeePerGas }] = await Promise.all([ - // check that block has a baseFeePerGas - provider.send("eth_getBlockByNumber", ["latest", false]), - // check that method eth_feeHistory exists. This will throw with code -32601 if it doesn't. - provider.send("eth_feeHistory", [ethers.utils.hexValue(1), "latest", [10]]), - ]) - return baseFeePerGas !== undefined + publicClient.chain?.fees?.defaultPriorityFee + const { + baseFeePerGas: [baseFee], + } = await publicClient.getFeeHistory({ blockCount: 1, rewardPercentiles: [] }) + return baseFee > 0n } catch (err) { + // TODO check that feeHistory returns -32601 when method doesn't exist const error = err as Error & { code?: number } if (error.code === ETH_ERROR_EIP1474_METHOD_NOT_FOUND) return false @@ -75,58 +76,49 @@ const useHasEip1559Support = (provider: ethers.providers.JsonRpcProvider | undef refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, - enabled: !!provider, + enabled: !!publicClient, }) return { hasEip1559Support: data ?? undefined, ...rest } } const useBlockFeeData = ( - provider: ethers.providers.JsonRpcProvider | undefined, - tx: ethers.providers.TransactionRequest | undefined, + publicClient: PublicClient | undefined, + tx: TransactionRequest | undefined, withFeeOptions: boolean | undefined ) => { const { data, ...rest } = useQuery({ - queryKey: ["block", provider?.network?.chainId, tx, withFeeOptions], + queryKey: [ + "useBlockFeeData", + publicClient?.chain?.id, + tx && serializeTransactionRequest(tx), + withFeeOptions, + ], queryFn: async () => { - if (!provider || !tx) return null - - // estimate gas without any gas setting, will be used as our gasLimit - // spread to keep only valid properties (exclude the one called "gas" and any undefined ones) - const { chainId, from, to, value = BigNumber.from("0"), data } = tx - const txForEstimate: ethers.providers.TransactionRequest = { - chainId, - from, - to, - value, - data, - } - if (tx.accessList !== undefined) txForEstimate.accessList = tx.accessList - if (tx.customData !== undefined) txForEstimate.customData = tx.customData - if (tx.ccipReadEnabled !== undefined) txForEstimate.ccipReadEnabled = tx.ccipReadEnabled + if (!publicClient?.chain?.id || !tx) return null + + // estimate gas without any gas or nonce setting to prevent rpc from validating these + const { from: account, to, value, data } = tx const [ gasPrice, - { gasLimit: blockGasLimit, baseFeePerGas, gasUsed, number: blockNumber }, + { gasLimit: blockGasLimit, baseFeePerGas, gasUsed }, feeHistoryAnalysis, estimatedGas, ] = await Promise.all([ - provider.getGasPrice(), - provider.getBlock("latest"), - withFeeOptions ? getFeeHistoryAnalysis(provider) : undefined, + publicClient.getGasPrice(), + publicClient.getBlock(), + withFeeOptions ? getFeeHistoryAnalysis(publicClient) : undefined, // estimate gas may change over time for contract calls, so we need to refresh it every time we prepare the tx to prevent an invalid transaction - provider.estimateGas(txForEstimate), + publicClient.estimateGas({ account, to, value, data }), ]) - if ( - feeHistoryAnalysis && - !UNRELIABLE_GASPRICE_NETWORK_IDS.includes(provider.network.chainId) - ) { + if (feeHistoryAnalysis && !UNRELIABLE_GASPRICE_NETWORK_IDS.includes(publicClient.chain.id)) { // minimum maxPriorityPerGas value required to be considered valid into next block is equal to `gasPrice - baseFee` - let minimumMaxPriorityFeePerGas = gasPrice.sub(baseFeePerGas ?? 0) - if (minimumMaxPriorityFeePerGas.lt(0)) { + let minimumMaxPriorityFeePerGas = gasPrice - feeHistoryAnalysis.nextBaseFee + if (minimumMaxPriorityFeePerGas < 0n) { // on a busy network, when there is a sudden lowering of amount of transactions, it can happen that baseFeePerGas is higher than gPrice - minimumMaxPriorityFeePerGas = BigNumber.from("0") + minimumMaxPriorityFeePerGas = 0n } // if feeHistory is invalid (network is inactive), use minimumMaxPriorityFeePerGas for all options. @@ -140,29 +132,24 @@ const useBlockFeeData = ( feeHistoryAnalysis.avgGasUsedRatio !== null && feeHistoryAnalysis.avgGasUsedRatio < 0.8 ) - feeHistoryAnalysis.maxPriorityPerGasOptions.low = minimumMaxPriorityFeePerGas.lt( - feeHistoryAnalysis.maxPriorityPerGasOptions.low - ) - ? minimumMaxPriorityFeePerGas - : feeHistoryAnalysis.maxPriorityPerGasOptions.low + feeHistoryAnalysis.maxPriorityPerGasOptions.low = + minimumMaxPriorityFeePerGas < feeHistoryAnalysis.maxPriorityPerGasOptions.low + ? minimumMaxPriorityFeePerGas + : feeHistoryAnalysis.maxPriorityPerGasOptions.low } - const networkUsage = - !gasUsed || !blockGasLimit - ? undefined - : gasUsed.mul(100).div(blockGasLimit).toNumber() / 100 + const networkUsage = Number((gasUsed * 100n) / blockGasLimit) / 100 return { estimatedGas, gasPrice, - baseFeePerGas, + baseFeePerGas: feeHistoryAnalysis?.nextBaseFee ?? baseFeePerGas, blockGasLimit, networkUsage, feeHistoryAnalysis, - blockNumber, } }, - enabled: !!tx && !!provider && withFeeOptions !== undefined, + enabled: !!tx && !!publicClient && withFeeOptions !== undefined, refetchInterval: 6_000, retry: false, }) @@ -183,50 +170,55 @@ const useBlockFeeData = ( } } -const useTransactionInfo = ( - provider: ethers.providers.JsonRpcProvider | undefined, - tx: ethers.providers.TransactionRequest | undefined +const useDecodeEvmTransaction = ( + publicClient: PublicClient | undefined, + tx: TransactionRequest | undefined ) => { const { data, ...rest } = useQuery({ // check tx as boolean as it's not pure - queryKey: ["transactionInfo", provider?.network?.chainId, tx], + queryKey: [ + "useDecodeEvmTransaction", + publicClient?.chain?.id, + tx && serializeTransactionRequest(tx), + ], queryFn: async () => { - if (!provider || !tx) return null - return await getEthTransactionInfo(provider, tx) + if (!publicClient || !tx) return null + return await decodeEvmTransaction(publicClient, tx) }, refetchInterval: false, refetchOnWindowFocus: false, // prevents error to be cleared when window gets focus - enabled: !!provider && !!tx, + enabled: !!publicClient && !!tx, }) - return { transactionInfo: data ?? undefined, ...rest } + return { decodedTx: data, ...rest } } const getEthGasSettingsFromTransaction = ( - tx: ethers.providers.TransactionRequest | undefined, + tx: TransactionRequest | undefined, hasEip1559Support: boolean | undefined, - estimatedGas: BigNumber | undefined, - blockGasLimit: BigNumber | undefined, + estimatedGas: bigint | undefined, + blockGasLimit: bigint | undefined, isContractCall: boolean | undefined = true // default to worse scenario ) => { - if (!tx || hasEip1559Support === undefined || !blockGasLimit || !estimatedGas) return undefined + if (!tx || hasEip1559Support === undefined || !isBigInt(blockGasLimit) || !isBigInt(estimatedGas)) + return undefined const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = tx - const gasLimit = getGasLimit(blockGasLimit, estimatedGas, tx, isContractCall) + const gas = getGasLimit(blockGasLimit, estimatedGas, tx, isContractCall) - if (hasEip1559Support && gasLimit && maxFeePerGas && maxPriorityFeePerGas) { + if (hasEip1559Support && gas && maxFeePerGas && maxPriorityFeePerGas) { return { - type: 2, - gasLimit, + type: "eip1559", + gas, maxFeePerGas, maxPriorityFeePerGas, } as EthGasSettingsEip1559 } - if (!hasEip1559Support && gasLimit && gasPrice) { + if (!hasEip1559Support && gas && gasPrice) { return { - type: 0, - gasLimit, + type: "legacy", + gas, gasPrice, } as EthGasSettingsLegacy } @@ -247,22 +239,29 @@ const useGasSettings = ({ isContractCall, }: { hasEip1559Support: boolean | undefined - baseFeePerGas: BigNumber | null | undefined - estimatedGas: BigNumber | null | undefined - gasPrice: BigNumber | null | undefined - blockGasLimit: BigNumber | null | undefined + baseFeePerGas: bigint | null | undefined + estimatedGas: bigint | null | undefined + gasPrice: bigint | null | undefined + blockGasLimit: bigint | null | undefined feeHistoryAnalysis: FeeHistoryAnalysis | null | undefined priority: EthPriorityOptionName | undefined - tx: ethers.providers.TransactionRequest | undefined + tx: TransactionRequest | undefined isReplacement: boolean | undefined isContractCall: boolean | undefined }) => { const [customSettings, setCustomSettings] = useState() const gasSettingsByPriority: GasSettingsByPriority | undefined = useMemo(() => { - if (hasEip1559Support === undefined || !estimatedGas || !gasPrice || !blockGasLimit || !tx) + if ( + hasEip1559Support === undefined || + !isBigInt(estimatedGas) || + !isBigInt(gasPrice) || + !isBigInt(blockGasLimit) || + !tx + ) return undefined - const gasLimit = getGasLimit(blockGasLimit, estimatedGas, tx, isContractCall) + + const gas = getGasLimit(blockGasLimit, estimatedGas, tx, isContractCall) const suggestedSettings = getEthGasSettingsFromTransaction( tx, hasEip1559Support, @@ -272,31 +271,29 @@ const useGasSettings = ({ ) if (hasEip1559Support) { - if (!feeHistoryAnalysis || !baseFeePerGas) return undefined + if (!feeHistoryAnalysis || !isBigInt(baseFeePerGas)) return undefined const mapMaxPriority = feeHistoryAnalysis.maxPriorityPerGasOptions - if (isReplacement) { + if (isReplacement && tx.maxPriorityFeePerGas !== undefined) { // for replacement transactions, ensure that maxPriorityFeePerGas is at least 10% higher than original tx - const minimumMaxPriorityFeePerGas = ethers.BigNumber.from(tx.maxPriorityFeePerGas) - .mul(110) - .div(100) - if (mapMaxPriority.low.lt(minimumMaxPriorityFeePerGas)) + const minimumMaxPriorityFeePerGas = (tx.maxPriorityFeePerGas * 110n) / 100n + if (mapMaxPriority.low < minimumMaxPriorityFeePerGas) mapMaxPriority.low = minimumMaxPriorityFeePerGas - if (mapMaxPriority.medium.lt(minimumMaxPriorityFeePerGas)) + if (mapMaxPriority.medium < minimumMaxPriorityFeePerGas) mapMaxPriority.medium = minimumMaxPriorityFeePerGas - if (mapMaxPriority.high.lt(minimumMaxPriorityFeePerGas)) + if (mapMaxPriority.high < minimumMaxPriorityFeePerGas) mapMaxPriority.high = minimumMaxPriorityFeePerGas } - const low = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.low, gasLimit) - const medium = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.medium, gasLimit) - const high = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.high, gasLimit) + const low = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.low, gas) + const medium = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.medium, gas) + const high = getGasSettingsEip1559(baseFeePerGas, mapMaxPriority.high, gas) const custom: EthGasSettingsEip1559 = - customSettings?.type === 2 + customSettings?.type === "eip1559" ? customSettings - : suggestedSettings?.type === 2 + : suggestedSettings?.type === "eip1559" ? suggestedSettings : { ...low, @@ -314,24 +311,23 @@ const useGasSettings = ({ } const recommendedSettings: EthGasSettingsLegacy = { - type: 0, - gasLimit, + type: "legacy", + gas, // in some cases (ex: claiming bridged tokens on Polygon zkEVM), // 0 is provided by the dapp and has to be used for the tx to succeed - gasPrice: tx.gasPrice && BigNumber.from(tx.gasPrice).isZero() ? BigNumber.from(0) : gasPrice, + gasPrice: tx.gasPrice === 0n ? 0n : gasPrice, } - if (isReplacement) { + if (isReplacement && tx.gasPrice !== undefined) { // for replacement transactions, ensure that maxPriorityFeePerGas is at least 10% higher than original tx - const minimumGasPrice = ethers.BigNumber.from(tx.gasPrice).mul(110).div(100) - if (ethers.BigNumber.from(gasPrice).lt(minimumGasPrice)) - recommendedSettings.gasPrice = minimumGasPrice + const minimumGasPrice = (tx.gasPrice * 110n) / 100n + if (gasPrice < minimumGasPrice) recommendedSettings.gasPrice = minimumGasPrice } const custom: EthGasSettingsLegacy = - customSettings?.type === 0 + customSettings?.type === "legacy" ? customSettings - : suggestedSettings?.type === 0 + : suggestedSettings?.type === "legacy" ? suggestedSettings : recommendedSettings @@ -369,19 +365,19 @@ const useGasSettings = ({ } export const useEthTransaction = ( - tx: ethers.providers.TransactionRequest | undefined, + tx: TransactionRequest | undefined, + evmNetworkId: EvmNetworkId | undefined, lockTransaction = false, isReplacement = false ) => { - const provider = useEthereumProvider(tx?.chainId?.toString()) - const { transactionInfo, error: errorTransactionInfo } = useTransactionInfo(provider, tx) - const { hasEip1559Support, error: errorEip1559Support } = useHasEip1559Support(provider) + const publicClient = usePublicClient(evmNetworkId) + const { decodedTx, isLoading: isDecoding } = useDecodeEvmTransaction(publicClient, tx) + const { hasEip1559Support, error: errorEip1559Support } = useHasEip1559Support(publicClient) const { nonce, error: nonceError } = useNonce( - tx?.from, - tx?.chainId?.toString(), - isReplacement && tx?.nonce ? BigNumber.from(tx.nonce).toNumber() : undefined + tx?.from as `0x${string}` | undefined, + evmNetworkId, + isReplacement && tx?.nonce ? tx.nonce : undefined ) - const { gasPrice, networkUsage, @@ -390,7 +386,7 @@ export const useEthTransaction = ( feeHistoryAnalysis, estimatedGas, error: blockFeeDataError, - } = useBlockFeeData(provider, tx, hasEip1559Support) + } = useBlockFeeData(publicClient, tx, hasEip1559Support) const [priority, setPriority] = useState() @@ -398,7 +394,7 @@ export const useEthTransaction = ( // ex: from send funds when switching from BSC (legacy) to mainnet (eip1559) useEffect(() => { setPriority(undefined) - }, [tx?.chainId]) + }, [evmNetworkId]) // set default priority based on EIP1559 support useEffect(() => { @@ -407,7 +403,7 @@ export const useEthTransaction = ( }, [hasEip1559Support, isReplacement, priority]) const { gasSettings, setCustomSettings, gasSettingsByPriority } = useGasSettings({ - tx, + tx: tx, priority, hasEip1559Support, baseFeePerGas, @@ -416,13 +412,13 @@ export const useEthTransaction = ( blockGasLimit, feeHistoryAnalysis, isReplacement, - isContractCall: transactionInfo?.isContractCall, + isContractCall: decodedTx?.isContractCall, }) const liveUpdatingTransaction = useMemo(() => { - if (!provider || !tx || !gasSettings || nonce === undefined) return undefined + if (!publicClient || !tx || !gasSettings || nonce === undefined) return undefined return prepareTransaction(tx, gasSettings, nonce) - }, [gasSettings, provider, tx, nonce]) + }, [gasSettings, publicClient, tx, nonce]) // transaction may be locked once sent to hardware device for signing const [transaction, setTransaction] = useState(liveUpdatingTransaction) @@ -433,10 +429,17 @@ export const useEthTransaction = ( // TODO replace this wierd object name with something else... gasInfo ? const txDetails: EthTransactionDetails | undefined = useMemo(() => { - if (!gasPrice || !estimatedGas || !transaction || !gasSettings) return undefined + if ( + !evmNetworkId || + !isBigInt(gasPrice) || + !isBigInt(estimatedGas) || + !transaction || + !gasSettings + ) + return undefined - // if type 2 transaction, wait for baseFee to be available - if (gasSettings?.type === 2 && !baseFeePerGas) return undefined + // if eip1559 transaction, wait for baseFee to be available + if (gasSettings?.type === "eip1559" && !isBigInt(baseFeePerGas)) return undefined const { estimatedFee, maxFee } = getTotalFeesFromGasSettings( gasSettings, @@ -445,6 +448,7 @@ export const useEthTransaction = ( ) return { + evmNetworkId, estimatedGas, gasPrice, baseFeePerGas, @@ -452,40 +456,48 @@ export const useEthTransaction = ( maxFee, baseFeeTrend: feeHistoryAnalysis?.baseFeeTrend, } - }, [baseFeePerGas, estimatedGas, feeHistoryAnalysis, gasPrice, gasSettings, transaction]) + }, [ + baseFeePerGas, + estimatedGas, + evmNetworkId, + feeHistoryAnalysis?.baseFeeTrend, + gasPrice, + gasSettings, + transaction, + ]) // use staleIsValid to prevent disabling approve button each time there is a new block (triggers gas check) - const { isValid, error: isValidError } = useIsValidEthTransaction(provider, transaction, priority) + const { isValid, error: isValidError } = useIsValidEthTransaction( + publicClient, + transaction, + priority + ) const { t } = useTranslation("request") const { error, errorDetails } = useMemo(() => { const anyError = (errorEip1559Support ?? nonceError ?? blockFeeDataError ?? - errorTransactionInfo ?? - isValidError) as Error & { code?: string; error?: Error } - - const userFriendlyError = getEthersErrorLabelFromCode(anyError?.code) + isValidError) as Error - // if ethers.js error, display underlying error that shows the RPC's error message - const errorToDisplay = anyError?.error ?? anyError + const userFriendlyError = getHumanReadableErrorMessage(anyError) - if (errorToDisplay) + if (anyError) return { error: userFriendlyError ?? t("Failed to prepare transaction"), - errorDetails: errorToDisplay.message, + errorDetails: anyError.message, } return { error: undefined, errorDetails: undefined } - }, [blockFeeDataError, isValidError, errorEip1559Support, errorTransactionInfo, nonceError, t]) + }, [blockFeeDataError, isValidError, errorEip1559Support, nonceError, t]) const isLoading = useMemo( - () => tx && !transactionInfo && !txDetails && !error, - [tx, transactionInfo, txDetails, error] + () => tx && isDecoding && !txDetails && !error, + [tx, isDecoding, txDetails, error] ) return { - transactionInfo, + decodedTx, transaction, txDetails, gasSettings, diff --git a/apps/extension/src/ui/domains/Ethereum/useEthereumProvider.ts b/apps/extension/src/ui/domains/Ethereum/useEthereumProvider.ts deleted file mode 100644 index 778dfb1358..0000000000 --- a/apps/extension/src/ui/domains/Ethereum/useEthereumProvider.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { EvmNetworkId } from "@core/domains/ethereum/types" -import { getExtensionEthereumProvider } from "@ui/domains/Ethereum/getExtensionEthereumProvider" -import { ethers } from "ethers" -import { useMemo } from "react" - -export const useEthereumProvider = ( - evmNetworkId?: EvmNetworkId -): ethers.providers.JsonRpcProvider | undefined => { - const provider = useMemo(() => { - if (!evmNetworkId) return undefined - return getExtensionEthereumProvider(evmNetworkId) - }, [evmNetworkId]) - - return provider -} diff --git a/apps/extension/src/ui/domains/Ethereum/useIsValidEthTransaction.ts b/apps/extension/src/ui/domains/Ethereum/useIsValidEthTransaction.ts index 9db9cb4e0e..95bccd2591 100644 --- a/apps/extension/src/ui/domains/Ethereum/useIsValidEthTransaction.ts +++ b/apps/extension/src/ui/domains/Ethereum/useIsValidEthTransaction.ts @@ -1,52 +1,55 @@ -import { getMaxTransactionCost } from "@core/domains/ethereum/helpers" +import { getMaxTransactionCost, serializeTransactionRequest } from "@core/domains/ethereum/helpers" import { EthPriorityOptionName } from "@core/domains/signing/types" import { useQuery } from "@tanstack/react-query" import { useAccountByAddress } from "@ui/hooks/useAccountByAddress" -import { ethers } from "ethers" import { useTranslation } from "react-i18next" +import { PublicClient, TransactionRequest } from "viem" import { useEthBalance } from "./useEthBalance" export const useIsValidEthTransaction = ( - provider: ethers.providers.JsonRpcProvider | undefined, - transaction: ethers.providers.TransactionRequest | undefined, + publicClient: PublicClient | undefined, + tx: TransactionRequest | undefined, priority: EthPriorityOptionName | undefined ) => { const { t } = useTranslation("request") - const account = useAccountByAddress(transaction?.from) - const { balance } = useEthBalance(provider, transaction?.from) + const account = useAccountByAddress(tx?.from) + const { balance } = useEthBalance(publicClient, tx?.from) const { data, error, isLoading } = useQuery({ queryKey: [ - "useCheckTransaction", - provider?.network?.chainId, - transaction, + "useIsValidEthTransaction", + publicClient?.chain?.id, + tx && serializeTransactionRequest(tx), account?.address, priority, ], queryFn: async () => { - if (!provider || !transaction || !account || balance === undefined) return null + if (!publicClient || !tx || !account || balance === undefined) return null if (account.origin === "WATCHED") throw new Error(t("Cannot sign transactions with a watched account")) // balance checks - const value = ethers.BigNumber.from(transaction.value ?? 0) - const maxTransactionCost = getMaxTransactionCost(transaction) - if (!balance || value.gt(balance)) throw new Error(t("Insufficient balance")) - if (!balance || maxTransactionCost.gt(balance)) + const value = tx.value ?? 0n + const maxTransactionCost = getMaxTransactionCost(tx) + if (!balance || value > balance) throw new Error(t("Insufficient balance")) + if (!balance || maxTransactionCost > balance) throw new Error(t("Insufficient balance to pay for fee")) // dry runs the transaction, if it fails we can't know for sure what the issue really is // there should be helpful message in the error though. - const estimatedGas = await provider.estimateGas(transaction) - return estimatedGas?.gt(0) + const estimatedGas = await publicClient.estimateGas({ + account: tx.from, + ...tx, + }) + return estimatedGas > 0n }, refetchInterval: false, refetchOnWindowFocus: false, retry: 0, keepPreviousData: true, - enabled: !!provider && !!transaction && !!account && balance !== undefined, + enabled: !!publicClient && !!tx && !!account && balance !== undefined, }) return { isValid: !!data, error, isLoading } diff --git a/apps/extension/src/ui/domains/Ethereum/usePublicClient.ts b/apps/extension/src/ui/domains/Ethereum/usePublicClient.ts new file mode 100644 index 0000000000..fd31bfbea0 --- /dev/null +++ b/apps/extension/src/ui/domains/Ethereum/usePublicClient.ts @@ -0,0 +1,67 @@ +import { EvmNetwork, EvmNetworkId } from "@core/domains/ethereum/types" +import { log } from "@core/log" +import { EvmNativeToken } from "@talismn/balances-evm-native" +import { api } from "@ui/api" +import { useEvmNetwork } from "@ui/hooks/useEvmNetwork" +import useToken from "@ui/hooks/useToken" +import { useMemo } from "react" +import { PublicClient, createPublicClient, custom } from "viem" + +type ViemRequest = (arg: { method: string; params?: unknown[] }) => Promise + +const viemRequest = + (chainId: EvmNetworkId): ViemRequest => + async ({ method, params }) => { + try { + return await api.ethRequest({ chainId, method, params }) + } catch (err) { + log.error("publicClient request error : %s", method, { err }) + throw err + } + } + +export const getExtensionPublicClient = ( + evmNetwork: EvmNetwork, + nativeToken: EvmNativeToken +): PublicClient => { + const name = evmNetwork.name ?? `EVM Chain ${evmNetwork.id}` + + return createPublicClient({ + chain: { + id: Number(evmNetwork.id), + name: name, + network: name, + nativeCurrency: { + symbol: nativeToken.symbol, + decimals: nativeToken.decimals, + name: nativeToken.symbol, + }, + rpcUrls: { + // rpcs are a typescript requirement, but won't be used by the custom transport + public: { http: [] }, + default: { http: [] }, + }, + }, + transport: custom( + { + request: viemRequest(evmNetwork.id), + }, + { + // backend will retry, at it's own transport level + retryCount: 0, + } + ), + }) +} + +export const usePublicClient = (evmNetworkId?: EvmNetworkId): PublicClient | undefined => { + const evmNetwork = useEvmNetwork(evmNetworkId) + const nativeToken = useToken(evmNetwork?.nativeToken?.id) + + const publicClient = useMemo(() => { + if (!evmNetwork || nativeToken?.type !== "evm-native") return undefined + return getExtensionPublicClient(evmNetwork, nativeToken) + }, [evmNetwork, nativeToken]) + + return publicClient +} diff --git a/apps/extension/src/ui/domains/SendFunds/SendFundsFeeTooltip.tsx b/apps/extension/src/ui/domains/SendFunds/SendFundsFeeTooltip.tsx index 7c0d5409a6..a2c430f2d4 100644 --- a/apps/extension/src/ui/domains/SendFunds/SendFundsFeeTooltip.tsx +++ b/apps/extension/src/ui/domains/SendFunds/SendFundsFeeTooltip.tsx @@ -1,6 +1,5 @@ import { WithTooltip } from "@talisman/components/Tooltip" import { InfoIcon } from "@talismn/icons" -import { ethers } from "ethers" import { useTranslation } from "react-i18next" import { TokensAndFiat } from "../Asset/TokensAndFiat" @@ -25,7 +24,7 @@ export const SendFundsFeeTooltip = () => {
{t("Max. fee:")}
diff --git a/apps/extension/src/ui/domains/SendFunds/SendFundsHardwareEthereum.tsx b/apps/extension/src/ui/domains/SendFunds/SendFundsHardwareEthereum.tsx index 42a5289f63..6efcd2953c 100644 --- a/apps/extension/src/ui/domains/SendFunds/SendFundsHardwareEthereum.tsx +++ b/apps/extension/src/ui/domains/SendFunds/SendFundsHardwareEthereum.tsx @@ -8,7 +8,7 @@ import { SignHardwareEthereum } from "../Sign/SignHardwareEthereum" import { useSendFunds } from "./useSendFunds" export const SendFundsHardwareEthereum = () => { - const { from, evmTransaction, sendWithSignature, setIsLocked } = useSendFunds() + const { from, evmTransaction, sendWithSignature, setIsLocked, evmNetwork } = useSendFunds() const account = useAccountByAddress(from) as AccountJsonDcent const [error, setError] = useState() @@ -29,6 +29,7 @@ export const SendFundsHardwareEthereum = () => { return ( = ({ tx }) => { ) } -const useStatusDetails = (tx: WalletTransaction) => { +const useStatusDetails = (tx?: WalletTransaction) => { const { t } = useTranslation("send-funds") const { title, subtitle, extra, animStatus } = useMemo<{ title: string @@ -81,11 +80,19 @@ const useStatusDetails = (tx: WalletTransaction) => { animStatus: ProcessAnimationStatus extra?: string }>(() => { + // missing tx can occur while loading + if (!tx) + return { + title: "", + subtitle: "", + animStatus: "processing", + } + const isReplacementCancel = tx.networkType === "evm" && tx.isReplacement && tx.unsigned.value && - ethers.BigNumber.from(tx.unsigned.value).isZero() + BigInt(tx.unsigned.value) === 0n switch (tx.status) { case "unknown": @@ -138,7 +145,7 @@ const useStatusDetails = (tx: WalletTransaction) => { } type SendFundsProgressBaseProps = { - tx: WalletTransaction + tx?: WalletTransaction className?: string blockNumber?: string onClose?: () => void @@ -184,7 +191,7 @@ const SendFundsProgressBase: FC = ({ extra )}
- {tx.status === "pending" && } + {tx?.status === "pending" && } ) : ( @@ -216,16 +259,7 @@ const SignLedgerEthereum: FC = ({ {t("Cancel")} )} - {error && ( - - {/* Shouldn't be a LedgerSigningStatus, just an error message */} - - - )} + ) } diff --git a/apps/extension/src/ui/domains/Sign/SignRequestContext/EthereumSignTransactionRequestContext.ts b/apps/extension/src/ui/domains/Sign/SignRequestContext/EthereumSignTransactionRequestContext.ts index 4081bff50f..0ebf994ee5 100644 --- a/apps/extension/src/ui/domains/Sign/SignRequestContext/EthereumSignTransactionRequestContext.ts +++ b/apps/extension/src/ui/domains/Sign/SignRequestContext/EthereumSignTransactionRequestContext.ts @@ -1,4 +1,7 @@ -import { rebuildTransactionRequestNumbers } from "@core/domains/ethereum/helpers" +import { + parseRpcTransactionRequestBase, + serializeTransactionRequest, +} from "@core/domains/ethereum/helpers" import { KnownSigningRequestIdOnly } from "@core/domains/signing/types" import { log } from "@core/log" import { HexString } from "@polkadot/util/types" @@ -15,8 +18,8 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< const signingRequest = useRequest(id) const network = useEvmNetwork(signingRequest?.ethChainId) - const transactionRequest = useMemo( - () => (signingRequest ? rebuildTransactionRequestNumbers(signingRequest.request) : undefined), + const txBase = useMemo( + () => (signingRequest ? parseRpcTransactionRequestBase(signingRequest.request) : undefined), [signingRequest] ) @@ -24,8 +27,8 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< const [isPayloadLocked, setIsPayloadLocked] = useState(false) const { + decodedTx, transaction, - transactionInfo, txDetails, priority, setPriority, @@ -36,7 +39,7 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< gasSettingsByPriority, setCustomSettings, isValid, - } = useEthTransaction(transactionRequest, isPayloadLocked) + } = useEthTransaction(txBase, signingRequest?.ethChainId, isPayloadLocked) const baseRequest = useAnySigningRequest({ currentRequest: signingRequest, @@ -45,7 +48,10 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< }) const approve = useCallback(() => { - return baseRequest && baseRequest.approve(transaction) + if (!baseRequest) throw new Error("Missing base request") + if (!transaction) throw new Error("Missing transaction") + const serialized = serializeTransactionRequest(transaction) + return baseRequest && baseRequest.approve(serialized) }, [baseRequest, transaction]) const approveHardware = useCallback( @@ -53,11 +59,13 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< if (!baseRequest || !transaction || !baseRequest.id) return baseRequest.setStatus.processing("Approving request") try { - await api.ethApproveSignAndSendHardware(baseRequest.id, transaction, signature) + const serialized = serializeTransactionRequest(transaction) + await api.ethApproveSignAndSendHardware(baseRequest.id, serialized, signature) baseRequest.setStatus.success("Approved") } catch (err) { log.error("failed to approve hardware", { err }) baseRequest.setStatus.error((err as Error).message) + setIsPayloadLocked(false) } }, [baseRequest, transaction] @@ -73,8 +81,8 @@ const useEthSignTransactionRequestProvider = ({ id }: KnownSigningRequestIdOnly< errorDetails, network, networkUsage, + decodedTx, transaction, - transactionInfo, approve, approveHardware, isPayloadLocked, diff --git a/apps/extension/src/ui/domains/Sign/ViewDetails/ViewDetailsEth.tsx b/apps/extension/src/ui/domains/Sign/ViewDetails/ViewDetailsEth.tsx index 4dbf0d279d..62b8573b1a 100644 --- a/apps/extension/src/ui/domains/Sign/ViewDetails/ViewDetailsEth.tsx +++ b/apps/extension/src/ui/domains/Sign/ViewDetails/ViewDetailsEth.tsx @@ -10,11 +10,10 @@ import { NetworkUsage } from "@ui/domains/Ethereum/NetworkUsage" import { useAnalytics } from "@ui/hooks/useAnalytics" import useToken from "@ui/hooks/useToken" import { useTokenRates } from "@ui/hooks/useTokenRates" -import { BigNumber, BigNumberish } from "ethers" -import { formatEther, formatUnits } from "ethers/lib/utils" import { FC, PropsWithChildren, ReactNode, useCallback, useEffect, useMemo } from "react" import { useTranslation } from "react-i18next" import { Button, Drawer, PillButton } from "talisman-ui" +import { formatEther, formatGwei } from "viem" import { Message } from "../Message" import { useEthSignTransactionRequest } from "../SignRequestContext" @@ -36,12 +35,12 @@ type ViewDetailsContentProps = { onClose: () => void } -const Gwei: FC<{ value: BigNumberish | null | undefined }> = ({ value }) => { +const Gwei: FC<{ value: bigint | null | undefined }> = ({ value }) => { const { t } = useTranslation("request") return ( <> - {value - ? t("{{value}} GWEI", { value: formatDecimals(formatUnits(value, "gwei")) }) + {value !== null && value !== undefined + ? t("{{value}} GWEI", { value: formatDecimals(formatGwei(value)) }) : t("N/A")} ) @@ -57,20 +56,15 @@ const ViewDetailsContent: FC = ({ onClose }) => { txDetails, priority, transaction, - transactionInfo, + decodedTx, error, errorDetails, } = useEthSignTransactionRequest() const { genericEvent } = useAnalytics() - const txInfo = useMemo(() => { - if (transactionInfo && transactionInfo.contractType !== "unknown") return transactionInfo - return undefined - }, [transactionInfo]) - const nativeToken = useToken(network?.nativeToken?.id) const formatEthValue = useCallback( - (value?: BigNumberish) => { + (value: bigint = 0n) => { return value ? `${formatEther(value)} ${nativeToken?.symbol ?? ""}` : null }, [nativeToken?.symbol] @@ -86,16 +80,8 @@ const ViewDetailsContent: FC = ({ onClose }) => { () => txDetails && nativeToken ? [ - new BalanceFormatter( - BigNumber.from(txDetails?.estimatedFee).toString(), - nativeToken?.decimals, - nativeTokenRates - ), - new BalanceFormatter( - BigNumber.from(txDetails?.maxFee).toString(), - nativeToken?.decimals, - nativeTokenRates - ), + new BalanceFormatter(txDetails.estimatedFee, nativeToken?.decimals, nativeTokenRates), + new BalanceFormatter(txDetails.maxFee, nativeToken?.decimals, nativeTokenRates), ] : [null, null], [nativeToken, nativeTokenRates, txDetails] @@ -126,10 +112,10 @@ const ViewDetailsContent: FC = ({ onClose }) => {
{t("Details")}
- {!!txInfo?.isContractCall && ( + {!!decodedTx?.isContractCall && ( - {txInfo?.contractType - ? `${txInfo?.contractType} : ${txInfo?.contractCall?.name ?? t("N/A")}` + {decodedTx?.contractType + ? `${decodedTx?.contractType} : ${decodedTx?.contractCall?.functionName ?? t("N/A")}` : t("Unknown")} )} @@ -140,11 +126,11 @@ const ViewDetailsContent: FC = ({ onClose }) => { /> - {formatEthValue(request.value)} + {formatEthValue(transaction?.value)} @@ -158,8 +144,13 @@ const ViewDetailsContent: FC = ({ onClose }) => { typeof networkUsage === "number" ? `${Math.round(networkUsage * 100)}%` : t("N/A") } /> - {transaction?.type === 2 && ( + + {transaction?.type === "eip1559" && ( <> + } + /> } @@ -181,7 +172,7 @@ const ViewDetailsContent: FC = ({ onClose }) => { {transaction ? ( - {transaction?.type === 2 ? ( + {transaction?.type === "eip1559" ? ( <> @@ -205,11 +196,7 @@ const ViewDetailsContent: FC = ({ onClose }) => { )} ) : ( diff --git a/apps/extension/src/ui/domains/Transactions/PendingTransactionsDrawer.tsx b/apps/extension/src/ui/domains/Transactions/PendingTransactionsDrawer.tsx index 0950b6d3ce..fab056444d 100644 --- a/apps/extension/src/ui/domains/Transactions/PendingTransactionsDrawer.tsx +++ b/apps/extension/src/ui/domains/Transactions/PendingTransactionsDrawer.tsx @@ -24,7 +24,6 @@ import { useTokenRates } from "@ui/hooks/useTokenRates" import { getTransactionHistoryUrl } from "@ui/util/getTransactionHistoryUrl" import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict" import { useLiveQuery } from "dexie-react-hooks" -import { BigNumber } from "ethers" import sortBy from "lodash/sortBy" import { FC, PropsWithChildren, forwardRef, useCallback, useEffect, useMemo, useState } from "react" import { Trans, useTranslation } from "react-i18next" @@ -348,10 +347,7 @@ const TransactionRowEvm: FC = ({ const [isCtxMenuOpen, setIsCtxMenuOpen] = useState(false) const amount = useMemo( - () => - token && value - ? new BalanceFormatter(BigNumber.from(value).toBigInt(), token.decimals, tokenRates) - : null, + () => (token && value ? new BalanceFormatter(value, token.decimals, tokenRates) : null), [token, tokenRates, value] ) diff --git a/apps/extension/src/ui/domains/Transactions/TxReplaceDrawer.tsx b/apps/extension/src/ui/domains/Transactions/TxReplaceDrawer.tsx index db7c2d2f0f..16b4a0a434 100644 --- a/apps/extension/src/ui/domains/Transactions/TxReplaceDrawer.tsx +++ b/apps/extension/src/ui/domains/Transactions/TxReplaceDrawer.tsx @@ -1,3 +1,4 @@ +import { serializeTransactionRequest } from "@core/domains/ethereum/helpers" import { EthTransactionDetails } from "@core/domains/signing/types" import { EvmWalletTransaction, WalletTransaction } from "@core/domains/transactions/types" import { HexString } from "@polkadot/util/types" @@ -11,8 +12,6 @@ import { useAccountByAddress } from "@ui/hooks/useAccountByAddress" import { useAnalyticsPageView } from "@ui/hooks/useAnalyticsPageView" import { useBalance } from "@ui/hooks/useBalance" import { useEvmNetwork } from "@ui/hooks/useEvmNetwork" -import { BigNumber } from "ethers" -import { ethers } from "ethers" import { FC, useCallback, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { Button, Drawer, useOpenCloseWithData } from "talisman-ui" @@ -56,21 +55,13 @@ export const EvmEstimatedFeeTooltip: FC<{
{t("Estimated fee:")}
- +
{!!txDetails?.maxFee && ( <>
{t("Max. fee:")}
- +
)} @@ -125,7 +116,7 @@ const EvmDrawerContent: FC<{ networkUsage, isLoading, isValid, - } = useEthReplaceTransaction(tx.unsigned, type, isLocked) + } = useEthReplaceTransaction(tx.unsigned, tx.evmNetworkId, type, isLocked) const account = useAccountByAddress(tx.account) @@ -136,11 +127,12 @@ const EvmDrawerContent: FC<{ setIsProcessing(true) try { const transferInfo = getTransferInfo(tx) - const newHash = await api.ethSignAndSend(transaction, transferInfo) + const serialized = serializeTransactionRequest(transaction) + const newHash = await api.ethSignAndSend(tx.evmNetworkId, serialized, transferInfo) api.analyticsCapture({ eventName: `transaction ${type}`, options: { - chainId: transaction.chainId, + chainId: Number(tx.evmNetworkId), networkType: "ethereum", }, }) @@ -166,11 +158,17 @@ const EvmDrawerContent: FC<{ setIsProcessing(true) try { const transferInfo = getTransferInfo(tx) - const newHash = await api.ethSendSigned(transaction, signature, transferInfo) + const serialized = serializeTransactionRequest(transaction) + const newHash = await api.ethSendSigned( + tx.evmNetworkId, + serialized, + signature, + transferInfo + ) api.analyticsCapture({ eventName: `transaction ${type}`, options: { - chainId: transaction.chainId, + chainId: Number(tx.evmNetworkId), networkType: "ethereum", }, }) @@ -259,7 +257,7 @@ const EvmDrawerContent: FC<{
{txDetails?.estimatedFee ? ( ) : null} diff --git a/apps/extension/src/ui/hooks/useErc20TokenInfo.ts b/apps/extension/src/ui/hooks/useErc20TokenInfo.ts index 42954cc471..cbdb9ef8fc 100644 --- a/apps/extension/src/ui/hooks/useErc20TokenInfo.ts +++ b/apps/extension/src/ui/hooks/useErc20TokenInfo.ts @@ -1,26 +1,27 @@ +import { EvmAddress } from "@core/domains/ethereum/types" import { CustomErc20TokenCreate } from "@core/domains/tokens/types" import { getErc20TokenInfo } from "@core/util/getErc20TokenInfo" import { EvmNetworkId } from "@talismn/chaindata-provider" -import { useEthereumProvider } from "@ui/domains/Ethereum/useEthereumProvider" +import { usePublicClient } from "@ui/domains/Ethereum/usePublicClient" import { useEffect, useState } from "react" -export const useErc20TokenInfo = (evmNetworkId?: EvmNetworkId, contractAddress?: string) => { +export const useErc20TokenInfo = (evmNetworkId?: EvmNetworkId, contractAddress?: EvmAddress) => { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState() const [token, setToken] = useState() - const provider = useEthereumProvider(evmNetworkId) + const publicClient = usePublicClient(evmNetworkId) useEffect(() => { setError(undefined) setToken(undefined) - if (!evmNetworkId || !provider || !contractAddress) return + if (!evmNetworkId || !publicClient || !contractAddress) return setIsLoading(true) - getErc20TokenInfo(provider, evmNetworkId, contractAddress) + getErc20TokenInfo(publicClient, evmNetworkId, contractAddress) .then(setToken) .catch(setError) .finally(() => setIsLoading(false)) - }, [contractAddress, evmNetworkId, provider]) + }, [contractAddress, evmNetworkId, publicClient]) return { isLoading, error, token } } diff --git a/apps/extension/webpack/webpack.common.js b/apps/extension/webpack/webpack.common.js index 540ab8cacf..869f680793 100644 --- a/apps/extension/webpack/webpack.common.js +++ b/apps/extension/webpack/webpack.common.js @@ -28,10 +28,6 @@ const config = (env) => ({ "@substrate/txwrapper-core", "@talismn/chaindata-provider-extension", "@metamask/eth-sig-util", - "@acala-network/types", - "@acala-network/eth-providers", - "@acala-network/eth-transactions", - "@acala-network/api-derive", ], // Wallet injected scripts diff --git a/apps/playground/src/components/Ethereum/shared/talismanChains.ts b/apps/playground/src/components/Ethereum/shared/talismanChains.ts index 63b725c4a9..6e04d7a49d 100644 --- a/apps/playground/src/components/Ethereum/shared/talismanChains.ts +++ b/apps/playground/src/components/Ethereum/shared/talismanChains.ts @@ -158,6 +158,51 @@ const shiden: Chain = { }, } +const mandalaTestnet: Chain = { + id: 595, + name: "Mandala Testnet", + network: "Mandala Testnet", + nativeCurrency: { + name: "ACA", + symbol: "ACA", + decimals: 18, + }, + rpcUrls: { + default: { http: ["https://mandala-rpc.aca-staging.network"] }, + public: { http: ["https://mandala-rpc.aca-staging.network"] }, + }, +} + +const acala: Chain = { + id: 787, + name: "Acala EVM+", + network: "Acala Acala EVM+", + nativeCurrency: { + name: "ACA", + symbol: "ACA", + decimals: 18, + }, + rpcUrls: { + default: { http: ["https://rpc.evm.acala.network"] }, + public: { http: ["https://rpc.evm.acala.network"] }, + }, +} + +const karura: Chain = { + id: 686, + name: "Karura EVM+", + network: "Karura EVM+", + nativeCurrency: { + name: "KAR", + symbol: "KAR", + decimals: 18, + }, + rpcUrls: { + default: { http: ["https://eth-rpc-karura.aca-api.network"] }, + public: { http: ["https://eth-rpc-karura.aca-api.network"] }, + }, +} + export const talismanChains: Chain[] = [ moonbeam, moonbase, @@ -177,4 +222,7 @@ export const talismanChains: Chain[] = [ optimismGoerli, polygon, polygonMumbai, + mandalaTestnet, + karura, + acala, ].sort((a, b) => a.name.localeCompare(b.name)) diff --git a/packages/balances-evm-erc20/package.json b/packages/balances-evm-erc20/package.json index 3f5c44aa7f..8a2b743ded 100644 --- a/packages/balances-evm-erc20/package.json +++ b/packages/balances-evm-erc20/package.json @@ -30,8 +30,8 @@ "@talismn/chaindata-provider": "workspace:*", "@talismn/util": "workspace:*", "anylogger": "^1.0.11", - "ethers": "5.7.2", - "lodash": "4.17.21" + "lodash": "4.17.21", + "viem": "^1.18.9" }, "devDependencies": { "@polkadot/util": "^11.1.1", diff --git a/packages/balances-evm-erc20/src/EvmErc20Module.ts b/packages/balances-evm-erc20/src/EvmErc20Module.ts index 9bd56fbe59..d2f859becf 100644 --- a/packages/balances-evm-erc20/src/EvmErc20Module.ts +++ b/packages/balances-evm-erc20/src/EvmErc20Module.ts @@ -1,6 +1,5 @@ import { assert } from "@polkadot/util" import { - Address, AddressesByToken, Amount, Balance, @@ -18,10 +17,10 @@ import { githubTokenLogoUrl, } from "@talismn/chaindata-provider" import { hasOwnProperty, isEthereumAddress } from "@talismn/util" -import { ethers } from "ethers" import isEqual from "lodash/isEqual" +import { PublicClient, getContract } from "viem" -import erc20Abi from "./erc20.json" +import { erc20Abi } from "./erc20Abi" import log from "./log" export { erc20Abi } @@ -36,7 +35,7 @@ export const evmErc20TokenId = ( export type EvmErc20Token = NewTokenType< ModuleType, { - contractAddress: string + contractAddress: `0x${string}` evmNetwork: { id: EvmNetworkId } | null } > @@ -61,7 +60,7 @@ export type EvmErc20ModuleConfig = { symbol?: string decimals?: number coingeckoId?: string - contractAddress?: string + contractAddress?: `0x${string}` }> } @@ -116,16 +115,17 @@ export const EvmErc20Module: NewBalanceModule< if (!contractAddress) continue const [contractSymbol, contractDecimals] = await (async () => { - const evmNetwork = await chaindataProvider.getEvmNetwork(chainId) - if (!evmNetwork) return [] + const publicClient = await chainConnector.getPublicClientForEvmNetwork(chainId) + if (!publicClient) return [] - const provider = await chainConnector.getProviderForEvmNetwork(evmNetwork) - if (!provider) return [] - - const contract = new ethers.Contract(contractAddress, erc20Abi, provider) + const contract = getContract({ + abi: erc20Abi, + address: contractAddress as `0x${string}`, + publicClient, + }) try { - return [await contract.symbol(), await contract.decimals()] + return Promise.all([contract.read.symbol(), contract.read.decimals()]) } catch (error) { log.error(`Failed to retrieve contract symbol and decimals`, String(error)) return [] @@ -245,10 +245,8 @@ export const EvmErc20Module: NewBalanceModule< const evmNetwork = evmNetworks[evmNetworkId] if (!evmNetwork) throw new Error(`Evm network ${evmNetworkId} not found`) - const provider = await chainConnectors.evm.getProviderForEvmNetwork(evmNetwork, { - batch: true, - }) - if (!provider) + const publicClient = await chainConnector.getPublicClientForEvmNetwork(evmNetworkId) + if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`) const tokensAndAddresses = Object.entries(addressesByToken).reduce( @@ -277,8 +275,6 @@ export const EvmErc20Module: NewBalanceModule< // fetch all balances const balanceRequests = tokensAndAddresses.flatMap(([token, addresses]) => { - const contract = new ethers.Contract(token.contractAddress, erc20Abi, provider) - return addresses.map( async (address) => new Balance({ @@ -291,7 +287,11 @@ export const EvmErc20Module: NewBalanceModule< evmNetworkId, tokenId: token.id, - free: await getFreeBalance(contract, address), + free: await getFreeBalance( + publicClient, + token.contractAddress as `0x${string}`, + address as `0x${string}` + ), }) ) }) @@ -354,18 +354,33 @@ function groupAddressesByTokenByEvmNetwork( }, {} as Record>) } -async function getFreeBalance(contract: ethers.Contract, address: Address): Promise { - if (!isEthereumAddress(address)) return "0" +async function getFreeBalance( + publicClient: PublicClient, + contractAddress: `0x${string}`, + accountAddress: `0x${string}` +): Promise { + if (!isEthereumAddress(accountAddress)) return "0" try { - return ((await contract.balanceOf(address)).toBigInt() ?? 0n).toString() + const res = await publicClient.readContract({ + abi: erc20Abi, + address: contractAddress, + functionName: "balanceOf", + args: [accountAddress], + }) + + return res.toString() } catch (error) { - const errorMessage = hasOwnProperty(error, "message") ? error.message : error + const errorMessage = hasOwnProperty(error, "shortMessage") + ? error.shortMessage + : hasOwnProperty(error, "message") + ? error.message + : error log.warn( - `Failed to get balance from contract ${contract.address} for address ${address}: ${errorMessage}` + `Failed to get balance from contract ${contractAddress} (chain ${publicClient.chain?.id}) for address ${accountAddress}: ${errorMessage}` ) throw new Error( - `Failed to get balance from contract ${contract.address} for address ${address}`, + `Failed to get balance from contract ${contractAddress} (chain ${publicClient.chain?.id}) for address ${accountAddress}`, { cause: error as Error } ) } diff --git a/packages/balances-evm-erc20/src/erc20.json b/packages/balances-evm-erc20/src/erc20.json deleted file mode 100644 index be7f18d343..0000000000 --- a/packages/balances-evm-erc20/src/erc20.json +++ /dev/null @@ -1,155 +0,0 @@ -[ - { - "constant": true, - "inputs": [], - "name": "name", - "outputs": [{ "name": "", "type": "string" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_spender", "type": "address" }, - { "name": "_value", "type": "uint256" } - ], - "name": "approve", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "totalSupply", - "outputs": [{ "name": "", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_from", "type": "address" }, - { "name": "_to", "type": "address" }, - { "name": "_value", "type": "uint256" } - ], - "name": "transferFrom", - "outputs": [{ "name": "", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "decimals", - "outputs": [{ "name": "", "type": "uint8" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_to", "type": "address" }, - { "name": "_value", "type": "uint256" }, - { "name": "_data", "type": "bytes" } - ], - "name": "transferAndCall", - "outputs": [{ "name": "success", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_spender", "type": "address" }, - { "name": "_subtractedValue", "type": "uint256" } - ], - "name": "decreaseApproval", - "outputs": [{ "name": "success", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [{ "name": "_owner", "type": "address" }], - "name": "balanceOf", - "outputs": [{ "name": "balance", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "symbol", - "outputs": [{ "name": "", "type": "string" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_to", "type": "address" }, - { "name": "_value", "type": "uint256" } - ], - "name": "transfer", - "outputs": [{ "name": "success", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { "name": "_spender", "type": "address" }, - { "name": "_addedValue", "type": "uint256" } - ], - "name": "increaseApproval", - "outputs": [{ "name": "success", "type": "bool" }], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { "name": "_owner", "type": "address" }, - { "name": "_spender", "type": "address" } - ], - "name": "allowance", - "outputs": [{ "name": "remaining", "type": "uint256" }], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { "inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "from", "type": "address" }, - { "indexed": true, "name": "to", "type": "address" }, - { "indexed": false, "name": "value", "type": "uint256" }, - { "indexed": false, "name": "data", "type": "bytes" } - ], - "name": "Transfer", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "name": "owner", "type": "address" }, - { "indexed": true, "name": "spender", "type": "address" }, - { "indexed": false, "name": "value", "type": "uint256" } - ], - "name": "Approval", - "type": "event" - } -] diff --git a/packages/balances-evm-erc20/src/erc20Abi.ts b/packages/balances-evm-erc20/src/erc20Abi.ts new file mode 100644 index 0000000000..d31a33f711 --- /dev/null +++ b/packages/balances-evm-erc20/src/erc20Abi.ts @@ -0,0 +1,155 @@ +export const erc20Abi = [ + { + constant: true, + inputs: [], + name: "name", + outputs: [{ name: "", type: "string" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { name: "_spender", type: "address" }, + { name: "_value", type: "uint256" }, + ], + name: "approve", + outputs: [{ name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "totalSupply", + outputs: [{ name: "", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { name: "_from", type: "address" }, + { name: "_to", type: "address" }, + { name: "_value", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ name: "", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "decimals", + outputs: [{ name: "", type: "uint8" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { name: "_to", type: "address" }, + { name: "_value", type: "uint256" }, + { name: "_data", type: "bytes" }, + ], + name: "transferAndCall", + outputs: [{ name: "success", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [ + { name: "_spender", type: "address" }, + { name: "_subtractedValue", type: "uint256" }, + ], + name: "decreaseApproval", + outputs: [{ name: "success", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [{ name: "_owner", type: "address" }], + name: "balanceOf", + outputs: [{ name: "balance", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [], + name: "symbol", + outputs: [{ name: "", type: "string" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { name: "_to", type: "address" }, + { name: "_value", type: "uint256" }, + ], + name: "transfer", + outputs: [{ name: "success", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: false, + inputs: [ + { name: "_spender", type: "address" }, + { name: "_addedValue", type: "uint256" }, + ], + name: "increaseApproval", + outputs: [{ name: "success", type: "bool" }], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [ + { name: "_owner", type: "address" }, + { name: "_spender", type: "address" }, + ], + name: "allowance", + outputs: [{ name: "remaining", type: "uint256" }], + payable: false, + stateMutability: "view", + type: "function", + }, + { inputs: [], payable: false, stateMutability: "nonpayable", type: "constructor" }, + { + anonymous: false, + inputs: [ + { indexed: true, name: "from", type: "address" }, + { indexed: true, name: "to", type: "address" }, + { indexed: false, name: "value", type: "uint256" }, + { indexed: false, name: "data", type: "bytes" }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, name: "owner", type: "address" }, + { indexed: true, name: "spender", type: "address" }, + { indexed: false, name: "value", type: "uint256" }, + ], + name: "Approval", + type: "event", + }, +] as const diff --git a/packages/balances-evm-native/package.json b/packages/balances-evm-native/package.json index 91f8ce6e5c..98e1d778c4 100644 --- a/packages/balances-evm-native/package.json +++ b/packages/balances-evm-native/package.json @@ -30,8 +30,8 @@ "@talismn/chaindata-provider": "workspace:*", "@talismn/util": "workspace:*", "anylogger": "^1.0.11", - "ethers": "5.7.2", - "lodash": "4.17.21" + "lodash": "4.17.21", + "viem": "^1.18.9" }, "devDependencies": { "@talismn/eslint-config": "workspace:*", diff --git a/packages/balances-evm-native/src/EvmNativeModule.ts b/packages/balances-evm-native/src/EvmNativeModule.ts index 9447745f44..b1ea4cc99a 100644 --- a/packages/balances-evm-native/src/EvmNativeModule.ts +++ b/packages/balances-evm-native/src/EvmNativeModule.ts @@ -15,9 +15,10 @@ import { githubTokenLogoUrl, } from "@talismn/chaindata-provider" import { hasOwnProperty, isEthereumAddress } from "@talismn/util" -import { ethers } from "ethers" import isEqual from "lodash/isEqual" +import { PublicClient } from "viem" +import { abiMulticall } from "./abi/multicall" import log from "./log" type ModuleType = "evm-native" @@ -193,16 +194,21 @@ export const EvmNativeModule: NewBalanceModule< const evmNetwork = evmNetworks[evmNetworkId] if (!evmNetwork) throw new Error(`Evm network ${evmNetworkId} not found`) - const provider = await chainConnectors.evm.getProviderForEvmNetwork(evmNetwork, { - batch: true, - }) - if (!provider) + const publicClient = await chainConnectors.evm.getPublicClientForEvmNetwork( + evmNetworkId + ) + + if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`) // fetch all balances - const balanceRequests = addresses.map( - async (address) => - new Balance({ + const freeBalances = await getFreeBalances(publicClient, addresses) + + const balanceResults = addresses + .map((address, i) => { + if (freeBalances[i] === "error") return false + + return new Balance({ source: "evm-native", status: "live", @@ -212,26 +218,13 @@ export const EvmNativeModule: NewBalanceModule< evmNetworkId, tokenId, - free: await getFreeBalance(provider, address), + free: freeBalances[i].toString(), }) - ) - - // wait for balance fetches to complete - const balanceResults = await Promise.allSettled(balanceRequests) - - // filter out errors - const balances = balanceResults - .map((result) => { - if (result.status === "rejected") { - log.debug(result.reason) - return false - } - return result.value }) .filter((balance): balance is Balance => balance !== false) // return to caller - return new Balances(balances) + return new Balances(balanceResults) }) ) ) @@ -250,21 +243,78 @@ export const EvmNativeModule: NewBalanceModule< } async function getFreeBalance( - provider: ethers.providers.JsonRpcProvider, + publicClient: PublicClient, address: Address -): Promise { - if (!isEthereumAddress(address)) return "0" +): Promise { + if (!isEthereumAddress(address)) return 0n try { - return ((await provider.getBalance(address)).toBigInt() ?? 0n).toString() + return await publicClient.getBalance({ address }) } catch (error) { - const errorMessage = hasOwnProperty(error, "message") ? error.message : error + const errorMessage = hasOwnProperty(error, "shortMessage") + ? error.shortMessage + : hasOwnProperty(error, "message") + ? error.message + : error log.warn( - `Failed to get balance from chain ${provider.network.chainId} for address ${address}: ${errorMessage}` - ) - throw new Error( - `Failed to get balance from chain ${provider.network.chainId} for address ${address}`, - { cause: error as Error } + `Failed to get balance from chain ${publicClient.chain?.id} for address ${address}: ${errorMessage}` ) + return "error" + } +} + +async function getFreeBalances( + publicClient: PublicClient, + addresses: Address[] +): Promise<(bigint | "error")[]> { + // if multicall is available, use it to save RPC rate limits + if (publicClient.batch?.multicall && publicClient.chain?.contracts?.multicall3?.address) { + try { + const ethAddresses = addresses.filter(isEthereumAddress) + + const addressMulticall = publicClient.chain.contracts.multicall3.address + + const callResults = await publicClient.multicall({ + contracts: ethAddresses.map((address) => ({ + address: addressMulticall, + abi: abiMulticall, + functionName: "getEthBalance", + args: [address], + })), + }) + + const ethBalanceResults = Object.fromEntries( + ethAddresses.map((address, i) => { + const { error } = callResults[i] + if (error) { + const errorMessage = hasOwnProperty(error, "shortMessage") + ? error.shortMessage + : hasOwnProperty(error, "message") + ? error.message + : error + log.warn( + `Failed to get balance from chain ${publicClient.chain?.id} for address ${address}: ${errorMessage}` + ) + } + + return [address, callResults[i].result ?? ("error" as const)] + }) + ) + + // default to 0 for non evm addresses + return addresses.map((address) => ethBalanceResults[address] ?? 0n) + } catch (err) { + const errorMessage = hasOwnProperty(err, "shortMessage") + ? err.shortMessage + : hasOwnProperty(err, "message") + ? err.message + : err + log.warn( + `Failed to get balance from chain ${publicClient.chain?.id} for ${addresses.length} addresses: ${errorMessage}` + ) + return addresses.map(() => "error") + } } + + return Promise.all(addresses.map((address) => getFreeBalance(publicClient, address))) } diff --git a/packages/balances-evm-native/src/abi/multicall.ts b/packages/balances-evm-native/src/abi/multicall.ts new file mode 100644 index 0000000000..756f5acc6d --- /dev/null +++ b/packages/balances-evm-native/src/abi/multicall.ts @@ -0,0 +1,24 @@ +import { parseAbi } from "viem" + +export const abiMulticall = parseAbi([ + "struct Call { address target; bytes callData; }", + "struct Call3 { address target; bool allowFailure; bytes callData; }", + "struct Call3Value { address target; bool allowFailure; uint256 value; bytes callData; }", + "struct Result { bool success; bytes returnData; }", + "function aggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes[] memory returnData)", + "function aggregate3(Call3[] calldata calls) public payable returns (Result[] memory returnData)", + "function aggregate3Value(Call3Value[] calldata calls) public payable returns (Result[] memory returnData)", + "function blockAndAggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData)", + "function getBasefee() view returns (uint256 basefee)", + "function getBlockHash(uint256 blockNumber) view returns (bytes32 blockHash)", + "function getBlockNumber() view returns (uint256 blockNumber)", + "function getChainId() view returns (uint256 chainid)", + "function getCurrentBlockCoinbase() view returns (address coinbase)", + "function getCurrentBlockDifficulty() view returns (uint256 difficulty)", + "function getCurrentBlockGasLimit() view returns (uint256 gaslimit)", + "function getCurrentBlockTimestamp() view returns (uint256 timestamp)", + "function getEthBalance(address addr) view returns (uint256 balance)", + "function getLastBlockHash() view returns (bytes32 blockHash)", + "function tryAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (Result[] memory returnData)", + "function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData)", +] as const) diff --git a/packages/balances-substrate-equilibrium/src/SubstrateEquilibriumModule.ts b/packages/balances-substrate-equilibrium/src/SubstrateEquilibriumModule.ts index b78311b68f..2ef3ce20c1 100644 --- a/packages/balances-substrate-equilibrium/src/SubstrateEquilibriumModule.ts +++ b/packages/balances-substrate-equilibrium/src/SubstrateEquilibriumModule.ts @@ -30,7 +30,7 @@ import { metadataIsV14, mutateMetadata, } from "@talismn/mutate-metadata" -import { decodeAnyAddress } from "@talismn/util" +import { decodeAnyAddress, isBigInt } from "@talismn/util" import log from "./log" @@ -446,7 +446,7 @@ const DEFAULT_DECIMALS = 9 const tokenSymbolFromU64Id = (u64: number | bigint | AbstractInt) => { const bytes = [] - let num = typeof u64 === "number" ? BigInt(u64) : typeof u64 === "bigint" ? u64 : u64.toBigInt() + let num = typeof u64 === "number" ? BigInt(u64) : isBigInt(u64) ? u64 : u64.toBigInt() do { bytes.unshift(Number(num % 256n)) num = num / 256n diff --git a/packages/balances-substrate-psp22/src/SubstratePsp22Module.ts b/packages/balances-substrate-psp22/src/SubstratePsp22Module.ts index 67c8111be8..95815b7f7a 100644 --- a/packages/balances-substrate-psp22/src/SubstratePsp22Module.ts +++ b/packages/balances-substrate-psp22/src/SubstratePsp22Module.ts @@ -267,7 +267,7 @@ export const SubPsp22Module: NewBalanceModule< }) if (token.contractAddress === undefined) { - log.debug(`Token ${tokenId} of type ${token.type} doesn't have a contractAddress`) + log.debug(`Token ${tokenId} of type substrate-psp22 doesn't have a contractAddress`) return [] } diff --git a/packages/balances/src/BalanceModule.ts b/packages/balances/src/BalanceModule.ts index 101887b4b4..fb46b742b2 100644 --- a/packages/balances/src/BalanceModule.ts +++ b/packages/balances/src/BalanceModule.ts @@ -2,7 +2,6 @@ import { UnsignedTransaction } from "@substrate/txwrapper-core" import { ChainConnector } from "@talismn/chain-connector" import { ChainConnectorEvm } from "@talismn/chain-connector-evm" import { ChainId, ChaindataProvider, IToken } from "@talismn/chaindata-provider" -import { ethers } from "ethers" import { AddressesByToken, Balances, SubscriptionCallback, UnsubscribeFn } from "./types" @@ -17,9 +16,7 @@ export type DefaultTransferParams = undefined export type NewTransferParamsType> = BaseTransferParams & T -export type TransferTokenTx = - | { type: "substrate"; tx: UnsignedTransaction } - | { type: "evm"; tx: ethers.providers.TransactionRequest } +export type TransferTokenTx = { type: "substrate"; tx: UnsignedTransaction } export type ChainConnectors = { substrate?: ChainConnector; evm?: ChainConnectorEvm } export type Hydrate = { diff --git a/packages/balances/src/types/balances.ts b/packages/balances/src/types/balances.ts index d9bed0ff76..fac6fc97d1 100644 --- a/packages/balances/src/types/balances.ts +++ b/packages/balances/src/types/balances.ts @@ -1,6 +1,6 @@ import { ChainList, EvmNetworkList, TokenList } from "@talismn/chaindata-provider" import { TokenRateCurrency, TokenRates, TokenRatesList } from "@talismn/token-rates" -import { BigMath, NonFunctionProperties, isArrayOf, planckToTokens } from "@talismn/util" +import { BigMath, NonFunctionProperties, isArrayOf, isBigInt, planckToTokens } from "@talismn/util" import { filterMirrorTokens } from "../helpers" import log from "../log" @@ -301,7 +301,7 @@ export class Balance { #format = (balance: bigint | string) => new BalanceFormatter( - typeof balance === "bigint" ? balance.toString() : balance, + isBigInt(balance) ? balance.toString() : balance, this.decimals || undefined, this.#db?.tokenRates && this.#db.tokenRates[this.tokenId] ) @@ -457,7 +457,7 @@ export class BalanceFormatter { decimals?: number | undefined, fiatRatios?: TokenRates ) { - this.#planck = typeof planck === "bigint" ? planck.toString() : planck ?? "0" + this.#planck = isBigInt(planck) ? planck.toString() : planck ?? "0" this.#decimals = decimals || 0 this.#fiatRatios = fiatRatios || null } diff --git a/packages/chain-connector-evm/package.json b/packages/chain-connector-evm/package.json index 5e9f3c4180..ca4d2f1bd2 100644 --- a/packages/chain-connector-evm/package.json +++ b/packages/chain-connector-evm/package.json @@ -26,13 +26,11 @@ "clean": "rm -rf dist && rm -rf .turbo rm -rf node_modules" }, "dependencies": { - "@acala-network/api": "^6.0.0", - "@acala-network/eth-providers": "^2.7.8", "@talismn/chaindata-provider": "workspace:*", "@talismn/util": "workspace:*", "anylogger": "^1.0.11", - "ethers": "5.7.2", - "lodash": "4.17.21" + "lodash": "4.17.21", + "viem": "^1.18.9" }, "devDependencies": { "@talismn/eslint-config": "workspace:*", diff --git a/packages/chain-connector-evm/src/ChainConnectorEvm.ts b/packages/chain-connector-evm/src/ChainConnectorEvm.ts index 4f01b84a2d..56c4141a72 100644 --- a/packages/chain-connector-evm/src/ChainConnectorEvm.ts +++ b/packages/chain-connector-evm/src/ChainConnectorEvm.ts @@ -1,27 +1,9 @@ -import { CustomEvmNetwork, EvmNetwork, EvmNetworkId } from "@talismn/chaindata-provider" +import { ChaindataTokenProvider, EvmNetworkId } from "@talismn/chaindata-provider" import { ChaindataEvmNetworkProvider } from "@talismn/chaindata-provider" -import { ethers } from "ethers" +import { Account, PublicClient, WalletClient } from "viem" -import { RPC_CALL_TIMEOUT } from "./constants" -import log from "./log" -import { - AcalaRpcProvider, - BatchRpcProvider, - StandardRpcProvider, - addOnfinalityApiKey, - getHealthyRpc, - isAcalaNetwork, -} from "./util" - -export type GetProviderOptions = { - /** If true, returns a provider which will batch requests */ - batch?: boolean -} - -const getEvmNetworkProviderCacheKey = (evmNetworkId: EvmNetworkId, batch?: boolean) => - `id-${evmNetworkId}-${batch ? "batch" : "standard"}` -const getUrlProviderCacheKey = (url: string, batch?: boolean) => - `url-${url}-${batch ? "batch" : "standard"}` +import { clearPublicClientCache, getEvmNetworkPublicClient } from "./getEvmNetworkPublicClient" +import { getEvmNetworkWalletClient } from "./getEvmNetworkWalletClient" export type ChainConnectorEvmOptions = { onfinalityApiKey?: string @@ -29,159 +11,53 @@ export type ChainConnectorEvmOptions = { export class ChainConnectorEvm { #chaindataEvmNetworkProvider: ChaindataEvmNetworkProvider + #chaindataTokenProvider: ChaindataTokenProvider #onfinalityApiKey?: string - // cache for providers - // - // per network providers will change over time if they get unhealthy - // per url providers do not change over time - // - // the cached object is a promise which will return the provider - // if we didn't store the promise, multiple rpc calls sent before an rpc provider is ready - // would end up in us creating multiple rpc providers, instead of what we want which is - // to wait for the single provider to spin up - #providerCache: Map< - EvmNetworkId | string, - Promise - > = new Map() - - // cache for rpc urls per network - // - // always initialized with the order defined in the database - // when an error is raised, push the current rpc to the back of the list - #rpcUrlsCache: Map = new Map() - constructor( chaindataEvmNetworkProvider: ChaindataEvmNetworkProvider, + chaindataTokenProvider: ChaindataTokenProvider, options?: ChainConnectorEvmOptions ) { this.#chaindataEvmNetworkProvider = chaindataEvmNetworkProvider + this.#chaindataTokenProvider = chaindataTokenProvider this.#onfinalityApiKey = options?.onfinalityApiKey ?? undefined } - setOnfinalityApiKey(apiKey: string | undefined) { + public setOnfinalityApiKey(apiKey: string | undefined) { this.#onfinalityApiKey = apiKey this.clearRpcProvidersCache() } - async getProviderForEvmNetworkId( - evmNetworkId: EvmNetworkId, - { batch }: GetProviderOptions = {} - ): Promise { + public async getPublicClientForEvmNetwork( + evmNetworkId: EvmNetworkId + ): Promise { const network = await this.#chaindataEvmNetworkProvider.getEvmNetwork(evmNetworkId) - if (!network) return null + if (!network?.nativeToken?.id) return null + const nativeToken = await this.#chaindataTokenProvider.getToken(network.nativeToken.id) + if (!nativeToken) return null - return await this.getProviderForEvmNetwork(network, { batch }) + return getEvmNetworkPublicClient(network, nativeToken, { + onFinalityApiKey: this.#onfinalityApiKey, + }) } - async getProviderForEvmNetwork( - evmNetwork: EvmNetwork | CustomEvmNetwork, - { batch }: GetProviderOptions = {} - ): Promise { - const cacheKey = getEvmNetworkProviderCacheKey(evmNetwork.id, batch) - - // By using `Promise.race`, this variable will immediately resolve to either the - // value of the promise at `this.#providerCache.get(cacheKey)`, - // or if it's still pending, then it will resolve to `undefined` - const cached = await Promise.race([this.#providerCache.get(cacheKey), undefined]) - - const createNewProvider = - // Check if #providerCache has no pending provider for this key - if so, we should attempt to create a new provider - !this.#providerCache.has(cacheKey) || - // Check if #providerCache has already resolved to `null` - if so, we should attempt to create a new provider - cached === null - - if (createNewProvider) { - // store the promise straight away - // otherwise another call to `getProviderForEvmNetwork` would create a new provider, - // instead of what we want which is to wait for this provider to spin up - this.#providerCache.set(cacheKey, this.newProviderFromEvmNetwork(evmNetwork, { batch })) - } - - return (await this.#providerCache.get(cacheKey)) ?? null - } - - clearRpcProvidersCache(evmNetworkId?: EvmNetworkId, clearRpcUrlsCache = true) { - if (evmNetworkId) { - this.#providerCache.delete(getEvmNetworkProviderCacheKey(evmNetworkId, false)) - this.#providerCache.delete(getEvmNetworkProviderCacheKey(evmNetworkId, true)) - if (clearRpcUrlsCache) this.#rpcUrlsCache.delete(evmNetworkId) - } else { - this.#providerCache.clear() - if (clearRpcUrlsCache) this.#rpcUrlsCache.clear() - } - } - - private rotateRpcUrls(evmNetworkId: EvmNetworkId) { - const prevUrls = this.#rpcUrlsCache.get(evmNetworkId) as string[] - if (!prevUrls || prevUrls.length < 2) return prevUrls - - const nextUrls = prevUrls.slice(1).concat(prevUrls[0]) - this.#rpcUrlsCache.set(evmNetworkId, nextUrls) - - return nextUrls + public async getWalletClientForEvmNetwork( + evmNetworkId: EvmNetworkId, + account?: `0x${string}` | Account + ): Promise { + const network = await this.#chaindataEvmNetworkProvider.getEvmNetwork(evmNetworkId) + if (!network?.nativeToken?.id) return null + const nativeToken = await this.#chaindataTokenProvider.getToken(network.nativeToken.id) + if (!nativeToken) return null + + return getEvmNetworkWalletClient(network, nativeToken, { + onFinalityApiKey: this.#onfinalityApiKey, + account, + }) } - private async newProviderFromEvmNetwork( - evmNetwork: EvmNetwork | CustomEvmNetwork, - { batch }: GetProviderOptions = {} - ): Promise { - if (!Array.isArray(evmNetwork.rpcs)) return null - - const network: ethers.providers.Network = { - name: evmNetwork.name ?? "unknown network", - chainId: parseInt(evmNetwork.id, 10), - } - - // Fixes ENS lookups by adding `ensAddress` to the `network` - which is then passed to the RpcProvider - const ensNetwork = ethers.providers.getNetwork(network.chainId) - if (typeof ensNetwork?.ensAddress === "string") network.ensAddress = ensNetwork.ensAddress - - // initialize cache for rpc urls if empty - if (!this.#rpcUrlsCache.has(evmNetwork.id)) { - const rpcUrls = evmNetwork.rpcs.map(({ url }) => - addOnfinalityApiKey(url, this.#onfinalityApiKey) - ) - this.#rpcUrlsCache.set(evmNetwork.id, rpcUrls) - } - let rpcUrls = this.#rpcUrlsCache.get(evmNetwork.id) as string[] - - const url = await getHealthyRpc(rpcUrls, network) - if (!url) return null - - // if healthy rpc url isn't the first one, rotate rpc urls cache to reflect that - while (rpcUrls.includes(url) && rpcUrls[0] !== url) rpcUrls = this.rotateRpcUrls(evmNetwork.id) - - const urlCacheKey = getUrlProviderCacheKey(url, batch) - if (!this.#providerCache.has(urlCacheKey)) { - const connection: ethers.utils.ConnectionInfo = { - url, - errorPassThrough: true, - timeout: RPC_CALL_TIMEOUT, - - // number of attempts when a request is throttled (429) : default is 12, minimum is 1 - // use 1 so we change RPC as soon as we detect a throttling, without trying again - throttleLimit: 1, - } - - const provider = - batch === true - ? new BatchRpcProvider(connection, network) - : isAcalaNetwork(network.chainId) - ? new AcalaRpcProvider(connection, network) - : new StandardRpcProvider(connection, network) - - // in case an error is thrown, rotate rpc urls cache - // also clear provider cache to force logic going through getHealthyRpc again on next call - provider.on("error", (...args: unknown[]) => { - log.error("EVM RPC error %s (%s)", url, batch ? "batch" : "standard", args) - this.rotateRpcUrls(evmNetwork.id) - this.clearRpcProvidersCache(evmNetwork.id, false) - }) - - this.#providerCache.set(urlCacheKey, Promise.resolve(provider)) - } - - return (await this.#providerCache.get(urlCacheKey)) ?? null + public clearRpcProvidersCache(evmNetworkId?: EvmNetworkId) { + clearPublicClientCache(evmNetworkId) } } diff --git a/packages/chain-connector-evm/src/EvmJsonRpcBatchProvider.ts b/packages/chain-connector-evm/src/EvmJsonRpcBatchProvider.ts deleted file mode 100644 index 3527050155..0000000000 --- a/packages/chain-connector-evm/src/EvmJsonRpcBatchProvider.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ethers } from "ethers" -import debounce from "lodash/debounce" - -type PendingRequest = { - method: string - params: unknown[] - resolve: (value: unknown) => void - reject: (reason?: unknown) => void -} - -type BatchItemResult = { - id: number - result?: unknown - error?: { message: string; code?: unknown; data?: unknown } -} - -const BATCH_SIZE_LIMIT = 50 // requests -const BATCH_MAX_WAIT = 10 // milliseconds - -export class EvmJsonRpcBatchProvider extends ethers.providers.StaticJsonRpcProvider { - private queue: PendingRequest[] = [] - private processQueue = debounce(this.sendBatch, BATCH_MAX_WAIT) - - private sendBatch() { - if (this.queue.length === 0) return - - const batch = this.queue.splice(0, BATCH_SIZE_LIMIT).map((req, index) => ({ - ...(req as Required), - id: index + 1, - jsonrpc: "2.0", - })) - - const payload = batch.map(({ method, params, id, jsonrpc }) => ({ - method, - params, - id, - jsonrpc, - })) - - ethers.utils - .fetchJson(this.connection, JSON.stringify(payload)) - .then((results: BatchItemResult[]) => { - if (!Array.isArray(results)) throw new Error("Invalid batch response") - - // order is not guaranteed, prepare a map to access items by their id - const resultsMap = results.reduce( - (map, result) => map.set(result.id, result), - new Map() - ) - batch.forEach((request) => { - const batchItem = resultsMap.get(request.id) - if (!batchItem) return request.reject(new Error("Missing batch item")) - - const { result, error } = batchItem - if (error) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const err: any = new Error(error.message) - err.code = error.code - err.data = error.data - request.reject(err) - } else { - request.resolve(result) - } - }) - }) - .catch((err) => { - // emit error to cycle fallback provider - this.emit("error", err) - - // reject all requests using a custom error message that will prevent more fallback cycling - batch.forEach((request) => { - request.reject(new Error("BATCH_FAILED", { cause: err })) - }) - }) - } - - send(method: string, params: unknown[]): Promise { - const request: Partial = { method, params } - - const result = new Promise((resolve, reject) => { - // will be resolved/rejected from the sendBatch method - request.resolve = resolve - request.reject = reject - }) - - this.queue.push(request as PendingRequest) - - // force batch to be processed if batch size is reached - if (this.queue.length >= BATCH_SIZE_LIMIT) this.processQueue.flush() - - // call debounced batch processing anyway in case we're over the batch size limit - this.processQueue() - - return result - } -} diff --git a/packages/chain-connector-evm/src/getChainFromEvmNetwork.ts b/packages/chain-connector-evm/src/getChainFromEvmNetwork.ts new file mode 100644 index 0000000000..69f904cd0d --- /dev/null +++ b/packages/chain-connector-evm/src/getChainFromEvmNetwork.ts @@ -0,0 +1,60 @@ +import { EvmNetwork, Token } from "@talismn/chaindata-provider" +import { Chain } from "viem" +import * as chains from "viem/chains" + +import { addOnfinalityApiKey } from "./util" + +// viem chains benefit from multicall config & other viem goodies +const VIEM_CHAINS = Object.keys(chains).reduce((acc, curr) => { + const chain = chains[curr as keyof typeof chains] + acc[chain.id] = chain + return acc +}, {} as Record) + +const chainsCache = new Map() + +export const clearChainsCache = (evmNetworkId?: string) => { + if (evmNetworkId) chainsCache.delete(evmNetworkId) + else chainsCache.clear() +} + +export type ChainOptions = { + onFinalityApiKey?: string +} + +export const getChainFromEvmNetwork = ( + evmNetwork: EvmNetwork, + nativeToken: Token, + options: ChainOptions = {} +): Chain => { + if (!evmNetwork?.nativeToken?.id) throw new Error("Undefined native token") + if (evmNetwork.nativeToken.id !== nativeToken.id) throw new Error("Native token mismatch") + + const { symbol, decimals } = nativeToken + + if (!chainsCache.has(evmNetwork.id)) { + const chainRpcs = + evmNetwork.rpcs?.map((rpc) => addOnfinalityApiKey(rpc.url, options.onFinalityApiKey)) ?? [] + + const viemChain = VIEM_CHAINS[Number(evmNetwork.id)] ?? {} + + const chain: Chain = { + ...viemChain, + id: Number(evmNetwork.id), + name: evmNetwork.name ?? `EVM Chain ${evmNetwork.id}`, + rpcUrls: { + public: { http: chainRpcs }, + default: { http: chainRpcs }, + }, + nativeCurrency: { + symbol, + decimals, + name: symbol, + }, + } + + chainsCache.set(evmNetwork.id, chain) + } + + return chainsCache.get(evmNetwork.id) as Chain +} diff --git a/packages/chain-connector-evm/src/getEvmNetworkPublicClient.ts b/packages/chain-connector-evm/src/getEvmNetworkPublicClient.ts new file mode 100644 index 0000000000..11683b33b3 --- /dev/null +++ b/packages/chain-connector-evm/src/getEvmNetworkPublicClient.ts @@ -0,0 +1,51 @@ +import { EvmNetwork, Token } from "@talismn/chaindata-provider" +import { PublicClient, createPublicClient } from "viem" + +import { clearChainsCache, getChainFromEvmNetwork } from "./getChainFromEvmNetwork" +import { getTransportForEvmNetwork } from "./getTransportForEvmNetwork" + +const MUTLICALL_BATCH_WAIT = 25 +const MUTLICALL_BATCH_SIZE = 1000 + +// cache to reuse previously created public clients +const publicClientCache = new Map() + +export const clearPublicClientCache = (evmNetworkId?: string) => { + clearChainsCache(evmNetworkId) + + if (evmNetworkId) publicClientCache.delete(evmNetworkId) + else publicClientCache.clear() +} + +type PublicClientOptions = { + onFinalityApiKey?: string +} + +export const getEvmNetworkPublicClient = ( + evmNetwork: EvmNetwork, + nativeToken: Token, + options: PublicClientOptions = {} +): PublicClient => { + const chain = getChainFromEvmNetwork(evmNetwork, nativeToken) + + if (!publicClientCache.has(evmNetwork.id)) { + if (!evmNetwork.rpcs?.length) throw new Error("No RPCs found for EVM network") + + const batch = chain.contracts?.multicall3 + ? { multicall: { wait: MUTLICALL_BATCH_WAIT, batchSize: MUTLICALL_BATCH_SIZE } } + : undefined + + const transport = getTransportForEvmNetwork(evmNetwork, options) + + publicClientCache.set( + evmNetwork.id, + createPublicClient({ + chain, + transport, + batch, + }) + ) + } + + return publicClientCache.get(evmNetwork.id) as PublicClient +} diff --git a/packages/chain-connector-evm/src/getEvmNetworkWalletClient.ts b/packages/chain-connector-evm/src/getEvmNetworkWalletClient.ts new file mode 100644 index 0000000000..15bf756951 --- /dev/null +++ b/packages/chain-connector-evm/src/getEvmNetworkWalletClient.ts @@ -0,0 +1,24 @@ +import { EvmNetwork, Token } from "@talismn/chaindata-provider" +import { Account, WalletClient, createWalletClient } from "viem" + +import { getChainFromEvmNetwork } from "./getChainFromEvmNetwork" +import { getTransportForEvmNetwork } from "./getTransportForEvmNetwork" + +type WalletClientOptions = { + account?: `0x${string}` | Account + onFinalityApiKey?: string +} + +export const getEvmNetworkWalletClient = ( + evmNetwork: EvmNetwork, + nativeToken: Token, + options: WalletClientOptions = {} +): WalletClient => { + const chain = getChainFromEvmNetwork(evmNetwork, nativeToken, { + onFinalityApiKey: options.onFinalityApiKey, + }) + + const transport = getTransportForEvmNetwork(evmNetwork, options) + + return createWalletClient({ chain, transport, account: options.account }) +} diff --git a/packages/chain-connector-evm/src/getTransportForEvmNetwork.ts b/packages/chain-connector-evm/src/getTransportForEvmNetwork.ts new file mode 100644 index 0000000000..981d15337d --- /dev/null +++ b/packages/chain-connector-evm/src/getTransportForEvmNetwork.ts @@ -0,0 +1,28 @@ +import { EvmNetwork } from "@talismn/chaindata-provider" +import { fallback, http } from "viem" + +import { addOnfinalityApiKey } from "./util" + +const HTTP_BATCH_WAIT = 25 +const HTTP_BATCH_SIZE = 30 + +type TransportOptions = { + onFinalityApiKey?: string +} + +export const getTransportForEvmNetwork = ( + evmNetwork: EvmNetwork, + options: TransportOptions = {} +) => { + if (!evmNetwork.rpcs?.length) throw new Error("No RPCs found for EVM network") + + return fallback( + evmNetwork.rpcs.map((rpc) => + http(addOnfinalityApiKey(rpc.url, options.onFinalityApiKey), { + batch: { wait: HTTP_BATCH_WAIT, batchSize: HTTP_BATCH_SIZE }, + retryCount: 0, + }) + ), + { retryCount: 0 } + ) +} diff --git a/packages/chain-connector-evm/src/util.ts b/packages/chain-connector-evm/src/util.ts index 8eca198395..ec7e4a7a1c 100644 --- a/packages/chain-connector-evm/src/util.ts +++ b/packages/chain-connector-evm/src/util.ts @@ -1,11 +1,3 @@ -import { AcalaJsonRpcProvider } from "@acala-network/eth-providers" -import { throwAfter } from "@talismn/util" -import { ethers } from "ethers" - -import { ACALA_NETWORK_IDS, RPC_HEALTHCHECK_TIMEOUT } from "./constants" -import { EvmJsonRpcBatchProvider } from "./EvmJsonRpcBatchProvider" -import log from "./log" - /** * Helper function to add our onfinality api key to a public onfinality RPC url. */ @@ -23,81 +15,3 @@ export const addOnfinalityApiKey = (rpcUrl: string, onfinalityApiKey?: string) = `https://$1.api.onfinality.io/rpc?apikey=${onfinalityApiKey}` ) } - -export const isHealthyRpc = async (url: string, chainId: number) => { - try { - // StaticJsonRpcProvider is better suited for this as it will not do health check requests on it's own - const provider = new ethers.providers.StaticJsonRpcProvider(url, { - chainId, - name: `EVM Network ${chainId}`, - }) - - // check that RPC responds in time - const rpcChainId = await Promise.race([ - provider.send("eth_chainId", []), - throwAfter(RPC_HEALTHCHECK_TIMEOUT, "timeout"), - ]) - - // with expected chain id - return parseInt(rpcChainId, 16) === chainId - } catch (err) { - log.error("Unhealthy EVM RPC %s", url, { err }) - return false - } -} - -export const isAcalaNetwork = (chainId: number) => ACALA_NETWORK_IDS.includes(chainId) - -export const getHealthyRpc = async (rpcUrls: string[], network: ethers.providers.Network) => { - for (const rpcUrl of rpcUrls) if (await isHealthyRpc(rpcUrl, network.chainId)) return rpcUrl - - log.warn("No healthy RPC for EVM network %s (%d)", network.name, network.chainId) - return null -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isUnhealthyRpcError = (err: any) => { - // expected errors that are not related to RPC health - // ex : throw revert on a transaction call that fails - if (err?.message === "BATCH_FAILED") return false - if (err?.reason === "processing response error") return false - - // if unknown, assume RPC is unhealthy - return true -} - -export class StandardRpcProvider extends ethers.providers.JsonRpcProvider { - async send(method: string, params: Array): Promise { - try { - return await super.send(method, params) - } catch (err) { - // emit error so rpc manager considers this rpc unhealthy - if (isUnhealthyRpcError(err)) this.emit("error", err) - throw err - } - } -} - -export class AcalaRpcProvider extends AcalaJsonRpcProvider { - async send(method: string, params: Array): Promise { - try { - return await super.send(method, params) - } catch (err) { - // emit error so rpc manager considers this rpc unhealthy - if (isUnhealthyRpcError(err)) this.emit("error", err) - throw err - } - } -} - -export class BatchRpcProvider extends EvmJsonRpcBatchProvider { - async send(method: string, params: Array): Promise { - try { - return await super.send(method, params) - } catch (err) { - // emit error so rpc manager considers this rpc unhealthy - if (isUnhealthyRpcError(err)) this.emit("error", err) - throw err - } - } -} diff --git a/packages/on-chain-id/src/index.ts b/packages/on-chain-id/src/index.ts index 9cd71d0d86..d6bdd3dcaf 100644 --- a/packages/on-chain-id/src/index.ts +++ b/packages/on-chain-id/src/index.ts @@ -30,18 +30,18 @@ export class OnChainId { async resolveNames(names: string[]): Promise> { const resolvedNames = new Map(names.map((name) => [name, null])) - const provider = await this.chainConnectors.evm?.getProviderForEvmNetworkId( + const client = await this.chainConnectors.evm?.getPublicClientForEvmNetwork( this.networkIdEthereum ) - if (!provider) { - log.warn(`Could not find Ethereum provider in OnChainId::resolveNames`) + if (!client) { + log.warn(`Could not find Ethereum client in OnChainId::resolveNames`) return resolvedNames } const results = await Promise.allSettled( names.map(async (name) => { try { - const address = await provider.resolveName(name) + const address = await client.getEnsAddress({ name }) name !== null && resolvedNames.set(name, address) } catch (cause) { throw new Error(`Failed to resolve address for ENS domain '${name}'`, { cause }) @@ -159,11 +159,11 @@ export class OnChainId { async lookupEnsAddresses(addresses: string[]): Promise { const onChainIds: OnChainIds = new Map(addresses.map((address) => [address, null])) - const provider = await this.chainConnectors.evm?.getProviderForEvmNetworkId( + const client = await this.chainConnectors.evm?.getPublicClientForEvmNetwork( this.networkIdEthereum ) - if (!provider) { - log.warn(`Could not find Ethereum provider in OnChainId::lookupEnsAddresses`) + if (!client) { + log.warn(`Could not find Ethereum client in OnChainId::lookupEnsAddresses`) return onChainIds } @@ -173,7 +173,7 @@ export class OnChainId { if (!isEthereumAddress(address)) return try { - const domain = await provider.lookupAddress(address) + const domain = await client.getEnsName({ address }) domain !== null && onChainIds.set(address, domain) } catch (cause) { throw new Error( diff --git a/packages/talisman-ui/src/components/Button.tsx b/packages/talisman-ui/src/components/Button.tsx index 6d578afbd6..4aa15b6ea4 100644 --- a/packages/talisman-ui/src/components/Button.tsx +++ b/packages/talisman-ui/src/components/Button.tsx @@ -54,7 +54,7 @@ export const Button: FC = ({ type="button" disabled={disabled || processing} className={classNames( - "bg relative inline-flex items-center justify-center rounded outline-none transition-colors ", + "bg relative inline-flex items-center justify-center rounded outline-none", small ? "h-20 px-8 text-sm" : "text-md h-28 px-12", fullWidth ? "w-full" : "", colors, diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 8403bf6ab8..aed026a586 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -8,6 +8,7 @@ export * from "./encodeAnyAddress" export * from "./formatDecimals" export * from "./hasOwnProperty" export * from "./isArrayOf" +export * from "./isBigInt" export * from "./isEthereumAddress" export * from "./planckToTokens" export * from "./sleep" diff --git a/packages/util/src/isBigInt.ts b/packages/util/src/isBigInt.ts new file mode 100644 index 0000000000..09f8299999 --- /dev/null +++ b/packages/util/src/isBigInt.ts @@ -0,0 +1 @@ +export const isBigInt = (value: unknown): value is bigint => typeof value === "bigint" diff --git a/packages/util/src/isEthereumAddress.ts b/packages/util/src/isEthereumAddress.ts index 291da24be9..feac1e011b 100644 --- a/packages/util/src/isEthereumAddress.ts +++ b/packages/util/src/isEthereumAddress.ts @@ -1,2 +1,2 @@ -export const isEthereumAddress = (address: string) => - address.startsWith("0x") && address.length === 42 +export const isEthereumAddress = (address: string | undefined | null): address is `0x${string}` => + !!address && address.startsWith("0x") && address.length === 42 diff --git a/packages/util/src/throwAfter.ts b/packages/util/src/throwAfter.ts index 321d130bf8..8e97741f9c 100644 --- a/packages/util/src/throwAfter.ts +++ b/packages/util/src/throwAfter.ts @@ -1,2 +1,2 @@ export const throwAfter = (ms: number, reason: string) => - new Promise((_, reject) => setTimeout(() => reject(reason), ms)) + new Promise((_, reject) => setTimeout(() => reject(reason), ms)) diff --git a/yarn.lock b/yarn.lock index 75e98701fb..a7d88d331a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,65 +12,6 @@ __metadata: languageName: node linkType: hard -"@acala-network/api-derive@npm:6.0.0": - version: 6.0.0 - resolution: "@acala-network/api-derive@npm:6.0.0" - dependencies: - "@acala-network/types": 6.0.0 - peerDependencies: - "@polkadot/api": ^10.9.1 - checksum: 8656e7b65bfef498cac780d7beb0d3e43a59b5343d09a8b28ce7ec3537494b7477b0162a01b4f6c53260cfa7fa63c59e311b3a254020da6a104832826e6099ed - languageName: node - linkType: hard - -"@acala-network/api@npm:^6.0.0": - version: 6.0.0 - resolution: "@acala-network/api@npm:6.0.0" - dependencies: - "@acala-network/api-derive": 6.0.0 - "@acala-network/types": 6.0.0 - peerDependencies: - "@polkadot/api": ^10.9.1 - checksum: 5ec48f880b80ad7a9acf070dbd90938e6aae8248407a283c19560e80b21179fae35f4e4d828d9f1ac300be4e119800ece834df2c96f02dbc35e73be1e83da8ad - languageName: node - linkType: hard - -"@acala-network/contracts@npm:4.3.4": - version: 4.3.4 - resolution: "@acala-network/contracts@npm:4.3.4" - checksum: 65cfa0dbd2aba323168232385c7651ccd4ae65262f8032780998d4a7d96b0de7066a13390628b901f05dcfc20f134dc549c8aada3ad452d9aa9b5bc673ca6ae0 - languageName: node - linkType: hard - -"@acala-network/eth-providers@npm:^2.7.8": - version: 2.7.8 - resolution: "@acala-network/eth-providers@npm:2.7.8" - dependencies: - "@acala-network/contracts": 4.3.4 - "@acala-network/eth-transactions": 2.7.8 - bn.js: ~5.2.0 - ethers: ~5.7.0 - graphql: ~16.0.1 - graphql-request: ~3.6.1 - lru-cache: ~7.8.2 - peerDependencies: - "@acala-network/api": ~6.0.0 - "@polkadot/api": ^10.9.1 - checksum: 6aee2f8322068883078da4a8909964a930b652c222ca5a0063b81797e80d7ee3b8e78b06de05b8decd86d9d91e7af4973b10ae0bde75697efff4a6ad7fc3fbb9 - languageName: node - linkType: hard - -"@acala-network/eth-transactions@npm:2.7.8": - version: 2.7.8 - resolution: "@acala-network/eth-transactions@npm:2.7.8" - dependencies: - ethers: ~5.7.0 - peerDependencies: - "@polkadot/util-crypto": ^12.4.2 - checksum: b28badf1fb1141532d2f00bf83062f937147e11972291f8752660fb1d3657e62cc2a97ef16ee7fd26490ff6fcb1a728370a78ec68714d25943962f8fb29be4e7 - languageName: node - linkType: hard - "@acala-network/type-definitions@npm:5.1.1": version: 5.1.1 resolution: "@acala-network/type-definitions@npm:5.1.1" @@ -80,15 +21,6 @@ __metadata: languageName: node linkType: hard -"@acala-network/types@npm:6.0.0": - version: 6.0.0 - resolution: "@acala-network/types@npm:6.0.0" - peerDependencies: - "@polkadot/api": ^10.9.1 - checksum: d5187f6eccf46e094cda7a69e23c6fd1f8221da4f1e7d30964c9769bb40795e0aa25c5a400c51b3ae4a2bb9dcc47996738febe08b7b4f2378ece7aa14461a74d - languageName: node - linkType: hard - "@adobe/css-tools@npm:^4.0.1": version: 4.0.1 resolution: "@adobe/css-tools@npm:4.0.1" @@ -96,6 +28,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:1.9.4": + version: 1.9.4 + resolution: "@adraffy/ens-normalize@npm:1.9.4" + checksum: 7d7fff58ebe2c4961f7e5e61dad123aa6a63fec0df5c84af1fa41079dc05d398599690be4427b3a94d2baa94084544bcfdf2d51cbed7504b9b0583b0960ad550 + languageName: node + linkType: hard + "@alectalisman/preconstruct-cli@npm:2.3.0-e": version: 2.3.0-e resolution: "@alectalisman/preconstruct-cli@npm:2.3.0-e" @@ -4911,12 +4850,12 @@ __metadata: languageName: node linkType: hard -"@ledgerhq/cryptoassets@npm:^9.5.0": - version: 9.5.0 - resolution: "@ledgerhq/cryptoassets@npm:9.5.0" +"@ledgerhq/cryptoassets@npm:^11.0.1": + version: 11.0.1 + resolution: "@ledgerhq/cryptoassets@npm:11.0.1" dependencies: invariant: 2 - checksum: 15c226edf8e90e7c858b6faa4e322f4d547c185c34f9f4b623b3a7399d8f596892867bb7ed4c546aa2e3f1c8627ea425f0476914d49f14690f7a07671c32c62e + checksum: 0dcb5ae8bf2958d746ebadba6309ee17c3a0225df01852ef2e9f3f24e590ac80ff5ef7aad59fc1f58386347cd4d5a0f3a9bd0445086665b2f8568eb9b0606c85 languageName: node linkType: hard @@ -4944,19 +4883,31 @@ __metadata: languageName: node linkType: hard -"@ledgerhq/domain-service@npm:^1.1.1": - version: 1.1.1 - resolution: "@ledgerhq/domain-service@npm:1.1.1" +"@ledgerhq/devices@npm:^8.0.7": + version: 8.0.7 + resolution: "@ledgerhq/devices@npm:8.0.7" dependencies: - "@ledgerhq/cryptoassets": ^9.5.0 - "@ledgerhq/errors": ^6.12.5 + "@ledgerhq/errors": ^6.14.0 "@ledgerhq/logs": ^6.10.1 - "@ledgerhq/types-live": ^6.34.0 + rxjs: 6 + semver: ^7.3.5 + checksum: 9dff60fc6b443d38a508e3f83b0ee3989fd58061c59345dd6ded524b5fd01b9a2840eb9bde7979829ae6d50d8e0c0e552e21284a1f10d8636a11ed737ca464a2 + languageName: node + linkType: hard + +"@ledgerhq/domain-service@npm:^1.1.13": + version: 1.1.13 + resolution: "@ledgerhq/domain-service@npm:1.1.13" + dependencies: + "@ledgerhq/cryptoassets": ^11.0.1 + "@ledgerhq/errors": ^6.14.0 + "@ledgerhq/logs": ^6.10.1 + "@ledgerhq/types-live": ^6.41.1 axios: ^1.3.4 - eip55: ^2.1.0 + eip55: ^2.1.1 react: ^17.0.2 react-dom: ^17.0.2 - checksum: 08f0b3f1d1cfa4e25fd5451791f867577caba3113d949d0f2309e39369520b8e89cfc66df1e52a6a6cdbf5575454013d24ce086499c09e4a614b3aa9aa065120 + checksum: f4234dd178837d72d8dfd25b487bb1c4625ca04cd41f9a60ab19d918a58343d37ba4695d11a1ba93f5b8656616b1f83bd139197f482ad00b2e7edec08d70ea14 languageName: node linkType: hard @@ -4974,32 +4925,53 @@ __metadata: languageName: node linkType: hard -"@ledgerhq/hw-app-eth@npm:6.33.3": - version: 6.33.3 - resolution: "@ledgerhq/hw-app-eth@npm:6.33.3" +"@ledgerhq/errors@npm:^6.14.0": + version: 6.14.0 + resolution: "@ledgerhq/errors@npm:6.14.0" + checksum: 2c4e7bf126952a781d9226752b9401e842a5a773861128d182e9f40f1f46bf6cd8667c2ca64a1c52254d151bc6bec6d85ea62ba8b3dd35e46bbe47a90c291830 + languageName: node + linkType: hard + +"@ledgerhq/evm-tools@npm:^1.0.9": + version: 1.0.9 + resolution: "@ledgerhq/evm-tools@npm:1.0.9" + dependencies: + "@ledgerhq/cryptoassets": ^11.0.1 + "@ledgerhq/live-env": ^0.6.0 + "@ledgerhq/live-network": ^1.1.7 + crypto-js: 4.1.1 + ethers: 5.7.2 + checksum: faa1027ee9ee6f132ee10c66a0c49a300ab688a0a2a8069c153a75047955c3779c62e331d237028716bb60e052d038e0395bf87d6570ce4eb66cd8d3b8d05e9f + languageName: node + linkType: hard + +"@ledgerhq/hw-app-eth@npm:6.34.8": + version: 6.34.8 + resolution: "@ledgerhq/hw-app-eth@npm:6.34.8" dependencies: "@ethersproject/abi": ^5.5.0 "@ethersproject/rlp": ^5.5.0 - "@ledgerhq/cryptoassets": ^9.5.0 - "@ledgerhq/domain-service": ^1.1.1 - "@ledgerhq/errors": ^6.12.5 - "@ledgerhq/hw-transport": ^6.28.3 - "@ledgerhq/hw-transport-mocker": ^6.27.14 + "@ledgerhq/cryptoassets": ^11.0.1 + "@ledgerhq/domain-service": ^1.1.13 + "@ledgerhq/errors": ^6.14.0 + "@ledgerhq/evm-tools": ^1.0.9 + "@ledgerhq/hw-transport": ^6.28.8 + "@ledgerhq/hw-transport-mocker": ^6.27.19 "@ledgerhq/logs": ^6.10.1 + "@ledgerhq/types-live": ^6.41.1 axios: ^1.3.4 - bignumber.js: ^9.1.0 - crypto-js: ^4.1.1 - checksum: 65ccc79acd2400b01cf42b9757102537ce9dd3ad3a1c1a863c6b1f43790b2f6134b3616cd31123ff82216c1ab47b913506ce7984178d0e85b72dbcb90fc95ded + bignumber.js: ^9.1.2 + checksum: d58d0c73d7d8a69ce8938ff8fb114bbbcea1b5edb2ac0db38ad209bf83c2eeeba291861f5e3e2406567be1618732df11699063f6412c3d499881fbfbab2dc905 languageName: node linkType: hard -"@ledgerhq/hw-transport-mocker@npm:^6.27.14": - version: 6.27.14 - resolution: "@ledgerhq/hw-transport-mocker@npm:6.27.14" +"@ledgerhq/hw-transport-mocker@npm:^6.27.19": + version: 6.27.19 + resolution: "@ledgerhq/hw-transport-mocker@npm:6.27.19" dependencies: - "@ledgerhq/hw-transport": ^6.28.3 + "@ledgerhq/hw-transport": ^6.28.8 "@ledgerhq/logs": ^6.10.1 - checksum: 328be53eeb443e72da9b5a3d08e86681c24553a32bfa8c2c5076398893e4edea0aac53e8f729fc82bf6948401e9847594fb92bd7a9a916c83a8e281025e03875 + checksum: f235f3263fa0366a923334d931d8fe24779cbbda33beb7f799356f11e18fb7cce29df1228964ed18bccdf511f9d0ac2077147aca8caf820c0bb1fa0a7887fb01 languageName: node linkType: hard @@ -5090,6 +5062,52 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/hw-transport@npm:^6.28.8": + version: 6.28.8 + resolution: "@ledgerhq/hw-transport@npm:6.28.8" + dependencies: + "@ledgerhq/devices": ^8.0.7 + "@ledgerhq/errors": ^6.14.0 + events: ^3.3.0 + checksum: 636a1d8229e10673745a322ed4714f85ef395c8bdf39ed14bef859fbc8132ee271a2cf164042a06d7a4d24ee0bbb77c6c29e15c950b3f58af53ad161f9854c51 + languageName: node + linkType: hard + +"@ledgerhq/live-env@npm:^0.6.0": + version: 0.6.0 + resolution: "@ledgerhq/live-env@npm:0.6.0" + dependencies: + rxjs: ^6.6.7 + utility-types: ^3.10.0 + checksum: 1833d07a42e9bd22aa889ae507544bcaefd1a8423d73feaa252ffb0039b9fbf7ca788547923b517e5b843c1aa21665f4f517aaa84ab062e7f267594494bb6a8f + languageName: node + linkType: hard + +"@ledgerhq/live-network@npm:^1.1.7": + version: 1.1.7 + resolution: "@ledgerhq/live-network@npm:1.1.7" + dependencies: + "@ledgerhq/errors": ^6.14.0 + "@ledgerhq/live-env": ^0.6.0 + "@ledgerhq/live-promise": ^0.0.1 + "@ledgerhq/logs": ^6.10.1 + "@types/node": ^20.2.5 + axios: 0.26.1 + invariant: ^2.2.2 + lru-cache: ^7.14.1 + checksum: f82c80e284cf47e4e19c3ed84cad6745ba89fd11d83610764a4d7668c99e262f00ca21e0fa8e0122dd5c6fdb78db8d85eb3a7e1a3bf2d79eec3238a926b3c286 + languageName: node + linkType: hard + +"@ledgerhq/live-promise@npm:^0.0.1": + version: 0.0.1 + resolution: "@ledgerhq/live-promise@npm:0.0.1" + dependencies: + "@ledgerhq/logs": ^6.10.1 + checksum: 7bfbab505d45646b57e2b35468a6226bcdbbaca37a89685d92f9b38726d1da236fa9db24863060ded9160caacb52b67c127bd5ddcc05c629e13f8f90a9e09473 + languageName: node + linkType: hard + "@ledgerhq/logs@npm:^6.10.1": version: 6.10.1 resolution: "@ledgerhq/logs@npm:6.10.1" @@ -5097,13 +5115,13 @@ __metadata: languageName: node linkType: hard -"@ledgerhq/types-live@npm:^6.34.0": - version: 6.34.0 - resolution: "@ledgerhq/types-live@npm:6.34.0" +"@ledgerhq/types-live@npm:^6.41.1": + version: 6.41.1 + resolution: "@ledgerhq/types-live@npm:6.41.1" dependencies: - bignumber.js: ^9.1.0 + bignumber.js: ^9.1.2 rxjs: 6 - checksum: 42829bee0f1343b60197c60bc5ba00a104bdefa2c2f17d7bcb52e37359a6c78b272a9fb98eec63376a34eb7867f5b6cabc43d1bfc10dd6c721678c2def8213db + checksum: 2b9089ca62bca7bc975ff1c2affc9e592be4146aba8edc858c58f20f867bfc28eee1de4a571c44574a321bfafa064d2b9357d1b053071d799de5e53ba5234453 languageName: node linkType: hard @@ -5346,6 +5364,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.2.0, @noble/curves@npm:~1.2.0": + version: 1.2.0 + resolution: "@noble/curves@npm:1.2.0" + dependencies: + "@noble/hashes": 1.3.2 + checksum: bb798d7a66d8e43789e93bc3c2ddff91a1e19fdb79a99b86cd98f1e5eff0ee2024a2672902c2576ef3577b6f282f3b5c778bebd55761ddbb30e36bf275e83dd0 + languageName: node + linkType: hard + "@noble/ed25519@npm:^1.7.0": version: 1.7.2 resolution: "@noble/ed25519@npm:1.7.2" @@ -5374,6 +5401,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.3.2, @noble/hashes@npm:~1.3.2": + version: 1.3.2 + resolution: "@noble/hashes@npm:1.3.2" + checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474 + languageName: node + linkType: hard + "@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:^1.6.3, @noble/secp256k1@npm:~1.7.0": version: 1.7.1 resolution: "@noble/secp256k1@npm:1.7.1" @@ -6722,6 +6756,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:~1.1.2": + version: 1.1.3 + resolution: "@scure/base@npm:1.1.3" + checksum: 1606ab8a4db898cb3a1ada16c15437c3bce4e25854fadc8eb03ae93cbbbac1ed90655af4b0be3da37e12056fef11c0374499f69b9e658c9e5b7b3e06353c630c + languageName: node + linkType: hard + "@scure/bip32@npm:1.1.5": version: 1.1.5 resolution: "@scure/bip32@npm:1.1.5" @@ -6744,6 +6785,17 @@ __metadata: languageName: node linkType: hard +"@scure/bip32@npm:1.3.2": + version: 1.3.2 + resolution: "@scure/bip32@npm:1.3.2" + dependencies: + "@noble/curves": ~1.2.0 + "@noble/hashes": ~1.3.2 + "@scure/base": ~1.1.2 + checksum: c5ae84fae43490853693b481531132b89e056d45c945fc8b92b9d032577f753dfd79c5a7bbcbf0a7f035951006ff0311b6cf7a389e26c9ec6335e42b20c53157 + languageName: node + linkType: hard + "@scure/bip39@npm:1.1.1": version: 1.1.1 resolution: "@scure/bip39@npm:1.1.1" @@ -6764,6 +6816,16 @@ __metadata: languageName: node linkType: hard +"@scure/bip39@npm:1.2.1": + version: 1.2.1 + resolution: "@scure/bip39@npm:1.2.1" + dependencies: + "@noble/hashes": ~1.3.0 + "@scure/base": ~1.1.0 + checksum: c5bd6f1328fdbeae2dcdd891825b1610225310e5e62a4942714db51066866e4f7bef242c7b06a1b9dcc8043a4a13412cf5c5df76d3b10aa9e36b82e9b6e3eeaa + languageName: node + linkType: hard + "@sentry-internal/tracing@npm:7.44.1": version: 7.44.1 resolution: "@sentry-internal/tracing@npm:7.44.1" @@ -7791,11 +7853,11 @@ __metadata: "@types/lodash": ^4.14.180 anylogger: ^1.0.11 eslint: ^8.52.0 - ethers: 5.7.2 jest: ^28.1.0 lodash: 4.17.21 ts-jest: ^28.0.2 typescript: ^5.2.2 + viem: ^1.18.9 peerDependencies: "@polkadot/util": 11.x languageName: unknown @@ -7814,11 +7876,11 @@ __metadata: "@types/lodash": ^4.14.180 anylogger: ^1.0.11 eslint: ^8.52.0 - ethers: 5.7.2 jest: ^28.1.0 lodash: 4.17.21 ts-jest: ^28.0.2 typescript: ^5.2.2 + viem: ^1.18.9 languageName: unknown linkType: soft @@ -8048,8 +8110,6 @@ __metadata: version: 0.0.0-use.local resolution: "@talismn/chain-connector-evm@workspace:packages/chain-connector-evm" dependencies: - "@acala-network/api": ^6.0.0 - "@acala-network/eth-providers": ^2.7.8 "@talismn/chaindata-provider": "workspace:*" "@talismn/eslint-config": "workspace:*" "@talismn/tsconfig": "workspace:*" @@ -8058,11 +8118,11 @@ __metadata: "@types/lodash": ^4.14.180 anylogger: ^1.0.11 eslint: ^8.52.0 - ethers: 5.7.2 jest: ^28.1.0 lodash: 4.17.21 ts-jest: ^28.0.2 typescript: ^5.2.2 + viem: ^1.18.9 languageName: unknown linkType: soft @@ -8957,6 +9017,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.2.5": + version: 20.8.10 + resolution: "@types/node@npm:20.8.10" + dependencies: + undici-types: ~5.26.4 + checksum: 7c61190e43e8074a1b571e52ff14c880bc67a0447f2fe5ed0e1a023eb8a23d5f815658edb98890f7578afe0f090433c4a635c7c87311762544e20dd78723e515 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -10282,6 +10351,21 @@ __metadata: languageName: node linkType: hard +"abitype@npm:0.9.8": + version: 0.9.8 + resolution: "abitype@npm:0.9.8" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3 >=3.19.1 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: d7d887f29d6821e3f7a400de9620511b80ead3f85c5c87308aaec97965d3493e6687ed816e88722b4f512249bd66dee9e69231b49af0e1db8f69400a62c87cf6 + languageName: node + linkType: hard + "abitype@npm:^0.3.0": version: 0.3.0 resolution: "abitype@npm:0.3.0" @@ -11024,6 +11108,15 @@ __metadata: languageName: node linkType: hard +"axios@npm:0.26.1": + version: 0.26.1 + resolution: "axios@npm:0.26.1" + dependencies: + follow-redirects: ^1.14.8 + checksum: d9eb58ff4bc0b36a04783fc9ff760e9245c829a5a1052ee7ca6013410d427036b1d10d04e7380c02f3508c5eaf3485b1ae67bd2adbfec3683704745c8d7a6e1a + languageName: node + linkType: hard + "axios@npm:^0.21.0": version: 0.21.4 resolution: "axios@npm:0.21.4" @@ -11460,13 +11553,20 @@ __metadata: languageName: node linkType: hard -"bignumber.js@npm:^9.1.0, bignumber.js@npm:^9.1.1": +"bignumber.js@npm:^9.1.1": version: 9.1.1 resolution: "bignumber.js@npm:9.1.1" checksum: ad243b7e2f9120b112d670bb3d674128f0bd2ca1745b0a6c9df0433bd2c0252c43e6315d944c2ac07b4c639e7496b425e46842773cf89c6a2dcd4f31e5c4b11e languageName: node linkType: hard +"bignumber.js@npm:^9.1.2": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.2.0 resolution: "binary-extensions@npm:2.2.0" @@ -11481,7 +11581,7 @@ __metadata: languageName: node linkType: hard -"bindings@npm:^1.2.1, bindings@npm:^1.3.0, bindings@npm:^1.5.0": +"bindings@npm:^1.3.0, bindings@npm:^1.5.0": version: 1.5.0 resolution: "bindings@npm:1.5.0" dependencies: @@ -11554,7 +11654,7 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:^5.0.0, bn.js@npm:^5.1.1, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1, bn.js@npm:~5.2.0": +"bn.js@npm:^5.0.0, bn.js@npm:^5.1.1, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1": version: 5.2.1 resolution: "bn.js@npm:5.2.1" checksum: 3dd8c8d38055fedfa95c1d5fc3c99f8dd547b36287b37768db0abab3c239711f88ff58d18d155dd8ad902b0b0cee973747b7ae20ea12a09473272b0201c9edd3 @@ -13023,15 +13123,6 @@ __metadata: languageName: node linkType: hard -"cross-fetch@npm:^3.0.6": - version: 3.1.8 - resolution: "cross-fetch@npm:3.1.8" - dependencies: - node-fetch: ^2.6.12 - checksum: 78f993fa099eaaa041122ab037fe9503ecbbcb9daef234d1d2e0b9230a983f64d645d088c464e21a247b825a08dc444a6e7064adfa93536d3a9454b4745b3632 - languageName: node - linkType: hard - "cross-spawn@npm:^5.1.0": version: 5.1.0 resolution: "cross-spawn@npm:5.1.0" @@ -13073,7 +13164,7 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.1.1": +"crypto-js@npm:4.1.1": version: 4.1.1 resolution: "crypto-js@npm:4.1.1" checksum: b3747c12ee3a7632fab3b3e171ea50f78b182545f0714f6d3e7e2858385f0f4101a15f2517e033802ce9d12ba50a391575ff4638c9de3dd9b2c4bc47768d5425 @@ -13949,12 +14040,12 @@ __metadata: languageName: node linkType: hard -"eip55@npm:^2.1.0": - version: 2.1.0 - resolution: "eip55@npm:2.1.0" +"eip55@npm:^2.1.1": + version: 2.1.1 + resolution: "eip55@npm:2.1.1" dependencies: - keccak: ^1.3.0 - checksum: 80999b66f407dc16aa7d06e1f1ddaa45e31db9e528cd8e5fb15364b2ef5062d0e8c817e46ac5d9b4a164e316b8258de7271b97c36b0719e3048600a4cca51b58 + keccak: ^3.0.3 + checksum: 512d319e4f91ab0c33b514f371206956521dcdcdd23e8eb4d6f9c21e3be9f72287c0b82feb854d3a1eec91805804d13c31e7a1a7dafd37f69eb9994a9c6c8f32 languageName: node linkType: hard @@ -15053,7 +15144,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:5.7.2, ethers@npm:^5.7.1, ethers@npm:^5.7.2, ethers@npm:~5.7.0": +"ethers@npm:5.7.2, ethers@npm:^5.7.1, ethers@npm:^5.7.2": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: @@ -15277,7 +15368,7 @@ __metadata: "@floating-ui/react-dom": 2.0.1 "@headlessui/react": 1.7.13 "@hookform/resolvers": 2.9.11 - "@ledgerhq/hw-app-eth": 6.33.3 + "@ledgerhq/hw-app-eth": 6.34.8 "@ledgerhq/hw-transport-webusb": 6.27.14 "@metamask/browser-passworder": 4.1.0 "@metamask/eth-sig-util": 5.1.0 @@ -15376,7 +15467,6 @@ __metadata: eslint: ^8.52.0 eslint-webpack-plugin: ^4.0.1 eth-phishing-detect: latest - ethers: 5.7.2 fake-indexeddb: 4.0.1 fork-ts-checker-notifier-webpack-plugin: ^6.0.0 fork-ts-checker-webpack-plugin: ^7.2.14 @@ -15437,6 +15527,7 @@ __metadata: url: ^0.11.0 url-join: ^5.0.0 uuid: ^8.3.2 + viem: ^1.18.9 webextension-polyfill: 0.8.0 webpack: ^5.88.1 webpack-bundle-analyzer: ^4.9.0 @@ -15466,13 +15557,6 @@ __metadata: languageName: node linkType: hard -"extract-files@npm:^9.0.0": - version: 9.0.0 - resolution: "extract-files@npm:9.0.0" - checksum: c31781d090f8d8f62cc541f1023b39ea863f24bd6fb3d4011922d71cbded70cef8191f2b70b43ec6cb5c5907cdad1dc5e9f29f78228936c10adc239091d8ab64 - languageName: node - linkType: hard - "extsprintf@npm:1.3.0": version: 1.3.0 resolution: "extsprintf@npm:1.3.0" @@ -15831,6 +15915,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.14.8": + version: 1.15.3 + resolution: "follow-redirects@npm:1.15.3" + peerDependenciesMeta: + debug: + optional: true + checksum: 584da22ec5420c837bd096559ebfb8fe69d82512d5585004e36a3b4a6ef6d5905780e0c74508c7b72f907d1fa2b7bd339e613859e9c304d0dc96af2027fd0231 + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -16560,19 +16654,6 @@ __metadata: languageName: node linkType: hard -"graphql-request@npm:~3.6.1": - version: 3.6.1 - resolution: "graphql-request@npm:3.6.1" - dependencies: - cross-fetch: ^3.0.6 - extract-files: ^9.0.0 - form-data: ^3.0.0 - peerDependencies: - graphql: 14.x || 15.x - checksum: 15e98c29760fca8ce230459dd2448a56eca54f7fcce9cb5f3fd82c9af1e31bfb5a85b67ef405479d9a45acae4d2a1f278ebda6ca1950cad9a5b40dc215b82dd3 - languageName: node - linkType: hard - "graphql-tag@npm:^2.11.0, graphql-tag@npm:^2.12.6": version: 2.12.6 resolution: "graphql-tag@npm:2.12.6" @@ -16600,13 +16681,6 @@ __metadata: languageName: node linkType: hard -"graphql@npm:~16.0.1": - version: 16.0.1 - resolution: "graphql@npm:16.0.1" - checksum: e2fbddc78ac93b84af5b1871cdced5ce796102373b91f888983e8bae26856788833aa641f5da15b9e4da05cef2edcc82b744a86a27e7e1ef6c9ce422571ab16c - languageName: node - linkType: hard - "growly@npm:^1.3.0": version: 1.3.0 resolution: "growly@npm:1.3.0" @@ -17499,7 +17573,7 @@ __metadata: languageName: node linkType: hard -"invariant@npm:2, invariant@npm:^2.2.4": +"invariant@npm:2, invariant@npm:^2.2.2, invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" dependencies: @@ -18131,6 +18205,15 @@ __metadata: languageName: node linkType: hard +"isows@npm:1.0.3": + version: 1.0.3 + resolution: "isows@npm:1.0.3" + peerDependencies: + ws: "*" + checksum: 9cacd5cf59f67deb51e825580cd445ab1725ecb05a67c704050383fb772856f3cd5e7da8ad08f5a3bd2823680d77d099459d0c6a7037972a74d6429af61af440 + languageName: node + linkType: hard + "isstream@npm:~0.1.2": version: 0.1.2 resolution: "isstream@npm:0.1.2" @@ -19948,28 +20031,27 @@ __metadata: languageName: node linkType: hard -"keccak@npm:^1.3.0": - version: 1.4.0 - resolution: "keccak@npm:1.4.0" +"keccak@npm:^3.0.0, keccak@npm:^3.0.1, keccak@npm:^3.0.2": + version: 3.0.3 + resolution: "keccak@npm:3.0.3" dependencies: - bindings: ^1.2.1 - inherits: ^2.0.3 - nan: ^2.2.1 + node-addon-api: ^2.0.0 node-gyp: latest - safe-buffer: ^5.1.0 - checksum: 236ba4183d64e1118566c4f123d812cc8fa5fb0fa477b6743bc398aced42595816f46a322bf0240a6a7589eff932aa1540066a30db2367e4049436d9fa30f537 + node-gyp-build: ^4.2.0 + readable-stream: ^3.6.0 + checksum: f08f04f5cc87013a3fc9e87262f761daff38945c86dd09c01a7f7930a15ae3e14f93b310ef821dcc83675a7b814eb1c983222399a2f263ad980251201d1b9a99 languageName: node linkType: hard -"keccak@npm:^3.0.0, keccak@npm:^3.0.1, keccak@npm:^3.0.2": - version: 3.0.3 - resolution: "keccak@npm:3.0.3" +"keccak@npm:^3.0.3": + version: 3.0.4 + resolution: "keccak@npm:3.0.4" dependencies: node-addon-api: ^2.0.0 node-gyp: latest node-gyp-build: ^4.2.0 readable-stream: ^3.6.0 - checksum: f08f04f5cc87013a3fc9e87262f761daff38945c86dd09c01a7f7930a15ae3e14f93b310ef821dcc83675a7b814eb1c983222399a2f263ad980251201d1b9a99 + checksum: 2bf27b97b2f24225b1b44027de62be547f5c7326d87d249605665abd0c8c599d774671c35504c62c9b922cae02758504c6f76a73a84234d23af8a2211afaaa11 languageName: node linkType: hard @@ -20496,6 +20578,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^7.14.1": + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 + languageName: node + linkType: hard + "lru-cache@npm:^7.7.1": version: 7.12.0 resolution: "lru-cache@npm:7.12.0" @@ -20503,13 +20592,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:~7.8.2": - version: 7.8.2 - resolution: "lru-cache@npm:7.8.2" - checksum: 58b5d9881581f9db23ebd9491a84e9268a1841bafd0e5dcb5492589bffffaa7cf3e07acb197a9bf98477eb6c55eb5f21a0176a63bc69bd39c5a531d93c61b652 - languageName: node - linkType: hard - "lru-queue@npm:^0.1.0": version: 0.1.0 resolution: "lru-queue@npm:0.1.0" @@ -21208,15 +21290,6 @@ __metadata: languageName: node linkType: hard -"nan@npm:^2.2.1": - version: 2.17.0 - resolution: "nan@npm:2.17.0" - dependencies: - node-gyp: latest - checksum: ec609aeaf7e68b76592a3ba96b372aa7f5df5b056c1e37410b0f1deefbab5a57a922061e2c5b369bae9c7c6b5e6eecf4ad2dac8833a1a7d3a751e0a7c7f849ed - languageName: node - linkType: hard - "nano-css@npm:^5.3.1": version: 5.3.5 resolution: "nano-css@npm:5.3.5" @@ -21402,20 +21475,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.12": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: ^5.0.0 - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 - languageName: node - linkType: hard - "node-fetch@npm:^3.3.1": version: 3.3.1 resolution: "node-fetch@npm:3.3.1" @@ -24141,7 +24200,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:6, rxjs@npm:^6.6.3": +"rxjs@npm:6, rxjs@npm:^6.6.3, rxjs@npm:^6.6.7": version: 6.6.7 resolution: "rxjs@npm:6.6.7" dependencies: @@ -26694,6 +26753,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 + languageName: node + linkType: hard + "undici@npm:^5.14.0": version: 5.22.0 resolution: "undici@npm:5.22.0" @@ -26958,6 +27024,13 @@ __metadata: languageName: node linkType: hard +"utility-types@npm:^3.10.0": + version: 3.10.0 + resolution: "utility-types@npm:3.10.0" + checksum: 8f274415c6196ab62883b8bd98c9d2f8829b58016e4269aaa1ebd84184ac5dda7dc2ca45800c0d5e0e0650966ba063bf9a412aaeaea6850ca4440a391283d5c8 + languageName: node + linkType: hard + "uuid@npm:^3.3.2": version: 3.4.0 resolution: "uuid@npm:3.4.0" @@ -27078,6 +27151,27 @@ __metadata: languageName: node linkType: hard +"viem@npm:^1.18.9": + version: 1.18.9 + resolution: "viem@npm:1.18.9" + dependencies: + "@adraffy/ens-normalize": 1.9.4 + "@noble/curves": 1.2.0 + "@noble/hashes": 1.3.2 + "@scure/bip32": 1.3.2 + "@scure/bip39": 1.2.1 + abitype: 0.9.8 + isows: 1.0.3 + ws: 8.13.0 + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 4bcd28a3bc2e0ff2f19a8de2c779dfdc0c7a479fe2c103a03cb4faa96450241075ddbc7e39bde4db9d8ad8e1dbc4d394223cdae00f8e135c2748ba354a2e2665 + languageName: node + linkType: hard + "vinyl-fs@npm:^3.0.2": version: 3.0.3 resolution: "vinyl-fs@npm:3.0.3" From 228731b34ae0c139be241beb338506625c4e72e2 Mon Sep 17 00:00:00 2001 From: Kheops <26880866+0xKheops@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:13:09 +0900 Subject: [PATCH 4/8] fix: prevent useOnChainId unnecessary refreshes (#1142) --- apps/extension/src/ui/hooks/useOnChainId.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/extension/src/ui/hooks/useOnChainId.ts b/apps/extension/src/ui/hooks/useOnChainId.ts index 9258c7d489..0ee4db5922 100644 --- a/apps/extension/src/ui/hooks/useOnChainId.ts +++ b/apps/extension/src/ui/hooks/useOnChainId.ts @@ -12,6 +12,10 @@ export const useOnChainId = (address?: string) => { }, enabled: !!address, cacheTime: Infinity, + refetchInterval: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, initialData: () => address && onChainIdsCache.get(address)?.onChainId, onSuccess: (onChainId) => { if (!address) return From 0ac93b9e30eccc1008f2c565b038d64abe5760e3 Mon Sep 17 00:00:00 2001 From: Kheops <26880866+0xKheops@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:54:56 +0900 Subject: [PATCH 5/8] feat: op stack layer 1 fees (viem edition) (#1146) * feat: op stack layer 1 fees * chore: cleanup --- apps/extension/package.json | 1 + .../ethereum/__tests__/ethereum.helpers.ts | 9 ++- .../src/core/domains/ethereum/helpers.ts | 23 +++++-- .../src/core/domains/signing/types.ts | 1 + .../Ethereum/GasSettings/FeeOptionsForm.tsx | 15 ++++- .../Ethereum/useEthEstimateL1DataFee.ts | 58 ++++++++++++++++++ .../ui/domains/Ethereum/useEthTransaction.ts | 17 +++++- .../Sign/ViewDetails/ViewDetailsEth.tsx | 60 ++++++++++++++++++- yarn.lock | 43 ++++++++++++- 9 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 apps/extension/src/ui/domains/Ethereum/useEthEstimateL1DataFee.ts diff --git a/apps/extension/package.json b/apps/extension/package.json index 30ce25b63b..6dc5c66a7c 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -9,6 +9,7 @@ "@babel/eslint-parser": "^7.19.1", "@dnd-kit/core": "6.0.7", "@dnd-kit/sortable": "7.0.2", + "@eth-optimism/contracts-ts": "^0.17.0", "@ethereumjs/util": "8.0.3", "@floating-ui/react": "0.24.3", "@floating-ui/react-dom": "2.0.1", diff --git a/apps/extension/src/core/domains/ethereum/__tests__/ethereum.helpers.ts b/apps/extension/src/core/domains/ethereum/__tests__/ethereum.helpers.ts index 7c21c223ac..cb1d061b1b 100644 --- a/apps/extension/src/core/domains/ethereum/__tests__/ethereum.helpers.ts +++ b/apps/extension/src/core/domains/ethereum/__tests__/ethereum.helpers.ts @@ -34,7 +34,8 @@ describe("Test ethereum helpers", () => { gas: 22000n, }, 21000n, - baseFeePerGas + baseFeePerGas, + 0n ) expect(estimatedFee).toEqual(42000000000000n) @@ -50,7 +51,8 @@ describe("Test ethereum helpers", () => { gas: 22000n, }, 21000n, - baseFeePerGas + baseFeePerGas, + 0n ) expect(estimatedFee).toEqual(52500000000000n) @@ -65,7 +67,8 @@ describe("Test ethereum helpers", () => { gas: 22000n, }, 21000n, - baseFeePerGas + baseFeePerGas, + 0n ) expect(estimatedFee).toEqual(parseGwei("210000")) diff --git a/apps/extension/src/core/domains/ethereum/helpers.ts b/apps/extension/src/core/domains/ethereum/helpers.ts index 9b186a3525..d6d27d3084 100644 --- a/apps/extension/src/core/domains/ethereum/helpers.ts +++ b/apps/extension/src/core/domains/ethereum/helpers.ts @@ -252,23 +252,36 @@ export const getGasSettingsEip1559 = ( export const getTotalFeesFromGasSettings = ( gasSettings: EthGasSettings, estimatedGas: bigint, - baseFeePerGas?: bigint | null + baseFeePerGas: bigint | null | undefined, + l1Fee: bigint ) => { + // L1 fee needs to be included in estimatedFee and maxFee to keep the same UX behavior whether or not the chain is a L2 + const estimatedL1DataFee = l1Fee > 0n ? l1Fee : null + + // OP Stack docs : Spikes in Ethereum gas prices may result in users paying a higher or lower than estimated L1 data fee, by up to 25% + // https://community.optimism.io/docs/developers/build/transaction-fees/#the-l1-data-fee + const maxL1Fee = (l1Fee * 125n) / 100n + if (gasSettings.type === "eip1559") { if (!isBigInt(baseFeePerGas)) throw new Error("baseFeePerGas argument is required for type 2 fee computation") return { + estimatedL1DataFee, estimatedFee: (gasSettings.maxPriorityFeePerGas + (baseFeePerGas < gasSettings.maxFeePerGas ? baseFeePerGas : gasSettings.maxFeePerGas)) * - (estimatedGas < gasSettings.gas ? estimatedGas : gasSettings.gas), - maxFee: (gasSettings.maxFeePerGas + gasSettings.maxPriorityFeePerGas) * gasSettings.gas, + (estimatedGas < gasSettings.gas ? estimatedGas : gasSettings.gas) + + l1Fee, + maxFee: + (gasSettings.maxFeePerGas + gasSettings.maxPriorityFeePerGas) * gasSettings.gas + maxL1Fee, } } else { return { + estimatedL1DataFee, estimatedFee: - gasSettings.gasPrice * (estimatedGas < gasSettings.gas ? estimatedGas : gasSettings.gas), - maxFee: gasSettings.gasPrice * gasSettings.gas, + gasSettings.gasPrice * (estimatedGas < gasSettings.gas ? estimatedGas : gasSettings.gas) + + l1Fee, + maxFee: gasSettings.gasPrice * gasSettings.gas + maxL1Fee, } } } diff --git a/apps/extension/src/core/domains/signing/types.ts b/apps/extension/src/core/domains/signing/types.ts index eef49f62a2..ed0a910f3c 100644 --- a/apps/extension/src/core/domains/signing/types.ts +++ b/apps/extension/src/core/domains/signing/types.ts @@ -155,6 +155,7 @@ export type EthTransactionDetails = { estimatedGas: bigint gasPrice: bigint estimatedFee: bigint + estimatedL1DataFee: bigint | null maxFee: bigint baseFeePerGas?: bigint | null baseFeeTrend?: EthBaseFeeTrend diff --git a/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx b/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx index 20b25e30fe..d69e624a23 100644 --- a/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx +++ b/apps/extension/src/ui/domains/Ethereum/GasSettings/FeeOptionsForm.tsx @@ -100,8 +100,19 @@ const PriorityOption = ({ }: PriorityOptionProps) => { const { estimatedFee, maxFee } = useMemo(() => { const gasSettings = getGasSettings(gasSettingsByPriority, priority) - return getTotalFeesFromGasSettings(gasSettings, txDetails.estimatedGas, txDetails.baseFeePerGas) - }, [gasSettingsByPriority, priority, txDetails.baseFeePerGas, txDetails.estimatedGas]) + return getTotalFeesFromGasSettings( + gasSettings, + txDetails.estimatedGas, + txDetails.baseFeePerGas, + txDetails.estimatedL1DataFee ?? 0n + ) + }, [ + gasSettingsByPriority, + priority, + txDetails.baseFeePerGas, + txDetails.estimatedGas, + txDetails.estimatedL1DataFee, + ]) const options = useFeePriorityOptionsUI() diff --git a/apps/extension/src/ui/domains/Ethereum/useEthEstimateL1DataFee.ts b/apps/extension/src/ui/domains/Ethereum/useEthEstimateL1DataFee.ts new file mode 100644 index 0000000000..7276a28240 --- /dev/null +++ b/apps/extension/src/ui/domains/Ethereum/useEthEstimateL1DataFee.ts @@ -0,0 +1,58 @@ +import { log } from "@core/log" +import { gasPriceOracleABI, gasPriceOracleAddress } from "@eth-optimism/contracts-ts" +import { useQuery } from "@tanstack/react-query" +import { useMemo } from "react" +import { Hex, PublicClient, TransactionRequest, getContract, serializeTransaction } from "viem" + +const OP_STACK_EVM_NETWORK_IDS = [ + 10, // OP Mainnet, + 420, // OP Goerli + 7777777, // Zora Mainnet + 999, // Zora Goerli + 8453, // Base Mainnet + 84531, // Base Goerli +] + +const getEthL1DataFee = async (publicClient: PublicClient, serializedTx: Hex): Promise => { + try { + const contract = getContract({ + address: gasPriceOracleAddress[420], + abi: gasPriceOracleABI, + publicClient, + }) + return await contract.read.getL1Fee([serializedTx]) + } catch (err) { + log.error(err) + throw new Error("Failed to get L1 data fee", { cause: err }) + } +} + +export const useEthEstimateL1DataFee = ( + publicClient: PublicClient | undefined, + tx: TransactionRequest | undefined +) => { + const serialized = useMemo( + () => + tx && publicClient?.chain?.id + ? serializeTransaction({ chainId: publicClient.chain.id, ...tx }) + : null, + [publicClient?.chain?.id, tx] + ) + + return useQuery({ + queryKey: ["useEthEstimateL1DataFee", publicClient?.chain?.id, serialized], + queryFn: () => { + if (!publicClient?.chain?.id || !serialized) return null + + return OP_STACK_EVM_NETWORK_IDS.includes(publicClient.chain.id) + ? getEthL1DataFee(publicClient, serialized) + : 0n + }, + keepPreviousData: true, + refetchInterval: 6_000, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + enabled: !!publicClient?.chain?.id && !!serialized, + }) +} diff --git a/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts b/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts index 8dd1e4e41d..b00cc4b913 100644 --- a/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts +++ b/apps/extension/src/ui/domains/Ethereum/useEthTransaction.ts @@ -30,6 +30,7 @@ import { useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { PublicClient, TransactionRequest } from "viem" +import { useEthEstimateL1DataFee } from "./useEthEstimateL1DataFee" import { useIsValidEthTransaction } from "./useIsValidEthTransaction" // gasPrice isn't reliable on polygon & mumbai, see https://github.com/ethers-io/ethers.js/issues/2828#issuecomment-1283014250 @@ -427,12 +428,18 @@ export const useEthTransaction = ( if (!lockTransaction) setTransaction(liveUpdatingTransaction) }, [liveUpdatingTransaction, lockTransaction]) + const { data: estimatedL1DataFee, error: l1FeeError } = useEthEstimateL1DataFee( + publicClient, + transaction + ) + // TODO replace this wierd object name with something else... gasInfo ? - const txDetails: EthTransactionDetails | undefined = useMemo(() => { + const txDetails = useMemo(() => { if ( !evmNetworkId || !isBigInt(gasPrice) || !isBigInt(estimatedGas) || + !isBigInt(estimatedL1DataFee) || !transaction || !gasSettings ) @@ -444,7 +451,8 @@ export const useEthTransaction = ( const { estimatedFee, maxFee } = getTotalFeesFromGasSettings( gasSettings, estimatedGas, - baseFeePerGas + baseFeePerGas, + estimatedL1DataFee ) return { @@ -453,6 +461,7 @@ export const useEthTransaction = ( gasPrice, baseFeePerGas, estimatedFee, + estimatedL1DataFee, maxFee, baseFeeTrend: feeHistoryAnalysis?.baseFeeTrend, } @@ -463,6 +472,7 @@ export const useEthTransaction = ( feeHistoryAnalysis?.baseFeeTrend, gasPrice, gasSettings, + estimatedL1DataFee, transaction, ]) @@ -478,6 +488,7 @@ export const useEthTransaction = ( const anyError = (errorEip1559Support ?? nonceError ?? blockFeeDataError ?? + l1FeeError ?? isValidError) as Error const userFriendlyError = getHumanReadableErrorMessage(anyError) @@ -489,7 +500,7 @@ export const useEthTransaction = ( } return { error: undefined, errorDetails: undefined } - }, [blockFeeDataError, isValidError, errorEip1559Support, nonceError, t]) + }, [errorEip1559Support, nonceError, blockFeeDataError, l1FeeError, isValidError, t]) const isLoading = useMemo( () => tx && isDecoding && !txDetails && !error, diff --git a/apps/extension/src/ui/domains/Sign/ViewDetails/ViewDetailsEth.tsx b/apps/extension/src/ui/domains/Sign/ViewDetails/ViewDetailsEth.tsx index 62b8573b1a..304ca3b21f 100644 --- a/apps/extension/src/ui/domains/Sign/ViewDetails/ViewDetailsEth.tsx +++ b/apps/extension/src/ui/domains/Sign/ViewDetails/ViewDetailsEth.tsx @@ -76,14 +76,28 @@ const ViewDetailsContent: FC = ({ onClose }) => { genericEvent("open sign transaction view details", { type: "ethereum" }) }, [genericEvent]) - const [estimatedFee, maximumFee] = useMemo( + const [estimatedFee, maximumFee, estimatedL1DataFee, estimatedL2Fee] = useMemo( () => txDetails && nativeToken ? [ new BalanceFormatter(txDetails.estimatedFee, nativeToken?.decimals, nativeTokenRates), new BalanceFormatter(txDetails.maxFee, nativeToken?.decimals, nativeTokenRates), + txDetails.estimatedL1DataFee + ? new BalanceFormatter( + txDetails.estimatedL1DataFee, + nativeToken.decimals, + nativeTokenRates + ) + : null, + txDetails.estimatedL1DataFee + ? new BalanceFormatter( + txDetails.estimatedFee - txDetails.estimatedL1DataFee, + nativeToken.decimals, + nativeTokenRates + ) + : null, ] - : [null, null], + : [null, null, null, null], [nativeToken, nativeTokenRates, txDetails] ) @@ -204,6 +218,48 @@ const ViewDetailsContent: FC = ({ onClose }) => { )} )} + {estimatedL1DataFee && estimatedL2Fee && nativeToken && ( + + + + + {estimatedL1DataFee && nativeTokenRates ? ( + <> + {" "} + / + + ) : null} + + } + /> + + + {estimatedL2Fee && nativeTokenRates ? ( + <> + {" "} + / + + ) : null} + + } + /> + + + )} {transaction ? ( diff --git a/yarn.lock b/yarn.lock index a7d88d331a..1a48289519 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2828,6 +2828,28 @@ __metadata: languageName: node linkType: hard +"@eth-optimism/contracts-ts@npm:^0.17.0": + version: 0.17.0 + resolution: "@eth-optimism/contracts-ts@npm:0.17.0" + dependencies: + "@testing-library/react": ^14.0.0 + "@types/change-case": ^2.3.1 + change-case: 4.1.2 + react: ^18.2.0 + react-dom: ^18.2.0 + viem: ^1.16.6 + peerDependencies: + "@wagmi/core": ">1.0.0" + wagmi: ">1.0.0" + peerDependenciesMeta: + "@wagmi/core": + optional: true + wagmi: + optional: true + checksum: d576c2fa3f9399de9942640122585ab76def50360a95d56e140c86f0f60523c8dd3be0546a43c1d8004503b9b3b15234b6089ca96afa7cd80e92bc3976f6ab2b + languageName: node + linkType: hard + "@ethereumjs/rlp@npm:^4.0.0-beta.2, @ethereumjs/rlp@npm:^4.0.1": version: 4.0.1 resolution: "@ethereumjs/rlp@npm:4.0.1" @@ -8702,6 +8724,15 @@ __metadata: languageName: node linkType: hard +"@types/change-case@npm:^2.3.1": + version: 2.3.1 + resolution: "@types/change-case@npm:2.3.1" + dependencies: + change-case: "*" + checksum: 73fc8d2f4e7939de2b487ed5143c32f3729fe2f4b035be2ca52f4ed3340cabcc0a66f225cf60c34a6b3eea8133f90c4b37d7478d590963bc99070c1248e6dc0b + languageName: node + linkType: hard + "@types/chrome@npm:^0.0.180": version: 0.0.180 resolution: "@types/chrome@npm:0.0.180" @@ -12256,7 +12287,14 @@ __metadata: languageName: node linkType: hard -"change-case@npm:^4.1.2": +"change-case@npm:*": + version: 5.1.2 + resolution: "change-case@npm:5.1.2" + checksum: b3a5bdd59b50b96bc3e42f4cb3876aa75178dcc0aa9a5a44975e569c9cd809fc4fc15345a887fb45b9656e0fde98ffe028a41fc3d97c4748c6ab80422eea6e7a + languageName: node + linkType: hard + +"change-case@npm:4.1.2, change-case@npm:^4.1.2": version: 4.1.2 resolution: "change-case@npm:4.1.2" dependencies: @@ -15363,6 +15401,7 @@ __metadata: "@babel/preset-typescript": ^7.18.6 "@dnd-kit/core": 6.0.7 "@dnd-kit/sortable": 7.0.2 + "@eth-optimism/contracts-ts": ^0.17.0 "@ethereumjs/util": 8.0.3 "@floating-ui/react": 0.24.3 "@floating-ui/react-dom": 2.0.1 @@ -27151,7 +27190,7 @@ __metadata: languageName: node linkType: hard -"viem@npm:^1.18.9": +"viem@npm:^1.16.6, viem@npm:^1.18.9": version: 1.18.9 resolution: "viem@npm:1.18.9" dependencies: From 072b7c98087d3f17e116a519baa96dc62f59a049 Mon Sep 17 00:00:00 2001 From: Kheops <26880866+0xKheops@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:56:11 +0900 Subject: [PATCH 6/8] feat: decode all xcm transfers (#1150) --- apps/extension/package.json | 1 + .../ui/domains/Sign/Substrate/SubSignBody.tsx | 4 + .../Substrate/shapes/VersionedMultiAssets.ts | 653 ++++++++++++++++++ .../shapes/VersionedMultiLocation.ts | 559 +++++++++++++++ .../Sign/Substrate/shapes/XcmV3Junction.ts | 133 ++++ .../xTokens/SubSignXTokensTransfer.tsx | 117 ++-- .../xcm/SubSignXcmTransferAssets.tsx | 93 ++- yarn.lock | 8 + 8 files changed, 1490 insertions(+), 78 deletions(-) create mode 100644 apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiAssets.ts create mode 100644 apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiLocation.ts create mode 100644 apps/extension/src/ui/domains/Sign/Substrate/shapes/XcmV3Junction.ts diff --git a/apps/extension/package.json b/apps/extension/package.json index 6dc5c66a7c..61eebe73bd 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -130,6 +130,7 @@ "scale-codec": "^0.11.0", "semver": "^7.5.4", "style-loader": "3.3.3", + "subshape": "^0.14.0", "talisman-ui": "workspace:*", "terser-webpack-plugin": "5.3.9", "toml": "^3.0.0", diff --git a/apps/extension/src/ui/domains/Sign/Substrate/SubSignBody.tsx b/apps/extension/src/ui/domains/Sign/Substrate/SubSignBody.tsx index f9502445de..fea9ec3bbe 100644 --- a/apps/extension/src/ui/domains/Sign/Substrate/SubSignBody.tsx +++ b/apps/extension/src/ui/domains/Sign/Substrate/SubSignBody.tsx @@ -27,8 +27,12 @@ const getComponentFromTxDetails = (extrinsic: GenericExtrinsic | null | undefine return SubSignConvictionVotingUndelegate case "convictionVoting.vote": return SubSignConvictionVotingVote + case "xcmPallet.reserveTransferAssets": case "xcmPallet.limitedReserveTransferAssets": + case "xcmPallet.limitedTeleportAssets": + case "polkadotXcm.reserveTransferAssets": case "polkadotXcm.limitedReserveTransferAssets": + case "polkadotXcm.limitedTeleportAssets": return SubSignXcmTransferAssets case "xTokens.transfer": return SubSignXTokensTransfer diff --git a/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiAssets.ts b/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiAssets.ts new file mode 100644 index 0000000000..5d3bcf0a26 --- /dev/null +++ b/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiAssets.ts @@ -0,0 +1,653 @@ +import * as $ from "subshape" + +// Type 379 - bounded_collections::weak_bounded_vec::WeakBoundedVec +// Param T : 2 +type BoundedCollectionsWeakBoundedVecWeakBoundedVec379 = Uint8Array +const $boundedCollectionsWeakBoundedVecWeakBoundedVec379: $.Shape = + $.uint8Array + +// Type 378 - xcm::v2::NetworkId +type XcmV2NetworkId = + | { type: "Any" } + | { type: "Named"; value: BoundedCollectionsWeakBoundedVecWeakBoundedVec379 } + | { type: "Polkadot" } + | { type: "Kusama" } +const $xcmV2NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Any"), + 1: $.variant("Named", $.field("value", $.uint8Array)), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), +}) + +// Type 380 - xcm::v2::BodyId +type XcmV2BodyId = + | { type: "Unit" } + | { type: "Named"; value: BoundedCollectionsWeakBoundedVecWeakBoundedVec379 } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV2BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Named", $.field("value", $boundedCollectionsWeakBoundedVecWeakBoundedVec379)), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 381 - xcm::v2::BodyPart +type XcmV2BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV2BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 377 - xcm::v2::junction::Junction +type XcmV2Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: XcmV2NetworkId; id: Uint8Array } + | { type: "AccountIndex64"; network: XcmV2NetworkId; index: bigint } + | { type: "AccountKey20"; network: XcmV2NetworkId; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { + type: "GeneralKey" + value: BoundedCollectionsWeakBoundedVecWeakBoundedVec379 + } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV2BodyId; part: XcmV2BodyPart } +const $xcmV2Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $xcmV2NetworkId), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $xcmV2NetworkId) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $xcmV2NetworkId) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("value", $boundedCollectionsWeakBoundedVecWeakBoundedVec379)), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV2BodyId), $.field("part", $xcmV2BodyPart)), +}) + +// Type 376 - xcm::v2::multilocation::Junctions +type XcmV2Junctions = + | { type: "Here" } + | { type: "X1"; value: XcmV2Junction } + | { type: "X2"; value: [XcmV2Junction, XcmV2Junction] } + | { type: "X3"; value: [XcmV2Junction, XcmV2Junction, XcmV2Junction] } + | { + type: "X4" + value: [XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction] + } + | { + type: "X5" + value: [XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction] + } + | { + type: "X6" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } + | { + type: "X7" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } + | { + type: "X8" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } +const $xcmV2Junctions: $.Shape = $.taggedUnion("type", { + 0: $.variant("Here"), + 1: $.variant("X1", $.field("value", $xcmV2Junction)), + 2: $.variant( + "X2", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 3: $.variant( + "X3", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 4: $.variant( + "X4", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 5: $.variant( + "X5", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 6: $.variant( + "X6", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 7: $.variant( + "X7", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 8: $.variant( + "X8", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), +}) + +// Type 375 - xcm::v2::multilocation::MultiLocation +type XcmV2MultiLocation = { parents: number; interior: XcmV2Junctions } +const $xcmV2MultiLocation: $.Shape = $.object( + $.field("parents", $.u8), + $.field("interior", $xcmV2Junctions) +) + +// Type 389 - xcm::v2::multiasset::AssetId +type XcmV2AssetId = + | { type: "Concrete"; value: XcmV2MultiLocation } + | { type: "Abstract"; value: Uint8Array } +const $xcmV2AssetId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Concrete", $.field("value", $xcmV2MultiLocation)), + 1: $.variant("Abstract", $.field("value", $.uint8Array)), +}) + +// Type 391 - xcm::v2::multiasset::AssetInstance +type XcmV2AssetInstance = + | { type: "Undefined" } + | { type: "Index"; value: bigint } + | { type: "Array4"; value: Uint8Array } + | { type: "Array8"; value: Uint8Array } + | { type: "Array16"; value: Uint8Array } + | { type: "Array32"; value: Uint8Array } + | { type: "Blob"; value: Uint8Array } +const $xcmV2AssetInstance: $.Shape = $.taggedUnion("type", { + 0: $.variant("Undefined"), + 1: $.variant("Index", $.field("value", $.compact($.u128))), + 2: $.variant("Array4", $.field("value", $.sizedUint8Array(4))), + 3: $.variant("Array8", $.field("value", $.sizedUint8Array(8))), + 4: $.variant("Array16", $.field("value", $.sizedUint8Array(16))), + 5: $.variant("Array32", $.field("value", $.sizedUint8Array(32))), + 6: $.variant("Blob", $.field("value", $.uint8Array)), +}) + +// Type 390 - xcm::v2::multiasset::Fungibility +type XcmV2Fungibility = + | { type: "Fungible"; value: bigint } + | { type: "NonFungible"; value: XcmV2AssetInstance } +const $xcmV2Fungibility: $.Shape = $.taggedUnion("type", { + 0: $.variant("Fungible", $.field("value", $.compact($.u128))), + 1: $.variant("NonFungible", $.field("value", $xcmV2AssetInstance)), +}) + +// Type 388 - xcm::v2::multiasset::MultiAsset +type XcmV2MultiAsset = { id: XcmV2AssetId; fun: XcmV2Fungibility } +const $xcmV2MultiAsset: $.Shape = $.object( + $.field("id", $xcmV2AssetId), + $.field("fun", $xcmV2Fungibility) +) + +// Type 386 - xcm::v2::multiasset::MultiAssets +type XcmV2MultiAssets = Array +const $xcmV2MultiAssets: $.Shape = $.array($xcmV2MultiAsset) + +// Type 168 - xcm::v3::junction::NetworkId +type XcmV3NetworkId = + | { type: "ByGenesis"; value: Uint8Array } + | { type: "ByFork"; blockNumber: bigint; blockHash: Uint8Array } + | { type: "Polkadot" } + | { type: "Kusama" } + | { type: "Westend" } + | { type: "Rococo" } + | { type: "Wococo" } + | { type: "Ethereum"; chainId: bigint } + | { type: "BitcoinCore" } + | { type: "BitcoinCash" } +const $xcmV3NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("ByGenesis", $.field("value", $.sizedUint8Array(32))), + 1: $.variant( + "ByFork", + $.field("blockNumber", $.u64), + $.field("blockHash", $.sizedUint8Array(32)) + ), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), + 4: $.variant("Westend"), + 5: $.variant("Rococo"), + 6: $.variant("Wococo"), + 7: $.variant("Ethereum", $.field("chainId", $.compact($.u64))), + 8: $.variant("BitcoinCore"), + 9: $.variant("BitcoinCash"), +}) + +// Type 167 - Option +// Param T : 168 +type Option167 = XcmV3NetworkId | undefined +const $option167: $.Shape = $.option($xcmV3NetworkId) + +// Type 169 - xcm::v3::junction::BodyId +type XcmV3BodyId = + | { type: "Unit" } + | { type: "Moniker"; value: Uint8Array } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV3BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Moniker", $.field("value", $.sizedUint8Array(4))), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 170 - xcm::v3::junction::BodyPart +type XcmV3BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV3BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 166 - xcm::v3::junction::Junction +type XcmV3Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: Option167; id: Uint8Array } + | { type: "AccountIndex64"; network: Option167; index: bigint } + | { type: "AccountKey20"; network: Option167; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { type: "GeneralKey"; length: number; data: Uint8Array } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV3BodyId; part: XcmV3BodyPart } + | { type: "GlobalConsensus"; value: XcmV3NetworkId } +const $xcmV3Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $.option($xcmV3NetworkId)), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $option167) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $option167) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("length", $.u8), $.field("data", $.sizedUint8Array(32))), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV3BodyId), $.field("part", $xcmV3BodyPart)), + 9: $.variant( + "GlobalConsensus", + $.field( + "value", + $.deferred(() => $xcmV3NetworkId) + ) + ), +}) + +// Type 165 - xcm::v3::junctions::Junctions +type XcmV3Junctions = + | { type: "Here" } + | { type: "X1"; value: XcmV3Junction } + | { type: "X2"; value: [XcmV3Junction, XcmV3Junction] } + | { type: "X3"; value: [XcmV3Junction, XcmV3Junction, XcmV3Junction] } + | { + type: "X4" + value: [XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction] + } + | { + type: "X5" + value: [XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction] + } + | { + type: "X6" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } + | { + type: "X7" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } + | { + type: "X8" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } +const $xcmV3Junctions: $.Shape = $.taggedUnion("type", { + 0: $.variant("Here"), + 1: $.variant("X1", $.field("value", $xcmV3Junction)), + 2: $.variant( + "X2", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 3: $.variant( + "X3", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 4: $.variant( + "X4", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 5: $.variant( + "X5", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 6: $.variant( + "X6", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 7: $.variant( + "X7", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 8: $.variant( + "X8", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), +}) + +// Type 164 - xcm::v3::multilocation::MultiLocation +type XcmV3MultiLocation = { parents: number; interior: XcmV3Junctions } +const $xcmV3MultiLocation: $.Shape = $.object( + $.field("parents", $.u8), + $.field("interior", $xcmV3Junctions) +) + +// Type 408 - xcm::v3::multiasset::AssetId +type XcmV3AssetId = + | { type: "Concrete"; value: XcmV3MultiLocation } + | { type: "Abstract"; value: Uint8Array } +const $xcmV3AssetId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Concrete", $.field("value", $xcmV3MultiLocation)), + 1: $.variant("Abstract", $.field("value", $.sizedUint8Array(32))), +}) + +// Type 410 - xcm::v3::multiasset::AssetInstance +type XcmV3AssetInstance = + | { type: "Undefined" } + | { type: "Index"; value: bigint } + | { type: "Array4"; value: Uint8Array } + | { type: "Array8"; value: Uint8Array } + | { type: "Array16"; value: Uint8Array } + | { type: "Array32"; value: Uint8Array } +const $xcmV3AssetInstance: $.Shape = $.taggedUnion("type", { + 0: $.variant("Undefined"), + 1: $.variant("Index", $.field("value", $.compact($.u128))), + 2: $.variant("Array4", $.field("value", $.sizedUint8Array(4))), + 3: $.variant("Array8", $.field("value", $.sizedUint8Array(8))), + 4: $.variant("Array16", $.field("value", $.sizedUint8Array(16))), + 5: $.variant("Array32", $.field("value", $.sizedUint8Array(32))), +}) + +// Type 409 - xcm::v3::multiasset::Fungibility +type XcmV3Fungibility = + | { type: "Fungible"; value: bigint } + | { type: "NonFungible"; value: XcmV3AssetInstance } +const $xcmV3Fungibility: $.Shape = $.taggedUnion("type", { + 0: $.variant("Fungible", $.field("value", $.compact($.u128))), + 1: $.variant("NonFungible", $.field("value", $xcmV3AssetInstance)), +}) + +// Type 407 - xcm::v3::multiasset::MultiAsset +type XcmV3MultiAsset = { id: XcmV3AssetId; fun: XcmV3Fungibility } +const $xcmV3MultiAsset: $.Shape = $.object( + $.field("id", $xcmV3AssetId), + $.field("fun", $xcmV3Fungibility) +) + +// Type 405 - xcm::v3::multiasset::MultiAssets +type XcmV3MultiAssets = Array +const $xcmV3MultiAssets: $.Shape = $.array($xcmV3MultiAsset) + +// Type 427 - xcm::VersionedMultiAssets +export type VersionedMultiAssets = + | { type: "V2"; value: XcmV2MultiAssets } + | { type: "V3"; value: XcmV3MultiAssets } + +export const $versionedMultiAssets: $.Shape = $.taggedUnion("type", { + 1: $.variant("V2", $.field("value", $xcmV2MultiAssets)), + 3: $.variant("V3", $.field("value", $xcmV3MultiAssets)), +}) diff --git a/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiLocation.ts b/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiLocation.ts new file mode 100644 index 0000000000..4720a6c1b0 --- /dev/null +++ b/apps/extension/src/ui/domains/Sign/Substrate/shapes/VersionedMultiLocation.ts @@ -0,0 +1,559 @@ +import * as $ from "subshape" + +// Type 119 - bounded_collections::weak_bounded_vec::WeakBoundedVec +// Param T : 2 +type BoundedCollectionsWeakBoundedVecWeakBoundedVec119 = Uint8Array +const $boundedCollectionsWeakBoundedVecWeakBoundedVec119: $.Shape = + $.uint8Array + +// Type 118 - xcm::v2::NetworkId +type XcmV2NetworkId = + | { type: "Any" } + | { type: "Named"; value: BoundedCollectionsWeakBoundedVecWeakBoundedVec119 } + | { type: "Polkadot" } + | { type: "Kusama" } +const $xcmV2NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Any"), + 1: $.variant("Named", $.field("value", $.uint8Array)), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), +}) + +// Type 120 - xcm::v2::BodyId +type XcmV2BodyId = + | { type: "Unit" } + | { type: "Named"; value: BoundedCollectionsWeakBoundedVecWeakBoundedVec119 } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV2BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Named", $.field("value", $boundedCollectionsWeakBoundedVecWeakBoundedVec119)), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 121 - xcm::v2::BodyPart +type XcmV2BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV2BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 117 - xcm::v2::junction::Junction +type XcmV2Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: XcmV2NetworkId; id: Uint8Array } + | { type: "AccountIndex64"; network: XcmV2NetworkId; index: bigint } + | { type: "AccountKey20"; network: XcmV2NetworkId; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { + type: "GeneralKey" + value: BoundedCollectionsWeakBoundedVecWeakBoundedVec119 + } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV2BodyId; part: XcmV2BodyPart } +const $xcmV2Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $xcmV2NetworkId), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $xcmV2NetworkId) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $xcmV2NetworkId) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("value", $boundedCollectionsWeakBoundedVecWeakBoundedVec119)), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV2BodyId), $.field("part", $xcmV2BodyPart)), +}) + +// Type 116 - xcm::v2::multilocation::Junctions +type XcmV2Junctions = + | { type: "Here" } + | { type: "X1"; value: XcmV2Junction } + | { type: "X2"; value: [XcmV2Junction, XcmV2Junction] } + | { type: "X3"; value: [XcmV2Junction, XcmV2Junction, XcmV2Junction] } + | { + type: "X4" + value: [XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction] + } + | { + type: "X5" + value: [XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction, XcmV2Junction] + } + | { + type: "X6" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } + | { + type: "X7" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } + | { + type: "X8" + value: [ + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction, + XcmV2Junction + ] + } +const $xcmV2Junctions: $.Shape = $.taggedUnion("type", { + 0: $.variant("Here"), + 1: $.variant("X1", $.field("value", $xcmV2Junction)), + 2: $.variant( + "X2", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 3: $.variant( + "X3", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 4: $.variant( + "X4", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 5: $.variant( + "X5", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 6: $.variant( + "X6", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 7: $.variant( + "X7", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), + 8: $.variant( + "X8", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction), + $.deferred(() => $xcmV2Junction) + ) + ) + ), +}) + +// Type 115 - xcm::v2::multilocation::MultiLocation +type XcmV2MultiLocation = { parents: number; interior: XcmV2Junctions } +const $xcmV2MultiLocation: $.Shape = $.object( + $.field("parents", $.u8), + $.field("interior", $xcmV2Junctions) +) + +// Type 79 - xcm::v3::junction::NetworkId +type XcmV3NetworkId = + | { type: "ByGenesis"; value: Uint8Array } + | { type: "ByFork"; blockNumber: bigint; blockHash: Uint8Array } + | { type: "Polkadot" } + | { type: "Kusama" } + | { type: "Westend" } + | { type: "Rococo" } + | { type: "Wococo" } + | { type: "Ethereum"; chainId: bigint } + | { type: "BitcoinCore" } + | { type: "BitcoinCash" } +const $xcmV3NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("ByGenesis", $.field("value", $.sizedUint8Array(32))), + 1: $.variant( + "ByFork", + $.field("blockNumber", $.u64), + $.field("blockHash", $.sizedUint8Array(32)) + ), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), + 4: $.variant("Westend"), + 5: $.variant("Rococo"), + 6: $.variant("Wococo"), + 7: $.variant("Ethereum", $.field("chainId", $.compact($.u64))), + 8: $.variant("BitcoinCore"), + 9: $.variant("BitcoinCash"), +}) + +// Type 78 - Option +// Param T : 79 +type Option78 = XcmV3NetworkId | undefined +const $option78: $.Shape = $.option($xcmV3NetworkId) + +// Type 80 - xcm::v3::junction::BodyId +type XcmV3BodyId = + | { type: "Unit" } + | { type: "Moniker"; value: Uint8Array } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV3BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Moniker", $.field("value", $.sizedUint8Array(4))), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 81 - xcm::v3::junction::BodyPart +type XcmV3BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV3BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 76 - xcm::v3::junction::Junction +type XcmV3Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: Option78; id: Uint8Array } + | { type: "AccountIndex64"; network: Option78; index: bigint } + | { type: "AccountKey20"; network: Option78; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { type: "GeneralKey"; length: number; data: Uint8Array } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV3BodyId; part: XcmV3BodyPart } + | { type: "GlobalConsensus"; value: XcmV3NetworkId } +const $xcmV3Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $.option($xcmV3NetworkId)), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $option78) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $option78) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("length", $.u8), $.field("data", $.sizedUint8Array(32))), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV3BodyId), $.field("part", $xcmV3BodyPart)), + 9: $.variant( + "GlobalConsensus", + $.field( + "value", + $.deferred(() => $xcmV3NetworkId) + ) + ), +}) + +// Type 75 - xcm::v3::junctions::Junctions +type XcmV3Junctions = + | { type: "Here" } + | { type: "X1"; value: XcmV3Junction } + | { type: "X2"; value: [XcmV3Junction, XcmV3Junction] } + | { type: "X3"; value: [XcmV3Junction, XcmV3Junction, XcmV3Junction] } + | { + type: "X4" + value: [XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction] + } + | { + type: "X5" + value: [XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction, XcmV3Junction] + } + | { + type: "X6" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } + | { + type: "X7" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } + | { + type: "X8" + value: [ + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction, + XcmV3Junction + ] + } +const $xcmV3Junctions: $.Shape = $.taggedUnion("type", { + 0: $.variant("Here"), + 1: $.variant("X1", $.field("value", $xcmV3Junction)), + 2: $.variant( + "X2", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 3: $.variant( + "X3", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 4: $.variant( + "X4", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 5: $.variant( + "X5", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 6: $.variant( + "X6", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 7: $.variant( + "X7", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), + 8: $.variant( + "X8", + $.field( + "value", + $.tuple( + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction), + $.deferred(() => $xcmV3Junction) + ) + ) + ), +}) + +// Type 74 - xcm::v3::multilocation::MultiLocation +type XcmV3MultiLocation = { parents: number; interior: XcmV3Junctions } +const $xcmV3MultiLocation: $.Shape = $.object( + $.field("parents", $.u8), + $.field("interior", $xcmV3Junctions) +) + +// Type 124 - xcm::VersionedMultiLocation +export type VersionedMultiLocation = + | { type: "V2"; value: XcmV2MultiLocation } + | { type: "V3"; value: XcmV3MultiLocation } + +export const $versionedMultiLocation: $.Shape = $.taggedUnion("type", { + 1: $.variant("V2", $.field("value", $xcmV2MultiLocation)), + 3: $.variant("V3", $.field("value", $xcmV3MultiLocation)), +}) diff --git a/apps/extension/src/ui/domains/Sign/Substrate/shapes/XcmV3Junction.ts b/apps/extension/src/ui/domains/Sign/Substrate/shapes/XcmV3Junction.ts new file mode 100644 index 0000000000..f7d2145770 --- /dev/null +++ b/apps/extension/src/ui/domains/Sign/Substrate/shapes/XcmV3Junction.ts @@ -0,0 +1,133 @@ +import * as $ from "subshape" + +// Type 79 - xcm::v3::junction::NetworkId +type XcmV3NetworkId = + | { type: "ByGenesis"; value: Uint8Array } + | { type: "ByFork"; blockNumber: bigint; blockHash: Uint8Array } + | { type: "Polkadot" } + | { type: "Kusama" } + | { type: "Westend" } + | { type: "Rococo" } + | { type: "Wococo" } + | { type: "Ethereum"; chainId: bigint } + | { type: "BitcoinCore" } + | { type: "BitcoinCash" } +const $xcmV3NetworkId: $.Shape = $.taggedUnion("type", { + 0: $.variant("ByGenesis", $.field("value", $.sizedUint8Array(32))), + 1: $.variant( + "ByFork", + $.field("blockNumber", $.u64), + $.field("blockHash", $.sizedUint8Array(32)) + ), + 2: $.variant("Polkadot"), + 3: $.variant("Kusama"), + 4: $.variant("Westend"), + 5: $.variant("Rococo"), + 6: $.variant("Wococo"), + 7: $.variant("Ethereum", $.field("chainId", $.compact($.u64))), + 8: $.variant("BitcoinCore"), + 9: $.variant("BitcoinCash"), +}) + +// Type 78 - Option +// Param T : 79 +type Option78 = XcmV3NetworkId | undefined +const $option78: $.Shape = $.option($xcmV3NetworkId) + +// Type 80 - xcm::v3::junction::BodyId +type XcmV3BodyId = + | { type: "Unit" } + | { type: "Moniker"; value: Uint8Array } + | { type: "Index"; value: number } + | { type: "Executive" } + | { type: "Technical" } + | { type: "Legislative" } + | { type: "Judicial" } + | { type: "Defense" } + | { type: "Administration" } + | { type: "Treasury" } +const $xcmV3BodyId: $.Shape = $.taggedUnion("type", { + 0: $.variant("Unit"), + 1: $.variant("Moniker", $.field("value", $.sizedUint8Array(4))), + 2: $.variant("Index", $.field("value", $.compact($.u32))), + 3: $.variant("Executive"), + 4: $.variant("Technical"), + 5: $.variant("Legislative"), + 6: $.variant("Judicial"), + 7: $.variant("Defense"), + 8: $.variant("Administration"), + 9: $.variant("Treasury"), +}) + +// Type 81 - xcm::v3::junction::BodyPart +type XcmV3BodyPart = + | { type: "Voice" } + | { type: "Members"; count: number } + | { type: "Fraction"; nom: number; denom: number } + | { type: "AtLeastProportion"; nom: number; denom: number } + | { type: "MoreThanProportion"; nom: number; denom: number } +const $xcmV3BodyPart: $.Shape = $.taggedUnion("type", { + 0: $.variant("Voice"), + 1: $.variant("Members", $.field("count", $.compact($.u32))), + 2: $.variant("Fraction", $.field("nom", $.compact($.u32)), $.field("denom", $.compact($.u32))), + 3: $.variant( + "AtLeastProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), + 4: $.variant( + "MoreThanProportion", + $.field("nom", $.compact($.u32)), + $.field("denom", $.compact($.u32)) + ), +}) + +// Type 76 - xcm::v3::junction::Junction +export type XcmV3Junction = + | { type: "Parachain"; value: number } + | { type: "AccountId32"; network: Option78; id: Uint8Array } + | { type: "AccountIndex64"; network: Option78; index: bigint } + | { type: "AccountKey20"; network: Option78; key: Uint8Array } + | { type: "PalletInstance"; value: number } + | { type: "GeneralIndex"; value: bigint } + | { type: "GeneralKey"; length: number; data: Uint8Array } + | { type: "OnlyChild" } + | { type: "Plurality"; id: XcmV3BodyId; part: XcmV3BodyPart } + | { type: "GlobalConsensus"; value: XcmV3NetworkId } + +export const $xcmV3Junction: $.Shape = $.taggedUnion("type", { + 0: $.variant("Parachain", $.field("value", $.compact($.u32))), + 1: $.variant( + "AccountId32", + $.field("network", $.option($xcmV3NetworkId)), + $.field("id", $.sizedUint8Array(32)) + ), + 2: $.variant( + "AccountIndex64", + $.field( + "network", + $.deferred(() => $option78) + ), + $.field("index", $.compact($.u64)) + ), + 3: $.variant( + "AccountKey20", + $.field( + "network", + $.deferred(() => $option78) + ), + $.field("key", $.sizedUint8Array(20)) + ), + 4: $.variant("PalletInstance", $.field("value", $.u8)), + 5: $.variant("GeneralIndex", $.field("value", $.compact($.u128))), + 6: $.variant("GeneralKey", $.field("length", $.u8), $.field("data", $.sizedUint8Array(32))), + 7: $.variant("OnlyChild"), + 8: $.variant("Plurality", $.field("id", $xcmV3BodyId), $.field("part", $xcmV3BodyPart)), + 9: $.variant( + "GlobalConsensus", + $.field( + "value", + $.deferred(() => $xcmV3NetworkId) + ) + ), +}) diff --git a/apps/extension/src/ui/domains/Sign/Substrate/xTokens/SubSignXTokensTransfer.tsx b/apps/extension/src/ui/domains/Sign/Substrate/xTokens/SubSignXTokensTransfer.tsx index cb7d2c9773..411b6afcf7 100644 --- a/apps/extension/src/ui/domains/Sign/Substrate/xTokens/SubSignXTokensTransfer.tsx +++ b/apps/extension/src/ui/domains/Sign/Substrate/xTokens/SubSignXTokensTransfer.tsx @@ -1,6 +1,5 @@ import { log } from "@core/log" import { Codec } from "@polkadot/types-codec/types" -import { JunctionV1, VersionedMultiLocation } from "@polkadot/types/interfaces/xcm" import { assert } from "@polkadot/util" import { Address } from "@talismn/balances" import { Chain, Token } from "@talismn/chaindata-provider" @@ -9,13 +8,37 @@ import useChains from "@ui/hooks/useChains" import { useExtrinsic } from "@ui/hooks/useExtrinsic" import { useTokenRatesMap } from "@ui/hooks/useTokenRatesMap" import useTokens from "@ui/hooks/useTokens" +import isEqual from "lodash/isEqual" import { useMemo } from "react" import { useTranslation } from "react-i18next" +import * as $ from "subshape" import { SignContainer } from "../../SignContainer" import { usePolkadotSigningRequest } from "../../SignRequestContext" +import { SignViewBodyShimmer } from "../../Views/SignViewBodyShimmer" import { SignViewIconHeader } from "../../Views/SignViewIconHeader" import { SignViewXTokensTransfer } from "../../Views/transfer/SignViewCrossChainTransfer" +import { $versionedMultiLocation, VersionedMultiLocation } from "../shapes/VersionedMultiLocation" +import { XcmV3Junction } from "../shapes/XcmV3Junction" + +const normalizeTokenId = (tokenId: unknown) => { + if (typeof tokenId === "string" && tokenId.startsWith("{") && tokenId.endsWith("}")) + tokenId = JSON.parse(tokenId) + if (typeof tokenId === "object") { + // some property names don't have the same case in chaindata. ex: vsKSM + return Object.entries(tokenId as Record).reduce((acc, [key, value]) => { + acc[key.toLowerCase()] = typeof value === "string" ? value.toLowerCase() : value + return acc + }, {} as Record) + } + return tokenId +} + +const isSameTokenId = (tokenId1: unknown, tokenId2: unknown) => { + tokenId1 = normalizeTokenId(tokenId1) + tokenId2 = normalizeTokenId(tokenId2) + return isEqual(tokenId1, tokenId2) +} const getTokenFromCurrency = (currency: Codec, chain: Chain, tokens: Token[]): Token => { // ex: HDX @@ -27,42 +50,45 @@ const getTokenFromCurrency = (currency: Codec, chain: Chain, tokens: Token[]): T log.warn("unknown currencyId %d on chain %s", currencyId, chain.id) } + const jsonCurrency = currency.toJSON() + // eslint-disable-next-line @typescript-eslint/no-explicit-any const unsafeCurrency = currency as any - if (unsafeCurrency.isToken) { - const token = tokens.find( - (t) => - // ex: ACA - (t.type === "substrate-native" && - t.symbol.toLowerCase() === unsafeCurrency.asToken.type.toLowerCase()) || - // ex: aUSD - (t.type === "substrate-tokens" && - t.onChainId?.toString()?.toLowerCase() === unsafeCurrency.toString().toLowerCase()) - ) - if (token) return token - } + const lsymbol = (unsafeCurrency.isToken ? unsafeCurrency.asToken.type : "").toLowerCase() + + const token = tokens.find( + (t) => + // ex: ACA + (t.type === "substrate-native" && t.symbol.toLowerCase() === lsymbol) || + (t.type === "substrate-tokens" && + // ex: vsKSM + (isSameTokenId(t.onChainId, jsonCurrency) || + // ex: aUSD + t.onChainId?.toString()?.toLowerCase() === jsonCurrency?.toString().toLowerCase())) + ) + if (token) return token // throw an error so the sign popup fallbacks to default view log.warn("unknown on chain %s", chain.id, currency.toHuman()) throw new Error("Token not found") } -const getTargetFromInteriorV1 = ( - interior: JunctionV1, +const getTargetFromInterior = ( + interior: XcmV3Junction, chain: Chain, chains: Chain[] ): { chain?: Chain; address?: Address } => { - if (interior.isParachain) { - const paraId = interior.asParachain.toNumber() + if (interior.type === "Parachain") { + const paraId = interior.value const relayId = chain.relay ? chain.relay.id : chain.id const targetChain = chains.find((c) => c.relay?.id === relayId && c.paraId === paraId) if (targetChain) return { chain: targetChain } } - if (interior.isAccountKey20) return { address: interior.asAccountKey20.key.toString() } - if (interior.isAccountId32) return { address: interior.asAccountId32.id.toString() } + if (interior.type === "AccountKey20") return { address: encodeAnyAddress(interior.key) } + if (interior.type === "AccountId32") return { address: encodeAnyAddress(interior.id) } // throw an error so the sign popup fallbacks to default view - log.warn("Unsupported interior", interior.toHuman()) + //log.warn("Unsupported interior", interior.toHuman()) throw new Error("Unknown interior") } @@ -72,24 +98,31 @@ const getTarget = ( chains: Chain[], address: Address ): { chain?: Chain; address?: Address } => { - if (multiLocation?.isV1) { + if (multiLocation?.type === "V3") { // const parents = multiLocation.asV1.parents.toNumber() - const interior = multiLocation.asV1.interior - if (interior.isHere && chain) return { chain, address } - - if (interior.isX1 && chain && interior.asX1.isParachain) { - const paraId = interior.asX1.asParachain.toNumber() - const relayId = chain.relay ? chain.relay.id : chain.id - const targetChain = chains.find((c) => c.relay?.id === relayId && c.paraId === paraId) - if (targetChain) return { chain: targetChain, address } + const interior = multiLocation.value.interior + if (interior.type === "Here" && chain) return { chain, address } + + if (interior.type === "X1") { + if (interior.value.type === "Parachain") { + const paraId = interior.value.value + const relayId = chain.relay ? chain.relay.id : chain.id + const targetChain = chains.find((c) => c.relay?.id === relayId && c.paraId === paraId) + if (targetChain) return { chain: targetChain, address } + } + + const targetChain = + multiLocation.value.parents === 1 ? chains.find((c) => c.id === chain.relay?.id) : chain + if (interior.value.type === "AccountKey20") + return { chain: targetChain, address: encodeAnyAddress(interior.value.key) } + if (interior.value.type === "AccountId32") + return { chain: targetChain, address: encodeAnyAddress(interior.value.id) } } - if (interior.isX1 && chain && interior.asX1.isAccountKey20) - return { chain, address: interior.asX1.asAccountKey20.key.toString() } - if (interior.isX2) { - const interiorX2 = interior.asX2 - const res0 = getTargetFromInteriorV1(interiorX2[0], chain, chains) - const res1 = getTargetFromInteriorV1(interiorX2[1], chain, chains) + if (interior.type === "X2") { + const interiorX2 = interior.value + const res0 = getTargetFromInterior(interiorX2[0], chain, chains) + const res1 = getTargetFromInterior(interiorX2[1], chain, chains) const resChain = res0.chain || res1.chain const resAddress = res0.address || res1.address if (!resChain || !resAddress) throw new Error("Unknown multi location") @@ -101,7 +134,7 @@ const getTarget = ( } // throw an error so the sign popup fallbacks to default view - log.warn("Unsupported multi location", multiLocation?.toHuman()) + //log.warn("Unsupported multi location", multiLocation?.toHuman()) throw new Error("Unknown multi location") } @@ -114,12 +147,15 @@ export const SubSignXTokensTransfer = () => { const { chains } = useChains(true) const props = useMemo(() => { + // wait for tokens to be loaded + if (!tokens.length) return null assert(extrinsic, "No extrinsic") assert(chain, "No chain") - const currency = extrinsic.method?.args[0] // as any - const value = extrinsic.registry.createType("u128", extrinsic.method?.args[1]).toBigInt() - const dest = extrinsic.registry.createType("VersionedMultiLocation", extrinsic.method?.args[2]) + // CurrencyId - currency ids are chain specific, can't use subshape easily + const currency = extrinsic.method.args[0] // as any + const value = $.u128.decode(extrinsic.method.args[1].toU8a()) + const dest = $versionedMultiLocation.decode(extrinsic.method.args[2].toU8a()) const token = getTokenFromCurrency( currency, @@ -129,7 +165,6 @@ export const SubSignXTokensTransfer = () => { const target = getTarget(dest, chain, chains, account.address) assert(target.chain && target.address, "Unknown target") - return { value, tokenDecimals: token.decimals, @@ -143,6 +178,8 @@ export const SubSignXTokensTransfer = () => { } }, [extrinsic, chain, tokens, chains, account.address, tokenRates, payload.address]) + if (!props) return + return ( { - if (multiAsset?.isV1) { + if (multiAsset?.type === "V3") { // our view only support displaying one asset - if (multiAsset.asV1.length === 1) { - const asset = multiAsset.asV1[0] - const fungible = asset.getAtIndex(1) as FungibilityV1 // property fungibility isn't mapped properly on pjs typings + if (multiAsset.value.length === 1) { + const asset = multiAsset.value[0] + + if (asset?.id.type === "Concrete" && asset.fun.type === "Fungible") { + const value = asset.fun.value + const interior = asset.id.value.interior + if (interior.type === "Here" && chain?.nativeToken?.id) { + return { tokenId: chain.nativeToken.id, value } + } + if (interior.type === "X2") { + if ( + interior.value[0].type === "PalletInstance" && + interior.value[0].value === 50 && + interior.value[1].type === "GeneralIndex" + ) { + // Assets pallet + const assetId = interior.value[1].value + // at this stage we don't know the symbol but we know the start of the id + const search = `${chain?.id}-substrate-assets-${assetId}` + const tokenId = Object.keys(tokens).find((id) => id.startsWith(search)) - if (asset?.id.isConcrete && fungible.isFungible) { - if (asset.id.asConcrete.interior.isHere && chain?.nativeToken?.id) { - return { tokenId: chain.nativeToken.id, value: fungible.asFungible.toBigInt() } + if (!tokenId) throw new Error("Unknown multi asset") + + return { tokenId, value } + } } } } } // throw an error so the sign popup fallbacks to default view + log.warn("Unknown multi asset", { multiAsset, chain }) throw new Error("Unknown multi asset") } -const getTargetChainId = ( - multiLocation: VersionedMultiLocation | undefined, +const getTargetChain = ( + multiLocation: VersionedMultiLocation, chain: Chain | null | undefined, chains: Chain[] ): Chain => { - if (multiLocation?.isV1) { + if (multiLocation.type === "V3") { // const parents = multiLocation.asV1.parents.toNumber() - const interior = multiLocation.asV1.interior - if (interior.isHere && chain) return chain - if (interior.isX1 && chain && interior.asX1.isParachain) { - const paraId = interior.asX1.asParachain.toNumber() + const interior = multiLocation.value.interior + if (interior.type === "Here" && chain) return chain + if (interior.type === "X1" && chain && interior.value.type === "Parachain") { + const paraId = interior.value.value const relayId = chain.relay ? chain.relay.id : chain.id const targetChain = chains.find((c) => c.relay?.id === relayId && c.paraId === paraId) if (targetChain) return targetChain @@ -60,7 +78,7 @@ const getTargetChainId = ( } // throw an error so the sign popup fallbacks to default view - log.warn("Unknown multi location", multiLocation?.toHuman()) + log.warn("Unknown multi location", multiLocation) throw new Error("Unknown multi location") } @@ -68,17 +86,17 @@ const getTargetAccount = ( multiLocation: VersionedMultiLocation | undefined, account: AccountJsonAny ): Address => { - if (multiLocation?.isV1) { + if (multiLocation?.type === "V3") { // const parents = multiLocation.asV1.parents.toNumber() - const interior = multiLocation.asV1.interior - if (interior.isHere && account) return account.address - if (interior.isX1) { - if (interior.asX1.isAccountKey20) return interior.asX1.asAccountKey20.key.toString() - if (interior.asX1.isAccountId32) return interior.asX1.asAccountId32.id.toString() + const interior = multiLocation.value.interior + if (interior.type === "Here" && account) return account.address + if (interior.type === "X1") { + if (interior.value.type === "AccountKey20") return encodeAnyAddress(interior.value.key) + if (interior.value.type === "AccountId32") return encodeAnyAddress(interior.value.id) } } // throw an error so the sign popup fallbacks to default view - log.warn("Unknown multi location", multiLocation?.toHuman()) + log.warn("Unknown multi location", multiLocation) throw new Error("Unknown multi location") } @@ -91,22 +109,19 @@ export const SubSignXcmTransferAssets = () => { const { chains } = useChains(true) const props = useMemo(() => { + if (Object.keys(tokensMap).length === 0) return null if (!chain) throw new Error("Unknown chain") if (!extrinsic) throw new Error("Unknown extrinsic") - // Note: breaks here fore statemine assets. hoping that next version of @polkadot/api will fix it - const dest = extrinsic.registry.createType("VersionedMultiLocation", extrinsic.method.args[0]) - const beneficiary = extrinsic.registry.createType( - "VersionedMultiLocation", - extrinsic.method.args[1] - ) - const assets = extrinsic.registry.createType("VersionedMultiAssets", extrinsic.method.args[2]) + const dest = $versionedMultiLocation.decode(extrinsic.method.args[0].toU8a()) + const beneficiary = $versionedMultiLocation.decode(extrinsic.method.args[1].toU8a()) + const assets = $versionedMultiAssets.decode(extrinsic.method.args[2].toU8a()) - const { tokenId, value } = getMultiAssetTokenId(assets, chain) + const { tokenId, value } = getMultiAssetTokenId(assets, chain, tokensMap) const token = tokensMap[tokenId] if (!token) throw new Error("Unknown token") - const toNetwork = getTargetChainId(dest, chain, chains) + const toNetwork = getTargetChain(dest, chain, chains) const toAddress = getTargetAccount(beneficiary, account) return { @@ -122,6 +137,8 @@ export const SubSignXcmTransferAssets = () => { } }, [chain, extrinsic, tokensMap, chains, account, tokenRates, payload.address]) + if (!props) return + return ( Date: Mon, 13 Nov 2023 11:35:01 +0800 Subject: [PATCH 7/8] Some more sentry noise fixes (#1151) * chore: add regex patterns for errors to ignore in sentry config * fix: handle unhandled promise rejection in substrate connections * fix: revert last and make throwAfter reject with a real Error object * fix: catch and emit error in websocket connection, when response is not JSON * chore: changeset * fix: handle error states arising from change to throwAfter function * fix: improve error handling in Websocket * chore: changeset --- .changeset/long-toys-act.md | 5 +++++ .changeset/quiet-papayas-cheat.md | 6 ++++++ apps/extension/src/core/config/sentry.ts | 4 ++++ .../src/core/domains/ethereum/handler.tabs.ts | 1 + .../transactions/watchEthereumTransaction.ts | 2 ++ .../src/ui/hooks/ledger/useLedgerEthereum.ts | 1 + packages/chain-connector/src/Websocket.ts | 13 ++++++++----- packages/util/src/throwAfter.ts | 2 +- 8 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 .changeset/long-toys-act.md create mode 100644 .changeset/quiet-papayas-cheat.md diff --git a/.changeset/long-toys-act.md b/.changeset/long-toys-act.md new file mode 100644 index 0000000000..4ca3635e0e --- /dev/null +++ b/.changeset/long-toys-act.md @@ -0,0 +1,5 @@ +--- +"@talismn/chain-connector": patch +--- + +Improved error handling in Websocket connector diff --git a/.changeset/quiet-papayas-cheat.md b/.changeset/quiet-papayas-cheat.md new file mode 100644 index 0000000000..92db0c3a8e --- /dev/null +++ b/.changeset/quiet-papayas-cheat.md @@ -0,0 +1,6 @@ +--- +"@talismn/chain-connector": patch +"@talismn/util": patch +--- + +Error handling improvements diff --git a/apps/extension/src/core/config/sentry.ts b/apps/extension/src/core/config/sentry.ts index 9f3e7d0582..33c883b3d1 100644 --- a/apps/extension/src/core/config/sentry.ts +++ b/apps/extension/src/core/config/sentry.ts @@ -22,6 +22,10 @@ export const initSentry = (sentry: typeof SentryBrowser | typeof SentryReact) => release: process.env.RELEASE, sampleRate: 1, maxBreadcrumbs: 20, + ignoreErrors: [ + /(No window with id: )(\d+).?/, + /(disconnected from wss)[(]?:\/\/[\w./:-]+: \d+:: Normal Closure[)]?/, + ], // prevents sending the event if user has disabled error tracking beforeSend: async (event) => ((await firstValueFrom(useErrorTracking)) ? event : null), // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/apps/extension/src/core/domains/ethereum/handler.tabs.ts b/apps/extension/src/core/domains/ethereum/handler.tabs.ts index ab7f563845..56e1a049b3 100644 --- a/apps/extension/src/core/domains/ethereum/handler.tabs.ts +++ b/apps/extension/src/core/domains/ethereum/handler.tabs.ts @@ -337,6 +337,7 @@ export class EthTabsHandler extends TabsHandler { client.request({ method: "eth_chainId" }), throwAfter(10_000, "timeout"), // 10 sec timeout ]) + assert(!!rpcChainIdHex, `No chainId returned for ${rpcUrl}`) const rpcChainId = hexToNumber(rpcChainIdHex) assert(rpcChainId === chainId, "chainId mismatch") diff --git a/apps/extension/src/core/domains/transactions/watchEthereumTransaction.ts b/apps/extension/src/core/domains/transactions/watchEthereumTransaction.ts index 74df0264ae..8454a2bba4 100644 --- a/apps/extension/src/core/domains/transactions/watchEthereumTransaction.ts +++ b/apps/extension/src/core/domains/transactions/watchEthereumTransaction.ts @@ -4,6 +4,7 @@ import { log } from "@core/log" import { createNotification } from "@core/notifications" import { chainConnectorEvm } from "@core/rpcs/chain-connector-evm" import { chaindataProvider } from "@core/rpcs/chaindata" +import { assert } from "@polkadot/util" import * as Sentry from "@sentry/browser" import { EvmNetworkId } from "@talismn/chaindata-provider" import { sleep, throwAfter } from "@talismn/util" @@ -57,6 +58,7 @@ export const watchEthereumTransaction = async ( throwAfter(5 * 60_000, "Transaction not found"), ]) + assert(receipt, "Transaction to watch not found") // check hash which may be incorrect for cancelled tx, in which case receipt includes the replacement tx hash if (receipt.transactionHash === hash) { // to test failing transactions, swap on busy AMM pools with a 0.05% slippage limit diff --git a/apps/extension/src/ui/hooks/ledger/useLedgerEthereum.ts b/apps/extension/src/ui/hooks/ledger/useLedgerEthereum.ts index 7cbc40910d..b3ad363599 100644 --- a/apps/extension/src/ui/hooks/ledger/useLedgerEthereum.ts +++ b/apps/extension/src/ui/hooks/ledger/useLedgerEthereum.ts @@ -53,6 +53,7 @@ export const useLedgerEthereum = (persist = false) => { ledger.getAddress(getEthLedgerDerivationPath("LedgerLive")), throwAfter(5_000, "Timeout"), ]) + const { version } = await ledger.getAppConfiguration() if (!gte(version, LEDGER_ETHEREUM_MIN_VERSION)) throw new LedgerError("Unsupported version", "UnsupportedVersion") diff --git a/packages/chain-connector/src/Websocket.ts b/packages/chain-connector/src/Websocket.ts index 0c2aae812d..d72739daa0 100644 --- a/packages/chain-connector/src/Websocket.ts +++ b/packages/chain-connector/src/Websocket.ts @@ -460,12 +460,15 @@ export class Websocket implements ProviderInterface { #onSocketMessage = (message: MessageEvent): void => { // log.debug(() => ["received", message.data]) + try { + const response = JSON.parse(message.data) as UnknownJsonRpcResponse - const response = JSON.parse(message.data) as UnknownJsonRpcResponse - - return isUndefined(response.method) - ? this.#onSocketMessageResult(response) - : this.#onSocketMessageSubscribe(response) + return isUndefined(response.method) + ? this.#onSocketMessageResult(response) + : this.#onSocketMessageSubscribe(response) + } catch (e) { + this.#emit("error", new Error("Invalid websocket message received", { cause: e })) + } } #onSocketMessageResult = (response: UnknownJsonRpcResponse): void => { diff --git a/packages/util/src/throwAfter.ts b/packages/util/src/throwAfter.ts index 8e97741f9c..a21b8a2412 100644 --- a/packages/util/src/throwAfter.ts +++ b/packages/util/src/throwAfter.ts @@ -1,2 +1,2 @@ export const throwAfter = (ms: number, reason: string) => - new Promise((_, reject) => setTimeout(() => reject(reason), ms)) + new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms)) From 6e29f2061c344f2a374e075b24af81e7658a7a6d Mon Sep 17 00:00:00 2001 From: Kheops <26880866+0xKheops@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:09:21 +0900 Subject: [PATCH 8/8] feat: migrate playground to viem and add base (#1147) * feat: migrate playground to viem and add base * feat: use builtin chain defs * chore: bump viem to same version as packages --- apps/playground/package.json | 6 +- .../src/components/Ethereum/NavEthereum.tsx | 6 - .../Ethereum/behavior/BehaviorPage.tsx | 10 - .../components/Ethereum/behavior/CrestAbi.ts | 368 ---------- .../Ethereum/behavior/NetworkSwitch.tsx | 88 --- .../Ethereum/contract/ContractTestBasics.tsx | 59 +- .../components/Ethereum/contracts/index.ts | 2 - .../Ethereum/contracts/types/Greeter.ts | 102 --- .../Ethereum/contracts/types/common.ts | 35 - .../types/factories/Greeter__factory.ts | 94 --- .../contracts/types/factories/index.ts | 4 - .../Ethereum/contracts/types/index.ts | 6 - .../Ethereum/erc20/ERC20ContractSelect.tsx | 34 +- .../components/Ethereum/erc20/ERC20Send.tsx | 58 +- .../Ethereum/erc721/ERC721ContractSelect.tsx | 34 +- .../components/Ethereum/erc721/ERC721Send.tsx | 69 +- .../src/components/Ethereum/index.tsx | 6 +- .../Ethereum/shared/ContractConnect.tsx | 27 +- .../Ethereum/shared/talismanChains.ts | 118 +--- .../shared/{connectors.ts => wagmiConfig.ts} | 9 +- .../components/Ethereum/sign/PersonalSign.tsx | 14 +- .../Ethereum/sign/PersonalSignBig.tsx | 14 +- .../Ethereum/sign/PersonalSignReversed.tsx | 38 +- .../Ethereum/sign/SignTypedData.tsx | 6 +- .../transaction/ArbitraryTransaction.tsx | 8 +- apps/playground/src/contracts/deployments.ts | 1 + apps/playground/src/main.tsx | 2 + apps/playground/src/serializableBigInt.ts | 9 + yarn.lock | 659 +++++++++--------- 29 files changed, 517 insertions(+), 1369 deletions(-) delete mode 100644 apps/playground/src/components/Ethereum/behavior/BehaviorPage.tsx delete mode 100644 apps/playground/src/components/Ethereum/behavior/CrestAbi.ts delete mode 100644 apps/playground/src/components/Ethereum/behavior/NetworkSwitch.tsx delete mode 100644 apps/playground/src/components/Ethereum/contracts/types/Greeter.ts delete mode 100644 apps/playground/src/components/Ethereum/contracts/types/common.ts delete mode 100644 apps/playground/src/components/Ethereum/contracts/types/factories/Greeter__factory.ts delete mode 100644 apps/playground/src/components/Ethereum/contracts/types/factories/index.ts delete mode 100644 apps/playground/src/components/Ethereum/contracts/types/index.ts rename apps/playground/src/components/Ethereum/shared/{connectors.ts => wagmiConfig.ts} (78%) create mode 100644 apps/playground/src/serializableBigInt.ts diff --git a/apps/playground/package.json b/apps/playground/package.json index 757e635f5a..115a1200d6 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -17,7 +17,6 @@ "@metamask/eth-sig-util": "5.1.0", "@talismn/wagmi-connector": "^0.2.0", "buffer": "^6.0.3", - "ethers": "^5.7.2", "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "^4.0.4", @@ -25,7 +24,8 @@ "react-router-dom": "6.14.1", "react-use": "^17.4.0", "talisman-ui": "workspace:*", - "wagmi": "^0.12.12" + "viem": "^1.18.9", + "wagmi": "^1.4.5" }, "devDependencies": { "@openzeppelin/contracts": "^4.9.2", @@ -39,7 +39,7 @@ "hardhat": "^2.16.1", "postcss": "^8.4.20", "tailwindcss": "^3.3.2", - "typescript": "4.9.4", + "typescript": "5.2.2", "vite": "^4.4.3", "vite-plugin-svgr": "^2.2.1" }, diff --git a/apps/playground/src/components/Ethereum/NavEthereum.tsx b/apps/playground/src/components/Ethereum/NavEthereum.tsx index 9b48332708..bb57c7b8f9 100644 --- a/apps/playground/src/components/Ethereum/NavEthereum.tsx +++ b/apps/playground/src/components/Ethereum/NavEthereum.tsx @@ -36,12 +36,6 @@ export const NavEthereum = () => { > Sign - - Behavior -
) } diff --git a/apps/playground/src/components/Ethereum/behavior/BehaviorPage.tsx b/apps/playground/src/components/Ethereum/behavior/BehaviorPage.tsx deleted file mode 100644 index 124f28b067..0000000000 --- a/apps/playground/src/components/Ethereum/behavior/BehaviorPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { EthereumLayout } from "../shared/EthereumLayout" -import { NetworkSwitch } from "./NetworkSwitch" - -export const BehaviorPage = () => { - return ( - - - - ) -} diff --git a/apps/playground/src/components/Ethereum/behavior/CrestAbi.ts b/apps/playground/src/components/Ethereum/behavior/CrestAbi.ts deleted file mode 100644 index 95e71f6e7e..0000000000 --- a/apps/playground/src/components/Ethereum/behavior/CrestAbi.ts +++ /dev/null @@ -1,368 +0,0 @@ -export const CrestAbi = [ - { - inputs: [ - { internalType: "address", name: "_store", type: "address" }, - { internalType: "address", name: "_auctionHouse", type: "address" }, - { internalType: "address", name: "_founders", type: "address" }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "owner", type: "address" }, - { indexed: true, internalType: "address", name: "approved", type: "address" }, - { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "Approval", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "owner", type: "address" }, - { indexed: true, internalType: "address", name: "operator", type: "address" }, - { indexed: false, internalType: "bool", name: "approved", type: "bool" }, - ], - name: "ApprovalForAll", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "delegator", type: "address" }, - { indexed: true, internalType: "address", name: "fromDelegate", type: "address" }, - { indexed: true, internalType: "address", name: "toDelegate", type: "address" }, - ], - name: "DelegateChanged", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "delegate", type: "address" }, - { indexed: false, internalType: "uint256", name: "previousBalance", type: "uint256" }, - { indexed: false, internalType: "uint256", name: "newBalance", type: "uint256" }, - ], - name: "DelegateVotesChanged", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "previousOwner", type: "address" }, - { indexed: true, internalType: "address", name: "newOwner", type: "address" }, - ], - name: "OwnershipTransferred", - type: "event", - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: "address", name: "from", type: "address" }, - { indexed: true, internalType: "address", name: "to", type: "address" }, - { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "Transfer", - type: "event", - }, - { - inputs: [], - name: "DOMAIN_SEPARATOR", - outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "approve", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "auctionHouse", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "owner", type: "address" }], - name: "balanceOf", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], - name: "burn", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "contractURI", - outputs: [{ internalType: "string", name: "", type: "string" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "delegatee", type: "address" }], - name: "delegate", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "delegatee", type: "address" }, - { internalType: "uint256", name: "nonce", type: "uint256" }, - { internalType: "uint256", name: "expiry", type: "uint256" }, - { internalType: "uint8", name: "v", type: "uint8" }, - { internalType: "bytes32", name: "r", type: "bytes32" }, - { internalType: "bytes32", name: "s", type: "bytes32" }, - ], - name: "delegateBySig", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "account", type: "address" }], - name: "delegates", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "", type: "uint256" }], - name: "dnaMap", - outputs: [{ internalType: "uint96", name: "", type: "uint96" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "founders", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], - name: "getApproved", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "blockNumber", type: "uint256" }], - name: "getPastTotalSupply", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "account", type: "address" }, - { internalType: "uint256", name: "blockNumber", type: "uint256" }, - ], - name: "getPastVotes", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "account", type: "address" }], - name: "getVotes", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "owner", type: "address" }, - { internalType: "address", name: "operator", type: "address" }, - ], - name: "isApprovedForAll", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "view", - type: "function", - }, - { inputs: [], name: "mint", outputs: [], stateMutability: "nonpayable", type: "function" }, - { - inputs: [ - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint96", name: "dna", type: "uint96" }, - ], - name: "mintSpecific", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "name", - outputs: [{ internalType: "string", name: "", type: "string" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "owner", type: "address" }], - name: "nonces", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "owner", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], - name: "ownerOf", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "renounceOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "from", type: "address" }, - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "safeTransferFrom", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "from", type: "address" }, - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "tokenId", type: "uint256" }, - { internalType: "bytes", name: "data", type: "bytes" }, - ], - name: "safeTransferFrom", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "operator", type: "address" }, - { internalType: "bool", name: "approved", type: "bool" }, - ], - name: "setApprovalForAll", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "_auctionHouse", type: "address" }], - name: "setAuctionHouse", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "string", name: "_uri", type: "string" }], - name: "setContractURI", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "_founders", type: "address" }], - name: "setFounders", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [], - name: "store", - outputs: [{ internalType: "address", name: "", type: "address" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], - name: "supportsInterface", - outputs: [{ internalType: "bool", name: "", type: "bool" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "symbol", - outputs: [{ internalType: "string", name: "", type: "string" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "index", type: "uint256" }], - name: "tokenByIndex", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "owner", type: "address" }, - { internalType: "uint256", name: "index", type: "uint256" }, - ], - name: "tokenOfOwnerByIndex", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], - name: "tokenURI", - outputs: [{ internalType: "string", name: "", type: "string" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [], - name: "totalSupply", - outputs: [{ internalType: "uint256", name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "from", type: "address" }, - { internalType: "address", name: "to", type: "address" }, - { internalType: "uint256", name: "tokenId", type: "uint256" }, - ], - name: "transferFrom", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [{ internalType: "address", name: "newOwner", type: "address" }], - name: "transferOwnership", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, -] as const diff --git a/apps/playground/src/components/Ethereum/behavior/NetworkSwitch.tsx b/apps/playground/src/components/Ethereum/behavior/NetworkSwitch.tsx deleted file mode 100644 index a5eb7393a7..0000000000 --- a/apps/playground/src/components/Ethereum/behavior/NetworkSwitch.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { ethers } from "ethers" -import { useCallback, useState } from "react" -import { Button } from "talisman-ui" -import { useAccount } from "wagmi" - -import { Section } from "../../shared/Section" -import { CrestAbi } from "./CrestAbi" - -export const NetworkSwitch = () => { - const [output, setOutput] = useState("") - - const { connector } = useAccount() - - const handleTestClick = useCallback(async () => { - setOutput("") - - const appendOutput = (text: string) => { - setOutput((prev) => prev + text + "\n") - } - - try { - const injectedProvider = await connector?.getProvider() - - const switchToMoonbeam = async () => { - appendOutput(`Switching to moonbeam `) - const res = await injectedProvider.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: "0x504" }], - }) - appendOutput(`Switched to moonbeam : ${res}`) - } - - const switchToAstar = async () => { - appendOutput(`Switching to astar`) - const res = await injectedProvider.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: "0x250" }], - }) - appendOutput(`Switched to astar : ${res}`) - } - - const callNftTokenUri = async () => { - appendOutput(`fetching NFT info`) - const ci = ethers.Contract.getInterface(CrestAbi) - const data = ci.encodeFunctionData("tokenURI", [1]) - - const res = await injectedProvider.request({ - method: "eth_call", - params: [ - { - to: "0x8417F77904a86436223942a516f00F8aDF933B70", - data, - }, - ], - }) - - const decoded = ci.decodeFunctionResult("tokenURI", res) - appendOutput(`fetched NFT info ${decoded.toString().slice(0, 40)}...`) - } - - await switchToMoonbeam() - const prom = callNftTokenUri() - await switchToAstar() - await prom - } catch (err) { - appendOutput(`Error : ${err?.toString()}`) - } - }, [connector]) - - return ( -
-
- This will : -
    -
  • - switch to Moonbeam
  • -
  • - asynchronously make an expensive read contract call
  • -
  • - switch to Astar
  • -
-
-
- -
-
-        {output}
-      
-
- ) -} diff --git a/apps/playground/src/components/Ethereum/contract/ContractTestBasics.tsx b/apps/playground/src/components/Ethereum/contract/ContractTestBasics.tsx index 54b26ec79d..4d6db4d3c0 100644 --- a/apps/playground/src/components/Ethereum/contract/ContractTestBasics.tsx +++ b/apps/playground/src/components/Ethereum/contract/ContractTestBasics.tsx @@ -1,5 +1,4 @@ -import { ethers } from "ethers" -import { useCallback, useMemo } from "react" +import { useCallback } from "react" import { useForm } from "react-hook-form" import { Button } from "talisman-ui" import { @@ -8,6 +7,7 @@ import { useContractWrite, useNetwork, usePrepareContractWrite, + useWalletClient, } from "wagmi" import { TestBasics, useDeployment } from "../../../contracts" @@ -31,10 +31,12 @@ export const ContractTestBasics = () => { } const ContractTestBasicsInner = () => { - const { isConnected, address: from, connector } = useAccount() + const { isConnected } = useAccount() const { chain } = useNetwork() const { address } = useDeployment("TestBasics", chain?.id) + const { data: walletClient } = useWalletClient() + const { data: readData, isLoading: readIsLoading } = useContractRead({ address, abi: TestBasics.abi, @@ -73,43 +75,15 @@ const ContractTestBasicsInner = () => { // allows testing an impossible contract interaction (transfer more than you have to test) const handleSendUnchecked = useCallback(async () => { - if (!connector) return - - const ci = new ethers.utils.Interface(TestBasics.abi) - - const funcFragment = ci.fragments.find( - (f) => f.type === "function" && f.name === "setValue" - ) as ethers.utils.FunctionFragment - - const data = ci.encodeFunctionData(funcFragment, [newValue]) - - const provider = await connector.getProvider() - await provider.request({ - method: "eth_sendTransaction", - params: [ - { - from, - to: address, - data, - }, - ], + if (!walletClient || !address) return + + await walletClient.writeContract({ + abi: TestBasics.abi, + address, + functionName: "setValue", + args: [newValue], }) - }, [address, connector, from, newValue]) - - const parsedError = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorData = (error as any)?.data?.data - if (!errorData) return undefined - - try { - const contract = new ethers.utils.Interface(TestBasics.abi) - return contract.parseError(errorData) - } catch (err) { - // eslint-disable-next-line no-console - console.error("Failed to parse error") - return null - } - }, [error]) + }, [address, newValue, walletClient]) if (!isConnected) return null @@ -138,12 +112,7 @@ const ContractTestBasicsInner = () => {
{!isLoading && newValue && error && ( -
-                    {JSON.stringify(error, undefined, 2)}
-
-                    {parsedError &&
-                      `\n\ndecoded error : ${JSON.stringify(parsedError, undefined, 2)}`}
-                  
+
{error.message}
)}
diff --git a/apps/playground/src/components/Ethereum/contracts/index.ts b/apps/playground/src/components/Ethereum/contracts/index.ts index ca23cf08a6..146ae5102c 100644 --- a/apps/playground/src/components/Ethereum/contracts/index.ts +++ b/apps/playground/src/components/Ethereum/contracts/index.ts @@ -1,7 +1,5 @@ import ContractsJson from "./ContractsAddresses.json" import GreeterJson from "./Greeter.json" -export * from "./types" -export * from "./types/common" export const GreeterAbi = GreeterJson diff --git a/apps/playground/src/components/Ethereum/contracts/types/Greeter.ts b/apps/playground/src/components/Ethereum/contracts/types/Greeter.ts deleted file mode 100644 index 371d645268..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/Greeter.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { FunctionFragment, Result } from "@ethersproject/abi" -import type { Listener, Provider } from "@ethersproject/providers" -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -import type { - BaseContract, - BigNumber, - BytesLike, - CallOverrides, - ContractTransaction, - Overrides, - PopulatedTransaction, - Signer, - utils, -} from "ethers" - -import type { OnEvent, TypedEvent, TypedEventFilter, TypedListener } from "./common" - -export interface GreeterInterface extends utils.Interface { - functions: { - "greet()": FunctionFragment - "setGreeting(string)": FunctionFragment - } - - getFunction(nameOrSignatureOrTopic: "greet" | "setGreeting"): FunctionFragment - - encodeFunctionData(functionFragment: "greet", values?: undefined): string - encodeFunctionData(functionFragment: "setGreeting", values: [string]): string - - decodeFunctionResult(functionFragment: "greet", data: BytesLike): Result - decodeFunctionResult(functionFragment: "setGreeting", data: BytesLike): Result - - events: {} -} - -export interface Greeter extends BaseContract { - connect(signerOrProvider: Signer | Provider | string): this - attach(addressOrName: string): this - deployed(): Promise - - interface: GreeterInterface - - queryFilter( - event: TypedEventFilter, - fromBlockOrBlockhash?: string | number | undefined, - toBlock?: string | number | undefined - ): Promise> - - listeners( - eventFilter?: TypedEventFilter - ): Array> - listeners(eventName?: string): Array - removeAllListeners(eventFilter: TypedEventFilter): this - removeAllListeners(eventName?: string): this - off: OnEvent - on: OnEvent - once: OnEvent - removeListener: OnEvent - - functions: { - greet(overrides?: CallOverrides): Promise<[string]> - - setGreeting( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise - } - - greet(overrides?: CallOverrides): Promise - - setGreeting( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise - - callStatic: { - greet(overrides?: CallOverrides): Promise - - setGreeting(_greeting: string, overrides?: CallOverrides): Promise - } - - filters: {} - - estimateGas: { - greet(overrides?: CallOverrides): Promise - - setGreeting( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise - } - - populateTransaction: { - greet(overrides?: CallOverrides): Promise - - setGreeting( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise - } -} diff --git a/apps/playground/src/components/Ethereum/contracts/types/common.ts b/apps/playground/src/components/Ethereum/contracts/types/common.ts deleted file mode 100644 index 5e38dde419..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/common.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -import type { Listener } from "@ethersproject/providers" -import type { Event, EventFilter } from "ethers" - -export interface TypedEvent = any, TArgsObject = any> extends Event { - args: TArgsArray & TArgsObject -} - -export interface TypedEventFilter<_TEvent extends TypedEvent> extends EventFilter {} - -export interface TypedListener { - (...listenerArg: [...__TypechainArgsArray, TEvent]): void -} - -type __TypechainArgsArray = T extends TypedEvent ? U : never - -export interface OnEvent { - ( - eventFilter: TypedEventFilter, - listener: TypedListener - ): TRes - (eventName: string, listener: Listener): TRes -} - -export type MinEthersFactory = { - deploy(...a: ARGS[]): Promise -} - -export type GetContractTypeFromFactory = F extends MinEthersFactory ? C : never - -export type GetARGsTypeFromFactory = F extends MinEthersFactory - ? Parameters - : never diff --git a/apps/playground/src/components/Ethereum/contracts/types/factories/Greeter__factory.ts b/apps/playground/src/components/Ethereum/contracts/types/factories/Greeter__factory.ts deleted file mode 100644 index 47727c4a66..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/factories/Greeter__factory.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Provider, TransactionRequest } from "@ethersproject/providers" -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -import { Contract, ContractFactory, Overrides, Signer, utils } from "ethers" - -import type { Greeter, GreeterInterface } from "../Greeter" - -const _abi = [ - { - inputs: [ - { - internalType: "string", - name: "_greeting", - type: "string", - }, - ], - stateMutability: "nonpayable", - type: "constructor", - }, - { - inputs: [], - name: "greet", - outputs: [ - { - internalType: "string", - name: "", - type: "string", - }, - ], - stateMutability: "view", - type: "function", - }, - { - inputs: [ - { - internalType: "string", - name: "_greeting", - type: "string", - }, - ], - name: "setGreeting", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, -] - -const _bytecode = - "0x60806040523480156200001157600080fd5b5060405162000c3238038062000c32833981810160405281019062000037919062000278565b6200006760405180606001604052806022815260200162000c1060229139826200008760201b620001ce1760201c565b80600090805190602001906200007f92919062000156565b5050620004c5565b620001298282604051602401620000a0929190620002fe565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506200012d60201b60201c565b5050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b8280546200016490620003ea565b90600052602060002090601f016020900481019282620001885760008555620001d4565b82601f10620001a357805160ff1916838001178555620001d4565b82800160010185558215620001d4579182015b82811115620001d3578251825591602001919060010190620001b6565b5b509050620001e39190620001e7565b5090565b5b8082111562000202576000816000905550600101620001e8565b5090565b60006200021d620002178462000362565b62000339565b9050828152602081018484840111156200023657600080fd5b62000243848285620003b4565b509392505050565b600082601f8301126200025d57600080fd5b81516200026f84826020860162000206565b91505092915050565b6000602082840312156200028b57600080fd5b600082015167ffffffffffffffff811115620002a657600080fd5b620002b4848285016200024b565b91505092915050565b6000620002ca8262000398565b620002d68185620003a3565b9350620002e8818560208601620003b4565b620002f381620004b4565b840191505092915050565b600060408201905081810360008301526200031a8185620002bd565b90508181036020830152620003308184620002bd565b90509392505050565b60006200034562000358565b905062000353828262000420565b919050565b6000604051905090565b600067ffffffffffffffff82111562000380576200037f62000485565b5b6200038b82620004b4565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b60005b83811015620003d4578082015181840152602081019050620003b7565b83811115620003e4576000848401525b50505050565b600060028204905060018216806200040357607f821691505b602082108114156200041a576200041962000456565b5b50919050565b6200042b82620004b4565b810181811067ffffffffffffffff821117156200044d576200044c62000485565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b61073b80620004d56000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063a41368621461003b578063cfae321714610057575b600080fd5b6100556004803603810190610050919061043d565b610075565b005b61005f61013c565b60405161006c91906104b7565b60405180910390f35b6101226040518060600160405280602381526020016106e3602391396000805461009e90610610565b80601f01602080910402602001604051908101604052809291908181526020018280546100ca90610610565b80156101175780601f106100ec57610100808354040283529160200191610117565b820191906000526020600020905b8154815290600101906020018083116100fa57829003601f168201915b50505050508361026a565b8060009080519060200190610138929190610332565b5050565b60606000805461014b90610610565b80601f016020809104026020016040519081016040528092919081815260200182805461017790610610565b80156101c45780601f10610199576101008083540402835291602001916101c4565b820191906000526020600020905b8154815290600101906020018083116101a757829003601f168201915b5050505050905090565b61026682826040516024016101e49291906104d9565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b5050565b61030483838360405160240161028293929190610510565b6040516020818303038152906040527f2ced7cef000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050610309565b505050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b82805461033e90610610565b90600052602060002090601f01602090048101928261036057600085556103a7565b82601f1061037957805160ff19168380011785556103a7565b828001600101855582156103a7579182015b828111156103a657825182559160200191906001019061038b565b5b5090506103b491906103b8565b5090565b5b808211156103d15760008160009055506001016103b9565b5090565b60006103e86103e384610581565b61055c565b90508281526020810184848401111561040057600080fd5b61040b8482856105ce565b509392505050565b600082601f83011261042457600080fd5b81356104348482602086016103d5565b91505092915050565b60006020828403121561044f57600080fd5b600082013567ffffffffffffffff81111561046957600080fd5b61047584828501610413565b91505092915050565b6000610489826105b2565b61049381856105bd565b93506104a38185602086016105dd565b6104ac816106d1565b840191505092915050565b600060208201905081810360008301526104d1818461047e565b905092915050565b600060408201905081810360008301526104f3818561047e565b90508181036020830152610507818461047e565b90509392505050565b6000606082019050818103600083015261052a818661047e565b9050818103602083015261053e818561047e565b90508181036040830152610552818461047e565b9050949350505050565b6000610566610577565b90506105728282610642565b919050565b6000604051905090565b600067ffffffffffffffff82111561059c5761059b6106a2565b5b6105a5826106d1565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b838110156105fb5780820151818401526020810190506105e0565b8381111561060a576000848401525b50505050565b6000600282049050600182168061062857607f821691505b6020821081141561063c5761063b610673565b5b50919050565b61064b826106d1565b810181811067ffffffffffffffff8211171561066a576106696106a2565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fe4368616e67696e67206772656574696e672066726f6d202725732720746f2027257327a264697066735822122062b06e5bdee39e73f7ae7ef8606fe9f23da851629e4e297316ce7747f5074b1964736f6c634300080400334465706c6f79696e67206120477265657465722077697468206772656574696e673a" - -type GreeterConstructorParams = [signer?: Signer] | ConstructorParameters - -const isSuperArgs = ( - xs: GreeterConstructorParams -): xs is ConstructorParameters => xs.length > 1 - -export class Greeter__factory extends ContractFactory { - constructor(...args: GreeterConstructorParams) { - if (isSuperArgs(args)) { - super(...args) - } else { - super(_abi, _bytecode, args[0]) - } - } - - override deploy( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): Promise { - return super.deploy(_greeting, overrides || {}) as Promise - } - override getDeployTransaction( - _greeting: string, - overrides?: Overrides & { from?: string | Promise } - ): TransactionRequest { - return super.getDeployTransaction(_greeting, overrides || {}) - } - override attach(address: string): Greeter { - return super.attach(address) as Greeter - } - override connect(signer: Signer): Greeter__factory { - return super.connect(signer) as Greeter__factory - } - - static readonly bytecode = _bytecode - static readonly abi = _abi - static createInterface(): GreeterInterface { - return new utils.Interface(_abi) as GreeterInterface - } - static connect(address: string, signerOrProvider: Signer | Provider): Greeter { - return new Contract(address, _abi, signerOrProvider) as Greeter - } -} diff --git a/apps/playground/src/components/Ethereum/contracts/types/factories/index.ts b/apps/playground/src/components/Ethereum/contracts/types/factories/index.ts deleted file mode 100644 index 0dc323c349..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/factories/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -export { Greeter__factory } from "./Greeter__factory" diff --git a/apps/playground/src/components/Ethereum/contracts/types/index.ts b/apps/playground/src/components/Ethereum/contracts/types/index.ts deleted file mode 100644 index c171bb5fc8..0000000000 --- a/apps/playground/src/components/Ethereum/contracts/types/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* Autogenerated file. Do not edit manually. */ -/* tslint:disable */ -/* eslint-disable */ -export type { Greeter } from "./Greeter" -export * as factories from "./factories" -export { Greeter__factory } from "./factories/Greeter__factory" diff --git a/apps/playground/src/components/Ethereum/erc20/ERC20ContractSelect.tsx b/apps/playground/src/components/Ethereum/erc20/ERC20ContractSelect.tsx index 4532ce42b8..e6d970441a 100644 --- a/apps/playground/src/components/Ethereum/erc20/ERC20ContractSelect.tsx +++ b/apps/playground/src/components/Ethereum/erc20/ERC20ContractSelect.tsx @@ -1,17 +1,26 @@ -import { ethers } from "ethers" import { formatUnits } from "ethers/lib/utils.js" import { useCallback, useRef, useState } from "react" -import { erc20ABI, useAccount, useContractRead, useNetwork } from "wagmi" +import { + erc20ABI, + useAccount, + useContractRead, + useNetwork, + usePublicClient, + useWalletClient, +} from "wagmi" import { useDeployment } from "../../../contracts" import { useErc20Contract } from "./context" export const ERC20ContractSelect = () => { const { chain } = useNetwork() - const { isConnected, address: account, connector } = useAccount() + const { isConnected, address: account } = useAccount() const [address, setAddress] = useErc20Contract() + const { data: walletClient } = useWalletClient() + const publicClient = usePublicClient() + const { bytecode } = useDeployment("TestERC20", chain?.id ?? 0) const [isDeploying, setIsDeploying] = useState(false) const [deployError, setDeployError] = useState() @@ -21,18 +30,15 @@ export const ERC20ContractSelect = () => { setIsDeploying(true) setDeployError(undefined) try { - const provider = await connector?.getProvider() - if (!provider || !chain) return + if (!walletClient) throw new Error("No wallet client") - const web3Provider = new ethers.providers.Web3Provider(provider) - const transaction: ethers.providers.TransactionRequest = { - from: account, - data: bytecode, - chainId: chain.id, - } + const hash = await walletClient.sendTransaction({ + data: bytecode as `0x${string}`, + }) + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }) - const txHash = await web3Provider.send("eth_sendTransaction", [transaction]) - const receipt = await web3Provider.waitForTransaction(txHash) if (!receipt.contractAddress) throw new Error("No contract address in receipt") setAddress(receipt.contractAddress as `0x${string}`) @@ -43,7 +49,7 @@ export const ERC20ContractSelect = () => { setDeployError(err as Error) } setIsDeploying(false) - }, [account, bytecode, chain, connector, setAddress]) + }, [bytecode, publicClient, setAddress, walletClient]) const { data: symbol, error: errorSymbol } = useContractRead({ address: address as `0x${string}`, diff --git a/apps/playground/src/components/Ethereum/erc20/ERC20Send.tsx b/apps/playground/src/components/Ethereum/erc20/ERC20Send.tsx index 90a2538e27..0f50de98da 100644 --- a/apps/playground/src/components/Ethereum/erc20/ERC20Send.tsx +++ b/apps/playground/src/components/Ethereum/erc20/ERC20Send.tsx @@ -1,16 +1,15 @@ -import { ethers } from "ethers" -import { parseUnits } from "ethers/lib/utils" import { useCallback } from "react" import { useForm } from "react-hook-form" import { useLocalStorage } from "react-use" import { Button } from "talisman-ui" +import { parseUnits } from "viem" import { erc20ABI, useAccount, useContractRead, useContractWrite, usePrepareContractWrite, - useSendTransaction, + useWalletClient, } from "wagmi" import { TransactionReceipt } from "../shared/TransactionReceipt" @@ -24,8 +23,8 @@ const DEFAULT_VALUE: FormData = { } export const ERC20Send = () => { - const { isConnected, address, connector } = useAccount() - + const { isConnected, address } = useAccount() + const { data: walletClient } = useWalletClient() const [contractAddress] = useErc20Contract() const [defaultValues, setDefaultValues] = useLocalStorage("pg:send-erc20", DEFAULT_VALUE) @@ -40,7 +39,7 @@ export const ERC20Send = () => { const formData = watch() - const { data: decimals } = useContractRead({ + const { data: decimals = 18 } = useContractRead({ address: contractAddress as `0x${string}`, abi: erc20ABI, functionName: "decimals", @@ -64,55 +63,32 @@ export const ERC20Send = () => { args: [formData.recipient as `0x${string}`, parseUnits(formData.amount, decimals)], }) - const { isLoading: writeIsLoading } = useContractWrite({ - address: contractAddress as `0x${string}`, - abi: erc20ABI, - functionName: "transfer", - mode: "recklesslyUnprepared", - args: [formData.recipient as `0x${string}`, parseUnits(formData.amount, decimals)], - }) - const { - sendTransaction, + isLoading: writeIsLoading, + write: send, + error: sendError, isLoading: sendIsLoading, isSuccess: sendIsSuccess, isError: sendIsError, data: senddata, - error: sendError, - } = useSendTransaction(config) + } = useContractWrite(config) const onSubmit = (data: FormData) => { setDefaultValues(data) - sendTransaction?.() + send?.() } // allows testing an impossible contract interaction (transfer more than you have to test) const handleSendUnchecked = useCallback(async () => { - if (!connector) return - - const ci = new ethers.utils.Interface(erc20ABI) - - const funcFragment = ci.fragments.find( - (f) => f.type === "function" && f.name === "transfer" - ) as ethers.utils.FunctionFragment - - const data = ci.encodeFunctionData(funcFragment, [ - formData.recipient, - parseUnits(formData.amount, decimals), - ]) + if (!walletClient) return - const provider = await connector.getProvider() - await provider.request({ - method: "eth_sendTransaction", - params: [ - { - from: address, - to: contractAddress, - data, - }, - ], + walletClient.writeContract({ + address: contractAddress as `0x${string}`, + abi: erc20ABI, + functionName: "transfer", + args: [formData.recipient as `0x${string}`, parseUnits(formData.amount, decimals)], }) - }, [address, connector, contractAddress, decimals, formData.amount, formData.recipient]) + }, [contractAddress, decimals, formData.amount, formData.recipient, walletClient]) if (!isConnected) return null diff --git a/apps/playground/src/components/Ethereum/erc721/ERC721ContractSelect.tsx b/apps/playground/src/components/Ethereum/erc721/ERC721ContractSelect.tsx index b1bf5f702e..a2356d0893 100644 --- a/apps/playground/src/components/Ethereum/erc721/ERC721ContractSelect.tsx +++ b/apps/playground/src/components/Ethereum/erc721/ERC721ContractSelect.tsx @@ -1,16 +1,25 @@ -import { ethers } from "ethers" import { useCallback, useRef, useState } from "react" -import { erc721ABI, useAccount, useContractRead, useNetwork } from "wagmi" +import { + erc721ABI, + useAccount, + useContractRead, + useNetwork, + usePublicClient, + useWalletClient, +} from "wagmi" import { useDeployment } from "../../../contracts" import { useErc721Contract } from "./context" export const ERC721ContractSelect = () => { const { chain } = useNetwork() - const { isConnected, address: account, connector } = useAccount() + const { isConnected, address: account } = useAccount() const [address, setAddress] = useErc721Contract() + const { data: walletClient } = useWalletClient() + const publicClient = usePublicClient() + const { bytecode } = useDeployment("TestERC721", chain?.id ?? 0) const [isDeploying, setIsDeploying] = useState(false) const [deployError, setDeployError] = useState() @@ -20,18 +29,15 @@ export const ERC721ContractSelect = () => { setIsDeploying(true) setDeployError(undefined) try { - const provider = await connector?.getProvider() - if (!provider || !chain) return + if (!walletClient) throw new Error("No wallet client") - const web3Provider = new ethers.providers.Web3Provider(provider) - const transaction: ethers.providers.TransactionRequest = { - from: account, - data: bytecode, - chainId: chain.id, - } + const hash = await walletClient.sendTransaction({ + data: bytecode as `0x${string}`, + }) + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + }) - const txHash = await web3Provider.send("eth_sendTransaction", [transaction]) - const receipt = await web3Provider.waitForTransaction(txHash) if (!receipt.contractAddress) throw new Error("No contract address in receipt") setAddress(receipt.contractAddress as `0x${string}`) @@ -42,7 +48,7 @@ export const ERC721ContractSelect = () => { setDeployError(err as Error) } setIsDeploying(false) - }, [account, bytecode, chain, connector, setAddress]) + }, [bytecode, publicClient, setAddress, walletClient]) const { data: symbol, error: errorSymbol } = useContractRead({ address: address as `0x${string}`, diff --git a/apps/playground/src/components/Ethereum/erc721/ERC721Send.tsx b/apps/playground/src/components/Ethereum/erc721/ERC721Send.tsx index 8eee47d072..2dae13a03e 100644 --- a/apps/playground/src/components/Ethereum/erc721/ERC721Send.tsx +++ b/apps/playground/src/components/Ethereum/erc721/ERC721Send.tsx @@ -1,16 +1,14 @@ -import { BigNumber, ethers } from "ethers" import { useCallback, useEffect, useState } from "react" import { useForm } from "react-hook-form" import { useLocalStorage } from "react-use" import { Button } from "talisman-ui" import { - erc20ABI, erc721ABI, useAccount, useContractRead, useContractWrite, usePrepareContractWrite, - useSendTransaction, + useWalletClient, } from "wagmi" import { TransactionReceipt } from "../shared/TransactionReceipt" @@ -65,8 +63,8 @@ const DEFAULT_VALUE: FormData = { } export const ERC721Send = () => { - const { isConnected, address, connector } = useAccount() - + const { isConnected, address } = useAccount() + const { data: walletClient } = useWalletClient() const [contractAddress] = useErc721Contract() const [defaultValues, setDefaultValues] = useLocalStorage("pg:send-erc721", DEFAULT_VALUE) @@ -85,7 +83,7 @@ export const ERC721Send = () => { address: contractAddress as `0x${string}`, abi: erc721ABI, functionName: "tokenURI", - args: [BigNumber.from(formData.tokenId)], + args: [BigInt(formData.tokenId)], enabled: !!contractAddress && !!formData.tokenId?.length, watch: true, }) @@ -104,33 +102,17 @@ export const ERC721Send = () => { abi: erc721ABI, functionName: "safeTransferFrom", enabled: !!contractAddress && !!balanceOfSelfData, - args: [ - address as `0x${string}`, - formData.recipient as `0x${string}`, - BigNumber.from(formData.tokenId), - ], - }) - - const { isLoading: writeIsLoading } = useContractWrite({ - address: contractAddress as `0x${string}`, - abi: erc721ABI, - functionName: "safeTransferFrom", - mode: "recklesslyUnprepared", - args: [ - address as `0x${string}`, - formData.recipient as `0x${string}`, - BigNumber.from(formData.tokenId), - ], + args: [address as `0x${string}`, formData.recipient as `0x${string}`, BigInt(formData.tokenId)], }) const { - sendTransaction, + write: sendTransaction, isLoading: sendIsLoading, isSuccess: sendIsSuccess, isError: sendIsError, data: senddata, error: sendError, - } = useSendTransaction(config) + } = useContractWrite(config) const onSubmit = (data: FormData) => { setDefaultValues(data) @@ -139,32 +121,19 @@ export const ERC721Send = () => { // allows testing an impossible contract interaction (transfer more than you have to test) const handleSendUnchecked = useCallback(async () => { - if (!connector) return - - const ci = new ethers.utils.Interface(erc20ABI) - - const funcFragment = ci.fragments.find( - (f) => f.type === "function" && f.name === "transfer" - ) as ethers.utils.FunctionFragment - - const data = ci.encodeFunctionData(funcFragment, [ - address, - formData.recipient, - formData.tokenId, - ]) - - const provider = await connector.getProvider() - await provider.request({ - method: "eth_sendTransaction", - params: [ - { - from: address, - to: contractAddress, - data, - }, + if (!walletClient) return + + walletClient.writeContract({ + address: contractAddress as `0x${string}`, + abi: erc721ABI, + functionName: "safeTransferFrom", + args: [ + address as `0x${string}`, + formData.recipient as `0x${string}`, + BigInt(formData.tokenId), ], }) - }, [address, connector, contractAddress, formData]) + }, [address, contractAddress, formData.recipient, formData.tokenId, walletClient]) // eslint-disable-next-line @typescript-eslint/no-explicit-any const [metadata, setMetadata] = useState() @@ -235,7 +204,7 @@ export const ERC721Send = () => {