Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improve protection logic #1732

Merged
merged 16 commits into from
Jan 21, 2025
Merged
15 changes: 0 additions & 15 deletions src/app/api/guard/route.ts

This file was deleted.

36 changes: 36 additions & 0 deletions src/app/api/protections/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextRequest, NextResponse } from 'next/server'

export async function GET(req: NextRequest) {
try {
const address = req.nextUrl.searchParams.get('address')
const path = address
? `/api/v2/protections?${new URLSearchParams({ address }).toString()}`
: '/api/v2/protections'
// FIXME: Update hostname
const res = await fetch(`https://api-pr-29-80wy.onrender.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,
Comment on lines +12 to +19
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallbacks are there so the code works on preview deployments (i.e. not passing through the cloudflare proxy)

},
})
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,
})
}
}
37 changes: 31 additions & 6 deletions src/components/smart-trade-button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()

Expand All @@ -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(() => {
Expand All @@ -97,10 +103,22 @@ export function SmartTradeButton(props: SmartTradeButtonProps) {
const [warnings, setWarnings] = useState<WarningType[]>([])

useEffect(() => {
if (!isTradablePair && !hiddenWarnings?.includes(WarningType.restricted)) {
if (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im thinking there is no effect here, so instead of an effect and a state you could just use useMemo, and return the result

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tokdaniel good call, updated to useMemo 👍

!isTradablePair &&
isRestrictedCountry &&
!hiddenWarnings?.includes(WarningType.restricted)
) {
setWarnings([WarningType.restricted])
return
}
if (
!isTradablePair &&
isUsingVpn &&
!hiddenWarnings?.includes(WarningType.vpn)
) {
setWarnings([WarningType.vpn])
return
}
if (
buttonState === TradeButtonState.signTerms &&
!hiddenWarnings?.includes(WarningType.signTerms)
Expand All @@ -117,7 +135,14 @@ export function SmartTradeButton(props: SmartTradeButtonProps) {
return
}
setWarnings([])
}, [buttonState, hiddenWarnings, isTradablePair, slippage])
}, [
buttonState,
hiddenWarnings,
isRestrictedCountry,
isTradablePair,
isUsingVpn,
slippage,
])

const onClick = useCallback(async () => {
if (buttonState === TradeButtonState.connectWallet) {
Expand Down
30 changes: 29 additions & 1 deletion src/components/swap/components/warning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum WarningType {
priceImpact,
restricted,
signTerms,
vpn,
}

export interface Warning {
Expand All @@ -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]: {
Expand Down Expand Up @@ -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{' '}
<Link
href='https://indexcoop.com/terms-of-service'
style={{ textDecoration: 'underline' }}
>
Terms of Service
</Link>
, we are required to restrict access to VPN users. Please also see our{' '}
<Link
href='https://indexcoop.com/tokens-restricted-for-restricted-persons'
style={{ textDecoration: 'underline' }}
>
Tokens Restricted for Restricted Persons
</Link>{' '}
page.
</>
),
},
[WarningType.signTerms]: {
title: 'Please sign the Terms and Conditions',
node: (
Expand Down
14 changes: 10 additions & 4 deletions src/components/swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -44,20 +44,26 @@ 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()

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 {
Expand Down
60 changes: 36 additions & 24 deletions src/lib/providers/protection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
import { createContext, useContext, useEffect, useState } from 'react'
'use client'

export const ProtectionContext = createContext<boolean>(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<boolean>(false)
export const ProtectionContext = createContext<Context>({
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 (
<ProtectionContext.Provider value={isProtectable}>
<ProtectionContext.Provider value={{ isRestrictedCountry, isUsingVpn }}>
{props.children}
</ProtectionContext.Provider>
)
Expand Down
Loading