diff --git a/src/app/api/guard/route.ts b/src/app/api/guard/route.ts deleted file mode 100644 index 4164aaed9..000000000 --- a/src/app/api/guard/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -export async function GET(req: NextRequest) { - try { - return NextResponse.json({ - 'x-forwarded-for': req.headers.get('x-forwarded-for'), - 'x-real-ip': req.headers.get('x-real-ip'), - 'x-vercel-country': req.headers.get('x-vercel-ip-country'), - 'cf-connecting-ip': req.headers.get('cf-connecting-ip'), - 'cf-ipcountry': req.headers.get('cf-ipcountry'), - }) - } catch (error) { - return NextResponse.json(error, { status: 500 }) - } -} diff --git a/src/app/api/protections/route.ts b/src/app/api/protections/route.ts new file mode 100644 index 000000000..b75244b22 --- /dev/null +++ b/src/app/api/protections/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(req: NextRequest) { + try { + const address = req.nextUrl.searchParams.get('address') + const path = address + ? `/v2/protections?${new URLSearchParams({ address }).toString()}` + : '/v2/protections' + const res = await fetch(`https://api.indexcoop.com${path}`, { + headers: { + ...req.headers, + 'ic-ip-address': + req.headers.get('cf-connecting-ip') ?? + req.headers.get('x-forwarded-for') ?? + undefined, + 'ic-ip-country': + req.headers.get('cf-ipcountry') ?? + req.headers.get('x-vercel-ip-country') ?? + undefined, + }, + }) + const { isRestrictedCountry, isUsingVpn } = await res.json() + + return NextResponse.json({ + isRestrictedCountry, + isUsingVpn, + }) + } catch (e) { + console.error('Caught protections error', e) + return NextResponse.json({ + isRestrictedCountry: false, + isUsingVpn: false, + }) + } +} diff --git a/src/components/smart-trade-button/index.tsx b/src/components/smart-trade-button/index.tsx index 8b42f30c0..9643c063a 100644 --- a/src/components/smart-trade-button/index.tsx +++ b/src/components/smart-trade-button/index.tsx @@ -1,5 +1,5 @@ import { useWeb3Modal } from '@web3modal/wagmi/react' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { Warnings, WarningType } from '@/components/swap/components/warning' import { useTradeButton } from '@/components/swap/hooks/use-trade-button' @@ -11,7 +11,7 @@ import { TradeButton } from '@/components/trade-button' import { Token } from '@/constants/tokens' import { useApproval } from '@/lib/hooks/use-approval' import { useNetwork } from '@/lib/hooks/use-network' -import { useProtection } from '@/lib/providers/protection' +import { useProtectionContext } from '@/lib/providers/protection' import { useSignTerms } from '@/lib/providers/sign-terms-provider' import { useSlippage } from '@/lib/providers/slippage' import { getNativeToken, isTokenPairTradable } from '@/lib/utils/tokens' @@ -52,7 +52,7 @@ export function SmartTradeButton(props: SmartTradeButtonProps) { const { chainId } = useNetwork() const { open } = useWeb3Modal() - const requiresProtection = useProtection() + const { isRestrictedCountry, isUsingVpn } = useProtectionContext() const { signTermsOfService } = useSignTerms() const { slippage } = useSlippage() @@ -65,12 +65,18 @@ export function SmartTradeButton(props: SmartTradeButtonProps) { const isTradablePair = useMemo( () => isTokenPairTradable( - requiresProtection, + isRestrictedCountry || isUsingVpn, inputToken.symbol, outputToken.symbol, chainId ?? 1, ), - [chainId, requiresProtection, inputToken, outputToken], + [ + isRestrictedCountry, + isUsingVpn, + inputToken.symbol, + outputToken.symbol, + chainId, + ], ) const shouldApprove = useMemo(() => { @@ -94,30 +100,42 @@ export function SmartTradeButton(props: SmartTradeButtonProps) { buttonLabelOverrides, ) - const [warnings, setWarnings] = useState([]) - - useEffect(() => { - if (!isTradablePair && !hiddenWarnings?.includes(WarningType.restricted)) { - setWarnings([WarningType.restricted]) - return + const warnings: WarningType[] = useMemo(() => { + if ( + !isTradablePair && + isRestrictedCountry && + !hiddenWarnings?.includes(WarningType.restricted) + ) { + return [WarningType.restricted] + } + if ( + !isTradablePair && + isUsingVpn && + !hiddenWarnings?.includes(WarningType.vpn) + ) { + return [WarningType.vpn] } if ( buttonState === TradeButtonState.signTerms && !hiddenWarnings?.includes(WarningType.signTerms) ) { - setWarnings([WarningType.signTerms]) - return + return [WarningType.signTerms] } if (slippage > 9 && !hiddenWarnings?.includes(WarningType.priceImpact)) { - setWarnings([WarningType.priceImpact]) - return + return [WarningType.priceImpact] } if (!hiddenWarnings?.includes(WarningType.flashbots)) { - setWarnings([WarningType.flashbots]) - return + return [WarningType.flashbots] } - setWarnings([]) - }, [buttonState, hiddenWarnings, isTradablePair, slippage]) + return [] + }, [ + buttonState, + hiddenWarnings, + isRestrictedCountry, + isTradablePair, + isUsingVpn, + slippage, + ]) const onClick = useCallback(async () => { if (buttonState === TradeButtonState.connectWallet) { diff --git a/src/components/swap/components/warning.tsx b/src/components/swap/components/warning.tsx index 66fa30eee..8d0ece1e5 100644 --- a/src/components/swap/components/warning.tsx +++ b/src/components/swap/components/warning.tsx @@ -10,6 +10,7 @@ export enum WarningType { priceImpact, restricted, signTerms, + vpn, } export interface Warning { @@ -18,7 +19,10 @@ export interface Warning { } const warningsData: Record< - WarningType.priceImpact | WarningType.restricted | WarningType.signTerms, + | WarningType.priceImpact + | WarningType.restricted + | WarningType.signTerms + | WarningType.vpn, Warning > = { [WarningType.priceImpact]: { @@ -48,6 +52,30 @@ const warningsData: Record< ), }, + [WarningType.vpn]: { + title: 'Not Available to VPN Users', + node: ( + <> + It appears you may be using a VPN and, because some of our tokens are + not available to Restricted Persons - including US persons - as defined + in our{' '} + + Terms of Service + + , we are required to restrict access to VPN users. Please also see our{' '} + + Tokens Restricted for Restricted Persons + {' '} + page. + + ), + }, [WarningType.signTerms]: { title: 'Please sign the Terms and Conditions', node: ( diff --git a/src/components/swap/index.tsx b/src/components/swap/index.tsx index 9547402c3..24d73870e 100644 --- a/src/components/swap/index.tsx +++ b/src/components/swap/index.tsx @@ -13,7 +13,7 @@ import { useBestQuote } from '@/lib/hooks/use-best-quote' import { QuoteType } from '@/lib/hooks/use-best-quote/types' import { useNetwork, useSupportedNetworks } from '@/lib/hooks/use-network' import { useWallet } from '@/lib/hooks/use-wallet' -import { useProtection } from '@/lib/providers/protection' +import { useProtectionContext } from '@/lib/providers/protection' import { useSelectedToken } from '@/lib/providers/selected-token-provider' import { useSlippage } from '@/lib/providers/slippage' import { colors } from '@/lib/styles/colors' @@ -44,7 +44,7 @@ export const Swap = (props: SwapProps) => { BASE.chainId, ]) const { logEvent } = useAnalytics() - const requiresProtection = useProtection() + const { isRestrictedCountry, isUsingVpn } = useProtectionContext() const { chainId } = useNetwork() const { slippage } = useSlippage() const { address } = useWallet() @@ -52,12 +52,18 @@ export const Swap = (props: SwapProps) => { const isTradablePair = useMemo( () => isTokenPairTradable( - requiresProtection, + isRestrictedCountry || isUsingVpn, inputToken.symbol, outputToken.symbol, chainId ?? 1, ), - [chainId, requiresProtection, inputToken, outputToken], + [ + isRestrictedCountry, + isUsingVpn, + inputToken.symbol, + outputToken.symbol, + chainId, + ], ) const { diff --git a/src/lib/providers/protection/index.tsx b/src/lib/providers/protection/index.tsx index 50a5fc03e..ea7ace4f4 100644 --- a/src/lib/providers/protection/index.tsx +++ b/src/lib/providers/protection/index.tsx @@ -1,34 +1,46 @@ -import { createContext, useContext, useEffect, useState } from 'react' +'use client' -export const ProtectionContext = createContext(false) +import { useQuery } from '@tanstack/react-query' +import { createContext, useContext } from 'react' +import { useAccount } from 'wagmi' -export const useProtection = () => useContext(ProtectionContext) +interface Context { + isRestrictedCountry: boolean + isUsingVpn: boolean +} -export const ProtectionProvider = (props: { children: any }) => { - const [isProtectable, setIsProtectable] = useState(false) +export const ProtectionContext = createContext({ + isRestrictedCountry: false, + isUsingVpn: false, +}) - const checkIfProtectable = async () => { - const API_KEY = - process.env.NEXT_PUBLIC_IP_LOOKUP_KEY ?? 'vN8S4cMfz4KPoq5eLx3X' - fetch('https://extreme-ip-lookup.com/json/?key=' + API_KEY) - .then((res) => res.json()) - .then((response) => { - if (response.country === 'United States') setIsProtectable(true) - }) - .catch((error) => { - console.error( - 'Cant determine whether or not we should protect the user because of this error: ', - error, - ) - }) - } +export const useProtectionContext = () => useContext(ProtectionContext) + +export const ProtectionProvider = (props: { children: any }) => { + const { address } = useAccount() + const { + data: { isRestrictedCountry, isUsingVpn }, + } = useQuery({ + gcTime: 2 * 60 * 1000, + refetchOnWindowFocus: false, + initialData: { isRestrictedCountry: false, isUsingVpn: false }, + queryKey: ['protections', address], + queryFn: async () => { + const url = address + ? `/api/protections?${new URLSearchParams({ address }).toString()}` + : '/api/protections' + const res = await fetch(url) + const { isRestrictedCountry, isUsingVpn } = await res.json() - useEffect(() => { - checkIfProtectable() - }, []) + return { + isRestrictedCountry, + isUsingVpn, + } + }, + }) return ( - + {props.children} )