diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx new file mode 100644 index 0000000000..ceb64112af --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/index.tsx @@ -0,0 +1,20 @@ +import { HookToDappMatch } from '@cowprotocol/hook-dapp-lib' + +import { Item, Wrapper } from './styled' + +export function HookItem({ item }: { item: HookToDappMatch }) { + return ( + + {item.dapp ? ( + + {item.dapp.name} +

+ {item.dapp.name} ({item.dapp.version}) +

+
+ ) : ( +
Unknown hook dapp
+ )} +
+ ) +} diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx new file mode 100644 index 0000000000..1a16cca1a6 --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/HookItem/styled.tsx @@ -0,0 +1,19 @@ +import styled from 'styled-components/macro' + +export const Item = styled.li` + list-style: none; + margin: 0; + padding: 0; +` + +export const Wrapper = styled.a` + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; + + > img { + width: 30px; + height: 30px; + } +` diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx new file mode 100644 index 0000000000..abf8be100a --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -0,0 +1,77 @@ +import { ReactElement, useMemo, useState } from 'react' + +import { latest } from '@cowprotocol/app-data' +import { HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib' + +import { ChevronDown, ChevronUp } from 'react-feather' + +import { AppDataInfo, decodeAppData } from 'modules/appData' + +import { HookItem } from './HookItem' +import { HooksList, InfoWrapper, ToggleButton, Wrapper } from './styled' + +interface OrderHooksDetailsProps { + appData: string | AppDataInfo + children: (content: ReactElement) => ReactElement +} + +export function OrderHooksDetails({ appData, children }: OrderHooksDetailsProps) { + const [isOpen, setOpen] = useState(false) + const appDataDoc = useMemo(() => { + return typeof appData === 'string' ? decodeAppData(appData) : appData.doc + }, [appData]) + + if (!appDataDoc) return null + + const metadata = appDataDoc.metadata as latest.Metadata + + const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || []) + const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || []) + + if (!preHooksToDapp.length && !postHooksToDapp.length) return null + + return children( + isOpen ? ( + + setOpen(false)}> + + + + + + ) : ( + + + {preHooksToDapp.length ? `Pre: ${preHooksToDapp.length}` : ''} + {preHooksToDapp.length && postHooksToDapp.length ? ' | ' : ''} + {postHooksToDapp.length ? `Post: ${postHooksToDapp.length}` : ''} + + setOpen(true)}> + + + + ), + ) +} + +interface HooksInfoProps { + data: HookToDappMatch[] + title: string +} + +function HooksInfo({ data, title }: HooksInfoProps) { + return ( + <> + {data.length ? ( + +

{title}

+ + {data.map((item) => { + return + })} + +
+ ) : null} + + ) +} diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx new file mode 100644 index 0000000000..c7a331922b --- /dev/null +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx @@ -0,0 +1,33 @@ +import styled from 'styled-components/macro' + +export const Wrapper = styled.div` + position: relative; + padding-right: 30px; +` + +export const HooksList = styled.ul` + margin: 0; + padding: 0; + padding-left: 10px; +` + +export const ToggleButton = styled.button` + cursor: pointer; + background: none; + border: 0; + outline: 0; + padding: 0; + margin: 0; + position: absolute; + right: 0; + top: -4px; + + &:hover { + opacity: 0.7; + } +` +export const InfoWrapper = styled.div` + h3 { + margin: 0; + } +` diff --git a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx index 927d342abc..89a101695b 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx @@ -25,6 +25,7 @@ import { useToggleAccountModal } from 'modules/account' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { EthFlowStepper } from 'modules/swap/containers/EthFlowStepper' +import { OrderHooksDetails } from 'common/containers/OrderHooksDetails' import { useCancelOrder } from 'common/hooks/useCancelOrder' import { isPending } from 'common/hooks/useCategorizeRecentActivity' import { useGetSurplusData } from 'common/hooks/useGetSurplusFiatValue' @@ -193,6 +194,7 @@ export function ActivityDetails(props: { const getShowCancellationModal = useCancelOrder() const isSwap = order && getUiOrderType(order) === UiOrderType.SWAP + const appData = !!order && order.fullAppData const { disableProgressBar } = useInjectedWidgetParams() @@ -390,9 +392,20 @@ export function ActivityDetails(props: { )} + + {appData && ( + + {(children) => ( + + Hooks + {children} + + )} + + )} ) : ( - summary ?? id + (summary ?? id) )} {activityLinkUrl && enhancedTransaction?.replacementType !== 'replaced' && ( diff --git a/apps/cowswap-frontend/src/modules/appData/index.ts b/apps/cowswap-frontend/src/modules/appData/index.ts index 2d10f8bb41..da9816331c 100644 --- a/apps/cowswap-frontend/src/modules/appData/index.ts +++ b/apps/cowswap-frontend/src/modules/appData/index.ts @@ -2,6 +2,7 @@ export { getAppData } from './utils/fullAppData' export * from './updater/AppDataUpdater' export { useAppData, useAppDataHooks, useUploadAppData } from './hooks' export { filterPermitSignerPermit } from './utils/appDataFilter' +export { decodeAppData } from './utils/decodeAppData' export { replaceHooksOnAppData, buildAppData, removePermitHookFromAppData } from './utils/buildAppData' export { buildAppDataHooks } from './utils/buildAppDataHooks' export * from './utils/getAppDataHooks' diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx index 339bc4aa22..ae3815e1b5 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx @@ -77,6 +77,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho tradeNavigate, inputCurrencyId, outputCurrencyId, + isDarkMode, ]) const dappProps = useMemo(() => ({ context, dapp, isPreHook }), [context, dapp, isPreHook]) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index d0e1862643..7cb991d41a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -10,6 +10,7 @@ import { useIsSellNative } from 'modules/trade' import { useSetRecipientOverride } from '../../hooks/useSetRecipientOverride' import { useSetupHooksStoreOrderParams } from '../../hooks/useSetupHooksStoreOrderParams' +import { IframeDappsManifestUpdater } from '../../updaters/iframeDappsManifestUpdater' import { HookRegistryList } from '../HookRegistryList' import { PostHookButton } from '../PostHookButton' import { PreHookButton } from '../PreHookButton' @@ -81,6 +82,7 @@ export function HooksStoreWidget() { + {isHookSelectionOpen && ( )} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts index d0998491d9..51623a63b0 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddCustomHookDapp.ts @@ -1,11 +1,11 @@ import { useSetAtom } from 'jotai' import { useCallback } from 'react' -import { addCustomHookDappAtom } from '../state/customHookDappsAtom' +import { upsertCustomHookDappAtom } from '../state/customHookDappsAtom' import { HookDappIframe } from '../types/hooks' export function useAddCustomHookDapp(isPreHook: boolean) { - const setState = useSetAtom(addCustomHookDappAtom) + const setState = useSetAtom(upsertCustomHookDappAtom) return useCallback( (dapp: HookDappIframe) => { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx index 9d0832a918..9d904c1d9e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AddCustomHookForm/CustomDappLoader/index.tsx @@ -1,20 +1,10 @@ import { Dispatch, SetStateAction, useEffect } from 'react' -import { - HOOK_DAPP_ID_LENGTH, - HookDappBase, - HookDappType, - HookDappWalletCompatibility, -} from '@cowprotocol/hook-dapp-lib' +import { HookDappBase, HookDappType } from '@cowprotocol/hook-dapp-lib' import { useWalletInfo } from '@cowprotocol/wallet' import { HookDappIframe } from '../../../types/hooks' - -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)) +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[] {