diff --git a/src/hooks/useConfirmTransaction.ts b/src/hooks/useConfirmTransaction.ts index c66698af1..083faa658 100644 --- a/src/hooks/useConfirmTransaction.ts +++ b/src/hooks/useConfirmTransaction.ts @@ -19,6 +19,10 @@ import { toDecimals } from 'utils/balance'; import { interpretTransferError } from 'utils/errors'; import { addTxToLocalStorage } from 'utils/inProgressTxCache'; import { validate, isTransferValid } from 'utils/transferValidation'; +import { + checkCircleGeoblock, + CIRCLE_GEOBLOCK_ERROR_MESSAGE, +} from 'utils/circle-geoblock'; import type { RootState } from 'store'; import type { RelayerFee } from 'store/relay'; @@ -136,6 +140,15 @@ const useConfirmTransaction = (props: Props): ReturnProps => { } } + const isCCTPRoute = route === 'AutomaticCCTP' || route === 'CCTP'; + if (isCCTPRoute) { + const isGeoblocked = await checkCircleGeoblock(); + if (isGeoblocked) { + setError(CIRCLE_GEOBLOCK_ERROR_MESSAGE); + return; + } + } + dispatch(setIsTransactionInProgress(true)); try { diff --git a/src/hooks/useFetchQuotes.ts b/src/hooks/useFetchQuotes.ts index 8dad6df76..a7ac3234a 100644 --- a/src/hooks/useFetchQuotes.ts +++ b/src/hooks/useFetchQuotes.ts @@ -12,6 +12,10 @@ import { } from '@wormhole-foundation/sdk'; import { QuoteParams, QuoteResult } from 'routes/operator'; import { calculateUSDPriceRaw } from 'utils'; +import { + checkCircleGeoblock, + CIRCLE_GEOBLOCK_ERROR_MESSAGE, +} from 'utils/circle-geoblock'; import config from 'config'; import { Token } from 'config/tokens'; @@ -46,6 +50,9 @@ export default (routes: string[], params: Params): HookReturn => { Record >({}); const [isVisible, setIsVisible] = useState(true); + const [isCircleGeoblocked, setIsCircleGeoblocked] = useState( + null, + ); useEffect(() => { const visibilityHandler = () => { @@ -58,6 +65,15 @@ export default (routes: string[], params: Params): HookReturn => { }; }, []); + useEffect(() => { + const hasCCTPRoute = routes.some( + (route) => route === 'AutomaticCCTP' || route === 'CCTP', + ); + if (hasCCTPRoute && isCircleGeoblocked === null) { + checkCircleGeoblock().then(setIsCircleGeoblocked); + } + }, [routes, isCircleGeoblocked]); + // TODO temporary // Calculate USD amount for temporary $10,000 Mayan limit const { getTokenPrice } = useTokens(); @@ -199,7 +215,21 @@ export default (routes: string[], params: Params): HookReturn => { config.routes.getQuotes(routes, rParams).then((quoteResults) => { if (!unmounted) { - setUnfilteredQuotes(quoteResults); + // Add geoblocking error to CCTP routes if user is geoblocked + if (isCircleGeoblocked) { + const modifiedQuotes = { ...quoteResults }; + for (const routeName in modifiedQuotes) { + if (routeName === 'AutomaticCCTP' || routeName === 'CCTP') { + modifiedQuotes[routeName] = { + success: false, + error: new Error(CIRCLE_GEOBLOCK_ERROR_MESSAGE), + }; + } + } + setUnfilteredQuotes(modifiedQuotes); + } else { + setUnfilteredQuotes(quoteResults); + } setIsFetchingInitialQuotes(false); } }); diff --git a/src/utils/circle-geoblock.ts b/src/utils/circle-geoblock.ts new file mode 100644 index 000000000..bf9b1562f --- /dev/null +++ b/src/utils/circle-geoblock.ts @@ -0,0 +1,19 @@ +export const checkCircleGeoblock = async (): Promise => { + try { + const response = await fetch('https://api.circle.com/ping'); + + // If the user is geoblocked, Circle returns a 403 or specific error + if (!response.ok && (response.status === 403 || response.status === 451)) { + return true; + } + + return false; + } catch (error) { + // In case of network errors, we should allow the transaction to proceed to avoid blocking legitimate users with connectivity issues + console.warn('Failed to check Circle geoblocking status:', error); + return false; + } +}; + +export const CIRCLE_GEOBLOCK_ERROR_MESSAGE = + 'You are attempting a transfer from a location that is restricted by Circle.';