diff --git a/.env.example b/.env.example index 64e1cdf6a..35bde4ae2 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,11 @@ EL_RPC_URLS_1= EL_RPC_URLS_5= EL_RPC_URLS_17000= +# IPFS prefill RPC URLs - list of URLs delimited by commas +PREFILL_UNSAFE_EL_RPC_URLS_1= +PREFILL_UNSAFE_EL_RPC_URLS_5= +PREFILL_UNSAFE_EL_RPC_URLS_17000= + # supported networks for connecting wallet SUPPORTED_CHAINS=1,17000 @@ -12,7 +17,6 @@ DEFAULT_CHAIN=1 # api key for ethplorer for token data ETHPLORER_API_KEY=freekey - # comma-separated trusted hosts for Content Security Policy # e.g. http://localhost:PORT for local development CSP_TRUSTED_HOSTS=https://*.lido.fi @@ -50,3 +54,6 @@ MATOMO_URL= # WalletConnect project ID WALLETCONNECT_PROJECT_ID= + +# ETH Stake Widget API for IPFS mode +WIDGET_API_BASE_PATH_FOR_IPFS= diff --git a/.github/workflows/ci-ipfs-test-production.yml b/.github/workflows/ci-ipfs-test-production.yml index f3fb96b69..a49604ffb 100644 --- a/.github/workflows/ci-ipfs-test-production.yml +++ b/.github/workflows/ci-ipfs-test-production.yml @@ -39,7 +39,7 @@ jobs: REWARDS_BACKEND_BASE_PATH: ${{ vars.REWARDS_BACKEND_BASE_PATH }} WQ_API_BASE_PATH: ${{ vars.WQ_API_BASE_PATH }} ETH_API_BASE_PATH: ${{ vars.ETH_API_BASE_PATH }} - PREFILL_UNSAFE_EL_RPC_URLS: ${{ secrets.PREFILL_UNSAFE_EL_RPC_URLS }} + PREFILL_UNSAFE_EL_RPC_URLS_1: ${{ secrets.PREFILL_UNSAFE_EL_RPC_URLS_1 }} WALLETCONNECT_PROJECT_ID: ${{ secrets.WALLETCONNECT_PROJECT_ID }} - uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/ci-ipfs-testnet.yml b/.github/workflows/ci-ipfs-testnet.yml index e4df1dbe3..b083cc1ce 100644 --- a/.github/workflows/ci-ipfs-testnet.yml +++ b/.github/workflows/ci-ipfs-testnet.yml @@ -39,7 +39,9 @@ jobs: REWARDS_BACKEND_BASE_PATH: ${{ vars.REWARDS_BACKEND_BASE_PATH }} WQ_API_BASE_PATH: ${{ vars.WQ_API_BASE_PATH }} ETH_API_BASE_PATH: ${{ vars.ETH_API_BASE_PATH }} - PREFILL_UNSAFE_EL_RPC_URLS: ${{ secrets.PREFILL_UNSAFE_EL_RPC_URLS }} + PREFILL_UNSAFE_EL_RPC_URLS_1: ${{ secrets.PREFILL_UNSAFE_EL_RPC_URLS_1 }} + PREFILL_UNSAFE_EL_RPC_URLS_5: ${{ secrets.PREFILL_UNSAFE_EL_RPC_URLS_5 }} + PREFILL_UNSAFE_EL_RPC_URLS_17000: ${{ secrets.PREFILL_UNSAFE_EL_RPC_URLS_17000 }} WALLETCONNECT_PROJECT_ID: ${{ secrets.WALLETCONNECT_PROJECT_ID }} - uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/ci-ipfs.yml b/.github/workflows/ci-ipfs.yml index efeea54be..e7b44db41 100644 --- a/.github/workflows/ci-ipfs.yml +++ b/.github/workflows/ci-ipfs.yml @@ -55,7 +55,7 @@ jobs: REWARDS_BACKEND_BASE_PATH: ${{ vars.REWARDS_BACKEND_BASE_PATH }} WQ_API_BASE_PATH: ${{ vars.WQ_API_BASE_PATH }} ETH_API_BASE_PATH: ${{ vars.ETH_API_BASE_PATH }} - PREFILL_UNSAFE_EL_RPC_URLS: ${{ secrets.PREFILL_UNSAFE_EL_RPC_URLS }} + PREFILL_UNSAFE_EL_RPC_URLS_1: ${{ secrets.PREFILL_UNSAFE_EL_RPC_URLS_1 }} WALLETCONNECT_PROJECT_ID: ${{ secrets.WALLETCONNECT_PROJECT_ID }} - uses: actions/upload-artifact@v3 with: diff --git a/README.md b/README.md index 37fe7ba5b..fd7763220 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A widget for submitting Ether to the pool based on [Lido Frontend Template](http ### Pre-requisites -- Node.js v16+ +- Node.js v16 - Yarn package manager This project requires an .env file which is distributed via private communication channels. A sample can be found in .env.example @@ -31,6 +31,12 @@ Step 4. Start the development server yarn dev ``` +for IPFS mode below: + +```bash +yarn dev-ipfs # will start with HMR +``` + ### Environment variables Note! Avoid using `NEXT_PUBLIC_` environment variables as it hinders our CI pipeline. Please use server-side environment variables and pass them to the client using `getInitialProps` in `_app.js`. @@ -52,6 +58,12 @@ git commit -m "feat: dark theme" yarn build && yarn start ``` +for IPFS mode below: + +```bash +yarn build-ipfs +``` + ## Adding a new route API - create a new file in `pages/api/` folder diff --git a/assets/icons/gear.svg b/assets/icons/gear.svg new file mode 100644 index 000000000..6a7a50fe3 --- /dev/null +++ b/assets/icons/gear.svg @@ -0,0 +1 @@ + diff --git a/config/index.ts b/config/index.ts index d26449ba8..68d9674af 100644 --- a/config/index.ts +++ b/config/index.ts @@ -6,12 +6,14 @@ export * from './api'; export * from './cache'; export * from './estimate'; export * from './locale'; +export * from './ipfs'; export * from './metrics'; export * from './rpc'; export * from './steth'; export * from './storage'; export * from './text'; export * from './tx'; +export * from './types'; export * from './units'; export * from './metrics'; export * from './rateLimit'; diff --git a/config/ipfs.ts b/config/ipfs.ts new file mode 100644 index 000000000..489b3f356 --- /dev/null +++ b/config/ipfs.ts @@ -0,0 +1,7 @@ +import getConfig from 'next/config'; +import dynamics from './dynamics'; + +const { serverRuntimeConfig } = getConfig(); +const { basePath = '' } = serverRuntimeConfig; + +export const BASE_PATH_ASSET = dynamics.ipfsMode ? '.' : basePath; diff --git a/config/matomoClickEvents.ts b/config/matomoClickEvents.ts index e4e18f4ca..94971658a 100644 --- a/config/matomoClickEvents.ts +++ b/config/matomoClickEvents.ts @@ -64,7 +64,7 @@ export const enum MATOMO_CLICK_EVENTS_TYPES { export const MATOMO_CLICK_EVENTS: Record< MATOMO_CLICK_EVENTS_TYPES, MatomoEventType -> = { + > = { // Global [MATOMO_CLICK_EVENTS_TYPES.connectWallet]: [ 'Ethereum_Staking_Widget', diff --git a/config/rpc.ts b/config/rpc.ts index 838746312..7782ecf16 100644 --- a/config/rpc.ts +++ b/config/rpc.ts @@ -1,12 +1,55 @@ +import { useCallback } from 'react'; +import invariant from 'tiny-invariant'; +import { useSDK } from '@lido-sdk/react'; + +import { useClientConfig } from 'providers/client-config'; import { CHAINS } from 'utils/chains'; +import dynamics from './dynamics'; + export const getBackendRPCPath = (chainId: string | number): string => { const BASE_URL = typeof window === 'undefined' ? '' : window.location.origin; return `${BASE_URL}/api/rpc?chainId=${chainId}`; }; -export const backendRPC = { - [CHAINS.Mainnet]: getBackendRPCPath(CHAINS.Mainnet), - [CHAINS.Goerli]: getBackendRPCPath(CHAINS.Goerli), - [CHAINS.Holesky]: getBackendRPCPath(CHAINS.Holesky), +export const useGetRpcUrlByChainId = () => { + const clientConfig = useClientConfig(); + + return useCallback( + (chainId: CHAINS) => { + // Needs this condition 'cause in 'providers/web3.tsx' we add `wagmiChains.polygonMumbai` to supportedChains + // so, here chainId = 80001 is arriving which to raises an invariant + // chainId = 1 we need anytime! + if ( + chainId !== CHAINS.Mainnet && + !clientConfig.supportedChainIds.includes(chainId) + ) { + // Has no effect on functionality. Just a fix. + // Return empty string as stub + // (see: 'providers/web3.tsx' --> jsonRpcBatchProvider --> getStaticRpcBatchProvider) + return ''; + } + + if (dynamics.ipfsMode) { + const rpc = + clientConfig.savedClientConfig.rpcUrls[chainId] || + clientConfig.prefillUnsafeElRpcUrls[chainId]?.[0]; + + invariant(rpc, '[useGetRpcUrlByChainId] RPC is required!'); + return rpc; + } else { + return ( + clientConfig.savedClientConfig.rpcUrls[chainId] || + getBackendRPCPath(chainId) + ); + } + }, + [clientConfig], + ); +}; + +export const useRpcUrl = () => { + const { chainId } = useSDK(); + const getRpcUrlByChainId = useGetRpcUrlByChainId(); + return getRpcUrlByChainId(chainId as number); }; diff --git a/config/storage.ts b/config/storage.ts index b05cc73b6..793839d93 100644 --- a/config/storage.ts +++ b/config/storage.ts @@ -2,3 +2,5 @@ export const STORAGE_TERMS_KEY = 'lido-terms-agree'; export const STORAGE_THEME_AUTO_KEY = 'lido-theme-auto'; export const STORAGE_THEME_MANUAL_KEY = 'lido-theme-manual'; export const STORAGE_CURRENCY_KEY = 'lido-currency'; +export const STORAGE_CLIENT_CONFIG = 'lido-client-config'; +export const STORAGE_IPFS_INFO_DISMISS = 'lido-ipfs-info-dismiss'; diff --git a/config/types.ts b/config/types.ts new file mode 100644 index 000000000..48c21d7bb --- /dev/null +++ b/config/types.ts @@ -0,0 +1,23 @@ +import { CHAINS } from 'utils/chains'; + +export type EnvConfigRaw = { + defaultChain: string | number; + supportedChains: number[]; + prefillUnsafeElRpcUrls1: string[]; + prefillUnsafeElRpcUrls5: string[]; + prefillUnsafeElRpcUrls17000: string[]; + ipfsMode: boolean; + walletconnectProjectId: string; +}; + +export type EnvConfigParsed = { + defaultChain: number; + supportedChainIds: number[]; + prefillUnsafeElRpcUrls: { + [CHAINS.Mainnet]: string[]; + [CHAINS.Goerli]: string[]; + [CHAINS.Holesky]: string[]; + }; + ipfsMode: boolean; + walletconnectProjectId: string; +}; diff --git a/config/urls.ts b/config/urls.ts new file mode 100644 index 000000000..72d8f4346 --- /dev/null +++ b/config/urls.ts @@ -0,0 +1,16 @@ +// TODO: path + basePath +export const HOME_PATH = '/'; +export const WRAP_PATH = '/wrap'; +export const WRAP_UNWRAP_PATH = '/wrap/unwrap'; +export const WITHDRAWALS_PATH = '/withdrawals'; +export const WITHDRAWALS_REQUEST_PATH = '/withdrawals/request'; +export const WITHDRAWALS_CLAIM_PATH = '/withdrawals/claim'; +export const REWARDS_PATH = '/rewards'; +export const SETTINGS_PATH = '/settings'; +export const REFERRAL_PATH = '/referral'; + +export const getPathWithoutFirstSlash = (path: string): string => { + if (path.length === 0 || path[0] !== '/') return path; + + return path.slice(1, path.length); +}; diff --git a/env-dynamics.mjs b/env-dynamics.mjs index 0c87e70c5..60d85d5c3 100644 --- a/env-dynamics.mjs +++ b/env-dynamics.mjs @@ -32,3 +32,18 @@ export const ethAPIBasePath = process.env.ETH_API_BASE_PATH; export const wqAPIBasePath = process.env.WQ_API_BASE_PATH; /** @type string */ export const walletconnectProjectId = process.env.WALLETCONNECT_PROJECT_ID; + +/** @type boolean */ +export const ipfsMode = toBoolean(process.env.IPFS_MODE); + +/** @type string[] */ +export const prefillUnsafeElRpcUrls1 = process.env.PREFILL_UNSAFE_EL_RPC_URLS_1?.split(',') ?? []; + +/** @type string[] */ +export const prefillUnsafeElRpcUrls5 = process.env.PREFILL_UNSAFE_EL_RPC_URLS_5?.split(',') ?? []; + +/** @type string[] */ +export const prefillUnsafeElRpcUrls17000 = process.env.PREFILL_UNSAFE_EL_RPC_URLS_17000?.split(',') ?? []; + +/** @type string */ +export const widgetApiBasePathForIpfs = process.env.WIDGET_API_BASE_PATH_FOR_IPFS; diff --git a/features/home/home-page-regular.tsx b/features/home/home-page-regular.tsx new file mode 100644 index 000000000..3c78cbf53 --- /dev/null +++ b/features/home/home-page-regular.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react'; +import Head from 'next/head'; + +import { Layout } from 'shared/components'; +import NoSSRWrapper from 'shared/components/no-ssr-wrapper'; +import { useWeb3Key } from 'shared/hooks/useWeb3Key'; + +import { Wallet } from './wallet/wallet'; +import { StakeForm } from './stake-form/stake-form'; +import { StakeFaq } from './stake-faq/stake-faq'; +import { LidoStats } from './lido-stats/lido-stats'; + +const HomePageRegular: FC = () => { + const key = useWeb3Key(); + + return ( + <> + + + Stake with Lido | Lido + + + + + + + + + + + ); +}; + +export default HomePageRegular; diff --git a/features/home/index.ts b/features/home/index.ts index db4fd42c2..dbd1c5b3d 100644 --- a/features/home/index.ts +++ b/features/home/index.ts @@ -1,5 +1,5 @@ export { StakeForm } from './stake-form/stake-form'; -export { OneinchInfo } from './oneinch-info/oneinch-info'; +export { OneInchInfo } from './one-inch-info/one-inch-info'; export { LidoStats } from './lido-stats/lido-stats'; export { Wallet } from './wallet/wallet'; export { StakeFaq } from './stake-faq/stake-faq'; diff --git a/features/home/lido-stats/lido-stats-item.tsx b/features/home/lido-stats/lido-stats-item.tsx new file mode 100644 index 000000000..310529489 --- /dev/null +++ b/features/home/lido-stats/lido-stats-item.tsx @@ -0,0 +1,32 @@ +import { FC, memo, PropsWithChildren, ReactNode } from 'react'; +import { DataTableRow } from '@lidofinance/lido-ui'; +import { DATA_UNAVAILABLE } from 'config'; + +type LidoStatsItemProps = { + show: boolean; + loading: boolean; + dataTestId: string; + title: ReactNode; + highlight?: boolean | undefined; +}; + +export const LidoStatsItem: FC> = memo( + (props) => { + const { show, loading, dataTestId, title, children, highlight } = props; + + if (!show) { + return null; + } + + return ( + + {children || DATA_UNAVAILABLE} + + ); + }, +); diff --git a/features/home/lido-stats/lido-stats.tsx b/features/home/lido-stats/lido-stats.tsx index 995735f30..84211e3a7 100644 --- a/features/home/lido-stats/lido-stats.tsx +++ b/features/home/lido-stats/lido-stats.tsx @@ -1,22 +1,24 @@ import { FC, memo, useMemo } from 'react'; + import { getEtherscanTokenLink } from '@lido-sdk/helpers'; import { useSDK } from '@lido-sdk/react'; import { getTokenAddress, TOKENS } from '@lido-sdk/constants'; -import { - Block, - DataTable, - DataTableRow, - Question, - Tooltip, -} from '@lidofinance/lido-ui'; +import { Block, DataTable, Question, Tooltip } from '@lidofinance/lido-ui'; + import { Section, MatomoLink } from 'shared/components'; +import { useLidoApr, useLidoStats } from 'shared/hooks'; import { LIDO_APR_TOOLTIP_TEXT, - DATA_UNAVAILABLE, MATOMO_CLICK_EVENTS_TYPES, + dynamics, } from 'config'; -import { useLidoApr, useLidoStats } from 'shared/hooks'; + import { FlexCenterVertical } from './styles'; +import { LidoStatsItem } from './lido-stats-item'; + +const isStatItemAvailable = (val: any): boolean => { + return val && val !== 'N/A'; +}; export const LidoStats: FC = memo(() => { const { chainId } = useSDK(); @@ -26,9 +28,22 @@ export const LidoStats: FC = memo(() => { getTokenAddress(chainId, TOKENS.STETH), ); }, [chainId]); + const lidoApr = useLidoApr(); const lidoStats = useLidoStats(); + const showApr = !dynamics.ipfsMode || isStatItemAvailable(lidoApr.apr); + const showTotalStaked = + !dynamics.ipfsMode || isStatItemAvailable(lidoStats.data.totalStaked); + const showStakers = + !dynamics.ipfsMode || isStatItemAvailable(lidoStats.data.stakers); + const showMarketCap = + !dynamics.ipfsMode || isStatItemAvailable(lidoStats.data.marketCap); + + if (!showApr && !showTotalStaked && !showStakers && !showMarketCap) { + return null; + } + return (
{ > - - Annual percentage rate - - - - - } - loading={lidoApr.initialLoading} - data-testid="lidoAPR" - highlight - > - {lidoApr.apr ? `${lidoApr.apr}%` : DATA_UNAVAILABLE} - - - {lidoStats.data.totalStaked} - - - {lidoStats.data.stakers} - - - {lidoStats.data.marketCap} - + <> + + Annual percentage rate + + + + + } + show={showApr} + loading={lidoApr.initialLoading} + dataTestId="lidoAPR" + highlight + > + {lidoApr.apr ?? `${lidoApr.apr}%`} + + + + {lidoStats.data.totalStaked} + + + + {lidoStats.data.stakers} + + + + {lidoStats.data.marketCap} + +
diff --git a/features/home/oneinch-info/oneinch-info.tsx b/features/home/one-inch-info/one-inch-info.tsx similarity index 65% rename from features/home/oneinch-info/oneinch-info.tsx rename to features/home/one-inch-info/one-inch-info.tsx index b928791af..46394e854 100644 --- a/features/home/oneinch-info/oneinch-info.tsx +++ b/features/home/one-inch-info/one-inch-info.tsx @@ -1,9 +1,13 @@ import { FC } from 'react'; + import { Button } from '@lidofinance/lido-ui'; import { trackEvent } from '@lidofinance/analytics-matomo'; + +import { dynamics, MATOMO_CLICK_EVENTS } from 'config'; import { useLidoSWR } from 'shared/hooks'; import { L2OneInch } from 'shared/banners/l2-oneinch'; -import { MATOMO_CLICK_EVENTS } from 'config'; +import { STRATEGY_LAZY } from 'utils/swrStrategies'; +import { prependBasePath } from 'utils'; import { Wrap, @@ -17,21 +21,28 @@ import { use1inchLinkProps } from '../hooks'; const ONE_INCH_RATE_LIMIT = 1.004; -export const OneinchInfo: FC = () => { +export const OneInchInfo: FC = () => { + const linkProps = use1inchLinkProps(); + + const apiOneInchRatePath = 'api/oneinch-rate?token=eth'; const { data, initialLoading } = useLidoSWR<{ rate: number }>( - '/api/oneinch-rate', + dynamics.ipfsMode + ? `${dynamics.widgetApiBasePathForIpfs}/${apiOneInchRatePath}` + : prependBasePath(apiOneInchRatePath), + STRATEGY_LAZY, ); - const rate = (data && data.rate) || 1; - const discount = (100 - (1 / rate) * 100).toFixed(2); - - const linkProps = use1inchLinkProps(); // for fix flashing banner if (initialLoading) return null; - if (!rate || rate < ONE_INCH_RATE_LIMIT) + const rate = (data && data.rate) || 1; + + const showL2 = !rate || rate > ONE_INCH_RATE_LIMIT; + if (showL2) return ; + const discountText = (100 - (1 / (rate || 1)) * 100).toFixed(2); + const linkClickHandler = () => trackEvent(...MATOMO_CLICK_EVENTS.oneInchDiscount); @@ -41,7 +52,7 @@ export const OneinchInfo: FC = () => { - Get a {discount}% discount by buying stETH on the 1inch + Get a {discountText}% discount by buying stETH on the 1inch platform diff --git a/features/home/oneinch-info/styles.ts b/features/home/one-inch-info/styles.ts similarity index 100% rename from features/home/oneinch-info/styles.ts rename to features/home/one-inch-info/styles.ts diff --git a/features/home/stake-faq/list/how-can-i-get-steth.tsx b/features/home/stake-faq/list/how-can-i-get-steth.tsx index 7f958d6d6..5d7f6ad71 100644 --- a/features/home/stake-faq/list/how-can-i-get-steth.tsx +++ b/features/home/stake-faq/list/how-can-i-get-steth.tsx @@ -1,7 +1,9 @@ import { FC } from 'react'; import { Accordion, Link as OuterLink } from '@lidofinance/lido-ui'; + import { MATOMO_CLICK_EVENTS_TYPES } from 'config'; import { trackMatomoEvent } from 'config/trackMatomoEvent'; +import { HOME_PATH } from 'config/urls'; import { LocalLink } from 'shared/components/local-link'; export const HowCanIGetSteth: FC = () => { @@ -10,17 +12,14 @@ export const HowCanIGetSteth: FC = () => {

You can get stETH many ways, including interacting with the smart contract directly.Yet, it is much easier to use a{' '} - - - trackMatomoEvent( - MATOMO_CLICK_EVENTS_TYPES.faqHowCanIGetStEthWidget, - ) - } - aria-hidden="true" - > - Lido Ethereum staking widget - + + trackMatomoEvent(MATOMO_CLICK_EVENTS_TYPES.faqHowCanIGetStEthWidget) + } + aria-hidden="true" + > + Lido Ethereum staking widget {' '} and in other{' '} { return (

You can use our{' '} - - - trackMatomoEvent( - MATOMO_CLICK_EVENTS_TYPES.faqHowCanIUnstakeStEthWithdrawals, - ) - } - aria-hidden="true" - > - Withdrawals Request and Claim tabs - + + trackMatomoEvent( + MATOMO_CLICK_EVENTS_TYPES.faqHowCanIUnstakeStEthWithdrawals, + ) + } + aria-hidden="true" + > + Withdrawals Request and Claim tabs {' '} to unstake stETH and receive ETH at a 1:1 ratio. Under normal circumstances, withdrawal period can take anywhere between 1-5 days. diff --git a/features/home/stake-form/hooks.ts b/features/home/stake-form/hooks.ts index a8f605da9..79b97ab36 100644 --- a/features/home/stake-form/hooks.ts +++ b/features/home/stake-form/hooks.ts @@ -1,18 +1,20 @@ +import { BigNumber } from 'ethers'; + import { AddressZero } from '@ethersproject/constants'; +import { parseEther } from '@ethersproject/units'; import { useLidoSWR, useSTETHContractRPC } from '@lido-sdk/react'; + import { ESTIMATE_ACCOUNT, STETH_SUBMIT_GAS_LIMIT_DEFAULT } from 'config'; -import { parseEther } from '@ethersproject/units'; -import { useWeb3 } from 'reef-knot/web3-react'; -import { BigNumber } from 'ethers'; import { getFeeData } from 'utils/getFeeData'; -import { CHAINS } from '@lido-sdk/constants'; +import { useCurrentStaticRpcProvider } from 'shared/hooks/use-current-static-rpc-provider'; type UseStethSubmitGasLimit = () => number | undefined; export const useStethSubmitGasLimit: UseStethSubmitGasLimit = () => { const stethContractRPC = useSTETHContractRPC(); - const { chainId } = useWeb3(); + const { chainId, staticRpcProvider } = useCurrentStaticRpcProvider(); + const { data } = useLidoSWR( ['swr:submit-gas-limit', chainId], async (_key, chainId) => { @@ -20,7 +22,7 @@ export const useStethSubmitGasLimit: UseStethSubmitGasLimit = () => { return; } - const feeData = await getFeeData(chainId as CHAINS); + const feeData = await getFeeData(staticRpcProvider); const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? undefined; const maxFeePerGas = feeData.maxFeePerGas ?? undefined; diff --git a/features/home/stake-form/stake-form.tsx b/features/home/stake-form/stake-form.tsx index 443b6d3e7..966a67259 100644 --- a/features/home/stake-form/stake-form.tsx +++ b/features/home/stake-form/stake-form.tsx @@ -7,7 +7,9 @@ import { useEffect, useRef, } from 'react'; +import { useWeb3 } from 'reef-knot/web3-react'; import { useRouter } from 'next/router'; + import { parseEther } from '@ethersproject/units'; import { useSDK, @@ -17,7 +19,6 @@ import { useSTETHContractRPC, useSTETHContractWeb3, } from '@lido-sdk/react'; -import { useWeb3 } from 'reef-knot/web3-react'; import { Block, Button, @@ -31,14 +32,16 @@ import { TxStageModal, TX_OPERATION, TX_STAGE } from 'shared/components'; import { useTxCostInUsd } from 'shared/hooks'; import { InputDecoratorMaxButton } from 'shared/forms/components/input-decorator-max-button'; import { useCurrencyInput } from 'shared/forms/hooks/useCurrencyInput'; +import { useIsMultisig } from 'shared/hooks/useIsMultisig'; +import { useCurrentStaticRpcProvider } from 'shared/hooks/use-current-static-rpc-provider'; +import { STRATEGY_LAZY } from 'utils/swrStrategies'; +import { getTokenDisplayName } from 'utils/getTokenDisplayName'; + import { FormStyled, InputStyled } from './styles'; -import { stakeProcessing } from './utils'; import { useStethSubmitGasLimit } from './hooks'; import { useStakeableEther } from '../hooks'; +import { stakeProcessing } from './utils'; import { useStakingLimitWarn } from './useStakingLimitWarn'; -import { getTokenDisplayName } from 'utils/getTokenDisplayName'; -import { useIsMultisig } from 'shared/hooks/useIsMultisig'; -import { STRATEGY_LAZY } from 'utils/swrStrategies'; export const StakeForm: FC = memo(() => { const router = useRouter(); @@ -67,6 +70,7 @@ export const StakeForm: FC = memo(() => { const { active, chainId } = useWeb3(); const { providerWeb3 } = useSDK(); + const { staticRpcProvider } = useCurrentStaticRpcProvider(); const etherBalance = useEthereumBalance(undefined, STRATEGY_LAZY); const stakeableEther = useStakeableEther(); const stethBalance = useSTETHBalance(); @@ -93,6 +97,7 @@ export const StakeForm: FC = memo(() => { const submit = useCallback( async (inputValue: string, resetForm: () => void) => { await stakeProcessing( + staticRpcProvider, providerWeb3, stethContractWeb3, openTxModal, @@ -108,6 +113,7 @@ export const StakeForm: FC = memo(() => { ); }, [ + staticRpcProvider, providerWeb3, stethContractWeb3, openTxModal, diff --git a/features/home/stake-form/utils.ts b/features/home/stake-form/utils.ts index 4073ba4dd..1c28e8223 100644 --- a/features/home/stake-form/utils.ts +++ b/features/home/stake-form/utils.ts @@ -1,25 +1,26 @@ +import { BigNumber } from 'ethers'; +import { isAddress } from 'ethers/lib/utils'; +import invariant from 'tiny-invariant'; + import { AddressZero } from '@ethersproject/constants'; import { parseEther } from '@ethersproject/units'; -import { isAddress } from 'ethers/lib/utils'; +import type { Web3Provider } from '@ethersproject/providers'; +import { StaticJsonRpcBatchProvider } from '@lidofinance/eth-providers'; import { StethAbi } from '@lido-sdk/contracts'; -import { CHAINS } from '@lido-sdk/constants'; -import { getStaticRpcBatchProvider } from '@lido-sdk/providers'; + +import { TX_STAGE } from 'shared/components'; import { enableQaHelpers, ErrorMessage, getErrorMessage, runWithTransactionLogger, } from 'utils'; -import { getBackendRPCPath } from 'config'; -import { TX_STAGE } from 'shared/components'; -import { BigNumber } from 'ethers'; -import invariant from 'tiny-invariant'; import { getFeeData } from 'utils/getFeeData'; -import type { Web3Provider } from '@ethersproject/providers'; const SUBMIT_EXTRA_GAS_TRANSACTION_RATIO = 1.05; type StakeProcessingProps = ( + staticRpcProvider: StaticJsonRpcBatchProvider, providerWeb3: Web3Provider | undefined, stethContractWeb3: StethAbi | null, openTxModal: () => void, @@ -36,16 +37,12 @@ type StakeProcessingProps = ( export const getAddress = async ( input: string | undefined, - chainId: CHAINS | undefined, + provider: StaticJsonRpcBatchProvider, ): Promise => { - if (!input || !chainId) return ''; + if (!input) return ''; if (isAddress(input)) return input; try { - const provider = getStaticRpcBatchProvider( - chainId, - getBackendRPCPath(chainId), - ); const address = await provider.resolveName(input); if (address) return address; @@ -65,6 +62,7 @@ class MockLimitReachedError extends Error { } export const stakeProcessing: StakeProcessingProps = async ( + staticRpcProvider, providerWeb3, stethContractWeb3, openTxModal, @@ -83,7 +81,7 @@ export const stakeProcessing: StakeProcessingProps = async ( invariant(chainId); invariant(providerWeb3); - const referralAddress = await getAddress(refFromQuery, chainId); + const referralAddress = await getAddress(refFromQuery, staticRpcProvider); const callback = async () => { if (isMultisig) { const tx = await stethContractWeb3.populateTransaction.submit( @@ -94,7 +92,7 @@ export const stakeProcessing: StakeProcessingProps = async ( ); return providerWeb3.getSigner().sendUncheckedTransaction(tx); } else { - const feeData = await getFeeData(chainId); + const feeData = await getFeeData(staticRpcProvider); const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? undefined; const maxFeePerGas = feeData.maxFeePerGas ?? undefined; const overrides = { diff --git a/features/ipfs/home-page-ipfs.tsx b/features/ipfs/home-page-ipfs.tsx new file mode 100644 index 000000000..d205078e4 --- /dev/null +++ b/features/ipfs/home-page-ipfs.tsx @@ -0,0 +1,118 @@ +import { FC, useMemo, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +import { + getPathWithoutFirstSlash, + HOME_PATH, + REWARDS_PATH, + SETTINGS_PATH, + WITHDRAWALS_PATH, + WITHDRAWALS_REQUEST_PATH, + WRAP_PATH, + REFERRAL_PATH, +} from 'config/urls'; +import NoSSRWrapper from 'shared/components/no-ssr-wrapper'; +import { usePrefixedReplace } from 'shared/hooks/use-prefixed-history'; + +import HomePageRegular from 'features/home/home-page-regular'; +import WrapPage from 'pages/wrap/[[...mode]]'; +import WithdrawalsPage from 'pages/withdrawals/[mode]'; +import ReferralPage from 'pages/referral'; +import RewardsPage from 'pages/rewards'; +import SettingsPage from 'pages/settings'; + +/** + * We are using single index.html endpoint + * with hash-based routing in ipfs build mode. + * It is necessary because ipfs infrastructure does not support + * redirects to make dynamic routes workable. + */ + +const IPFS_ROUTABLE_PAGES = [ + // HOME_PATH not need here + getPathWithoutFirstSlash(WRAP_PATH), + getPathWithoutFirstSlash(WITHDRAWALS_PATH), + getPathWithoutFirstSlash(REWARDS_PATH), + getPathWithoutFirstSlash(REFERRAL_PATH), + getPathWithoutFirstSlash(SETTINGS_PATH), +]; + +const HomePageIpfs: FC = () => { + const router = useRouter(); + const { asPath } = router; + + const replace = usePrefixedReplace(); + + const parsedPath = useMemo(() => { + const hashPath = asPath.split('#')[1]; + if (!hashPath) return []; + return hashPath.split('/').splice(1); + }, [asPath]); + + useEffect(() => { + if ( + parsedPath[0] === getPathWithoutFirstSlash(WITHDRAWALS_PATH) && + !parsedPath[1] + ) { + void replace( + WITHDRAWALS_REQUEST_PATH, + router.query as Record, + ); + } + + if (parsedPath[0] && !IPFS_ROUTABLE_PAGES.includes(parsedPath[0])) { + void replace(HOME_PATH, router.query as Record); + } + }, [replace, parsedPath, router.query]); + + /** + * TODO: + * We can upgrade this routing algorithm with a `match` method + * and router config if we will need more functionality + * Example: https://v5.reactrouter.com/web/api/match + */ + let spaPage; + switch (parsedPath[0]) { + case getPathWithoutFirstSlash(WRAP_PATH): { + if (parsedPath[1] === 'unwrap') { + spaPage = ; + } else { + spaPage = ; + } + break; + } + + case getPathWithoutFirstSlash(WITHDRAWALS_PATH): { + if (parsedPath[1] === 'claim') { + spaPage = ; + } else { + spaPage = ; + } + break; + } + + case getPathWithoutFirstSlash(REWARDS_PATH): { + spaPage = ; + break; + } + + case getPathWithoutFirstSlash(REFERRAL_PATH): { + spaPage = ; + break; + } + + case getPathWithoutFirstSlash(SETTINGS_PATH): { + spaPage = ; + break; + } + + default: { + spaPage = ; + } + } + + // Fix for runtime of `dev-ipfs` (see: package.json scripts) + return {spaPage}; +}; + +export default HomePageIpfs; diff --git a/features/ipfs/ipfs-base-script.tsx b/features/ipfs/ipfs-base-script.tsx new file mode 100644 index 000000000..e8e9bd186 --- /dev/null +++ b/features/ipfs/ipfs-base-script.tsx @@ -0,0 +1,20 @@ +// IPFS Next.js configuration reference: +// https://github.com/Velenir/nextjs-ipfs-example + +let ipfsBaseScript = ''; + +// #!if IPFS_MODE === "true" +ipfsBaseScript = ` +(function () { + const base = document.createElement('base') + base.href = window.location.pathname + document.head.append(base) +})(); +`; +// #!endif + +export const InsertIpfsBaseScript = () => { + return ipfsBaseScript ? ( +