Skip to content

Commit

Permalink
feat(twap): calculate and display TWAP orders execution info (#2764)
Browse files Browse the repository at this point in the history
  • Loading branch information
shoom3301 authored Jul 6, 2023
1 parent a1d121e commit a13101c
Show file tree
Hide file tree
Showing 19 changed files with 255 additions and 103 deletions.
2 changes: 1 addition & 1 deletion src/api/gnosisProtocol/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { isTruthy } from 'legacy/utils/misc'
import { supportedChainId } from 'legacy/utils/supportedChainId'

import { emulatedPartOrdersAtom } from 'modules/twap/state/emulatedPartOrdersAtom'
import { emulatedTwapOrdersAtom } from 'modules/twap/state/twapOrdersListAtom'
import { emulatedTwapOrdersAtom } from 'modules/twap/state/emulatedTwapOrdersAtom'
import { TwapPartOrderItem, twapPartOrdersListAtom } from 'modules/twap/state/twapPartOrdersAtom'
import { useWalletInfo } from 'modules/wallet'

Expand Down
14 changes: 7 additions & 7 deletions src/modules/ordersTable/pure/OrderStatusBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ const Wrapper = styled.div<{
--statusColor: ${({ theme, status, cancelling, partiallyFilled }) =>
status === OrderStatus.CANCELLED
? theme.danger
: cancelling
? theme.text1
: status === OrderStatus.FULFILLED || partiallyFilled
? theme.success
: cancelling
? theme.text1
: status === OrderStatus.PENDING // OPEN order
? theme.text3
: status === OrderStatus.EXPIRED
Expand Down Expand Up @@ -66,17 +66,17 @@ function getOrderStatusTitle(order: ParsedOrder): string {
return orderStatusTitleMap[order.status]
}

// Cancelling is not a real order status
if (order.isCancelling) {
return 'Cancelling...'
}

// We consider the order fully filled for display purposes even if not 100% filled
// For this reason we use the flag to override the order status
if (order.executionData.fullyFilled) {
return orderStatusTitleMap[OrderStatus.FULFILLED]
}

// Cancelling is not a real order status
if (order.isCancelling) {
return 'Cancelling...'
}

// Partially filled is also not a real status
if (order.executionData.partiallyFilled) {
return 'Partially Filled'
Expand Down
8 changes: 7 additions & 1 deletion src/modules/twap/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ms from 'ms.macro'

import { USDC } from 'legacy/constants/tokens'

import { TwapOrderStatus } from './types'
import { TwapOrderExecutionInfo, TwapOrderStatus } from './types'

export const DEFAULT_TWAP_SLIPPAGE = new Percent(10, 100) // 10%

Expand Down Expand Up @@ -44,3 +44,9 @@ export const MINIMUM_PART_SELL_AMOUNT_FIAT: Record<SupportedChainId, CurrencyAmo
}

export const MINIMUM_PART_TIME = ms`5min` / 1000 // in seconds

export const DEFAULT_TWAP_EXECUTION_INFO: TwapOrderExecutionInfo = {
executedSellAmount: '0',
executedBuyAmount: '0',
executedFeeAmount: '0',
}
2 changes: 2 additions & 0 deletions src/modules/twap/hooks/useCreateTwapOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useExtensibleFallbackContext } from './useExtensibleFallbackContext'
import { useTwapOrderCreationContext } from './useTwapOrderCreationContext'

import { useSafeAppsSdk } from '../../wallet/web3-react/hooks/useSafeAppsSdk'
import { DEFAULT_TWAP_EXECUTION_INFO } from '../const'
import { createTwapOrderTxs } from '../services/createTwapOrderTxs'
import { extensibleFallbackSetupTxs } from '../services/extensibleFallbackSetupTxs'
import { twapOrderAtom } from '../state/twapOrderAtom'
Expand Down Expand Up @@ -99,6 +100,7 @@ export function useCreateTwapOrder() {
safeAddress: account,
submissionDate: new Date().toISOString(),
id: orderId,
executionInfo: DEFAULT_TWAP_EXECUTION_INFO,
})

uploadAppData({ chainId, orderId, appData: appDataInfo })
Expand Down
4 changes: 2 additions & 2 deletions src/modules/twap/hooks/useTwapOrderById.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useAtomValue } from 'jotai/utils'
import { useMemo } from 'react'

import { twapOrdersListAtom } from '../state/twapOrdersListAtom'
import { twapOrdersAtom } from '../state/twapOrdersListAtom'
import { TwapOrderItem } from '../types'

export function useTwapOrderById(orderId: string | undefined): TwapOrderItem | null {
const twapOrdersList = useAtomValue(twapOrdersListAtom)
const twapOrdersList = useAtomValue(twapOrdersAtom)

return useMemo(() => {
return (orderId && twapOrdersList[orderId]) || null
Expand Down
73 changes: 73 additions & 0 deletions src/modules/twap/hooks/useTwapOrdersExecutions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useAtomValue } from 'jotai'
import { useMemo } from 'react'

import { Order, OrderInfoApi } from 'legacy/state/orders/actions'
import { useOrdersById } from 'legacy/state/orders/hooks'

import { useWalletInfo } from 'modules/wallet'

import { DEFAULT_TWAP_EXECUTION_INFO } from '../const'
import { twapPartOrdersAtom } from '../state/twapPartOrdersAtom'
import { TwapOrderExecutionInfo } from '../types'

export type TwapOrdersExecutionMap = { [id: string]: TwapOrderExecutionInfo }

type PartsIdsInfo = {
sets: { [id: string]: Set<string> }
ids: string[]
}

export function useTwapOrdersExecutions(ids: string[]): TwapOrdersExecutionMap {
const { chainId } = useWalletInfo()
const twapPartOrders = useAtomValue(twapPartOrdersAtom)

const { sets: partSets, ids: allPartIds } = useMemo(() => {
return Object.keys(twapPartOrders).reduce<PartsIdsInfo>(
(acc, val) => {
const { sets, ids } = acc

const partsIds = (twapPartOrders[val] || []).map((item) => item.uid)

sets[val] = new Set<string>(partsIds)
ids.push(...partsIds)

return acc
},
{ sets: {}, ids: [] }
)
}, [twapPartOrders])

const allPartOrdersMap = useOrdersById({ chainId, ids: allPartIds })

const allPartOrders = useMemo(() => {
if (!allPartOrdersMap) return []

return Object.values(allPartOrdersMap)
}, [allPartOrdersMap])

return useMemo(() => {
return ids.reduce<TwapOrdersExecutionMap>((acc, id) => {
const childrenIds = partSets[id]

if (childrenIds?.size > 0) {
const children = allPartOrders.filter((order) => childrenIds.has(order.id))

const executedBuyAmount = sumChildrenAmount(children, 'executedBuyAmount').toString()
const executedSellAmount = sumChildrenAmount(children, 'executedSellAmount').toString()
const executedFeeAmount = sumChildrenAmount(children, 'executedFeeAmount').toString()

acc[id] = { executedSellAmount, executedFeeAmount, executedBuyAmount }
} else {
acc[id] = DEFAULT_TWAP_EXECUTION_INFO
}

return acc
}, {})
}, [ids, partSets, allPartOrders])
}

function sumChildrenAmount(children: Order[], key: keyof OrderInfoApi): BigInt {
return children.reduce((acc, order) => {
return acc + BigInt((order.apiAdditionalInfo?.[key] || '0') as string)
}, BigInt(0))
}
8 changes: 7 additions & 1 deletion src/modules/twap/hooks/useTwapWarningsContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { useMemo } from 'react'
import { useTradePriceImpact } from 'modules/trade'
import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFormValidation'

const NOT_BLOCKING_VALIDATIONS = [
TradeFormValidation.ExpertApproveAndSwap,
TradeFormValidation.ApproveAndSwap,
TradeFormValidation.ApproveRequired,
]

export interface TwapWarningsContext {
canTrade: boolean
showPriceImpactWarning: boolean
Expand All @@ -16,7 +22,7 @@ export function useTwapWarningsContext(): TwapWarningsContext {
return useMemo(() => {
// TODO: bind to settings
const expertMode = false
const canTrade = !primaryFormValidation || primaryFormValidation >= TradeFormValidation.ExpertApproveAndSwap
const canTrade = !primaryFormValidation || NOT_BLOCKING_VALIDATIONS.includes(primaryFormValidation)
const showPriceImpactWarning = canTrade && !expertMode && !!priceImpact.error
const walletIsNotConnected = primaryFormValidation === TradeFormValidation.WalletNotConnected

Expand Down
4 changes: 2 additions & 2 deletions src/modules/twap/state/emulatedPartOrdersAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { OrderStatus as OrderStatusInApp } from 'legacy/state/orders/actions'

import { OrderWithComposableCowInfo } from 'common/types'

import { twapOrdersListAtom } from './twapOrdersListAtom'
import { twapOrdersAtom } from './twapOrdersListAtom'
import { twapPartOrdersListAtom } from './twapPartOrdersAtom'

import { TwapOrderItem, TwapOrderStatus } from '../types'

export const emulatedPartOrdersAtom = atom<OrderWithComposableCowInfo[]>((get) => {
const twapOrders = get(twapOrdersListAtom)
const twapOrders = get(twapOrdersAtom)
const twapParticleOrders = get(twapPartOrdersListAtom)

return twapParticleOrders.map<OrderWithComposableCowInfo>((order) => {
Expand Down
45 changes: 45 additions & 0 deletions src/modules/twap/state/emulatedTwapOrdersAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { atom } from 'jotai'

import { OrderStatus } from 'legacy/state/orders/actions'

import { tokensByAddressAtom } from 'modules/tokensList/state/tokensListAtom'
import { walletInfoAtom } from 'modules/wallet/api/state'

import { OrderWithComposableCowInfo } from 'common/types'

import { twapOrdersListAtom } from './twapOrdersListAtom'

import { TwapOrderStatus } from '../types'
import { emulateTwapAsOrder } from '../utils/emulateTwapAsOrder'

const statusesMap: Record<TwapOrderStatus, OrderStatus> = {
[TwapOrderStatus.Cancelled]: OrderStatus.CANCELLED,
[TwapOrderStatus.Expired]: OrderStatus.EXPIRED,
[TwapOrderStatus.Pending]: OrderStatus.PENDING,
[TwapOrderStatus.Scheduled]: OrderStatus.SCHEDULED,
[TwapOrderStatus.WaitSigning]: OrderStatus.PRESIGNATURE_PENDING,
[TwapOrderStatus.Fulfilled]: OrderStatus.FULFILLED,
}

export const emulatedTwapOrdersAtom = atom((get) => {
const { account, chainId } = get(walletInfoAtom)
const tokens = get(tokensByAddressAtom)
const orders = get(twapOrdersListAtom)
const accountLowerCase = account?.toLowerCase()

if (!accountLowerCase) return []

const orderWithComposableCowInfo: OrderWithComposableCowInfo[] = orders
.filter((order) => order.chainId === chainId && order.safeAddress.toLowerCase() === accountLowerCase)
.map((order) => {
return {
order: emulateTwapAsOrder(tokens, order),
composableCowInfo: {
id: order.id,
status: statusesMap[order.status],
},
}
})

return orderWithComposableCowInfo
})
62 changes: 20 additions & 42 deletions src/modules/twap/state/twapOrdersListAtom.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,42 @@
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'

import { OrderStatus } from 'legacy/state/orders/actions'

import { tokensByAddressAtom } from 'modules/tokensList/state/tokensListAtom'
import { walletInfoAtom } from 'modules/wallet/api/state'

import { OrderWithComposableCowInfo } from 'common/types'
import { deepEqual } from 'utils/deepEqual'

import { TwapOrderItem, TwapOrderStatus } from '../types'
import { emulateTwapAsOrder } from '../utils/emulateTwapAsOrder'
import { TwapOrderItem } from '../types'
import { updateTwapOrdersList } from '../utils/updateTwapOrdersList'

export type TwapOrdersList = { [key: string]: TwapOrderItem }

export const twapOrdersListAtom = atomWithStorage<TwapOrdersList>('twap-orders-list:v4', {})
export const twapOrdersAtom = atomWithStorage<TwapOrdersList>('twap-orders-list:v5', {})

export const twapOrdersListAtom = atom<TwapOrderItem[]>((get) => {
const { account, chainId } = get(walletInfoAtom)

if (!account || !chainId) return []

const accountLowerCase = account.toLowerCase()

const orders = Object.values(get(twapOrdersAtom))

return orders
.flat()
.filter((order) => order.safeAddress.toLowerCase() === accountLowerCase && order.chainId === chainId)
})

export const updateTwapOrdersListAtom = atom(null, (get, set, nextState: TwapOrdersList) => {
const currentState = get(twapOrdersListAtom)
const currentState = get(twapOrdersAtom)
const newState = updateTwapOrdersList(currentState, nextState)

if (!deepEqual(currentState, newState)) {
set(twapOrdersListAtom, newState)
set(twapOrdersAtom, newState)
}
})

export const addTwapOrderToListAtom = atom(null, (get, set, order: TwapOrderItem) => {
const currentState = get(twapOrdersListAtom)

set(twapOrdersListAtom, { ...currentState, [order.id]: order })
})

const statusesMap: Record<TwapOrderStatus, OrderStatus> = {
[TwapOrderStatus.Cancelled]: OrderStatus.CANCELLED,
[TwapOrderStatus.Expired]: OrderStatus.EXPIRED,
[TwapOrderStatus.Pending]: OrderStatus.PENDING,
[TwapOrderStatus.Scheduled]: OrderStatus.SCHEDULED,
[TwapOrderStatus.WaitSigning]: OrderStatus.PRESIGNATURE_PENDING,
}

export const emulatedTwapOrdersAtom = atom((get) => {
const { account, chainId } = get(walletInfoAtom)
const tokens = get(tokensByAddressAtom)
const orders = Object.values(get(twapOrdersListAtom))
const accountLowerCase = account?.toLowerCase()

if (!accountLowerCase) return []

const orderWithComposableCowInfo: OrderWithComposableCowInfo[] = orders
.filter((order) => order.chainId === chainId && order.safeAddress.toLowerCase() === accountLowerCase)
.map((order) => {
return {
order: emulateTwapAsOrder(tokens, order),
composableCowInfo: {
id: order.id,
status: statusesMap[order.status],
},
}
})
const currentState = get(twapOrdersAtom)

return orderWithComposableCowInfo
set(twapOrdersAtom, { ...currentState, [order.id]: order })
})
2 changes: 1 addition & 1 deletion src/modules/twap/state/twapPartOrdersAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface TwapPartOrderItem {
}
export type TwapPartOrders = { [twapOrderHash: string]: TwapPartOrderItem[] }

export const twapPartOrdersAtom = atomWithStorage<TwapPartOrders>('twap-part-orders-list:v2', {})
export const twapPartOrdersAtom = atomWithStorage<TwapPartOrders>('twap-part-orders-list:v3', {})

export const twapPartOrdersListAtom = atom<TwapPartOrderItem[]>((get) => {
const { account, chainId } = get(walletInfoAtom)
Expand Down
8 changes: 8 additions & 0 deletions src/modules/twap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,20 @@ export enum TwapOrderStatus {
Scheduled = 'Scheduled',
Cancelled = 'Cancelled',
Expired = 'Expired',
Fulfilled = 'Fulfilled',
}

export interface TwapOrdersSafeData {
conditionalOrderParams: ConditionalOrderParams
safeTxParams: SafeTransactionParams
}

export interface TwapOrderExecutionInfo {
executedSellAmount: string
executedBuyAmount: string
executedFeeAmount: string
}

export interface TwapOrderItem {
order: TWAPOrderStruct
status: TwapOrderStatus
Expand All @@ -52,6 +59,7 @@ export interface TwapOrderItem {
safeAddress: string
id: string
safeTxParams?: SafeTransactionParams
executionInfo: TwapOrderExecutionInfo
}

export interface ConditionalOrderParams {
Expand Down
5 changes: 2 additions & 3 deletions src/modules/twap/updaters/PartOrdersUpdater.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ import { TwapOrderItem } from '../types'

export function PartOrdersUpdater() {
const { chainId, account } = useWalletInfo()
const twapOrdersList = useAtomValue(twapOrdersListAtom)
const twapOrders = 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)
Expand All @@ -39,7 +38,7 @@ export function PartOrdersUpdater() {

updateTwapPartOrders(ordersMap)
})
}, [chainId, account, twapOrdersList, updateTwapPartOrders])
}, [chainId, account, twapOrders, updateTwapPartOrders])

return null
}
Expand Down
Loading

0 comments on commit a13101c

Please sign in to comment.