From b639c09de4c4477c53f8874a059da586257c2742 Mon Sep 17 00:00:00 2001 From: Jimmy Hardwick Date: Thu, 1 Aug 2024 15:47:38 +1000 Subject: [PATCH] feat: Block Sanctioned Addresses (#2044) Co-authored-by: Mimi Immutable --- packages/checkout/sdk/src/index.ts | 2 + packages/checkout/sdk/src/sanctions/index.ts | 1 + .../checkout/sdk/src/sanctions/sanctions.ts | 23 +++++++ .../ConnectLoader/ConnectLoader.tsx | 48 +++++++------- .../ConnectLoaderContext.ts | 2 +- .../src/context/view-context/ViewContext.ts | 14 ++-- .../checkout/widgets-lib/src/lib/constants.ts | 2 +- .../checkout/widgets-lib/src/locales/en.json | 6 +- .../checkout/widgets-lib/src/locales/ja.json | 6 +- .../checkout/widgets-lib/src/locales/ko.json | 6 +- .../checkout/widgets-lib/src/locales/zh.json | 6 +- .../error/ServiceUnavailableErrorView.tsx | 64 ++++++++++--------- .../src/views/error/serviceTypes.ts | 1 + .../src/widgets/bridge/BridgeWidget.tsx | 8 +++ .../components/WalletAndNetworkSelector.tsx | 35 +++++++++- .../src/widgets/connect/ConnectWidget.tsx | 64 +++++++++++-------- .../widgets/connect/components/WalletList.tsx | 19 ++++++ 17 files changed, 208 insertions(+), 99 deletions(-) create mode 100644 packages/checkout/sdk/src/sanctions/index.ts create mode 100644 packages/checkout/sdk/src/sanctions/sanctions.ts diff --git a/packages/checkout/sdk/src/index.ts b/packages/checkout/sdk/src/index.ts index 58b88d6dbf..742b2b1f52 100644 --- a/packages/checkout/sdk/src/index.ts +++ b/packages/checkout/sdk/src/index.ts @@ -150,6 +150,8 @@ export { PostMessageData, } from './postMessageHandler'; +export { isAddressSanctioned } from './sanctions'; + export type { ErrorType } from './errors'; export { CheckoutErrorType } from './errors'; diff --git a/packages/checkout/sdk/src/sanctions/index.ts b/packages/checkout/sdk/src/sanctions/index.ts new file mode 100644 index 0000000000..ddb4c44b67 --- /dev/null +++ b/packages/checkout/sdk/src/sanctions/index.ts @@ -0,0 +1 @@ +export * from './sanctions'; diff --git a/packages/checkout/sdk/src/sanctions/sanctions.ts b/packages/checkout/sdk/src/sanctions/sanctions.ts new file mode 100644 index 0000000000..3803a4156b --- /dev/null +++ b/packages/checkout/sdk/src/sanctions/sanctions.ts @@ -0,0 +1,23 @@ +import axios from 'axios'; +import { Environment } from '@imtbl/config'; +import { CHECKOUT_CDN_BASE_URL } from '../env'; + +export const isAddressSanctioned = async ( + address: string, + environment: Environment, +): Promise => { + let isSanctioned = false; + try { + const response = await axios.get( + `${CHECKOUT_CDN_BASE_URL[environment]}/v1/address/check/${address}`, + ); + + if (response.data.identifications.length > 0) { + isSanctioned = true; + } + } catch (error) { + return false; + } + + return isSanctioned; +}; diff --git a/packages/checkout/widgets-lib/src/components/ConnectLoader/ConnectLoader.tsx b/packages/checkout/widgets-lib/src/components/ConnectLoader/ConnectLoader.tsx index 60609abf26..6bc2a5578b 100644 --- a/packages/checkout/widgets-lib/src/components/ConnectLoader/ConnectLoader.tsx +++ b/packages/checkout/widgets-lib/src/components/ConnectLoader/ConnectLoader.tsx @@ -229,38 +229,38 @@ export function ConnectLoader({ return ( <> {(connectionStatus === ConnectionStatus.LOADING) && ( - + )} {(connectionStatus === ConnectionStatus.NOT_CONNECTED_NO_PROVIDER - || connectionStatus === ConnectionStatus.NOT_CONNECTED - || connectionStatus === ConnectionStatus.CONNECTED_WRONG_NETWORK) && ( - + || connectionStatus === ConnectionStatus.NOT_CONNECTED + || connectionStatus === ConnectionStatus.CONNECTED_WRONG_NETWORK) && ( + )} {/* If the user has connected then render the widget */} {connectionStatus === ConnectionStatus.CONNECTED_WITH_NETWORK && (children)} {connectionStatus === ConnectionStatus.ERROR && ( - { - connectLoaderDispatch({ - payload: { - type: ConnectLoaderActions.UPDATE_CONNECTION_STATUS, - connectionStatus: ConnectionStatus.NOT_CONNECTED, - }, - }); - }} - actionText="Try Again" - /> + { + connectLoaderDispatch({ + payload: { + type: ConnectLoaderActions.UPDATE_CONNECTION_STATUS, + connectionStatus: ConnectionStatus.NOT_CONNECTED, + }, + }); + }} + actionText="Try Again" + /> )} ); diff --git a/packages/checkout/widgets-lib/src/context/connect-loader-context/ConnectLoaderContext.ts b/packages/checkout/widgets-lib/src/context/connect-loader-context/ConnectLoaderContext.ts index 0b80de2a6a..61f3d21bd8 100644 --- a/packages/checkout/widgets-lib/src/context/connect-loader-context/ConnectLoaderContext.ts +++ b/packages/checkout/widgets-lib/src/context/connect-loader-context/ConnectLoaderContext.ts @@ -61,7 +61,7 @@ export interface SetProviderPayload { // eslint-disable-next-line @typescript-eslint/naming-convention export const ConnectLoaderContext = createContext({ connectLoaderState: initialConnectLoaderState, - connectLoaderDispatch: () => {}, + connectLoaderDispatch: () => { }, }); export type Reducer = (prevState: S, action: A) => S; diff --git a/packages/checkout/widgets-lib/src/context/view-context/ViewContext.ts b/packages/checkout/widgets-lib/src/context/view-context/ViewContext.ts index 433d5c008e..531877697d 100644 --- a/packages/checkout/widgets-lib/src/context/view-context/ViewContext.ts +++ b/packages/checkout/widgets-lib/src/context/view-context/ViewContext.ts @@ -15,10 +15,10 @@ export enum SharedViews { } export type SharedView = -LoadingView -| ErrorView -| ServiceUnavailableErrorView -| TopUpView; + LoadingView + | ErrorView + | ServiceUnavailableErrorView + | TopUpView; interface LoadingView extends ViewType { type: SharedViews.LOADING_VIEW @@ -30,7 +30,7 @@ export interface ErrorView extends ViewType { tryAgain?: () => Promise } -export interface ServiceUnavailableErrorView extends ViewType { +interface ServiceUnavailableErrorView extends ViewType { type: SharedViews.SERVICE_UNAVAILABLE_ERROR_VIEW; error: Error; } @@ -96,7 +96,7 @@ export interface GoBackToPayload { // eslint-disable-next-line @typescript-eslint/naming-convention export const ViewContext = createContext({ viewState: initialViewState, - viewDispatch: () => {}, + viewDispatch: () => { }, }); ViewContext.displayName = 'ViewContext'; // help with debugging Context in browser @@ -118,7 +118,7 @@ export const viewReducer: Reducer = ( const { history } = state; if ( history.length === 0 - || history[history.length - 1].type !== view.type + || history[history.length - 1].type !== view.type ) { // currentViewData should only be set on the current view before updating if (currentViewData) { diff --git a/packages/checkout/widgets-lib/src/lib/constants.ts b/packages/checkout/widgets-lib/src/lib/constants.ts index e830330218..829ca640a6 100644 --- a/packages/checkout/widgets-lib/src/lib/constants.ts +++ b/packages/checkout/widgets-lib/src/lib/constants.ts @@ -121,6 +121,6 @@ export const WITHDRAWAL_CLAIM_GAS_LIMIT = 91000; */ export const CHECKOUT_APP_URL = { [ENV_DEVELOPMENT]: 'https://checkout.dev.immutable.com', - [Environment.SANDBOX]: 'https://checkout.sandbox.immutable.com', + [Environment.SANDBOX]: 'http://localhost:3001', [Environment.PRODUCTION]: 'https://checkout.immutable.com', }; diff --git a/packages/checkout/widgets-lib/src/locales/en.json b/packages/checkout/widgets-lib/src/locales/en.json index 20fecf0ca0..185df5ff16 100644 --- a/packages/checkout/widgets-lib/src/locales/en.json +++ b/packages/checkout/widgets-lib/src/locales/en.json @@ -70,10 +70,12 @@ }, "SERVICE_UNAVAILABLE_ERROR_VIEW": { "heading": { - "swap": "Swapping is not available in your region" + "swap": "Swapping is not available in your region", + "generic": "This service is not available in your region" }, "body": { - "swap": "Please refer to Quickswap’s website for further information." + "swap": "Please refer to Quickswap’s website for further information.", + "generic": "For further assistance visit the Immutable support page." } }, "LOADING_VIEW": { diff --git a/packages/checkout/widgets-lib/src/locales/ja.json b/packages/checkout/widgets-lib/src/locales/ja.json index 8877ac3a35..86f118badf 100644 --- a/packages/checkout/widgets-lib/src/locales/ja.json +++ b/packages/checkout/widgets-lib/src/locales/ja.json @@ -74,10 +74,12 @@ }, "SERVICE_UNAVAILABLE_ERROR_VIEW": { "heading": { - "swap": "お住まいの地域では交換が利用できません" + "swap": "お住まいの地域では交換が利用できません", + "generic": "このサービスはお住まいの地域では利用できません" }, "body": { - "swap": "Quickswapのウェブサイトで詳細情報をご覧ください。" + "swap": "Quickswapのウェブサイトで詳細情報をご覧ください。", + "generic": "詳しいサポートについては、Immutableサポートページをご覧ください." } }, "LOADING_VIEW": { diff --git a/packages/checkout/widgets-lib/src/locales/ko.json b/packages/checkout/widgets-lib/src/locales/ko.json index 3082d21ebe..b56ce6890d 100644 --- a/packages/checkout/widgets-lib/src/locales/ko.json +++ b/packages/checkout/widgets-lib/src/locales/ko.json @@ -70,10 +70,12 @@ }, "SERVICE_UNAVAILABLE_ERROR_VIEW": { "heading": { - "swap": "귀하의 지역에서는 스왑이 가능하지 않습니다" + "swap": "귀하의 지역에서는 스왑이 가능하지 않습니다", + "generic": "이 서비스는 귀하의 지역에서 이용할 수 없습니다" }, "body": { - "swap": "자세한 정보는 Quickswap 웹사이트를 참조하십시오." + "swap": "자세한 정보는 Quickswap 웹사이트를 참조하십시오.", + "generic": "추가 지원이 필요하시면 Immutable 지원 페이지를 방문하세요." } }, "LOADING_VIEW": { diff --git a/packages/checkout/widgets-lib/src/locales/zh.json b/packages/checkout/widgets-lib/src/locales/zh.json index 876a44f016..827a7d3294 100644 --- a/packages/checkout/widgets-lib/src/locales/zh.json +++ b/packages/checkout/widgets-lib/src/locales/zh.json @@ -70,10 +70,12 @@ }, "SERVICE_UNAVAILABLE_ERROR_VIEW": { "heading": { - "swap": "您所在的地区不支持兑换" + "swap": "您所在的地区不支持兑换", + "generic": "该服务在您的地区不可用" }, "body": { - "swap": "请参考Quickswap的网站了解更多信息。" + "swap": "请参考Quickswap的网站了解更多信息。", + "generic": "如需进一步帮助,请访问Immutable支持页面。" } }, "LOADING_VIEW": { diff --git a/packages/checkout/widgets-lib/src/views/error/ServiceUnavailableErrorView.tsx b/packages/checkout/widgets-lib/src/views/error/ServiceUnavailableErrorView.tsx index b4caec9d70..d068b6b465 100644 --- a/packages/checkout/widgets-lib/src/views/error/ServiceUnavailableErrorView.tsx +++ b/packages/checkout/widgets-lib/src/views/error/ServiceUnavailableErrorView.tsx @@ -44,6 +44,10 @@ export function ServiceUnavailableErrorView({ size="small" rc={} />, + immutableSupport: } + />, }} /> @@ -59,41 +63,41 @@ export function ServiceUnavailableErrorView({ > {primaryActionText && onPrimaryButtonClick && ( - - - + + )} {secondaryActionText && onSecondaryButtonClick && ( - - - + + )} diff --git a/packages/checkout/widgets-lib/src/views/error/serviceTypes.ts b/packages/checkout/widgets-lib/src/views/error/serviceTypes.ts index 661dd38af1..2b618a1512 100644 --- a/packages/checkout/widgets-lib/src/views/error/serviceTypes.ts +++ b/packages/checkout/widgets-lib/src/views/error/serviceTypes.ts @@ -1,3 +1,4 @@ export enum ServiceType { SWAP = 'swap', + GENERIC = 'generic', } diff --git a/packages/checkout/widgets-lib/src/widgets/bridge/BridgeWidget.tsx b/packages/checkout/widgets-lib/src/widgets/bridge/BridgeWidget.tsx index 1af8b1b8ff..2af9ad1a76 100644 --- a/packages/checkout/widgets-lib/src/widgets/bridge/BridgeWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/bridge/BridgeWidget.tsx @@ -62,6 +62,8 @@ import { BridgeWidgetViews, } from '../../context/view-context/BridgeViewContextTypes'; import { ClaimWithdrawal } from './views/ClaimWithdrawal'; +import { ServiceType } from '../../views/error/serviceTypes'; +import { ServiceUnavailableErrorView } from '../../views/error/ServiceUnavailableErrorView'; export type BridgeWidgetInputs = BridgeWidgetParams & { config: StrongCheckoutWidgetsConfig, @@ -337,6 +339,12 @@ export default function BridgeWidget({ testId="claim-withdrawal-fail-view" /> )} + {viewState.view.type === SharedViews.SERVICE_UNAVAILABLE_ERROR_VIEW && ( + sendBridgeWidgetCloseEvent(eventTarget)} + /> + )} diff --git a/packages/checkout/widgets-lib/src/widgets/bridge/components/WalletAndNetworkSelector.tsx b/packages/checkout/widgets-lib/src/widgets/bridge/components/WalletAndNetworkSelector.tsx index ca08829a1b..94f8f6d3ab 100644 --- a/packages/checkout/widgets-lib/src/widgets/bridge/components/WalletAndNetworkSelector.tsx +++ b/packages/checkout/widgets-lib/src/widgets/bridge/components/WalletAndNetworkSelector.tsx @@ -11,7 +11,9 @@ import { useMemo, useState, } from 'react'; -import { ChainId, WalletProviderName, WalletProviderRdns } from '@imtbl/checkout-sdk'; +import { + ChainId, isAddressSanctioned, WalletProviderName, WalletProviderRdns, +} from '@imtbl/checkout-sdk'; import { Web3Provider } from '@ethersproject/providers'; import { useTranslation } from 'react-i18next'; import { BridgeWidgetViews } from '../../../context/view-context/BridgeViewContextTypes'; @@ -24,7 +26,7 @@ import { } from '../../../lib/provider'; import { getL1ChainId, getL2ChainId } from '../../../lib'; import { getChainNameById } from '../../../lib/chains'; -import { ViewActions, ViewContext } from '../../../context/view-context/ViewContext'; +import { SharedViews, ViewActions, ViewContext } from '../../../context/view-context/ViewContext'; import { abbreviateAddress } from '../../../lib/addressUtils'; import { useAnalytics, @@ -206,6 +208,21 @@ export function WalletAndNetworkSelector() { } const web3Provider = new Web3Provider(event.provider as any); const connectedProvider = await connectToProvider(checkout, web3Provider, changeAccount); + + // CM-793 Check for sanctioned address + if (await isAddressSanctioned(await connectedProvider.getSigner().getAddress(), checkout.config.environment)) { + viewDispatch({ + payload: { + type: ViewActions.UPDATE_VIEW, + view: { + type: SharedViews.SERVICE_UNAVAILABLE_ERROR_VIEW, + error: new Error('Sanctioned address'), + }, + }, + }); + return; + } + await handleFromWalletConnectionSuccess(connectedProvider); }, [checkout], @@ -322,6 +339,20 @@ export function WalletAndNetworkSelector() { const web3Provider = new Web3Provider(event.provider as any); const connectedProvider = await connectToProvider(checkout, web3Provider, false); + // CM-793 Check for sanctioned address + if (await isAddressSanctioned(await connectedProvider.getSigner().getAddress(), checkout.config.environment)) { + viewDispatch({ + payload: { + type: ViewActions.UPDATE_VIEW, + view: { + type: SharedViews.SERVICE_UNAVAILABLE_ERROR_VIEW, + error: new Error('Sanctioned address'), + }, + }, + }); + return; + } + if (isWalletConnectProvider(connectedProvider)) { handleWalletConnectToWalletConnection(connectedProvider); } else { diff --git a/packages/checkout/widgets-lib/src/widgets/connect/ConnectWidget.tsx b/packages/checkout/widgets-lib/src/widgets/connect/ConnectWidget.tsx index e82eca4e3e..4a0d38d259 100644 --- a/packages/checkout/widgets-lib/src/widgets/connect/ConnectWidget.tsx +++ b/packages/checkout/widgets-lib/src/widgets/connect/ConnectWidget.tsx @@ -1,7 +1,5 @@ /* eslint-disable react/jsx-no-constructed-context-values */ -import React, { - useContext, useMemo, useEffect, useReducer, useCallback, -} from 'react'; +import { Web3Provider } from '@ethersproject/providers'; import { ChainId, Checkout, @@ -12,9 +10,38 @@ import { getPassportProviderDetail, WalletConnectManager as IWalletConnectManager, } from '@imtbl/checkout-sdk'; -import { Web3Provider } from '@ethersproject/providers'; +import { + useCallback, + useContext, + useEffect, + useMemo, + useReducer, +} from 'react'; import { useTranslation } from 'react-i18next'; +import { ConnectLoaderSuccess } from '../../components/ConnectLoader/ConnectLoaderSuccess'; +import { StatusType } from '../../components/Status/StatusType'; +import { StatusView } from '../../components/Status/StatusView'; +import { useAnalytics, UserJourney } from '../../context/analytics-provider/SegmentAnalyticsProvider'; +import { EventTargetContext } from '../../context/event-target-context/EventTargetContext'; +import { ConnectWidgetView, ConnectWidgetViews } from '../../context/view-context/ConnectViewContextTypes'; +import { + initialViewState, + SharedViews, + ViewActions, + ViewContext, + viewReducer, +} from '../../context/view-context/ViewContext'; +import { addProviderListenersForWidgetRoot, sendProviderUpdatedEvent } from '../../lib'; +import { identifyUser } from '../../lib/analytics/identifyUser'; +import { useWalletConnect } from '../../lib/hooks/useWalletConnect'; +import { isMetaMaskProvider, isPassportProvider, isWalletConnectProvider } from '../../lib/provider'; import { isL1EthChainId, isZkEvmChainId } from '../../lib/utils'; +import { WalletConnectManager, walletConnectProviderInfo } from '../../lib/walletConnect'; +import { StrongCheckoutWidgetsConfig } from '../../lib/withDefaultWidgetConfig'; +import { ErrorView } from '../../views/error/ErrorView'; +import { ServiceType } from '../../views/error/serviceTypes'; +import { ServiceUnavailableErrorView } from '../../views/error/ServiceUnavailableErrorView'; +import { LoadingView } from '../../views/loading/LoadingView'; import { sendCloseWidgetEvent, sendConnectFailedEvent, @@ -27,30 +54,9 @@ import { connectReducer, initialConnectState, } from './context/ConnectContext'; -import { ConnectWidgetView, ConnectWidgetViews } from '../../context/view-context/ConnectViewContextTypes'; import { ConnectWallet } from './views/ConnectWallet'; -import { SwitchNetworkZkEVM } from './views/SwitchNetworkZkEVM'; -import { LoadingView } from '../../views/loading/LoadingView'; -import { ConnectLoaderSuccess } from '../../components/ConnectLoader/ConnectLoaderSuccess'; -import { - viewReducer, - initialViewState, - ViewActions, - ViewContext, - SharedViews, -} from '../../context/view-context/ViewContext'; -import { StatusType } from '../../components/Status/StatusType'; -import { StatusView } from '../../components/Status/StatusView'; -import { StrongCheckoutWidgetsConfig } from '../../lib/withDefaultWidgetConfig'; -import { addProviderListenersForWidgetRoot, sendProviderUpdatedEvent } from '../../lib'; import { SwitchNetworkEth } from './views/SwitchNetworkEth'; -import { ErrorView } from '../../views/error/ErrorView'; -import { EventTargetContext } from '../../context/event-target-context/EventTargetContext'; -import { UserJourney, useAnalytics } from '../../context/analytics-provider/SegmentAnalyticsProvider'; -import { identifyUser } from '../../lib/analytics/identifyUser'; -import { isMetaMaskProvider, isPassportProvider, isWalletConnectProvider } from '../../lib/provider'; -import { WalletConnectManager, walletConnectProviderInfo } from '../../lib/walletConnect'; -import { useWalletConnect } from '../../lib/hooks/useWalletConnect'; +import { SwitchNetworkZkEVM } from './views/SwitchNetworkZkEVM'; export type ConnectWidgetInputs = ConnectWidgetParams & { config: StrongCheckoutWidgetsConfig @@ -253,6 +259,12 @@ export default function ConnectWidget({ onCloseClick={() => sendCloseEvent()} /> )} + {view.type === SharedViews.SERVICE_UNAVAILABLE_ERROR_VIEW && ( + + )} diff --git a/packages/checkout/widgets-lib/src/widgets/connect/components/WalletList.tsx b/packages/checkout/widgets-lib/src/widgets/connect/components/WalletList.tsx index a65dd241c4..8ff1b32cc9 100644 --- a/packages/checkout/widgets-lib/src/widgets/connect/components/WalletList.tsx +++ b/packages/checkout/widgets-lib/src/widgets/connect/components/WalletList.tsx @@ -4,6 +4,7 @@ import { ChainId, CheckoutErrorType, EIP6963ProviderDetail, + isAddressSanctioned, WalletProviderName, WalletProviderRdns, } from '@imtbl/checkout-sdk'; @@ -24,6 +25,7 @@ import { ConnectWidgetViews } from '../../../context/view-context/ConnectViewCon import { ConnectActions, ConnectContext } from '../context/ConnectContext'; import { WalletItem } from './WalletItem'; import { + SharedViews, ViewActions, ViewContext, } from '../../../context/view-context/ViewContext'; @@ -169,6 +171,23 @@ export function WalletList(props: WalletListProps) { requestWalletPermissions: changeAccount, }); + // CM-793 Check for sanctioned address + if (await isAddressSanctioned( + await connectResult.provider.getSigner().getAddress(), + checkout.config.environment, + )) { + viewDispatch({ + payload: { + type: ViewActions.UPDATE_VIEW, + view: { + type: SharedViews.SERVICE_UNAVAILABLE_ERROR_VIEW, + error: new Error('Sanctioned address'), + }, + }, + }); + return; + } + // Set up EIP-1193 provider event listeners for widget root instances addProviderListenersForWidgetRoot(connectResult.provider); await identifyUser(identify, connectResult.provider);