diff --git a/audit-ci.jsonc b/audit-ci.jsonc index 7003d1ccb1..52bdc29750 100644 --- a/audit-ci.jsonc +++ b/audit-ci.jsonc @@ -1,14 +1,5 @@ { "$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json", "low": true, - "allowlist": [ - // elliptic - // waiting for it to release a fix but low severity so we can ignore it - // https://github.com/advisories/GHSA-49q7-c7j4-3p7m - "GHSA-49q7-c7j4-3p7m", - // https://github.com/advisories/GHSA-977x-g7h5-7qgw - "GHSA-977x-g7h5-7qgw", - // https://github.com/advisories/GHSA-f7q4-pwc6-w24p - "GHSA-f7q4-pwc6-w24p" - ] + "allowlist": [] } diff --git a/package.json b/package.json index b826dc8d2c..74e39a32d6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "resolutions": { "**/@walletconnect/ethereum-provider": "2.13.1", "**/@ethersproject/providers/ws": "7.5.10", - "**/@synthetixio/synpress/ws": "8.17.1" + "**/@synthetixio/synpress/ws": "8.17.1", + "**/elliptic": "6.5.7" }, "keywords": [], "author": "", diff --git a/packages/arb-token-bridge-ui/package.json b/packages/arb-token-bridge-ui/package.json index 28a4f5ea79..c4c0e8636d 100644 --- a/packages/arb-token-bridge-ui/package.json +++ b/packages/arb-token-bridge-ui/package.json @@ -22,7 +22,7 @@ "@vercel/edge-config": "^0.4.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "axios": "^1.6.6", + "axios": "^1.7.4", "boring-avatars": "^1.7.0", "cheerio": "^1.0.0-rc.12", "dayjs": "^1.11.8", @@ -34,7 +34,7 @@ "next-query-params": "^5.0.0", "overmind": "^28.0.1", "overmind-react": "^29.0.1", - "posthog-js": "^1.57.2", + "posthog-js": "^1.155.4", "query-string": "^8.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx index 1d3d4065e1..c090e311a3 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx @@ -4,7 +4,6 @@ import { ChevronDownIcon } from '@heroicons/react/24/outline' import { twMerge } from 'tailwind-merge' import { useAppState } from '../../state' -import { sanitizeImageSrc } from '../../util' import { TokenSearch } from '../TransferPanel/TokenSearch' import { sanitizeTokenSymbol } from '../../util/TokenUtils' import { useNativeCurrency } from '../../hooks/useNativeCurrency' @@ -17,43 +16,31 @@ import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { Transition } from '../common/Transition' -export function TokenButton(): JSX.Element { +export type TokenButtonOptions = { + symbol?: string + disabled?: boolean +} + +export function TokenButton({ + options +}: { + options?: TokenButtonOptions +}): JSX.Element { const { - app: { - selectedToken, - arbTokenBridge: { bridgeTokens }, - arbTokenBridgeLoaded - } + app: { selectedToken } } = useAppState() + const disabled = options?.disabled ?? false + const [networks] = useNetworks() const { childChainProvider } = useNetworksRelationship(networks) const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) - const tokenLogo = useMemo(() => { - const selectedAddress = selectedToken?.address - if (!selectedAddress) { - return nativeCurrency.logoUrl - } - if (!arbTokenBridgeLoaded) { - return undefined - } - if (typeof bridgeTokens === 'undefined') { - return undefined - } - const logo = bridgeTokens[selectedAddress]?.logoURI - if (logo) { - return sanitizeImageSrc(logo) + const tokenSymbol = useMemo(() => { + if (typeof options?.symbol !== 'undefined') { + return options.symbol } - return undefined - }, [ - nativeCurrency, - bridgeTokens, - selectedToken?.address, - arbTokenBridgeLoaded - ]) - const tokenSymbol = useMemo(() => { if (!selectedToken) { return nativeCurrency.symbol } @@ -62,7 +49,7 @@ export function TokenButton(): JSX.Element { erc20L1Address: selectedToken.address, chainId: networks.sourceChain.id }) - }, [selectedToken, networks.sourceChain.id, nativeCurrency.symbol]) + }, [selectedToken, networks.sourceChain.id, nativeCurrency.symbol, options]) return ( <> @@ -73,6 +60,7 @@ export function TokenButton(): JSX.Element { className="arb-hover h-full w-max rounded-bl rounded-tl px-3 py-3 text-white" aria-label="Select Token" onClick={onPopoverButtonClick} + disabled={disabled} >
{/* Commenting it out until we update the token image source files to be of better quality */} @@ -90,12 +78,14 @@ export function TokenButton(): JSX.Element { {tokenSymbol} - + {!disabled && ( + + )}
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx index 640649a7ae..2aa2b2be15 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -564,10 +564,6 @@ export function TokenSearch({ return } - if (typeof bridgeTokens === 'undefined') { - return - } - try { // Native USDC on L2 won't have a corresponding L1 address const isNativeUSDC = @@ -609,6 +605,10 @@ export function TokenSearch({ return } + if (typeof bridgeTokens === 'undefined') { + return + } + // Token not added to the bridge, so we'll handle importing it if (typeof bridgeTokens[lowercasedTokenAddress] === 'undefined') { setTokenQueryParam(lowercasedTokenAddress) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx index 698d4d0222..ed274b56d9 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -1,7 +1,7 @@ import dayjs from 'dayjs' import { useState, useMemo } from 'react' import Tippy from '@tippyjs/react' -import { constants, utils } from 'ethers' +import { BigNumber, constants, utils } from 'ethers' import { useLatest } from 'react-use' import { useAccount, useChainId, useSigner } from 'wagmi' import { TransactionResponse } from '@ethersproject/providers' @@ -49,7 +49,10 @@ import { isUserRejectedError } from '../../util/isUserRejectedError' import { getUsdcTokenAddressFromSourceChainId } from '../../state/cctpState' import { DepositStatus, MergedTransaction } from '../../state/app/state' import { useNativeCurrency } from '../../hooks/useNativeCurrency' -import { AssetType } from '../../hooks/arbTokenBridge.types' +import { + AssetType, + DepositGasEstimates +} from '../../hooks/arbTokenBridge.types' import { ImportTokenModalStatus, getWarningTokenDescription, @@ -57,14 +60,16 @@ import { } from './TransferPanelUtils' import { useImportTokenModal } from '../../hooks/TransferPanel/useImportTokenModal' import { useTransferReadiness } from './useTransferReadiness' -import { useGasSummary } from '../../hooks/TransferPanel/useGasSummary' import { useTransactionHistory } from '../../hooks/useTransactionHistory' import { getBridgeUiConfigForChain } from '../../util/bridgeUiConfig' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { CctpTransferStarter } from '@/token-bridge-sdk/CctpTransferStarter' import { BridgeTransferStarterFactory } from '@/token-bridge-sdk/BridgeTransferStarterFactory' -import { BridgeTransfer } from '@/token-bridge-sdk/BridgeTransferStarter' +import { + BridgeTransfer, + TransferOverrides +} from '@/token-bridge-sdk/BridgeTransferStarter' import { addDepositToCache } from '../TransactionHistory/helpers' import { convertBridgeSdkToMergedTransaction, @@ -75,6 +80,7 @@ import { useSetInputAmount } from '../../hooks/TransferPanel/useSetInputAmount' import { getSmartContractWalletTeleportTransfersNotSupportedErrorMessage } from './useTransferReadinessUtils' import { useBalances } from '../../hooks/useBalances' import { captureSentryErrorWithExtraData } from '../../util/SentryUtils' +import { useIsBatchTransferSupported } from '../../hooks/TransferPanel/useIsBatchTransferSupported' const networkConnectionWarningToast = () => warningToast( @@ -125,6 +131,7 @@ export function TransferPanel() { isTeleportMode } = useNetworksRelationship(networks) const latestNetworks = useLatest(networks) + const isBatchTransferSupported = useIsBatchTransferSupported() const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) @@ -150,9 +157,9 @@ export function TransferPanel() { // Link the amount state directly to the amount in query params - no need of useState // Both `amount` getter and setter will internally be using `useArbQueryParams` functions - const [{ amount }] = useArbQueryParams() + const [{ amount, amount2 }] = useArbQueryParams() - const setAmount = useSetInputAmount() + const { setAmount, setAmount2 } = useSetInputAmount() const [tokenImportDialogProps] = useDialog() const [tokenCheckDialogProps, openTokenCheckDialog] = useDialog() @@ -180,12 +187,7 @@ export function TransferPanel() { const [isCctp, setIsCctp] = useState(false) - const gasSummary = useGasSummary() - - const { transferReady, errorMessage } = useTransferReadiness({ - amount, - gasSummary - }) + const { transferReady } = useTransferReadiness() const { color: destinationChainUIcolor } = getBridgeUiConfigForChain( networks.destinationChain.id @@ -200,6 +202,7 @@ export function TransferPanel() { function clearAmountInput() { // clear amount input on transfer panel setAmount('') + setAmount2('') } useImportTokenModal({ @@ -861,11 +864,34 @@ export function TransferPanel() { ) } + const overrides: TransferOverrides = {} + + const isBatchTransfer = isBatchTransferSupported && Number(amount2) > 0 + + if (isBatchTransfer) { + // when sending additional ETH with ERC-20, we add the additional ETH value as maxSubmissionCost + const gasEstimates = (await bridgeTransferStarter.transferEstimateGas({ + amount: amountBigNumber, + signer + })) as DepositGasEstimates + + if (!gasEstimates.estimatedChildChainSubmissionCost) { + errorToast('Failed to estimate deposit maxSubmissionCost') + throw 'Failed to estimate deposit maxSubmissionCost' + } + + overrides.maxSubmissionCost = utils + .parseEther(amount2) + .add(gasEstimates.estimatedChildChainSubmissionCost) + overrides.excessFeeRefundAddress = destinationAddress + } + // finally, call the transfer function const transfer = await bridgeTransferStarter.transfer({ amount: amountBigNumber, signer, - destinationAddress + destinationAddress, + overrides: Object.keys(overrides).length > 0 ? overrides : undefined }) // transaction submitted callback @@ -1028,7 +1054,7 @@ export function TransferPanel() { 'sm:rounded sm:border' )} > - + diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx index b892b0e97a..237c001d13 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect } from 'react' +import { isTeleport } from '@/token-bridge-sdk/teleport' import { getNetworkName } from '../../../util/networks' import { NetworkButton, @@ -29,20 +30,20 @@ import { import { ExternalLink } from '../../common/ExternalLink' import { EstimatedGas } from '../EstimatedGas' import { TransferPanelMainInput } from '../TransferPanelMainInput' -import { AmountQueryParamEnum } from '../../../hooks/useArbQueryParams' -import { TransferReadinessRichErrorMessage } from '../useTransferReadinessUtils' +import { + AmountQueryParamEnum, + useArbQueryParams +} from '../../../hooks/useArbQueryParams' import { useMaxAmount } from './useMaxAmount' import { useSetInputAmount } from '../../../hooks/TransferPanel/useSetInputAmount' import { useDialog } from '../../common/Dialog' +import { useTransferReadiness } from '../useTransferReadiness' +import { useIsBatchTransferSupported } from '../../../hooks/TransferPanel/useIsBatchTransferSupported' export function SourceNetworkBox({ - amount, - errorMessage, customFeeTokenBalances, showUsdcSpecificInfo }: { - amount: string - errorMessage: string | TransferReadinessRichErrorMessage | undefined customFeeTokenBalances: Balances showUsdcSpecificInfo: boolean }) { @@ -55,30 +56,45 @@ export function SourceNetworkBox({ const { ethParentBalance, ethChildBalance } = useBalances() const selectedTokenBalances = useSelectedTokenBalances() const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) - const setAmount = useSetInputAmount() - const { maxAmount } = useMaxAmount({ + const [{ amount, amount2 }] = useArbQueryParams() + const { setAmount, setAmount2 } = useSetInputAmount() + const { maxAmount, maxAmount2 } = useMaxAmount({ customFeeTokenBalances }) const [sourceNetworkSelectionDialogProps, openSourceNetworkSelectionDialog] = useDialog() + const isBatchTransferSupported = useIsBatchTransferSupported() + + const { errorMessages } = useTransferReadiness() const isMaxAmount = amount === AmountQueryParamEnum.MAX + const isMaxAmount2 = amount2 === AmountQueryParamEnum.MAX - // whenever the user changes the `amount` input, it should update the amount in browser query params as well + // covers MAX string from query params useEffect(() => { if (isMaxAmount && typeof maxAmount !== 'undefined') { setAmount(maxAmount) - } else { - setAmount(amount) } }, [amount, maxAmount, isMaxAmount, setAmount]) + useEffect(() => { + if (isMaxAmount2 && typeof maxAmount2 !== 'undefined') { + setAmount2(maxAmount2) + } + }, [amount2, maxAmount2, isMaxAmount2, setAmount2]) + const maxButtonOnClick = useCallback(() => { if (typeof maxAmount !== 'undefined') { setAmount(maxAmount) } }, [maxAmount, setAmount]) + const amount2MaxButtonOnClick = useCallback(() => { + if (typeof maxAmount2 !== 'undefined') { + setAmount2(maxAmount2) + } + }, [maxAmount2, setAmount2]) + return ( <> @@ -141,10 +157,24 @@ export function SourceNetworkBox({
setAmount(e.target.value)} /> + {isBatchTransferSupported && ( + setAmount2(e.target.value)} + tokenButtonOptions={{ + symbol: nativeCurrency.symbol, + disabled: true + }} + /> + )} + {showUsdcSpecificInfo && (

Bridged USDC (USDC.e) will work but is different from Native USDC.{' '} diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts index d023eba854..b9c6d9a14d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts @@ -31,23 +31,7 @@ export function useMaxAmount({ const { estimatedParentChainGasFees, estimatedChildChainGasFees } = useGasSummary() - const maxAmount = useMemo(() => { - if (selectedToken) { - const tokenBalance = isDepositMode - ? selectedTokenBalances.parentBalance - : selectedTokenBalances.childBalance - - if (!tokenBalance) { - return - } - - // For token deposits and withdrawals, we can set the max amount, as gas fees are paid in ETH / custom fee token - return utils.formatUnits( - tokenBalance, - selectedToken?.decimals ?? defaultErc20Decimals - ) - } - + const nativeCurrencyMaxAmount = useMemo(() => { const customFeeTokenParentBalance = customFeeTokenBalances.parentBalance // For custom fee token deposits, we can set the max amount, as the fees will be paid in ETH if ( @@ -61,14 +45,13 @@ export function useMaxAmount({ ) } - // We have already handled token deposits and deposits of the custom fee token - // The remaining cases are ETH deposits, and ETH/custom fee token withdrawals (which can be handled in the same case) + // ETH deposits and ETH/custom fee token withdrawals const nativeCurrencyBalance = isDepositMode ? ethParentBalance : ethChildBalance if (!nativeCurrencyBalance) { - return + return undefined } const nativeCurrencyBalanceFormatted = utils.formatUnits( @@ -76,8 +59,15 @@ export function useMaxAmount({ nativeCurrency.decimals ) + if ( + typeof estimatedParentChainGasFees === 'undefined' || + typeof estimatedChildChainGasFees === 'undefined' + ) { + return undefined + } + const estimatedTotalGasFees = - (estimatedParentChainGasFees ?? 0) + (estimatedChildChainGasFees ?? 0) + estimatedParentChainGasFees + estimatedChildChainGasFees const maxAmount = parseFloat(nativeCurrencyBalanceFormatted) - estimatedTotalGasFees * 1.4 @@ -90,18 +80,55 @@ export function useMaxAmount({ return nativeCurrencyBalanceFormatted }, [ - nativeCurrency, - ethParentBalance, + customFeeTokenBalances.parentBalance, + estimatedChildChainGasFees, + estimatedParentChainGasFees, ethChildBalance, + ethParentBalance, isDepositMode, + nativeCurrency.decimals, + nativeCurrency.isCustom + ]) + + const maxAmount = useMemo(() => { + if (selectedToken) { + const tokenBalance = isDepositMode + ? selectedTokenBalances.parentBalance + : selectedTokenBalances.childBalance + + if (!tokenBalance) { + return undefined + } + + // For token deposits and withdrawals, we can set the max amount, as gas fees are paid in ETH / custom fee token + return utils.formatUnits( + tokenBalance, + selectedToken?.decimals ?? defaultErc20Decimals + ) + } + + return nativeCurrencyMaxAmount + }, [ selectedToken, - selectedTokenBalances, - estimatedParentChainGasFees, - estimatedChildChainGasFees, - customFeeTokenBalances + isDepositMode, + nativeCurrencyMaxAmount, + selectedTokenBalances.parentBalance, + selectedTokenBalances.childBalance ]) + const maxAmount2 = useMemo(() => { + if (!isDepositMode) { + return undefined + } + if (nativeCurrency.isCustom) { + return undefined + } + + return nativeCurrencyMaxAmount + }, [isDepositMode, nativeCurrency.isCustom, nativeCurrencyMaxAmount]) + return { - maxAmount + maxAmount, + maxAmount2 } } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx index 5eb2afdae3..e05d7e4919 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx @@ -1,14 +1,11 @@ import { twMerge } from 'tailwind-merge' -import { useEffect, useMemo } from 'react' +import { useMemo } from 'react' -import { TokenButton } from './TokenButton' +import { TokenButton, TokenButtonOptions } from './TokenButton' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { useSelectedTokenBalances } from '../../hooks/TransferPanel/useSelectedTokenBalances' import { useAppState } from '../../state' -import { useSetInputAmount } from '../../hooks/TransferPanel/useSetInputAmount' -import { countDecimals } from '../../util/NumberUtils' -import { useSelectedTokenDecimals } from '../../hooks/TransferPanel/useSelectedTokenDecimals' import { useBalances } from '../../hooks/useBalances' import { TransferReadinessRichErrorMessage } from './useTransferReadinessUtils' import { ExternalLink } from '../common/ExternalLink' @@ -74,17 +71,7 @@ function MaxButton(props: React.ButtonHTMLAttributes) { function TransferPanelInputField( props: React.InputHTMLAttributes ) { - const { value = '', onChange, ...rest } = props - const setAmount = useSetInputAmount() - const decimals = useSelectedTokenDecimals() - - useEffect(() => { - // if number of decimals of query param value is greater than token decimals, - // truncate the decimals and update the amount query param value - if (countDecimals(String(value)) > decimals) { - setAmount(String(value)) - } - }, [value, decimals, setAmount]) + const { value = '', ...rest } = props return ( { - onChange?.(event) - setAmount(event.target.value) - }} {...rest} /> ) @@ -156,10 +139,11 @@ export type TransferPanelMainInputProps = errorMessage?: string | TransferReadinessRichErrorMessage | undefined maxButtonOnClick: React.ButtonHTMLAttributes['onClick'] value: string + tokenButtonOptions?: TokenButtonOptions } export function TransferPanelMainInput(props: TransferPanelMainInputProps) { - const { errorMessage, maxButtonOnClick, ...rest } = props + const { errorMessage, maxButtonOnClick, tokenButtonOptions, ...rest } = props return ( <> @@ -171,7 +155,7 @@ export function TransferPanelMainInput(props: TransferPanelMainInputProps) { : 'border-white/30 text-white' )} > - +

{ - if (isNaN(Number(amount)) || Number(amount) === 0) { + const { estimatedL1GasFees, estimatedL2GasFees } = sanitizeEstimatedGasFees( + gasSummary, + { + isSmartContractWallet, + isDepositMode + } + ) + + const ethBalanceFloat = isDepositMode + ? ethL1BalanceFloat + : ethL2BalanceFloat + const selectedTokenBalanceFloat = isDepositMode + ? selectedTokenL1BalanceFloat + : selectedTokenL2BalanceFloat + const customFeeTokenBalanceFloat = isDepositMode + ? customFeeTokenL1BalanceFloat + : ethL2BalanceFloat + + // No error while loading balance + if (ethBalanceFloat === null) { return notReady() } + const sendsAdditionalEth = Number(amount2) > 0 + const notEnoughEthForAdditionalEthTransfer = + Number(amount2) > + ethBalanceFloat - (estimatedL1GasFees + estimatedL2GasFees) + + if (isNaN(Number(amount)) || Number(amount) === 0) { + return notReady({ + errorMessages: { + inputAmount2: + sendsAdditionalEth && notEnoughEthForAdditionalEthTransfer + ? getInsufficientFundsErrorMessage({ + asset: ether.symbol, + chain: networks.sourceChain.name + }) + : undefined + } + }) + } + if (isTransferring) { return notReady() } @@ -222,18 +265,22 @@ export function useTransferReadiness({ // native currency (ETH or custom fee token) transfers using SC wallets not enabled yet if (isSmartContractWallet && !selectedToken) { return notReady({ - errorMessage: - getSmartContractWalletNativeCurrencyTransfersNotSupportedErrorMessage( - { asset: nativeCurrency.symbol } - ) + errorMessages: { + inputAmount1: + getSmartContractWalletNativeCurrencyTransfersNotSupportedErrorMessage( + { asset: nativeCurrency.symbol } + ) + } }) } // teleport transfers using SC wallets not enabled yet if (isSmartContractWallet && isTeleportMode) { return notReady({ - errorMessage: - getSmartContractWalletTeleportTransfersNotSupportedErrorMessage() + errorMessages: { + inputAmount1: + getSmartContractWalletTeleportTransfersNotSupportedErrorMessage() + } }) } @@ -242,21 +289,6 @@ export function useTransferReadiness({ return notReady() } - const ethBalanceFloat = isDepositMode - ? ethL1BalanceFloat - : ethL2BalanceFloat - const selectedTokenBalanceFloat = isDepositMode - ? selectedTokenL1BalanceFloat - : selectedTokenL2BalanceFloat - const customFeeTokenBalanceFloat = isDepositMode - ? customFeeTokenL1BalanceFloat - : ethL2BalanceFloat - - // No error while loading balance - if (ethBalanceFloat === null) { - return notReady() - } - // ERC-20 if (selectedToken) { const selectedTokenIsWithdrawOnly = isWithdrawOnlyToken( @@ -275,12 +307,16 @@ export function useTransferReadiness({ if (isDepositMode && selectedTokenIsWithdrawOnly) { return notReady({ - errorMessage: TransferReadinessRichErrorMessage.TOKEN_WITHDRAW_ONLY + errorMessages: { + inputAmount1: TransferReadinessRichErrorMessage.TOKEN_WITHDRAW_ONLY + } }) } else if (selectedTokenIsDisabled) { return notReady({ - errorMessage: - TransferReadinessRichErrorMessage.TOKEN_TRANSFER_DISABLED + errorMessages: { + inputAmount1: + TransferReadinessRichErrorMessage.TOKEN_TRANSFER_DISABLED + } }) } else if (withdrawalDisabled(selectedToken.address)) { return notReady() @@ -294,10 +330,19 @@ export function useTransferReadiness({ // Check amount against ERC-20 balance if (Number(amount) > selectedTokenBalanceFloat) { return notReady({ - errorMessage: getInsufficientFundsErrorMessage({ - asset: selectedToken.symbol, - chain: networks.sourceChain.name - }) + errorMessages: { + inputAmount1: getInsufficientFundsErrorMessage({ + asset: selectedToken.symbol, + chain: networks.sourceChain.name + }), + inputAmount2: + sendsAdditionalEth && notEnoughEthForAdditionalEthTransfer + ? getInsufficientFundsErrorMessage({ + asset: ether.symbol, + chain: networks.sourceChain.name + }) + : undefined + } }) } } @@ -311,10 +356,12 @@ export function useTransferReadiness({ // Check amount against custom fee token balance if (Number(amount) > customFeeTokenBalanceFloat) { return notReady({ - errorMessage: getInsufficientFundsErrorMessage({ - asset: nativeCurrency.symbol, - chain: networks.sourceChain.name - }) + errorMessages: { + inputAmount1: getInsufficientFundsErrorMessage({ + asset: nativeCurrency.symbol, + chain: networks.sourceChain.name + }) + } }) } } @@ -322,10 +369,12 @@ export function useTransferReadiness({ // Check amount against ETH balance else if (Number(amount) > ethBalanceFloat) { return notReady({ - errorMessage: getInsufficientFundsErrorMessage({ - asset: ether.symbol, - chain: networks.sourceChain.name - }) + errorMessages: { + inputAmount1: getInsufficientFundsErrorMessage({ + asset: ether.symbol, + chain: networks.sourceChain.name + }) + } }) } @@ -340,19 +389,16 @@ export function useTransferReadiness({ case 'error': return notReady({ - errorMessage: TransferReadinessRichErrorMessage.GAS_ESTIMATION_FAILURE + errorMessages: { + inputAmount1: + TransferReadinessRichErrorMessage.GAS_ESTIMATION_FAILURE + } }) case 'insufficientBalance': return notReady() case 'success': { - const { estimatedL1GasFees, estimatedL2GasFees } = - sanitizeEstimatedGasFees(gasSummary, { - isSmartContractWallet, - isDepositMode - }) - if (selectedToken) { // If depositing into a custom fee token network, gas is split between ETH and the custom fee token if (nativeCurrency.isCustom && isDepositMode) { @@ -364,20 +410,24 @@ export function useTransferReadiness({ // We have to check if there's enough ETH to cover L1 gas if (estimatedL1GasFees > ethBalanceFloat) { return notReady({ - errorMessage: getInsufficientFundsForGasFeesErrorMessage({ - asset: ether.symbol, - chain: networks.sourceChain.name - }) + errorMessages: { + inputAmount1: getInsufficientFundsForGasFeesErrorMessage({ + asset: ether.symbol, + chain: networks.sourceChain.name + }) + } }) } // We have to check if there's enough of the custom fee token to cover L2 gas if (estimatedL2GasFees > customFeeTokenL1BalanceFloat) { return notReady({ - errorMessage: getInsufficientFundsForGasFeesErrorMessage({ - asset: nativeCurrency.symbol, - chain: networks.sourceChain.name - }) + errorMessages: { + inputAmount1: getInsufficientFundsForGasFeesErrorMessage({ + asset: nativeCurrency.symbol, + chain: networks.sourceChain.name + }) + } }) } @@ -385,12 +435,29 @@ export function useTransferReadiness({ } // Everything is paid in ETH, so we sum it up - if (estimatedL1GasFees + estimatedL2GasFees > ethBalanceFloat) { + const notEnoughEthForGasFees = + estimatedL1GasFees + estimatedL2GasFees > ethBalanceFloat + + if ( + notEnoughEthForGasFees || + (sendsAdditionalEth && notEnoughEthForAdditionalEthTransfer) + ) { return notReady({ - errorMessage: getInsufficientFundsForGasFeesErrorMessage({ - asset: ether.symbol, - chain: networks.sourceChain.name - }) + errorMessages: { + inputAmount1: notEnoughEthForGasFees + ? getInsufficientFundsForGasFeesErrorMessage({ + asset: ether.symbol, + chain: networks.sourceChain.name + }) + : undefined, + inputAmount2: + sendsAdditionalEth && notEnoughEthForAdditionalEthTransfer + ? getInsufficientFundsErrorMessage({ + asset: ether.symbol, + chain: networks.sourceChain.name + }) + : undefined + } }) } @@ -402,10 +469,12 @@ export function useTransferReadiness({ // Withdrawals of the custom fee token will be treated same as ETH withdrawals (in the case below) if (estimatedL1GasFees + estimatedL2GasFees > ethBalanceFloat) { return notReady({ - errorMessage: getInsufficientFundsForGasFeesErrorMessage({ - asset: ether.symbol, - chain: networks.sourceChain.name - }) + errorMessages: { + inputAmount1: getInsufficientFundsForGasFeesErrorMessage({ + asset: ether.symbol, + chain: networks.sourceChain.name + }) + } }) } @@ -418,10 +487,12 @@ export function useTransferReadiness({ if (total > ethBalanceFloat) { return notReady({ - errorMessage: getInsufficientFundsForGasFeesErrorMessage({ - asset: nativeCurrency.symbol, - chain: networks.sourceChain.name - }) + errorMessages: { + inputAmount1: getInsufficientFundsForGasFeesErrorMessage({ + asset: nativeCurrency.symbol, + chain: networks.sourceChain.name + }) + } }) } @@ -430,6 +501,7 @@ export function useTransferReadiness({ } }, [ amount, + amount2, isTransferring, destinationAddressError, isSmartContractWallet, diff --git a/packages/arb-token-bridge-ui/src/components/common/Header.tsx b/packages/arb-token-bridge-ui/src/components/common/Header.tsx index 56a1158ba2..67f6d0b0d2 100644 --- a/packages/arb-token-bridge-ui/src/components/common/Header.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/Header.tsx @@ -7,23 +7,31 @@ import { isNetwork } from '../../util/networks' import { useNetworks } from '../../hooks/useNetworks' import { useDestinationChainStyle } from '../../hooks/useDestinationChainStyle' import { AppMobileSidebar } from '../Sidebar/AppMobileSidebar' +import { isExperimentalModeEnabled } from '../../util' export function Header({ children }: { children?: React.ReactNode }) { const [{ sourceChain }] = useNetworks() const { isTestnet } = isNetwork(sourceChain.id) + const isExperimentalMode = isExperimentalModeEnabled() + const destinationChainStyle = useDestinationChainStyle() return (
Arbitrum - {isTestnet && TESTNET MODE} + {isTestnet && !isExperimentalMode && ( + TESTNET MODE + )} + {isExperimentalMode && ( + + EXPERIMENTAL MODE: features may be incomplete or not work properly + + )}
{children}
diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts new file mode 100644 index 0000000000..13c592d0cb --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts @@ -0,0 +1,35 @@ +import { useAppState } from '../../state' +import { isExperimentalFeatureEnabled } from '../../util' +import { useNativeCurrency } from '../useNativeCurrency' +import { useNetworks } from '../useNetworks' +import { useNetworksRelationship } from '../useNetworksRelationship' + +export const useIsBatchTransferSupported = () => { + const [networks] = useNetworks() + const { isDepositMode, isTeleportMode, childChainProvider } = + useNetworksRelationship(networks) + const { + app: { selectedToken } + } = useAppState() + const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) + + if (!isExperimentalFeatureEnabled('batch')) { + return false + } + if (!selectedToken) { + return false + } + if (!isDepositMode) { + return false + } + // TODO: teleport is disabled for now but it needs to be looked into more to check whether it is or can be supported + if (isTeleportMode) { + return false + } + // TODO: disable custom native currency for now, check if this works + if (nativeCurrency.isCustom) { + return false + } + + return true +} diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts index f12ca9534b..a9f1ba6b23 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts @@ -17,5 +17,14 @@ export function useSetInputAmount() { [decimals, setQueryParams] ) - return setAmount + const setAmount2 = useCallback( + (newAmount: string) => { + const correctDecimalsAmount = truncateExtraDecimals(newAmount, 18) + + setQueryParams({ amount2: correctDecimalsAmount }) + }, + [setQueryParams] + ) + + return { setAmount, setAmount2 } } diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx b/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx index b66553598c..18e6e38738 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx +++ b/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx @@ -49,6 +49,7 @@ export const useArbQueryParams = () => { sourceChain: ChainParam, destinationChain: ChainParam, amount: withDefault(AmountQueryParam, ''), // amount which is filled in Transfer panel + amount2: withDefault(AmountQueryParam, ''), // extra eth to send together with erc20 token: StringParam, // import a new token using a Dialog Box settingsOpen: withDefault(BooleanParam, false) }) diff --git a/packages/arb-token-bridge-ui/src/pages/index.tsx b/packages/arb-token-bridge-ui/src/pages/index.tsx index 345101068f..1720c741f8 100644 --- a/packages/arb-token-bridge-ui/src/pages/index.tsx +++ b/packages/arb-token-bridge-ui/src/pages/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react' import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next' import dynamic from 'next/dynamic' +import { decodeString, encodeString } from 'use-query-params' import { registerCustomArbitrumNetwork } from '@arbitrum/sdk' import { Loader } from '../components/common/atoms/Loader' @@ -14,6 +15,7 @@ import { decodeChainQueryParam, encodeChainQueryParam } from '../hooks/useArbQueryParams' +import { sanitizeExperimentalFeaturesQueryParam } from '../util' const App = dynamic(() => import('../components/App/App'), { ssr: false, @@ -31,6 +33,7 @@ function getDestinationWithSanitizedQueryParams( sanitized: { sourceChainId: number destinationChainId: number + experiments: string | undefined }, query: GetServerSidePropsContext['query'] ) { @@ -38,7 +41,11 @@ function getDestinationWithSanitizedQueryParams( for (const key in query) { // don't copy "sourceChain" and "destinationChain" query params - if (key === 'sourceChain' || key === 'destinationChain') { + if ( + key === 'sourceChain' || + key === 'destinationChain' || + key === 'experiments' + ) { continue } @@ -52,6 +59,7 @@ function getDestinationWithSanitizedQueryParams( const encodedSource = encodeChainQueryParam(sanitized.sourceChainId) const encodedDestination = encodeChainQueryParam(sanitized.destinationChainId) + const encodedExperiments = encodeString(sanitized.experiments) if (encodedSource) { params.set('sourceChain', encodedSource) @@ -61,6 +69,10 @@ function getDestinationWithSanitizedQueryParams( } } + if (encodedExperiments) { + params.set('experiments', encodedExperiments) + } + return `/?${params.toString()}` } @@ -82,6 +94,7 @@ export function getServerSideProps({ }: GetServerSidePropsContext): GetServerSidePropsResult> { const sourceChainId = decodeChainQueryParam(query.sourceChain) const destinationChainId = decodeChainQueryParam(query.destinationChain) + const experiments = decodeString(query.experiments) // If both sourceChain and destinationChain are not present, let the client sync with Metamask if (!sourceChainId && !destinationChainId) { @@ -94,19 +107,23 @@ export function getServerSideProps({ addOrbitChainsToArbitrumSDK() // sanitize the query params - const sanitized = sanitizeQueryParams({ sourceChainId, destinationChainId }) + const sanitized = { + ...sanitizeQueryParams({ sourceChainId, destinationChainId }), + experiments: sanitizeExperimentalFeaturesQueryParam(experiments) + } // if the sanitized query params are different from the initial values, redirect to the url with sanitized query params if ( sourceChainId !== sanitized.sourceChainId || - destinationChainId !== sanitized.destinationChainId + destinationChainId !== sanitized.destinationChainId || + experiments !== sanitized.experiments ) { console.log(`[getServerSideProps] sanitizing query params`) console.log( - `[getServerSideProps] sourceChain=${sourceChainId}&destinationChain=${destinationChainId} (before)` + `[getServerSideProps] sourceChain=${sourceChainId}&destinationChain=${destinationChainId}&experiments=${experiments} (before)` ) console.log( - `[getServerSideProps] sourceChain=${sanitized.sourceChainId}&destinationChain=${sanitized.destinationChainId} (after)` + `[getServerSideProps] sourceChain=${sanitized.sourceChainId}&destinationChain=${sanitized.destinationChainId}&experiments=${sanitized.experiments} (after)` ) return { redirect: { diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/BridgeTransferStarter.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/BridgeTransferStarter.ts index 3efd4df181..7dcbafa9ef 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/BridgeTransferStarter.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/BridgeTransferStarter.ts @@ -48,10 +48,16 @@ export type TransferEstimateGas = { signer: Signer } +export type TransferOverrides = { + maxSubmissionCost?: BigNumber + excessFeeRefundAddress?: string +} + export type TransferProps = { amount: BigNumber signer: Signer destinationAddress?: string + overrides?: TransferOverrides } export type RequiresNativeCurrencyApprovalProps = { diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20DepositStarter.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20DepositStarter.ts index 7d90d2b4e9..d11dcbdcf3 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20DepositStarter.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/Erc20DepositStarter.ts @@ -273,7 +273,12 @@ export class Erc20DepositStarter extends BridgeTransferStarter { }) } - public async transfer({ amount, signer, destinationAddress }: TransferProps) { + public async transfer({ + amount, + signer, + destinationAddress, + overrides + }: TransferProps) { if (!this.sourceChainErc20Address) { throw Error('Erc20 token address not found') } @@ -292,7 +297,8 @@ export class Erc20DepositStarter extends BridgeTransferStarter { // the gas limit may vary by about 20k due to SSTORE (zero vs nonzero) // the 30% gas limit increase should cover the difference gasLimit: { percentIncrease: BigNumber.from(30) } - } + }, + ...overrides }) const gasLimit = await this.sourceChainProvider.estimateGas( diff --git a/packages/arb-token-bridge-ui/src/util/NumberUtils.ts b/packages/arb-token-bridge-ui/src/util/NumberUtils.ts index d315a289f6..0ce966667b 100644 --- a/packages/arb-token-bridge-ui/src/util/NumberUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/NumberUtils.ts @@ -116,20 +116,6 @@ export const formatAmount = ( ) } -export const countDecimals = (num: number | string) => { - if (Math.floor(Number(num)) === Number(num)) { - return 0 - } - - const decimalPart = String(num).split('.')[1] - - if (typeof decimalPart === 'undefined') { - return 0 - } - - return decimalPart.length -} - export const truncateExtraDecimals = (amount: string, decimals: number) => { const decimalPart = amount.split('.')[1] diff --git a/packages/arb-token-bridge-ui/src/util/index.ts b/packages/arb-token-bridge-ui/src/util/index.ts index d4ee03f1be..8392214fa0 100644 --- a/packages/arb-token-bridge-ui/src/util/index.ts +++ b/packages/arb-token-bridge-ui/src/util/index.ts @@ -53,3 +53,49 @@ export const getAPIBaseUrl = () => { // Resolves: next-js-error-only-absolute-urls-are-supported in test:ci return process.env.NODE_ENV === 'test' ? 'http://localhost:3000' : '' } + +const featureFlags = ['batch'] as const + +type FeatureFlag = (typeof featureFlags)[number] + +export const isExperimentalFeatureEnabled = (flag: FeatureFlag) => { + const query = new URLSearchParams(window.location.search) + const flags = query.get('experiments') + + if (!flags) { + return false + } + + return flags.split(',').includes(flag) +} + +export const isExperimentalModeEnabled = () => { + const query = new URLSearchParams(window.location.search) + const flags = query.get('experiments') + + return flags !== null +} + +export const sanitizeExperimentalFeaturesQueryParam = ( + flags: string | null | undefined +) => { + if (!flags) { + return undefined + } + + const flagsArray = flags.split(',') + + if (flagsArray.length === 0) { + return undefined + } + + const validFlagsArray = flagsArray.filter(f => + featureFlags.includes(f as FeatureFlag) + ) + + if (validFlagsArray.length === 0) { + return undefined + } + + return validFlagsArray.join(',') +} diff --git a/yarn.lock b/yarn.lock index 4e027cffbb..d114739aec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3916,12 +3916,12 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" -axios@^1.4.0, axios@^1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.6.tgz#878db45401d91fe9e53aed8ac962ed93bde8dd1c" - integrity sha512-XZLZDFfXKM9U/Y/B4nNynfCRUqNyVZ4sBC/n9GDRCkq9vd2mIvKjKKsbIh1WPmHmNbg6ND7cTBY3Y2+u1G3/2Q== +axios@^1.4.0, axios@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2" + integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== dependencies: - follow-redirects "^1.15.4" + follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -5635,10 +5635,10 @@ electron-to-chromium@^1.4.431: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.431.tgz#47990d6e43465d69aa1fbd0abdec43114946edd0" integrity sha512-m232JTVmCawA2vG+1azVxhKZ9Sv1Q//xxNv5PkP5rWxGgQE8c3CiZFrh8Xnp+d1NmNxlu3QQrGIfdeW5TtXX5w== -elliptic@6.5.4, elliptic@^6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== +elliptic@6.5.4, elliptic@6.5.7, elliptic@^6.5.4: + version "6.5.7" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" + integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== dependencies: bn.js "^4.11.9" brorand "^1.1.0" @@ -6696,7 +6696,7 @@ fetch-retry@^5.0.3: resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-5.0.6.tgz#17d0bc90423405b7a88b74355bf364acd2a7fa56" integrity sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ== -fflate@^0.4.1: +fflate@^0.4.8: version "0.4.8" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== @@ -6850,7 +6850,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0, follow-redirects@^1.15.4: +follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0, follow-redirects@^1.15.6: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== @@ -10430,12 +10430,14 @@ postcss@8.4.31, postcss@^8.4.23, postcss@^8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -posthog-js@^1.57.2: - version "1.68.1" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.68.1.tgz#eadca3db9e45287771fe3a8b4100bffe50891750" - integrity sha512-edwURtegKXIUEdjgLErJF8cfCuwj7kw8JDmomnVXp9cjwaJT8Y3BRLh5Lh81GnjwGuifp1vjOL0hD3AamtsmGg== +posthog-js@^1.155.4: + version "1.155.4" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.155.4.tgz#611a63cf95b8fa908b3b50b1043cbdcb4c2a712b" + integrity sha512-suxwAsmZGqMDXJe/RaCKI3PaDEHiuMDDhKcJklgGAg7eDnywieRkr5CoPcOOvnqTDMnuOPETr98jpYBXKUwGFQ== dependencies: - fflate "^0.4.1" + fflate "^0.4.8" + preact "^10.19.3" + web-vitals "^4.0.1" postinstall-postinstall@^2.1.0: version "2.1.0" @@ -10447,6 +10449,11 @@ preact@^10.12.0, preact@^10.5.9: resolved "https://registry.yarnpkg.com/preact/-/preact-10.15.1.tgz#a1de60c9fc0c79a522d969c65dcaddc5d994eede" integrity sha512-qs2ansoQEwzNiV5eAcRT1p1EC/dmEzaATVDJNiB3g2sRDWdA7b7MurXdJjB2+/WQktGWZwxvDrnuRFbWuIr64g== +preact@^10.19.3: + version "10.23.2" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.23.2.tgz#52deec92796ae0f0cc6b034d9c66e0fbc1b837dc" + integrity sha512-kKYfePf9rzKnxOAKDpsWhg/ysrHPqT+yQ7UW4JjdnqjFIeNUnNcEJvhuA8fDenxAGWzUqtd51DfVg7xp/8T9NA== + prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -12968,6 +12975,11 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +web-vitals@^4.0.1: + version "4.2.3" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.3.tgz#270c4baecfbc6ec6fc15da1989e465e5f9b94fb7" + integrity sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"