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,
};