From 7901e1ac0fa6df6410be78382949675ac94be8a5 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 5 Jul 2023 13:13:12 +0600 Subject: [PATCH] feat(twap): generate all part orders (#2759) --- src/api/gnosisProtocol/hooks.ts | 6 - src/common/types.ts | 2 +- .../twap/containers/TwapFormWidget/index.tsx | 2 + .../twap/hooks/useFetchTwapPartOrders.ts | 84 ------------ src/modules/twap/state/twapPartOrdersAtom.ts | 22 +--- .../twap/updaters/PartOrdersUpdater.tsx | 120 ++++++++++++++++++ .../twap/updaters/TwapOrdersUpdater.tsx | 14 +- .../twap/utils/buildTwapOrdersItems.ts | 4 +- src/modules/twap/utils/getTwapOrderStatus.ts | 15 ++- 9 files changed, 143 insertions(+), 126 deletions(-) delete mode 100644 src/modules/twap/hooks/useFetchTwapPartOrders.ts create mode 100644 src/modules/twap/updaters/PartOrdersUpdater.tsx diff --git a/src/api/gnosisProtocol/hooks.ts b/src/api/gnosisProtocol/hooks.ts index 7dc8b6602a..43d2c4ff14 100644 --- a/src/api/gnosisProtocol/hooks.ts +++ b/src/api/gnosisProtocol/hooks.ts @@ -115,12 +115,6 @@ function useTwapChildOrders(prodOrders: EnrichedOrder[] | undefined): OrderWithC }, [twapParticleOrders, prodOrders]) } -export function useHasOrders(account?: string | null): boolean | undefined { - const gpOrders = useGpOrders(account) - - return (gpOrders?.length || 0) > 0 -} - export type UseSurplusAmountResult = { surplusAmount: Nullish> isLoading: boolean diff --git a/src/common/types.ts b/src/common/types.ts index 79b41ccd55..e6569d3086 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -19,7 +19,7 @@ export type OrderWithComposableCowInfo = { export type SafeTransactionParams = { submissionDate: string - executionDate: string + executionDate: string | null isExecuted: boolean nonce: number confirmationsRequired: number diff --git a/src/modules/twap/containers/TwapFormWidget/index.tsx b/src/modules/twap/containers/TwapFormWidget/index.tsx index cf4d79cd6e..9e1286c8c3 100644 --- a/src/modules/twap/containers/TwapFormWidget/index.tsx +++ b/src/modules/twap/containers/TwapFormWidget/index.tsx @@ -28,6 +28,7 @@ import { partsStateAtom } from '../../state/partsStateAtom' import { twapTimeIntervalAtom } from '../../state/twapOrderAtom' import { twapOrdersSettingsAtom, updateTwapOrdersSettingsAtom } from '../../state/twapOrdersSettingsAtom' import { FallbackHandlerVerificationUpdater } from '../../updaters/FallbackHandlerVerificationUpdater' +import { PartOrdersUpdater } from '../../updaters/PartOrdersUpdater' import { TwapOrdersUpdater } from '../../updaters/TwapOrdersUpdater' import { deadlinePartsDisplay } from '../../utils/deadlinePartsDisplay' import { ActionButtons } from '../ActionButtons' @@ -82,6 +83,7 @@ export function TwapFormWidget() { <> + {shouldLoadTwapOrders && ( )} diff --git a/src/modules/twap/hooks/useFetchTwapPartOrders.ts b/src/modules/twap/hooks/useFetchTwapPartOrders.ts deleted file mode 100644 index 0ab8e6167b..0000000000 --- a/src/modules/twap/hooks/useFetchTwapPartOrders.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { useMemo } from 'react' - -import { ComposableCoW } from '@cowprotocol/abis' -import type { Order } from '@cowprotocol/contracts' -import { OrderParameters, SupportedChainId } from '@cowprotocol/cow-sdk' - -import { useAsyncMemo } from 'use-async-memo' - -import { computeOrderUid } from 'utils/orderUtils/computeOrderUid' - -import { TradeableOrderWithSignature, useTwapOrdersTradeableMulticall } from './useTwapOrdersTradeableMulticall' - -import { TwapPartOrderItem, TwapPartOrders } from '../state/twapPartOrdersAtom' -import { TwapOrderInfo } from '../types' - -export function useFetchTwapPartOrders( - safeAddress: string, - chainId: SupportedChainId, - composableCowContract: ComposableCoW, - ordersInfo: TwapOrderInfo[] -): TwapPartOrders | null { - const ordersToVerifyParams = useMemo(() => { - return ordersInfo.map((info) => info.safeData.conditionalOrderParams) - }, [ordersInfo]) - - const ordersTradeableData = useTwapOrdersTradeableMulticall(safeAddress, composableCowContract, ordersToVerifyParams) - - const items = useAsyncMemo( - () => { - if (ordersInfo.length !== ordersTradeableData.length) return null - - const safeAddressLowerCase = safeAddress.toLowerCase() - - return Promise.all( - ordersInfo.map(({ id }, index) => { - const data = ordersTradeableData[index] - return data ? getTwapPartOrderItem(chainId, safeAddressLowerCase, data, id) : Promise.resolve(null) - }) - ) - }, - [chainId, safeAddress, ordersInfo, ordersTradeableData], - null - ) - - return useMemo(() => { - if (!items) return null - - return ordersInfo.reduce((acc, { id }, index) => { - const item = items[index] - - if (item) acc[id] = item - - return acc - }, {} as TwapPartOrders) - }, [ordersInfo, items]) -} - -async function getTwapPartOrderItem( - chainId: SupportedChainId, - safeAddress: string, - data: TradeableOrderWithSignature, - twapOrderId: string -): Promise { - if (!data) return null - - const { order: partOrder, signature } = data - const { sellToken, buyToken, receiver, validTo, appData } = partOrder - const fixedOrder = { - sellToken, - buyToken, - receiver, - validTo, - appData, - sellAmount: partOrder.sellAmount.toString(), - buyAmount: partOrder.buyAmount.toString(), - feeAmount: partOrder.feeAmount.toString(), - kind: 'sell', // Twap order is always sell - partiallyFillable: partOrder.partiallyFillable, - } as Order - - const uid = await computeOrderUid(chainId, safeAddress, fixedOrder) - - return { uid, chainId, safeAddress, twapOrderId, order: fixedOrder as OrderParameters, signature } -} diff --git a/src/modules/twap/state/twapPartOrdersAtom.ts b/src/modules/twap/state/twapPartOrdersAtom.ts index bba7770079..5b5579428e 100644 --- a/src/modules/twap/state/twapPartOrdersAtom.ts +++ b/src/modules/twap/state/twapPartOrdersAtom.ts @@ -3,29 +3,19 @@ import { atomWithStorage } from 'jotai/utils' import { OrderParameters, SupportedChainId } from '@cowprotocol/cow-sdk' -import deepEqual from 'fast-deep-equal' - import { walletInfoAtom } from 'modules/wallet/api/state' export interface TwapPartOrderItem { uid: string + index: number chainId: SupportedChainId safeAddress: string twapOrderId: string order: OrderParameters - signature: string } -export type TwapPartOrders = { [twapOrderHash: string]: TwapPartOrderItem } - -export const twapPartOrdersAtom = atomWithStorage('twap-part-orders-list:v1', {}) +export type TwapPartOrders = { [twapOrderHash: string]: TwapPartOrderItem[] } -export const updateTwapPartOrdersAtom = atom(null, (get, set, nextState: TwapPartOrders) => { - const currentState = get(twapPartOrdersAtom) - - if (!deepEqual(currentState, nextState)) { - set(twapPartOrdersAtom, nextState) - } -}) +export const twapPartOrdersAtom = atomWithStorage('twap-part-orders-list:v2', {}) export const twapPartOrdersListAtom = atom((get) => { const { account, chainId } = get(walletInfoAtom) @@ -34,7 +24,7 @@ export const twapPartOrdersListAtom = atom((get) => { const accountLowerCase = account.toLowerCase() - return Object.values(get(twapPartOrdersAtom)).filter( - (order) => order.safeAddress === accountLowerCase && order.chainId === chainId - ) + const orders = Object.values(get(twapPartOrdersAtom)) + + return orders.flat().filter((order) => order.safeAddress === accountLowerCase && order.chainId === chainId) }) diff --git a/src/modules/twap/updaters/PartOrdersUpdater.tsx b/src/modules/twap/updaters/PartOrdersUpdater.tsx new file mode 100644 index 0000000000..b460e4051f --- /dev/null +++ b/src/modules/twap/updaters/PartOrdersUpdater.tsx @@ -0,0 +1,120 @@ +import { useAtomValue, useUpdateAtom } from 'jotai/utils' +import { useEffect } from 'react' + +import { Order } from '@cowprotocol/contracts' +import { OrderParameters, SupportedChainId } from '@cowprotocol/cow-sdk' + +import { isTruthy } from 'legacy/utils/misc' + +import { useWalletInfo } from 'modules/wallet' + +import { computeOrderUid } from 'utils/orderUtils/computeOrderUid' + +import { twapOrdersListAtom } from '../state/twapOrdersListAtom' +import { TwapPartOrderItem, twapPartOrdersAtom } from '../state/twapPartOrdersAtom' +import { TwapOrderItem } from '../types' + +export function PartOrdersUpdater() { + const { chainId, account } = useWalletInfo() + const twapOrdersList = useAtomValue(twapOrdersListAtom) + const updateTwapPartOrders = useUpdateAtom(twapPartOrdersAtom) + + useEffect(() => { + if (!chainId || !account) return + + const accountLowerCase = account.toLowerCase() + const twapOrders = Object.values(twapOrdersList) + + const ordersParts$ = twapOrders.map((twapOrder) => { + return generateTwapOrderParts(twapOrder, accountLowerCase, chainId) + }) + + Promise.all(ordersParts$).then((ordersParts) => { + const ordersMap = ordersParts.reduce((acc, item) => { + return { + ...acc, + ...item, + } + }, {}) + + updateTwapPartOrders(ordersMap) + }) + }, [chainId, account, twapOrdersList, updateTwapPartOrders]) + + return null +} + +async function generateTwapOrderParts( + twapOrder: TwapOrderItem, + safeAddress: string, + chainId: SupportedChainId +): Promise<{ [id: string]: TwapPartOrderItem[] }> { + const twapOrderId = twapOrder.id + + const parts = [...new Array(twapOrder.order.n)] + .map((_, index) => createPartOrderFromParent(twapOrder, index)) + .filter(isTruthy) + + const ids = await Promise.all(parts.map((part) => computeOrderUid(chainId, safeAddress, part as Order))) + + return { + [twapOrderId]: ids.map((uid, index) => { + return { + uid, + index, + twapOrderId, + chainId, + safeAddress, + order: parts[index], + } + }), + } +} + +function createPartOrderFromParent(twapOrder: TwapOrderItem, index: number): OrderParameters | null { + const executionDate = twapOrder.safeTxParams?.executionDate + + if (!executionDate) { + return null + } + + const blockTimestamp = new Date(executionDate) + + return { + sellToken: twapOrder.order.sellToken, + buyToken: twapOrder.order.buyToken, + receiver: twapOrder.order.receiver, + sellAmount: twapOrder.order.partSellAmount, + buyAmount: twapOrder.order.minPartLimit, + validTo: calculateValidTo({ + part: index, + startTime: Math.ceil(blockTimestamp.getTime() / 1000), + span: twapOrder.order.span, + frequency: twapOrder.order.t, + }), + appData: twapOrder.order.appData, + feeAmount: '0', + kind: 'sell', + partiallyFillable: false, + sellTokenBalance: 'erc20', + buyTokenBalance: 'erc20', + } as OrderParameters +} + +function calculateValidTo({ + part, + startTime, + frequency, + span, +}: { + part: number + startTime: number + frequency: number + span: number +}): number { + if (span === 0) { + return startTime + (part + 1) * frequency - 1 + } + + return startTime + part * frequency + span - 1 +} diff --git a/src/modules/twap/updaters/TwapOrdersUpdater.tsx b/src/modules/twap/updaters/TwapOrdersUpdater.tsx index 2ec086996a..dc98105b8d 100644 --- a/src/modules/twap/updaters/TwapOrdersUpdater.tsx +++ b/src/modules/twap/updaters/TwapOrdersUpdater.tsx @@ -8,11 +8,9 @@ import { isTruthy } from 'legacy/utils/misc' import { TWAP_PENDING_STATUSES } from '../const' import { useFetchTwapOrdersFromSafe } from '../hooks/useFetchTwapOrdersFromSafe' -import { useFetchTwapPartOrders } from '../hooks/useFetchTwapPartOrders' import { useTwapDiscreteOrders } from '../hooks/useTwapDiscreteOrders' import { useTwapOrdersAuthMulticall } from '../hooks/useTwapOrdersAuthMulticall' import { twapOrdersListAtom, updateTwapOrdersListAtom } from '../state/twapOrdersListAtom' -import { updateTwapPartOrdersAtom } from '../state/twapPartOrdersAtom' import { TwapOrderInfo } from '../types' import { buildTwapOrdersItems } from '../utils/buildTwapOrdersItems' import { getConditionalOrderId } from '../utils/getConditionalOrderId' @@ -29,7 +27,6 @@ export function TwapOrdersUpdater(props: { const twapDiscreteOrders = useTwapDiscreteOrders() const twapOrdersList = useAtomValue(twapOrdersListAtom) const updateTwapOrders = useUpdateAtom(updateTwapOrdersListAtom) - const updateTwapPartOrders = useUpdateAtom(updateTwapPartOrdersAtom) const ordersSafeData = useFetchTwapOrdersFromSafe(props) const allOrdersInfo: TwapOrderInfo[] = useMemo(() => { @@ -38,12 +35,13 @@ export function TwapOrdersUpdater(props: { try { const id = getConditionalOrderId(data.conditionalOrderParams) const order = parseTwapOrderStruct(data.conditionalOrderParams.staticInput) + const { executionDate } = data.safeTxParams return { id, orderStruct: order, safeData: data, - isExpired: isTwapOrderExpired(order), + isExpired: isTwapOrderExpired(order, executionDate ? new Date(executionDate) : null), } } catch (e) { return null @@ -65,7 +63,6 @@ export function TwapOrdersUpdater(props: { }) }, [allOrdersInfo, twapOrdersList]) - const partOrders = useFetchTwapPartOrders(safeAddress, chainId, composableCowContract, pendingOrCancelledOrders) // Here we know which orders are cancelled: if it's auth === false, then it's cancelled const ordersAuthResult = useTwapOrdersAuthMulticall(safeAddress, composableCowContract, pendingOrCancelledOrders) @@ -73,15 +70,8 @@ export function TwapOrdersUpdater(props: { if (!ordersAuthResult || !twapDiscreteOrders) return const items = buildTwapOrdersItems(chainId, safeAddress, allOrdersInfo, ordersAuthResult, twapDiscreteOrders) - updateTwapOrders(items) }, [chainId, safeAddress, allOrdersInfo, ordersAuthResult, twapDiscreteOrders, updateTwapOrders]) - useEffect(() => { - if (!partOrders) return - - updateTwapPartOrders(partOrders) - }, [partOrders, updateTwapPartOrders]) - return null } diff --git a/src/modules/twap/utils/buildTwapOrdersItems.ts b/src/modules/twap/utils/buildTwapOrdersItems.ts index 6788be02c7..1a7bc39566 100644 --- a/src/modules/twap/utils/buildTwapOrdersItems.ts +++ b/src/modules/twap/utils/buildTwapOrdersItems.ts @@ -31,9 +31,9 @@ function getTwapOrderItem( discreteOrder: Order | undefined ): TwapOrderItem { const { conditionalOrderParams, safeTxParams } = safeData - const { isExecuted, submissionDate } = safeTxParams + const { isExecuted, submissionDate, executionDate: _executionDate } = safeTxParams - const executionDate = new Date(safeTxParams.executionDate) + const executionDate = _executionDate ? new Date(_executionDate) : null const order = parseTwapOrderStruct(conditionalOrderParams.staticInput) const status = getTwapOrderStatus(order, isExecuted, executionDate, authorized, discreteOrder) diff --git a/src/modules/twap/utils/getTwapOrderStatus.ts b/src/modules/twap/utils/getTwapOrderStatus.ts index 4590514e0e..4c6a4b945a 100644 --- a/src/modules/twap/utils/getTwapOrderStatus.ts +++ b/src/modules/twap/utils/getTwapOrderStatus.ts @@ -9,11 +9,11 @@ const AUTH_THRESHOLD = ms`1m` export function getTwapOrderStatus( order: TWAPOrderStruct, isExecuted: boolean, - executionDate: Date, + executionDate: Date | null, auth: boolean | undefined, discreteOrder: Order | undefined ): TwapOrderStatus { - if (isTwapOrderExpired(order)) return TwapOrderStatus.Expired + if (isTwapOrderExpired(order, executionDate)) return TwapOrderStatus.Expired if (!isExecuted) return TwapOrderStatus.WaitSigning @@ -24,8 +24,11 @@ export function getTwapOrderStatus( return TwapOrderStatus.Scheduled } -export function isTwapOrderExpired(order: TWAPOrderStruct): boolean { - const { t0: startTime, n: numOfParts, t: timeInterval } = order +export function isTwapOrderExpired(order: TWAPOrderStruct, startDate: Date | null): boolean { + if (!startDate) return false + + const startTime = Math.ceil(startDate.getTime() / 1000) + const { n: numOfParts, t: timeInterval } = order const endTime = startTime + timeInterval * numOfParts const nowTimestamp = Math.ceil(Date.now() / 1000) @@ -36,7 +39,9 @@ export function isTwapOrderExpired(order: TWAPOrderStruct): boolean { * ComposableCow.singleOrders returns false by default * To avoid false-positive values, we should not check authorized flag within first minute after execution time */ -function shouldCheckAuth(executionDate: Date): boolean { +function shouldCheckAuth(executionDate: Date | null): boolean { + if (!executionDate) return false + const executionTimestamp = executionDate.getTime() const nowTimestamp = Date.now()