diff --git a/IPFS.json b/IPFS.json index df2d553d1..df6502566 100644 --- a/IPFS.json +++ b/IPFS.json @@ -3,7 +3,8 @@ "cid": "bafybeib3zmyqlmantvdd6i5q4ehmo4larvorgquyanne3uoqdbedwgh3aq", "leastSafeVersion": "0.36.1", "config": { - "enabledWithdrawalDexes": ["one-inch", "paraswap", "bebop"] + "enabledWithdrawalDexes": ["one-inch", "paraswap", "bebop"], + "multiChainBanner": [324, 10, 42161, 137, 8453, 5000, 59144, 534352, 56] } }, "5": { @@ -16,7 +17,10 @@ "cid": "bafybeibbsoqlofslw273b4ih2pdxfaz2zbjmred2ijog725tcmfoewix7y", "leastSafeVersion": "0.36.1", "config": { - "enabledWithdrawalDexes": ["one-inch", "paraswap", "bebop"] + "enabledWithdrawalDexes": ["one-inch", "paraswap", "bebop"], + "multiChainBanner": [ + 324, 10, 42161, 137, 8453, 5000, 59144, 534352, 56, 34443 + ] } } } diff --git a/assets/icons/lido-multichain/mode.svg b/assets/icons/lido-multichain/mode.svg new file mode 100644 index 000000000..c3eaca426 --- /dev/null +++ b/assets/icons/lido-multichain/mode.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/config/external-config/types.ts b/config/external-config/types.ts index 92ea1ad04..7c0435064 100644 --- a/config/external-config/types.ts +++ b/config/external-config/types.ts @@ -12,6 +12,7 @@ export type ManifestEntry = { export type ManifestConfig = { enabledWithdrawalDexes: DexWithdrawalApi[]; + multiChainBanner: number[]; }; export type ExternalConfig = Omit & diff --git a/config/external-config/utils.ts b/config/external-config/utils.ts index 9dc7e8d86..ad7f72c1f 100644 --- a/config/external-config/utils.ts +++ b/config/external-config/utils.ts @@ -4,7 +4,45 @@ import { getDexConfig } from 'features/withdrawals/request/withdrawal-rates'; import FallbackLocalManifest from 'IPFS.json' assert { type: 'json' }; -// TODO: refactor on config expansion +const isEnabledDexesValid = (config: object) => { + if ( + !( + 'enabledWithdrawalDexes' in config && + Array.isArray(config.enabledWithdrawalDexes) + ) + ) + return false; + + const enabledWithdrawalDexes = config.enabledWithdrawalDexes; + + if ( + !enabledWithdrawalDexes.every( + (dex) => typeof dex === 'string' && dex !== '', + ) + ) + return false; + + return new Set(enabledWithdrawalDexes).size === enabledWithdrawalDexes.length; +}; + +const isMultiChainBannerValid = (config: object) => { + // allow empty config + if (!('multiChainBanner' in config) || !config.multiChainBanner) return true; + + if (!Array.isArray(config.multiChainBanner)) return false; + + const multiChainBanner = config.multiChainBanner; + + if ( + !multiChainBanner.every( + (chainId) => typeof chainId === 'number' && chainId > 0, + ) + ) + return false; + + return !(new Set(multiChainBanner).size !== multiChainBanner.length); +}; + export const isManifestEntryValid = ( entry?: unknown, ): entry is ManifestEntry => { @@ -18,16 +56,10 @@ export const isManifestEntryValid = ( entry.config ) { const config = entry.config; - if ( - 'enabledWithdrawalDexes' in config && - Array.isArray(config.enabledWithdrawalDexes) - ) { - const enabledWithdrawalDexes = config.enabledWithdrawalDexes; - return ( - new Set(enabledWithdrawalDexes).size === enabledWithdrawalDexes.length - ); - } - return false; + + return [isEnabledDexesValid, isMultiChainBannerValid] + .map((validator) => validator(config)) + .every((isValid) => isValid); } return false; }; @@ -39,6 +71,7 @@ export const getBackwardCompatibleConfig = ( enabledWithdrawalDexes: config.enabledWithdrawalDexes.filter( (dex) => !!getDexConfig(dex), ), + multiChainBanner: config.multiChainBanner ?? [], }; }; diff --git a/config/groups/revalidation.ts b/config/groups/revalidation.ts index 59c55f761..523d09e79 100644 --- a/config/groups/revalidation.ts +++ b/config/groups/revalidation.ts @@ -1,15 +1,2 @@ -import type { ManifestConfig, ManifestEntry } from 'config/external-config'; - export const DEFAULT_REVALIDATION = 60 * 15; // 15 minutes export const ERROR_REVALIDATION_SECONDS = 60; // 1 minute - -export const FALLBACK_CONFIG: ManifestConfig = { - enabledWithdrawalDexes: [], -}; - -export const FALLBACK_MANIFEST_ENTRY: ManifestEntry = { - cid: undefined, - ens: undefined, - leastSafeVersion: undefined, - config: FALLBACK_CONFIG, -}; diff --git a/config/groups/web3.ts b/config/groups/web3.ts index 06f27f001..3e072920d 100644 --- a/config/groups/web3.ts +++ b/config/groups/web3.ts @@ -2,6 +2,8 @@ import { parseEther } from '@ethersproject/units'; // interval in ms for RPC event polling for token balance and tx updates export const PROVIDER_POLLING_INTERVAL = 12_000; +// how long in ms to wait for RPC batching(multicall and provider) +export const PROVIDER_BATCH_TIME = 150; // account for gas estimation // will always have >=0.001 ether, >=0.001 stETH, >=0.001 wstETH diff --git a/consts/chains.ts b/consts/chains.ts index e5bb343be..506330d20 100644 --- a/consts/chains.ts +++ b/consts/chains.ts @@ -14,4 +14,5 @@ export enum LIDO_MULTICHAIN_CHAINS { Linea = 59144, Scroll = 534352, 'BNB Chain' = 56, + 'Mode Chain' = 34443, } diff --git a/consts/external-links.ts b/consts/external-links.ts index af4db36ed..a62599c78 100644 --- a/consts/external-links.ts +++ b/consts/external-links.ts @@ -5,6 +5,6 @@ export const LINK_ADD_NFT_GUIDE = `${config.helpOrigin}/en/articles/7858367-how- export const OPEN_OCEAN_REFERRAL_ADDRESS = '0xbb1263222b2c020f155d409dba05c4a3861f18f8'; -// for dev and local testing you can set to 'http:/localhost:3000/runtime/IPFS.json' and have file at /public/runtime/IPFS.json +// for dev and local testing you can set to 'http://localhost:3000/runtime/IPFS.json' and have file at /public/runtime/IPFS.json export const IPFS_MANIFEST_URL = 'https://raw.githubusercontent.com/lidofinance/ethereum-staking-widget/main/IPFS.json'; diff --git a/consts/matomo-click-events.ts b/consts/matomo-click-events.ts index 0955fc01f..91f34032c 100644 --- a/consts/matomo-click-events.ts +++ b/consts/matomo-click-events.ts @@ -63,6 +63,16 @@ export const enum MATOMO_CLICK_EVENTS_TYPES { // /withdrawal/request and /withdrawal/claim shared events withdrawalWhatAreStakingPenaltiesFAQ = 'withdrawalWhatAreStakingPenaltiesFAQ', withdrawalNFTGuideFAQ = 'withdrawalNFTGuideFAQ', + + // /rewards page + rewardsExportCSV = 'rewardsExportCSV', + rewardsHistoricalStethPriceCheck = 'rewardsHistoricalStethPriceCheck', + rewardsHistoricalStethPriceUncheck = 'rewardsHistoricalStethPriceUncheck', + rewardsIncludeTransfersCheck = 'rewardsIncludeTransfersCheck', + rewardsIncludeTransfersUncheck = 'rewardsIncludeTransfersUncheck', + rewardsHistoricalCurrencyUSD = 'rewardsHistoricalCurrencyUSD', + rewardsHistoricalCurrencyEUR = 'rewardsHistoricalCurrencyEUR', + rewardsHistoricalCurrencyGBP = 'rewardsHistoricalCurrencyGBP', } export const MATOMO_CLICK_EVENTS: Record< @@ -339,4 +349,46 @@ export const MATOMO_CLICK_EVENTS: Record< 'Push on "How do I add the Lido NFT to my wallet" guide link in FAQ', 'eth_withdrawals_how_to_add_nft_guide_FAQ', ], + + // /rewards page + [MATOMO_CLICK_EVENTS_TYPES.rewardsExportCSV]: [ + 'Ethereum_Rewards_Widget', + 'Click on "Export CSV"', + 'eth_rewards_export_csv', + ], + [MATOMO_CLICK_EVENTS_TYPES.rewardsHistoricalStethPriceCheck]: [ + 'Ethereum_Rewards_Widget', + 'Click check on "Historical stETH price" in check-box', + 'eth_historical_stETH_price_check_box_check', + ], + [MATOMO_CLICK_EVENTS_TYPES.rewardsHistoricalStethPriceUncheck]: [ + 'Ethereum_Rewards_Widget', + 'Click uncheck on "Historical stETH price" in check-box', + 'eth_historical_stETH_price_check_box_uncheck', + ], + [MATOMO_CLICK_EVENTS_TYPES.rewardsIncludeTransfersCheck]: [ + 'Ethereum_Rewards_Widget', + 'Click check on "Include transfers" in check-box', + 'eth_include_transfers_check_box_check', + ], + [MATOMO_CLICK_EVENTS_TYPES.rewardsIncludeTransfersUncheck]: [ + 'Ethereum_Rewards_Widget', + 'Click uncheck on "Include transfers" in check-box', + 'eth_include_transfers_check_box_uncheck', + ], + [MATOMO_CLICK_EVENTS_TYPES.rewardsHistoricalCurrencyUSD]: [ + 'Ethereum_Rewards_Widget', + 'Click on "USD" in currency choice', + 'eth_historical_usd_currency_choice', + ], + [MATOMO_CLICK_EVENTS_TYPES.rewardsHistoricalCurrencyEUR]: [ + 'Ethereum_Rewards_Widget', + 'Click on "EUR" in currency choice', + 'eth_historical_eur_currency_choice', + ], + [MATOMO_CLICK_EVENTS_TYPES.rewardsHistoricalCurrencyGBP]: [ + 'Ethereum_Rewards_Widget', + 'Click on "GBP" in currency choice', + 'eth_historical_gbp_currency_choice', + ], }; diff --git a/features/ipfs/security-status-banner/use-version-check.ts b/features/ipfs/security-status-banner/use-version-check.ts index 414a24e37..f74449dbf 100644 --- a/features/ipfs/security-status-banner/use-version-check.ts +++ b/features/ipfs/security-status-banner/use-version-check.ts @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react'; -import { useLidoSWR } from '@lido-sdk/react'; -import { useWeb3 } from 'reef-knot/web3-react'; import { useForceDisconnect } from 'reef-knot/core-react'; +import { useLidoSWR } from '@lido-sdk/react'; import buildInfo from 'build-info.json'; import { config } from 'config'; import { useUserConfig } from 'config/user-config'; import { STRATEGY_IMMUTABLE } from 'consts/swr-strategies'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; import { overrideWithQAMockBoolean } from 'utils/qa'; import { isVersionLess } from './utils'; @@ -19,7 +19,7 @@ const URL_CID_REGEX = /[/.](?Qm[1-9A-HJ-NP-Za-km-z]{44,}|b[A-Za-z2-7]{58,}|B[A-Z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,}|F[0-9A-F]{50,})([./#?]|$)/; export const useVersionCheck = () => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const { setIsWalletConnectionAllowed } = useUserConfig(); const { forceDisconnect } = useForceDisconnect(); const [areConditionsAccepted, setConditionsAccepted] = useState(false); @@ -84,7 +84,7 @@ export const useVersionCheck = () => { forceDisconnect(); } }, [ - active, + isDappActive, forceDisconnect, isNotVerifiable, isVersionUnsafe, diff --git a/features/rewards/components/export/Export.tsx b/features/rewards/components/export/Export.tsx index cb59ca184..3bea9230a 100644 --- a/features/rewards/components/export/Export.tsx +++ b/features/rewards/components/export/Export.tsx @@ -5,6 +5,8 @@ import { backendRequest } from 'features/rewards/fetchers/backend'; import { ButtonStyle } from './Exportstyled'; import type { CurrencyType } from 'features/rewards/constants'; +import { trackEvent } from '@lidofinance/analytics-matomo'; +import { MATOMO_CLICK_EVENTS } from 'consts/matomo-click-events'; type ExportProps = { currency: CurrencyType; @@ -33,6 +35,7 @@ export const Export = ({ }); const formatted = genExportData(currencyObject, result.events); saveAsCSV(formatted); + trackEvent(...MATOMO_CLICK_EVENTS.rewardsExportCSV); }; return ( diff --git a/features/rewards/components/rewardsListContent/RewardsListContent.tsx b/features/rewards/components/rewardsListContent/RewardsListContent.tsx index e868152ae..b7c8ad636 100644 --- a/features/rewards/components/rewardsListContent/RewardsListContent.tsx +++ b/features/rewards/components/rewardsListContent/RewardsListContent.tsx @@ -1,13 +1,11 @@ import { FC } from 'react'; import { Loader, Divider } from '@lidofinance/lido-ui'; -import { useSDK, useTokenBalance } from '@lido-sdk/react'; -import { TOKENS, getTokenAddress } from '@lido-sdk/constants'; import { Zero } from '@ethersproject/constants'; -import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { useRewardsHistory } from 'features/rewards/hooks'; import { ErrorBlockNoSteth } from 'features/rewards/components/errorBlocks/ErrorBlockNoSteth'; import { RewardsTable } from 'features/rewards/components/rewardsTable'; +import { useStethBalance } from 'shared/hooks/use-balance'; import { useDappStatus } from 'shared/hooks/use-dapp-status'; import { RewardsListsEmpty } from './RewardsListsEmpty'; @@ -19,6 +17,8 @@ import { ErrorWrapper, } from './RewardsListContentStyles'; +import type { Address } from 'viem'; + export const RewardsListContent: FC = () => { const { isWalletConnected, isSupportedChain } = useDappStatus(); const { @@ -31,14 +31,11 @@ export const RewardsListContent: FC = () => { setPage, isLagging, } = useRewardsHistory(); - // temporarily until we switched to a new SDK - const { chainId } = useSDK(); - const { data: stethBalance, initialLoading: isStethBalanceLoading } = - useTokenBalance( - getTokenAddress(chainId || 1, TOKENS.STETH), - address, - STRATEGY_LAZY, - ); + const { data: stethBalance, isLoading: isStethBalanceLoading } = + useStethBalance({ + account: address as Address, + shouldSubscribeToUpdates: false, + }); const hasSteth = stethBalance?.gt(Zero); if (isWalletConnected && !isSupportedChain) diff --git a/features/rewards/components/rewardsListHeader/LeftOptions.tsx b/features/rewards/components/rewardsListHeader/LeftOptions.tsx index fef130268..fdb6e6bcc 100644 --- a/features/rewards/components/rewardsListHeader/LeftOptions.tsx +++ b/features/rewards/components/rewardsListHeader/LeftOptions.tsx @@ -3,6 +3,8 @@ import { Tooltip, Checkbox } from '@lidofinance/lido-ui'; import { useRewardsHistory } from 'features/rewards/hooks/useRewardsHistory'; import { LeftOptionsWrapper } from './styles'; +import { trackEvent } from '@lidofinance/analytics-matomo'; +import { MATOMO_CLICK_EVENTS } from 'consts/matomo-click-events'; export const LeftOptions: FC = () => { const { @@ -20,7 +22,14 @@ export const LeftOptions: FC = () => { > setIsIncludeTransfers(!isIncludeTransfers)} + onChange={() => { + trackEvent( + ...(!isIncludeTransfers + ? MATOMO_CLICK_EVENTS.rewardsIncludeTransfersCheck + : MATOMO_CLICK_EVENTS.rewardsIncludeTransfersUncheck), + ); + setIsIncludeTransfers(!isIncludeTransfers); + }} data-testid="includeTransfersCheckbox" label="Include transfers" /> @@ -33,9 +42,14 @@ export const LeftOptions: FC = () => { > - setIsUseArchiveExchangeRate(!isUseArchiveExchangeRate) - } + onChange={() => { + trackEvent( + ...(!isUseArchiveExchangeRate + ? MATOMO_CLICK_EVENTS.rewardsHistoricalStethPriceCheck + : MATOMO_CLICK_EVENTS.rewardsHistoricalStethPriceUncheck), + ); + setIsUseArchiveExchangeRate(!isUseArchiveExchangeRate); + }} data-testid="historicalStEthCheckbox" label="Historical stETH price" /> diff --git a/features/rewards/components/rewardsListHeader/RightOptions.tsx b/features/rewards/components/rewardsListHeader/RightOptions.tsx index 5b5632b6d..b323761c0 100644 --- a/features/rewards/components/rewardsListHeader/RightOptions.tsx +++ b/features/rewards/components/rewardsListHeader/RightOptions.tsx @@ -4,6 +4,14 @@ import { Export } from 'features/rewards/components/export'; import { RightOptionsWrapper } from './styles'; import { useRewardsHistory } from 'features/rewards/hooks/useRewardsHistory'; +import { MatomoEventType, trackEvent } from '@lidofinance/analytics-matomo'; +import { MATOMO_CLICK_EVENTS } from 'consts/matomo-click-events'; + +const MATOMO_EVENTS_MAP_CURRENCY_SELECTOR: Record = { + usd: MATOMO_CLICK_EVENTS.rewardsHistoricalCurrencyUSD, + eur: MATOMO_CLICK_EVENTS.rewardsHistoricalCurrencyEUR, + gbp: MATOMO_CLICK_EVENTS.rewardsHistoricalCurrencyGBP, +}; export const RightOptions: FC = () => { const { @@ -15,7 +23,14 @@ export const RightOptions: FC = () => { } = useRewardsHistory(); return ( - + { + const event = MATOMO_EVENTS_MAP_CURRENCY_SELECTOR[value]; + if (event) trackEvent(...event); + setCurrency(value); + }} + /> { const useStakeFormNetworkData = (): StakeFormNetworkData => { const { data: stethBalance, - update: updateStethBalance, - initialLoading: isStethBalanceLoading, - } = useSTETHBalance(STRATEGY_LAZY); + refetch: updateStethBalance, + isLoading: isStethBalanceLoading, + } = useStethBalance(); const { isMultisig, isLoading: isMultisigLoading } = useIsMultisig(); const gasLimit = useStethSubmitGasLimit(); const { maxGasPrice, initialLoading: isMaxGasPriceLoading } = @@ -75,9 +74,10 @@ const useStakeFormNetworkData = (): StakeFormNetworkData => { const { data: etherBalance, - update: updateEtherBalance, - initialLoading: isEtherBalanceLoading, - } = useEthereumBalance(undefined, STRATEGY_LAZY); + refetch: updateEtherBalance, + isLoading: isEtherBalanceLoading, + } = useEthereumBalance(); + const { data: stakingLimitInfo, mutate: mutateStakeLimit, diff --git a/features/stake/stake-form/stake-form-context/validation.ts b/features/stake/stake-form/stake-form-context/validation.ts index 26d0cadcf..b908e6f9f 100644 --- a/features/stake/stake-form/stake-form-context/validation.ts +++ b/features/stake/stake-form/stake-form-context/validation.ts @@ -1,9 +1,9 @@ import { useMemo } from 'react'; import invariant from 'tiny-invariant'; -import { useWeb3 } from 'reef-knot/web3-react'; import { Zero } from '@ethersproject/constants'; import { validateEtherAmount } from 'shared/hook-form/validation/validate-ether-amount'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; import { VALIDATION_CONTEXT_TIMEOUT } from 'features/withdrawals/withdrawals-constants'; import { handleResolverValidationError } from 'shared/hook-form/validation/validation-error'; import { awaitWithTimeout } from 'utils/await-with-timeout'; @@ -72,26 +72,26 @@ export const stakeFormValidationResolver: Resolver< export const useStakeFormValidationContext = ( networkData: StakeFormNetworkData, ): Promise => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const { stakingLimitInfo, etherBalance, isMultisig, gasCost } = networkData; const validationContextAwaited = useMemo(() => { if ( stakingLimitInfo && // we ether not connected or must have all account related data - (!active || (etherBalance && gasCost && isMultisig !== undefined)) + (!isDappActive || (etherBalance && gasCost && isMultisig !== undefined)) ) { return { - isWalletActive: active, + isWalletActive: isDappActive, stakingLimitLevel: stakingLimitInfo.stakeLimitLevel, currentStakeLimit: stakingLimitInfo.currentStakeLimit, - // condition above guaranties stubs will only be passed when active = false + // condition above guaranties stubs will only be passed when isDappActive = false etherBalance: etherBalance ?? Zero, gasCost: gasCost ?? Zero, isMultisig: isMultisig ?? false, }; } return undefined; - }, [active, etherBalance, gasCost, isMultisig, stakingLimitInfo]); + }, [isDappActive, etherBalance, gasCost, isMultisig, stakingLimitInfo]); return useAwaiter(validationContextAwaited).awaiter; }; diff --git a/features/stake/stake-form/use-stake.ts b/features/stake/stake-form/use-stake.ts index a772a63f5..33fe39827 100644 --- a/features/stake/stake-form/use-stake.ts +++ b/features/stake/stake-form/use-stake.ts @@ -1,7 +1,7 @@ import { BigNumber } from 'ethers'; import { useCallback } from 'react'; -import { useWeb3 } from 'reef-knot/web3-react'; import invariant from 'tiny-invariant'; +import { useAccount } from 'wagmi'; import { useSDK, @@ -18,6 +18,7 @@ import { MockLimitReachedError, getAddress } from './utils'; import { useTxModalStagesStake } from './hooks/use-tx-modal-stages-stake'; import { sendTx } from 'utils/send-tx'; +import { useTxConfirmation } from 'shared/hooks/use-tx-conformation'; type StakeArguments = { amount: BigNumber | null; @@ -31,18 +32,18 @@ type StakeOptions = { export const useStake = ({ onConfirm, onRetry }: StakeOptions) => { const stethContractWeb3 = useSTETHContractWeb3(); + const { address } = useAccount(); const stethContract = useSTETHContractRPC(); - const { account, chainId } = useWeb3(); const { staticRpcProvider } = useCurrentStaticRpcProvider(); const { providerWeb3 } = useSDK(); const { txModalStages } = useTxModalStagesStake(); + const waitForTx = useTxConfirmation(); return useCallback( async ({ amount, referral }: StakeArguments): Promise => { try { invariant(amount, 'amount is null'); - invariant(chainId, 'chainId is not defined'); - invariant(account, 'account is not defined'); + invariant(address, 'account is not defined'); invariant(providerWeb3, 'providerWeb3 not defined'); invariant(stethContractWeb3, 'steth is not defined'); @@ -56,7 +57,7 @@ export const useStake = ({ onConfirm, onRetry }: StakeOptions) => { txModalStages.sign(amount); const [isMultisig, referralAddress] = await Promise.all([ - isContract(account, staticRpcProvider), + isContract(address, staticRpcProvider), referral ? getAddress(referral, staticRpcProvider) : config.STAKE_FALLBACK_REFERRAL_ADDRESS, @@ -92,13 +93,11 @@ export const useStake = ({ onConfirm, onRetry }: StakeOptions) => { txModalStages.pending(amount, txHash); - if (!isMultisig) { - await runWithTransactionLogger('Stake block confirmation', () => - staticRpcProvider.waitForTransaction(txHash), - ); - } + await runWithTransactionLogger('Stake block confirmation', () => + waitForTx(txHash), + ); - const stethBalance = await stethContract.balanceOf(account); + const stethBalance = await stethContract.balanceOf(address); await onConfirm?.(); @@ -112,14 +111,14 @@ export const useStake = ({ onConfirm, onRetry }: StakeOptions) => { } }, [ - chainId, - account, + address, providerWeb3, stethContractWeb3, txModalStages, staticRpcProvider, stethContract, onConfirm, + waitForTx, onRetry, ], ); diff --git a/features/stake/stake.tsx b/features/stake/stake.tsx index 15f3f451b..237eb888c 100644 --- a/features/stake/stake.tsx +++ b/features/stake/stake.tsx @@ -1,5 +1,5 @@ import { FaqPlaceholder } from 'features/ipfs'; -import { useWeb3Key } from 'shared/hooks/useWeb3Key'; +import { useWagmiKey } from 'shared/hooks/use-wagmi-key'; import NoSSRWrapper from 'shared/components/no-ssr-wrapper'; import { OnlyInfraRender } from 'shared/components/only-infra-render'; @@ -8,7 +8,7 @@ import { LidoStats } from './lido-stats/lido-stats'; import { StakeForm } from './stake-form'; export const Stake = () => { - const key = useWeb3Key(); + const key = useWagmiKey(); return ( <> diff --git a/features/withdrawals/claim/claim-form-context/claim-form-context.tsx b/features/withdrawals/claim/claim-form-context/claim-form-context.tsx index ac4c471b6..555257a7e 100644 --- a/features/withdrawals/claim/claim-form-context/claim-form-context.tsx +++ b/features/withdrawals/claim/claim-form-context/claim-form-context.tsx @@ -7,26 +7,29 @@ import { useMemo, } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; - import invariant from 'tiny-invariant'; + +import { useClaim } from 'features/withdrawals/hooks'; +import { useClaimData } from 'features/withdrawals/contexts/claim-data-context'; +import { useFormControllerRetry } from 'shared/hook-form/form-controller/use-form-controller-retry-delegate'; +import { + FormControllerContext, + FormControllerContextValueType, +} from 'shared/hook-form/form-controller'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; + import { ClaimFormInputType, ClaimFormValidationContext } from './types'; import { claimFormValidationResolver } from './validation'; -import { useClaim } from 'features/withdrawals/hooks'; import { useMaxSelectedCount } from './use-max-selected-count'; -import { useFormControllerRetry } from 'shared/hook-form/form-controller/use-form-controller-retry-delegate'; import { generateDefaultValues, useGetDefaultValues, } from './use-default-values'; import { ClaimFormHelperState, useHelperState } from './use-helper-state'; -import { - FormControllerContext, - FormControllerContextValueType, -} from 'shared/hook-form/form-controller'; -import { useClaimData } from 'features/withdrawals/contexts/claim-data-context'; -import { useWeb3 } from 'reef-knot/web3-react'; -type ClaimFormDataContextValueType = ClaimFormHelperState; +type ClaimFormDataContextValueType = ClaimFormHelperState & { + maxSelectedCountReason: string | null; +}; const claimFormDataContext = createContext(null); @@ -39,25 +42,32 @@ export const useClaimFormData = () => { }; export const ClaimFormProvider: FC = ({ children }) => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const { data } = useClaimData(); - const { maxSelectedRequestCount, defaultSelectedRequestCount } = - useMaxSelectedCount(); + const { + maxSelectedRequestCount, + defaultSelectedRequestCount, + maxSelectedCountReason, + } = useMaxSelectedCount(); const { getDefaultValues } = useGetDefaultValues(defaultSelectedRequestCount); const formObject = useForm({ defaultValues: getDefaultValues, resolver: claimFormValidationResolver, - context: { maxSelectedRequestCount, isWalletActive: active }, + context: { maxSelectedRequestCount, isWalletActive: isDappActive }, mode: 'onChange', reValidateMode: 'onChange', }); const { watch, reset, setValue, getValues, formState } = formObject; - const claimFormDataContextValue = useHelperState( - watch, - maxSelectedRequestCount, + + const helperState = useHelperState(watch, maxSelectedRequestCount); + + const claimFormDataContextValue = useMemo( + () => ({ ...helperState, maxSelectedCountReason }), + [helperState, maxSelectedCountReason], ); + const { retryEvent, retryFire } = useFormControllerRetry(); const claim = useClaim({ onRetry: retryFire }); diff --git a/features/withdrawals/claim/claim-form-context/use-max-selected-count.ts b/features/withdrawals/claim/claim-form-context/use-max-selected-count.ts index caab735ad..d59b2cc3b 100644 --- a/features/withdrawals/claim/claim-form-context/use-max-selected-count.ts +++ b/features/withdrawals/claim/claim-form-context/use-max-selected-count.ts @@ -15,8 +15,14 @@ export const useMaxSelectedCount = () => { DEFAULT_CLAIM_REQUEST_SELECTED, maxSelectedRequestCount, ); + + const maxSelectedCountReason = isLedgerLive + ? 'Ledger Clear Sign allows to claim up to 2 requests per transaction' + : null; + return { maxSelectedRequestCount, defaultSelectedRequestCount, + maxSelectedCountReason, }; }; diff --git a/features/withdrawals/claim/form/requests-list/request-item.tsx b/features/withdrawals/claim/form/requests-list/request-item.tsx index fcf25521d..a811f8cdd 100644 --- a/features/withdrawals/claim/form/requests-list/request-item.tsx +++ b/features/withdrawals/claim/form/requests-list/request-item.tsx @@ -1,8 +1,13 @@ import { forwardRef } from 'react'; -import { useWeb3 } from 'reef-knot/web3-react'; import { useFormState, useWatch } from 'react-hook-form'; +import { useAccount } from 'wagmi'; -import { Checkbox, CheckboxProps, External } from '@lidofinance/lido-ui'; +import { + Checkbox, + CheckboxProps, + External, + Tooltip, +} from '@lidofinance/lido-ui'; import { FormatToken } from 'shared/formatters'; import { RequestStatus } from './request-item-status'; @@ -19,9 +24,9 @@ type RequestItemProps = { export const RequestItem = forwardRef( ({ token_id, name, disabled, index, ...props }, ref) => { - const { chainId } = useWeb3(); + const { chainId } = useAccount(); const { isSubmitting } = useFormState(); - const { canSelectMore } = useClaimFormData(); + const { canSelectMore, maxSelectedCountReason } = useClaimFormData(); const { checked, status } = useWatch< ClaimFormInputType, `requests.${number}` @@ -42,15 +47,23 @@ export const RequestItem = forwardRef( : status.amountOfStETH; const symbol = isClaimable ? 'ETH' : 'stETH'; + const showDisabledReasonTooltip = + maxSelectedCountReason && + !canSelectMore && + status.isFinalized && + !checked; + const label = ( ); - return ( + const requestBody = ( ( ); + + return showDisabledReasonTooltip ? ( + + {requestBody} + + ) : ( + requestBody + ); }, ); diff --git a/features/withdrawals/claim/form/requests-list/requests-list.tsx b/features/withdrawals/claim/form/requests-list/requests-list.tsx index 4ed62d276..1e7ed79e8 100644 --- a/features/withdrawals/claim/form/requests-list/requests-list.tsx +++ b/features/withdrawals/claim/form/requests-list/requests-list.tsx @@ -1,11 +1,11 @@ +import { useFieldArray, useFormContext, useFormState } from 'react-hook-form'; import { useDappStatus } from 'shared/hooks/use-dapp-status'; +import { ClaimFormInputType } from '../../claim-form-context'; import { RequestItem } from './request-item'; import { RequestsEmpty } from './requests-empty'; import { Wrapper } from './styles'; import { RequestsLoader } from './requests-loader'; -import { useFieldArray, useFormContext, useFormState } from 'react-hook-form'; -import { ClaimFormInputType } from '../../claim-form-context'; export const RequestsList: React.FC = () => { const { isWalletConnected, isDappActive } = useDappStatus(); diff --git a/features/withdrawals/hooks/contract/useClaim.ts b/features/withdrawals/hooks/contract/useClaim.ts index 0e55e9bcf..f55dce17f 100644 --- a/features/withdrawals/hooks/contract/useClaim.ts +++ b/features/withdrawals/hooks/contract/useClaim.ts @@ -1,40 +1,43 @@ import { useCallback } from 'react'; import { BigNumber } from 'ethers'; +import invariant from 'tiny-invariant'; +import { useAccount } from 'wagmi'; -import { useClaimData } from 'features/withdrawals/contexts/claim-data-context'; -import { runWithTransactionLogger } from 'utils'; +import { useSDK } from '@lido-sdk/react'; -import { useWithdrawalsContract } from './useWithdrawalsContract'; +import { useClaimData } from 'features/withdrawals/contexts/claim-data-context'; import { RequestStatusClaimable } from 'features/withdrawals/types/request-status'; -import invariant from 'tiny-invariant'; -import { isContract } from 'utils/isContract'; -import { useWeb3 } from 'reef-knot/web3-react'; -import { useSDK } from '@lido-sdk/react'; import { useTxModalStagesClaim } from 'features/withdrawals/claim/transaction-modal-claim/use-tx-modal-stages-claim'; import { useCurrentStaticRpcProvider } from 'shared/hooks/use-current-static-rpc-provider'; +import { runWithTransactionLogger } from 'utils'; +import { isContract } from 'utils/isContract'; import { sendTx } from 'utils/send-tx'; +import { useTxConfirmation } from 'shared/hooks/use-tx-conformation'; + +import { useWithdrawalsContract } from './useWithdrawalsContract'; type Args = { onRetry?: () => void; }; export const useClaim = ({ onRetry }: Args) => { - const { account } = useWeb3(); + const { address } = useAccount(); const { providerWeb3 } = useSDK(); const { contractWeb3 } = useWithdrawalsContract(); const { staticRpcProvider } = useCurrentStaticRpcProvider(); const { optimisticClaimRequests } = useClaimData(); const { txModalStages } = useTxModalStagesClaim(); + const waitForTx = useTxConfirmation(); return useCallback( async (sortedRequests: RequestStatusClaimable[]) => { try { invariant(contractWeb3, 'must have contract'); invariant(sortedRequests, 'must have requests'); - invariant(account, 'must have address'); + invariant(address, 'must have address'); invariant(providerWeb3, 'must have provider'); - const isMultisig = await isContract(account, contractWeb3.provider); + const isMultisig = await isContract(address, contractWeb3.provider); const amount = sortedRequests.reduce( (s, r) => s.add(r.claimableEth), @@ -70,8 +73,8 @@ export const useClaim = ({ onRetry }: Args) => { txModalStages.pending(amount, txHash); - await runWithTransactionLogger('Claim block confirmation', async () => - staticRpcProvider.waitForTransaction(txHash), + await runWithTransactionLogger('Claim block confirmation', () => + waitForTx(txHash), ); await optimisticClaimRequests(sortedRequests); @@ -86,11 +89,12 @@ export const useClaim = ({ onRetry }: Args) => { }, [ contractWeb3, - account, + address, providerWeb3, txModalStages, - staticRpcProvider, optimisticClaimRequests, + staticRpcProvider, + waitForTx, onRetry, ], ); diff --git a/features/withdrawals/hooks/contract/useRequest.ts b/features/withdrawals/hooks/contract/useRequest.ts index 16a4cfba2..82c7d17c3 100644 --- a/features/withdrawals/hooks/contract/useRequest.ts +++ b/features/withdrawals/hooks/contract/useRequest.ts @@ -2,7 +2,6 @@ import { useCallback } from 'react'; import { BigNumber } from 'ethers'; import invariant from 'tiny-invariant'; -import { useWeb3 } from 'reef-knot/web3-react'; import { useAccount } from 'wagmi'; import { Zero } from '@ethersproject/constants'; import { @@ -29,12 +28,13 @@ import { useTxModalStagesRequest } from 'features/withdrawals/request/transactio import { useTransactionModal } from 'shared/transaction-modal/transaction-modal'; import { sendTx } from 'utils/send-tx'; import { overrideWithQAMockBoolean } from 'utils/qa'; +import { useTxConfirmation } from 'shared/hooks/use-tx-conformation'; // this encapsulates permit/approval & steth/wsteth flows const useWithdrawalRequestMethods = () => { const { providerWeb3 } = useSDK(); const { staticRpcProvider } = useCurrentStaticRpcProvider(); - const { account, chainId, contractWeb3 } = useWithdrawalsContract(); + const { address, contractWeb3 } = useWithdrawalsContract(); const permitSteth = useCallback( async ({ @@ -44,8 +44,6 @@ const useWithdrawalRequestMethods = () => { signature?: GatherPermitSignatureResult; requests: BigNumber[]; }) => { - invariant(chainId, 'must have chainId'); - invariant(account, 'must have account'); invariant(providerWeb3, 'must have providerWeb3'); invariant(signature, 'must have signature'); invariant(contractWeb3, 'must have contractWeb3'); @@ -73,7 +71,7 @@ const useWithdrawalRequestMethods = () => { return callback; }, - [account, chainId, contractWeb3, providerWeb3, staticRpcProvider], + [contractWeb3, providerWeb3, staticRpcProvider], ); const permitWsteth = useCallback( @@ -84,8 +82,6 @@ const useWithdrawalRequestMethods = () => { signature?: GatherPermitSignatureResult; requests: BigNumber[]; }) => { - invariant(chainId, 'must have chainId'); - invariant(account, 'must have account'); invariant(signature, 'must have signature'); invariant(providerWeb3, 'must have providerWeb3'); invariant(contractWeb3, 'must have contractWeb3'); @@ -113,21 +109,20 @@ const useWithdrawalRequestMethods = () => { return callback; }, - [account, chainId, contractWeb3, providerWeb3, staticRpcProvider], + [contractWeb3, providerWeb3, staticRpcProvider], ); const steth = useCallback( async ({ requests }: { requests: BigNumber[] }) => { - invariant(chainId, 'must have chainId'); - invariant(account, 'must have account'); + invariant(address, 'must have account'); invariant(contractWeb3, 'must have contractWeb3'); invariant(providerWeb3, 'must have providerWeb3'); - const isMultisig = await isContract(account, contractWeb3.provider); + const isMultisig = await isContract(address, staticRpcProvider); const tx = await contractWeb3.populateTransaction.requestWithdrawals( requests, - account, + address, ); const callback = async () => @@ -140,21 +135,20 @@ const useWithdrawalRequestMethods = () => { return callback; }, - [account, chainId, contractWeb3, staticRpcProvider, providerWeb3], + [address, contractWeb3, staticRpcProvider, providerWeb3], ); const wstETH = useCallback( async ({ requests }: { requests: BigNumber[] }) => { - invariant(chainId, 'must have chainId'); - invariant(account, 'must have account'); + invariant(address, 'must have address'); invariant(contractWeb3, 'must have contractWeb3'); invariant(providerWeb3, 'must have providerWeb3'); - const isMultisig = await isContract(account, contractWeb3.provider); + const isMultisig = await isContract(address, staticRpcProvider); const tx = await contractWeb3.populateTransaction.requestWithdrawalsWstETH( requests, - account, + address, ); const callback = async () => @@ -167,7 +161,7 @@ const useWithdrawalRequestMethods = () => { return callback; }, - [account, chainId, contractWeb3, staticRpcProvider, providerWeb3], + [address, contractWeb3, staticRpcProvider, providerWeb3], ); return useCallback( @@ -202,14 +196,13 @@ export const useWithdrawalRequest = ({ }: useWithdrawalRequestParams) => { const { chainId } = useSDK(); const withdrawalQueueAddress = getWithdrawalQueueAddress(chainId); - const { staticRpcProvider } = useCurrentStaticRpcProvider(); - const { connector } = useAccount(); - const { account } = useWeb3(); + const { connector, address } = useAccount(); const { isBunker } = useWithdrawals(); const { txModalStages } = useTxModalStagesRequest(); const getRequestMethod = useWithdrawalRequestMethods(); const { isMultisig, isLoading: isMultisigLoading } = useIsMultisig(); + const waitForTx = useTxConfirmation(); const wstethContract = useWSTETHContractRPC(); const stethContract = useSTETHContractRPC(); @@ -224,12 +217,13 @@ export const useWithdrawalRequest = ({ approve, needsApprove, allowance, - initialLoading: loadingUseApprove, + isLoading: loadingUseApprove, + refetch: refetchAllowance, } = useApprove( valueBN, tokenContract.address, withdrawalQueueAddress, - account ?? undefined, + address ?? undefined, ); const { gatherPermitSignature } = useERC20PermitSignature({ @@ -322,12 +316,17 @@ export const useWithdrawalRequest = ({ txModalStages.pending(amount, token, txHash); if (!isMultisig) { - await runWithTransactionLogger('Stake block confirmation', () => - staticRpcProvider.waitForTransaction(txHash), + await runWithTransactionLogger( + 'Withdrawal Request block confirmation', + () => waitForTx(txHash), ); } - await onConfirm?.(); + await Promise.all([ + onConfirm?.(), + isApprovalFlow && + refetchAllowance({ throwOnError: false, cancelRefetch: false }), + ]); txModalStages.success(amount, token, txHash); return true; } catch (error) { @@ -347,8 +346,9 @@ export const useWithdrawalRequest = ({ needsApprove, onConfirm, onRetry, - staticRpcProvider, + refetchAllowance, txModalStages, + waitForTx, ], ); diff --git a/features/withdrawals/hooks/contract/useWithdrawalsContract.ts b/features/withdrawals/hooks/contract/useWithdrawalsContract.ts index e874c7b84..f19c23cdb 100644 --- a/features/withdrawals/hooks/contract/useWithdrawalsContract.ts +++ b/features/withdrawals/hooks/contract/useWithdrawalsContract.ts @@ -1,14 +1,14 @@ +import { useAccount } from 'wagmi'; import { useWithdrawalQueueContractWeb3, useWithdrawalQueueContractRPC, } from '@lido-sdk/react'; -import { useWeb3 } from 'reef-knot/web3-react'; export const useWithdrawalsContract = () => { const contractWeb3 = useWithdrawalQueueContractWeb3(); const contractRpc = useWithdrawalQueueContractRPC(); - const { account, chainId } = useWeb3(); + const { address, chainId } = useAccount(); - return { contractWeb3, contractRpc, account, chainId }; + return { contractWeb3, contractRpc, address, chainId }; }; diff --git a/features/withdrawals/hooks/contract/useWithdrawalsData.ts b/features/withdrawals/hooks/contract/useWithdrawalsData.ts index 592f97d8f..138078dec 100644 --- a/features/withdrawals/hooks/contract/useWithdrawalsData.ts +++ b/features/withdrawals/hooks/contract/useWithdrawalsData.ts @@ -80,15 +80,15 @@ const getRequestTimeForWQRequestIds = async ( }; export const useWithdrawalRequests = () => { - const { contractRpc, account, chainId } = useWithdrawalsContract(); + const { contractRpc, address, chainId } = useWithdrawalsContract(); // const { data: currentShareRate } = useLidoShareRate(); const swr = useLidoSWR( // TODO: use this fragment for expected eth calculation // currentShareRate - // ? ['swr:withdrawals-requests', account, chainId, currentShareRate] + // ? ['swr:withdrawals-requests', address, chainId, currentShareRate] // : false, - ['swr:withdrawals-requests', account, chainId], + ['swr:withdrawals-requests', address, chainId], async (...args: unknown[]) => { const account = args[1] as string; // const currentShareRate = args[3] as BigNumber; diff --git a/features/withdrawals/hooks/useNftDataByTxHash.ts b/features/withdrawals/hooks/useNftDataByTxHash.ts index 098f719c9..bbfd4ebaf 100644 --- a/features/withdrawals/hooks/useNftDataByTxHash.ts +++ b/features/withdrawals/hooks/useNftDataByTxHash.ts @@ -14,13 +14,13 @@ type NFTApiData = { }; export const useNftDataByTxHash = (txHash: string | null) => { - const { contractRpc, account } = useWithdrawalsContract(); + const { contractRpc, address } = useWithdrawalsContract(); const { staticRpcProvider } = useCurrentStaticRpcProvider(); const swrNftApiData = useLidoSWR( - account && txHash ? ['swr:nft-data-by-tx-hash', txHash, account] : null, + address && txHash ? ['swr:nft-data-by-tx-hash', txHash, address] : null, async () => { - if (!txHash || !account) return null; + if (!txHash || !address) return null; const txReciept: TransactionReceipt = await staticRpcProvider.getTransactionReceipt(txHash); diff --git a/features/withdrawals/hooks/useTvlError.ts b/features/withdrawals/hooks/useTvlError.ts new file mode 100644 index 000000000..707ae2f21 --- /dev/null +++ b/features/withdrawals/hooks/useTvlError.ts @@ -0,0 +1,26 @@ +import { useMemo } from 'react'; +import { useController } from 'react-hook-form'; +import { + TvlErrorPayload, + ValidationTvlJoke, +} from '../request/request-form-context/validators'; +import { RequestFormInputType } from 'features/withdrawals/request/request-form-context'; + +const getTvlError = (error?: unknown) => + error && + typeof error === 'object' && + 'type' in error && + error.type == ValidationTvlJoke.type && + 'payload' in error + ? (error.payload as TvlErrorPayload) + : { balanceDiffSteth: undefined, tvlDiff: undefined }; + +export const useTvlError = () => { + const { + fieldState: { error }, + } = useController({ + name: 'amount', + }); + + return useMemo(() => getTvlError(error), [error]); +}; diff --git a/features/withdrawals/hooks/useTvlMessage.ts b/features/withdrawals/hooks/useTvlMessage.ts index f9e0c9804..485bc6b6d 100644 --- a/features/withdrawals/hooks/useTvlMessage.ts +++ b/features/withdrawals/hooks/useTvlMessage.ts @@ -2,10 +2,7 @@ import { useMemo } from 'react'; import { formatEther } from '@ethersproject/units'; import { shortenTokenValue } from 'utils'; -import { - TvlErrorPayload, - ValidationTvlJoke, -} from '../request/request-form-context/validators'; +import { useTvlError } from './useTvlError'; const texts: ((amount: string) => string)[] = [ (amount) => @@ -17,18 +14,11 @@ const texts: ((amount: string) => string)[] = [ const getText = () => texts[Math.floor(Math.random() * texts.length)]; -export const useTvlMessage = (error?: unknown) => { +export const useTvlMessage = () => { // To render one text per page before refresh const textTemplate = useMemo(() => getText(), []); - const { balanceDiffSteth, tvlDiff } = - error && - typeof error === 'object' && - 'type' in error && - error.type == ValidationTvlJoke.type && - 'payload' in error - ? (error.payload as TvlErrorPayload) - : { balanceDiffSteth: undefined, tvlDiff: undefined }; + const { balanceDiffSteth, tvlDiff } = useTvlError(); return { balanceDiff: balanceDiffSteth, diff --git a/features/withdrawals/hooks/useWithdrawTxPrice.ts b/features/withdrawals/hooks/useWithdrawTxPrice.ts index 14af8fd1e..be986487e 100644 --- a/features/withdrawals/hooks/useWithdrawTxPrice.ts +++ b/features/withdrawals/hooks/useWithdrawTxPrice.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { BigNumber } from 'ethers'; import invariant from 'tiny-invariant'; -import { useWeb3 } from 'reef-knot/web3-react'; +import { useAccount } from 'wagmi'; import { TOKENS } from '@lido-sdk/constants'; import { useLidoSWR, useSDK } from '@lido-sdk/react'; @@ -109,7 +109,7 @@ export const useRequestTxPrice = ({ export const useClaimTxPrice = (requests: RequestStatusClaimable[]) => { const { contractRpc } = useWithdrawalsContract(); - const { account, chainId } = useWeb3(); + const { address, chainId } = useAccount(); const requestCount = requests.length || 1; const debouncedSortedSelectedRequests = useDebouncedValue(requests, 2000); @@ -118,13 +118,13 @@ export const useClaimTxPrice = (requests: RequestStatusClaimable[]) => { [ 'swr:claim-request-gas-limit', debouncedSortedSelectedRequests, - account, + address, chainId, ], async () => { if ( !chainId || - !account || + !address || !contractRpc || debouncedSortedSelectedRequests.length === 0 ) @@ -135,12 +135,12 @@ export const useClaimTxPrice = (requests: RequestStatusClaimable[]) => { .claimWithdrawals( sortedRequests.map((r) => r.id), sortedRequests.map((r) => r.hint), - { from: account }, + { from: address }, ) .catch((error) => { console.warn('Could not estimate gas for claim', { ids: sortedRequests.map((r) => r.id), - account, + address, error, }); return undefined; diff --git a/features/withdrawals/request/form/controls/input-group-request.tsx b/features/withdrawals/request/form/controls/input-group-request.tsx index 5a5e7d516..290eb7421 100644 --- a/features/withdrawals/request/form/controls/input-group-request.tsx +++ b/features/withdrawals/request/form/controls/input-group-request.tsx @@ -1,14 +1,9 @@ import { FC, PropsWithChildren } from 'react'; import { useTvlMessage } from 'features/withdrawals/hooks'; -import { useFormState } from 'react-hook-form'; import { InputGroupHookForm } from 'shared/hook-form/controls/input-group-hook-form'; -import { RequestFormInputType } from '../../request-form-context'; export const InputGroupRequest: FC = ({ children }) => { - const { - errors: { amount: amountError }, - } = useFormState({ name: 'amount' }); - const { tvlMessage } = useTvlMessage(amountError); + const { tvlMessage } = useTvlMessage(); return ( {children} diff --git a/features/withdrawals/request/form/controls/token-amount-input-request.tsx b/features/withdrawals/request/form/controls/token-amount-input-request.tsx index ab5e9b255..da15fffa6 100644 --- a/features/withdrawals/request/form/controls/token-amount-input-request.tsx +++ b/features/withdrawals/request/form/controls/token-amount-input-request.tsx @@ -1,4 +1,4 @@ -import { useController, useWatch } from 'react-hook-form'; +import { useWatch } from 'react-hook-form'; import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo-click-events'; import { InputDecoratorTvlStake } from 'features/withdrawals/shared/input-decorator-tvl-stake'; @@ -17,13 +17,7 @@ export const TokenAmountInputRequest = () => { const token = useWatch({ name: 'token' }); const { maxAmount, isTokenLocked } = useRequestFormData(); - const { - fieldState: { error }, - } = useController({ - name: 'amount', - }); - - const { balanceDiff } = useTvlMessage(error); + const { balanceDiff } = useTvlMessage(); return ( = (props) => { const [showMore, setShowMore] = useState(false); const [buttonText, setButtonText] = useState('See all options'); + + const { balanceDiffSteth } = useTvlError(); + const isPausedByTvlError = balanceDiffSteth !== undefined; + const { data, initialLoading, amount, selectedToken, enabledDexes } = - useWithdrawalRates(); + useWithdrawalRates({ + isPaused: isPausedByTvlError, + }); const isAnyDexEnabled = enabledDexes.length > 0; const allowExpand = enabledDexes.length > MAX_SHOWN_ELEMENTS; + const showLoader = !isPausedByTvlError && isAnyDexEnabled && initialLoading; + const showList = !isPausedByTvlError && isAnyDexEnabled && !initialLoading; + const showPausedList = isPausedByTvlError; + + const dexesListData = useMemo(() => { + if (showList) return data; + if (showPausedList) { + return enabledDexes.map((dexId) => ({ + ...getDexConfig(dexId), + toReceive: null, + rate: null, + })); + } + return null; + }, [data, enabledDexes, showList, showPausedList]); + return ( <> Aggregator's prices are not available now )} - {isAnyDexEnabled && - initialLoading && - enabledDexes.map((_, i) => )} - {isAnyDexEnabled && - !initialLoading && - data?.map(({ title, toReceive, link, rate, matomoEvent, icon }) => { - return ( - trackMatomoEvent(matomoEvent)} - url={link(amount, selectedToken)} - key={title} - toReceive={rate ? toReceive : null} - /> - ); - })} + {showLoader && enabledDexes.map((_, i) => )} + {(showList || showPausedList) && + dexesListData?.map( + ({ title, toReceive, link, rate, matomoEvent, icon }) => { + return ( + trackMatomoEvent(matomoEvent)} + url={link(amount, selectedToken)} + key={title} + toReceive={rate ? toReceive : null} + /> + ); + }, + )} {allowExpand && ( (Math.floor(num * 10000) / 10000).toString(); const DexButton: React.FC = ({ isActive, onClick }) => { + const { balanceDiffSteth } = useTvlError(); + const isPausedByTvlError = balanceDiffSteth !== undefined; const { initialLoading, bestRate, enabledDexes } = useWithdrawalRates({ + isPaused: isPausedByTvlError, fallbackValue: DEFAULT_VALUE_FOR_RATE, }); const isAnyDexEnabled = enabledDexes.length > 0; + const bestRateFloored = bestRate !== null && toFloor(bestRate); const bestRateValue = - bestRate && isAnyDexEnabled ? `1 : ${toFloor(bestRate)}` : '—'; + !isPausedByTvlError && + isAnyDexEnabled && + bestRateFloored && + bestRateFloored !== '0' + ? `1 : ${bestRateFloored}` + : '—'; + return ( = ({ isActive, onClick }) => { Best Rate: - {initialLoading ? : bestRateValue} + {initialLoading && !isPausedByTvlError ? ( + + ) : ( + bestRateValue + )} Waiting time:{' '} diff --git a/features/withdrawals/request/form/transaction-info.tsx b/features/withdrawals/request/form/transaction-info.tsx index 995b806ae..1b0b53e8d 100644 --- a/features/withdrawals/request/form/transaction-info.tsx +++ b/features/withdrawals/request/form/transaction-info.tsx @@ -1,21 +1,23 @@ +import { useWatch } from 'react-hook-form'; import { TOKENS } from '@lido-sdk/constants'; import { DataTableRow } from '@lidofinance/lido-ui'; + import { useRequestTxPrice } from 'features/withdrawals/hooks/useWithdrawTxPrice'; import { useApproveGasLimit } from 'features/wsteth/wrap/hooks/use-approve-gas-limit'; -import { useWatch } from 'react-hook-form'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; +import { AllowanceDataTableRow } from 'shared/components/allowance-data-table-row'; import { DataTableRowStethByWsteth } from 'shared/components/data-table-row-steth-by-wsteth'; import { FormatPrice } from 'shared/formatters'; import { useTxCostInUsd } from 'shared/hooks'; + import { RequestFormInputType, useRequestFormData, useValidationResults, } from '../request-form-context'; -import { useWeb3 } from 'reef-knot/web3-react'; -import { AllowanceDataTableRow } from 'shared/components/allowance-data-table-row'; export const TransactionInfo = () => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const { isApprovalFlow, isApprovalFlowLoading, allowance } = useRequestFormData(); const token = useWatch({ name: 'token' }); @@ -55,7 +57,7 @@ export const TransactionInfo = () => { data-testid="allowance" token={token} allowance={allowance} - isBlank={!active} + isBlank={!isDappActive} loading={isApprovalFlowLoading} /> {token === TOKENS.STETH ? ( diff --git a/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts b/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts index 2d88b5683..86bddb7ac 100644 --- a/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts +++ b/features/withdrawals/request/request-form-context/use-request-form-data-context-value.ts @@ -1,14 +1,10 @@ -import { - useSTETHContractRPC, - useSTETHBalance, - useWSTETHBalance, - useContractSWR, -} from '@lido-sdk/react'; +import { useSTETHContractRPC, useContractSWR } from '@lido-sdk/react'; import { useClaimData } from 'features/withdrawals/contexts/claim-data-context'; import { useWithdrawals } from 'features/withdrawals/contexts/withdrawals-context'; import { useUnfinalizedStETH } from 'features/withdrawals/hooks'; import { useCallback, useMemo } from 'react'; import { useWstethBySteth } from 'shared/hooks'; +import { useStethBalance, useWstethBalance } from 'shared/hooks/use-balance'; import { STRATEGY_LAZY } from 'consts/swr-strategies'; // Provides all data fetching for form to function @@ -28,14 +24,14 @@ export const useRequestFormDataContextValue = () => { } = useWithdrawals(); const { data: balanceSteth, - update: stethUpdate, - initialLoading: isStethBalanceLoading, - } = useSTETHBalance(STRATEGY_LAZY); + refetch: stethUpdate, + isLoading: isStethBalanceLoading, + } = useStethBalance(); const { data: balanceWSteth, - update: wstethUpdate, - initialLoading: isWstethBalanceLoading, - } = useWSTETHBalance(STRATEGY_LAZY); + refetch: wstethUpdate, + isLoading: isWstethBalanceLoading, + } = useWstethBalance(); const { data: unfinalizedStETH, update: unfinalizedStETHUpdate, diff --git a/features/withdrawals/request/request-form-context/use-validation-context.ts b/features/withdrawals/request/request-form-context/use-validation-context.ts index af171e763..904523adc 100644 --- a/features/withdrawals/request/request-form-context/use-validation-context.ts +++ b/features/withdrawals/request/request-form-context/use-validation-context.ts @@ -1,23 +1,23 @@ +import { useMemo } from 'react'; import { MAX_REQUESTS_COUNT_LEDGER_LIMIT, MAX_REQUESTS_COUNT, } from 'features/withdrawals/withdrawals-constants'; -import { useMemo } from 'react'; import { useIsLedgerLive } from 'shared/hooks/useIsLedgerLive'; import { useAwaiter } from 'shared/hooks/use-awaiter'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; import type { RequestFormDataType, RequestFormValidationAsyncContextType, RequestFormValidationContextType, } from './types'; -import { useWeb3 } from 'reef-knot/web3-react'; // Prepares validation context object from request form data export const useValidationContext = ( requestData: RequestFormDataType, setIntermediateValidationResults: RequestFormValidationContextType['setIntermediateValidationResults'], ): RequestFormValidationContextType => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const isLedgerLive = useIsLedgerLive(); const maxRequestCount = isLedgerLive ? MAX_REQUESTS_COUNT_LEDGER_LIMIT @@ -68,7 +68,7 @@ export const useValidationContext = ( useAwaiter(context).awaiter; return { - isWalletActive: active, + isWalletActive: isDappActive, asyncContext, setIntermediateValidationResults, }; diff --git a/features/withdrawals/request/withdrawal-rates/use-withdrawal-rates.ts b/features/withdrawals/request/withdrawal-rates/use-withdrawal-rates.ts index 36d9da023..39128bedf 100644 --- a/features/withdrawals/request/withdrawal-rates/use-withdrawal-rates.ts +++ b/features/withdrawals/request/withdrawal-rates/use-withdrawal-rates.ts @@ -20,6 +20,7 @@ import { useConfig } from 'config'; export type useWithdrawalRatesOptions = { fallbackValue?: BigNumber; + isPaused?: boolean; }; const getWithdrawalRates = async ( @@ -52,6 +53,7 @@ const getWithdrawalRates = async ( export const useWithdrawalRates = ({ fallbackValue = Zero, + isPaused, }: useWithdrawalRatesOptions = {}) => { const [token, amount] = useWatch({ name: ['token', 'amount'], @@ -71,6 +73,7 @@ export const useWithdrawalRates = ({ { ...STRATEGY_LAZY, isPaused: () => + isPaused || !debouncedAmount || !debouncedAmount._isBigNumber || enabledDexes.length === 0, diff --git a/features/withdrawals/withdrawals-constants/index.ts b/features/withdrawals/withdrawals-constants/index.ts index 72fd896a5..1a8112fb2 100644 --- a/features/withdrawals/withdrawals-constants/index.ts +++ b/features/withdrawals/withdrawals-constants/index.ts @@ -1,5 +1,6 @@ // max requests count for one tx export const MAX_REQUESTS_COUNT = 256; +// Leger Clear Sign only allows 2 requests per claim export const MAX_REQUESTS_COUNT_LEDGER_LIMIT = 2; export const DEFAULT_CLAIM_REQUEST_SELECTED = 80; diff --git a/features/wsteth/shared/wallet/wallet.tsx b/features/wsteth/shared/wallet/wallet.tsx index 3f9276051..175e670da 100644 --- a/features/wsteth/shared/wallet/wallet.tsx +++ b/features/wsteth/shared/wallet/wallet.tsx @@ -2,15 +2,8 @@ import { memo } from 'react'; import { Divider, Text } from '@lidofinance/lido-ui'; import { TOKENS } from '@lido-sdk/constants'; -import { - useSDK, - useEthereumBalance, - useSTETHBalance, - useWSTETHBalance, - useTokenAddress, -} from '@lido-sdk/react'; +import { useSDK, useTokenAddress } from '@lido-sdk/react'; -import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { FormatToken } from 'shared/formatters'; import { TokenToWallet } from 'shared/components'; import { useWstethBySteth, useStethByWsteth } from 'shared/hooks'; @@ -26,12 +19,17 @@ import { } from 'shared/wallet'; import { StyledCard } from './styles'; +import { + useEthereumBalance, + useStethBalance, + useWstethBalance, +} from 'shared/hooks/use-balance'; const WalletComponent: WalletComponentType = (props) => { const { account } = useSDK(); - const ethBalance = useEthereumBalance(undefined, STRATEGY_LAZY); - const stethBalance = useSTETHBalance(STRATEGY_LAZY); - const wstethBalance = useWSTETHBalance(STRATEGY_LAZY); + const ethBalance = useEthereumBalance(); + const stethBalance = useStethBalance(); + const wstethBalance = useWstethBalance(); const stethAddress = useTokenAddress(TOKENS.STETH); const wstethAddress = useTokenAddress(TOKENS.WSTETH); @@ -44,7 +42,7 @@ const WalletComponent: WalletComponentType = (props) => { { { { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const { maxAmount } = networkData; const validationContextAwaited = useMemo(() => { - if (active && !maxAmount) { + if (isDappActive && !maxAmount) { return undefined; } return { - isWalletActive: active, + isWalletActive: isDappActive, maxAmount, }; - }, [active, maxAmount]); + }, [isDappActive, maxAmount]); return useAwaiter(validationContextAwaited).awaiter; }; diff --git a/features/wsteth/unwrap/hooks/use-unwrap-form-network-data.ts b/features/wsteth/unwrap/hooks/use-unwrap-form-network-data.ts index 284d80061..6bbe9e9a4 100644 --- a/features/wsteth/unwrap/hooks/use-unwrap-form-network-data.ts +++ b/features/wsteth/unwrap/hooks/use-unwrap-form-network-data.ts @@ -1,14 +1,12 @@ import { useCallback, useMemo } from 'react'; import { useIsMultisig } from 'shared/hooks/useIsMultisig'; -import { useSTETHBalance, useWSTETHBalance } from '@lido-sdk/react'; -import { STRATEGY_LAZY } from 'consts/swr-strategies'; +import { useStethBalance, useWstethBalance } from 'shared/hooks/use-balance'; export const useUnwrapFormNetworkData = () => { const { isMultisig } = useIsMultisig(); - const { data: stethBalance, update: stethBalanceUpdate } = - useSTETHBalance(STRATEGY_LAZY); - const { data: wstethBalance, update: wstethBalanceUpdate } = - useWSTETHBalance(STRATEGY_LAZY); + const { data: stethBalance, refetch: stethBalanceUpdate } = useStethBalance(); + const { data: wstethBalance, refetch: wstethBalanceUpdate } = + useWstethBalance(); const revalidateUnwrapFormData = useCallback(async () => { await Promise.allSettled([stethBalanceUpdate(), wstethBalanceUpdate()]); diff --git a/features/wsteth/unwrap/hooks/use-unwrap-form-processing.ts b/features/wsteth/unwrap/hooks/use-unwrap-form-processing.ts index 21450e3f5..9c99fc485 100644 --- a/features/wsteth/unwrap/hooks/use-unwrap-form-processing.ts +++ b/features/wsteth/unwrap/hooks/use-unwrap-form-processing.ts @@ -1,18 +1,25 @@ +import { useCallback } from 'react'; import invariant from 'tiny-invariant'; +import { useAccount } from 'wagmi'; -import { useCallback } from 'react'; -import { useSTETHContractRPC, useWSTETHContractRPC } from '@lido-sdk/react'; -import { useWeb3 } from 'reef-knot/web3-react'; -import { useUnwrapTxProcessing } from './use-unwrap-tx-processing'; -import { useTxModalStagesUnwrap } from './use-tx-modal-stages-unwrap'; +import { + useSDK, + useSTETHContractRPC, + useWSTETHContractRPC, + useWSTETHContractWeb3, +} from '@lido-sdk/react'; +import { useCurrentStaticRpcProvider } from 'shared/hooks/use-current-static-rpc-provider'; import { isContract } from 'utils/isContract'; import { runWithTransactionLogger } from 'utils'; + import type { UnwrapFormInputType } from '../unwrap-form-context'; -import { useCurrentStaticRpcProvider } from 'shared/hooks/use-current-static-rpc-provider'; +import { useTxModalStagesUnwrap } from './use-tx-modal-stages-unwrap'; +import { sendTx } from 'utils/send-tx'; +import { useTxConfirmation } from 'shared/hooks/use-tx-conformation'; type UseUnwrapFormProcessorArgs = { - onConfirm?: () => Promise; + onConfirm: () => Promise; onRetry?: () => void; }; @@ -20,25 +27,43 @@ export const useUnwrapFormProcessor = ({ onConfirm, onRetry, }: UseUnwrapFormProcessorArgs) => { - const { account } = useWeb3(); + const { address } = useAccount(); + const { providerWeb3 } = useSDK(); const { staticRpcProvider } = useCurrentStaticRpcProvider(); - const processWrapTx = useUnwrapTxProcessing(); + const { txModalStages } = useTxModalStagesUnwrap(); const stETHContractRPC = useSTETHContractRPC(); const wstETHContractRPC = useWSTETHContractRPC(); - const { txModalStages } = useTxModalStagesUnwrap(); + const wstethContractWeb3 = useWSTETHContractWeb3(); + const waitForTx = useTxConfirmation(); return useCallback( async ({ amount }: UnwrapFormInputType) => { try { invariant(amount, 'amount should be presented'); - invariant(account, 'address should be presented'); - const isMultisig = await isContract(account, staticRpcProvider); - const willReceive = await wstETHContractRPC.getStETHByWstETH(amount); + invariant(address, 'address should be presented'); + invariant(providerWeb3, 'providerWeb3 must be presented'); + invariant(wstethContractWeb3, 'must have wstethContractWeb3'); + + const [isMultisig, willReceive] = await Promise.all([ + isContract(address, staticRpcProvider), + wstETHContractRPC.getStETHByWstETH(amount), + ]); txModalStages.sign(amount, willReceive); - const txHash = await runWithTransactionLogger('Unwrap signing', () => - processWrapTx({ amount, isMultisig }), + const txHash = await runWithTransactionLogger( + 'Unwrap signing', + async () => { + const tx = + await wstethContractWeb3.populateTransaction.unwrap(amount); + + return sendTx({ + tx, + isMultisig, + staticProvider: staticRpcProvider, + walletProvider: providerWeb3, + }); + }, ); if (isMultisig) { @@ -48,13 +73,15 @@ export const useUnwrapFormProcessor = ({ txModalStages.pending(amount, willReceive, txHash); - await runWithTransactionLogger('Unwrap block confirmation', async () => - staticRpcProvider.waitForTransaction(txHash), + await runWithTransactionLogger('Unwrap block confirmation', () => + waitForTx(txHash), ); - const stethBalance = await stETHContractRPC.balanceOf(account); + const [stethBalance] = await Promise.all([ + stETHContractRPC.balanceOf(address), + onConfirm(), + ]); - await onConfirm?.(); txModalStages.success(stethBalance, txHash); return true; } catch (error: any) { @@ -64,13 +91,15 @@ export const useUnwrapFormProcessor = ({ } }, [ - account, + address, + providerWeb3, + wstethContractWeb3, + staticRpcProvider, wstETHContractRPC, txModalStages, stETHContractRPC, onConfirm, - processWrapTx, - staticRpcProvider, + waitForTx, onRetry, ], ); diff --git a/features/wsteth/unwrap/hooks/use-unwrap-tx-processing.ts b/features/wsteth/unwrap/hooks/use-unwrap-tx-processing.ts deleted file mode 100644 index eb0742eed..000000000 --- a/features/wsteth/unwrap/hooks/use-unwrap-tx-processing.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useCallback } from 'react'; -import invariant from 'tiny-invariant'; - -import { useSDK, useWSTETHContractWeb3 } from '@lido-sdk/react'; - -import { useCurrentStaticRpcProvider } from 'shared/hooks/use-current-static-rpc-provider'; - -import type { UnwrapFormInputType } from '../unwrap-form-context'; -import { sendTx } from 'utils/send-tx'; - -type UnwrapTxProcessorArgs = Omit & { - isMultisig: boolean; -}; - -export const useUnwrapTxProcessing = () => { - const { chainId, providerWeb3 } = useSDK(); - const { staticRpcProvider } = useCurrentStaticRpcProvider(); - const wstethContractWeb3 = useWSTETHContractWeb3(); - - return useCallback( - async ({ isMultisig, amount }: UnwrapTxProcessorArgs) => { - invariant(amount, 'amount id must be presented'); - invariant(chainId, 'chain id must be presented'); - invariant(providerWeb3, 'providerWeb3 must be presented'); - invariant(wstethContractWeb3, 'must have wstethContractWeb3'); - - const tx = await wstethContractWeb3.populateTransaction.unwrap(amount); - - return sendTx({ - tx, - isMultisig, - staticProvider: staticRpcProvider, - walletProvider: providerWeb3, - }); - }, - [chainId, providerWeb3, staticRpcProvider, wstethContractWeb3], - ); -}; diff --git a/features/wsteth/wrap/hooks/use-approve-gas-limit.tsx b/features/wsteth/wrap/hooks/use-approve-gas-limit.tsx index 3a5dbec3f..68f8cc380 100644 --- a/features/wsteth/wrap/hooks/use-approve-gas-limit.tsx +++ b/features/wsteth/wrap/hooks/use-approve-gas-limit.tsx @@ -1,5 +1,5 @@ import { BigNumber } from 'ethers'; -import { useWeb3 } from 'reef-knot/web3-react'; +import { useAccount } from 'wagmi'; import { useLidoSWR, useSTETHContractRPC, @@ -13,7 +13,7 @@ import { STRATEGY_IMMUTABLE } from 'consts/swr-strategies'; export const useApproveGasLimit = () => { const steth = useSTETHContractRPC(); const wsteth = useWSTETHContractRPC(); - const { chainId } = useWeb3(); + const { chainId } = useAccount(); const { data } = useLidoSWR( ['swr:approve-wrap-gas-limit', chainId], diff --git a/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts b/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts index b437f6b75..b0da4caf7 100644 --- a/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts +++ b/features/wsteth/wrap/hooks/use-wrap-form-network-data.ts @@ -1,29 +1,24 @@ import { useCallback, useMemo } from 'react'; -import { - useWSTETHBalance, - useSTETHBalance, - useEthereumBalance, -} from '@lido-sdk/react'; import { config } from 'config'; -import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { useIsMultisig } from 'shared/hooks/useIsMultisig'; import { useTokenMaxAmount } from 'shared/hooks/use-token-max-amount'; import { useMaxGasPrice, useStakingLimitInfo } from 'shared/hooks'; import { useWrapGasLimit } from './use-wrap-gas-limit'; +import { + useEthereumBalance, + useStethBalance, + useWstethBalance, +} from 'shared/hooks/use-balance'; // Provides all data fetching for form to function export const useWrapFormNetworkData = () => { const { isMultisig, isLoading: isMultisigLoading } = useIsMultisig(); - const { data: ethBalance, update: ethBalanceUpdate } = useEthereumBalance( - undefined, - STRATEGY_LAZY, - ); - const { data: stethBalance, update: stethBalanceUpdate } = - useSTETHBalance(STRATEGY_LAZY); - const { data: wstethBalance, update: wstethBalanceUpdate } = - useWSTETHBalance(STRATEGY_LAZY); + const { data: ethBalance, refetch: ethBalanceUpdate } = useEthereumBalance(); + const { data: stethBalance, refetch: stethBalanceUpdate } = useStethBalance(); + const { data: wstethBalance, refetch: wstethBalanceUpdate } = + useWstethBalance(); const { data: stakeLimitInfo, mutate: stakeLimitInfoUpdate } = useStakingLimitInfo(); diff --git a/features/wsteth/wrap/hooks/use-wrap-form-processing.ts b/features/wsteth/wrap/hooks/use-wrap-form-processing.ts index 3c5daa786..ca9c4e423 100644 --- a/features/wsteth/wrap/hooks/use-wrap-form-processing.ts +++ b/features/wsteth/wrap/hooks/use-wrap-form-processing.ts @@ -1,22 +1,24 @@ +import { useCallback } from 'react'; import invariant from 'tiny-invariant'; +import { useAccount } from 'wagmi'; -import { useCallback } from 'react'; -import { useWeb3 } from 'reef-knot/web3-react'; -import { useWrapTxProcessing } from './use-wrap-tx-processing'; -import { useTxModalWrap } from './use-tx-modal-stages-wrap'; -import { useWSTETHContractRPC } from '@lido-sdk/react'; +import { useSDK, useWSTETHContractRPC } from '@lido-sdk/react'; +import { useCurrentStaticRpcProvider } from 'shared/hooks/use-current-static-rpc-provider'; import { runWithTransactionLogger } from 'utils'; import { isContract } from 'utils/isContract'; +import { useTxConfirmation } from 'shared/hooks/use-tx-conformation'; + import type { WrapFormApprovalData, WrapFormInputType, } from '../wrap-form-context'; -import { useCurrentStaticRpcProvider } from 'shared/hooks/use-current-static-rpc-provider'; +import { useWrapTxProcessing } from './use-wrap-tx-processing'; +import { useTxModalWrap } from './use-tx-modal-stages-wrap'; type UseWrapFormProcessorArgs = { approvalData: WrapFormApprovalData; - onConfirm?: () => Promise; + onConfirm: () => Promise; onRetry?: () => void; }; @@ -25,20 +27,27 @@ export const useWrapFormProcessor = ({ onConfirm, onRetry, }: UseWrapFormProcessorArgs) => { - const { account } = useWeb3(); + const { address } = useAccount(); + const { providerWeb3 } = useSDK(); + const { staticRpcProvider } = useCurrentStaticRpcProvider(); + const wstETHContractRPC = useWSTETHContractRPC(); + + const { txModalStages } = useTxModalWrap(); const processWrapTx = useWrapTxProcessing(); + const waitForTx = useTxConfirmation(); const { isApprovalNeededBeforeWrap, processApproveTx } = approvalData; - const { txModalStages } = useTxModalWrap(); - const wstETHContractRPC = useWSTETHContractRPC(); - const { staticRpcProvider } = useCurrentStaticRpcProvider(); return useCallback( async ({ amount, token }: WrapFormInputType) => { try { invariant(amount, 'amount should be presented'); - invariant(account, 'address should be presented'); - const isMultisig = await isContract(account, staticRpcProvider); - const willReceive = await wstETHContractRPC.getWstETHByStETH(amount); + invariant(address, 'address should be presented'); + invariant(providerWeb3, 'providerWeb3 should be presented'); + + const [isMultisig, willReceive] = await Promise.all([ + isContract(address, staticRpcProvider), + wstETHContractRPC.getWstETHByStETH(amount), + ]); if (isApprovalNeededBeforeWrap) { txModalStages.signApproval(amount, token); @@ -70,12 +79,14 @@ export const useWrapFormProcessor = ({ txModalStages.pending(amount, token, willReceive, txHash); await runWithTransactionLogger('Wrap block confirmation', () => - staticRpcProvider.waitForTransaction(txHash), + waitForTx(txHash), ); - const wstethBalance = await wstETHContractRPC.balanceOf(account); + const [wstethBalance] = await Promise.all([ + wstETHContractRPC.balanceOf(address), + onConfirm(), + ]); - await onConfirm?.(); txModalStages.success(wstethBalance, txHash); return true; } catch (error) { @@ -85,14 +96,16 @@ export const useWrapFormProcessor = ({ } }, [ - account, + address, + providerWeb3, + staticRpcProvider, wstETHContractRPC, isApprovalNeededBeforeWrap, txModalStages, onConfirm, processApproveTx, processWrapTx, - staticRpcProvider, + waitForTx, onRetry, ], ); diff --git a/features/wsteth/wrap/hooks/use-wrap-form-validation-context.ts b/features/wsteth/wrap/hooks/use-wrap-form-validation-context.ts index 6ad2763f3..e044da344 100644 --- a/features/wsteth/wrap/hooks/use-wrap-form-validation-context.ts +++ b/features/wsteth/wrap/hooks/use-wrap-form-validation-context.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { useWeb3 } from 'reef-knot/web3-react'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; import { useAwaiter } from 'shared/hooks/use-awaiter'; import type { @@ -15,7 +15,7 @@ type UseWrapFormValidationContextArgs = { export const useWrapFormValidationContext = ({ networkData, }: UseWrapFormValidationContextArgs): WrapFormValidationContext => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const { stakeLimitInfo, ethBalance, @@ -24,7 +24,7 @@ export const useWrapFormValidationContext = ({ wrapEthGasCost, } = networkData; - const waitForAccountData = active + const waitForAccountData = isDappActive ? stethBalance && ethBalance && isMultisig !== undefined : true; @@ -38,7 +38,7 @@ export const useWrapFormValidationContext = ({ useMemo(() => { return isDataReady ? ({ - isWalletActive: active, + isWalletActive: isDappActive, stethBalance, etherBalance: ethBalance, isMultisig, @@ -49,7 +49,7 @@ export const useWrapFormValidationContext = ({ : undefined; }, [ isDataReady, - active, + isDappActive, stethBalance, ethBalance, isMultisig, diff --git a/features/wsteth/wrap/hooks/use-wrap-gas-limit.ts b/features/wsteth/wrap/hooks/use-wrap-gas-limit.ts index 60d44f792..4433fc87e 100644 --- a/features/wsteth/wrap/hooks/use-wrap-gas-limit.ts +++ b/features/wsteth/wrap/hooks/use-wrap-gas-limit.ts @@ -1,4 +1,4 @@ -import { useWeb3 } from 'reef-knot/web3-react'; +import { useAccount } from 'wagmi'; import { useLidoSWR, useWSTETHContractRPC } from '@lido-sdk/react'; import { config } from 'config'; @@ -8,7 +8,7 @@ import { applyGasLimitRatio } from 'utils/apply-gas-limit-ratio'; export const useWrapGasLimit = () => { const wsteth = useWSTETHContractRPC(); - const { chainId } = useWeb3(); + const { chainId } = useAccount(); const { staticRpcProvider } = useCurrentStaticRpcProvider(); const { data } = useLidoSWR( diff --git a/features/wsteth/wrap/hooks/use-wrap-tx-approve.ts b/features/wsteth/wrap/hooks/use-wrap-tx-approve.ts index ada13cc5e..f8fd6396f 100644 --- a/features/wsteth/wrap/hooks/use-wrap-tx-approve.ts +++ b/features/wsteth/wrap/hooks/use-wrap-tx-approve.ts @@ -1,11 +1,13 @@ import { useMemo } from 'react'; -import { useWeb3 } from 'reef-knot/web3-react'; -import { useSDK } from '@lido-sdk/react'; -import { useApprove } from 'shared/hooks/useApprove'; - +import { useAccount } from 'wagmi'; import type { BigNumber } from 'ethers'; + import { getTokenAddress, TOKENS } from '@lido-sdk/constants'; +import { useSDK } from '@lido-sdk/react'; + import { TokensWrappable, TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; +import { useApprove } from 'shared/hooks/useApprove'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; type UseWrapTxApproveArgs = { amount: BigNumber; @@ -13,7 +15,8 @@ type UseWrapTxApproveArgs = { }; export const useWrapTxApprove = ({ amount, token }: UseWrapTxApproveArgs) => { - const { active, account } = useWeb3(); + const { isDappActive } = useDappStatus(); + const { address } = useAccount(); const { chainId } = useSDK(); const [stethTokenAddress, wstethTokenAddress] = useMemo( @@ -28,16 +31,17 @@ export const useWrapTxApprove = ({ amount, token }: UseWrapTxApproveArgs) => { approve: processApproveTx, needsApprove, allowance, - loading: isApprovalLoading, + isLoading: isApprovalLoading, + refetch: refetchAllowance, } = useApprove( amount, stethTokenAddress, wstethTokenAddress, - account ? account : undefined, + address ? address : undefined, ); const isApprovalNeededBeforeWrap = - active && needsApprove && token === TOKENS_TO_WRAP.STETH; + isDappActive && needsApprove && token === TOKENS_TO_WRAP.STETH; return useMemo( () => ({ @@ -46,6 +50,7 @@ export const useWrapTxApprove = ({ amount, token }: UseWrapTxApproveArgs) => { allowance, isApprovalLoading, isApprovalNeededBeforeWrap, + refetchAllowance, }), [ allowance, @@ -53,6 +58,7 @@ export const useWrapTxApprove = ({ amount, token }: UseWrapTxApproveArgs) => { needsApprove, isApprovalLoading, processApproveTx, + refetchAllowance, ], ); }; diff --git a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx index 245582359..b2bf010d4 100644 --- a/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx +++ b/features/wsteth/wrap/wrap-form-context/wrap-form-context.tsx @@ -5,6 +5,7 @@ import { useMemo, createContext, useContext, + useCallback, } from 'react'; import { useForm, FormProvider } from 'react-hook-form'; import { useWrapTxApprove } from '../hooks/use-wrap-tx-approve'; @@ -72,9 +73,16 @@ export const WrapFormProvider: FC = ({ children }) => { const approvalData = useWrapTxApprove({ amount: amount ?? Zero, token }); const isSteth = token === TOKENS_TO_WRAP.STETH; + const onConfirm = useCallback(async () => { + await Promise.allSettled([ + networkData.revalidateWrapFormData(), + approvalData.refetchAllowance(), + ]); + }, [networkData, approvalData]); + const processWrapFormFlow = useWrapFormProcessor({ approvalData, - onConfirm: networkData.revalidateWrapFormData, + onConfirm, onRetry: retryFire, }); diff --git a/features/wsteth/wrap/wrap-form/wrap-stats.tsx b/features/wsteth/wrap/wrap-form/wrap-stats.tsx index 4767eb814..1d26d5e2c 100644 --- a/features/wsteth/wrap/wrap-form/wrap-stats.tsx +++ b/features/wsteth/wrap/wrap-form/wrap-stats.tsx @@ -1,5 +1,4 @@ import { useFormContext } from 'react-hook-form'; -import { useWeb3 } from 'reef-knot/web3-react'; import { parseEther } from '@ethersproject/units'; import { DataTable, DataTableRow } from '@lidofinance/lido-ui'; @@ -11,6 +10,7 @@ import { TOKENS_TO_WRAP } from 'features/wsteth/shared/types'; import { AllowanceDataTableRow } from 'shared/components/allowance-data-table-row'; import { FormatPrice, FormatToken } from 'shared/formatters'; import { useTxCostInUsd, useWstethBySteth } from 'shared/hooks'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; import { useApproveGasLimit } from '../hooks/use-approve-gas-limit'; import { useWrapFormData, WrapFormInputType } from '../wrap-form-context'; @@ -18,7 +18,7 @@ import { useWrapFormData, WrapFormInputType } from '../wrap-form-context'; const oneSteth = parseEther('1'); export const WrapFormStats = () => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const { allowance, wrapGasLimit, isApprovalLoading } = useWrapFormData(); const { watch } = useFormContext(); @@ -82,7 +82,7 @@ export const WrapFormStats = () => { diff --git a/package.json b/package.json index 5ca71f1a9..79c3ddb37 100644 --- a/package.json +++ b/package.json @@ -33,18 +33,19 @@ "@lido-sdk/helpers": "^1.6.0", "@lido-sdk/providers": "^1.4.15", "@lido-sdk/react": "^2.0.6", - "@lidofinance/analytics-matomo": "^0.41.0", - "@lidofinance/api-metrics": "^0.41.0", - "@lidofinance/api-rpc": "^0.41.0", - "@lidofinance/eth-api-providers": "^0.41.0", - "@lidofinance/eth-providers": "^0.41.0", + "@lidofinance/analytics-matomo": "^0.45.1", + "@lidofinance/api-metrics": "^0.45.1", + "@lidofinance/api-rpc": "^0.45.1", + "@lidofinance/eth-api-providers": "^0.45.1", + "@lidofinance/eth-providers": "^0.45.1", + "@lidofinance/lido-ethereum-sdk": "^3.4.0", "@lidofinance/lido-ui": "^3.26.0", - "@lidofinance/next-api-wrapper": "^0.41.0", - "@lidofinance/next-ip-rate-limit": "^0.41.0", - "@lidofinance/next-pages": "^0.41.0", - "@lidofinance/rpc": "^0.41.0", - "@lidofinance/satanizer": "^0.41.0", - "@tanstack/react-query": "^5.48.0", + "@lidofinance/next-api-wrapper": "^0.45.1", + "@lidofinance/next-ip-rate-limit": "^0.45.1", + "@lidofinance/next-pages": "^0.45.1", + "@lidofinance/rpc": "^0.45.1", + "@lidofinance/satanizer": "^0.45.1", + "@tanstack/react-query": "^5.51.21", "bignumber.js": "9.1.0", "copy-to-clipboard": "^3.3.1", "cors": "^2.8.5", @@ -78,7 +79,7 @@ "tiny-async-pool": "^1.2.0", "tiny-invariant": "^1.1.0", "uuid": "^8.3.2", - "viem": "2.13.3", + "viem": "2.18.8", "wagmi": "2.12.2" }, "devDependencies": { diff --git a/pages/withdrawals/[mode].tsx b/pages/withdrawals/[mode].tsx index 3dacb7c42..b4bf20e70 100644 --- a/pages/withdrawals/[mode].tsx +++ b/pages/withdrawals/[mode].tsx @@ -2,15 +2,14 @@ import type { FC } from 'react'; import type { GetStaticPaths } from 'next'; import Head from 'next/head'; -import { Layout } from 'shared/components'; - import { WithdrawalsTabs } from 'features/withdrawals'; import { WithdrawalsProvider } from 'features/withdrawals/contexts/withdrawals-context'; -import { useWeb3Key } from 'shared/hooks/useWeb3Key'; +import { Layout } from 'shared/components'; +import { useWagmiKey } from 'shared/hooks/use-wagmi-key'; import { getDefaultStaticProps } from 'utilsApi/get-default-static-props'; const Withdrawals: FC = ({ mode }) => { - const key = useWeb3Key(); + const key = useWagmiKey(); return ( = ({ mode }) => { - const key = useWeb3Key(); + const key = useWagmiKey(); + return ( > = ({ - - - {children} - - + + + + {children} + + + diff --git a/providers/ipfs-info-box-statuses.tsx b/providers/ipfs-info-box-statuses.tsx index 76823e0d0..c79cc6959 100644 --- a/providers/ipfs-info-box-statuses.tsx +++ b/providers/ipfs-info-box-statuses.tsx @@ -9,6 +9,7 @@ import { import { useLidoSWR, useLocalStorage, useSDK } from '@lido-sdk/react'; import invariant from 'tiny-invariant'; +import { config } from 'config'; import { useRpcUrl } from 'config/rpc'; import { SETTINGS_PATH } from 'consts/urls'; import { STRATEGY_LAZY } from 'consts/swr-strategies'; @@ -56,7 +57,7 @@ export const IPFSInfoBoxStatusesProvider: FC = ({ const { data: isRPCAvailableRaw, initialLoading: isLoading } = useLidoSWR( `rpc-url-check-${rpcUrl}-${chainId}`, async () => await checkRpcUrl(rpcUrl, chainId), - STRATEGY_LAZY, + { ...STRATEGY_LAZY, isPaused: () => !config.ipfsMode }, ); const isRPCAvailable = isRPCAvailableRaw === true; diff --git a/providers/lido-sdk.tsx b/providers/lido-sdk.tsx new file mode 100644 index 000000000..09ebaca09 --- /dev/null +++ b/providers/lido-sdk.tsx @@ -0,0 +1,54 @@ +import { createContext, useContext, useMemo } from 'react'; +import { LidoSDKCore } from '@lidofinance/lido-ethereum-sdk/core'; +import { + LidoSDKstETH, + LidoSDKwstETH, +} from '@lidofinance/lido-ethereum-sdk/erc20'; +import invariant from 'tiny-invariant'; +import { useChainId, useClient, useConnectorClient } from 'wagmi'; +import { useTokenTransferSubscription } from 'shared/hooks/use-balance'; +import { useGetRpcUrlByChainId } from 'config/rpc'; + +type LidoSDKContextValue = { + core: LidoSDKCore; + steth: LidoSDKstETH; + wsteth: LidoSDKwstETH; + subscribeToTokenUpdates: ReturnType; +}; + +const LidoSDKContext = createContext(null); +LidoSDKContext.displayName = 'LidoSDKContext'; + +export const useLidoSDK = () => { + const value = useContext(LidoSDKContext); + invariant(value, 'useLidoSDK was used outside of LidoSDKProvider'); + return value; +}; + +export const LidoSDKProvider = ({ children }: React.PropsWithChildren) => { + const subscribe = useTokenTransferSubscription(); + const publicClient = useClient(); + const chainId = useChainId(); + const getRpcUrl = useGetRpcUrlByChainId(); + const fallbackRpcUrl = !publicClient ? getRpcUrl(chainId) : undefined; + const { data: walletClient } = useConnectorClient(); + + const sdk = useMemo(() => { + const core = new LidoSDKCore({ + chainId, + logMode: 'none', + rpcProvider: publicClient as any, + web3Provider: walletClient as any, + // viem client can be unavailable on ipfs+dev first renders + rpcUrls: !publicClient && fallbackRpcUrl ? [fallbackRpcUrl] : undefined, + }); + + const steth = new LidoSDKstETH({ core }); + const wsteth = new LidoSDKwstETH({ core }); + + return { core, steth, wsteth, subscribeToTokenUpdates: subscribe }; + }, [chainId, fallbackRpcUrl, publicClient, subscribe, walletClient]); + return ( + {children} + ); +}; diff --git a/providers/sdk-legacy.tsx b/providers/sdk-legacy.tsx index f01e86d86..a42f693dd 100644 --- a/providers/sdk-legacy.tsx +++ b/providers/sdk-legacy.tsx @@ -1,13 +1,15 @@ import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'; -import { useSupportedChains, useWeb3 } from 'reef-knot/web3-react'; -import { useClient, useConfig } from 'wagmi'; +import { useReefKnotContext } from 'reef-knot/core-react'; +// TODO: to remove the 'reef-knot/web3-react' after it will be deprecated +import { useSupportedChains } from 'reef-knot/web3-react'; +import { useAccount, useClient, useConfig } from 'wagmi'; +import { mainnet } from 'wagmi/chains'; import { Web3Provider } from '@ethersproject/providers'; import { ProviderSDK } from '@lido-sdk/react'; - -import { mainnet } from 'wagmi/chains'; import { getStaticRpcBatchProvider } from '@lido-sdk/providers'; -import { useReefKnotContext } from 'reef-knot/core-react'; + +import { useDappStatus } from 'shared/hooks/use-dapp-status'; type SDKLegacyProviderProps = PropsWithChildren<{ defaultChainId: number; @@ -19,8 +21,9 @@ export const SDKLegacyProvider = ({ defaultChainId, pollingInterval, }: SDKLegacyProviderProps) => { - const { chainId: web3ChainId = defaultChainId, account, active } = useWeb3(); + const { chainId: wagmiChainId = defaultChainId, address } = useAccount(); const { supportedChains } = useSupportedChains(); + const { isDappActive } = useDappStatus(); const config = useConfig(); const client = useClient(); const { rpc } = useReefKnotContext(); @@ -40,7 +43,7 @@ export const SDKLegacyProvider = ({ }; const getProviderValue = async () => { - if (!client || !account || !active) return undefined; + if (!client || !address || !isDappActive) return undefined; const { chain } = client; const providerTransport = await getProviderTransport(); @@ -65,7 +68,7 @@ export const SDKLegacyProvider = ({ return () => { isHookMounted = false; }; - }, [config, config.state, client, account, active, pollingInterval]); + }, [config, config.state, client, address, isDappActive, pollingInterval]); const supportedChainIds = useMemo( () => supportedChains.map((chain) => chain.chainId), @@ -73,10 +76,10 @@ export const SDKLegacyProvider = ({ ); const chainId = useMemo(() => { - return supportedChainIds.indexOf(web3ChainId) > -1 - ? web3ChainId + return supportedChainIds.indexOf(wagmiChainId) > -1 + ? wagmiChainId : defaultChainId; - }, [defaultChainId, supportedChainIds, web3ChainId]); + }, [defaultChainId, supportedChainIds, wagmiChainId]); const providerRpc = useMemo( () => getStaticRpcBatchProvider(chainId, rpc[chainId], 0, pollingInterval), @@ -102,7 +105,7 @@ export const SDKLegacyProvider = ({ providerWeb3={providerWeb3} providerRpc={providerRpc} providerMainnetRpc={providerMainnetRpc} - account={account ?? undefined} + account={address ?? undefined} > {children} diff --git a/providers/web3.tsx b/providers/web3.tsx index c13000bae..a7abd995f 100644 --- a/providers/web3.tsx +++ b/providers/web3.tsx @@ -1,6 +1,6 @@ -import { FC, PropsWithChildren, useMemo } from 'react'; +import { FC, PropsWithChildren, useEffect, useMemo } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { http, WagmiProvider, createConfig } from 'wagmi'; +import { WagmiProvider, createConfig, useConnections } from 'wagmi'; import * as wagmiChains from 'wagmi/chains'; import { AutoConnect, @@ -16,12 +16,19 @@ import { CHAINS } from 'consts/chains'; import { ConnectWalletModal } from 'shared/wallet/connect-wallet-modal'; import { SDKLegacyProvider } from './sdk-legacy'; +import { useWeb3Transport } from 'utils/use-web3-transport'; type ChainsList = [wagmiChains.Chain, ...wagmiChains.Chain[]]; const wagmiChainsArray = Object.values(wagmiChains) as any as ChainsList; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, + }, +}); const Web3Provider: FC = ({ children }) => { const { @@ -63,32 +70,39 @@ const Web3Provider: FC = ({ children }) => { return getWalletsDataList({ walletsList: WalletsListEthereum, rpc: backendRPC, - walletconnectProjectId: walletconnectProjectId, - defaultChain: defaultChain, + walletconnectProjectId, + defaultChain, }); }, [backendRPC, defaultChain, walletconnectProjectId]); + const { transportMap, onActiveConnection } = useWeb3Transport( + supportedChains, + backendRPC, + ); + const wagmiConfig = useMemo(() => { return createConfig({ chains: supportedChains, ssr: true, + connectors: [], + batch: { - // eth_call's will be batched via multicall contract every 100ms + // eth_call's can be batched via multicall contract multicall: { - wait: 100, + wait: config.PROVIDER_BATCH_TIME, }, }, multiInjectedProviderDiscovery: false, pollingInterval: config.PROVIDER_POLLING_INTERVAL, - transports: supportedChains.reduce( - (res, curr) => ({ - ...res, - [curr.id]: http(backendRPC[curr.id], { batch: true }), - }), - {}, - ), + transports: transportMap, }); - }, [supportedChains, backendRPC]); + }, [supportedChains, transportMap]); + + const [activeConnection] = useConnections({ config: wagmiConfig }); + + useEffect(() => { + void onActiveConnection(activeConnection ?? null); + }, [activeConnection, onActiveConnection]); return ( // default wagmi autoConnect, MUST be false in our case, because we use custom autoConnect from Reef Knot diff --git a/shared/hook-form/form-controller/form-controller.tsx b/shared/hook-form/form-controller/form-controller.tsx index e06b24f60..b1a799249 100644 --- a/shared/hook-form/form-controller/form-controller.tsx +++ b/shared/hook-form/form-controller/form-controller.tsx @@ -1,6 +1,8 @@ import { FC, PropsWithChildren, useEffect, useMemo } from 'react'; -import { useWeb3 } from 'reef-knot/web3-react'; import { useFormContext } from 'react-hook-form'; + +import { useDappStatus } from 'shared/hooks/use-dapp-status'; + import { useFormControllerContext } from './form-controller-context'; type FormControllerProps = React.ComponentProps<'form'>; @@ -9,7 +11,7 @@ export const FormController: FC> = ({ children, ...props }) => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const { handleSubmit, reset: resetDefault } = useFormContext(); const { onSubmit, @@ -34,11 +36,11 @@ export const FormController: FC> = ({ // Reset form amount after disconnect wallet useEffect(() => { - if (!active) resetDefault(); + if (!isDappActive) resetDefault(); // reset will be captured when active changes // so we don't need it in deps // eslint-disable-next-line react-hooks/exhaustive-deps - }, [active]); + }, [isDappActive]); return (
diff --git a/shared/hooks/use-allowance.ts b/shared/hooks/use-allowance.ts new file mode 100644 index 000000000..63061d661 --- /dev/null +++ b/shared/hooks/use-allowance.ts @@ -0,0 +1,129 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { BigNumber } from 'ethers'; +import { useCallback, useMemo } from 'react'; +import { Address, WatchContractEventOnLogsFn } from 'viem'; +import { useReadContract, useWatchContractEvent } from 'wagmi'; + +const nativeToBN = (data: bigint) => BigNumber.from(data.toString()); + +const Erc20AllowanceAbi = [ + { + type: 'event', + name: 'Approval', + inputs: [ + { indexed: true, name: 'owner', type: 'address' }, + { indexed: true, name: 'spender', type: 'address' }, + { indexed: false, name: 'value', type: 'uint256' }, + ], + }, + { + type: 'event', + name: 'Transfer', + inputs: [ + { indexed: true, name: 'from', type: 'address' }, + { indexed: true, name: 'to', type: 'address' }, + { indexed: false, name: 'value', type: 'uint256' }, + ], + }, + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address', + }, + { + name: '_spender', + type: 'address', + }, + ], + name: 'allowance', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, +] as const; + +type OnLogsFn = WatchContractEventOnLogsFn< + typeof Erc20AllowanceAbi, + 'Transfer' | 'Approval', + true +>; + +type UseAllowanceProps = { + token: Address; + account: Address; + spender: Address; +}; + +export const useAllowance = ({ + token, + account, + spender, +}: UseAllowanceProps) => { + const queryClient = useQueryClient(); + const enabled = !!(token && account && spender); + + const allowanceQuery = useReadContract({ + abi: Erc20AllowanceAbi, + address: token, + functionName: 'allowance', + args: [account, spender], + query: { enabled, select: nativeToBN }, + }); + + const onLogs: OnLogsFn = useCallback( + () => { + void queryClient.invalidateQueries( + { + queryKey: allowanceQuery.queryKey, + }, + { cancelRefetch: false }, + ); + }, + // queryKey is unstable + // eslint-disable-next-line react-hooks/exhaustive-deps + [account, spender, token], + ); + + useWatchContractEvent({ + abi: Erc20AllowanceAbi, + eventName: 'Approval', + batch: true, + poll: true, + args: useMemo( + () => ({ + owner: account, + spender, + }), + [account, spender], + ), + address: token, + enabled, + onLogs, + }); + + useWatchContractEvent({ + abi: Erc20AllowanceAbi, + eventName: 'Transfer', + batch: false, + poll: true, + args: useMemo( + () => ({ + from: account, + }), + [account], + ), + address: token, + enabled, + onLogs, + }); + + return allowanceQuery; +}; diff --git a/shared/hooks/use-balance.ts b/shared/hooks/use-balance.ts new file mode 100644 index 000000000..3eabdd4b3 --- /dev/null +++ b/shared/hooks/use-balance.ts @@ -0,0 +1,287 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { QueryKey, useQuery, useQueryClient } from '@tanstack/react-query'; +import { BigNumber } from 'ethers'; +import { useLidoSDK } from 'providers/lido-sdk'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + useBlockNumber, + useBalance, + useAccount, + useReadContract, + useWatchContractEvent, +} from 'wagmi'; + +import type { AbstractLidoSDKErc20 } from '@lidofinance/lido-ethereum-sdk/erc20'; +import type { GetBalanceData } from 'wagmi/query'; +import type { Address, WatchContractEventOnLogsFn } from 'viem'; +import { config } from 'config'; + +const nativeToBN = (data: bigint) => BigNumber.from(data.toString()); + +const balanceToBN = (data: GetBalanceData) => nativeToBN(data.value); + +export const useEthereumBalance = () => { + const queryClient = useQueryClient(); + const { address } = useAccount(); + const { data: blockNumber } = useBlockNumber({ + watch: { + poll: true, + pollingInterval: config.PROVIDER_POLLING_INTERVAL, + enabled: !!address, + }, + cacheTime: config.PROVIDER_POLLING_INTERVAL, + }); + + const queryData = useBalance({ + address, + query: { + select: balanceToBN, + // because we subscribe to block + staleTime: Infinity, + enabled: !!address, + }, + }); + + useEffect(() => { + void queryClient.invalidateQueries( + { queryKey: queryData.queryKey }, + // this tells RQ to not force another refetch if this query is already revalidating + // dedups rpc requests + { cancelRefetch: false }, + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [blockNumber]); + + return queryData; +}; + +type TokenContract = Awaited< + ReturnType['getContract']> +>; + +type TokenSubscriptionState = Record< + Address, + { + subscribers: number; + queryKey: QueryKey; + } +>; + +type SubscribeArgs = { + tokenAddress: Address; + queryKey: QueryKey; +}; + +type UseBalanceProps = { + account?: Address; + shouldSubscribeToUpdates?: boolean; +}; + +export const Erc20EventsAbi = [ + { + type: 'event', + name: 'Transfer', + inputs: [ + { indexed: true, name: 'from', type: 'address' }, + { indexed: true, name: 'to', type: 'address' }, + { indexed: false, name: 'value', type: 'uint256' }, + ], + }, +] as const; + +type OnLogsFn = WatchContractEventOnLogsFn< + typeof Erc20EventsAbi, + 'Transfer', + true +>; + +export const useTokenTransferSubscription = () => { + const { address } = useAccount(); + const queryClient = useQueryClient(); + const [subscriptions, setSubscriptions] = useState( + {}, + ); + + const tokens = useMemo( + () => Object.keys(subscriptions) as Address[], + [subscriptions], + ); + + const onLogs: OnLogsFn = useCallback( + (logs) => { + for (const log of logs) { + const subscription = + subscriptions[log.address.toLowerCase() as Address]; + if (!subscription) continue; + // we could optimistically update balance data + // but it's easier to refetch balance after transfer + void queryClient.invalidateQueries( + { + queryKey: subscription.queryKey, + }, + { cancelRefetch: false }, + ); + } + }, + [queryClient, subscriptions], + ); + + const shouldWatch = address && tokens.length > 0; + + useWatchContractEvent({ + abi: Erc20EventsAbi, + eventName: 'Transfer', + args: useMemo( + () => ({ + to: address, + }), + [address], + ), + address: tokens, + enabled: shouldWatch, + onLogs, + }); + + useWatchContractEvent({ + abi: Erc20EventsAbi, + eventName: 'Transfer', + args: useMemo( + () => ({ + from: address, + }), + [address], + ), + address: tokens, + enabled: shouldWatch, + onLogs, + }); + + const subscribe = useCallback( + ({ tokenAddress: _tokenAddress, queryKey }: SubscribeArgs) => { + const tokenAddress = _tokenAddress.toLowerCase() as Address; + setSubscriptions((old) => { + const existing = old[tokenAddress]; + return { + ...old, + [tokenAddress]: { + queryKey, + subscribers: existing?.subscribers ?? 0 + 1, + }, + }; + }); + + // returns unsubscribe to be used as useEffect return fn (for unmount) + return () => { + setSubscriptions((old) => { + const existing = old[tokenAddress]; + if (!existing) return old; + if (existing.subscribers > 1) { + return { + ...old, + [tokenAddress]: { + ...existing, + subscribers: existing.subscribers - 1, + }, + }; + } else { + delete old[tokenAddress]; + return { ...old }; + } + }); + }; + }, + [], + ); + + return subscribe; +}; + +// NB: contract can be undefined but for better wagmi typings is casted as NoNNullable +const useTokenBalance = ( + contract: TokenContract, + address?: Address, + shouldSubscribe = true, +) => { + const { subscribeToTokenUpdates } = useLidoSDK(); + + const balanceQuery = useReadContract({ + abi: contract?.abi, + address: contract?.address, + functionName: 'balanceOf', + args: address && [address], + query: { + enabled: !!address, + select: nativeToBN, + // because we update on events we can have high staleTime + // this prevents loader when changing pages + staleTime: 30_000, + }, + }); + + useEffect(() => { + if (shouldSubscribe && address && contract?.address) { + return subscribeToTokenUpdates({ + tokenAddress: contract.address, + queryKey: balanceQuery.queryKey, + }); + } + // queryKey causes rerender + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [address, contract?.address]); + + return balanceQuery; +}; + +export const useStethBalance = ({ + account, + shouldSubscribeToUpdates = true, +}: UseBalanceProps = {}) => { + const { address } = useAccount(); + const mergedAccount = account ?? address; + + const { steth, core } = useLidoSDK(); + + const { data: contract, isLoading } = useQuery({ + queryKey: ['steth-contract', core.chainId], + enabled: !!mergedAccount, + + staleTime: Infinity, + queryFn: async () => steth.getContract(), + }); + + const balanceData = useTokenBalance( + contract!, + mergedAccount, + shouldSubscribeToUpdates, + ); + + return { ...balanceData, isLoading: isLoading || balanceData.isLoading }; +}; + +export const useWstethBalance = ({ + account, + shouldSubscribeToUpdates = true, +}: UseBalanceProps = {}) => { + const { address } = useAccount(); + const mergedAccount = account ?? address; + + const { wsteth, core } = useLidoSDK(); + + const { data: contract, isLoading } = useQuery({ + queryKey: ['wsteth-contract', core.chainId], + enabled: !!mergedAccount, + staleTime: Infinity, + queryFn: async () => wsteth.getContract(), + }); + + const balanceData = useTokenBalance( + contract!, + mergedAccount, + shouldSubscribeToUpdates, + ); + + return { ...balanceData, isLoading: isLoading || balanceData.isLoading }; +}; diff --git a/shared/hooks/use-current-static-rpc-provider.ts b/shared/hooks/use-current-static-rpc-provider.ts index d426728c1..31c82797f 100644 --- a/shared/hooks/use-current-static-rpc-provider.ts +++ b/shared/hooks/use-current-static-rpc-provider.ts @@ -5,6 +5,7 @@ import { getStaticRpcBatchProvider } from '@lido-sdk/providers'; import { StaticJsonRpcBatchProvider } from '@lidofinance/eth-providers'; import { useRpcUrl } from 'config/rpc'; +import { config } from 'config'; export const useCurrentStaticRpcProvider = (): { staticRpcProvider: StaticJsonRpcBatchProvider; @@ -14,7 +15,11 @@ export const useCurrentStaticRpcProvider = (): { const rpcUrl = useRpcUrl(); const staticRpcProvider = useMemo(() => { - return getStaticRpcBatchProvider(chainId, rpcUrl); + return getStaticRpcBatchProvider( + chainId, + rpcUrl, + config.PROVIDER_POLLING_INTERVAL, + ); }, [chainId, rpcUrl]); return { diff --git a/shared/hooks/use-dapp-status.ts b/shared/hooks/use-dapp-status.ts index 2ddc41cb1..b4f6909b3 100644 --- a/shared/hooks/use-dapp-status.ts +++ b/shared/hooks/use-dapp-status.ts @@ -4,14 +4,19 @@ import { useAccount } from 'wagmi'; import { LIDO_MULTICHAIN_CHAINS } from 'consts/chains'; import { useIsSupportedChain } from './use-is-supported-chain'; +import { useConfig } from 'config'; export const useDappStatus = () => { + const { multiChainBanner } = useConfig().externalConfig; const { chainId, isConnected: isWalletConnected } = useAccount(); const isSupportedChain = useIsSupportedChain(); const isLidoMultichainChain = useMemo( - () => !!chainId && !!LIDO_MULTICHAIN_CHAINS[chainId], - [chainId], + () => + !!chainId && + !!LIDO_MULTICHAIN_CHAINS[chainId] && + multiChainBanner.includes(chainId), + [chainId, multiChainBanner], ); const isDappActive = useMemo(() => { diff --git a/shared/hooks/use-mainnet-static-rpc-provider.ts b/shared/hooks/use-mainnet-static-rpc-provider.ts index 4da4fe368..674a67e59 100644 --- a/shared/hooks/use-mainnet-static-rpc-provider.ts +++ b/shared/hooks/use-mainnet-static-rpc-provider.ts @@ -4,10 +4,15 @@ import { StaticJsonRpcBatchProvider } from '@lidofinance/eth-providers'; import { useGetRpcUrlByChainId } from 'config/rpc'; import { CHAINS } from 'consts/chains'; +import { config } from 'config'; export const useMainnetStaticRpcProvider = (): StaticJsonRpcBatchProvider => { const getRpcUrl = useGetRpcUrlByChainId(); return useMemo(() => { - return getStaticRpcBatchProvider(1, getRpcUrl(CHAINS.Mainnet)); + return getStaticRpcBatchProvider( + CHAINS.Mainnet, + getRpcUrl(CHAINS.Mainnet), + config.PROVIDER_POLLING_INTERVAL, + ); }, [getRpcUrl]); }; diff --git a/shared/hooks/use-staking-limit-warning.ts b/shared/hooks/use-staking-limit-warning.ts index 35f07abb5..f0f8d59b9 100644 --- a/shared/hooks/use-staking-limit-warning.ts +++ b/shared/hooks/use-staking-limit-warning.ts @@ -1,15 +1,15 @@ -import { useWeb3 } from 'reef-knot/web3-react'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; import { LIMIT_LEVEL } from 'types'; export const useStakingLimitWarning = (stakingLimitLevel?: LIMIT_LEVEL) => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const limitWarning = - stakingLimitLevel === LIMIT_LEVEL.WARN && active + stakingLimitLevel === LIMIT_LEVEL.WARN && isDappActive ? 'Stake limit is almost exhausted. Your transaction may not go through.' : null; const limitError = - stakingLimitLevel === LIMIT_LEVEL.REACHED && active + stakingLimitLevel === LIMIT_LEVEL.REACHED && isDappActive ? 'Stake limit is exhausted. Please wait until the limit is restored.' : null; diff --git a/shared/hooks/use-tx-conformation.ts b/shared/hooks/use-tx-conformation.ts new file mode 100644 index 000000000..7560046ef --- /dev/null +++ b/shared/hooks/use-tx-conformation.ts @@ -0,0 +1,19 @@ +import { useCallback } from 'react'; +import type { Hash } from 'viem'; +import { waitForTransactionReceipt } from 'viem/actions'; +import { useClient } from 'wagmi'; + +// helper hook until migration to wagmi is complete +// awaits TX trough wagmi transport to allow sync with balance hooks +export const useTxConfirmation = () => { + const client = useClient(); + return useCallback( + (hash: string) => { + return waitForTransactionReceipt(client as any, { + confirmations: 1, + hash: hash as Hash, + }); + }, + [client], + ); +}; diff --git a/shared/hooks/use-wagmi-key.ts b/shared/hooks/use-wagmi-key.ts new file mode 100644 index 000000000..f9c07ce6c --- /dev/null +++ b/shared/hooks/use-wagmi-key.ts @@ -0,0 +1,9 @@ +import { useAccount } from 'wagmi'; +import { config } from 'config'; + +// In order to simplify side effects of switching wallets/chains +// we can remount by this key, resetting all internal states +export const useWagmiKey = () => { + const { address, chainId } = useAccount(); + return `${address ?? 'NO_ADDRESS'}_${chainId ?? config.defaultChain}`; +}; diff --git a/shared/hooks/useApprove.ts b/shared/hooks/useApprove.ts index 4c962f330..3d26c6043 100644 --- a/shared/hooks/useApprove.ts +++ b/shared/hooks/useApprove.ts @@ -1,34 +1,33 @@ import invariant from 'tiny-invariant'; import { useCallback } from 'react'; -import type { ContractReceipt } from '@ethersproject/contracts'; import { BigNumber } from '@ethersproject/bignumber'; import { getERC20Contract } from '@lido-sdk/contracts'; -import { useAllowance, useSDK } from '@lido-sdk/react'; +import { useSDK } from '@lido-sdk/react'; import { isContract } from 'utils/isContract'; import { runWithTransactionLogger } from 'utils'; import { useCurrentStaticRpcProvider } from './use-current-static-rpc-provider'; -import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { sendTx } from 'utils/send-tx'; +import { useAllowance } from './use-allowance'; +import { useTxConfirmation } from './use-tx-conformation'; + +import type { Address, TransactionReceipt } from 'viem'; type ApproveOptions = | { onTxStart?: () => void | Promise; onTxSent?: (tx: string) => void | Promise; - onTxAwaited?: (tx: ContractReceipt) => void | Promise; + onTxAwaited?: (tx: TransactionReceipt) => void | Promise; } | undefined; export type UseApproveResponse = { approve: (options?: ApproveOptions) => Promise; + allowance: ReturnType['data']; needsApprove: boolean; - initialLoading: boolean; - allowance: BigNumber | undefined; - loading: boolean; - error: unknown; -}; +} & ReturnType; export const useApprove = ( amount: BigNumber, @@ -36,24 +35,27 @@ export const useApprove = ( spender: string, owner?: string, ): UseApproveResponse => { - const { providerWeb3, account, chainId } = useSDK(); + const { providerWeb3, account } = useSDK(); const { staticRpcProvider } = useCurrentStaticRpcProvider(); + const waitForTx = useTxConfirmation(); const mergedOwner = owner ?? account; invariant(token != null, 'Token is required'); invariant(spender != null, 'Spender is required'); - const result = useAllowance(token, spender, mergedOwner, STRATEGY_LAZY); - const { data: allowance, initialLoading, update: updateAllowance } = result; + const allowanceQuery = useAllowance({ + token: token as Address, + account: mergedOwner as Address, + spender: spender as Address, + }); const needsApprove = Boolean( - !initialLoading && allowance && !amount.isZero() && amount.gt(allowance), + allowanceQuery.data && !amount.isZero() && amount.gt(allowanceQuery.data), ); const approve = useCallback( async ({ onTxStart, onTxSent, onTxAwaited } = {}) => { invariant(providerWeb3 != null, 'Web3 provider is required'); - invariant(chainId, 'chain id is required'); invariant(account, 'account is required'); await onTxStart?.(); const contractWeb3 = getERC20Contract(token, providerWeb3.getSigner()); @@ -81,44 +83,31 @@ export const useApprove = ( if (!isMultisig) { const receipt = await runWithTransactionLogger( 'Approve block confirmation', - () => staticRpcProvider.waitForTransaction(approveTxHash), + () => waitForTx(approveTxHash), ); await onTxAwaited?.(receipt); } - await updateAllowance(); + await allowanceQuery.refetch(); return approveTxHash; }, [ - chainId, + providerWeb3, account, token, - updateAllowance, + staticRpcProvider, + allowanceQuery, spender, amount, - staticRpcProvider, - providerWeb3, + waitForTx, ], ); return { approve, needsApprove, - - allowance, - initialLoading, - - /* - * support dependency collection - * https://swr.vercel.app/advanced/performance#dependency-collection - */ - - get loading() { - return result.loading; - }, - get error() { - return result.error; - }, + allowance: allowanceQuery.data, + ...allowanceQuery, }; }; diff --git a/shared/hooks/useERC20PermitSignature.ts b/shared/hooks/useERC20PermitSignature.ts index 4ed13bdfe..cd4825f61 100644 --- a/shared/hooks/useERC20PermitSignature.ts +++ b/shared/hooks/useERC20PermitSignature.ts @@ -1,14 +1,12 @@ import { useCallback } from 'react'; +import { BigNumber, TypedDataDomain } from 'ethers'; import invariant from 'tiny-invariant'; +import { useAccount } from 'wagmi'; import { hexValue, splitSignature } from '@ethersproject/bytes'; import { MaxUint256 } from '@ethersproject/constants'; -import { BigNumber, TypedDataDomain } from 'ethers'; - import { useSDK } from '@lido-sdk/react'; - import { Erc20Abi, StethAbi } from '@lido-sdk/contracts'; -import { useWeb3 } from 'reef-knot/web3-react'; export type GatherPermitSignatureResult = { v: number; @@ -57,13 +55,13 @@ export const useERC20PermitSignature = < tokenProvider, spender, }: UseERC20PermitSignatureProps): UseERC20PermitSignatureResult => { - const { chainId, account } = useWeb3(); + const { address, chainId } = useAccount(); const { providerWeb3 } = useSDK(); const gatherPermitSignature = useCallback( async (amount: BigNumber) => { invariant(chainId, 'chainId is needed'); - invariant(account, 'account is needed'); + invariant(address, 'account is needed'); invariant(providerWeb3, 'providerWeb3 is needed'); invariant(tokenProvider, 'tokenProvider is needed'); @@ -86,10 +84,10 @@ export const useERC20PermitSignature = < verifyingContract: tokenProvider.address, }; } - const nonce = await tokenProvider.nonces(account); + const nonce = await tokenProvider.nonces(address); const message = { - owner: account, + owner: address, spender, value: amount.toString(), nonce: hexValue(nonce), @@ -113,12 +111,12 @@ export const useERC20PermitSignature = < deadline, chainId: chainId, nonce: message.nonce, - owner: account, + owner: address, spender, }; }); }, - [chainId, account, providerWeb3, tokenProvider, spender], + [chainId, address, providerWeb3, tokenProvider, spender], ); return { gatherPermitSignature }; diff --git a/shared/hooks/useIsMultisig.ts b/shared/hooks/useIsMultisig.ts index 5a15936b6..d4cd13374 100644 --- a/shared/hooks/useIsMultisig.ts +++ b/shared/hooks/useIsMultisig.ts @@ -1,10 +1,10 @@ -import { useWeb3 } from 'reef-knot/web3-react'; +import { useAccount } from 'wagmi'; import { useIsContract } from 'shared/hooks'; export const useIsMultisig = () => { - const { account } = useWeb3(); + const { address } = useAccount(); const { isContract: isMultisig, isLoading } = useIsContract( - account ?? undefined, + address ?? undefined, ); return { isMultisig, isLoading }; }; diff --git a/shared/hooks/useWeb3Key.ts b/shared/hooks/useWeb3Key.ts deleted file mode 100644 index 074a47f67..000000000 --- a/shared/hooks/useWeb3Key.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useWeb3 } from 'reef-knot/web3-react'; - -import { config } from 'config'; - -// In order to simplify side effects of switching wallets/chains -// we can remount by this key, resetting all internal states -export const useWeb3Key = () => { - const { account, chainId } = useWeb3(); - return `${account ?? 'NO_ACCOUNT'}_${chainId ?? config.defaultChain}`; -}; diff --git a/shared/transaction-modal/hooks/use-transaction-modal-stage.tsx b/shared/transaction-modal/hooks/use-transaction-modal-stage.tsx index 4403fb2ca..3a6f50da5 100644 --- a/shared/transaction-modal/hooks/use-transaction-modal-stage.tsx +++ b/shared/transaction-modal/hooks/use-transaction-modal-stage.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef } from 'react'; -import { useWeb3 } from 'reef-knot/web3-react'; import { useModalActions } from 'providers/modal-provider'; +import { useDappStatus } from 'shared/hooks/use-dapp-status'; import { useTransactionModal, TransactionModal } from '../transaction-modal'; export type TransactionModalTransitStage = ( @@ -12,7 +12,7 @@ export type TransactionModalTransitStage = ( export const useTransactionModalStage = >( getStages: (transitStage: TransactionModalTransitStage) => S, ) => { - const { active } = useWeb3(); + const { isDappActive } = useDappStatus(); const { openModal } = useTransactionModal(); const { closeModal } = useModalActions(); const isMountedRef = useRef(true); @@ -39,10 +39,10 @@ export const useTransactionModalStage = >( }, []); useEffect(() => { - if (!active) { + if (!isDappActive) { closeModal(TransactionModal); } - }, [active, closeModal]); + }, [isDappActive, closeModal]); return { txModalStages, diff --git a/shared/wallet/button/button.tsx b/shared/wallet/button/button.tsx index 17f80c380..7d6e9a451 100644 --- a/shared/wallet/button/button.tsx +++ b/shared/wallet/button/button.tsx @@ -1,9 +1,7 @@ import { FC } from 'react'; import { useAccount } from 'wagmi'; import { ButtonProps, useBreakpoint } from '@lidofinance/lido-ui'; -import { useEthereumBalance } from '@lido-sdk/react'; -import { STRATEGY_LAZY } from 'consts/swr-strategies'; import { FormatToken } from 'shared/formatters'; import { useDappStatus } from 'shared/hooks/use-dapp-status'; @@ -16,6 +14,7 @@ import { WalledButtonBalanceStyle, WalledButtonLoaderStyle, } from './styles'; +import { useEthereumBalance } from 'shared/hooks/use-balance'; export const Button: FC = (props) => { const { onClick, ...rest } = props; @@ -25,10 +24,7 @@ export const Button: FC = (props) => { const { isDappActive } = useDappStatus(); const { openModal } = useWalletModal(); - const { data: balance, initialLoading } = useEthereumBalance( - undefined, - STRATEGY_LAZY, - ); + const { data: balance, isLoading } = useEthereumBalance(); return ( = (props) => { variant="text" color="secondary" onClick={() => openModal({})} - $isAddPaddingLeft={!initialLoading && !isDappActive && !isMobile} + $isAddPaddingLeft={!isLoading && !isDappActive && !isMobile} {...rest} > - {initialLoading ? ( + {isLoading ? ( ) : ( isDappActive && ( diff --git a/shared/wallet/fallback/useErrorMessage.ts b/shared/wallet/fallback/useErrorMessage.ts index 887bf0865..9c37a45fd 100644 --- a/shared/wallet/fallback/useErrorMessage.ts +++ b/shared/wallet/fallback/useErrorMessage.ts @@ -2,13 +2,15 @@ import { useConnectorInfo, getUnsupportedChainError, } from 'reef-knot/core-react'; -import { helpers, useSupportedChains, useWeb3 } from 'reef-knot/web3-react'; -import { useAccount, useConfig } from 'wagmi'; +// TODO: to remove the 'reef-knot/web3-react' after it will be deprecated +import { helpers, useSupportedChains } from 'reef-knot/web3-react'; +import { useAccount, useConnect, useConfig } from 'wagmi'; export const useErrorMessage = (): string | undefined => { - const { error } = useWeb3(); const { chains } = useConfig(); const { isConnected } = useAccount(); + const { error } = useConnect(); + const { isUnsupported } = useSupportedChains(); const { isLedger } = useConnectorInfo(); diff --git a/shared/wallet/lido-multichain-fallback/lido-multichain-fallback.tsx b/shared/wallet/lido-multichain-fallback/lido-multichain-fallback.tsx index bbf6b4784..534e9479f 100644 --- a/shared/wallet/lido-multichain-fallback/lido-multichain-fallback.tsx +++ b/shared/wallet/lido-multichain-fallback/lido-multichain-fallback.tsx @@ -11,6 +11,7 @@ import { ReactComponent as PolygonLogo } from 'assets/icons/lido-multichain/poly import { ReactComponent as ZkSyncLogo } from 'assets/icons/lido-multichain/zk-sync.svg'; import { ReactComponent as ScrollLogo } from 'assets/icons/lido-multichain/scroll.svg'; import { ReactComponent as BNBLogo } from 'assets/icons/lido-multichain/bnb.svg'; +import { ReactComponent as ModeLogo } from 'assets/icons/lido-multichain/mode.svg'; import { config } from 'config'; import { useUserConfig } from 'config/user-config'; @@ -34,6 +35,7 @@ const multichainLogos = { [LIDO_MULTICHAIN_CHAINS['zkSync Era']]: ZkSyncLogo, [LIDO_MULTICHAIN_CHAINS.Scroll]: ScrollLogo, [LIDO_MULTICHAIN_CHAINS['BNB Chain']]: BNBLogo, + [LIDO_MULTICHAIN_CHAINS['Mode Chain']]: ModeLogo, }; const getChainLogo = (chainId: LIDO_MULTICHAIN_CHAINS) => { diff --git a/shared/wallet/lido-multichain-fallback/styles.tsx b/shared/wallet/lido-multichain-fallback/styles.tsx index 965a0057c..9ed5fe727 100644 --- a/shared/wallet/lido-multichain-fallback/styles.tsx +++ b/shared/wallet/lido-multichain-fallback/styles.tsx @@ -63,6 +63,14 @@ export const Wrap = styled((props) => )` #f0b90b 91.42% ); `; + case LIDO_MULTICHAIN_CHAINS['Mode Chain']: + return css` + background: linear-gradient( + 54.14deg, + #626931 -22.38%, + #b4c740 91.42% + ); + `; default: return css` background: linear-gradient( diff --git a/shared/wallet/wallet-modal/wallet-modal.tsx b/shared/wallet/wallet-modal/wallet-modal.tsx index 4433fd70a..4a10a9594 100644 --- a/shared/wallet/wallet-modal/wallet-modal.tsx +++ b/shared/wallet/wallet-modal/wallet-modal.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect } from 'react'; +import { useAccount } from 'wagmi'; import { ButtonIcon, Modal, @@ -9,7 +10,6 @@ import { } from '@lidofinance/lido-ui'; import { useEtherscanOpen } from '@lido-sdk/react'; import { useConnectorInfo, useDisconnect } from 'reef-knot/core-react'; -import { useWeb3 } from 'reef-knot/web3-react'; import type { ModalComponentType } from 'providers/modal-provider'; import { useCopyToClipboard } from 'shared/hooks'; @@ -24,7 +24,7 @@ import { } from './styles'; export const WalletModal: ModalComponentType = ({ onClose, ...props }) => { - const { account } = useWeb3(); + const { address } = useAccount(); const { connectorName } = useConnectorInfo(); const { disconnect } = useDisconnect(); @@ -33,15 +33,22 @@ export const WalletModal: ModalComponentType = ({ onClose, ...props }) => { onClose?.(); }, [disconnect, onClose]); - const handleCopy = useCopyToClipboard(account ?? ''); - const handleEtherscan = useEtherscanOpen(account ?? '', 'address'); + const handleCopy = useCopyToClipboard(address ?? ''); + const handleEtherscan = useEtherscanOpen(address ?? '', 'address'); useEffect(() => { // Close the modal if a wallet was somehow disconnected while the modal was open - if (account == null || account.length === 0) { + if (address == null || address.length === 0) { onClose?.(); } - }, [account, onClose]); + }, [address, onClose]); + + useEffect(() => { + // Close the modal if a wallet was somehow disconnected while the modal was open + if (address == null || address.length === 0) { + onClose?.(); + } + }, [address, onClose]); return ( @@ -66,11 +73,11 @@ export const WalletModal: ModalComponentType = ({ onClose, ...props }) => { - +
diff --git a/test/consts.ts b/test/consts.ts index 8dae119cd..47faf7e0a 100644 --- a/test/consts.ts +++ b/test/consts.ts @@ -13,68 +13,7 @@ export interface PostRequest { schema: object; } -const FLOAT_REGEX = /^\d+(\.\d+)?$/; - export const GET_REQUESTS: GetRequest[] = [ - { - uri: '/api/oneinch-rate?token=ETH', - isDeprecated: true, - schema: { - type: 'object', - properties: { - rate: { type: 'number', min: 0 }, - toReceive: { type: 'string' }, - fromAmount: { type: 'string' }, - }, - required: ['rate', 'toReceive', 'fromAmount'], - additionalProperties: false, - }, - }, - { - uri: `/api/short-lido-stats?chainId=${CONFIG.STAND_CONFIG.chainId}`, - isDeprecated: true, - schema: { - type: 'object', - properties: { - uniqueAnytimeHolders: { type: 'string' }, - uniqueHolders: { type: 'string' }, - totalStaked: { type: 'string' }, - marketCap: { type: 'number' }, - }, - required: [ - 'totalStaked', - 'marketCap', - 'uniqueAnytimeHolders', - 'uniqueHolders', - ], - additionalProperties: true, - }, - }, - { - uri: '/api/eth-apr', - isDeprecated: true, - schema: { type: 'string', pattern: FLOAT_REGEX }, - }, - { - uri: '/api/totalsupply', - isDeprecated: true, - schema: { type: 'string', pattern: FLOAT_REGEX }, - }, - { - uri: '/api/eth-price', - isDeprecated: true, - schema: { - type: 'object', - properties: { - price: { - type: 'number', - min: 0, - }, - }, - required: ['price'], - additionalProperties: true, - }, - }, { uri: '/api/rewards?address=0x87c0e047F4e4D3e289A56a36570D4CB957A37Ef1¤cy=usd&onlyRewards=false&archiveRate=true&skip=0&limit=10', skipTestnet: true, // api/rewards don't work on testnet @@ -121,14 +60,6 @@ export const GET_REQUESTS: GetRequest[] = [ }, }, }, - { - uri: '/api/sma-steth-apr', - isDeprecated: true, - schema: { - type: 'string', - pattern: FLOAT_REGEX, - }, - }, ]; export const POST_REQUESTS: PostRequest[] = [ diff --git a/utils/use-web3-transport.ts b/utils/use-web3-transport.ts new file mode 100644 index 000000000..26a594d04 --- /dev/null +++ b/utils/use-web3-transport.ts @@ -0,0 +1,140 @@ +import { config } from 'config'; +import { useMemo, useCallback } from 'react'; +import { + type Transport, + fallback, + createTransport, + http, + EIP1193Provider, + custom, + Chain, + UnsupportedProviderMethodError, +} from 'viem'; +import type { OnResponseFn } from 'viem/_types/clients/transports/fallback'; +import type { Connection } from 'wagmi'; + +// We disable those methods so wagmi uses getLogs intestead to watch events +// Filters are not suitable for public rpc and break between fallbacks +const DISABLED_METHODS = new Set([ + 'eth_newFilter', + 'eth_getFilterChanges', + 'eth_uninstallFilter', +]); + +const NOOP = () => {}; + +// Viem transport wrapper that allows runtime changes via setter +const runtimeMutableTransport = ( + mainTransports: Transport[], +): [Transport, (t: Transport | null) => void] => { + let withInjectedTransport: Transport | null = null; + return [ + (params) => { + const defaultTransport = fallback(mainTransports)(params); + let responseFn: OnResponseFn = NOOP; + return createTransport( + { + key: 'RuntimeMutableTransport', + name: 'RuntimeMutableTransport', + //@ts-expect-error invalid typings + async request(requestParams, options) { + const transport = withInjectedTransport + ? withInjectedTransport(params) + : defaultTransport; + + if (DISABLED_METHODS.has(requestParams.method)) { + const error = new UnsupportedProviderMethodError( + new Error(`Method ${requestParams.method} is not supported`), + ); + responseFn({ + error, + method: requestParams.method, + params: params as unknown[], + transport, + status: 'error', + }); + throw error; + } + + transport.value?.onResponse(responseFn); + return transport.request(requestParams, options); + }, + type: 'fallback', + }, + { + transports: defaultTransport.value?.transports, + onResponse: (fn: OnResponseFn) => (responseFn = fn), + }, + ); + }, + (injectedTransport: Transport | null) => { + if (injectedTransport) { + withInjectedTransport = fallback([ + injectedTransport, + ...mainTransports, + ]); + } else { + withInjectedTransport = null; + } + }, + ]; +}; + +// returns Viem transport map that uses browser wallet RPC provider when avaliable fallbacked by our RPC +export const useWeb3Transport = ( + supportedChains: Chain[], + backendRpcMap: Record, +) => { + const { transportMap, setTransportMap } = useMemo(() => { + return supportedChains.reduce( + ({ transportMap, setTransportMap }, chain) => { + const [transport, setTransport] = runtimeMutableTransport([ + http(backendRpcMap[chain.id], { + batch: { wait: config.PROVIDER_BATCH_TIME }, + name: backendRpcMap[chain.id], + }), + http(undefined, { + batch: { wait: config.PROVIDER_BATCH_TIME }, + name: 'default HTTP RPC', + }), + ]); + return { + transportMap: { + ...transportMap, + [chain.id]: transport, + }, + setTransportMap: { + ...setTransportMap, + [chain.id]: setTransport, + }, + }; + }, + { + transportMap: {} as Record, + setTransportMap: {} as Record void>, + }, + ); + }, [supportedChains, backendRpcMap]); + + const onActiveConnection = useCallback( + async (activeConnection: Connection | null) => { + for (const chain of supportedChains) { + const setTransport = setTransportMap[chain.id]; + if ( + activeConnection && + chain.id === activeConnection.chainId && + activeConnection.connector.type === 'injected' + ) { + const provider = (await activeConnection.connector?.getProvider?.({ + chainId: chain.id, + })) as EIP1193Provider | undefined; + + setTransport(provider ? custom(provider) : null); + } else setTransport(null); + } + }, + [setTransportMap, supportedChains], + ); + + return { transportMap, onActiveConnection }; +}; diff --git a/yarn.lock b/yarn.lock index a7aeb0d40..cf3a92077 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1821,6 +1821,11 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@graphql-typed-document-node/core@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" + integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== + "@humanwhocodes/config-array@^0.11.13": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -2311,20 +2316,20 @@ tiny-invariant "^1.1.0" tiny-warning "^1.0.3" -"@lidofinance/analytics-matomo@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/analytics-matomo/-/analytics-matomo-0.41.0.tgz#2cc20edba20ce21e0d745e2b44a54cd7acfd424e" - integrity sha512-8Z9n3BovqG6nmS8lUemJauiyq5EIK5TaPZdPQ3Dp5VcpVRqMekFhXeBf9srTqCrEPxnTwcBu09Mun59bgnNSoQ== +"@lidofinance/analytics-matomo@^0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/analytics-matomo/-/analytics-matomo-0.45.1.tgz#f1ee0df1f6babb1f806e258831efd4ba568ad188" + integrity sha512-D35q+0XnqhWkKJidmT0iWKtLZ+KDj8z9J3t3wP+D0zi53+bP9dd7sU1ciZBUal1Sp0IjEzfiGVSiWKFQqyB8Pg== -"@lidofinance/api-metrics@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/api-metrics/-/api-metrics-0.41.0.tgz#d7a85fe951031de00a3748de5aeb11c2c8533c9e" - integrity sha512-huIL210Lw0SzQqqTwjARXEbNhCj+0w8qbZQZVZRnIh6Zyra+cIQHRCLeBoUmyGeI3Usrjrn4wY5I8+gPQixRkA== +"@lidofinance/api-metrics@^0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/api-metrics/-/api-metrics-0.45.1.tgz#0c093ebeb5fa3b2b73533ec933e0bbd20bbb37dd" + integrity sha512-iPzE4RzPxeApCF7+rIQcng/zos7gHY4ibTBkTF4Vt32P9aCtt70YinQ2Sf2KY8AnBeWhGJLXjvAPWr9P3OI18A== -"@lidofinance/api-rpc@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/api-rpc/-/api-rpc-0.41.0.tgz#f62aed905c7b808583528e6d9377c9f2bc1ff271" - integrity sha512-CYQkNU5xgTN6tZlH3T3F6jgiZ5gKxWgmiGIBf9HwHzEfTdmOVETWQLdmKrpp0IUCZNOaSsT5fye02MdB5Kw+4Q== +"@lidofinance/api-rpc@^0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/api-rpc/-/api-rpc-0.45.1.tgz#0a2002e233bd1233ed68d3a479a41ed93868a6d8" + integrity sha512-rTuLPs0EiOZa3tap2+zAgJ1/kpygjX+R5LgfkBw5oxZn1Dj9k+XvWuPAWBkHAsNVKonYTucD4F/NP0XhNlYuYQ== "@lidofinance/eslint-config@^0.34.0": version "0.34.0" @@ -2333,15 +2338,25 @@ dependencies: typescript "^4.7" -"@lidofinance/eth-api-providers@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/eth-api-providers/-/eth-api-providers-0.41.0.tgz#29b0c8c7ff8b697f83d44d2439a9d4ad0aacdf26" - integrity sha512-9+8Uh3AcqwHobCcxkGvt9haflr2+0Ws8fThT/nTQA638oJXzMbOHdBYp/UEdwSAe6lMlhJnODwoO7eIF0gAT4Q== +"@lidofinance/eth-api-providers@^0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/eth-api-providers/-/eth-api-providers-0.45.1.tgz#090e6a3957b664ce54641453357850cac0022a97" + integrity sha512-/8BbPUmBUhB8hRHZpzFXcK/i2jk7pmzhh2NJWjya+G3nSmXaw8U6PInpArB/ZxRAErs0zufUIOm4w7Qrr9mVPw== + +"@lidofinance/eth-providers@^0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/eth-providers/-/eth-providers-0.45.1.tgz#54cbd893c92c06f7ad605ebe0fd8059fc326f180" + integrity sha512-ugkRCI0BPFjWY2h/cDcM8IiibQ8i/wycUgVk2C+E7gCZkjL1YPxGZdnQkfFgIHXAtPhw2YeKHP+VC6HiZpVrZw== -"@lidofinance/eth-providers@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/eth-providers/-/eth-providers-0.41.0.tgz#31e8c4abbc22375ea0b1eb54a34eee72f175e906" - integrity sha512-qz/+C1J6soJjtrAOBIlfTdez60lK9lS8jqvdUSEGPHMr88Pg+6p77Zh5kzQynGXuwMeg7xKVqB5uMLFJPAOGGA== +"@lidofinance/lido-ethereum-sdk@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@lidofinance/lido-ethereum-sdk/-/lido-ethereum-sdk-3.4.0.tgz#8da4e41bf5d045e50017345bf12a363c17a5273d" + integrity sha512-dHCRhDah9QOlgZgf+GzMMEpE3wPEfudoczeuThW5kTFIUygmww1fPeV8y3HnOip5wpP/4dT4p3/ABCcy0bO0pw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + graphql "^16.8.1" + graphql-request "^6.1.0" + viem "^2.0.6" "@lidofinance/lido-ui@^3.18.0": version "3.21.0" @@ -2371,34 +2386,34 @@ ua-parser-js "^1.0.35" use-callback-ref "1.2.5" -"@lidofinance/next-api-wrapper@^0.41.0", "@lidofinance/next-api-wrapper@~0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/next-api-wrapper/-/next-api-wrapper-0.41.0.tgz#994f22383032367d11ae4f2ea9ca9b5d15863a0f" - integrity sha512-ADo9OpuTddC4/e+EFnJExh/ztsdKYrpNEyVUuC4iciZ7oUndfA0/NXlBdKyC89L4cC72DCNtbAgemYpBv4wELQ== +"@lidofinance/next-api-wrapper@^0.45.1", "@lidofinance/next-api-wrapper@~0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/next-api-wrapper/-/next-api-wrapper-0.45.1.tgz#59fe3c654c9276eaef8674302470fcc7bb9d8211" + integrity sha512-SUAc520aooNwNJZ0Q1391Yz8iONPiVMQrRC0wWXWDETMFy0ZhCsQ5VCuRKf6rmZwxYjK7K600GV8ke06IIetaw== -"@lidofinance/next-ip-rate-limit@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/next-ip-rate-limit/-/next-ip-rate-limit-0.41.0.tgz#1d37abb0c7a0fa533368733199122be9673c9cd6" - integrity sha512-HOJV+6dlwyuB+o7JbzCErcaeuUx7wSGWdTcfs1HyLr14YJb67kyVG7WmaSj1hbmpAE14ljSRL6UlS/WNIKjXcQ== +"@lidofinance/next-ip-rate-limit@^0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/next-ip-rate-limit/-/next-ip-rate-limit-0.45.1.tgz#79ab181e9f125ad79c46a642dbeec5d8a194397c" + integrity sha512-8d0l6zi4IqX08Op5UCnGM5Z76tkX6GjzzfyjD6Mt7cNt/eNS8AGGKK9wbhUGmumu97l5L0hTZgEW3rNxA8+ptg== dependencies: - "@lidofinance/next-api-wrapper" "~0.41.0" + "@lidofinance/next-api-wrapper" "~0.45.1" -"@lidofinance/next-pages@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/next-pages/-/next-pages-0.41.0.tgz#d50cf36c4b3958b0fdab238f43b45ce95b21b5af" - integrity sha512-xZqMthO0dPCrmNGmkUi2ZDolzl0fKj0UtaZdg3cZPT0DdrqiYrAh6ut9CmFTEpa8IfqclDXUAdlzJfWt8TbKQA== +"@lidofinance/next-pages@^0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/next-pages/-/next-pages-0.45.1.tgz#b4c8fe555ba8f04b9cb2fe88a258632fe3e8552a" + integrity sha512-eaqZ2LUWsQg+lOhdzdL+WGQ3h5p7R+HYJ78q3UhnhWMrwZkMDNgxX6dWw7TudcD1yVC9yeZeT9tpfe7Hbd/XEQ== -"@lidofinance/rpc@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/rpc/-/rpc-0.41.0.tgz#11f9ec0c9d45301939d90d07517a2d253f29d9c2" - integrity sha512-83ZqclCFMQ+5TBlMCbn+J03fspyvFZuCW6130bUdDSlqrV+WLxAFVcL+9lYv3NV6z7Ds3//6l35aRURLQiPdBg== +"@lidofinance/rpc@^0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/rpc/-/rpc-0.45.1.tgz#53e2717ac434c4467ba0135d5144b64cb7af5548" + integrity sha512-F3ZgvBqM9sm8sQEiqGRO05fwBV/ZaexT4RmeSp3I6NcBtpaNuTf8z8IMDcslDhgk7HZZHt/VIAY+8TgVqV8jXw== dependencies: isomorphic-fetch "^3.0.0" -"@lidofinance/satanizer@^0.41.0": - version "0.41.0" - resolved "https://registry.yarnpkg.com/@lidofinance/satanizer/-/satanizer-0.41.0.tgz#005793a7d98c9a6cee97adcb4d6ff782ff5e8820" - integrity sha512-AK43IDg/HZBgnsk88RIPGE0WzJ81KtBYKvsWPjEAs2etTGpY4j3swUmJl218LBT3LqzLM5BZ0UrdFQpSW/GlYw== +"@lidofinance/satanizer@^0.45.1": + version "0.45.1" + resolved "https://registry.yarnpkg.com/@lidofinance/satanizer/-/satanizer-0.45.1.tgz#22fe644d7657b333074e9806e2b4229d210ba3a7" + integrity sha512-RC9jKEKykFhHFHUJNziFzvTP4LEIr1idRHgK96myKOn2w5GsiZkTd6SiaI7H9/X+Rg9ZbnTb0zcLZdYQPG946A== "@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0": version "1.1.2" @@ -2730,13 +2745,6 @@ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz#d28ea15a72cdcf96201c60a43e9630cd7fda168f" integrity sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg== -"@noble/curves@1.2.0", "@noble/curves@~1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" - integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== - dependencies: - "@noble/hashes" "1.3.2" - "@noble/curves@1.4.0", "@noble/curves@~1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" @@ -2744,20 +2752,22 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/hashes@1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" - integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/curves@^1.4.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" + integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== + dependencies: + "@noble/hashes" "1.4.0" -"@noble/hashes@1.4.0", "@noble/hashes@^1.3.1", "@noble/hashes@~1.4.0": +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0", "@noble/hashes@~1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== -"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": - version "1.3.3" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" - integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== +"@noble/hashes@~1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" + integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -3132,19 +3142,15 @@ resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.14.0.tgz#9581c524c1ea4956555f40761eb6b4007392aa82" integrity sha512-/dqU66RvHw50n+7x3nwnJedq8V6iLQyoWitNdjx5cFTBmae+rpP+LvHq+LqZfXJVkB1qNytMdjFjdyES0t79gQ== -"@scure/base@^1.1.3", "@scure/base@~1.1.0", "@scure/base@~1.1.2", "@scure/base@~1.1.6": +"@scure/base@^1.1.3", "@scure/base@~1.1.6": version "1.1.7" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== -"@scure/bip32@1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" - integrity sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA== - dependencies: - "@noble/curves" "~1.2.0" - "@noble/hashes" "~1.3.2" - "@scure/base" "~1.1.2" +"@scure/base@~1.1.8": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.8.tgz#8f23646c352f020c83bca750a82789e246d42b50" + integrity sha512-6CyAclxj3Nb0XT7GHK6K4zK6k2xJm6E4Ft0Ohjt4WgegiFUHEtFb2CGzmPmGBwoIhrLsqNLYfLr04Y1GePrzZg== "@scure/bip32@1.4.0": version "1.4.0" @@ -3155,14 +3161,6 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" -"@scure/bip39@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" - integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== - dependencies: - "@noble/hashes" "~1.3.0" - "@scure/base" "~1.1.0" - "@scure/bip39@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" @@ -3171,6 +3169,14 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" +"@scure/bip39@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.4.0.tgz#664d4f851564e2e1d4bffa0339f9546ea55960a6" + integrity sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw== + dependencies: + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.8" + "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" @@ -3546,17 +3552,17 @@ dependencies: tslib "^2.4.0" -"@tanstack/query-core@5.48.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.48.0.tgz#a3308ec925d8c16d64c789899d6c084c2fe30cbc" - integrity sha512-lZAfPPeVIqXCswE9SSbG33B6/91XOWt/Iq41bFeWb/mnHwQSIfFRbkS4bfs+WhIk9abRArF9Id2fp0Mgo+hq6Q== +"@tanstack/query-core@5.51.21": + version "5.51.21" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.51.21.tgz#a510469c6c30d3de2a8b8798e340169a4b0fd08f" + integrity sha512-POQxm42IUp6n89kKWF4IZi18v3fxQWFRolvBA6phNVmA8psdfB1MvDnGacCJdS+EOX12w/CyHM62z//rHmYmvw== -"@tanstack/react-query@^5.48.0": - version "5.48.0" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.48.0.tgz#7890620272b48aeb278498dfe082f27518f3ac6d" - integrity sha512-GDExbjYWzvDokyRqMSWXdrPiYpp95Aig0oeMIrxTaruOJJgWiWfUP//OAaowm2RrRkGVsavSZdko/XmIrrV2Nw== +"@tanstack/react-query@^5.51.21": + version "5.51.21" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.51.21.tgz#cdd14677bcc809a83e01b6c38842c841ce7420af" + integrity sha512-Q/V81x3sAYgCsxjwOkfLXfrmoG+FmDhLeHH5okC/Bp8Aaw2c33lbEo/mMcMnkxUPVtB2FLpzHT0tq3c+OlZEbw== dependencies: - "@tanstack/query-core" "5.48.0" + "@tanstack/query-core" "5.51.21" "@trysound/sax@0.2.0": version "0.2.0" @@ -4323,11 +4329,6 @@ JSONStream@^1.3.5: jsonparse "^1.2.0" through ">=2.2.7 <3" -abitype@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" - integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== - abitype@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.5.tgz#29d0daa3eea867ca90f7e4123144c1d1270774b6" @@ -4981,9 +4982,9 @@ camelize@^1.0.0: integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001580: - version "1.0.30001581" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz#0dfd4db9e94edbdca67d57348ebc070dece279f4" - integrity sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ== + version "1.0.30001651" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz" + integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== "cbw-sdk@npm:@coinbase/wallet-sdk@3.9.3": version "3.9.3" @@ -6840,6 +6841,19 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +graphql-request@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-6.1.0.tgz#f4eb2107967af3c7a5907eb3131c671eac89be4f" + integrity sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw== + dependencies: + "@graphql-typed-document-node/core" "^3.2.0" + cross-fetch "^3.1.5" + +graphql@^16.8.1: + version "16.9.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.9.0.tgz#1c310e63f16a49ce1fbb230bd0a000e99f6f115f" + integrity sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw== + gray-matter@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" @@ -11418,19 +11432,35 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" -viem@2.13.3: - version "2.13.3" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.13.3.tgz#950426e4cacf5e12fab2c202a339371901712481" - integrity sha512-3tlwDRKHSelupFjbFMdUxF41f79ktyH2F9PAQ9Dltbs1DpdDlR1x+Ksa0th6qkyjjAbpDZP3F5nMTJv/1GVPdQ== +viem@2.18.8: + version "2.18.8" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.18.8.tgz#5e65050ddc3bdc0f928c0ca22b33bc720bf36639" + integrity sha512-Fi5d9fd/LBiVtJ5eV2c99yrdt4dJH5Vbkf2JajwCqHYuV4ErSk/sm+L6Ru3rzT67rfRHSOQibTZxByEBua/WLw== dependencies: "@adraffy/ens-normalize" "1.10.0" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@scure/bip32" "1.3.2" - "@scure/bip39" "1.2.1" - abitype "1.0.0" + "@noble/curves" "1.4.0" + "@noble/hashes" "1.4.0" + "@scure/bip32" "1.4.0" + "@scure/bip39" "1.3.0" + abitype "1.0.5" isows "1.0.4" - ws "8.13.0" + webauthn-p256 "0.0.5" + ws "8.17.1" + +viem@^2.0.6: + version "2.21.2" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.2.tgz#8e6cae0babd12edee745e551b437d9e38985a97f" + integrity sha512-gTzwKbmyepEDUBKXs3GslTcg5KXfDIgQfHKNxIV9cs7Xout55F8NvHhNeBGBfuw1Ix4Vz8aCMFGYwX5a64CGFg== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.4.0" + "@noble/hashes" "1.4.0" + "@scure/bip32" "1.4.0" + "@scure/bip39" "1.4.0" + abitype "1.0.5" + isows "1.0.4" + webauthn-p256 "0.0.5" + ws "8.17.1" viem@^2.1.1: version "2.17.9" @@ -11462,6 +11492,14 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +webauthn-p256@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.5.tgz#0baebd2ba8a414b21cc09c0d40f9dd0be96a06bd" + integrity sha512-drMGNWKdaixZNobeORVIqq7k5DsRC9FnG201K2QjeOoQLmtSDaSsVZdkg6n5jUALJKcAG++zBPJXmv6hy0nWFg== + dependencies: + "@noble/curves" "^1.4.0" + "@noble/hashes" "^1.4.0" + "webextension-polyfill@>=0.10.0 <1.0": version "0.12.0" resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz#f62c57d2cd42524e9fbdcee494c034cae34a3d69" @@ -11652,11 +11690,6 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== -ws@8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== - ws@8.17.1, ws@~8.17.1: version "8.17.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"