From db4838c490f23930616b194e973e6c65a54c42b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Thu, 28 Aug 2025 10:12:13 +0200 Subject: [PATCH 1/4] feat(ng): evm tx fee widget --- .../src/components/CollapsedTokenAmount.tsx | 33 +-- .../src/hooks/useCurrentFeesForNetwork.ts | 26 +++ apps/next/src/hooks/useNativeToken.ts | 16 ++ .../next/src/hooks/useUpdateAccountBalance.ts | 19 ++ .../localization/locales/en/translation.json | 20 ++ .../ActionDetails/evm/EvmActionDetails.tsx | 9 +- .../EvmNetworkFeeWidget.tsx | 72 ++++++ .../components/CustomGasSettings.tsx | 212 ++++++++++++++++++ .../components/FeePresetSelector.tsx | 90 ++++++++ .../components/GaslessSwitch.tsx | 28 +++ .../EvmNetworkFeeWidget/components/index.ts | 3 + .../hooks/useEvmTransactionFee/index.ts | 1 + .../useEvmTransactionFee/lib/getFeeInfo.ts | 9 + .../lib/getInitialFeeRate.ts | 11 + .../lib/hasEnoughForFee.ts | 16 ++ .../hooks/useEvmTransactionFee/lib/index.ts | 3 + .../hooks/useEvmTransactionFee/types.ts | 44 ++++ .../useEvmTransactionFee.tsx | 130 +++++++++++ .../evm/EvmNetworkFeeWidget/index.ts | 1 + .../evm/EvmNetworkFeeWidget/types.ts | 1 + .../generic/DetailsItem/items/DetailRow.tsx | 24 +- .../ActionDetails/generic/DetailsSection.tsx | 13 +- .../generic/NetworkFee/FeePresetButton.tsx | 13 ++ .../generic/NetworkFee/TotalFeeAmount.tsx | 41 ++++ .../ActionDetails/generic/NetworkFee/index.ts | 2 + .../Contacts/components/InvisibleInput.tsx | 9 +- .../SendBody/hooks/useEvmNativeSend.ts | 1 - 27 files changed, 823 insertions(+), 24 deletions(-) create mode 100644 apps/next/src/hooks/useCurrentFeesForNetwork.ts create mode 100644 apps/next/src/hooks/useNativeToken.ts create mode 100644 apps/next/src/hooks/useUpdateAccountBalance.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/EvmNetworkFeeWidget.tsx create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/CustomGasSettings.tsx create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/FeePresetSelector.tsx create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/GaslessSwitch.tsx create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/index.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/index.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/getFeeInfo.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/getInitialFeeRate.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/hasEnoughForFee.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/index.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/types.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/useEvmTransactionFee.tsx create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/index.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/types.ts create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/generic/NetworkFee/FeePresetButton.tsx create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/generic/NetworkFee/TotalFeeAmount.tsx create mode 100644 apps/next/src/pages/Approve/components/ActionDetails/generic/NetworkFee/index.ts diff --git a/apps/next/src/components/CollapsedTokenAmount.tsx b/apps/next/src/components/CollapsedTokenAmount.tsx index 52cc3d023..06ddecd51 100644 --- a/apps/next/src/components/CollapsedTokenAmount.tsx +++ b/apps/next/src/components/CollapsedTokenAmount.tsx @@ -1,6 +1,7 @@ import { Stack, StackProps, + Tooltip, Typography, TypographyProps, } from '@avalabs/k2-alpine'; @@ -66,21 +67,23 @@ export const CollapsedTokenAmount = ({ if (fraction && indexOfNonZero) { return ( - - {integer}.0 - {zeroCount} - - {fraction.slice( - indexOfNonZero, - indexOfNonZero + MAX_DIGITS_AFTER_CONSECUTIVE_ZEROES, - )} - - + + + {integer}.0 + {zeroCount} + + {fraction.slice( + indexOfNonZero, + indexOfNonZero + MAX_DIGITS_AFTER_CONSECUTIVE_ZEROES, + )} + + + ); } return {amount}; diff --git a/apps/next/src/hooks/useCurrentFeesForNetwork.ts b/apps/next/src/hooks/useCurrentFeesForNetwork.ts new file mode 100644 index 000000000..36379d144 --- /dev/null +++ b/apps/next/src/hooks/useCurrentFeesForNetwork.ts @@ -0,0 +1,26 @@ +import { NetworkFee, NetworkWithCaipId } from '@core/types'; +import { useEffect, useState } from 'react'; + +import { useNetworkFeeContext } from '@core/ui'; + +export const useCurrentFeesForNetwork = (network: NetworkWithCaipId) => { + const { getNetworkFee } = useNetworkFeeContext(); + + const [networkFee, setNetworkFee] = useState(); + + useEffect(() => { + let isMounted = true; + + getNetworkFee(network.caipId).then((newFee) => { + if (isMounted) { + setNetworkFee(newFee); + } + }); + + return () => { + isMounted = false; + }; + }, [getNetworkFee, network]); + + return networkFee; +}; diff --git a/apps/next/src/hooks/useNativeToken.ts b/apps/next/src/hooks/useNativeToken.ts new file mode 100644 index 000000000..d28f7ad27 --- /dev/null +++ b/apps/next/src/hooks/useNativeToken.ts @@ -0,0 +1,16 @@ +import { useMemo } from 'react'; +import { TokenType } from '@avalabs/vm-module-types'; + +import { NativeTokenBalance, NetworkWithCaipId } from '@core/types'; +import { useTokensWithBalances } from '@core/ui'; + +type UseNativeTokenArgs = { network: NetworkWithCaipId }; + +export const useNativeToken = ({ network }: UseNativeTokenArgs) => { + const tokens = useTokensWithBalances({ network }); + + return useMemo( + () => tokens.find(({ type }) => type === TokenType.NATIVE), + [tokens], + ) as NativeTokenBalance; +}; diff --git a/apps/next/src/hooks/useUpdateAccountBalance.ts b/apps/next/src/hooks/useUpdateAccountBalance.ts new file mode 100644 index 000000000..893b4aebe --- /dev/null +++ b/apps/next/src/hooks/useUpdateAccountBalance.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { NetworkWithCaipId } from '@core/types'; +import { useAccountsContext, useBalancesContext } from '@core/ui'; + +export const useUpdateAccountBalance = (network: NetworkWithCaipId) => { + const { + accounts: { active: activeAccount }, + } = useAccountsContext(); + + const { updateBalanceOnNetworks } = useBalancesContext(); + + useEffect(() => { + if (!activeAccount) { + return; + } + + updateBalanceOnNetworks([activeAccount], [network.chainId]); + }, [activeAccount, network.chainId, updateBalanceOnNetworks]); +}; diff --git a/apps/next/src/localization/locales/en/translation.json b/apps/next/src/localization/locales/en/translation.json index 7372aa574..a5fd10ff1 100644 --- a/apps/next/src/localization/locales/en/translation.json +++ b/apps/next/src/localization/locales/en/translation.json @@ -91,6 +91,7 @@ "Closing the settings menu will require you to restart the 2 day waiting period.": "Closing the settings menu will require you to restart the 2 day waiting period.", "Code copied to clipboard": "Code copied to clipboard", "Code verification error": "Code verification error", + "Coming soon!": "Coming soon!", "Confirm": "Confirm", "Confirm Bridge": "Confirm Bridge", "Confirm addresses": "Confirm addresses", @@ -127,6 +128,7 @@ "Current password": "Current password", "Current password is incorrect": "Current password is incorrect", "Currently using {{name}}": "Currently using {{name}}", + "Custom": "Custom", "Customize Core to your liking": "Customize Core to your liking", "Dark": "Dark", "Decide what default view works best for you, either a floating interface or a sidebar docked to the side to show more content": "Decide what default view works best for you, either a floating interface or a sidebar docked to the side to show more content", @@ -145,6 +147,7 @@ "Done": "Done", "Download Ledger Live to update": "Download Ledger Live to update", "Drop your file here to upload": "Drop your file here to upload", + "Edit network fee": "Edit network fee", "Email address": "Email address", "Enable a sandbox environment for testing without using real funds": "Enable a sandbox environment for testing without using real funds", "English": "English", @@ -181,6 +184,7 @@ "Failed to save contact": "Failed to save contact", "Failed to send the transaction": "Failed to send the transaction", "Farming": "Farming", + "Fast": "Fast", "Feature is disabled": "Feature is disabled", "Fetching available authentication methods...": "Fetching available authentication methods...", "File Upload Failed": "File Upload Failed", @@ -190,10 +194,13 @@ "Follow the instructions in your browser window to add this key to your account.": "Follow the instructions in your browser window to add this key to your account.", "Forgot password?": "Forgot password?", "French": "French", + "Gas fees paid by Core": "Gas fees paid by Core", + "Gas limit": "Gas limit", "General": "General", "Generate a new account in your active wallet": "Generate a new account in your active wallet", "German": "German", "Get Core to work for you. Whether it’s transferring, sending crypto, just ask away!": "Get Core to work for you. Whether it’s transferring, sending crypto, just ask away!", + "Get free gas": "Get free gas", "Get started by adding crypto to your wallet": "Get started by adding crypto to your wallet", "Governance": "Governance", "Help center": "Help center", @@ -224,6 +231,7 @@ "Incorrect token address": "Incorrect token address", "Install or open your authenticator app to scan the QR code. If you prefer, you can manually copy the code by clicking the link below.": "Install or open your authenticator app to scan the QR code. If you prefer, you can manually copy the code by clicking the link below.", "Insufficient balance": "Insufficient balance", + "Insufficient balance for fee": "Insufficient balance for fee", "Insufficient balance.": "Insufficient balance.", "Insufficient funds": "Insufficient funds", "Insurance Buyer": "Insurance Buyer", @@ -275,6 +283,9 @@ "Manually enter a recovery phrase": "Manually enter a recovery phrase", "Manually enter your private key to import": "Manually enter your private key to import", "Max": "Max", + "Max base fee": "Max base fee", + "Max base fee must be greater than max priority fee": "Max base fee must be greater than max priority fee", + "Max priority fee": "Max priority fee", "Maximum available amount after fees is ~{{maxAmount}}.": "Maximum available amount after fees is ~{{maxAmount}}.", "Minimum amount is {{minimum}}.": "Minimum amount is {{minimum}}.", "Mismatching provider": "Mismatching provider", @@ -287,6 +298,7 @@ "Name your Passkey": "Name your Passkey", "Name your Yubikey": "Name your Yubikey", "Network error": "Network error", + "Network fee amount": "Network fee amount", "New password": "New password", "New password must be different from current one": "New password must be different from current one", "Next": "Next", @@ -301,6 +313,7 @@ "No routes found with enough liquidity.": "No routes found with enough liquidity.", "No saved addresses": "No saved addresses", "No thanks": "No thanks", + "Normal": "Normal", "Not connected": "Not connected", "Only Keystore files from the Avalanche Wallet are supported": "Only Keystore files from the Avalanche Wallet are supported", "Only keystore files exported from the Avalanche Wallet are supported.": "Only keystore files exported from the Avalanche Wallet are supported.", @@ -327,6 +340,7 @@ "Please enter a password": "Please enter a password", "Please enter a recipient address.": "Please enter a recipient address.", "Please enter a valid amount.": "Please enter a valid amount.", + "Please enter a valid decimal value": "Please enter a valid decimal value", "Please enter a valid password": "Please enter a valid password", "Please enter an amount": "Please enter an amount", "Please enter the private key.": "Please enter the private key.", @@ -418,6 +432,7 @@ "Sidebar": "Sidebar", "Skip": "Skip", "Slippage tolerance exceeded, increase the slippage and try again.": "Slippage tolerance exceeded, increase the slippage and try again.", + "Slow": "Slow", "Solana": "Solana", "Some of the required parameters are invalid.": "Some of the required parameters are invalid.", "Some of the required parameters are missing.": "Some of the required parameters are missing.", @@ -439,11 +454,13 @@ "That's it!": "That's it!", "The active account does not support Bitcoin.": "The active account does not support Bitcoin.", "The amount cannot be lower than the bridging fee": "The amount cannot be lower than the bridging fee", + "The base fee is set by the network and changes frequently. Any difference between the set max base fee and the actual base fee will be refunded.": "The base fee is set by the network and changes frequently. Any difference between the set max base fee and the actual base fee will be refunded.", "The bridging fee is unknown": "The bridging fee is unknown", "The export process could not be completed. Please try again.": "The export process could not be completed. Please try again.", "The field is required": "The field is required", "The key you entered is invalid. Please try again": "The key you entered is invalid. Please try again", "The operation either timed out or was not allowed. Please try again.": "The operation either timed out or was not allowed. Please try again.", + "The priority fee is an incentive paid to network operators to prioritize processing of this transaction.": "The priority fee is an incentive paid to network operators to prioritize processing of this transaction.", "The recovery phrase your entered is invalid. Please double check for spelling mistakes or the order of each word": "The recovery phrase your entered is invalid. Please double check for spelling mistakes or the order of each word", "The transaction has been reverted": "The transaction has been reverted", "The transaction timed out": "The transaction timed out", @@ -473,6 +490,9 @@ "Token": "Token", "TokenSets": "TokenSets", "Total balance": "Total balance", + "Total network fee": "Total network fee", + "Total network fee = (current base fee + max priority fee) × gas limit. It will never be higher than max base fee × gas limit.": "Total network fee = (current base fee + max priority fee) × gas limit. It will never be higher than max base fee × gas limit.", + "Total units of gas needed to complete the transaction. Editing is not possible at this time.": "Total units of gas needed to complete the transaction. Editing is not possible at this time.", "Transaction has been blocked": "Transaction has been blocked", "Transaction has been cancelled": "Transaction has been cancelled", "Transaction has been rejected": "Transaction has been rejected", diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmActionDetails.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmActionDetails.tsx index fe90ffc2d..e0ea9cb95 100644 --- a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmActionDetails.tsx +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmActionDetails.tsx @@ -6,12 +6,16 @@ import { ActionDetailsProps } from '../../../types'; import { DetailsSection } from '../generic/DetailsSection'; import { DetailsItem } from '../generic/DetailsItem'; import { TransactionBalanceChange } from '../generic/TransactionBalanceChange/TransactionBalanceChange'; +import { EvmNetworkFeeWidget } from './EvmNetworkFeeWidget/EvmNetworkFeeWidget'; type EvmActionDetailsProps = Omit & { network: EvmNetwork; }; -export const EvmActionDetails = ({ action }: EvmActionDetailsProps) => { +export const EvmActionDetails = ({ + action, + network, +}: EvmActionDetailsProps) => { return ( {action.displayData.balanceChange && ( @@ -28,6 +32,9 @@ export const EvmActionDetails = ({ action }: EvmActionDetailsProps) => { ))} ))} + {action.displayData.networkFeeSelector && ( + + )} ); }; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/EvmNetworkFeeWidget.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/EvmNetworkFeeWidget.tsx new file mode 100644 index 000000000..3a3e6afc8 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/EvmNetworkFeeWidget.tsx @@ -0,0 +1,72 @@ +import { DisplayData } from '@avalabs/vm-module-types'; +import { Fade, Stack, Typography } from '@avalabs/k2-alpine'; + +import { Action, EvmNetwork } from '@core/types'; + +import { DetailsSection } from '../../generic/DetailsSection'; +import { TotalFeeAmount } from '../../generic/NetworkFee'; + +import { GaslessSwitchRow } from './components/GaslessSwitch'; +import { useEvmTransactionFee } from './hooks/useEvmTransactionFee'; +import { FeePresetSelector } from './components'; +import { useTranslation } from 'react-i18next'; + +type EvmNetworkFeeWidgetProps = { + action: Action; + network: EvmNetwork; +}; + +export const EvmNetworkFeeWidget = ({ + action, + network, +}: EvmNetworkFeeWidgetProps) => { + const { t } = useTranslation(); + const { + gasLimit, + feePreset, + choosePreset, + fee, + nativeToken, + customPreset, + feeDecimals, + isLoading, + hasEnoughForNetworkFee, + } = useEvmTransactionFee({ + action, + network, + }); + + return ( + + + + + {!isLoading && ( + <> + + + + )} + + + + + + {t('Insufficient balance for fee')} + + + + + ); +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/CustomGasSettings.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/CustomGasSettings.tsx new file mode 100644 index 000000000..b01b34d28 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/CustomGasSettings.tsx @@ -0,0 +1,212 @@ +import { useTranslation } from 'react-i18next'; +import { formatUnits, parseUnits } from 'ethers'; +import { ComponentProps, FC, useEffect, useState } from 'react'; +import { Button, Fade, Stack, Tooltip, Typography } from '@avalabs/k2-alpine'; + +import { calculateGasAndFees } from '@core/common'; +import { FeeRate, NativeTokenBalance } from '@core/types'; +import { useKeyboardShortcuts, useSettingsContext } from '@core/ui'; + +import { Page } from '@/components/Page'; +import { InvisibileInput } from '@/pages/Contacts/components'; + +import { DetailsSection } from '../../../generic/DetailsSection'; +import { TxDetailsRow } from '../../../generic/DetailsItem/items/DetailRow'; + +type CustomGasSettingsProps = { + onBack: () => void; + customPreset: FeeRate; + nativeToken: NativeTokenBalance; + gasLimit: number; + onSave: (feeRate: FeeRate) => void; + feeDecimals: number; +}; + +export const CustomGasSettings: FC = ({ + onBack, + customPreset, + nativeToken, + gasLimit, + onSave, + feeDecimals, +}) => { + const { t } = useTranslation(); + + const { currencyFormatter } = useSettingsContext(); + + const [error, setError] = useState(''); + const [customMaxFeeString, setCustomMaxFeeString] = useState( + formatUnits(customPreset.maxFeePerGas, feeDecimals), + ); + const [customMaxTipString, setCustomMaxTipString] = useState( + formatUnits(customPreset.maxPriorityFeePerGas ?? 1n, feeDecimals), + ); + + const fees = calculateGasAndFees({ + maxFeePerGas: customMaxFeeString + ? parseUnits(customMaxFeeString, feeDecimals) + : customPreset.maxFeePerGas, + tokenPrice: nativeToken.priceInCurrency, + tokenDecimals: nativeToken.decimals, + gasLimit, + }); + + const totalFeeInCurrency = fees?.feeUSD ?? null; + + useEffect(() => { + setError(''); + + try { + const newMaxFee = customMaxFeeString + ? parseUnits(customMaxFeeString, feeDecimals) + : customPreset.maxFeePerGas; + const newMaxTip = customMaxTipString + ? parseUnits(customMaxTipString, feeDecimals) + : customPreset.maxPriorityFeePerGas; + + if (!newMaxTip || !newMaxFee) { + return; + } + + if (newMaxTip > newMaxFee) { + setError(t('Max base fee must be greater than max priority fee')); + } + } catch { + setError(t('Please enter a valid decimal value')); + } + }, [ + customMaxFeeString, + customMaxTipString, + customPreset.maxFeePerGas, + customPreset.maxPriorityFeePerGas, + feeDecimals, + t, + ]); + + const handleSave = () => { + onSave({ + maxFeePerGas: parseUnits(customMaxFeeString, feeDecimals), + maxPriorityFeePerGas: parseUnits(customMaxTipString, feeDecimals), + }); + }; + + const keyboardShortcuts = useKeyboardShortcuts({ + Enter: handleSave, + }); + + return ( + + + + + + { + setCustomMaxFeeString(ev.currentTarget.value || '0'); + }} + /> + + + {currencyFormatter(totalFeeInCurrency ?? 0)} + + + + + + { + setCustomMaxTipString(ev.currentTarget.value || '0'); + }} + /> + + + + + + + + + + + + ~{fees?.feeUnit.toDisplay()} {nativeToken.symbol} + + + + + {currencyFormatter(totalFeeInCurrency ?? 0)} + + + + + + + + {error} + + + + + + + + + + ); +}; + +const FeeInput = (props: ComponentProps) => ( + +); diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/FeePresetSelector.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/FeePresetSelector.tsx new file mode 100644 index 000000000..479a1e9f7 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/FeePresetSelector.tsx @@ -0,0 +1,90 @@ +import { FC, useState } from 'react'; +import { Stack } from '@avalabs/k2-alpine'; +import { useTranslation } from 'react-i18next'; + +import { FeeRate, NativeTokenBalance } from '@core/types'; + +import { SlideUpDialog } from '@/components/Dialog'; + +import { FeePresetButton } from '../../../generic/NetworkFee'; +import { CustomGasSettings } from './CustomGasSettings'; +import { EvmFeePreset } from '../types'; + +type FeePresetSelectorProps = { + gasLimit: number; + feePreset: EvmFeePreset; + customPreset?: FeeRate; + nativeToken: NativeTokenBalance; + choosePreset: (preset: EvmFeePreset, feeRate?: FeeRate) => void; + feeDecimals: number; +}; + +export const FeePresetSelector: FC = ({ + gasLimit, + feePreset, + nativeToken, + choosePreset, + customPreset, + feeDecimals, +}) => { + const { t } = useTranslation(); + + const [dialogAnchor, setDialogAnchor] = useState( + null, + ); + + const customGasOpen = Boolean(dialogAnchor); + const closeDialog = () => setDialogAnchor(null); + + return ( + + choosePreset('slow')} + > + {t('Slow')} + + choosePreset('normal')} + > + {t('Normal')} + + choosePreset('fast')} + > + {t('Fast')} + + setDialogAnchor(ev.currentTarget)} + > + {t('Custom')} + + + {customPreset && ( + + { + choosePreset('custom', feeRate); + closeDialog(); + }} + onBack={closeDialog} + feeDecimals={feeDecimals} + /> + + )} + + ); +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/GaslessSwitch.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/GaslessSwitch.tsx new file mode 100644 index 000000000..7aa51b79e --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/GaslessSwitch.tsx @@ -0,0 +1,28 @@ +import { useTranslation } from 'react-i18next'; +import { Stack, Switch, Tooltip, Typography } from '@avalabs/k2-alpine'; + +export const GaslessSwitchRow = () => { + const { t } = useTranslation(); + + return ( + + + + {t('Get free gas')} + + + {t('Gas fees paid by Core')} + + + + + + + ); +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/index.ts new file mode 100644 index 000000000..8bee91f9d --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/index.ts @@ -0,0 +1,3 @@ +export * from './GaslessSwitch'; +export * from './CustomGasSettings'; +export * from './FeePresetSelector'; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/index.ts new file mode 100644 index 000000000..38b744de3 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/index.ts @@ -0,0 +1 @@ +export * from './useEvmTransactionFee'; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/getFeeInfo.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/getFeeInfo.ts new file mode 100644 index 000000000..dd978aad1 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/getFeeInfo.ts @@ -0,0 +1,9 @@ +import { EvmTxSigningData } from '../types'; + +export const getFeeInfo = ({ data }: EvmTxSigningData) => ({ + feeRate: data.maxFeePerGas ? BigInt(data.maxFeePerGas) : 0n, + maxTipRate: data.maxPriorityFeePerGas + ? BigInt(data.maxPriorityFeePerGas) + : 0n, + limit: Number(data.gasLimit ?? 0), +}); diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/getInitialFeeRate.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/getInitialFeeRate.ts new file mode 100644 index 000000000..b5c17e05e --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/getInitialFeeRate.ts @@ -0,0 +1,11 @@ +import { EvmTxSigningData } from '../types'; + +export const getInitialFeeRate = ( + data?: EvmTxSigningData, +): bigint | undefined => { + if (!data) { + return undefined; + } + + return data.data.maxFeePerGas ? BigInt(data.data.maxFeePerGas) : undefined; +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/hasEnoughForFee.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/hasEnoughForFee.ts new file mode 100644 index 000000000..cfd30743f --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/hasEnoughForFee.ts @@ -0,0 +1,16 @@ +import { NativeTokenBalance } from '@core/types'; + +import { EvmTxSigningData } from '../types'; +import { getFeeInfo } from './getFeeInfo'; + +export const hasEnoughForFee = ( + data?: EvmTxSigningData, + nativeToken?: NativeTokenBalance, +) => { + if (!data || !nativeToken) return false; + + const info = getFeeInfo(data); + const need = info.feeRate * BigInt(info.limit); + + return nativeToken.balance > need; +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/index.ts new file mode 100644 index 000000000..f937ecaf7 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/lib/index.ts @@ -0,0 +1,3 @@ +export * from './getFeeInfo'; +export * from './hasEnoughForFee'; +export * from './getInitialFeeRate'; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/types.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/types.ts new file mode 100644 index 000000000..3d19abf54 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/types.ts @@ -0,0 +1,44 @@ +import { DisplayData, RpcMethod, SigningData } from '@avalabs/vm-module-types'; + +import type { calculateGasAndFees } from '@core/common'; +import { Action, EvmNetwork, FeeRate, NativeTokenBalance } from '@core/types'; +import { EvmFeePreset } from '../../types'; + +export type EvmTxSigningData = Extract< + SigningData, + { type: RpcMethod.ETH_SEND_TRANSACTION } +>; + +type ResultBase = { + feePreset: EvmFeePreset; + presets: Record; + gasLimit: number; + choosePreset: (preset: EvmFeePreset, feeRate?: FeeRate) => void; + customPreset: FeeRate; + setCustomPreset: (preset: FeeRate) => void; + nativeToken: NativeTokenBalance; + fee: ReturnType; + feeDecimals: number; + hasEnoughForNetworkFee: boolean; +}; + +export type UseEvmTransactionFeeReadyResult = { + isLoading: false; +} & ResultBase; + +export type UseEvmTransactionFeeLoadingResult = { + isLoading: true; +} & Partial; + +export type UseEvmTransactionFeeArgs = { + action: Action; + network: EvmNetwork; +}; + +export type UseEvmTransactionFeeResult = + | UseEvmTransactionFeeReadyResult + | UseEvmTransactionFeeLoadingResult; + +export type UseEvmTransactionFee = ( + args: UseEvmTransactionFeeArgs, +) => UseEvmTransactionFeeResult; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/useEvmTransactionFee.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/useEvmTransactionFee.tsx new file mode 100644 index 000000000..f2e70a59d --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/useEvmTransactionFee.tsx @@ -0,0 +1,130 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { useConnectionContext } from '@core/ui'; +import { ExtensionRequest, FeeRate } from '@core/types'; +import { type UpdateActionTxDataHandler } from '@core/service-worker'; +import { calculateGasAndFees, isAvalancheNetwork } from '@core/common'; + +import { useNativeToken } from '@/hooks/useNativeToken'; +import { useUpdateAccountBalance } from '@/hooks/useUpdateAccountBalance'; +import { useCurrentFeesForNetwork } from '@/hooks/useCurrentFeesForNetwork'; + +import { EvmFeePreset } from '../../types'; +import { getFeeInfo, hasEnoughForFee } from './lib'; +import { EvmTxSigningData, UseEvmTransactionFee } from './types'; + +export const useEvmTransactionFee: UseEvmTransactionFee = ({ + action, + network, +}) => { + useUpdateAccountBalance(network); + + const { request } = useConnectionContext(); + + const networkFee = useCurrentFeesForNetwork(network); + const nativeToken = useNativeToken({ network }); + const signingData = action.signingData as EvmTxSigningData; + + const [customPreset, setCustomPreset] = useState(networkFee?.high); + + const [feePreset, setFeePreset] = useState( + isAvalancheNetwork(network) ? 'fast' : 'slow', + ); + + const fee = calculateGasAndFees({ + maxFeePerGas: getFeeInfo(signingData).feeRate, + tokenPrice: nativeToken?.priceInCurrency, + tokenDecimals: network?.networkToken.decimals, + gasLimit: getFeeInfo(signingData).limit, + }); + + const updateFee = useCallback( + async (maxFeeRate: bigint, maxTipRate?: bigint) => { + if (!action.actionId) { + return; + } + + await request({ + method: ExtensionRequest.ACTION_UPDATE_TX_DATA, + params: [action.actionId, { maxFeeRate, maxTipRate }], + }); + }, + [action?.actionId, request], + ); + + useEffect(() => { + if (!networkFee) { + return; + } + + // Initialize the custom preset with some default value. + // Usually people will try to bump the fees, so we default to the fastest preset. + setCustomPreset((previous) => previous ?? networkFee.high); + }, [networkFee]); + + const choosePreset = useCallback( + async (preset: EvmFeePreset, feeRate?: FeeRate) => { + if (!networkFee) { + return; + } + + if (preset === 'custom') { + if (!feeRate) return; + + setFeePreset('custom'); + setCustomPreset(feeRate); + await updateFee(feeRate.maxFeePerGas, feeRate.maxPriorityFeePerGas); + return; + } + + setFeePreset(preset); + + switch (preset) { + case 'slow': + await updateFee( + networkFee.low.maxFeePerGas, + networkFee.low.maxPriorityFeePerGas, + ); + break; + case 'normal': + await updateFee( + networkFee.medium.maxFeePerGas, + networkFee.medium.maxPriorityFeePerGas, + ); + break; + case 'fast': + await updateFee( + networkFee.high.maxFeePerGas, + networkFee.high.maxPriorityFeePerGas, + ); + break; + } + }, + [networkFee, updateFee], + ); + + if (!networkFee || !nativeToken || !signingData || !customPreset) { + return { + isLoading: true, + }; + } + + return { + hasEnoughForNetworkFee: hasEnoughForFee(signingData, nativeToken), + fee, + feeDecimals: networkFee.displayDecimals, + feePreset, + presets: { + slow: networkFee.low, + normal: networkFee.medium, + fast: networkFee.high, + custom: customPreset, + }, + gasLimit: Number(signingData.data.gasLimit ?? 0), + choosePreset, + customPreset, + setCustomPreset, + nativeToken, + isLoading: false, + }; +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/index.ts new file mode 100644 index 000000000..0a9e96dc9 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/index.ts @@ -0,0 +1 @@ +export * from './EvmNetworkFeeWidget'; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/types.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/types.ts new file mode 100644 index 000000000..867299942 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/types.ts @@ -0,0 +1 @@ +export type EvmFeePreset = 'slow' | 'normal' | 'fast' | 'custom'; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx index de6d8084f..671f9ea5f 100644 --- a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx @@ -1,11 +1,20 @@ -import { Stack, StackProps, Typography } from '@avalabs/k2-alpine'; +import { + Box, + Stack, + StackProps, + Tooltip, + Typography, +} from '@avalabs/k2-alpine'; +import { MdInfoOutline } from 'react-icons/md'; type TxDetailsRowProps = StackProps & { label: string; + tooltip?: string; }; export const TxDetailsRow = ({ label, + tooltip, children, ...rest }: TxDetailsRowProps) => ( @@ -19,7 +28,18 @@ export const TxDetailsRow = ({ justifyContent="space-between" {...rest} > - {label} + + + {label} + + {tooltip && ( + + + + + + )} + {children} ); diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx index 20e92776d..b09158609 100644 --- a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx @@ -1,11 +1,16 @@ -import { PropsWithChildren } from 'react'; -import { Divider, Stack, styled } from '@avalabs/k2-alpine'; +import { + CardProps, + combineSx, + Divider, + Stack, + styled, +} from '@avalabs/k2-alpine'; import { Card } from '@/components/Card'; -export const DetailsSection = ({ children }: PropsWithChildren) => { +export const DetailsSection = ({ children, sx, ...props }: CardProps) => { return ( - + }>{children} ); diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/NetworkFee/FeePresetButton.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/NetworkFee/FeePresetButton.tsx new file mode 100644 index 000000000..f3ebf77cd --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/NetworkFee/FeePresetButton.tsx @@ -0,0 +1,13 @@ +import { Button, ButtonProps, styled } from '@avalabs/k2-alpine'; + +export const FeePresetButton = styled((props: ButtonProps) => ( + + + + + + ); +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomLimitTrigger.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomLimitTrigger.tsx new file mode 100644 index 000000000..221b76b18 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomLimitTrigger.tsx @@ -0,0 +1,84 @@ +import { + ChevronRightIcon, + Stack, + StackProps, + Typography, +} from '@avalabs/k2-alpine'; +import { useTranslation } from 'react-i18next'; +import { TokenApproval } from '@avalabs/vm-module-types'; + +import { useSettingsContext } from '@core/ui'; + +import { CollapsedTokenAmount } from '@/components/CollapsedTokenAmount'; + +import { ApprovalValue, SpendLimit } from '../types'; +import { InfinitySymbol } from './InfinitySymbol'; + +type CustomLimitTriggerProps = StackProps & { + spendLimit: SpendLimit; + approval: TokenApproval; + approvalValue: ApprovalValue; +}; + +export const CustomLimitTrigger = ({ + spendLimit, + approval, + approvalValue, + ...props +}: CustomLimitTriggerProps) => { + const { t } = useTranslation(); + const { currencyFormatter, currency } = useSettingsContext(); + + const { tokenValue, currencyValue, isUnlimited } = approvalValue; + + return ( + + + + {spendLimit.type === 'unlimited' ? ( + + ) : ( + + )} + + {approval.token.symbol} + + + {isUnlimited ? ( + + {t('Unlimited {{currency}}', { + currency, + })} + + ) : ( + currencyValue && ( + + {currencyFormatter(Number(currencyValue || '0'))} + + ) + )} + + + + ); +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/InfinitySymbol.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/InfinitySymbol.tsx new file mode 100644 index 000000000..6e9768b16 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/InfinitySymbol.tsx @@ -0,0 +1,33 @@ +import { Typography, TypographyProps } from '@avalabs/k2-alpine'; + +type InfinitySymbolProps = TypographyProps & { + symbolSize: 'small' | 'large'; +}; + +const PROPS_BY_SIZE: Record< + InfinitySymbolProps['symbolSize'], + TypographyProps +> = { + small: { + variant: 'h5', + fontWeight: 400, + }, + large: { + variant: 'h2', + fontWeight: 400, + fontSize: 64, + lineHeight: '20px', + height: 36, + }, +}; + +export const InfinitySymbol = ({ + symbolSize, + ...props +}: InfinitySymbolProps) => { + return ( + + ∞ + + ); +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/TokenSpendLimitCard.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/TokenSpendLimitCard.tsx new file mode 100644 index 000000000..503c6a092 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/TokenSpendLimitCard.tsx @@ -0,0 +1,148 @@ +import { FC } from 'react'; +import { + Avatar, + AvatarProps, + Stack, + styled, + Typography, +} from '@avalabs/k2-alpine'; +import { useTranslation } from 'react-i18next'; +import { TokenApproval, TokenType } from '@avalabs/vm-module-types'; + +import { useSettingsContext } from '@core/ui'; + +import { CollapsedTokenAmount } from '@/components/CollapsedTokenAmount'; + +import { ApprovalValue } from '../types'; +import { InfinitySymbol } from './InfinitySymbol'; +import { TxDetailsRow } from '../../../generic/DetailsItem/items/DetailRow'; + +type TokenSpendLimitCardProps = { + approval: TokenApproval; + approvalValue: ApprovalValue; +}; + +export const TokenSpendLimitCard: FC = ({ + approval, + approvalValue, +}) => { + return approval.token.type !== TokenType.ERC20 ? ( + + ) : ( + + ); +}; + +const NFTApprovalHeader = ({ + approval, + approvalValue, +}: { + approval: TokenApproval; + approvalValue: ApprovalValue; +}) => { + const { t } = useTranslation(); + const { currencyFormatter } = useSettingsContext(); + + const { isUnlimited, tokenValue, currencyValue } = approvalValue; + const { logoUri } = approval; + + if (!isUnlimited) { + return ( + + + + + + {approval.token.symbol} + + + {currencyValue && ( + + {currencyFormatter(Number(currencyValue || '0'))} + + )} + + + ); + } + + return ( + + + {approval.token.name} + + {t('Approves all {{token}}', { token: approval.token.name || '' })} + + + ); +}; + +const ERC20ApprovalHeader = ({ + approval, + approvalValue, +}: { + approval: TokenApproval; + approvalValue: ApprovalValue; +}) => { + const { t } = useTranslation(); + const { currencyFormatter, currency } = useSettingsContext(); + + const { tokenValue, isUnlimited, currencyValue } = approvalValue; + + return ( + + + {isUnlimited ? ( + + ) : ( + + )} + {approval.token.symbol} + + {isUnlimited ? ( + + {t('Unlimited {{currency}}', { currency })} + + ) : ( + currencyValue && ( + + {currencyFormatter(Number(currencyValue || '0'))} + + ) + )} + + ); +}; + +type SizedAvatarProps = AvatarProps & { + size: number; +}; +const SizedAvatar = styled(Avatar)(({ size }) => ({ + width: size, + height: size, + backgroundColor: 'transparent', +})); diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/index.ts new file mode 100644 index 000000000..39c2326cd --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/index.ts @@ -0,0 +1,4 @@ +export * from './CustomApprovalLimit'; +export * from './InfinitySymbol'; +export * from './AmountInput'; +export * from './TokenSpendLimitCard'; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/index.ts new file mode 100644 index 000000000..0022a7de0 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/index.ts @@ -0,0 +1 @@ +export * from './EvmTokenApprovals'; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/getApprovalValue.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/getApprovalValue.ts new file mode 100644 index 000000000..d5c13cc05 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/getApprovalValue.ts @@ -0,0 +1,36 @@ +import { MaxUint256 } from 'ethers'; +import { TokenUnit } from '@avalabs/core-utils-sdk'; +import { TokenApproval, TokenType } from '@avalabs/vm-module-types'; + +export const getApprovalValue = ( + approval: TokenApproval, + getTokenPrice: (symbolOrAddress: string) => number | undefined, +) => { + if (!approval.value) { + return null; + } + + const isNFT = + approval.token.type === TokenType.ERC721 || + approval.token.type === TokenType.ERC1155; + const tokenAmount = new TokenUnit( + typeof approval.value === 'string' + ? BigInt(approval.value) + : (approval.value ?? 0n), + approval.token.type !== TokenType.ERC20 ? 0 : approval.token.decimals, + '', + ); + const tokenPrice = getTokenPrice(approval.token.address); + const isUnlimited = tokenAmount.toSubUnit() === MaxUint256; + + return { + isNFT, + isUnlimited, + tokenValue: tokenAmount, + logoUri: approval.token.logoUri, + currencyValue: + typeof tokenPrice === 'number' && Number.isFinite(tokenPrice) + ? tokenAmount.toDisplay({ asNumber: true }) * tokenPrice + : undefined, + }; +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/index.ts new file mode 100644 index 000000000..d98f2850a --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/index.ts @@ -0,0 +1,2 @@ +export * from './getApprovalValue'; +export * from './isUnlimitedApproval'; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/isUnlimitedApproval.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/isUnlimitedApproval.ts new file mode 100644 index 000000000..32ee00054 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/isUnlimitedApproval.ts @@ -0,0 +1,8 @@ +import { MaxUint256 } from 'ethers'; +import { TokenApproval } from '@avalabs/vm-module-types'; + +const MAX_UINT256_HEX = `0x${MaxUint256.toString(16)}`; + +export const isUnlimitedApproval = (approval: TokenApproval) => { + return approval.value === MAX_UINT256_HEX; +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/types.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/types.ts new file mode 100644 index 000000000..08b7da559 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/types.ts @@ -0,0 +1,10 @@ +import { getApprovalValue } from './lib'; + +export type LimitType = 'requested' | 'unlimited' | 'custom'; +export type SpendLimit = { + type: LimitType; + value?: bigint; +}; + +export type NullableApprovalValue = ReturnType; +export type ApprovalValue = NonNullable; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/DetailsItem.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/DetailsItem.tsx index 2360fad30..cb9b85560 100644 --- a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/DetailsItem.tsx +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/DetailsItem.tsx @@ -6,6 +6,8 @@ import { AddressDetail } from './items/AddressDetail'; import { LinkDetail } from './items/LinkDetail'; import { NetworkDetail } from './items/NetworkDetail'; import { RawDataDetail } from './items/RawDataDetails/RawDataDetail'; +import { FundsRecipientDetail } from './items/FundsRecipientDetail'; +import { CurrencyDetail } from './items/CurrencyDetail'; type DetailsItemProps = { item: DetailItem; @@ -31,5 +33,11 @@ export const DetailsItem = ({ item }: DetailsItemProps) => { case DetailItemType.DATA: return ; + + case DetailItemType.FUNDS_RECIPIENT: + return ; + + case DetailItemType.CURRENCY: + return ; } }; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/CurrencyDetail.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/CurrencyDetail.tsx new file mode 100644 index 000000000..12d97c1bc --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/CurrencyDetail.tsx @@ -0,0 +1,36 @@ +import { CurrencyItem } from '@avalabs/vm-module-types'; +import { Stack, Typography } from '@avalabs/k2-alpine'; + +import { TokenUnit } from '@avalabs/core-utils-sdk'; +import { useBalancesContext, useSettingsContext } from '@core/ui'; + +import { TxDetailsRow } from './DetailRow'; + +type CurrencyDetailProps = { + item: CurrencyItem; + customLabel?: React.ReactNode; +}; + +export const CurrencyDetail = ({ item, customLabel }: CurrencyDetailProps) => { + const { getTokenPrice } = useBalancesContext(); + const { currencyFormatter } = useSettingsContext(); + const { label, symbol, maxDecimals, value } = item; + + const token = new TokenUnit(value, maxDecimals, symbol); + const price = getTokenPrice(symbol); + + return ( + + + + {token.toDisplay()} {symbol} + + {price && ( + + {currencyFormatter(price * token.toDisplay({ asNumber: true }))} + + )} + + + ); +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx index 671f9ea5f..03a5fa156 100644 --- a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx @@ -8,7 +8,7 @@ import { import { MdInfoOutline } from 'react-icons/md'; type TxDetailsRowProps = StackProps & { - label: string; + label: React.ReactNode | string; tooltip?: string; }; @@ -29,9 +29,13 @@ export const TxDetailsRow = ({ {...rest} > - - {label} - + {typeof label === 'string' ? ( + + {label} + + ) : ( + label + )} {tooltip && ( diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/FundsRecipientDetail.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/FundsRecipientDetail.tsx new file mode 100644 index 000000000..4b687763b --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/FundsRecipientDetail.tsx @@ -0,0 +1,31 @@ +import { DetailItemType, FundsRecipientItem } from '@avalabs/vm-module-types'; +import { Tooltip, truncateAddress, Typography } from '@avalabs/k2-alpine'; + +import { CurrencyDetail } from './CurrencyDetail'; + +type FundsRecipientDetailProps = { + item: FundsRecipientItem; +}; + +export const FundsRecipientDetail = ({ item }: FundsRecipientDetailProps) => { + const { label: address, amount, symbol, maxDecimals } = item; + + return ( + + + {truncateAddress(address, 10)} + + + } + item={{ + label: address, + value: amount, + symbol, + maxDecimals, + type: DetailItemType.CURRENCY, + }} + /> + ); +}; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx index b09158609..4144d30bc 100644 --- a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx @@ -2,16 +2,28 @@ import { CardProps, combineSx, Divider, + DividerProps, Stack, styled, } from '@avalabs/k2-alpine'; import { Card } from '@/components/Card'; -export const DetailsSection = ({ children, sx, ...props }: CardProps) => { +type DetailsSectionProps = CardProps & { + dimmedDivider?: boolean; +}; + +export const DetailsSection = ({ + children, + sx, + dimmedDivider = false, + ...props +}: DetailsSectionProps) => { return ( - - }>{children} + + }> + {children} + ); }; @@ -20,9 +32,15 @@ export const DetailsSection = ({ children, sx, ...props }: CardProps) => { * Some children might be null, which breaks 's "divider" prop. * We need to hide the dividers if they're either first, last or next to another. */ -const StyledDivider = styled(Divider)(({ theme }) => ({ +type StyledDividerProps = DividerProps & { + dimmed?: boolean; +}; +const StyledDivider = styled(Divider, { + shouldForwardProp: (prop) => prop !== 'dimmed', +})(({ theme, dimmed }) => ({ marginInline: theme.spacing(2), '&:last-child, &:first-child, &+&': { display: 'none', }, + opacity: dimmed ? 0.5 : 1, })); diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/TransactionBalanceChange.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/TransactionBalanceChange.tsx index 8b0574378..bd529e4d0 100644 --- a/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/TransactionBalanceChange.tsx +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/TransactionBalanceChange.tsx @@ -10,6 +10,8 @@ import { BatchTokenBalanceChange, SingleTokenBalanceChange, } from './components'; +import { SimulationAlertBox } from './components/SimulationAlertBox'; +import { useTranslation } from 'react-i18next'; type TransactionBalanceChangeProps = BalanceChange & { isSimulationSuccessful?: boolean; @@ -18,51 +20,85 @@ type TransactionBalanceChangeProps = BalanceChange & { export const TransactionBalanceChange: FC = ({ ins, outs, + isSimulationSuccessful, }) => { - // TODO: Add the warnings below -- to be defined by the UX team. - // const hasSentItems = outs.length > 0; - // const hasReceivedItems = ins.length > 0; - // const showNoPreExecWarning = isSimulationSuccessful === false; // may be undefined - // const showNoDataWarning = - // !hasSentItems && !hasReceivedItems && !isSimulationSuccessful; + const { t } = useTranslation(); + + const hasSentItems = outs.filter(({ items }) => items.length > 0).length > 0; + const hasReceivedItems = + ins.filter(({ items }) => items.length > 0).length > 0; + const hasSomeBalanceChangeInfo = hasSentItems || hasReceivedItems; + const showNoPreExecWarning = isSimulationSuccessful === false; // may be undefined + const showNoDataWarning = + !hasSomeBalanceChangeInfo && !isSimulationSuccessful; return ( - - {outs.map(({ token, items }) => - items.length === 1 ? ( - - ) : ( - - ), + <> + {hasSomeBalanceChangeInfo && ( + + {outs.map(({ token, items }) => + items.length <= 1 ? ( + + ) : ( + + ), + )} + {ins + .filter(({ items }) => items.length > 0) + .map(({ token, items }) => + items.length <= 1 ? ( + + ) : ( + + ), + )} + + )} + {showNoPreExecWarning && ( + )} - {ins.map(({ token, items }) => - items.length === 1 ? ( - - ) : ( - - ), + {!showNoPreExecWarning && showNoDataWarning && ( + )} - + ); }; diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/components/SimulationAlertBox.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/components/SimulationAlertBox.tsx new file mode 100644 index 000000000..86ee2d1e2 --- /dev/null +++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/components/SimulationAlertBox.tsx @@ -0,0 +1,32 @@ +import { Box, Stack, StackProps, Typography } from '@avalabs/k2-alpine'; +import { FiAlertCircle } from 'react-icons/fi'; + +type SimulationAlertBoxProps = StackProps & { + textLines: string[]; +}; + +export const SimulationAlertBox = ({ + textLines, + ...stackProps +}: SimulationAlertBoxProps) => { + return ( + + + + + + {textLines.map((text) => ( + + {text} + + ))} + + + ); +}; diff --git a/apps/next/src/pages/Approve/components/ActionDrawer.tsx b/apps/next/src/pages/Approve/components/ActionDrawer.tsx index 3c6ab5854..d67fc1c49 100644 --- a/apps/next/src/pages/Approve/components/ActionDrawer.tsx +++ b/apps/next/src/pages/Approve/components/ActionDrawer.tsx @@ -6,10 +6,15 @@ import { StackProps, styled, } from '@avalabs/k2-alpine'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DisplayData } from '@avalabs/vm-module-types'; -import { Action, ActionStatus } from '@core/types'; +import { Action, ActionStatus, GaslessPhase } from '@core/types'; + +import { hasOverlayWarning } from '../lib'; +import { InDrawerAlert } from './warnings/InDrawerAlert'; +import { useGasless } from '../hooks'; type ActionDrawerProps = StackProps & { open: boolean; @@ -27,39 +32,55 @@ export const ActionDrawer = ({ }: ActionDrawerProps) => { const { t } = useTranslation(); + const { gaslessPhase } = useGasless({ action }); + + const isMalicious = hasOverlayWarning(action); + const [userHasConfirmed, setUserHasConfirmed] = useState(false); + + const isProcessing = + action.status === ActionStatus.SUBMITTING || + gaslessPhase === GaslessPhase.FUNDING_IN_PROGRESS; + return ( - {approve && ( - - )} - {reject && ( - + {isMalicious && ( + )} + + {approve && ( + + )} + {reject && ( + + )} + ); }; -const Drawer = styled(Stack)(({ theme }) => ({ +export const Drawer = styled(Stack)(({ theme }) => ({ width: '100%', position: 'sticky', bottom: 0, diff --git a/apps/next/src/pages/Approve/components/index.ts b/apps/next/src/pages/Approve/components/index.ts index a2b347deb..0c0e57cad 100644 --- a/apps/next/src/pages/Approve/components/index.ts +++ b/apps/next/src/pages/Approve/components/index.ts @@ -4,3 +4,4 @@ export * from './ApprovalScreenTitle'; export * from './LoadingScreen'; export * from './UnsupportedNetworkScreen'; export * as Styled from './Styled'; +export * from './warnings'; diff --git a/apps/next/src/pages/Approve/components/warnings/InDrawerAlert.tsx b/apps/next/src/pages/Approve/components/warnings/InDrawerAlert.tsx new file mode 100644 index 000000000..f7a95b013 --- /dev/null +++ b/apps/next/src/pages/Approve/components/warnings/InDrawerAlert.tsx @@ -0,0 +1,60 @@ +import { + Box, + Stack, + styled, + Typography, + useTheme, + Switch, +} from '@avalabs/k2-alpine'; +import { MdOutlineRemoveModerator } from 'react-icons/md'; +import { Trans } from 'react-i18next'; +import { Dispatch, FC, SetStateAction } from 'react'; + +type InDrawerAlertProps = { + isConfirmed: boolean; + setIsConfirmed: Dispatch>; +}; + +export const InDrawerAlert: FC = ({ + isConfirmed, + setIsConfirmed, +}) => { + const theme = useTheme(); + return ( + + + + + + + , + }} + /> + + + + setIsConfirmed(!isConfirmed)} + /> + + + ); +}; + +const Wrapper = styled(Stack)(({ theme }) => ({ + flexDirection: 'row', + paddingInline: theme.spacing(2.5), + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(1), + marginInline: theme.spacing(-2), + gap: theme.spacing(4), + borderTop: `1px solid ${theme.palette.divider}`, + background: + theme.palette.mode === 'light' + ? theme.palette.common.white + : theme.palette.alphaMatch.backdropSolid, +})); diff --git a/apps/next/src/pages/Approve/components/warnings/MaliciousTxOverlay.tsx b/apps/next/src/pages/Approve/components/warnings/MaliciousTxOverlay.tsx new file mode 100644 index 000000000..aa70929aa --- /dev/null +++ b/apps/next/src/pages/Approve/components/warnings/MaliciousTxOverlay.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Alert } from '@avalabs/vm-module-types'; +import { MdOutlineRemoveModerator } from 'react-icons/md'; +import { Stack, Box, Typography, Button } from '@avalabs/k2-alpine'; + +import { SlideUpDialog } from '@/components/Dialog'; + +import { Drawer } from '../ActionDrawer'; + +type MaliciousTxOverlayProps = { + open: boolean; + cancelHandler: () => void; + alert: Alert; +}; + +export const MaliciousTxOverlay = ({ + open, + cancelHandler, + alert, +}: MaliciousTxOverlayProps) => { + const { t } = useTranslation(); + const [isAlertDialogOpen, setIsAlertDialogOpen] = useState(open); + + return ( + setIsAlertDialogOpen(false)} + > + + + + + + {alert.details.title} + + + {alert.details.description} + + + + + + + + + ); +}; diff --git a/apps/next/src/pages/Approve/components/warnings/NoteWarning.tsx b/apps/next/src/pages/Approve/components/warnings/NoteWarning.tsx new file mode 100644 index 000000000..49e40ef04 --- /dev/null +++ b/apps/next/src/pages/Approve/components/warnings/NoteWarning.tsx @@ -0,0 +1,27 @@ +import { FC } from 'react'; +import { Alert } from '@avalabs/vm-module-types'; +import { FiAlertCircle } from 'react-icons/fi'; +import { Stack, Box, Typography } from '@avalabs/k2-alpine'; + +type NoteWarningProps = { + alert: Alert; +}; + +export const NoteWarning: FC = ({ alert }) => ( + + + + + + {alert.details.title}. {alert.details.description} + + +); diff --git a/apps/next/src/pages/Approve/components/warnings/index.ts b/apps/next/src/pages/Approve/components/warnings/index.ts new file mode 100644 index 000000000..7c550e096 --- /dev/null +++ b/apps/next/src/pages/Approve/components/warnings/index.ts @@ -0,0 +1,3 @@ +export * from './MaliciousTxOverlay'; +export * from './InDrawerAlert'; +export * from './NoteWarning'; diff --git a/apps/next/src/pages/Approve/hooks/index.ts b/apps/next/src/pages/Approve/hooks/index.ts new file mode 100644 index 000000000..231bb3ad0 --- /dev/null +++ b/apps/next/src/pages/Approve/hooks/index.ts @@ -0,0 +1 @@ +export * from './useGasless'; diff --git a/apps/next/src/pages/Approve/hooks/types.ts b/apps/next/src/pages/Approve/hooks/types.ts new file mode 100644 index 000000000..8834f6ff1 --- /dev/null +++ b/apps/next/src/pages/Approve/hooks/types.ts @@ -0,0 +1,30 @@ +import { DisplayData } from '@avalabs/vm-module-types'; +import { Action } from '@core/types'; +import { useNetworkFeeContext } from '@core/ui'; + +export type UseGaslessArgs = { + action: Action; +}; + +export type UseGaslessReturn = Pick< + ReturnType, + | 'fetchAndSolveGaslessChallange' + | 'gaslessFundTx' + | 'isGaslessOn' + | 'setIsGaslessOn' + | 'fundTxHex' + | 'setGaslessDefaultValues' + | 'gaslessPhase' + | 'setGaslessEligibility' + | 'isGaslessEligible' +> & { + tryFunding: (approveCallback: () => void) => Promise; +}; + +export type UseGasless = (args: UseGaslessArgs) => UseGaslessReturn; + +export type GaslessEligibilityParams = [ + chainId: number, + fromAddress: string | undefined, + nonce: number | undefined, +]; diff --git a/apps/next/src/pages/Approve/hooks/useGasless.ts b/apps/next/src/pages/Approve/hooks/useGasless.ts new file mode 100644 index 000000000..7ea23acce --- /dev/null +++ b/apps/next/src/pages/Approve/hooks/useGasless.ts @@ -0,0 +1,118 @@ +import { DisplayData, RpcMethod } from '@avalabs/vm-module-types'; +import { caipToChainId } from '@core/common'; +import { Action, GaslessPhase } from '@core/types'; +import { useAnalyticsContext, useNetworkFeeContext } from '@core/ui'; +import { GaslessEligibilityParams, UseGasless } from './types'; +import { isUndefined } from 'lodash'; +import { useCallback, useEffect, useMemo } from 'react'; +import { toast } from '@avalabs/k2-alpine'; +import { useTranslation } from 'react-i18next'; + +export const useGasless: UseGasless = ({ action }) => { + const { t } = useTranslation(); + const { + isGaslessOn, + setIsGaslessOn, + gaslessFundTx, + fundTxHex, + setGaslessDefaultValues, + gaslessPhase, + setGaslessEligibility, + fetchAndSolveGaslessChallange, + isGaslessEligible, + } = useNetworkFeeContext(); + const { captureEncrypted } = useAnalyticsContext(); + + const eligibilityParams = useMemo( + () => getEligibilityParams(action), + [action], + ); + + // First check if the action is elligible for gasless + useEffect(() => { + if (eligibilityParams) { + setGaslessEligibility(...eligibilityParams); + } + }, [eligibilityParams, setGaslessEligibility]); + + // If we're eligible, fetch the gasless challenge + useEffect(() => { + if (isGaslessEligible && gaslessPhase === GaslessPhase.NOT_READY) { + fetchAndSolveGaslessChallange(); + } + }, [isGaslessEligible, fetchAndSolveGaslessChallange, gaslessPhase]); + + // Capture analytics events + useEffect(() => { + if (gaslessPhase === GaslessPhase.ERROR) { + captureEncrypted('GaslessFundFailed'); + } + if (gaslessPhase === GaslessPhase.FUNDED && fundTxHex) { + captureEncrypted('GaslessFundSuccessful', { + fundTxHex, + }); + } + }, [captureEncrypted, fundTxHex, gaslessPhase, setGaslessDefaultValues]); + + // Wrapper around the approval screen's approve callback so we don't pollute it with gasless funding logic + const tryFunding = useCallback( + async (approveCallback: () => void) => { + if (isGaslessOn && isGaslessEligible) { + try { + await gaslessFundTx(action?.signingData); + } catch { + toast.error(t('Gasless funding failed')); + // Do not auto-submit if user wanted to fund the transaction, but it failed + return; + } + } + // Submit the transaction + approveCallback(); + // Clear the gasless state + setGaslessDefaultValues(); + }, + [ + isGaslessOn, + isGaslessEligible, + gaslessFundTx, + action?.signingData, + t, + setGaslessDefaultValues, + ], + ); + + return { + isGaslessOn, + setIsGaslessOn, + gaslessFundTx, + fundTxHex, + setGaslessDefaultValues, + gaslessPhase, + setGaslessEligibility, + fetchAndSolveGaslessChallange, + isGaslessEligible, + tryFunding, + }; +}; + +const getEligibilityParams = ( + action: Action, +): GaslessEligibilityParams | null => { + if (!action) return null; + + const { signingData } = action; + const evmChainId = caipToChainId(action.scope); + + if (signingData?.type === RpcMethod.ETH_SEND_TRANSACTION) { + const fromAddress = isUndefined(signingData?.data.from) + ? undefined + : String(signingData?.data.from); + const nonce = isUndefined(signingData?.data.nonce) + ? undefined + : Number(signingData?.data.nonce); + + return [evmChainId, fromAddress, nonce]; + } + + return [evmChainId, undefined, undefined]; +}; diff --git a/apps/next/src/pages/Approve/lib/hasNoteWarning.ts b/apps/next/src/pages/Approve/lib/hasNoteWarning.ts new file mode 100644 index 000000000..d4d4d31a2 --- /dev/null +++ b/apps/next/src/pages/Approve/lib/hasNoteWarning.ts @@ -0,0 +1,7 @@ +import { Action, EnsureDefined } from '@core/types'; +import { AlertType, DisplayData } from '@avalabs/vm-module-types'; + +export const hasNoteWarning = ( + action: Action, +): action is Action> => + action.displayData.alert?.type === AlertType.WARNING; diff --git a/apps/next/src/pages/Approve/lib/hasOverlayWarning.ts b/apps/next/src/pages/Approve/lib/hasOverlayWarning.ts new file mode 100644 index 000000000..11025596f --- /dev/null +++ b/apps/next/src/pages/Approve/lib/hasOverlayWarning.ts @@ -0,0 +1,7 @@ +import { AlertType, DisplayData } from '@avalabs/vm-module-types'; +import { Action, EnsureDefined } from '@core/types'; + +export const hasOverlayWarning = ( + action: Action, +): action is Action> => + action.displayData.alert?.type === AlertType.DANGER; diff --git a/apps/next/src/pages/Approve/lib/index.ts b/apps/next/src/pages/Approve/lib/index.ts new file mode 100644 index 000000000..6b5a46cc2 --- /dev/null +++ b/apps/next/src/pages/Approve/lib/index.ts @@ -0,0 +1,2 @@ +export * from './hasNoteWarning'; +export * from './hasOverlayWarning'; diff --git a/apps/next/src/popup/app.tsx b/apps/next/src/popup/app.tsx index 82f87b9e1..003caf3fd 100644 --- a/apps/next/src/popup/app.tsx +++ b/apps/next/src/popup/app.tsx @@ -43,6 +43,7 @@ const pagesWithoutHeader = [ '/receive', '/approve', '/permissions', + '/network/switch', getContactsPath(), getSendPath(), ]; diff --git a/apps/next/src/routing/ApprovalRoutes.tsx b/apps/next/src/routing/ApprovalRoutes.tsx index b626bea65..fd7b9fe5d 100644 --- a/apps/next/src/routing/ApprovalRoutes.tsx +++ b/apps/next/src/routing/ApprovalRoutes.tsx @@ -4,6 +4,7 @@ import { Route, Switch, SwitchProps } from 'react-router-dom'; import { GenericApprovalScreen } from '@/pages/Approve/GenericApprovalScreen'; import { ApproveDappConnection } from '@/pages/Approve/ApproveDappConnection'; +import { ExtensionActionApprovalScreen } from '@/pages/Approve/ExtensionActionApprovalScreen'; export const ApprovalRoutes = (props: SwitchProps) => ( ( } > + + + + isAvalancheNetwork(network) ? DEFAULT_FEE_PRESET_C_CHAIN : DEFAULT_FEE_PRESET; diff --git a/packages/inpage/package.json b/packages/inpage/package.json index 38fdc4433..573f7118c 100644 --- a/packages/inpage/package.json +++ b/packages/inpage/package.json @@ -15,8 +15,8 @@ "typecheck": "yarn tsc --skipLibCheck --noEmit" }, "dependencies": { - "@avalabs/evm-module": "1.9.10", - "@avalabs/svm-module": "1.9.10", + "@avalabs/evm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304", + "@avalabs/svm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304", "@core/common": "workspace:*", "@core/messaging": "workspace:*", "@core/types": "workspace:*", diff --git a/packages/service-worker/package.json b/packages/service-worker/package.json index cf9f5ff3c..95a529ad0 100644 --- a/packages/service-worker/package.json +++ b/packages/service-worker/package.json @@ -16,9 +16,9 @@ "typecheck": "yarn tsc --skipLibCheck --noEmit" }, "dependencies": { - "@avalabs/avalanche-module": "1.9.10", + "@avalabs/avalanche-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304", "@avalabs/avalanchejs": "5.1.0-alpha.2", - "@avalabs/bitcoin-module": "1.9.10", + "@avalabs/bitcoin-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304", "@avalabs/bridge-unified": "4.0.3", "@avalabs/core-bridge-sdk": "3.1.0-alpha.60", "@avalabs/core-chains-sdk": "3.1.0-alpha.60", @@ -30,13 +30,13 @@ "@avalabs/core-token-prices-sdk": "3.1.0-alpha.60", "@avalabs/core-utils-sdk": "3.1.0-alpha.60", "@avalabs/core-wallets-sdk": "3.1.0-alpha.60", - "@avalabs/evm-module": "1.9.10", + "@avalabs/evm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304", "@avalabs/glacier-sdk": "3.1.0-alpha.60", - "@avalabs/hvm-module": "1.9.10", + "@avalabs/hvm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304", "@avalabs/hw-app-avalanche": "0.14.1", - "@avalabs/svm-module": "1.9.10", + "@avalabs/svm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304", "@avalabs/types": "3.1.0-alpha.60", - "@avalabs/vm-module-types": "1.9.10", + "@avalabs/vm-module-types": "0.0.0-fix-nft-set-approval-for-all-20250829175304", "@blockaid/client": "0.10.0", "@coinbase/cbpay-js": "1.6.0", "@cubist-labs/cubesigner-sdk": "0.3.28", diff --git a/packages/types/src/network-fee.ts b/packages/types/src/network-fee.ts index e390401a0..c18c106bb 100644 --- a/packages/types/src/network-fee.ts +++ b/packages/types/src/network-fee.ts @@ -1,3 +1,5 @@ +import { NetworkFees } from '@avalabs/vm-module-types'; + export type FeeRate = { maxFeePerGas: bigint; maxPriorityFeePerGas?: bigint; @@ -12,7 +14,10 @@ export interface NetworkFee { isFixedFee: boolean; } -export type TransactionPriority = 'low' | 'medium' | 'high'; +export type TransactionPriority = Extract< + keyof NetworkFees, + 'low' | 'medium' | 'high' +>; export type SerializedNetworkFee = Omit< NetworkFee, diff --git a/packages/types/src/network.ts b/packages/types/src/network.ts index f7c99b7ed..7384c4337 100644 --- a/packages/types/src/network.ts +++ b/packages/types/src/network.ts @@ -75,7 +75,12 @@ export type AddEthereumChainDisplayData = { export const PLACEHOLDER_RPC_HEADERS = { '': '' }; export type EvmNetwork = NetworkWithCaipId & { vmName: NetworkVMType.EVM }; +export type BtcNetwork = NetworkWithCaipId & { vmName: NetworkVMType.BITCOIN }; export const isEvmNetwork = ( network: NetworkWithCaipId, ): network is EvmNetwork => network.vmName === NetworkVMType.EVM; + +export const isBtcNetwork = ( + network: NetworkWithCaipId, +): network is BtcNetwork => network.vmName === NetworkVMType.BITCOIN; diff --git a/packages/ui/package.json b/packages/ui/package.json index 562443e23..8e00fba99 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -23,7 +23,7 @@ "@avalabs/glacier-sdk": "3.1.0-alpha.60", "@avalabs/hw-app-avalanche": "0.14.1", "@avalabs/types": "3.1.0-alpha.60", - "@avalabs/vm-module-types": "1.9.10", + "@avalabs/vm-module-types": "0.0.0-fix-nft-set-approval-for-all-20250829175304", "@blockaid/client": "0.10.0", "@cubist-labs/cubesigner-sdk": "0.3.28", "@ethereumjs/common": "2.6.5", diff --git a/packages/ui/src/contexts/BalancesProvider/BalancesProvider.tsx b/packages/ui/src/contexts/BalancesProvider/BalancesProvider.tsx index c9d9d74b0..e11337388 100644 --- a/packages/ui/src/contexts/BalancesProvider/BalancesProvider.tsx +++ b/packages/ui/src/contexts/BalancesProvider/BalancesProvider.tsx @@ -345,10 +345,14 @@ export function BalancesProvider({ children }: PropsWithChildren) { return; } + const accountBalances = + balances.tokens?.[tokenNetwork.chainId]?.[addressForChain]; + const token = - balances.tokens?.[tokenNetwork.chainId]?.[addressForChain]?.[ - addressOrSymbol - ]; + accountBalances?.[addressOrSymbol] ?? + // Also try lower-cased. + // Native token symbols are not lower-cased by the balance services. + accountBalances?.[addressOrSymbol.toLowerCase()]; return token?.priceInCurrency; }, diff --git a/packages/ui/src/hooks/useApproveAction.ts b/packages/ui/src/hooks/useApproveAction.ts index fadc2079b..65ec8c857 100644 --- a/packages/ui/src/hooks/useApproveAction.ts +++ b/packages/ui/src/hooks/useApproveAction.ts @@ -8,11 +8,13 @@ import { isBatchApprovalAction, ContextContainer, } from '@core/types'; +import { filter } from 'rxjs'; import { GetActionHandler, UpdateActionHandler } from '@core/service-worker'; import { isSpecificContextContainer } from '../utils/isSpecificContextContainer'; import { useCallback, useEffect, useState } from 'react'; import { getUpdatedSigningData } from '@core/common'; import { useWindowGetsClosedOrHidden } from './useWindowGetsClosedOrHidden'; +import { isActionsUpdate } from '../contexts/ApprovalsProvider/isActionsUpdate'; type ActionType = IsBatchApproval extends true ? MultiTxAction @@ -44,7 +46,7 @@ export function useApproveAction( actionId: string, isBatchApproval: boolean = false, ): HookResult | MultiTxAction | undefined> { - const { request } = useConnectionContext(); + const { request, events } = useConnectionContext(); const isConfirmPopup = isSpecificContextContainer(ContextContainer.CONFIRM); const { approval } = useApprovalsContext(); const [action, setAction] = useState>(); @@ -125,6 +127,22 @@ export function useApproveAction( } }, [actionId, request, approval, isConfirmPopup, isBatchApproval]); + useEffect(() => { + const actionsUpdates = events() + .pipe(filter(isActionsUpdate)) + .subscribe(async (event) => { + setAction((prev) => { + const actionFromEvent = event.value[actionId]; + + return actionFromEvent ?? prev; + }); + }); + + return () => { + actionsUpdates.unsubscribe(); + }; + }); + useWindowGetsClosedOrHidden(cancelHandler); return { action, updateAction, error, cancelHandler }; diff --git a/yarn.lock b/yarn.lock index d406c044d..b330495f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49,9 +49,9 @@ __metadata: languageName: node linkType: hard -"@avalabs/avalanche-module@npm:1.9.10": - version: 1.9.10 - resolution: "@avalabs/avalanche-module@npm:1.9.10" +"@avalabs/avalanche-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304": + version: 0.0.0-fix-nft-set-approval-for-all-20250829175304 + resolution: "@avalabs/avalanche-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" dependencies: "@avalabs/avalanchejs": "npm:5.1.0-alpha.2" "@avalabs/core-chains-sdk": "npm:3.1.0-alpha.58" @@ -61,12 +61,12 @@ __metadata: "@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58" "@avalabs/glacier-sdk": "npm:3.1.0-alpha.58" "@avalabs/types": "npm:3.1.0-alpha.58" - "@avalabs/vm-module-types": "npm:1.9.10" + "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@metamask/rpc-errors": "npm:6.3.0" big.js: "npm:6.2.1" bn.js: "npm:5.2.1" zod: "npm:3.23.8" - checksum: 10c0/b63fe029070a1070422cb07ab5f3480614d98b43aaa07617cf8d3d9821a56b735a43c1f9d2ec33b21d0fdb42cf66772c127015788bf2a09ad03aa1897e22abd7 + checksum: 10c0/dbb35d0c3633f96aff180ab4ac656efb144c3a8691e78f204e235ef4c8ef31484b87b10885ce5b9ef73a2f48f5cc3ecec55e3731626d7fbf48363c0c4794f620 languageName: node linkType: hard @@ -96,20 +96,20 @@ __metadata: languageName: node linkType: hard -"@avalabs/bitcoin-module@npm:1.9.10": - version: 1.9.10 - resolution: "@avalabs/bitcoin-module@npm:1.9.10" +"@avalabs/bitcoin-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304": + version: 0.0.0-fix-nft-set-approval-for-all-20250829175304 + resolution: "@avalabs/bitcoin-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" dependencies: "@avalabs/core-coingecko-sdk": "npm:3.1.0-alpha.58" "@avalabs/core-utils-sdk": "npm:3.1.0-alpha.58" "@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58" - "@avalabs/vm-module-types": "npm:1.9.10" + "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@metamask/rpc-errors": "npm:6.3.0" big.js: "npm:6.2.1" bitcoinjs-lib: "npm:5.2.0" bn.js: "npm:5.2.1" zod: "npm:3.23.8" - checksum: 10c0/2d1574869435495c951cbdf8f908373b1e7ffd79727de97e8994b3fe84feace0a896bb49db507db96f5d6276f989fb29a70140ec89e5da52a3d702dc6bdb1880 + checksum: 10c0/52eaa0db84693b97d0ea470c256fa12ffc7b395ee309668cc26c9f1d37460374f3f89d854857422c63fcd5747d40fa24d423a82ea969e8f99f33723d4709229b languageName: node linkType: hard @@ -367,9 +367,9 @@ __metadata: languageName: node linkType: hard -"@avalabs/evm-module@npm:1.9.10": - version: 1.9.10 - resolution: "@avalabs/evm-module@npm:1.9.10" +"@avalabs/evm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304": + version: 0.0.0-fix-nft-set-approval-for-all-20250829175304 + resolution: "@avalabs/evm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" dependencies: "@avalabs/core-chains-sdk": "npm:3.1.0-alpha.58" "@avalabs/core-coingecko-sdk": "npm:3.1.0-alpha.58" @@ -378,7 +378,7 @@ __metadata: "@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58" "@avalabs/glacier-sdk": "npm:3.1.0-alpha.58" "@avalabs/types": "npm:3.1.0-alpha.58" - "@avalabs/vm-module-types": "npm:1.9.10" + "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@blockaid/client": "npm:0.36.0" "@metamask/rpc-errors": "npm:6.3.0" "@openzeppelin/contracts": "npm:4.9.6" @@ -388,7 +388,7 @@ __metadata: zod: "npm:3.23.8" peerDependencies: ethers: 6.13.5 - checksum: 10c0/630f485da7791aeb56227aec5db5928dae5a4dc28d0181663f8885dc21624999de2f8ade9d077f1166e5a79c9469a64b0533b3d69383a8015a8053bb353f496c + checksum: 10c0/e21b993efc1bff7b5817a78577a781905f8faf10557b885347bb84eee84affa0b1d58299e69da77f726963d3ee9a071fc051233df63c60c4b1398ac962d9b303 languageName: node linkType: hard @@ -406,18 +406,18 @@ __metadata: languageName: node linkType: hard -"@avalabs/hvm-module@npm:1.9.10": - version: 1.9.10 - resolution: "@avalabs/hvm-module@npm:1.9.10" +"@avalabs/hvm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304": + version: 0.0.0-fix-nft-set-approval-for-all-20250829175304 + resolution: "@avalabs/hvm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" dependencies: "@avalabs/core-utils-sdk": "npm:3.1.0-alpha.58" - "@avalabs/vm-module-types": "npm:1.9.10" + "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@metamask/rpc-errors": "npm:6.3.0" "@noble/hashes": "npm:1.5.0" "@scure/base": "npm:1.2.4" hypersdk-client: "npm:0.4.16" zod: "npm:3.23.8" - checksum: 10c0/49f2114d90f516fcf51c4e309fbb93f5f80d818a33d2888765d5e2855a9d8d92a929c074aa59d8f07159bd580d2ad5a5746ead0666f6d668213fc78ea8f9859d + checksum: 10c0/89716ab6c99083ce96b0cbd0fb0df1b24ca0069d609f6f1969eec6e8133b26e2dc39478ae3d687c129d8acc914f2d4eca9c09ef06b58d6c6b8ad40005c6247f1 languageName: node linkType: hard @@ -456,15 +456,15 @@ __metadata: languageName: node linkType: hard -"@avalabs/svm-module@npm:1.9.10": - version: 1.9.10 - resolution: "@avalabs/svm-module@npm:1.9.10" +"@avalabs/svm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304": + version: 0.0.0-fix-nft-set-approval-for-all-20250829175304 + resolution: "@avalabs/svm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" dependencies: "@avalabs/core-chains-sdk": "npm:3.1.0-alpha.58" "@avalabs/core-coingecko-sdk": "npm:3.1.0-alpha.58" "@avalabs/core-utils-sdk": "npm:3.1.0-alpha.58" "@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58" - "@avalabs/vm-module-types": "npm:1.9.10" + "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@blockaid/client": "npm:0.48.0" "@metamask/rpc-errors": "npm:6.3.0" "@scure/base": "npm:1.2.4" @@ -475,7 +475,7 @@ __metadata: "@wallet-standard/base": "npm:1.1.0" "@wallet-standard/features": "npm:1.1.0" zod: "npm:3.23.8" - checksum: 10c0/b52fd88b179f24467f9aaaefc41f70e2c27fc4c2eda0777a7e48bd0b9d10a5d096fac6119cda48580ea7496454417785f7ffb94e42646ada4ffaee1b644f2ca8 + checksum: 10c0/f56b2cc05cf43befede0696fc7dd9816664139d492b45e63ede0f6b67b053d241b7c0c3f88521930e5545e30f794454581c35a2c193b2867e1acc50150324eb0 languageName: node linkType: hard @@ -493,9 +493,9 @@ __metadata: languageName: node linkType: hard -"@avalabs/vm-module-types@npm:1.9.10": - version: 1.9.10 - resolution: "@avalabs/vm-module-types@npm:1.9.10" +"@avalabs/vm-module-types@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304": + version: 0.0.0-fix-nft-set-approval-for-all-20250829175304 + resolution: "@avalabs/vm-module-types@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" dependencies: "@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58" "@avalabs/glacier-sdk": "npm:3.1.0-alpha.58" @@ -505,7 +505,7 @@ __metadata: zod: "npm:3.23.8" peerDependencies: ethers: 6.13.5 - checksum: 10c0/d09fe3ee7cc61a1c43b456d31c4490f6fbe5b6b94c7a54a9d339ae99afdbf4c1cb3170716fafc9a619809ce8dd0a77028098c8c27e568afe2cc291b88d6a068c + checksum: 10c0/de1b338fd4a1bb55d43a8829a439a55c9430d05c7a8d906799a5fc95624593977cc40f03718156cd99af29a44bb2d4dab8138e97ad00563047125bd32dcf4ee3 languageName: node linkType: hard @@ -3781,9 +3781,9 @@ __metadata: version: 0.0.0-use.local resolution: "@core-ext/legacy@workspace:apps/legacy" dependencies: - "@avalabs/avalanche-module": "npm:1.9.10" + "@avalabs/avalanche-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/avalanchejs": "npm:5.1.0-alpha.2" - "@avalabs/bitcoin-module": "npm:1.9.10" + "@avalabs/bitcoin-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/bridge-unified": "npm:4.0.3" "@avalabs/core-bridge-sdk": "npm:3.1.0-alpha.60" "@avalabs/core-chains-sdk": "npm:3.1.0-alpha.60" @@ -3796,13 +3796,13 @@ __metadata: "@avalabs/core-token-prices-sdk": "npm:3.1.0-alpha.60" "@avalabs/core-utils-sdk": "npm:3.1.0-alpha.60" "@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.60" - "@avalabs/evm-module": "npm:1.9.10" + "@avalabs/evm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/glacier-sdk": "npm:3.1.0-alpha.60" - "@avalabs/hvm-module": "npm:1.9.10" + "@avalabs/hvm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/hw-app-avalanche": "npm:0.14.1" - "@avalabs/svm-module": "npm:1.9.10" + "@avalabs/svm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/types": "npm:3.1.0-alpha.60" - "@avalabs/vm-module-types": "npm:1.9.10" + "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@babel/plugin-proposal-decorators": "npm:7.24.1" "@babel/preset-env": "npm:7.24.4" "@babel/preset-react": "npm:7.24.1" @@ -3977,7 +3977,7 @@ __metadata: "@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.60" "@avalabs/k2-alpine": "npm:1.228.0" "@avalabs/types": "npm:3.1.0-alpha.60" - "@avalabs/vm-module-types": "npm:1.9.10" + "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@babel/plugin-proposal-decorators": "npm:7.24.1" "@babel/preset-env": "npm:7.24.4" "@babel/preset-react": "npm:7.24.1" @@ -4136,8 +4136,8 @@ __metadata: version: 0.0.0-use.local resolution: "@core/inpage@workspace:packages/inpage" dependencies: - "@avalabs/evm-module": "npm:1.9.10" - "@avalabs/svm-module": "npm:1.9.10" + "@avalabs/evm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" + "@avalabs/svm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@core/common": "workspace:*" "@core/messaging": "workspace:*" "@core/types": "workspace:*" @@ -4220,9 +4220,9 @@ __metadata: version: 0.0.0-use.local resolution: "@core/service-worker@workspace:packages/service-worker" dependencies: - "@avalabs/avalanche-module": "npm:1.9.10" + "@avalabs/avalanche-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/avalanchejs": "npm:5.1.0-alpha.2" - "@avalabs/bitcoin-module": "npm:1.9.10" + "@avalabs/bitcoin-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/bridge-unified": "npm:4.0.3" "@avalabs/core-bridge-sdk": "npm:3.1.0-alpha.60" "@avalabs/core-chains-sdk": "npm:3.1.0-alpha.60" @@ -4234,13 +4234,13 @@ __metadata: "@avalabs/core-token-prices-sdk": "npm:3.1.0-alpha.60" "@avalabs/core-utils-sdk": "npm:3.1.0-alpha.60" "@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.60" - "@avalabs/evm-module": "npm:1.9.10" + "@avalabs/evm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/glacier-sdk": "npm:3.1.0-alpha.60" - "@avalabs/hvm-module": "npm:1.9.10" + "@avalabs/hvm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/hw-app-avalanche": "npm:0.14.1" - "@avalabs/svm-module": "npm:1.9.10" + "@avalabs/svm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@avalabs/types": "npm:3.1.0-alpha.60" - "@avalabs/vm-module-types": "npm:1.9.10" + "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@babel/plugin-proposal-decorators": "npm:7.24.1" "@babel/preset-env": "npm:7.24.4" "@babel/preset-typescript": "npm:7.24.1" @@ -4402,7 +4402,7 @@ __metadata: "@avalabs/glacier-sdk": "npm:3.1.0-alpha.60" "@avalabs/hw-app-avalanche": "npm:0.14.1" "@avalabs/types": "npm:3.1.0-alpha.60" - "@avalabs/vm-module-types": "npm:1.9.10" + "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304" "@blockaid/client": "npm:0.10.0" "@cubist-labs/cubesigner-sdk": "npm:0.3.28" "@eslint/compat": "npm:1.2.4" From 965fb9547a0b54b42b3c7cd2df1ab71f020d268f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 5 Sep 2025 09:38:44 +0200 Subject: [PATCH 4/4] fix: custom gas settings modal --- .../evm/EvmNetworkFeeWidget/components/CustomGasSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/CustomGasSettings.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/CustomGasSettings.tsx index b01b34d28..207eb33a6 100644 --- a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/CustomGasSettings.tsx +++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/CustomGasSettings.tsx @@ -8,10 +8,10 @@ import { FeeRate, NativeTokenBalance } from '@core/types'; import { useKeyboardShortcuts, useSettingsContext } from '@core/ui'; import { Page } from '@/components/Page'; -import { InvisibileInput } from '@/pages/Contacts/components'; import { DetailsSection } from '../../../generic/DetailsSection'; import { TxDetailsRow } from '../../../generic/DetailsItem/items/DetailRow'; +import { InvisibileInput } from '@/components/Forms/InvisibleInput'; type CustomGasSettingsProps = { onBack: () => void;