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

[2] feat(twap): generate all part orders #2759

Merged
merged 11 commits into from
Jul 5, 2023
Merged
6 changes: 0 additions & 6 deletions src/api/gnosisProtocol/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CurrencyAmount<Currency>>
isLoading: boolean
Expand Down
2 changes: 1 addition & 1 deletion src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type OrderWithComposableCowInfo = {

export type SafeTransactionParams = {
submissionDate: string
executionDate: string
executionDate: string | null
isExecuted: boolean
nonce: number
confirmationsRequired: number
Expand Down
2 changes: 2 additions & 0 deletions src/modules/twap/containers/TwapFormWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -82,6 +83,7 @@ export function TwapFormWidget() {
<>
<QuoteObserverUpdater />
<FallbackHandlerVerificationUpdater />
<PartOrdersUpdater />
{shouldLoadTwapOrders && (
<TwapOrdersUpdater composableCowContract={composableCowContract} safeAddress={account} chainId={chainId} />
)}
Expand Down
84 changes: 0 additions & 84 deletions src/modules/twap/hooks/useFetchTwapPartOrders.ts

This file was deleted.

22 changes: 6 additions & 16 deletions src/modules/twap/state/twapPartOrdersAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TwapPartOrders>('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<TwapPartOrders>('twap-part-orders-list:v2', {})

export const twapPartOrdersListAtom = atom<TwapPartOrderItem[]>((get) => {
const { account, chainId } = get(walletInfoAtom)
Expand All @@ -34,7 +24,7 @@ export const twapPartOrdersListAtom = atom<TwapPartOrderItem[]>((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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is order.safeAddress always lowercase?

})
120 changes: 120 additions & 0 deletions src/modules/twap/updaters/PartOrdersUpdater.tsx
Original file line number Diff line number Diff line change
@@ -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) => {
alfetopito marked this conversation as resolved.
Show resolved Hide resolved
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)
alfetopito marked this conversation as resolved.
Show resolved Hide resolved

const ids = await Promise.all(parts.map((part) => computeOrderUid(chainId, safeAddress, part as Order)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need to cast the part into an order?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately yes. Because computeOrderUid is based on @cowprotocol/contracts but, in the rest of CowSwap we mostly use types from @cowprotocol/cow-sdk.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately yes. Because computeOrderUid is based on @cowprotocol/contracts but, in the rest of CowSwap we mostly use types from @cowprotocol/cow-sdk.


return {
[twapOrderId]: ids.map<TwapPartOrderItem>((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
}
14 changes: 2 additions & 12 deletions src/modules/twap/updaters/TwapOrdersUpdater.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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(() => {
Expand All @@ -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
Expand All @@ -65,23 +63,15 @@ 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)

useEffect(() => {
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
}
4 changes: 2 additions & 2 deletions src/modules/twap/utils/buildTwapOrdersItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
15 changes: 10 additions & 5 deletions src/modules/twap/utils/getTwapOrderStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

Expand All @@ -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()

Expand Down