diff --git a/features/home/one-inch-info/one-inch-info.tsx b/features/home/one-inch-info/one-inch-info.tsx
index 46394e854..5fa307578 100644
--- a/features/home/one-inch-info/one-inch-info.tsx
+++ b/features/home/one-inch-info/one-inch-info.tsx
@@ -24,7 +24,7 @@ const ONE_INCH_RATE_LIMIT = 1.004;
export const OneInchInfo: FC = () => {
const linkProps = use1inchLinkProps();
- const apiOneInchRatePath = 'api/oneinch-rate?token=eth';
+ const apiOneInchRatePath = 'api/oneinch-rate/?token=eth';
const { data, initialLoading } = useLidoSWR<{ rate: number }>(
dynamics.ipfsMode
? `${dynamics.widgetApiBasePathForIpfs}/${apiOneInchRatePath}`
diff --git a/features/ipfs/csp-violation-box/csp-violation-box.tsx b/features/ipfs/csp-violation-box/csp-violation-box.tsx
new file mode 100644
index 000000000..e98c6a11d
--- /dev/null
+++ b/features/ipfs/csp-violation-box/csp-violation-box.tsx
@@ -0,0 +1,31 @@
+import { Wrap, InfoLink, Text } from './styles';
+
+const GATEWAY_CHECKER_URL = 'https://ipfs.github.io/public-gateway-checker/';
+const DESKTOP_APP_URL = 'https://github.com/ipfs/ipfs-desktop#ipfs-desktop';
+
+export const CSPViolationBox = () => {
+ return (
+
+
+ Insufficient Gateway CSP
+
+
+ RPC requests are blocked by Content Security Policy set by IPFS Gateway
+ service you are using.
+
+
+ Possible actions:
+
+
+ Use another gateway service:
+
+ Public Gateway Checker
+
+
+ Or install your IPFS Gateway:
+
+ IPFS Desktop
+
+
+ );
+};
diff --git a/features/ipfs/csp-violation-box/index.tsx b/features/ipfs/csp-violation-box/index.tsx
new file mode 100644
index 000000000..3070ef119
--- /dev/null
+++ b/features/ipfs/csp-violation-box/index.tsx
@@ -0,0 +1 @@
+export * from './csp-violation-box';
diff --git a/features/ipfs/csp-violation-box/styles.tsx b/features/ipfs/csp-violation-box/styles.tsx
new file mode 100644
index 000000000..a74907368
--- /dev/null
+++ b/features/ipfs/csp-violation-box/styles.tsx
@@ -0,0 +1,35 @@
+import { ComponentProps, FC } from 'react';
+import styled from 'styled-components';
+import { Text as TextOriginal, themeDefault } from '@lidofinance/lido-ui';
+import { LinkArrow } from 'shared/components/link-arrow/link-arrow';
+
+type TextProps = Omit, 'color'> & {
+ color?: keyof typeof themeDefault.colors;
+};
+export const Text: FC = styled(TextOriginal)`
+ color: ${({ color }) => `var(--lido-color-${color})`};
+`;
+
+export const Wrap = styled.div`
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ border-radius: ${({ theme }) => theme.borderRadiusesMap.sm}px;
+ padding: ${({ theme }) => theme.spaceMap.md}px
+ ${({ theme }) => theme.spaceMap.md}px;
+
+ color: var(--lido-color-accentContrast);
+ background-color: var(--lido-color-error);
+`;
+
+export const InfoLink = styled(LinkArrow)`
+ font-weight: 700;
+
+ &,
+ &:visited,
+ &:hover {
+ color: var(--lido-color-accentContrast);
+ }
+`;
diff --git a/features/ipfs/csp-violation-box/use-csp-violation.ts b/features/ipfs/csp-violation-box/use-csp-violation.ts
new file mode 100644
index 000000000..bed757267
--- /dev/null
+++ b/features/ipfs/csp-violation-box/use-csp-violation.ts
@@ -0,0 +1,20 @@
+import { useEffect, useState } from 'react';
+
+export const useCSPViolation = () => {
+ const [isCSPViolated, setCSPViolated] = useState(false);
+
+ useEffect(() => {
+ const handler = () => {
+ setCSPViolated(true);
+ };
+ document.addEventListener('securitypolicyviolation', handler);
+
+ return () => {
+ document.removeEventListener('securitypolicyviolation', handler);
+ };
+ }, []);
+
+ return {
+ isCSPViolated,
+ };
+};
diff --git a/features/ipfs/ipfs-info-box/ipfs-info-box.tsx b/features/ipfs/ipfs-info-box/ipfs-info-box.tsx
index 695c7adf9..3abc57e66 100644
--- a/features/ipfs/ipfs-info-box/ipfs-info-box.tsx
+++ b/features/ipfs/ipfs-info-box/ipfs-info-box.tsx
@@ -1,102 +1,15 @@
-import { useCallback } from 'react';
-import { useLidoSWR, useLocalStorage, useSDK } from '@lido-sdk/react';
+import { useIPFSInfoBoxStatuses } from 'providers/ipfs-info-box-statuses';
-import { useRpcUrl } from 'config/rpc';
-import { SETTINGS_PATH } from 'config/urls';
-import { usePrefixedPush } from 'shared/hooks/use-prefixed-history';
-import { useRouterPath } from 'shared/hooks/use-router-path';
-import { LinkArrow } from 'shared/components/link-arrow/link-arrow';
-
-import { Check, Close } from '@lidofinance/lido-ui';
-import { Wrap, RpcStatusBox, Button, Text } from './styles';
-
-import { checkRpcUrl } from 'utils/check-rpc-url';
-import { STORAGE_IPFS_INFO_DISMISS } from 'config/storage';
-
-const IPFS_INFO_URL = 'https://docs.ipfs.tech/concepts/what-is-ipfs/';
+import { CSPViolationBox } from '../csp-violation-box';
+import { RPCAvailabilityCheckResultBox } from '../rpc-availability-check-result-box';
export const IPFSInfoBox = () => {
- const { chainId } = useSDK();
- const push = usePrefixedPush();
- const [isDismissed, setDismissStorage] = useLocalStorage(
- STORAGE_IPFS_INFO_DISMISS,
- false,
- );
-
- const rpcUrl = useRpcUrl();
- const { data: rpcCheckResult, initialLoading: isLoading } = useLidoSWR(
- `rpc-url-check-${rpcUrl}-${chainId}`,
- async () => {
- return await checkRpcUrl(rpcUrl, chainId);
- },
- );
-
- const handleClickDismiss = useCallback(() => {
- setDismissStorage(true);
- }, [setDismissStorage]);
-
- const handleClickSettings = useCallback(() => {
- void push(SETTINGS_PATH);
- setDismissStorage(true);
- }, [push, setDismissStorage]);
+ const { isCSPViolated, isShownTheRPCNotAvailableBox } =
+ useIPFSInfoBoxStatuses();
- const pathname = useRouterPath();
- const isSettingsPage = pathname === SETTINGS_PATH;
+ if (isCSPViolated) return ;
- if ((isDismissed && rpcCheckResult === true) || isLoading || isSettingsPage) {
- return null;
- }
+ if (isShownTheRPCNotAvailableBox) return ;
- return (
-
-
- You are currently using the IPFS widget's version.
-
- IPFS
- {rpcCheckResult === true && (
- <>
-
-
- The pre-installed RPC node URL is now functioning correctly.
-
-
-
-
- However, you can visit the settings if you wish to customize your
- own RPC node URL.
-
-
- >
- )}
- {rpcCheckResult !== true && (
- <>
-
-
- The pre-installed RPC node URL is not now functioning.
-
-
-
-
- You should visit the settings page and specify your own RPC node
- URL.
-
-
- >
- )}
-
- );
+ return null;
};
diff --git a/features/ipfs/rpc-availability-check-result-box/index.tsx b/features/ipfs/rpc-availability-check-result-box/index.tsx
new file mode 100644
index 000000000..b2a30bb28
--- /dev/null
+++ b/features/ipfs/rpc-availability-check-result-box/index.tsx
@@ -0,0 +1 @@
+export * from './rpc-availability-check-result-box';
diff --git a/features/ipfs/rpc-availability-check-result-box/rpc-availability-check-result-box.tsx b/features/ipfs/rpc-availability-check-result-box/rpc-availability-check-result-box.tsx
new file mode 100644
index 000000000..fa2488ac1
--- /dev/null
+++ b/features/ipfs/rpc-availability-check-result-box/rpc-availability-check-result-box.tsx
@@ -0,0 +1,75 @@
+import { useCallback } from 'react';
+import { Check, Close } from '@lidofinance/lido-ui';
+
+import { SETTINGS_PATH } from 'config/urls';
+import { useIPFSInfoBoxStatuses } from 'providers/ipfs-info-box-statuses';
+import { usePrefixedPush } from 'shared/hooks/use-prefixed-history';
+import { LinkArrow } from 'shared/components/link-arrow/link-arrow';
+
+import { Wrap, RpcStatusBox, Button, Text } from './styles';
+
+const IPFS_INFO_URL = 'https://docs.ipfs.tech/concepts/what-is-ipfs/';
+
+export const RPCAvailabilityCheckResultBox = () => {
+ const { isRPCAvailable, handleClickDismiss } = useIPFSInfoBoxStatuses();
+
+ const push = usePrefixedPush();
+
+ const handleClickSettings = useCallback(() => {
+ void push(SETTINGS_PATH);
+ handleClickDismiss();
+ }, [push, handleClickDismiss]);
+
+ return (
+
+
+ You are currently using the IPFS widget's version.
+
+ IPFS
+ {isRPCAvailable && (
+ <>
+
+
+ The pre-installed RPC node URL is now functioning correctly.
+
+
+
+
+ However, you can visit the settings if you wish to customize your
+ own RPC node URL.
+
+
+ >
+ )}
+ {!isRPCAvailable && (
+ <>
+
+
+ The pre-installed RPC node URL is not now functioning.
+
+
+
+
+ You should visit the settings page and specify your own RPC node
+ URL.
+
+
+ >
+ )}
+
+ );
+};
diff --git a/features/ipfs/ipfs-info-box/styles.tsx b/features/ipfs/rpc-availability-check-result-box/styles.tsx
similarity index 100%
rename from features/ipfs/ipfs-info-box/styles.tsx
rename to features/ipfs/rpc-availability-check-result-box/styles.tsx
diff --git a/features/rewards/components/rewardsListContent/RewardsListContent.tsx b/features/rewards/components/rewardsListContent/RewardsListContent.tsx
index 28782a615..a940cc100 100644
--- a/features/rewards/components/rewardsListContent/RewardsListContent.tsx
+++ b/features/rewards/components/rewardsListContent/RewardsListContent.tsx
@@ -5,7 +5,11 @@ import { ErrorBlockNoSteth } from 'features/rewards/components/errorBlocks/Error
import { RewardsListsEmpty } from './RewardsListsEmpty';
import { RewardsListErrorMessage } from './RewardsListErrorMessage';
-import { LoaderWrapper, TableWrapperStyle } from './RewardsListContentStyles';
+import {
+ LoaderWrapper,
+ TableWrapperStyle,
+ ErrorWrapper,
+} from './RewardsListContentStyles';
import { RewardsTable } from 'features/rewards/components/rewardsTable';
export const RewardsListContent: FC = () => {
@@ -31,7 +35,13 @@ export const RewardsListContent: FC = () => {
>
);
}
- if (error) return ;
+ if (error) {
+ return (
+
+
+
+ );
+ }
if (data && data.events.length === 0) return ;
return (
diff --git a/features/rewards/components/rewardsListContent/RewardsListContentStyles.ts b/features/rewards/components/rewardsListContent/RewardsListContentStyles.ts
index 4ca31d086..292f7f3ca 100644
--- a/features/rewards/components/rewardsListContent/RewardsListContentStyles.ts
+++ b/features/rewards/components/rewardsListContent/RewardsListContentStyles.ts
@@ -8,3 +8,7 @@ export const LoaderWrapper = styled.div`
export const TableWrapperStyle = styled.div`
margin-top: 20px;
`;
+
+export const ErrorWrapper = styled.div`
+ word-wrap: break-word;
+`;
diff --git a/features/rewards/components/rewardsListHeader/styles.ts b/features/rewards/components/rewardsListHeader/styles.ts
index c58a4a178..98190d54b 100644
--- a/features/rewards/components/rewardsListHeader/styles.ts
+++ b/features/rewards/components/rewardsListHeader/styles.ts
@@ -10,7 +10,7 @@ export const RewardsListHeaderStyle = styled.div`
color: ${({ theme }) => theme.colors.secondary};
- ${({ theme }) => theme.mediaQueries.md} {
+ ${({ theme }) => theme.mediaQueries.lg} {
flex-direction: column;
height: auto;
align-items: initial;
@@ -28,6 +28,7 @@ export const LeftOptionsWrapper = styled.div`
flex-wrap: wrap;
margin-right: auto;
gap: 16px;
+
${({ theme }) => theme.mediaQueries.lg} {
order: 3;
margin-right: 0;
@@ -36,6 +37,10 @@ export const LeftOptionsWrapper = styled.div`
flex: 1 0;
}
}
+
+ ${({ theme }) => theme.mediaQueries.md} {
+ flex-direction: column;
+ }
`;
export const RightOptionsWrapper = styled.div`
diff --git a/features/rewards/fetchers/requesters/json/backend.ts b/features/rewards/fetchers/requesters/json/backend.ts
index b27738277..202a32336 100644
--- a/features/rewards/fetchers/requesters/json/backend.ts
+++ b/features/rewards/fetchers/requesters/json/backend.ts
@@ -1,3 +1,5 @@
+import { dynamics } from 'config';
+
export type BackendQuery = {
address: string;
currency?: string;
@@ -12,7 +14,11 @@ export const backendRequest = async (query: BackendQuery) => {
Object.entries(query).forEach(([k, v]) => params.append(k, v.toString()));
- const requested = await fetch(`/api/rewards?${params.toString()}`);
+ const apiRewardsPath = `/api/rewards/?${params.toString()}`;
+ const apiRewardsUrl = dynamics.ipfsMode
+ ? `${dynamics.widgetApiBasePathForIpfs}${apiRewardsPath}`
+ : apiRewardsPath;
+ const requested = await fetch(apiRewardsUrl);
if (!requested.ok) {
const responded = await requested.json();
diff --git a/features/rewards/hooks/useRewardsDataLoad.ts b/features/rewards/hooks/useRewardsDataLoad.ts
index 6f41da2c4..d07e62008 100644
--- a/features/rewards/hooks/useRewardsDataLoad.ts
+++ b/features/rewards/hooks/useRewardsDataLoad.ts
@@ -1,5 +1,6 @@
-import { Backend } from 'features/rewards/types';
import { useEffect, useRef } from 'react';
+import { Backend } from 'features/rewards/types';
+import { dynamics } from 'config';
import { useLidoSWR } from 'shared/hooks';
import { swrAbortableMiddleware } from 'utils';
@@ -43,8 +44,14 @@ export const useRewardsDataLoad: UseRewardsDataLoad = (props) => {
Object.entries(requestOptions).forEach(([k, v]) =>
params.append(k, v.toString()),
);
+
+ const apiRewardsPath = `/api/rewards/?${params.toString()}`;
+ const apiRewardsUrl = dynamics.ipfsMode
+ ? `${dynamics.widgetApiBasePathForIpfs}${apiRewardsPath}`
+ : apiRewardsPath;
+
const { data, ...rest } = useLidoSWR(
- address ? `/api/rewards?${params.toString()}` : null,
+ address ? apiRewardsUrl : null,
{
shouldRetryOnError: false,
revalidateOnFocus: false,
diff --git a/features/withdrawals/hooks/useWithdrawalRates.ts b/features/withdrawals/hooks/useWithdrawalRates.ts
index 7b3b635ee..9282ae24d 100644
--- a/features/withdrawals/hooks/useWithdrawalRates.ts
+++ b/features/withdrawals/hooks/useWithdrawalRates.ts
@@ -60,7 +60,7 @@ const getOneInchRate: GetRateType = async (amount, token) => {
};
}
- const apiOneInchRatePath = `api/oneinch-rate?token=${token}`;
+ const apiOneInchRatePath = `api/oneinch-rate/?token=${token}`;
const respData = await standardFetcher<{ rate: string }>(
dynamics.ipfsMode
? `${dynamics.widgetApiBasePathForIpfs}/${apiOneInchRatePath}`
diff --git a/features/withdrawals/request/form/options/dex-options.tsx b/features/withdrawals/request/form/options/dex-options.tsx
index 70cfd35aa..cc9cfecd3 100644
--- a/features/withdrawals/request/form/options/dex-options.tsx
+++ b/features/withdrawals/request/form/options/dex-options.tsx
@@ -99,7 +99,7 @@ const DexOption: React.FC = ({
Go to {title}
- {loading && }
+ {loading && !toReceive && }
{toReceive ? (
)
: data?.map(({ name, toReceive, rate }) => {
const dex = dexInfo[name];
- if (!dex) return null;
+ if (!dex || (amount.gt('0') && !rate)) return null;
return (
= ({ children }) => (
- {children}
+
+ {children}
+
diff --git a/providers/ipfs-info-box-statuses.tsx b/providers/ipfs-info-box-statuses.tsx
new file mode 100644
index 000000000..59fd51260
--- /dev/null
+++ b/providers/ipfs-info-box-statuses.tsx
@@ -0,0 +1,93 @@
+import {
+ FC,
+ PropsWithChildren,
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+} from 'react';
+import { useLidoSWR, useLocalStorage, useSDK } from '@lido-sdk/react';
+import invariant from 'tiny-invariant';
+
+import { useCSPViolation } from 'features/ipfs/csp-violation-box/use-csp-violation';
+import { useRpcUrl } from 'config/rpc';
+import { STORAGE_IPFS_INFO_DISMISS } from 'config/storage';
+import { SETTINGS_PATH } from 'config/urls';
+
+import { useRouterPath } from 'shared/hooks/use-router-path';
+
+import { STRATEGY_LAZY } from 'utils/swrStrategies';
+import { checkRpcUrl } from 'utils/check-rpc-url';
+
+type IPFSInfoBoxStatusesContextValue = {
+ isCSPViolated: boolean;
+ isShownTheRPCNotAvailableBox: boolean;
+ isRPCAvailable: boolean;
+ handleClickDismiss: () => void;
+};
+
+const IPFSInfoBoxStatusContext =
+ createContext(null);
+
+export const useIPFSInfoBoxStatuses = () => {
+ const value = useContext(IPFSInfoBoxStatusContext);
+ invariant(value, 'useIPFSInfoBoxStatuses was called outside the provider');
+ return value;
+};
+
+export const IPFSInfoBoxStatusesProvider: FC = ({
+ children,
+}) => {
+ const { chainId } = useSDK();
+
+ // CSP violation box
+ const { isCSPViolated } = useCSPViolation();
+
+ // RPC availability check result box
+ const [isDismissed, setDismissStorage] = useLocalStorage(
+ STORAGE_IPFS_INFO_DISMISS,
+ false,
+ );
+
+ const handleClickDismiss = useCallback(() => {
+ setDismissStorage(true);
+ }, [setDismissStorage]);
+
+ const rpcUrl = useRpcUrl();
+ const { data: isRPCAvailableRaw, initialLoading: isLoading } = useLidoSWR(
+ `rpc-url-check-${rpcUrl}-${chainId}`,
+ async () => await checkRpcUrl(rpcUrl, chainId),
+ STRATEGY_LAZY,
+ );
+ const isRPCAvailable = isRPCAvailableRaw === true;
+
+ const pathname = useRouterPath();
+ const isSettingsPage = pathname === SETTINGS_PATH;
+
+ const isShownTheRPCNotAvailableBox =
+ (!isDismissed || !isRPCAvailable) &&
+ !isLoading &&
+ !isSettingsPage &&
+ !isCSPViolated;
+
+ const contextValue = useMemo(
+ () => ({
+ isCSPViolated,
+ isShownTheRPCNotAvailableBox,
+ isRPCAvailable,
+ handleClickDismiss,
+ }),
+ [
+ handleClickDismiss,
+ isCSPViolated,
+ isRPCAvailable,
+ isShownTheRPCNotAvailableBox,
+ ],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/shared/components/header/components/header-wallet.tsx b/shared/components/header/components/header-wallet.tsx
index 2745b210a..86ade2496 100644
--- a/shared/components/header/components/header-wallet.tsx
+++ b/shared/components/header/components/header-wallet.tsx
@@ -10,7 +10,11 @@ import { Button, Connect } from 'shared/wallet';
import NoSSRWrapper from 'shared/components/no-ssr-wrapper';
import { HeaderSettingsButton } from './header-settings-button';
-import { HeaderWalletChainStyle, DotStyle, IPFSInfoBoxWrap } from '../styles';
+import {
+ HeaderWalletChainStyle,
+ DotStyle,
+ IPFSInfoBoxWrapper,
+} from '../styles';
const HeaderWallet: FC = () => {
const { active } = useWeb3();
@@ -38,9 +42,9 @@ const HeaderWallet: FC = () => {
{dynamics.ipfsMode && }
{dynamics.ipfsMode && (
-
+
-
+
)}
);
diff --git a/shared/components/header/styles.tsx b/shared/components/header/styles.tsx
index 35c2acbe9..637154fa5 100644
--- a/shared/components/header/styles.tsx
+++ b/shared/components/header/styles.tsx
@@ -39,7 +39,7 @@ export const DotStyle = styled.p`
margin-right: 6px;
`;
-export const IPFSInfoBoxWrap = styled.div`
+export const IPFSInfoBoxWrapper = styled.div`
position: absolute;
right: 0;
top: calc(100% + 15px);
diff --git a/shared/hooks/useLidoStats.ts b/shared/hooks/useLidoStats.ts
index 1fd7b3095..2f23daedc 100644
--- a/shared/hooks/useLidoStats.ts
+++ b/shared/hooks/useLidoStats.ts
@@ -21,7 +21,7 @@ export const useLidoStats = (): {
initialLoading: boolean;
} => {
const { chainId } = useSDK();
- const apiShortLidoStatsPath = `api/short-lido-stats?chainId=${chainId}`;
+ const apiShortLidoStatsPath = `api/short-lido-stats/?chainId=${chainId}`;
const lidoStats = useLidoSWR(
dynamics.ipfsMode
? `${dynamics.widgetApiBasePathForIpfs}/${apiShortLidoStatsPath}`