diff --git a/.env.example b/.env.example index 35bde4ae2..904c2110a 100644 --- a/.env.example +++ b/.env.example @@ -57,3 +57,4 @@ WALLETCONNECT_PROJECT_ID= # ETH Stake Widget API for IPFS mode WIDGET_API_BASE_PATH_FOR_IPFS= + diff --git a/.prettierrc b/.prettierrc index b8f95aec6..4e3503753 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,5 +2,6 @@ "useTabs": false, "singleQuote": true, "tabWidth": 2, - "trailingComma": "all" + "trailingComma": "all", + "printWidth": 80 } diff --git a/IPFS.json b/IPFS.json new file mode 100644 index 000000000..1538a25da --- /dev/null +++ b/IPFS.json @@ -0,0 +1,4 @@ +{ + "__warning__": "For testing purposes only", + "cid": "bafybeightexodcodz3srlseidd6olah7lezz4tmyeiy7qod6oz2ol45r3i" +} diff --git a/assets/icons/attention-triangle-ipfs.svg b/assets/icons/attention-triangle-ipfs.svg new file mode 100644 index 000000000..3d09ced4a --- /dev/null +++ b/assets/icons/attention-triangle-ipfs.svg @@ -0,0 +1,3 @@ + + + diff --git a/env-dynamics.mjs b/env-dynamics.mjs index 60d85d5c3..7c97639b1 100644 --- a/env-dynamics.mjs +++ b/env-dynamics.mjs @@ -37,13 +37,17 @@ export const walletconnectProjectId = process.env.WALLETCONNECT_PROJECT_ID; export const ipfsMode = toBoolean(process.env.IPFS_MODE); /** @type string[] */ -export const prefillUnsafeElRpcUrls1 = process.env.PREFILL_UNSAFE_EL_RPC_URLS_1?.split(',') ?? []; +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(',') ?? []; +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(',') ?? []; +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; +export const widgetApiBasePathForIpfs = + process.env.WIDGET_API_BASE_PATH_FOR_IPFS; diff --git a/features/ipfs/ipfs-base-script.tsx b/features/ipfs/ipfs-base-script.tsx index e8e9bd186..6e053e9fc 100644 --- a/features/ipfs/ipfs-base-script.tsx +++ b/features/ipfs/ipfs-base-script.tsx @@ -6,9 +6,9 @@ let ipfsBaseScript = ''; // #!if IPFS_MODE === "true" ipfsBaseScript = ` (function () { - const base = document.createElement('base') - base.href = window.location.pathname - document.head.append(base) + const base = document.createElement('base'); + base.href = window.location.pathname; + document.head.append(base); })(); `; // #!endif diff --git a/features/ipfs/outdated-hash-banner/index.ts b/features/ipfs/outdated-hash-banner/index.ts new file mode 100644 index 000000000..2c9cc7d86 --- /dev/null +++ b/features/ipfs/outdated-hash-banner/index.ts @@ -0,0 +1 @@ +export { OutdatedHashBanner } from './outdated-hash-banner'; diff --git a/features/ipfs/outdated-hash-banner/outdated-hash-banner.tsx b/features/ipfs/outdated-hash-banner/outdated-hash-banner.tsx new file mode 100644 index 000000000..98e1a4028 --- /dev/null +++ b/features/ipfs/outdated-hash-banner/outdated-hash-banner.tsx @@ -0,0 +1,43 @@ +import { Button, Modal } from '@lidofinance/lido-ui'; +import { dynamics } from 'config'; +import { WarningIcon, Wrapper, WarningText } from './styles'; +import { useIpfsHashCheck } from './use-ipfs-hash-check'; +import NoSsrWrapper from 'shared/components/no-ssr-wrapper'; + +export const OutdatedHashBanner = dynamics.ipfsMode + ? () => { + const { isUpdateAvailable, data, setConditionsAccepted } = + useIpfsHashCheck(); + + return ( + + + + + + This is not the most recent version of IPFS Widget + + + + + + + + + ); + } + : () => null; diff --git a/features/ipfs/outdated-hash-banner/styles.tsx b/features/ipfs/outdated-hash-banner/styles.tsx new file mode 100644 index 000000000..41fd9ad68 --- /dev/null +++ b/features/ipfs/outdated-hash-banner/styles.tsx @@ -0,0 +1,38 @@ +import styled from 'styled-components'; +import WarningIconSrc from 'assets/icons/attention-triangle-ipfs.svg'; +import { Text } from '@lidofinance/lido-ui'; + +export const WarningIcon = styled.img.attrs({ + src: WarningIconSrc, + alt: 'warning', +})` + display: block; + width: 58px; + height: 51px; +`; + +export const Wrapper = styled.div` + display: flex; + align-items: center; + flex-direction: column; + gap: 12px; + padding: 0px; + + button { + white-space: unset; + } + + a { + align-self: stretch; + width: 100%; + } +`; + +export const WarningText = styled(Text).attrs({ + weight: 700, + size: 'lg', +})` + text-align: center; + margin: 12px 0 28px; + text-wrap: balance; +`; diff --git a/features/ipfs/outdated-hash-banner/use-ipfs-hash-check.ts b/features/ipfs/outdated-hash-banner/use-ipfs-hash-check.ts new file mode 100644 index 000000000..2da1979cb --- /dev/null +++ b/features/ipfs/outdated-hash-banner/use-ipfs-hash-check.ts @@ -0,0 +1,103 @@ +import { useLidoSWR } from '@lido-sdk/react'; +import { BASE_PATH_ASSET, dynamics } from 'config'; +import { useState } from 'react'; +import { useMainnetStaticRpcProvider } from 'shared/hooks/use-mainnet-static-rpc-provider'; +import { standardFetcher } from 'utils/standardFetcher'; +import { STRATEGY_IMMUTABLE, STRATEGY_LAZY } from 'utils/swrStrategies'; + +type EnsHashCheckReturn = { + cid: string; + ens?: string; + link: string; +} | null; + +type ReleaseInfo = { + cid?: string; + ens?: string; +}; + +// works with any type of IPFS hash +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,})([./#?]|$)/; + +// for dev and local testing you can set to '/runtime/IPFS.json' and have file at /public/runtime/ +const IPFS_RELEASE_URL = + 'https://raw.githubusercontent.com/lidofinance/ethereum-staking-widget/main/IPFS.json'; + +export const useIpfsHashCheck = () => { + const [areConditionsAccepted, setConditionsAccepted] = useState(false); + const provider = useMainnetStaticRpcProvider(); + + // local cid extraction + const currentCidSWR = useLidoSWR( + ['swr:ipfs-cid-extraction'], + async () => { + const urlCid = URL_CID_REGEX.exec(window.location.href)?.groups?.cid; + if (urlCid) return urlCid; + const headers = await fetch(`${BASE_PATH_ASSET}/runtime/window-env.js`, { + method: 'HEAD', + }); + return headers.headers.get('X-Ipfs-Roots'); + }, + { ...STRATEGY_IMMUTABLE, isPaused: () => !dynamics.ipfsMode }, + ); + + // ens cid extraction + const remoteCidSWR = useLidoSWR( + ['swr:ipfs-hash-check'], + async (): Promise => { + const releaseInfo = await standardFetcher(IPFS_RELEASE_URL, { + headers: { Accept: 'application/json' }, + }); + if (releaseInfo.ens) { + const resolver = await provider.getResolver(releaseInfo.ens); + if (resolver) { + const contentHash = await resolver.getContentHash(); + if (contentHash) { + return { + cid: contentHash, + ens: releaseInfo.ens, + link: `https://${releaseInfo.ens}.link`, + }; + } + } + } + if (releaseInfo.cid) { + return { + cid: releaseInfo.cid, + link: `https://${releaseInfo.cid}.ipfs.cf-ipfs.com`, + }; + } + return null; + }, + { ...STRATEGY_LAZY, isPaused: () => !dynamics.ipfsMode }, + ); + + const isUpdateAvailable = Boolean( + !areConditionsAccepted && + remoteCidSWR.data && + currentCidSWR.data && + remoteCidSWR.data.cid !== currentCidSWR.data, + ); + + return { + isUpdateAvailable, + setConditionsAccepted, + get data() { + return { + remoteCid: remoteCidSWR.data?.cid, + currentCid: currentCidSWR.data, + remoteCidLink: remoteCidSWR.data?.link, + }; + }, + get initialLoading() { + return remoteCidSWR.initialLoading || currentCidSWR.initialLoading; + }, + get loading() { + return remoteCidSWR.loading || currentCidSWR.loading; + }, + get error() { + return remoteCidSWR.error || currentCidSWR.error; + }, + }; +}; diff --git a/features/stake/stake.tsx b/features/stake/stake.tsx index 1fec99d76..7f89b7f2a 100644 --- a/features/stake/stake.tsx +++ b/features/stake/stake.tsx @@ -1,10 +1,11 @@ +import { dynamics } from 'config'; import { useWeb3Key } from 'shared/hooks/useWeb3Key'; import NoSSRWrapper from 'shared/components/no-ssr-wrapper'; +import { GoerliSunsetBanner } from 'shared/banners/goerli-sunset'; import { StakeFaq } from './stake-faq/stake-faq'; import { LidoStats } from './lido-stats/lido-stats'; import { StakeForm } from './stake-form'; -import { GoerliSunsetBanner } from 'shared/banners/goerli-sunset'; export const Stake = () => { const key = useWeb3Key(); @@ -15,7 +16,7 @@ export const Stake = () => { - + {!dynamics.ipfsMode && } ); }; diff --git a/features/withdrawals/claim/claim.tsx b/features/withdrawals/claim/claim.tsx index 4590c584c..19f0c6b98 100644 --- a/features/withdrawals/claim/claim.tsx +++ b/features/withdrawals/claim/claim.tsx @@ -1,3 +1,4 @@ +import { dynamics } from 'config'; import { TransactionModalProvider } from 'shared/transaction-modal/transaction-modal-context'; import { ClaimFaq } from 'features/withdrawals/withdrawals-faq/claim-faq'; @@ -12,7 +13,7 @@ export const Claim = () => { - + {!dynamics.ipfsMode && } diff --git a/features/withdrawals/request/request.tsx b/features/withdrawals/request/request.tsx index c6458178b..08b1bf5db 100644 --- a/features/withdrawals/request/request.tsx +++ b/features/withdrawals/request/request.tsx @@ -1,3 +1,4 @@ +import { dynamics } from 'config'; import { RequestFormProvider } from './request-form-context'; import { RequestFaq } from '../withdrawals-faq/request-faq'; import { RequestForm } from './form'; @@ -11,7 +12,7 @@ export const Request = () => { - + {!dynamics.ipfsMode && } diff --git a/features/withdrawals/withdrawals-tabs.tsx b/features/withdrawals/withdrawals-tabs.tsx index 5011b0a03..a3af59604 100644 --- a/features/withdrawals/withdrawals-tabs.tsx +++ b/features/withdrawals/withdrawals-tabs.tsx @@ -24,6 +24,7 @@ export const WithdrawalsTabs = () => { return ( + {isClaimTab ? : } diff --git a/features/wsteth/wrap-unwrap-tabs.tsx b/features/wsteth/wrap-unwrap-tabs.tsx index 002fd1443..3e8f860b7 100644 --- a/features/wsteth/wrap-unwrap-tabs.tsx +++ b/features/wsteth/wrap-unwrap-tabs.tsx @@ -1,3 +1,4 @@ +import { dynamics } from 'config'; import { WRAP_PATH, WRAP_UNWRAP_PATH } from 'config/urls'; import { Wallet } from 'features/wsteth/shared/wallet'; import { WrapForm } from 'features/wsteth/wrap/wrap-form/wrap-form'; @@ -27,7 +28,7 @@ export const WrapUnwrapTabs = ({ mode }: WrapUnwrapLayoutProps) => { {isUnwrapMode ? : } - + {!dynamics.ipfsMode && } ); }; diff --git a/pages/_app.tsx b/pages/_app.tsx index 4387ff2dd..3fd9068f7 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -14,6 +14,7 @@ import { Providers } from 'providers'; import { BackgroundGradient } from 'shared/components/background-gradient/background-gradient'; import { nprogress, COOKIES_ALLOWED_FULL_KEY } from 'utils'; import { withCsp } from 'utilsApi/withCSP'; +import { OutdatedHashBanner } from 'features/ipfs/outdated-hash-banner'; // Migrations old theme cookies to new cross domain cookies migrationThemeCookiesToCrossDomainCookiesClientSide(); @@ -47,6 +48,7 @@ const AppWrapper = (props: AppProps): JSX.Element => { + ); }; diff --git a/utilsApi/withCSP.ts b/utilsApi/withCSP.ts index 49c078529..d881688fb 100644 --- a/utilsApi/withCSP.ts +++ b/utilsApi/withCSP.ts @@ -25,15 +25,19 @@ export const contentSecurityPolicy: ContentSecurityPolicyOption = { 'https://*.walletconnect.org', 'https://*.walletconnect.com', ], - scriptSrc: ["'self'", "'unsafe-inline'", ...trustedHosts], + scriptSrc: [ + "'self'", + "'unsafe-inline'", + ...(developmentMode ? ["'unsafe-eval'"] : []), // for HMR + ...trustedHosts, + ], // Allow fetch connections to any secure host connectSrc: [ "'self'", 'https:', 'wss:', - // for HMR - ...(developmentMode ? ['ws:'] : []), + ...(developmentMode ? ['ws:'] : []), // for HMR ], ...(!dynamics.ipfsMode && { @@ -48,7 +52,7 @@ export const contentSecurityPolicy: ContentSecurityPolicyOption = { 'https://*.walletconnect.com', ], workerSrc: ["'none'"], - 'base-uri': ["'none'"], + 'base-uri': dynamics.ipfsMode ? undefined : ["'none'"], }, reportOnly, };