Skip to content

Commit

Permalink
feat: support EIP-5792 for twap orders
Browse files Browse the repository at this point in the history
  • Loading branch information
shoom3301 committed Dec 30, 2024
1 parent 7aabab1 commit cbf2d4c
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 28 deletions.
93 changes: 93 additions & 0 deletions apps/cowswap-frontend/src/common/hooks/useSendSafeTransactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { isTruthy } from '@cowprotocol/common-utils'
import { useSafeAppsSdk, useWalletCapabilities } from '@cowprotocol/wallet'
import { useWalletProvider } from '@cowprotocol/wallet-provider'
import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types'

import { useAppKitAccount, useAppKitNetwork } from '@reown/appkit/react'

type GetCallsResult = {
status: 'PENDING' | 'CONFIRMED'
receipts?: {
logs: {
address: `0x${string}`
data: `0x${string}`
topics: `0x${string}`[]
}[]
status: `0x${string}` // Hex 1 or 0 for success or failure, respectively
chainId: `0x${string}`
blockHash: `0x${string}`
blockNumber: `0x${string}`
gasUsed: `0x${string}`
transactionHash: `0x${string}`
}[]
}

export function useSendSafeTransactions() {
const safeAppsSdk = useSafeAppsSdk()
const provider = useWalletProvider()
const { address: account } = useAppKitAccount()
const { chainId } = useAppKitNetwork()
const capabilities = useWalletCapabilities()
const isAtomicBatchSupported = !!capabilities?.atomicBatch?.supported

return async function sendSafeTransaction(txs: MetaTransactionData[]): Promise<string> {
if (isAtomicBatchSupported && provider && account && chainId) {
const chainIdHex = '0x' + (+chainId).toString(16)

return provider
.send('wallet_sendCalls', [
{ version: '1.0', from: account, calls: txs.map((tx) => ({ ...tx, chainId: chainIdHex })) },
])
.then((batchId) => {
return new Promise((resolve, reject) => {
let intervalId: NodeJS.Timer | null = null
let triesCount = 0

// TODO: store batchId into localStorage and monitor it in background
function checkStatus() {
if (!provider) return undefined

return provider.send('wallet_getCallsStatus', [batchId]).then((response: GetCallsResult) => {
triesCount++

const safeTxHashes = response.receipts
?.map((r) => {
const log = r.logs.find((l) => {
// ExecutionSuccess topic
return l.topics[0] === '0x442e715f626346e8c54381002da614f62bee8d27386535b2521ec8540898556e'
})

return log ? log.data.slice(0, 66) : undefined
})
.filter(isTruthy)

const safeTxHash = safeTxHashes?.[0]

if (response.status === 'CONFIRMED' && safeTxHash) {
resolve(safeTxHash)
if (intervalId) clearInterval(intervalId)
}

if (triesCount > 30) {
if (intervalId) clearInterval(intervalId)
reject(new Error('Cannot get batch transaction result'))
}
})
}

intervalId = setInterval(checkStatus, 1000)

checkStatus()
})
})
}

if (safeAppsSdk) {
const tx = await safeAppsSdk.txs.send({ txs })

return tx.safeTxHash
} else {
throw new Error('Safe Apps SDK not available')
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useCallback } from 'react'

import { OrderKind } from '@cowprotocol/cow-sdk'
import { UiOrderType } from '@cowprotocol/types'
import { useSafeAppsSdk, useWalletInfo } from '@cowprotocol/wallet'
import { useWalletInfo } from '@cowprotocol/wallet'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'

import { Nullish } from 'types'
Expand All @@ -17,6 +17,7 @@ import { useTradeConfirmActions, useTradePriceImpact } from 'modules/trade'
import { TradeFlowAnalyticsContext, tradeFlowAnalytics } from 'modules/trade/utils/tradeFlowAnalytics'

import { useConfirmPriceImpactWithoutFee } from 'common/hooks/useConfirmPriceImpactWithoutFee'
import { useSendSafeTransactions } from 'common/hooks/useSendSafeTransactions'

import { useExtensibleFallbackContext } from './useExtensibleFallbackContext'
import { useTwapOrderCreationContext } from './useTwapOrderCreationContext'
Expand All @@ -40,7 +41,7 @@ export function useCreateTwapOrder() {
const { inputCurrencyAmount, outputCurrencyAmount } = useAdvancedOrdersDerivedState()

const appDataInfo = useAppData()
const safeAppsSdk = useSafeAppsSdk()
const sendSafeTransactions = useSendSafeTransactions()
const twapOrderCreationContext = useTwapOrderCreationContext(inputCurrencyAmount as Nullish<CurrencyAmount<Token>>)
const extensibleFallbackContext = useExtensibleFallbackContext()

Expand All @@ -60,7 +61,6 @@ export function useCreateTwapOrder() {
!outputCurrencyAmount ||
!twapOrderCreationContext ||
!extensibleFallbackContext ||
!safeAppsSdk ||
!appDataInfo ||
!twapOrder
)
Expand Down Expand Up @@ -101,7 +101,7 @@ export function useCreateTwapOrder() {
// upload the app data here, as application might need it to decode the order info before it is being signed
uploadAppData({ chainId, orderId, appData: appDataInfo })
const createOrderTxs = createTwapOrderTxs(twapOrder, paramsStruct, twapOrderCreationContext)
const { safeTxHash } = await safeAppsSdk.txs.send({ txs: [...fallbackSetupTxs, ...createOrderTxs] })
const safeTxHash = await sendSafeTransactions([...fallbackSetupTxs, ...createOrderTxs])

const orderItem: TwapOrderItem = {
order: twapOrderToStruct(twapOrder),
Expand Down Expand Up @@ -150,7 +150,7 @@ export function useCreateTwapOrder() {
outputCurrencyAmount,
twapOrderCreationContext,
extensibleFallbackContext,
safeAppsSdk,
sendSafeTransactions,
appDataInfo,
twapOrder,
confirmPriceImpactWithoutFee,
Expand All @@ -159,6 +159,6 @@ export function useCreateTwapOrder() {
addTwapOrderToList,
uploadAppData,
updateAdvancedOrdersState,
]
],
)
}
28 changes: 15 additions & 13 deletions apps/cowswap-frontend/src/modules/twap/hooks/useTwapFormState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useAtomValue } from 'jotai'
import { useMemo } from 'react'

import { useIsSafeApp, useWalletInfo } from '@cowprotocol/wallet'
import { useIsSafeApp, useIsWalletConnect, useWalletCapabilities, useWalletInfo } from '@cowprotocol/wallet'

import { useReceiveAmountInfo } from 'modules/trade'
import { useUsdAmount } from 'modules/usdAmount'
Expand All @@ -23,15 +22,18 @@ export function useTwapFormState(): TwapFormState | null {

const verification = useFallbackHandlerVerification()
const isSafeApp = useIsSafeApp()

return useMemo(() => {
return getTwapFormState({
isSafeApp,
verification,
twapOrder,
sellAmountPartFiat,
chainId,
partTime,
})
}, [isSafeApp, verification, twapOrder, sellAmountPartFiat, chainId, partTime])
const isWalletConnect = useIsWalletConnect()
const walletCapabilities = useWalletCapabilities()

// TODO: fix the condition in order to check whether is it a Safe via WC
const isSafeWithBundlingTx = isSafeApp || Boolean(isWalletConnect && walletCapabilities?.atomicBatch?.supported)

return getTwapFormState({
isSafeWithBundlingTx,
verification,
twapOrder,
sellAmountPartFiat,
chainId,
partTime,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('getTwapFormState()', () => {
describe('When sell fiat amount is under threshold', () => {
it('And order has buy amount, then should return SELL_AMOUNT_TOO_SMALL', () => {
const result = getTwapFormState({
isSafeApp: true,
isSafeWithBundlingTx: true,
verification: ExtensibleFallbackVerification.HAS_DOMAIN_VERIFIER,
twapOrder: { ...twapOrder },
sellAmountPartFiat: CurrencyAmount.fromRawAmount(WETH_SEPOLIA, 10000000),
Expand All @@ -37,7 +37,7 @@ describe('getTwapFormState()', () => {

it('And order does NOT have buy amount, then should return null', () => {
const result = getTwapFormState({
isSafeApp: true,
isSafeWithBundlingTx: true,
verification: ExtensibleFallbackVerification.HAS_DOMAIN_VERIFIER,
twapOrder: { ...twapOrder, buyAmount: CurrencyAmount.fromRawAmount(COW_SEPOLIA, 0) },
sellAmountPartFiat: CurrencyAmount.fromRawAmount(WETH_SEPOLIA, 10000000),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { isPartTimeIntervalTooShort } from '../../utils/isPartTimeIntervalTooSho
import { isSellAmountTooSmall } from '../../utils/isSellAmountTooSmall'

export interface TwapFormStateParams {
isSafeApp: boolean
isSafeWithBundlingTx: boolean
verification: ExtensibleFallbackVerification | null
twapOrder: TWAPOrder | null
sellAmountPartFiat: Nullish<CurrencyAmount<Currency>>
Expand All @@ -28,9 +28,9 @@ export enum TwapFormState {
}

export function getTwapFormState(props: TwapFormStateParams): TwapFormState | null {
const { twapOrder, isSafeApp, verification, sellAmountPartFiat, chainId, partTime } = props
const { twapOrder, isSafeWithBundlingTx, verification, sellAmountPartFiat, chainId, partTime } = props

if (!isSafeApp) return TwapFormState.NOT_SAFE
if (!isSafeWithBundlingTx) return TwapFormState.NOT_SAFE

if (verification === null) return TwapFormState.LOADING_SAFE_INFO

Expand Down
6 changes: 3 additions & 3 deletions apps/cowswap-frontend/src/modules/twap/updaters/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { percentToBps } from '@cowprotocol/common-utils'
import { useIsSafeApp, useWalletInfo } from '@cowprotocol/wallet'
import { useIsSafeWallet, useWalletInfo } from '@cowprotocol/wallet'

import { useComposableCowContract } from 'modules/advancedOrders/hooks/useComposableCowContract'
import { AppDataUpdater } from 'modules/appData'
Expand All @@ -16,11 +16,11 @@ import { useTwapSlippage } from '../hooks/useTwapSlippage'

export function TwapUpdaters() {
const { chainId, account } = useWalletInfo()
const isSafeApp = useIsSafeApp()
const isSafeWallet = useIsSafeWallet()
const composableCowContract = useComposableCowContract()
const twapOrderSlippage = useTwapSlippage()

const shouldLoadTwapOrders = !!(isSafeApp && chainId && account && composableCowContract)
const shouldLoadTwapOrders = !!(isSafeWallet && chainId && account && composableCowContract)

return (
<>
Expand Down
5 changes: 4 additions & 1 deletion libs/wallet/src/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useAtomValue } from 'jotai'

import { useWalletInfo as useReOwnWalletInfo } from '@reown/appkit/react'

import { useWalletCapabilities } from './hooks/useWalletCapabilities'
import { gnosisSafeInfoAtom, walletDetailsAtom, walletDisplayedAddress, walletInfoAtom } from './state'
import { GnosisSafeInfo, WalletDetails, WalletInfo } from './types'

Expand All @@ -25,10 +26,12 @@ export function useGnosisSafeInfo(): GnosisSafeInfo | undefined {
}

export function useIsBundlingSupported(): boolean {
const capabilities = useWalletCapabilities()

// For now, bundling can only be performed while the App is loaded as a Safe App
// Pending a custom RPC endpoint implementation on Safe side to allow
// tx bundling via WalletConnect
return useIsSafeApp()
return useIsSafeApp() || !!capabilities?.atomicBatch?.supported
}

export function useIsAssetWatchingSupported(): boolean {
Expand Down
29 changes: 29 additions & 0 deletions libs/wallet/src/api/hooks/useWalletCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { SWR_NO_REFRESH_OPTIONS } from '@cowprotocol/common-const'
import { useWalletProvider } from '@cowprotocol/wallet-provider'

import { useAppKitAccount, useAppKitNetwork } from '@reown/appkit/react'
import useSWR from 'swr'

export type WalletCapabilities = {
atomicBatch?: { supported: boolean }
}

export function useWalletCapabilities(): WalletCapabilities | undefined {
const provider = useWalletProvider()
const { address: account } = useAppKitAccount()
const { chainId } = useAppKitNetwork()

return useSWR(
provider && account && chainId ? [provider, account, chainId] : null,
([provider, account, chainId]) => {
return provider
.send('wallet_getCapabilities', [account])
.then((result: { [chainIdHex: string]: WalletCapabilities }) => {
const chainIdHex = '0x' + (+chainId).toString(16)

return result[chainIdHex]
})
},
SWR_NO_REFRESH_OPTIONS,
).data
}
1 change: 1 addition & 0 deletions libs/wallet/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './assets'
// Hooks
export * from './api/hooks'
export { useOpenWalletConnectionModal } from './api/hooks/useOpenWalletConnectionModal'
export { useWalletCapabilities } from './api/hooks/useWalletCapabilities'
export * from './reown/hooks/useWalletMetadata'
export * from './reown/hooks/useIsWalletConnect'
export * from './reown/hooks/useSafeAppsSdk'
Expand Down

0 comments on commit cbf2d4c

Please sign in to comment.