-
-const MANDATORY_DAPP_FIELDS: (keyof HookDappBaseInfo)[] = ['id', 'name', 'image', 'version', 'website']
-
-const isHex = (val: string) => Boolean(val.match(/^[0-9a-f]+$/i))
+import { validateHookDappManifest } from '../../../validateHookDappManifest'
interface ExternalDappLoaderProps {
input: string
@@ -45,47 +35,24 @@ export function ExternalDappLoader({
.then((data) => {
if (!isRequestRelevant) return
- const { conditions = {}, ...dapp } = data.cow_hook_dapp as HookDappBase
+ const dapp = data.cow_hook_dapp as HookDappBase
- if (dapp) {
- const emptyFields = MANDATORY_DAPP_FIELDS.filter((field) => typeof dapp[field] === 'undefined')
+ const validationError = validateHookDappManifest(
+ data.cow_hook_dapp as HookDappBase,
+ chainId,
+ isPreHook,
+ isSmartContractWallet,
+ )
- if (emptyFields.length > 0) {
- setManifestError(`${emptyFields.join(',')} fields are no set.`)
- } else {
- if (
- isSmartContractWallet === true &&
- conditions.walletCompatibility &&
- !conditions.walletCompatibility.includes(HookDappWalletCompatibility.SMART_CONTRACT)
- ) {
- setManifestError('The app does not support smart-contract wallets.')
- } else if (!isHex(dapp.id) || dapp.id.length !== HOOK_DAPP_ID_LENGTH) {
- setManifestError(Hook dapp id must be a hex with length 64.
)
- } else if (conditions.supportedNetworks && !conditions.supportedNetworks.includes(chainId)) {
- setManifestError(This app/hook doesn't support current network (chainId={chainId}).
)
- } else if (conditions.position === 'post' && isPreHook) {
- setManifestError(
-
- This app/hook can only be used as a post-hook and cannot be added as a pre-hook.
-
,
- )
- } else if (conditions.position === 'pre' && !isPreHook) {
- setManifestError(
-
- This app/hook can only be used as a pre-hook and cannot be added as a post-hook.
-
,
- )
- } else {
- setManifestError(null)
- setDappInfo({
- ...dapp,
- type: HookDappType.IFRAME,
- url: input,
- })
- }
- }
+ if (validationError) {
+ setManifestError(validationError)
} else {
- setManifestError('Manifest does not contain "cow_hook_dapp" property.')
+ setManifestError(null)
+ setDappInfo({
+ ...dapp,
+ type: HookDappType.IFRAME,
+ url: input,
+ })
}
})
.catch((error) => {
diff --git a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts
index d00c525a10..eb0abc8cf6 100644
--- a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts
+++ b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts
@@ -39,7 +39,7 @@ export const customPostHookDappsAtom = atom((get) => {
return Object.values(get(customHookDappsAtom).post) as HookDappIframe[]
})
-export const addCustomHookDappAtom = atom(null, (get, set, isPreHook: boolean, dapp: HookDappIframe) => {
+export const upsertCustomHookDappAtom = atom(null, (get, set, isPreHook: boolean, dapp: HookDappIframe) => {
const { chainId } = get(walletInfoAtom)
const state = get(customHookDappsInner)
diff --git a/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx b/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx
new file mode 100644
index 0000000000..6a5d950898
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/hooksStore/updaters/iframeDappsManifestUpdater.tsx
@@ -0,0 +1,81 @@
+import { useSetAtom } from 'jotai'
+import { useAtomValue } from 'jotai/index'
+import { useCallback, useEffect, useMemo } from 'react'
+
+import { HookDappBase, HookDappType } from '@cowprotocol/hook-dapp-lib'
+import { useWalletInfo } from '@cowprotocol/wallet'
+
+import ms from 'ms.macro'
+
+import { customHookDappsAtom, upsertCustomHookDappAtom } from '../state/customHookDappsAtom'
+import { validateHookDappManifest } from '../validateHookDappManifest'
+
+const UPDATE_TIME_KEY = 'HOOK_DAPPS_UPDATE_TIME'
+const HOOK_DAPPS_UPDATE_INTERVAL = ms`6h`
+
+const getLastUpdateTimestamp = () => {
+ const lastUpdate = localStorage.getItem(UPDATE_TIME_KEY)
+ return lastUpdate ? +lastUpdate : null
+}
+
+export function IframeDappsManifestUpdater() {
+ const hooksState = useAtomValue(customHookDappsAtom)
+ const upsertCustomHookDapp = useSetAtom(upsertCustomHookDappAtom)
+ const { chainId } = useWalletInfo()
+
+ const [preHooks, postHooks] = useMemo(
+ () => [Object.values(hooksState.pre), Object.values(hooksState.post)],
+ [hooksState],
+ )
+
+ const fetchAndUpdateHookDapp = useCallback(
+ (url: string, isPreHook: boolean) => {
+ return fetch(`${url}/manifest.json`)
+ .then((res) => res.json())
+ .then((data) => {
+ const dapp = data.cow_hook_dapp as HookDappBase
+
+ // Don't pass parameters that are not needed for validation
+ // In order to skip validation of the already added hook-dapp
+ const validationError = validateHookDappManifest(
+ data.cow_hook_dapp as HookDappBase,
+ undefined,
+ undefined,
+ undefined,
+ )
+
+ if (validationError) {
+ console.error('Cannot update iframe hook dapp:', validationError)
+ } else {
+ upsertCustomHookDapp(isPreHook, {
+ ...dapp,
+ type: HookDappType.IFRAME,
+ url,
+ })
+ }
+ })
+ },
+ [chainId, upsertCustomHookDapp],
+ )
+
+ /**
+ * Update iframe hook dapps not more often than every 6 hours
+ */
+ useEffect(() => {
+ if (!preHooks.length && !postHooks.length) return
+
+ const lastUpdate = getLastUpdateTimestamp()
+ const shouldUpdate = !lastUpdate || lastUpdate + HOOK_DAPPS_UPDATE_INTERVAL < Date.now()
+
+ if (!shouldUpdate) return
+
+ console.debug('Updating iframe hook dapps...', { preHooks, postHooks })
+
+ localStorage.setItem(UPDATE_TIME_KEY, Date.now().toString())
+
+ preHooks.forEach((hook) => fetchAndUpdateHookDapp(hook.url, true))
+ postHooks.forEach((hook) => fetchAndUpdateHookDapp(hook.url, false))
+ }, [preHooks, postHooks, fetchAndUpdateHookDapp])
+
+ return null
+}
diff --git a/apps/cowswap-frontend/src/modules/hooksStore/validateHookDappManifest.tsx b/apps/cowswap-frontend/src/modules/hooksStore/validateHookDappManifest.tsx
new file mode 100644
index 0000000000..b0370b4473
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/hooksStore/validateHookDappManifest.tsx
@@ -0,0 +1,55 @@
+import { ReactElement } from 'react'
+
+import { SupportedChainId } from '@cowprotocol/cow-sdk'
+import { HOOK_DAPP_ID_LENGTH, HookDappBase, HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib'
+
+type HookDappBaseInfo = Omit
+
+const MANDATORY_DAPP_FIELDS: (keyof HookDappBaseInfo)[] = ['id', 'name', 'image', 'version', 'website']
+
+const isHex = (val: string) => Boolean(val.match(/^[0-9a-f]+$/i))
+
+export function validateHookDappManifest(
+ data: HookDappBase,
+ chainId: SupportedChainId | undefined,
+ isPreHook: boolean | undefined,
+ isSmartContractWallet: boolean | undefined,
+): ReactElement | string | null {
+ const { conditions = {}, ...dapp } = data
+
+ if (dapp) {
+ const emptyFields = MANDATORY_DAPP_FIELDS.filter((field) => typeof dapp[field] === 'undefined')
+
+ if (emptyFields.length > 0) {
+ return `${emptyFields.join(',')} fields are no set.`
+ } else {
+ if (
+ isSmartContractWallet === true &&
+ conditions.walletCompatibility &&
+ !conditions.walletCompatibility.includes(HookDappWalletCompatibility.SMART_CONTRACT)
+ ) {
+ return 'The app does not support smart-contract wallets.'
+ } else if (!isHex(dapp.id) || dapp.id.length !== HOOK_DAPP_ID_LENGTH) {
+ return Hook dapp id must be a hex with length 64.
+ } else if (chainId && conditions.supportedNetworks && !conditions.supportedNetworks.includes(chainId)) {
+ return This app/hook doesn't support current network (chainId={chainId}).
+ } else if (conditions.position === 'post' && isPreHook === true) {
+ return (
+
+ This app/hook can only be used as a post-hook and cannot be added as a pre-hook.
+
+ )
+ } else if (conditions.position === 'pre' && isPreHook === false) {
+ return (
+
+ This app/hook can only be used as a pre-hook and cannot be added as a post-hook.
+
+ )
+ }
+ }
+ } else {
+ return 'Manifest does not contain "cow_hook_dapp" property.'
+ }
+
+ return null
+}
diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx
index 83d2885a28..8f68220e52 100644
--- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx
+++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx
@@ -30,6 +30,7 @@ import { RateInfoParams } from 'common/pure/RateInfo'
import { TransactionSubmittedContent } from 'common/pure/TransactionSubmittedContent'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
+import { useBaseFlowContextSource } from '../../hooks/useFlowContext'
import { useIsEoaEthFlow } from '../../hooks/useIsEoaEthFlow'
import { useNavigateToNewOrderCallback } from '../../hooks/useNavigateToNewOrderCallback'
import { useShouldPayGas } from '../../hooks/useShouldPayGas'
@@ -54,7 +55,6 @@ export interface ConfirmSwapModalSetupProps {
doTrade(): void
}
-
export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) {
const {
chainId,
@@ -77,6 +77,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) {
const shouldPayGas = useShouldPayGas()
const isEoaEthFlow = useIsEoaEthFlow()
const nativeCurrency = useNativeCurrency()
+ const baseFlowContextSource = useBaseFlowContextSource()
const isInvertedState = useState(false)
@@ -89,7 +90,10 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) {
const labelsAndTooltips = useMemo(
() => ({
- slippageLabel: isEoaEthFlow || isSmartSlippageApplied ? `Slippage tolerance (${isEoaEthFlow ? 'modified' : 'dynamic'})` : undefined,
+ slippageLabel:
+ isEoaEthFlow || isSmartSlippageApplied
+ ? `Slippage tolerance (${isEoaEthFlow ? 'modified' : 'dynamic'})`
+ : undefined,
slippageTooltip: isEoaEthFlow
? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol])
: getNonNativeSlippageTooltip(),
@@ -99,7 +103,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) {
networkCostsSuffix: shouldPayGas ? : null,
networkCostsTooltipSuffix: ,
}),
- [chainId, allowedSlippage, nativeCurrency.symbol, isEoaEthFlow, isExactIn, shouldPayGas]
+ [chainId, allowedSlippage, nativeCurrency.symbol, isEoaEthFlow, isExactIn, shouldPayGas],
)
const submittedContent = useSubmittedContent(chainId)
@@ -119,6 +123,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) {
priceImpact={priceImpact}
buttonText={buttonText}
recipient={recipient}
+ appData={baseFlowContextSource?.appData || undefined}
>
<>
{receiveAmountInfo && (
@@ -166,7 +171,6 @@ function useSubmittedContent(chainId: SupportedChainId) {
navigateToNewOrderCallback={navigateToNewOrderCallback}
/>
),
- [chainId, transactionHash, orderProgressBarV2Props, navigateToNewOrderCallback]
+ [chainId, transactionHash, orderProgressBarV2Props, navigateToNewOrderCallback],
)
}
-
diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx
index 42050b895c..021a8fd42e 100644
--- a/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx
+++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapUpdaters/index.tsx
@@ -1,10 +1,10 @@
-
import { percentToBps } from '@cowprotocol/common-utils'
import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied'
import { AppDataUpdater } from '../../../appData'
import { useSwapSlippage } from '../../hooks/useSwapSlippage'
+import { BaseFlowContextUpdater } from '../../updaters/BaseFlowContextUpdater'
import { SmartSlippageUpdater } from '../../updaters/SmartSlippageUpdater'
import { SwapAmountsFromUrlUpdater } from '../../updaters/SwapAmountsFromUrlUpdater'
import { SwapDerivedStateUpdater } from '../../updaters/SwapDerivedStateUpdater'
@@ -15,10 +15,15 @@ export function SwapUpdaters() {
return (
<>
-
+
+
>
)
}
diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts
index 0188cffa13..6bed3d4da2 100644
--- a/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts
+++ b/apps/cowswap-frontend/src/modules/swap/hooks/useBaseSafeBundleFlowContext.ts
@@ -6,15 +6,15 @@ import { useSafeAppsSdk } from '@cowprotocol/wallet'
import { useWalletProvider } from '@cowprotocol/wallet-provider'
import { TradeType } from '@uniswap/sdk-core'
-import { getFlowContext, useBaseFlowContextSetup } from 'modules/swap/hooks/useFlowContext'
+import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext'
import { BaseSafeFlowContext } from 'modules/swap/services/types'
import { useGP2SettlementContract } from 'common/hooks/useContract'
import { useTradeSpenderAddress } from 'common/hooks/useTradeSpenderAddress'
export function useBaseSafeBundleFlowContext(): BaseSafeFlowContext | null {
- const baseProps = useBaseFlowContextSetup()
- const sellToken = baseProps.trade ? getWrappedToken(baseProps.trade.inputAmount.currency) : undefined
+ const baseProps = useBaseFlowContextSource()
+ const sellToken = baseProps?.trade ? getWrappedToken(baseProps.trade.inputAmount.currency) : undefined
const settlementContract = useGP2SettlementContract()
const spender = useTradeSpenderAddress()
@@ -22,7 +22,7 @@ export function useBaseSafeBundleFlowContext(): BaseSafeFlowContext | null {
const provider = useWalletProvider()
return useMemo(() => {
- if (!baseProps.trade || !settlementContract || !spender || !safeAppsSdk || !provider) return null
+ if (!baseProps?.trade || !settlementContract || !spender || !safeAppsSdk || !provider) return null
const baseContext = getFlowContext({
baseProps,
diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts
index 784fcb328c..c3939fb295 100644
--- a/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts
+++ b/apps/cowswap-frontend/src/modules/swap/hooks/useEthFlowContext.ts
@@ -6,7 +6,7 @@ import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk'
import { useTransactionAdder } from 'legacy/state/enhancedTransactions/hooks'
-import { FlowType, getFlowContext, useBaseFlowContextSetup } from 'modules/swap/hooks/useFlowContext'
+import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext'
import { EthFlowContext } from 'modules/swap/services/types'
import { addInFlightOrderIdAtom } from 'modules/swap/state/EthFlow/ethFlowInFlightOrderIdsAtom'
@@ -14,12 +14,14 @@ import { useEthFlowContract } from 'common/hooks/useContract'
import { useCheckEthFlowOrderExists } from './useCheckEthFlowOrderExists'
+import { FlowType } from '../types/flowContext'
+
export function useEthFlowContext(): EthFlowContext | null {
const contract = useEthFlowContract()
- const baseProps = useBaseFlowContextSetup()
+ const baseProps = useBaseFlowContextSource()
const addTransaction = useTransactionAdder()
- const sellToken = baseProps.chainId ? NATIVE_CURRENCIES[baseProps.chainId as SupportedChainId] : undefined
+ const sellToken = baseProps?.chainId ? NATIVE_CURRENCIES[baseProps.chainId as SupportedChainId] : undefined
const addInFlightOrderId = useSetAtom(addInFlightOrderIdAtom)
@@ -27,16 +29,17 @@ export function useEthFlowContext(): EthFlowContext | null {
const baseContext = useMemo(
() =>
+ baseProps &&
getFlowContext({
baseProps,
sellToken,
kind: OrderKind.SELL,
}),
- [baseProps, sellToken]
+ [baseProps, sellToken],
)
return useMemo(() => {
- if (!baseContext || !contract || baseProps.flowType !== FlowType.EOA_ETH_FLOW) return null
+ if (!baseContext || !contract || baseProps?.flowType !== FlowType.EOA_ETH_FLOW) return null
return {
...baseContext,
@@ -45,5 +48,5 @@ export function useEthFlowContext(): EthFlowContext | null {
checkEthFlowOrderExists,
addInFlightOrderId,
}
- }, [baseContext, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, baseProps.flowType])
+ }, [baseContext, contract, addTransaction, checkEthFlowOrderExists, addInFlightOrderId, baseProps?.flowType])
}
diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts
index 53a2f42cb4..9510a7c33d 100644
--- a/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts
+++ b/apps/cowswap-frontend/src/modules/swap/hooks/useFlowContext.ts
@@ -1,79 +1,26 @@
-import { Erc20, Weth } from '@cowprotocol/abis'
+import { useAtomValue } from 'jotai/index'
+
import { NATIVE_CURRENCIES } from '@cowprotocol/common-const'
-import { getAddress, getIsNativeToken } from '@cowprotocol/common-utils'
+import { getIsNativeToken } from '@cowprotocol/common-utils'
import { OrderClass, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk'
-import { useENSAddress } from '@cowprotocol/ens'
-import { Command, UiOrderType } from '@cowprotocol/types'
-import { GnosisSafeInfo, useGnosisSafeInfo, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet'
-import { useWalletProvider } from '@cowprotocol/wallet-provider'
-import { Web3Provider } from '@ethersproject/providers'
-import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
-
-import { useDispatch } from 'react-redux'
+import { UiOrderType } from '@cowprotocol/types'
+import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
-import { AppDispatch } from 'legacy/state'
-import { useCloseModals } from 'legacy/state/application/hooks'
-import { AddOrderCallback, useAddPendingOrder } from 'legacy/state/orders/hooks'
-import { useGetQuoteAndStatus } from 'legacy/state/price/hooks'
-import type { QuoteInformationObject } from 'legacy/state/price/reducer'
-import TradeGp from 'legacy/state/swap/TradeGp'
-import { useUserTransactionTTL } from 'legacy/state/user/hooks'
import { computeSlippageAdjustedAmounts } from 'legacy/utils/prices'
import { PostOrderParams } from 'legacy/utils/trade'
-import { AppDataInfo, TypedAppDataHooks, UploadAppDataParams, useAppDataHooks } from 'modules/appData'
-import { useAppData, useUploadAppData } from 'modules/appData'
-import { useGetCachedPermit } from 'modules/permit'
-import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow'
import { BaseFlowContext } from 'modules/swap/services/types'
-import { TradeConfirmActions, useTradeConfirmActions } from 'modules/trade'
import { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics'
+import { getOrderValidTo } from 'modules/tradeQuote'
-import { useTokenContract, useWETHContract } from 'common/hooks/useContract'
-import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle'
import { useSafeMemo } from 'common/hooks/useSafeMemo'
-import { useIsSafeEthFlow } from './useIsSafeEthFlow'
import { useSwapSlippage } from './useSwapSlippage'
-import { useDerivedSwapInfo, useSwapState } from './useSwapState'
+import { useDerivedSwapInfo } from './useSwapState'
-import { getOrderValidTo } from '../../tradeQuote/utils/quoteDeadline'
import { getAmountsForSignature } from '../helpers/getAmountsForSignature'
-
-export enum FlowType {
- REGULAR = 'REGULAR',
- EOA_ETH_FLOW = 'EOA_ETH_FLOW',
- SAFE_BUNDLE_APPROVAL = 'SAFE_BUNDLE_APPROVAL',
- SAFE_BUNDLE_ETH = 'SAFE_BUNDLE_ETH',
-}
-
-interface BaseFlowContextSetup {
- chainId: SupportedChainId
- account: string | undefined
- sellTokenContract: Erc20 | null
- provider: Web3Provider | undefined
- trade: TradeGp | undefined
- appData: AppDataInfo | null
- wethContract: Weth | null
- inputAmountWithSlippage: CurrencyAmount | undefined
- outputAmountWithSlippage: CurrencyAmount | undefined
- gnosisSafeInfo: GnosisSafeInfo | undefined
- recipient: string | null
- recipientAddressOrName: string | null
- deadline: number
- ensRecipientAddress: string | null
- allowsOffchainSigning: boolean
- flowType: FlowType
- closeModals: Command
- uploadAppData: (update: UploadAppDataParams) => void
- addOrderCallback: AddOrderCallback
- dispatch: AppDispatch
- allowedSlippage: Percent
- tradeConfirmActions: TradeConfirmActions
- getCachedPermit: ReturnType
- quote: QuoteInformationObject | undefined
- typedHooks: TypedAppDataHooks | undefined
-}
+import { baseFlowContextSourceAtom } from '../state/baseFlowContextSourceAtom'
+import { BaseFlowContextSource } from '../types/flowContext'
export function useSwapAmountsWithSlippage(): [
CurrencyAmount | undefined,
@@ -87,115 +34,12 @@ export function useSwapAmountsWithSlippage(): [
return useSafeMemo(() => [INPUT, OUTPUT], [INPUT, OUTPUT])
}
-export function useBaseFlowContextSetup(): BaseFlowContextSetup {
- const provider = useWalletProvider()
- const { account, chainId } = useWalletInfo()
- const { allowsOffchainSigning } = useWalletDetails()
- const gnosisSafeInfo = useGnosisSafeInfo()
- const { recipient } = useSwapState()
- const slippage = useSwapSlippage()
- const { trade, currenciesIds } = useDerivedSwapInfo()
- const { quote } = useGetQuoteAndStatus({
- token: currenciesIds.INPUT,
- chainId,
- })
-
- const appData = useAppData()
- const typedHooks = useAppDataHooks()
- const closeModals = useCloseModals()
- const uploadAppData = useUploadAppData()
- const addOrderCallback = useAddPendingOrder()
- const dispatch = useDispatch()
- const tradeConfirmActions = useTradeConfirmActions()
-
- const { address: ensRecipientAddress } = useENSAddress(recipient)
- const recipientAddressOrName = recipient || ensRecipientAddress
- const [deadline] = useUserTransactionTTL()
- const wethContract = useWETHContract()
- const isEoaEthFlow = useIsEoaEthFlow()
- const isSafeEthFlow = useIsSafeEthFlow()
- const getCachedPermit = useGetCachedPermit()
-
- const [inputAmountWithSlippage, outputAmountWithSlippage] = useSwapAmountsWithSlippage()
- const sellTokenContract = useTokenContract(getAddress(inputAmountWithSlippage?.currency) || undefined, true)
-
- const isSafeBundle = useIsSafeApprovalBundle(inputAmountWithSlippage)
- const flowType = _getFlowType(isSafeBundle, isEoaEthFlow, isSafeEthFlow)
-
- return useSafeMemo(
- () => ({
- chainId,
- account,
- sellTokenContract,
- provider,
- trade,
- appData,
- wethContract,
- inputAmountWithSlippage,
- outputAmountWithSlippage,
- gnosisSafeInfo,
- recipient,
- recipientAddressOrName,
- deadline,
- ensRecipientAddress,
- allowsOffchainSigning,
- uploadAppData,
- flowType,
- closeModals,
- addOrderCallback,
- dispatch,
- allowedSlippage: slippage,
- tradeConfirmActions,
- getCachedPermit,
- quote,
- typedHooks,
- }),
- [
- chainId,
- account,
- sellTokenContract,
- provider,
- trade,
- appData,
- wethContract,
- inputAmountWithSlippage,
- outputAmountWithSlippage,
- gnosisSafeInfo,
- recipient,
- recipientAddressOrName,
- deadline,
- ensRecipientAddress,
- allowsOffchainSigning,
- uploadAppData,
- flowType,
- closeModals,
- addOrderCallback,
- dispatch,
- slippage,
- tradeConfirmActions,
- getCachedPermit,
- quote,
- typedHooks,
- ],
- )
-}
-
-function _getFlowType(isSafeBundle: boolean, isEoaEthFlow: boolean, isSafeEthFlow: boolean): FlowType {
- if (isSafeEthFlow) {
- // Takes precedence over bundle approval
- return FlowType.SAFE_BUNDLE_ETH
- } else if (isSafeBundle) {
- // Takes precedence over eth flow
- return FlowType.SAFE_BUNDLE_APPROVAL
- } else if (isEoaEthFlow) {
- // Takes precedence over regular flow
- return FlowType.EOA_ETH_FLOW
- }
- return FlowType.REGULAR
+export function useBaseFlowContextSource(): BaseFlowContextSource | null {
+ return useAtomValue(baseFlowContextSourceAtom)
}
type BaseGetFlowContextProps = {
- baseProps: BaseFlowContextSetup
+ baseProps: BaseFlowContextSource
sellToken?: Token
kind: OrderKind
}
diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts
index f0f3410fed..6f342d3120 100644
--- a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts
+++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleApprovalFlowContext.ts
@@ -2,13 +2,14 @@ import { useMemo } from 'react'
import { getWrappedToken } from '@cowprotocol/common-utils'
-import { FlowType } from 'modules/swap/hooks/useFlowContext'
import { SafeBundleApprovalFlowContext } from 'modules/swap/services/types'
import { useTokenContract } from 'common/hooks/useContract'
import { useBaseSafeBundleFlowContext } from './useBaseSafeBundleFlowContext'
+import { FlowType } from '../types/flowContext'
+
export function useSafeBundleApprovalFlowContext(): SafeBundleApprovalFlowContext | null {
const baseContext = useBaseSafeBundleFlowContext()
const trade = baseContext?.context.trade
diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts
index d7e9781149..cc8bfb1a15 100644
--- a/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts
+++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSafeBundleEthFlowContext.ts
@@ -1,6 +1,5 @@
import { useMemo } from 'react'
-import { FlowType } from 'modules/swap/hooks/useFlowContext'
import { SafeBundleEthFlowContext } from 'modules/swap/services/types'
import { useWETHContract } from 'common/hooks/useContract'
@@ -8,6 +7,8 @@ import { useNeedsApproval } from 'common/hooks/useNeedsApproval'
import { useBaseSafeBundleFlowContext } from './useBaseSafeBundleFlowContext'
+import { FlowType } from '../types/flowContext'
+
export function useSafeBundleEthFlowContext(): SafeBundleEthFlowContext | null {
const baseContext = useBaseSafeBundleFlowContext()
diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts
index 4beb0715c4..77bbff00bb 100644
--- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts
+++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapFlowContext.ts
@@ -2,38 +2,34 @@ import { useMemo } from 'react'
import { getWrappedToken } from '@cowprotocol/common-utils'
import { COW_PROTOCOL_VAULT_RELAYER_ADDRESS, OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk'
-import { useWalletInfo } from '@cowprotocol/wallet'
import { TradeType as UniTradeType } from '@uniswap/sdk-core'
import { useGeneratePermitHook, usePermitInfo } from 'modules/permit'
-import {
- FlowType,
- getFlowContext,
- useBaseFlowContextSetup,
- useSwapAmountsWithSlippage,
-} from 'modules/swap/hooks/useFlowContext'
+import { getFlowContext, useBaseFlowContextSource } from 'modules/swap/hooks/useFlowContext'
import { SwapFlowContext } from 'modules/swap/services/types'
import { useEnoughBalanceAndAllowance } from 'modules/tokens'
import { TradeType } from 'modules/trade'
import { useGP2SettlementContract } from 'common/hooks/useContract'
+import { FlowType } from '../types/flowContext'
+
export function useSwapFlowContext(): SwapFlowContext | null {
const contract = useGP2SettlementContract()
- const baseProps = useBaseFlowContextSetup()
- const sellCurrency = baseProps.trade?.inputAmount?.currency
+ const baseProps = useBaseFlowContextSource()
+ const sellCurrency = baseProps?.trade?.inputAmount?.currency
const permitInfo = usePermitInfo(sellCurrency, TradeType.SWAP)
const generatePermitHook = useGeneratePermitHook()
- const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[baseProps.chainId || SupportedChainId.MAINNET]
+ const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[baseProps?.chainId || SupportedChainId.MAINNET]
const { enoughAllowance } = useEnoughBalanceAndAllowance({
- account: baseProps.account,
- amount: baseProps.inputAmountWithSlippage,
+ account: baseProps?.account,
+ amount: baseProps?.inputAmountWithSlippage,
checkAllowanceAddress,
})
return useMemo(() => {
- if (!baseProps.trade) {
+ if (!baseProps?.trade) {
return null
}
@@ -55,17 +51,3 @@ export function useSwapFlowContext(): SwapFlowContext | null {
}
}, [baseProps, contract, enoughAllowance, permitInfo, generatePermitHook])
}
-
-export function useSwapEnoughAllowance(): boolean | undefined {
- const { chainId, account } = useWalletInfo()
- const [inputAmountWithSlippage] = useSwapAmountsWithSlippage()
-
- const checkAllowanceAddress = COW_PROTOCOL_VAULT_RELAYER_ADDRESS[chainId]
- const { enoughAllowance } = useEnoughBalanceAndAllowance({
- account,
- amount: inputAmountWithSlippage,
- checkAllowanceAddress,
- })
-
- return enoughAllowance
-}
diff --git a/apps/cowswap-frontend/src/modules/swap/services/types.ts b/apps/cowswap-frontend/src/modules/swap/services/types.ts
index 8ed51c9e7a..e48dd370f5 100644
--- a/apps/cowswap-frontend/src/modules/swap/services/types.ts
+++ b/apps/cowswap-frontend/src/modules/swap/services/types.ts
@@ -17,7 +17,8 @@ import { TradeConfirmActions } from 'modules/trade'
import { TradeFlowAnalyticsContext } from 'modules/trade/utils/tradeFlowAnalytics'
import { EthFlowOrderExistsCallback } from '../hooks/useCheckEthFlowOrderExists'
-import { FlowType } from '../hooks/useFlowContext'
+import { FlowType } from '../types/flowContext'
+
export interface BaseFlowContext {
context: {
chainId: number
diff --git a/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts b/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts
new file mode 100644
index 0000000000..5295aaffde
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/swap/state/baseFlowContextSourceAtom.ts
@@ -0,0 +1,5 @@
+import { atom } from 'jotai'
+
+import { BaseFlowContextSource } from '../types/flowContext'
+
+export const baseFlowContextSourceAtom = atom(null)
diff --git a/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts b/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts
new file mode 100644
index 0000000000..79b9fb126d
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/swap/types/flowContext.ts
@@ -0,0 +1,51 @@
+import type { Erc20, Weth } from '@cowprotocol/abis'
+import type { SupportedChainId } from '@cowprotocol/cow-sdk'
+import type { Command } from '@cowprotocol/types'
+import type { GnosisSafeInfo } from '@cowprotocol/wallet'
+import type { Web3Provider } from '@ethersproject/providers'
+import type { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
+
+import type { AppDispatch } from 'legacy/state'
+import type { AddOrderCallback } from 'legacy/state/orders/hooks'
+import type { QuoteInformationObject } from 'legacy/state/price/reducer'
+import type TradeGp from 'legacy/state/swap/TradeGp'
+
+import type { useGetCachedPermit } from 'modules/permit'
+import type { TradeConfirmActions } from 'modules/trade'
+
+import type { AppDataInfo, TypedAppDataHooks, UploadAppDataParams } from '../../appData'
+
+export enum FlowType {
+ REGULAR = 'REGULAR',
+ EOA_ETH_FLOW = 'EOA_ETH_FLOW',
+ SAFE_BUNDLE_APPROVAL = 'SAFE_BUNDLE_APPROVAL',
+ SAFE_BUNDLE_ETH = 'SAFE_BUNDLE_ETH',
+}
+
+export interface BaseFlowContextSource {
+ chainId: SupportedChainId
+ account: string | undefined
+ sellTokenContract: Erc20 | null
+ provider: Web3Provider | undefined
+ trade: TradeGp | undefined
+ appData: AppDataInfo | null
+ wethContract: Weth | null
+ inputAmountWithSlippage: CurrencyAmount | undefined
+ outputAmountWithSlippage: CurrencyAmount | undefined
+ gnosisSafeInfo: GnosisSafeInfo | undefined
+ recipient: string | null
+ recipientAddressOrName: string | null
+ deadline: number
+ ensRecipientAddress: string | null
+ allowsOffchainSigning: boolean
+ flowType: FlowType
+ closeModals: Command
+ uploadAppData: (update: UploadAppDataParams) => void
+ addOrderCallback: AddOrderCallback
+ dispatch: AppDispatch
+ allowedSlippage: Percent
+ tradeConfirmActions: TradeConfirmActions
+ getCachedPermit: ReturnType
+ quote: QuoteInformationObject | undefined
+ typedHooks: TypedAppDataHooks | undefined
+}
diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx b/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx
new file mode 100644
index 0000000000..12857e8c11
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/swap/updaters/BaseFlowContextUpdater.tsx
@@ -0,0 +1,147 @@
+import { useSetAtom } from 'jotai'
+import { useEffect } from 'react'
+
+import { getAddress } from '@cowprotocol/common-utils'
+import { useENSAddress } from '@cowprotocol/ens'
+import { useGnosisSafeInfo, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet'
+import { useWalletProvider } from '@cowprotocol/wallet-provider'
+
+import { useDispatch } from 'react-redux'
+
+import { AppDispatch } from 'legacy/state'
+import { useCloseModals } from 'legacy/state/application/hooks'
+import { useAddPendingOrder } from 'legacy/state/orders/hooks'
+import { useGetQuoteAndStatus } from 'legacy/state/price/hooks'
+import { useUserTransactionTTL } from 'legacy/state/user/hooks'
+
+import { useAppData, useAppDataHooks, useUploadAppData } from 'modules/appData'
+import { useGetCachedPermit } from 'modules/permit'
+import { useTradeConfirmActions } from 'modules/trade'
+
+import { useTokenContract, useWETHContract } from 'common/hooks/useContract'
+import { useIsSafeApprovalBundle } from 'common/hooks/useIsSafeApprovalBundle'
+import { useSafeMemo } from 'common/hooks/useSafeMemo'
+
+import { useSwapAmountsWithSlippage } from '../hooks/useFlowContext'
+import { useIsEoaEthFlow } from '../hooks/useIsEoaEthFlow'
+import { useIsSafeEthFlow } from '../hooks/useIsSafeEthFlow'
+import { useSwapSlippage } from '../hooks/useSwapSlippage'
+import { useDerivedSwapInfo, useSwapState } from '../hooks/useSwapState'
+import { baseFlowContextSourceAtom } from '../state/baseFlowContextSourceAtom'
+import { FlowType } from '../types/flowContext'
+
+export function BaseFlowContextUpdater() {
+ const setBaseFlowContextSource = useSetAtom(baseFlowContextSourceAtom)
+ const provider = useWalletProvider()
+ const { account, chainId } = useWalletInfo()
+ const { allowsOffchainSigning } = useWalletDetails()
+ const gnosisSafeInfo = useGnosisSafeInfo()
+ const { recipient } = useSwapState()
+ const slippage = useSwapSlippage()
+ const { trade, currenciesIds } = useDerivedSwapInfo()
+ const { quote } = useGetQuoteAndStatus({
+ token: currenciesIds.INPUT,
+ chainId,
+ })
+
+ const appData = useAppData()
+ const typedHooks = useAppDataHooks()
+ const closeModals = useCloseModals()
+ const uploadAppData = useUploadAppData()
+ const addOrderCallback = useAddPendingOrder()
+ const dispatch = useDispatch()
+ const tradeConfirmActions = useTradeConfirmActions()
+
+ const { address: ensRecipientAddress } = useENSAddress(recipient)
+ const recipientAddressOrName = recipient || ensRecipientAddress
+ const [deadline] = useUserTransactionTTL()
+ const wethContract = useWETHContract()
+ const isEoaEthFlow = useIsEoaEthFlow()
+ const isSafeEthFlow = useIsSafeEthFlow()
+ const getCachedPermit = useGetCachedPermit()
+
+ const [inputAmountWithSlippage, outputAmountWithSlippage] = useSwapAmountsWithSlippage()
+ const sellTokenContract = useTokenContract(getAddress(inputAmountWithSlippage?.currency) || undefined, true)
+
+ const isSafeBundle = useIsSafeApprovalBundle(inputAmountWithSlippage)
+ const flowType = getFlowType(isSafeBundle, isEoaEthFlow, isSafeEthFlow)
+
+ const source = useSafeMemo(
+ () => ({
+ chainId,
+ account,
+ sellTokenContract,
+ provider,
+ trade,
+ appData,
+ wethContract,
+ inputAmountWithSlippage,
+ outputAmountWithSlippage,
+ gnosisSafeInfo,
+ recipient,
+ recipientAddressOrName,
+ deadline,
+ ensRecipientAddress,
+ allowsOffchainSigning,
+ uploadAppData,
+ flowType,
+ closeModals,
+ addOrderCallback,
+ dispatch,
+ allowedSlippage: slippage,
+ tradeConfirmActions,
+ getCachedPermit,
+ quote,
+ typedHooks,
+ }),
+ [
+ chainId,
+ account,
+ sellTokenContract,
+ provider,
+ trade,
+ appData,
+ wethContract,
+ inputAmountWithSlippage,
+ outputAmountWithSlippage,
+ gnosisSafeInfo,
+ recipient,
+ recipientAddressOrName,
+ deadline,
+ ensRecipientAddress,
+ allowsOffchainSigning,
+ uploadAppData,
+ flowType,
+ closeModals,
+ addOrderCallback,
+ dispatch,
+ slippage,
+ tradeConfirmActions,
+ getCachedPermit,
+ quote,
+ typedHooks,
+ ],
+ )
+
+ useEffect(() => {
+ setBaseFlowContextSource(source)
+ }, [source, setBaseFlowContextSource])
+
+ return null
+}
+
+function getFlowType(isSafeBundle: boolean, isEoaEthFlow: boolean, isSafeEthFlow: boolean): FlowType {
+ if (isSafeEthFlow) {
+ // Takes precedence over bundle approval
+ return FlowType.SAFE_BUNDLE_ETH
+ }
+ if (isSafeBundle) {
+ // Takes precedence over eth flow
+ return FlowType.SAFE_BUNDLE_APPROVAL
+ }
+ if (isEoaEthFlow) {
+ // Takes precedence over regular flow
+ return FlowType.EOA_ETH_FLOW
+ }
+ return FlowType.REGULAR
+}
diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx
index 80018cf1e3..24d858e630 100644
--- a/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx
+++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeConfirmation/index.tsx
@@ -16,6 +16,9 @@ import ms from 'ms.macro'
import { upToMedium, useMediaQuery } from 'legacy/hooks/useMediaQuery'
import { PriceImpact } from 'legacy/hooks/usePriceImpact'
+import type { AppDataInfo } from 'modules/appData'
+
+import { OrderHooksDetails } from 'common/containers/OrderHooksDetails'
import { CurrencyAmountPreview, CurrencyPreviewInfo } from 'common/pure/CurrencyInputPanel'
import { QuoteCountdown } from './CountDown'
@@ -23,6 +26,7 @@ import { useIsPriceChanged } from './hooks/useIsPriceChanged'
import * as styledEl from './styled'
import { useTradeConfirmState } from '../../hooks/useTradeConfirmState'
+import { ConfirmDetailsItem } from '../ConfirmDetailsItem'
import { PriceUpdatedBanner } from '../PriceUpdatedBanner'
const ONE_SEC = ms`1s`
@@ -34,6 +38,7 @@ export interface TradeConfirmationProps {
account: string | undefined
ensName: string | undefined
+ appData?: string | AppDataInfo
inputCurrencyInfo: CurrencyPreviewInfo
outputCurrencyInfo: CurrencyPreviewInfo
isConfirmDisabled: boolean
@@ -70,6 +75,7 @@ export function TradeConfirmation(props: TradeConfirmationProps) {
children,
recipient,
isPriceStatic,
+ appData,
} = frozenProps || props
/**
@@ -143,6 +149,15 @@ export function TradeConfirmation(props: TradeConfirmationProps) {
/>
{children}
+ {appData && (
+
+ {(children) => (
+
+ {children}
+
+ )}
+
+ )}
{/*Banners*/}
{showRecipientWarning && }
{isPriceChanged && !isPriceStatic && }
diff --git a/libs/hook-dapp-lib/src/utils.ts b/libs/hook-dapp-lib/src/utils.ts
index 4cbfc1a902..091af2011b 100644
--- a/libs/hook-dapp-lib/src/utils.ts
+++ b/libs/hook-dapp-lib/src/utils.ts
@@ -2,6 +2,9 @@ import { HOOK_DAPP_ID_LENGTH } from './consts'
import * as hookDappsRegistry from './hookDappsRegistry.json'
import { CowHook, HookDappBase } from './types'
+// permit() function selector
+const PERMIT_SELECTOR = '0xd505accf'
+
export interface HookToDappMatch {
dapp: HookDappBase | null
hook: CowHook
@@ -13,13 +16,27 @@ export function matchHooksToDapps(hooks: CowHook[], dapps: HookDappBase[]): Hook
acc[dapp.id] = dapp
return acc
},
- {} as Record,
+ {} as Record,
)
- return hooks.map((hook) => ({
- hook,
- dapp: dappsMap[hook.callData.slice(-HOOK_DAPP_ID_LENGTH)] || null,
- }))
+ return hooks.map((hook) => {
+ const dapp = dappsMap[hook.callData.slice(-HOOK_DAPP_ID_LENGTH)]
+
+ /**
+ * Permit token is a special case, as it's not a dapp, but a hook
+ */
+ if (!dapp && hook.callData.startsWith(PERMIT_SELECTOR)) {
+ return {
+ hook,
+ dapp: hookDappsRegistry.PERMIT_TOKEN as HookDappBase,
+ }
+ }
+
+ return {
+ hook,
+ dapp: dapp || null,
+ }
+ })
}
export function matchHooksToDappsRegistry(hooks: CowHook[]): HookToDappMatch[] {