Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Wrapper } from '../styled'
import { ButtonPrimary } from '@cowprotocol/ui'
import { HookDappProps } from 'modules/hooksStore/types/hooks'
import { Address, Chain, createPublicClient, encodeFunctionData, formatUnits, http, isAddress, parseUnits } from 'viem'
import { Erc20Abi } from '@cowprotocol/abis'

import { mainnet, base } from 'viem/chains'

import { CowShedHooks } from '@cowprotocol/cow-sdk'
import { BaseTransaction, useCowShedSignature } from './useCowShedSignature'
import { BigNumber } from 'ethers'

import { JsonRpcProvider, Web3Provider } from '@ethersproject/providers'
import { useHandleTokenAllowance } from './useHandleTokenAllowance'
import { useReadTokenContract } from './useReadTokenContract'
import { useWalletProvider } from '@cowprotocol/common-hooks'

export function TransferHookApp({ context }: HookDappProps) {
const hookToEdit = context.hookToEdit
const isPreHook = context.isPreHook
const account = context?.account
const tokenAddress = context?.orderParams?.buyTokenAddress as Address | undefined

const target = tokenAddress

const web3Provider = useWalletProvider()
const signer = useMemo(() => web3Provider && web3Provider.getSigner(), [web3Provider])

const chain = ([mainnet, base].find((chain) => chain.id === context?.chainId) ?? mainnet) as Chain

const publicClient = useMemo(() => {
if (!context?.chainId) return
return createPublicClient({
chain,
transport: http(context?.chainId === 1 ? 'https://eth.llamarpc.com' : 'https://base.llamarpc.com'),
})
}, [context?.chainId])

const jsonRpcProvider = useMemo(() => {
if (!context?.chainId) return
return new JsonRpcProvider('https://eth.llamarpc.com')
}, [context?.chainId])

const { tokenDecimals, tokenSymbol, userBalance } = useReadTokenContract({
tokenAddress: context?.orderParams?.buyTokenAddress as Address | undefined,
context,
publicClient,
})

const formattedBalance =
tokenDecimals && userBalance !== undefined ? formatUnits(userBalance, tokenDecimals) : undefined

const gasLimit = '100000'

const cowShed = useMemo(() => {
if (!context?.chainId) return
return new CowShedHooks(context.chainId)
}, [context?.chainId])

const cowShedProxy = useMemo(() => {
if (!context?.account || !cowShed) return
return cowShed.proxyOf(context.account)
}, [context?.account, cowShed]) as Address | undefined

const cowShedSignature = useCowShedSignature({
cowShed,
signer,
context,
})

const handleTokenAllowance = useHandleTokenAllowance({
spender: cowShedProxy,
context,
web3Provider,
publicClient,
jsonRpcProvider,
signer,
})

const onButtonClick = useCallback(async () => {
if (!cowShed || !target || !gasLimit) return

//@ts-ignore
const amount = String(document?.getElementById('amount')?.value)
//@ts-ignore
const toAddress = String(document?.getElementById('address')?.value)

if (!amount || !toAddress || !isAddress(toAddress) || Number(amount) <= 0) return

const amountBigint = parseUnits(amount, tokenDecimals)

const callData = encodeFunctionData({
abi: Erc20Abi,
functionName: 'transferFrom',
args: [account, toAddress, amountBigint],
})

const hookTx: BaseTransaction = {
to: target,
value: BigInt(0),
callData,
}

const permitTx = await handleTokenAllowance(BigNumber.from(amount), tokenAddress)

const permitTxAdjusted = permitTx
? {
to: permitTx.target,
value: BigInt(0),
callData: permitTx.callData,
}
: undefined

const txs = permitTxAdjusted ? [permitTxAdjusted, hookTx] : [hookTx]

const cowShedCall = await cowShedSignature(txs)
if (!cowShedCall) throw new Error('Error signing hooks')

const hook = {
target: cowShed.getFactoryAddress(),
callData: cowShedCall,
gasLimit,
}

if (hookToEdit) {
context.editHook({ hook, uuid: hookToEdit.uuid })
return
}

context.addHook({ hook })
}, [target, gasLimit, context])

const buttonProps = useMemo(() => {
if (!context.account) return { message: 'Connect wallet', disabled: true }
if (!context.orderParams) return { message: 'Missing order params', disabled: true }
return { message: 'Add Post-hook', disabled: false }
}, [hookToEdit, context.account, isPreHook])

return (
<Wrapper>
<span>Amount of {tokenSymbol} to transfer</span>
<input
id="amount"
type="number"
inputMode="decimal"
step={`0.${'0'.repeat(tokenDecimals - 1)}1`}
style={{ padding: '8px', fontSize: 16 }}
/>
<span>Your balance: {formattedBalance}</span>
<span>Address to transfer</span>
<input id="address" style={{ padding: '8px', fontSize: 16 }} />
<ButtonPrimary onClick={onButtonClick} disabled={buttonProps.disabled}>
{buttonProps.message}
</ButtonPrimary>
</Wrapper>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useCallback, useMemo } from 'react'

import { SigningScheme } from '@cowprotocol/contracts'
import type { CowShedHooks, ICoWShedCall } from '@cowprotocol/cow-sdk'
import { HookDappContext } from '@cowprotocol/hook-dapp-lib'

import { stringToHex } from 'viem'

import type { Signer } from 'ethers'

export interface BaseTransaction {
to: string
value: bigint
callData: string
isDelegateCall?: boolean
}

export function useHookDeadline({ context }: { context: HookDappContext | undefined }) {
return useMemo(() => {
const now = new Date()
const validToOnTimezone = context?.orderParams?.validTo || 0
const validToTimestamp = validToOnTimezone + now.getTimezoneOffset() * 60
const currentTimestamp = new Date().getTime() / 1000
const oneHourAfter = Number(currentTimestamp.toFixed()) + 60 * 60

if (validToTimestamp < oneHourAfter) return BigInt(oneHourAfter)
return BigInt(validToTimestamp)
}, [context?.orderParams?.validTo])
}

export function getCowShedNonce() {
return stringToHex(Date.now().toString(), { size: 32 })
}

export function useCowShedSignature({
cowShed,
signer,
context,
}: {
cowShed: CowShedHooks | undefined
signer: Signer | undefined
context: HookDappContext | undefined
}) {
const hookDeadline = useHookDeadline({ context })

return useCallback(
async (txs: BaseTransaction[]) => {
if (!cowShed || !signer || !context?.account) return
const cowShedCalls: ICoWShedCall[] = txs.map((tx) => {
return {
target: tx.to,
value: BigInt(tx.value),
callData: tx.callData,
allowFailure: false,
isDelegateCall: !!tx.isDelegateCall,
}
})
const nonce = getCowShedNonce()
const signature = await cowShed
.signCalls(cowShedCalls, nonce, hookDeadline, signer, SigningScheme.EIP712)
.catch(() => {
throw new Error('User rejected signature')
})
return cowShed.encodeExecuteHooksForFactory(cowShedCalls, nonce, hookDeadline, context.account, signature)
},
[hookDeadline, cowShed, signer, context],
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useCallback } from 'react'

import { Erc20Abi } from '@cowprotocol/abis'
import { HookDappContext } from '@cowprotocol/hook-dapp-lib'
import {
type GetTokenPermitIntoResult,
type PermitInfo,
generatePermitHook,
getPermitUtilsInstance,
getTokenPermitInfo,
} from '@cowprotocol/permit-utils'
import { JsonRpcProvider, Web3Provider } from '@ethersproject/providers'

import { BigNumber, type Signer } from 'ethers'
import { Abi, type Address, maxUint256, PublicClient } from 'viem'

import { handleTokenApprove } from './useHandleTokenApprove'

export function useHandleTokenAllowance({
spender,
context,
web3Provider,
publicClient,
jsonRpcProvider,
signer,
}: {
spender: Address | undefined
context: HookDappContext
web3Provider: Web3Provider | undefined
publicClient: PublicClient | undefined
jsonRpcProvider: JsonRpcProvider | undefined
signer: Signer | undefined
}) {
return useCallback(
async (amount: BigNumber, tokenAddress: Address) => {
if (!publicClient || !jsonRpcProvider || !context?.account || !spender || !web3Provider)
throw new Error('Missing context')

const tokenContract = {
address: tokenAddress,
abi: Erc20Abi as Abi,
}

const [{ result: currentAllowance }, { result: tokenName }] = await publicClient.multicall({
contracts: [
{
...tokenContract,
functionName: 'allowance',
args: [context.account, spender],
},
{
...tokenContract,
functionName: 'name',
},
],
})
if (currentAllowance === undefined || !tokenName) {
throw new Error('Token allowance not available')
}

if (amount.lte(BigNumber.from(currentAllowance))) {
// amount is less than or equal to current allowance so no need to approve
return
}

const { chainId, account } = context

const eip2162Utils = getPermitUtilsInstance(chainId, web3Provider, account)

const [permitInfo, nonce] = await Promise.all([
getTokenPermitInfo({
spender,
tokenAddress,
chainId,
provider: jsonRpcProvider,
}),
eip2162Utils.getTokenNonce(tokenAddress, account),
]).catch(() => [undefined, undefined])

if (!permitInfo || !checkIsPermitInfo(permitInfo)) {
await handleTokenApprove({
signer,
spender,
tokenAddress,
amount: maxUint256,
})
return
}

const hook = await generatePermitHook({
chainId,
inputToken: {
address: tokenAddress,
name: tokenName as string,
},
spender,
provider: jsonRpcProvider,
permitInfo,
eip2162Utils: eip2162Utils,
account,
nonce,
})
if (!hook) throw new Error('User rejected permit')
return hook
},
[jsonRpcProvider, context, publicClient, spender, signer, web3Provider],
)
}

export function checkIsPermitInfo(permitInfo: GetTokenPermitIntoResult): permitInfo is PermitInfo {
return 'type' in permitInfo && permitInfo.type !== 'unsupported'
}
Loading
Loading