Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support EIP-5792 for twap orders #5261

Draft
wants to merge 2 commits into
base: feat/reown-app-kit
Choose a base branch
from
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
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
Loading
Loading