Skip to content
10 changes: 10 additions & 0 deletions apps/cowswap-frontend/src/locales/en-US.po
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,12 @@ msgstr "Unsupported Token"
#~ msgid "Limit price (incl. costs)"
#~ msgstr "Limit price (incl. costs)"

#: apps/cowswap-frontend/src/modules/rwa/hooks/useImportTokenConsentFlow.tsx
#: apps/cowswap-frontend/src/modules/tokensList/hooks/useRestrictedTokenImportStatus.ts
#: apps/cowswap-frontend/src/modules/tokensList/hooks/useRestrictedTokensImportStatus.ts
msgid "Checking token availability."
msgstr "Checking token availability."

#: apps/cowswap-frontend/src/modules/ordersTable/pure/ContextMenu/OrderContextMenu.pure.tsx
msgid "Order receipt"
msgstr "Order receipt"
Expand Down Expand Up @@ -1984,6 +1990,10 @@ msgstr "Import List"
msgid "TWAP orders currently require a Safe with a special fallback handler. Have one? Switch to it! Need setup? <0>Click here</0>. Future updates may extend wallet support!"
msgstr "TWAP orders currently require a Safe with a special fallback handler. Have one? Switch to it! Need setup? <0>Click here</0>. Future updates may extend wallet support!"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx
msgid "Checking token availability"
msgstr "Checking token availability"

#: apps/cowswap-frontend/src/modules/orderProgressBar/constants.ts
msgid "CoW Swap was the first DEX to offer intent-based trading, gasless swaps, coincidences of wants, and many other DeFi innovations."
msgstr "CoW Swap was the first DEX to offer intent-based trading, gasless swaps, coincidences of wants, and many other DeFi innovations."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function Updaters(): ReactNode {
isYieldEnabled={isYieldEnabled}
bridgeNetworkInfo={bridgeNetworkInfo?.data}
/>
<RestrictedTokensListUpdater isRwaGeoblockEnabled={!!isRwaGeoblockEnabled} />
<RestrictedTokensListUpdater isRwaGeoblockEnabled={isRwaGeoblockEnabled} />
<BlockedListSourcesUpdater />
<RecentTokensStorageUpdater />
<GeoDataUpdater />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useSetAtom } from 'jotai'
import { useMemo } from 'react'

import { WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/common-const'
import { TokenWithLogo, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/common-const'
import { Command } from '@cowprotocol/types'
import { useWalletInfo } from '@cowprotocol/wallet'

Expand All @@ -10,8 +10,9 @@ import { Field } from 'legacy/state/types'

import { MAX_APPROVE_AMOUNT, TradeApproveCallback } from 'modules/erc20Approve'
import { useIsInfiniteApproveDisabledInWidget } from 'modules/injectedWidget'
import { RwaTokenStatus, useRwaConsentModalState, useRwaTokenStatus } from 'modules/rwa'
import { useSwapPartialApprovalToggleState } from 'modules/swap/hooks/useSwapSettings'
import { useOnCurrencySelection, useTradeConfirmActions } from 'modules/trade'
import { useDerivedTradeState, useOnCurrencySelection, useTradeConfirmActions } from 'modules/trade'

import { updateEthFlowContextAtom } from '../../../state/ethFlowContextAtom'

Expand All @@ -32,14 +33,21 @@ export interface EthFlowActions {
directSwap(): void
}

// eslint-disable-next-line max-lines-per-function
export function useEthFlowActions(callbacks: EthFlowActionCallbacks, amountToApprove?: bigint): EthFlowActions {
const { chainId } = useWalletInfo()
const { outputCurrency } = useDerivedTradeState() || {}

const updateEthFlowContext = useSetAtom(updateEthFlowContextAtom)
const onCurrencySelection = useOnCurrencySelection()
const { onOpen: openSwapConfirmModal } = useTradeConfirmActions()
const { openModal: openRwaConsentModal } = useRwaConsentModalState()
const [isPartialApproveEnabledBySettings] = useSwapPartialApprovalToggleState()
const isInfiniteApproveDisabledInWidget = useIsInfiniteApproveDisabledInWidget()
const { status: rwaStatus, rwaTokenInfo } = useRwaTokenStatus({
inputCurrency: WRAPPED_NATIVE_CURRENCIES[chainId],
outputCurrency,
})

return useMemo(() => {
function sendTransaction(type: 'approve' | 'wrap', callback: () => Promise<string | undefined>): Promise<void> {
Expand All @@ -61,6 +69,19 @@ export function useEthFlowActions(callbacks: EthFlowActionCallbacks, amountToApp
}

const swap = async (): Promise<void> => {
if (rwaStatus === RwaTokenStatus.ChecksPending || rwaStatus === RwaTokenStatus.Restricted) {
return
}

if (rwaStatus === RwaTokenStatus.RequiredConsent && rwaTokenInfo) {
callbacks.dismiss()
openRwaConsentModal({
consentHash: rwaTokenInfo.consentHash,
token: TokenWithLogo.fromToken(rwaTokenInfo.token),
})
return
}

callbacks.dismiss()
onCurrencySelection(Field.INPUT, WRAPPED_NATIVE_CURRENCIES[chainId], () => {
openSwapConfirmModal(true)
Expand Down Expand Up @@ -114,8 +135,11 @@ export function useEthFlowActions(callbacks: EthFlowActionCallbacks, amountToApp
onCurrencySelection,
chainId,
openSwapConfirmModal,
openRwaConsentModal,
isPartialApproveEnabledBySettings,
isInfiniteApproveDisabledInWidget,
amountToApprove,
rwaStatus,
rwaTokenInfo,
])
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { useImportTokenWithConsent } from './useImportTokenWithConsent'

import { RwaConsentModal } from '../pure/RwaConsentModal'

function getRestrictedFlowResult(): CustomFlowResult<TokenSelectorView.ImportToken> {
function getBlockedFlowResult(message: string): CustomFlowResult<TokenSelectorView.ImportToken> {
return {
content: null,
data: {
restriction: {
isBlocked: true,
message: t`This token is not available in your region.`,
message,
},
},
}
Expand All @@ -38,7 +38,11 @@ export function useImportTokenConsentFlow(): ViewFlowConfig<TokenSelectorView.Im
if (!tokenToImport) return null

if (rwaStatus === 'restricted') {
return getRestrictedFlowResult()
return getBlockedFlowResult(t`This token is not available in your region.`)
}

if (rwaStatus === 'checks-pending') {
return getBlockedFlowResult(t`Checking token availability.`)
}

// Don't show consent modal to non-connected users - they can import the token
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useSelectTokenWidgetState } from 'modules/tokensList'

import { useRwaTokenStatus, RwaTokenStatus, RwaTokenInfo } from './useRwaTokenStatus'

type ImportTokenRwaStatus = 'allowed' | 'restricted' | 'requires-consent' | null
type ImportTokenRwaStatus = 'allowed' | 'restricted' | 'requires-consent' | 'checks-pending' | null

interface UseImportTokenRwaCheckResult {
tokenToImport: TokenWithLogo | undefined
Expand All @@ -16,6 +16,7 @@ interface UseImportTokenRwaCheckResult {

const RWA_STATUS_MAP: Readonly<Record<RwaTokenStatus, ImportTokenRwaStatus>> = {
[RwaTokenStatus.Allowed]: 'allowed',
[RwaTokenStatus.ChecksPending]: 'checks-pending',
[RwaTokenStatus.Restricted]: 'restricted',
[RwaTokenStatus.RequiredConsent]: 'requires-consent',
[RwaTokenStatus.ConsentIsSigned]: 'allowed',
Expand Down
73 changes: 58 additions & 15 deletions apps/cowswap-frontend/src/modules/rwa/hooks/useRwaTokenStatus.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useAtomValue } from 'jotai'
import { useMemo } from 'react'

import { useFeatureFlags } from '@cowprotocol/common-hooks'
import { areTokensEqual } from '@cowprotocol/cow-sdk'
import { Currency, Token } from '@cowprotocol/currency'
import { getCountryAsKey, RestrictedTokenInfo, useAnyRestrictedToken } from '@cowprotocol/tokens'
import { getCountryAsKey, restrictedTokensAtom, RestrictedTokenInfo, useRestrictedToken } from '@cowprotocol/tokens'
import { Nullish } from '@cowprotocol/types'
import { useWalletInfo } from '@cowprotocol/wallet'

Expand Down Expand Up @@ -31,6 +32,8 @@ export interface UseRwaTokenStatusParams {
export enum RwaTokenStatus {
/** No RWA restrictions - proceed normally */
Allowed = 'Allowed',
/** Restriction metadata is still loading - do not proceed yet */
ChecksPending = 'ChecksPending',
/** User's country is in the blocked list - cannot trade */
Restricted = 'Restricted',
/** Country unknown/loading and consent not yet given - show consent modal */
Expand All @@ -43,21 +46,45 @@ export function useRwaTokenStatus({ inputCurrency, outputCurrency }: UseRwaToken
const { isRwaGeoblockEnabled } = useFeatureFlags()
const { account } = useWalletInfo()
const geoStatus = useGeoStatus()
const restrictedTokensState = useAtomValue(restrictedTokensAtom)

const inputToken = inputCurrency?.isToken ? inputCurrency : undefined
const outputToken = outputCurrency?.isToken ? outputCurrency : undefined
const inputRestrictedToken = useRestrictedToken(inputToken)
const outputRestrictedToken = useRestrictedToken(outputToken)
const hasTokenToCheck = Boolean(inputToken || outputToken)

const restrictedTokenInfo = useAnyRestrictedToken(inputToken, outputToken)
const rwaTokenInfos = useMemo((): RwaTokenInfo[] => {
const tokensToCheck = [[inputToken, inputRestrictedToken] as const, [outputToken, outputRestrictedToken] as const]

return tokensToCheck.reduce<RwaTokenInfo[]>((acc, [token, restrictedToken]) => {
if (!token || !restrictedToken) {
return acc
}

if (acc.some((item) => areTokensEqual(item.token, token))) {
return acc
}

acc.push(convertToRwaTokenInfo(restrictedToken, token))

return acc
}, [])
}, [inputRestrictedToken, inputToken, outputRestrictedToken, outputToken])

const rwaTokenInfo = useMemo((): RwaTokenInfo | null => {
if (!restrictedTokenInfo) return null
if (rwaTokenInfos.length === 0) {
return null
}

const matchedToken = areTokensEqual(inputToken, restrictedTokenInfo.token) ? inputToken : outputToken
if (geoStatus.country === null) {
return rwaTokenInfos[0]
}

if (!matchedToken) return null
const countryKey = getCountryAsKey(geoStatus.country)

return convertToRwaTokenInfo(restrictedTokenInfo, matchedToken)
}, [restrictedTokenInfo, inputToken, outputToken])
return rwaTokenInfos.find((token) => token.blockedCountries.has(countryKey)) ?? rwaTokenInfos[0]
}, [geoStatus.country, rwaTokenInfos])

const consentKey = useMemo((): RwaConsentKey | null => {
if (!rwaTokenInfo || !account) {
Expand All @@ -72,33 +99,49 @@ export function useRwaTokenStatus({ inputCurrency, outputCurrency }: UseRwaToken
const { consentStatus } = useRwaConsentStatus(consentKey)

const status = useMemo((): RwaTokenStatus => {
// If RWA geoblock feature is disabled, always allow trading
if (isRwaGeoblockEnabled === undefined) {
return hasTokenToCheck ? RwaTokenStatus.ChecksPending : RwaTokenStatus.Allowed
}

if (!isRwaGeoblockEnabled) {
return RwaTokenStatus.Allowed
}

if (!rwaTokenInfo) {
if (!hasTokenToCheck) {
return RwaTokenStatus.Allowed
}

if (!restrictedTokensState.isLoaded) {
return RwaTokenStatus.ChecksPending
}

if (rwaTokenInfos.length === 0) {
return RwaTokenStatus.Allowed
}

// Geo API response is PRIMARY - overrides any previous consent
// If we can determine the country, use it regardless of consent status
// Note: while loading, country is null so we fall through to consent check
if (geoStatus.country !== null) {
const countryKey = getCountryAsKey(geoStatus.country)
if (rwaTokenInfo.blockedCountries.has(countryKey)) {

if (rwaTokenInfos.some((token) => token.blockedCountries.has(countryKey))) {
return RwaTokenStatus.Restricted
}

return RwaTokenStatus.Allowed
}

// Country unknown (loading, failed, or unavailable) - fall back to consent check
if (consentStatus === 'valid') {
return RwaTokenStatus.ConsentIsSigned
}

return RwaTokenStatus.RequiredConsent
}, [isRwaGeoblockEnabled, rwaTokenInfo, geoStatus.country, consentStatus])
}, [
consentStatus,
geoStatus.country,
hasTokenToCheck,
isRwaGeoblockEnabled,
restrictedTokensState.isLoaded,
rwaTokenInfos,
])

return useMemo(() => ({ status, rwaTokenInfo }), [status, rwaTokenInfo])
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useAtomValue } from 'jotai'
import { useMemo } from 'react'

import { useFeatureFlags } from '@cowprotocol/common-hooks'
import { getSourceAsKey, ListState, restrictedListsAtom, useFilterBlockedLists } from '@cowprotocol/tokens'
import { useWalletInfo } from '@cowprotocol/wallet'

Expand All @@ -13,6 +14,7 @@ import { getConsentFromCache, rwaConsentCacheAtom, RwaConsentKey, useGeoStatus }
*/
export function useFilterListsWithConsent(lists: ListState[]): ListState[] {
const { account } = useWalletInfo()
const { isRwaGeoblockEnabled } = useFeatureFlags()
const geoStatus = useGeoStatus()
const restrictedLists = useAtomValue(restrictedListsAtom)
const consentCache = useAtomValue(rwaConsentCacheAtom)
Expand All @@ -21,18 +23,16 @@ export function useFilterListsWithConsent(lists: ListState[]): ListState[] {
const countryFilteredLists = useFilterBlockedLists(lists, geoStatus.country)

return useMemo(() => {
// If country is known, just return country-filtered lists
if (geoStatus.country) {
return countryFilteredLists
if (isRwaGeoblockEnabled === false) {
return lists
}

// If geo is still loading, return all lists for now
if (geoStatus.isLoading) {
return countryFilteredLists
if (isRwaGeoblockEnabled !== true || !restrictedLists.isLoaded) {
return []
}

// if restricted lists not loaded, return all
if (!restrictedLists.isLoaded) {
// If country is known, just return country-filtered lists
if (geoStatus.country) {
return countryFilteredLists
}

Expand All @@ -55,5 +55,5 @@ export function useFilterListsWithConsent(lists: ListState[]): ListState[] {
// Only show if consent is given
return !!existingConsent?.acceptedAt
})
}, [countryFilteredLists, geoStatus, restrictedLists, account, consentCache])
}, [account, consentCache, countryFilteredLists, geoStatus.country, isRwaGeoblockEnabled, lists, restrictedLists])
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ export function useIsListRequiresConsent(listSource: string | undefined): ListCo
const consentCache = useAtomValue(rwaConsentCacheAtom)

return useMemo(() => {
// skip consent check if ff is disabled
if (!isRwaGeoblockEnabled) {
if (isRwaGeoblockEnabled === false) {
return { requiresConsent: false, consentHash: null, isLoading: false }
}

Expand All @@ -32,8 +31,7 @@ export function useIsListRequiresConsent(listSource: string | undefined): ListCo
return { requiresConsent: false, consentHash: null, isLoading: false }
}

// If still loading, return loading state
if (!restrictedLists.isLoaded || geoStatus.isLoading) {
if (isRwaGeoblockEnabled !== true || !restrictedLists.isLoaded) {
return { requiresConsent: false, consentHash: null, isLoading: true }
}

Expand All @@ -51,9 +49,8 @@ export function useIsListRequiresConsent(listSource: string | undefined): ListCo
}

// Country is unknown - check if consent is given
// If no wallet connected, don't block - the consent modal will handle wallet connection
if (!account) {
return { requiresConsent: false, consentHash, isLoading: false }
return { requiresConsent: true, consentHash, isLoading: false }
}

const consentKey: RwaConsentKey = { wallet: account, ipfsHash: consentHash }
Expand All @@ -65,5 +62,5 @@ export function useIsListRequiresConsent(listSource: string | undefined): ListCo
consentHash,
isLoading: false,
}
}, [isRwaGeoblockEnabled, listSource, restrictedLists, geoStatus, account, consentCache])
}, [account, consentCache, geoStatus.country, isRwaGeoblockEnabled, listSource, restrictedLists])
}
Loading
Loading