diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx index 3107eb5ab1..8953a4b5ce 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookRegistryList/index.tsx @@ -3,16 +3,16 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg' import { Command } from '@cowprotocol/types' import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' -import { useIsSmartContractWallet, useWalletInfo } from '@cowprotocol/wallet' +import { useIsSmartContractWallet } from '@cowprotocol/wallet' import { NewModal } from 'common/pure/NewModal' import { EmptyList, HookDappsList, Wrapper } from './styled' -import { POST_HOOK_REGISTRY, PRE_HOOK_REGISTRY } from '../../hookRegistry' import { useAddCustomHookDapp } from '../../hooks/useAddCustomHookDapp' import { useCustomHookDapps } from '../../hooks/useCustomHookDapps' import { useHookById } from '../../hooks/useHookById' +import { useInternalHookDapps } from '../../hooks/useInternalHookDapps' import { useRemoveCustomHookDapp } from '../../hooks/useRemoveCustomHookDapp' import { AddCustomHookForm } from '../../pure/AddCustomHookForm' import { HookDappDetails } from '../../pure/HookDappDetails' @@ -31,7 +31,6 @@ interface HookStoreModal { } export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStoreModal) { - const { chainId } = useWalletInfo() const [selectedDapp, setSelectedDapp] = useState(null) const [dappDetails, setDappDetails] = useState(null) @@ -51,9 +50,7 @@ export function HookRegistryList({ onDismiss, isPreHook, hookToEdit }: HookStore setSearchQuery('') }, []) - const internalHookDapps = useMemo(() => { - return (isPreHook ? PRE_HOOK_REGISTRY[chainId] : POST_HOOK_REGISTRY[chainId]) || [] - }, [isPreHook, chainId]) + const internalHookDapps = useInternalHookDapps(isPreHook) const currentDapps = useMemo(() => { return isAllHooksTab ? internalHookDapps.concat(customHookDapps) : customHookDapps diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hook.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hook.tsx deleted file mode 100644 index 99c847b421..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/hook.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { HookDappInternal, HookDappType, HookDappWalletCompatibility } from 'modules/hooksStore/types/hooks' - -import airdropImage from './airdrop.svg' - -import { AirdropHookApp } from './index' - -const Description = () => { - return ( - <> -

- Effortless Airdrop Claims! - The Claim COW Airdrop feature simplifies the process of collecting free COW tokens before or after your swap, - seamlessly integrating into the CoW Swap platform. -

-
-

- Whether you're claiming new airdrops or exploring CoW on a new network, this tool ensures you get your rewards - quickly and easily. -

- - ) -} - -export const AIRDROP_HOOK_APP: HookDappInternal = { - name: 'Claim COW Airdrop', - description: , - descriptionShort: 'Retrieve COW tokens before or after a swap', - type: HookDappType.INTERNAL, - image: airdropImage, - component: (props) => , - version: '0.1.0', - website: 'https://github.com/bleu/cow-airdrop-contract-deployer', - walletCompatibility: [HookDappWalletCompatibility.EOA, HookDappWalletCompatibility.SMART_CONTRACT], -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/hook.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/hook.tsx deleted file mode 100644 index dd2f973f44..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/hook.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import buildImg from './build.png' - -import { HookDappInternal, HookDappType, HookDappWalletCompatibility } from '../../types/hooks' - -import { BuildHookApp } from './index' - -const getAppDetails = (isPreHook: boolean): HookDappInternal => { - return { - name: `Build your own ${isPreHook ? 'Pre' : 'Post'}-hook`, - descriptionShort: 'Call any smart contract with your own parameters', - description: `Didn't find a suitable hook? You can always create your own! To do this, you need to specify which smart contract you want to call, the parameters for the call and the gas limit.`, - type: HookDappType.INTERNAL, - image: buildImg, - component: (props) => , - version: 'v0.1.0', - website: 'https://docs.cow.fi/cow-protocol/reference/core/intents/hooks', - walletCompatibility: [HookDappWalletCompatibility.SMART_CONTRACT, HookDappWalletCompatibility.EOA], - } -} - -export const PRE_BUILD = getAppDetails(true) -export const POST_BUILD = getAppDetails(false) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/const.ts b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/const.ts index e6ea31fd70..a316307aae 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/const.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/const.ts @@ -1 +1,9 @@ +import { SBCDepositContract as SBCDepositContractType, SBCDepositContractAbi } from '@cowprotocol/abis' +import { Contract } from '@ethersproject/contracts' + export const SBC_DEPOSIT_CONTRACT_ADDRESS = '0x0B98057eA310F4d31F2a452B414647007d1645d9' + +export const SBCDepositContract = new Contract( + SBC_DEPOSIT_CONTRACT_ADDRESS, + SBCDepositContractAbi, +) as SBCDepositContractType diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/hook.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/hook.tsx deleted file mode 100644 index a1f7178a76..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/hook.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import gnoLogo from '@cowprotocol/assets/cow-swap/network-gnosis-chain-logo.svg' - -import { HookDappInternal, HookDappType, HookDappWalletCompatibility } from '../../types/hooks' - -import { ClaimGnoHookApp } from './index' - -export const PRE_CLAIM_GNO: HookDappInternal = { - name: 'Claim GNO from validators', - descriptionShort: 'Withdraw rewards from your Gnosis validators.', - description: ( - <> - This hook allows you to withdraw rewards from your Gnosis Chain validators through CoW Swap. It automates the - process of interacting with the Gnosis Deposit Contract, enabling you to claim any available rewards directly to - your specified withdrawal address. -
-
- The hook monitors your validator's accrued rewards and triggers the claimWithdrawals function when rewards are - ready for withdrawal. This simplifies the management of Gnosis validator earnings without requiring ready for - withdrawal. This simplifies the management of Gnosis validator earnings without requiring manual contract - interaction, providing a smoother and more efficient experience for users. - - ), - type: HookDappType.INTERNAL, - component: (props) => , - image: gnoLogo, - version: 'v0.1.1', - website: 'https://www.gnosis.io/', - walletCompatibility: [HookDappWalletCompatibility.SMART_CONTRACT, HookDappWalletCompatibility.EOA], -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx index 1b2ac92766..6c9be054d0 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/index.tsx @@ -1,17 +1,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react' +import { SupportedChainId } from '@cowprotocol/cow-sdk' import { ButtonPrimary } from '@cowprotocol/ui' import { UI } from '@cowprotocol/ui' +import { useWalletProvider } from '@cowprotocol/wallet-provider' import { BigNumber } from '@ethersproject/bignumber' import { formatUnits } from 'ethers/lib/utils' -import { SBC_DEPOSIT_CONTRACT_ADDRESS } from './const' -import { useSBCDepositContract } from './useSBCDepositContract' +import { SBC_DEPOSIT_CONTRACT_ADDRESS, SBCDepositContract } from './const' import { HookDappProps } from '../../types/hooks' import { ContentWrapper, Text, LoadingLabel, Wrapper } from '../styled' +const SbcDepositContractInterface = SBCDepositContract.interface + /** * Dapp that creates the hook to the connected wallet GNO Rewards. * @@ -20,42 +23,44 @@ import { ContentWrapper, Text, LoadingLabel, Wrapper } from '../styled' * - Master: 0x4fef25519256e24a1fc536f7677152da742fe3ef */ export function ClaimGnoHookApp({ context }: HookDappProps) { - const SbcDepositContract = useSBCDepositContract() + const provider = useWalletProvider() const [claimable, setClaimable] = useState(undefined) const [gasLimit, setGasLimit] = useState(undefined) const [error, setError] = useState(false) const loading = (!gasLimit || !claimable) && !error - const SbcDepositContractInterface = SbcDepositContract?.interface + const account = context?.account + const callData = useMemo(() => { - if (!SbcDepositContractInterface || !context?.account) { + if (!account) { return null } - return SbcDepositContractInterface.encodeFunctionData('claimWithdrawal', [context.account]) - }, [SbcDepositContractInterface, context]) + return SbcDepositContractInterface.encodeFunctionData('claimWithdrawal', [account]) + }, [context]) useEffect(() => { - if (!SbcDepositContract || !context?.account) { + if (!account || !provider) { return } + const handleError = (e: any) => { console.error('[ClaimGnoHookApp] Error getting balance/gasEstimation', e) setError(true) } // Get balance - SbcDepositContract.withdrawableAmount(context.account) + SBCDepositContract.connect(provider) + .withdrawableAmount(account) .then((claimable) => { console.log('[ClaimGnoHookApp] get claimable', claimable) setClaimable(claimable) }) .catch(handleError) - // Get gas estimation - SbcDepositContract.estimateGas.claimWithdrawal(context.account).then(setGasLimit).catch(handleError) - }, [SbcDepositContract, setClaimable, context]) + SBCDepositContract.connect(provider).estimateGas.claimWithdrawal(account).then(setGasLimit).catch(handleError) + }, [setClaimable, account, provider]) const clickOnAddHook = useCallback(() => { if (!callData || !gasLimit || !context || !claimable) { @@ -85,9 +90,9 @@ export function ClaimGnoHookApp({ context }: HookDappProps) { return ( - {!SbcDepositContractInterface ? ( + {context.chainId !== SupportedChainId.GNOSIS_CHAIN ? ( 'Unsupported network. Please change to Gnosis Chain' - ) : !context?.account ? ( + ) : !account ? ( 'Connect your wallet first' ) : ( <> diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/useSBCDepositContract.ts b/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/useSBCDepositContract.ts deleted file mode 100644 index 0724f8bf91..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/ClaimGnoHookApp/useSBCDepositContract.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { SBCDepositContract, SBCDepositContractAbi } from '@cowprotocol/abis' -import { SupportedChainId } from '@cowprotocol/cow-sdk' -import { useWalletInfo } from '@cowprotocol/wallet' - -import { useContract } from 'common/hooks/useContract' - -import { SBC_DEPOSIT_CONTRACT_ADDRESS } from './const' - -export function useSBCDepositContract(): SBCDepositContract | null { - const { chainId } = useWalletInfo() - return useContract( - chainId === SupportedChainId.GNOSIS_CHAIN ? SBC_DEPOSIT_CONTRACT_ADDRESS : undefined, - SBCDepositContractAbi, - true, - ) -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/hook.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/hook.tsx deleted file mode 100644 index 9f9d6fed74..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/hook.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import permitImg from './icon.png' - -import { HookDappInternal, HookDappType, HookDappWalletCompatibility } from '../../types/hooks' - -import { PermitHookApp } from './index' - -export const PERMIT_HOOK: HookDappInternal = { - name: `Permit a token`, - descriptionShort: 'Infinite permit an address to spend one token on your behalf', - description: `This hook allows you to permit an address to spend your tokens on your behalf. This is useful for allowing a smart contract to spend your tokens without needing to approve each transaction.`, - type: HookDappType.INTERNAL, - image: permitImg, - component: (props) => , - version: 'v0.1.0', - website: 'https://docs.cow.fi/cow-protocol/reference/core/intents/hooks', - walletCompatibility: [HookDappWalletCompatibility.EOA], -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx index 93fa2b9975..0fcf02b699 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx @@ -1,21 +1,26 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { hookDappsRegistry } from '@cowprotocol/hook-dapp-lib' -import { AIRDROP_HOOK_APP } from './dapps/AirdropHookApp/hook' -import { PRE_BUILD, POST_BUILD } from './dapps/BuildHookApp/hook' -import { PRE_CLAIM_GNO } from './dapps/ClaimGnoHookApp/hook' -import { PERMIT_HOOK } from './dapps/PermitHookApp/hook' +import { AirdropHookApp } from './dapps/AirdropHookApp' +import { BuildHookApp } from './dapps/BuildHookApp' +import { ClaimGnoHookApp } from './dapps/ClaimGnoHookApp' +import { PermitHookApp } from './dapps/PermitHookApp' import { HookDapp } from './types/hooks' -export const PRE_HOOK_REGISTRY: Record = { - [SupportedChainId.MAINNET]: [PRE_BUILD], - [SupportedChainId.GNOSIS_CHAIN]: [PRE_CLAIM_GNO, PRE_BUILD], - [SupportedChainId.SEPOLIA]: [PRE_BUILD, PERMIT_HOOK, AIRDROP_HOOK_APP], - [SupportedChainId.ARBITRUM_ONE]: [PRE_BUILD], -} - -export const POST_HOOK_REGISTRY: Record = { - [SupportedChainId.MAINNET]: [POST_BUILD], - [SupportedChainId.GNOSIS_CHAIN]: [POST_BUILD], - [SupportedChainId.SEPOLIA]: [POST_BUILD, AIRDROP_HOOK_APP, PERMIT_HOOK], - [SupportedChainId.ARBITRUM_ONE]: [POST_BUILD], -} +export const ALL_HOOK_DAPPS = [ + { + ...hookDappsRegistry.BUILD_CUSTOM_HOOK, + component: (props) => , + }, + { + ...hookDappsRegistry.CLAIM_GNO_FROM_VALIDATORS, + component: (props) => , + }, + { + ...hookDappsRegistry.PERMIT_TOKEN, + component: (props) => , + }, + { + ...hookDappsRegistry.CLAIM_COW_AIRDROP, + component: (props) => , + }, +] as HookDapp[] diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts index b8bf146d11..8b57cf575a 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts @@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid' import { setHooksAtom } from '../state/hookDetailsAtom' import { AddHook, CowHookDetailsSerialized, HookDapp } from '../types/hooks' -import { getHookDappId } from '../utils' +import { appendDappIdToCallData } from '../utils' export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { const updateHooks = useSetAtom(setHooksAtom) @@ -16,8 +16,15 @@ export function useAddHook(dapp: HookDapp, isPreHook: boolean): AddHook { const uuid = uuidv4() const hookDetails: CowHookDetailsSerialized = { - hookDetails: { ...hookToAdd, uuid }, - dappId: getHookDappId(dapp), + hookDetails: { + ...hookToAdd, + uuid, + hook: { + ...hookToAdd.hook, + callData: appendDappIdToCallData(hookToAdd.hook.callData, dapp.id), + }, + }, + dappId: dapp.id, } updateHooks((hooks) => { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts index f441010de0..0edeb39916 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAllHookDapps.ts @@ -1,17 +1,15 @@ import { useMemo } from 'react' -import { useWalletInfo } from '@cowprotocol/wallet' - import { useCustomHookDapps } from './useCustomHookDapps' +import { useInternalHookDapps } from './useInternalHookDapps' -import { POST_HOOK_REGISTRY, PRE_HOOK_REGISTRY } from '../hookRegistry' import { HookDapp } from '../types/hooks' export function useAllHookDapps(isPreHook: boolean): HookDapp[] { - const { chainId } = useWalletInfo() + const internalHookDapps = useInternalHookDapps(isPreHook) const customHookDapps = useCustomHookDapps(isPreHook) return useMemo(() => { - return (isPreHook ? PRE_HOOK_REGISTRY : POST_HOOK_REGISTRY)[chainId].concat(customHookDapps) - }, [customHookDapps, chainId]) + return internalHookDapps.concat(customHookDapps) + }, [customHookDapps, internalHookDapps]) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts index 8d30ecac43..c6f5e3d27f 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useEditHook.ts @@ -5,6 +5,7 @@ import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { setHooksAtom } from '../state/hookDetailsAtom' import { EditHook } from '../types/hooks' +import { appendDappIdToCallData } from '../utils' export function useEditHook(isPreHook: boolean): EditHook { const updateHooks = useSetAtom(setHooksAtom) @@ -18,9 +19,17 @@ export function useEditHook(isPreHook: boolean): EditHook { if (hookIndex < 0) return state const typeState = [...state[type]] + const hookDetails = typeState[hookIndex] + typeState[hookIndex] = { - ...typeState[hookIndex], - hookDetails: update, + ...hookDetails, + hookDetails: { + ...update, + hook: { + ...update.hook, + callData: appendDappIdToCallData(update.hook.callData, hookDetails.dappId), + }, + }, } return { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useInternalHookDapps.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useInternalHookDapps.ts new file mode 100644 index 0000000000..1d374ea50e --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useInternalHookDapps.ts @@ -0,0 +1,26 @@ +import { useMemo } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import { ALL_HOOK_DAPPS } from '../hookRegistry' +import { HookDapp } from '../types/hooks' + +export function useInternalHookDapps(isPreHook: boolean): HookDapp[] { + const { chainId } = useWalletInfo() + + return useMemo(() => { + return ALL_HOOK_DAPPS.filter((dapp) => { + const position = dapp?.conditions?.position + const supportedNetworks = dapp?.conditions?.supportedNetworks + + if (supportedNetworks && !supportedNetworks.includes(chainId)) return false + + if (position) { + if (isPreHook && position !== 'pre') return false + if (!isPreHook && position !== 'post') return false + } + + return true + }) + }, [chainId, isPreHook]) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useMatchHooksToDapps.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useMatchHooksToDapps.ts new file mode 100644 index 0000000000..bf06d1604f --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useMatchHooksToDapps.ts @@ -0,0 +1,21 @@ +import { useCallback, useMemo } from 'react' + +import { CowHook, matchHooksToDapps } from '@cowprotocol/hook-dapp-lib' + +import { useAllHookDapps } from './useAllHookDapps' + +export function useMatchHooksToDapps() { + const allPreHookDapps = useAllHookDapps(true) + const allPostHookDapps = useAllHookDapps(false) + + const allHookDapps = useMemo(() => { + return allPreHookDapps.concat(allPostHookDapps) + }, [allPreHookDapps, allPostHookDapps]) + + return useCallback( + (hooks: CowHook[]) => { + return matchHooksToDapps(hooks, allHookDapps) + }, + [allHookDapps], + ) +} 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 c24890d833..9d0832a918 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,19 +1,20 @@ import { Dispatch, SetStateAction, useEffect } from 'react' -import { HookDappBase, HookDappIframe, HookDappType } from '../../../types/hooks' +import { + HOOK_DAPP_ID_LENGTH, + HookDappBase, + HookDappType, + HookDappWalletCompatibility, +} from '@cowprotocol/hook-dapp-lib' +import { useWalletInfo } from '@cowprotocol/wallet' -interface HookDappConditions { - position?: 'post' | 'pre' - smartContractWalletSupported?: boolean -} +import { HookDappIframe } from '../../../types/hooks' -type HookDappBaseInfo = Omit +type HookDappBaseInfo = Omit -type HookDappManifest = HookDappBaseInfo & { - conditions?: HookDappConditions -} +const MANDATORY_DAPP_FIELDS: (keyof HookDappBaseInfo)[] = ['id', 'name', 'image', 'version', 'website'] -const MANDATORY_DAPP_FIELDS: (keyof HookDappBaseInfo)[] = ['name', 'image', 'version', 'website'] +const isHex = (val: string) => Boolean(val.match(/^[0-9a-f]+$/i)) interface ExternalDappLoaderProps { input: string @@ -32,6 +33,8 @@ export function ExternalDappLoader({ isSmartContractWallet, isPreHook, }: ExternalDappLoaderProps) { + const { chainId } = useWalletInfo() + useEffect(() => { let isRequestRelevant = true @@ -42,7 +45,7 @@ export function ExternalDappLoader({ .then((data) => { if (!isRequestRelevant) return - const { conditions = {}, ...dapp } = data.cow_hook_dapp as HookDappManifest + const { conditions = {}, ...dapp } = data.cow_hook_dapp as HookDappBase if (dapp) { const emptyFields = MANDATORY_DAPP_FIELDS.filter((field) => typeof dapp[field] === 'undefined') @@ -50,8 +53,16 @@ export function ExternalDappLoader({ if (emptyFields.length > 0) { setManifestError(`${emptyFields.join(',')} fields are no set.`) } else { - if (conditions.smartContractWalletSupported === false && isSmartContractWallet === true) { + 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(

@@ -92,7 +103,7 @@ export function ExternalDappLoader({ return () => { isRequestRelevant = false } - }, [input]) + }, [input, chainId]) return null } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx index 0107376e85..c292d66ed9 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/HookDappDetails/index.tsx @@ -1,11 +1,12 @@ import { useMemo } from 'react' +import { HookDappType, HookDappWalletCompatibility } from '@cowprotocol/hook-dapp-lib' import { Command } from '@cowprotocol/types' import { HelpTooltip } from '@cowprotocol/ui' import * as styled from './styled' -import { HookDapp, HookDappType, HookDappWalletCompatibility } from '../../types/hooks' +import { HookDapp } from '../../types/hooks' import { HookDetailHeader } from '../HookDetailHeader' interface HookDappDetailsProps { @@ -15,7 +16,8 @@ interface HookDappDetailsProps { export function HookDappDetails({ dapp, onSelect }: HookDappDetailsProps) { const tags = useMemo(() => { - const { version, website, type, walletCompatibility = [] } = dapp + const { version, website, type, conditions } = dapp + const walletCompatibility = conditions?.walletCompatibility || [] const getWalletCompatibilityTooltip = () => { const isSmartContract = walletCompatibility.includes(HookDappWalletCompatibility.SMART_CONTRACT) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts index 1610661e01..d00c525a10 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/state/customHookDappsAtom.ts @@ -8,7 +8,6 @@ import { walletInfoAtom } from '@cowprotocol/wallet' import { setHooksAtom } from './hookDetailsAtom' import { HookDappIframe } from '../types/hooks' -import { getHookDappId } from '../utils' type CustomHookDapps = Record @@ -69,7 +68,7 @@ export const removeCustomHookDappAtom = atom(null, (get, set, dapp: HookDappIfra [chainId]: currentState, }) - const hookDappId = getHookDappId(dapp) + const hookDappId = dapp.id // Delete applied hooks along with the deleting hook-dapp set(setHooksAtom, (hooksState) => ({ diff --git a/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts b/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts index dea723d383..7091944e34 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/types/hooks.ts @@ -1,44 +1,22 @@ import type { ReactNode } from 'react' -import type { +import { CowHook, CowHookCreation, HookDappOrderParams, CoWHookDappActions, HookDappContext as GenericHookDappContext, CowHookDetails, + HookDappBase, + HookDappType, } from '@cowprotocol/hook-dapp-lib' import type { Signer } from '@ethersproject/abstract-signer' export type { CowHook, CowHookCreation, HookDappOrderParams } -export enum HookDappType { - INTERNAL = 'INTERNAL', - IFRAME = 'IFRAME', -} - -export enum HookDappWalletCompatibility { - EOA = 'EOA', - SMART_CONTRACT = 'Smart contract', -} - -export interface HookDappBase { - name: string - descriptionShort?: string - description?: ReactNode | string - type: HookDappType - version: string - website: string - image: string - walletCompatibility: HookDappWalletCompatibility[] -} - -export type DappId = `${HookDappType}:::${HookDappBase['name']}` - export interface HookDappInternal extends HookDappBase { type: HookDappType.INTERNAL component: (props: HookDappProps) => ReactNode - walletCompatibility: HookDappWalletCompatibility[] } export interface HookDappIframe extends HookDappBase { @@ -50,7 +28,7 @@ export type HookDapp = HookDappInternal | HookDappIframe export interface CowHookDetailsSerialized { hookDetails: CowHookDetails - dappId: DappId + dappId: string } export type AddHook = CoWHookDappActions['addHook'] diff --git a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts index f3b14c81c5..0d379e5abf 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/utils.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/utils.ts @@ -1,21 +1,16 @@ -import { CowHookDetailsSerialized, DappId, HookDapp, HookDappBase, HookDappIframe, HookDappType } from './types/hooks' +import { HookDappType } from '@cowprotocol/hook-dapp-lib' + +import { CowHookDetailsSerialized, HookDapp, HookDappIframe } from './types/hooks' // Do a safe guard assertion that receives a HookDapp and asserts is a HookDappIframe export function isHookDappIframe(dapp: HookDapp): dapp is HookDappIframe { return dapp.type === HookDappType.IFRAME } -export const getHookDappId = (dapp: HookDapp): DappId => `${dapp.type}:::${dapp.name}` -export function parseDappId(id: DappId): Pick { - const [type, name] = id.split(':::') - - return { type: type as HookDappType, name } -} - export function findHookDappById(dapps: HookDapp[], hookDetails: CowHookDetailsSerialized): HookDapp | undefined { - return dapps.find((i) => { - const { type, name } = parseDappId(hookDetails.dappId) + return dapps.find((i) => i.id === hookDetails.dappId) +} - return i.type === type && i.name === name - }) +export function appendDappIdToCallData(callData: string, dappId: string): string { + return callData.endsWith(dappId) ? callData : callData + dappId } diff --git a/apps/hook-dapp-omnibridge/public/manifest.json b/apps/hook-dapp-omnibridge/public/manifest.json index aaf2080e3a..ff04cb43e3 100644 --- a/apps/hook-dapp-omnibridge/public/manifest.json +++ b/apps/hook-dapp-omnibridge/public/manifest.json @@ -23,15 +23,17 @@ "start_url": ".", "theme_color": "#ffffff", "cow_hook_dapp": { + "id": "75716a3cb48fdbb43ebdff58ce6c541f6a2c269be690513131355800367f2da2", "name": "Omnibridge", "descriptionShort": "Bridge from Gnosis Chain to Mainnet", "description": "The Omnibridge can be used to bridge ERC-20 tokens between Ethereum and Gnosis. The first time a token is bridged, a new ERC677 token contract is deployed on GC with an additional suffix to differentiate the token. It will say \"token name on xDai\", as this was the original chain name prior to re-branding. If a token has been bridged previously, the previously deployed contract is used. The requested token amount is minted and sent to the account initiating the transfer (or an alternative receiver account specified by the sender).", "version": "0.0.1", "website": "https://omni.legacy.gnosischain.com", - "image": "http://localhost:3000/hook-dapp-omnibridge/apple-touch-icon.png", + "image": "http://localhost:4317/hook-dapp-omnibridge/apple-touch-icon.png", "conditions": { "position": "post", - "smartContractWalletSupported": false + "smartContractWalletSupported": false, + "supportedNetworks": [100] } } } diff --git a/libs/hook-dapp-lib/package.json b/libs/hook-dapp-lib/package.json index dbec39ed41..808cf0db50 100644 --- a/libs/hook-dapp-lib/package.json +++ b/libs/hook-dapp-lib/package.json @@ -21,7 +21,6 @@ "hook-dapp-lib" ], "dependencies": { - "@cowprotocol/cow-sdk": "^5.4.1", "@cowprotocol/iframe-transport": "^1.0.0" } } diff --git a/libs/hook-dapp-lib/src/consts.ts b/libs/hook-dapp-lib/src/consts.ts new file mode 100644 index 0000000000..b09a9ce29d --- /dev/null +++ b/libs/hook-dapp-lib/src/consts.ts @@ -0,0 +1,11 @@ +export enum HookDappType { + INTERNAL = 'INTERNAL', + IFRAME = 'IFRAME', +} + +export enum HookDappWalletCompatibility { + EOA = 'EOA', + SMART_CONTRACT = 'SMART_CONTRACT', +} + +export const HOOK_DAPP_ID_LENGTH = 64 diff --git a/libs/hook-dapp-lib/src/hookDappsRegistry.json b/libs/hook-dapp-lib/src/hookDappsRegistry.json new file mode 100644 index 0000000000..ce98edb976 --- /dev/null +++ b/libs/hook-dapp-lib/src/hookDappsRegistry.json @@ -0,0 +1,53 @@ +{ + "BUILD_CUSTOM_HOOK": { + "id": "c768665aa144bcf18c14eea0249b6322050e5daeba046d7e94df743a2e504586", + "type": "INTERNAL", + "name": "Build your own hook", + "descriptionShort": "Call any smart contract with your own parameters", + "description": "Didn't find a suitable hook? You can always create your own! To do this, you need to specify which smart contract you want to call, the parameters for the call and the gas limit.", + "image": "https://raw.githubusercontent.com/cowprotocol/cowswap/refs/heads/develop/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/build.png", + "version": "v0.1.0", + "website": "https://docs.cow.fi/cow-protocol/tutorials/hook-dapp" + }, + "CLAIM_GNO_FROM_VALIDATORS": { + "id": "ee4a6b1065cda592972b9ff7448ec111f29a566f137fef101ead7fbf8b01dd0b", + "type": "INTERNAL", + "name": "Claim GNO from validators", + "descriptionShort": "Withdraw rewards from your Gnosis validators.", + "description": "This hook allows you to withdraw rewards from your Gnosis Chain validators through CoW Swap. It automates the process of interacting with the Gnosis Deposit Contract, enabling you to claim any available rewards directly to your specified withdrawal address. The hook monitors your validator's accrued rewards and triggers the claimWithdrawals function when rewards are ready for withdrawal. This simplifies the management of Gnosis validator earnings without requiring ready for withdrawal. This simplifies the management of Gnosis validator earnings without requiring manual contract interaction, providing a smoother and more efficient experience for users.", + "image": "https://raw.githubusercontent.com/cowprotocol/cowswap/897ce91ca60a6b2d3823e6a002c3bf64c5384afe/libs/assets/src/cow-swap/network-gnosis-chain-logo.svg", + "version": "v0.1.1", + "website": "https://www.gnosis.io", + "conditions": { + "supportedNetworks": [100], + "position": "pre" + } + }, + "PERMIT_TOKEN": { + "id": "1db4bacb661a90fb6b475fd5b585acba9745bc373573c65ecc3e8f5bfd5dee1f", + "type": "INTERNAL", + "name": "Permit a token", + "descriptionShort": "Infinite permit an address to spend one token on your behalf.", + "description": "This hook allows you to permit an address to spend your tokens on your behalf. This is useful for allowing a smart contract to spend your tokens without needing to approve each transaction.", + "image": "https://raw.githubusercontent.com/cowprotocol/cowswap/refs/heads/develop/apps/cowswap-frontend/src/modules/hooksStore/dapps/PermitHookApp/icon.png", + "version": "v0.1.0", + "website": "https://docs.cow.fi/cow-protocol/tutorials/hook-dapp", + "conditions": { + "walletCompatibility": ["EOA"], + "supportedNetworks": [11155111] + } + }, + "CLAIM_COW_AIRDROP": { + "id": "40ed08569519f3b58c410ba35a8e684612663a7c9b58025e0a9c3a54551fb0ff", + "type": "INTERNAL", + "name": "Claim COW Airdrop", + "descriptionShort": "Retrieve COW tokens before or after a swap.", + "description": "Effortless Airdrop Claims! The Claim COW Airdrop feature simplifies the process of collecting free COW tokens before or after your swap, seamlessly integrating into the CoW Swap platform. Whether you're claiming new airdrops or exploring CoW on a new network, this tool ensures you get your rewards quickly and easily.", + "image": "https://raw.githubusercontent.com/cowprotocol/cowswap/897ce91ca60a6b2d3823e6a002c3bf64c5384afe/apps/cowswap-frontend/src/modules/hooksStore/dapps/AirdropHookApp/airdrop.svg", + "version": "v0.1.0", + "website": "https://github.com/bleu/cow-airdrop-contract-deployer", + "conditions": { + "supportedNetworks": [11155111] + } + } +} diff --git a/libs/hook-dapp-lib/src/index.ts b/libs/hook-dapp-lib/src/index.ts index f5d3fe208d..aecd63a181 100644 --- a/libs/hook-dapp-lib/src/index.ts +++ b/libs/hook-dapp-lib/src/index.ts @@ -1,3 +1,7 @@ export { initCoWHookDapp } from './initCoWHookDapp' export * from './hookDappIframeTransport' export * from './types' +export * from './consts' +export * from './utils' +import * as hookDappsRegistry from './hookDappsRegistry.json' +export { hookDappsRegistry } diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index 32201d1725..da26daa099 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -1,4 +1,6 @@ -import type { SupportedChainId } from '@cowprotocol/cow-sdk' +import type { ReactNode } from 'react' + +import { HookDappType, HookDappWalletCompatibility } from './consts' export interface CowHook { target: string @@ -6,6 +8,12 @@ export interface CowHook { gasLimit: string } +export interface HookDappConditions { + position?: 'post' | 'pre' + walletCompatibility?: HookDappWalletCompatibility[] + supportedNetworks?: number[] +} + export interface CowHookCreation { hook: CowHook recipientOverride?: string @@ -33,7 +41,7 @@ export interface HookDappOrderParams { } export interface HookDappContext { - chainId: SupportedChainId + chainId: number account?: string orderParams: HookDappOrderParams | null hookToEdit?: CowHookDetails @@ -41,3 +49,15 @@ export interface HookDappContext { isPreHook: boolean isDarkMode: boolean } + +export interface HookDappBase { + id: string + name: string + descriptionShort?: string + description?: ReactNode | string + type: HookDappType + version: string + website: string + image: string + conditions?: HookDappConditions +} diff --git a/libs/hook-dapp-lib/src/utils.ts b/libs/hook-dapp-lib/src/utils.ts new file mode 100644 index 0000000000..4cbfc1a902 --- /dev/null +++ b/libs/hook-dapp-lib/src/utils.ts @@ -0,0 +1,27 @@ +import { HOOK_DAPP_ID_LENGTH } from './consts' +import * as hookDappsRegistry from './hookDappsRegistry.json' +import { CowHook, HookDappBase } from './types' + +export interface HookToDappMatch { + dapp: HookDappBase | null + hook: CowHook +} + +export function matchHooksToDapps(hooks: CowHook[], dapps: HookDappBase[]): HookToDappMatch[] { + const dappsMap = dapps.reduce( + (acc, dapp) => { + acc[dapp.id] = dapp + return acc + }, + {} as Record, + ) + + return hooks.map((hook) => ({ + hook, + dapp: dappsMap[hook.callData.slice(-HOOK_DAPP_ID_LENGTH)] || null, + })) +} + +export function matchHooksToDappsRegistry(hooks: CowHook[]): HookToDappMatch[] { + return matchHooksToDapps(hooks, Object.values(hookDappsRegistry) as HookDappBase[]) +}