Skip to content
This repository was archived by the owner on Apr 4, 2022. It is now read-only.

1046 load from api orderstable #611

Merged
merged 11 commits into from
Sep 20, 2021
12 changes: 10 additions & 2 deletions src/api/operator/operatorApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,24 @@ export async function getOrder(params: GetOrderParams): Promise<RawOrder | null>
* - owner: address
* - sellToken: address
* - buyToken: address
* - minValidTo: number
*/
export async function getOrders(params: GetOrdersParams): Promise<RawOrder[]> {
const { networkId, ...searchParams } = params
const { owner, sellToken, buyToken } = searchParams
const { owner, sellToken, buyToken, minValidTo } = searchParams
const defaultValues = {
includeFullyExecuted: 'true',
includeInvalidated: 'true',
includeInsufficientBalance: 'true',
includePresignaturePending: 'true',
includeUnsupportedTokens: 'true',
}

console.log(
`[getOrders] Fetching orders on network ${networkId} with filters: owner=${owner} sellToken=${sellToken} buyToken=${buyToken}`,
)

const searchString = buildSearchString({ ...searchParams })
const searchString = buildSearchString({ ...searchParams, ...defaultValues, minValidTo: String(minValidTo) })

const queryString = '/orders/' + searchString

Expand Down
3 changes: 2 additions & 1 deletion src/api/operator/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ export type GetOrderParams = WithNetworkId & {
}

export type GetOrdersParams = WithNetworkId & {
owner?: string
owner: string
minValidTo: number
sellToken?: string
buyToken?: string
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { useContext } from 'react'
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

import OrdersTable from 'components/orders/OrdersUserDetailsTable'
import { EmptyItemWrapper } from 'components/common/StyledUserDetailsTable'
import { OrdersTableContext, OrdersTableState } from './context/OrdersTableContext'

export const OrdersTableWithData: React.FC = () => {
const { orders, kind } = useContext(OrdersTableContext)

return kind === OrdersTableState.Loading ? (
<EmptyItemWrapper>
<FontAwesomeIcon icon={faSpinner} spin size="3x" />
</EmptyItemWrapper>
) : (
<OrdersTable orders={orders} />
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react'

import { Order } from 'api/operator'

export enum OrdersTableState {
Loading,
Loaded,
Error,
}

interface CommonState {
orders: Order[]
error: string
kind: OrdersTableState
}

export const OrdersTableContext = React.createContext({} as CommonState)
85 changes: 85 additions & 0 deletions src/apps/explorer/components/OrdersTableWidget/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { useMemo } from 'react'
import styled from 'styled-components'
import { useParams } from 'react-router'
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

import { media } from 'theme/styles/media'

import ExplorerTabs from 'apps/explorer/components/common/ExplorerTabs/ExplorerTab'
import { OrdersTableWithData } from './OrdersTableWithData'
import { OrdersTableContext, OrdersTableState } from './context/OrdersTableContext'
import { useGetOrders } from './useGetOrders'
import { TabItemInterface } from 'components/common/Tabs/Tabs'

const Wrapper = styled.div`
padding: 1.6rem;
margin: 0 auto;
width: 100%;
max-width: 140rem;

${media.mediumDown} {
max-width: 94rem;
}

${media.mobile} {
max-width: 100%;
}
`

const StyledTabLoader = styled.span`
padding-left: 4px;
`

const tabItems = (ordersTableState: OrdersTableState): TabItemInterface[] => {
return [
{
id: 1,
tab: (
<>
Orders
<StyledTabLoader>
{ordersTableState === OrdersTableState.Loading && <FontAwesomeIcon icon={faSpinner} spin size="1x" />}
</StyledTabLoader>
</>
),
content: <OrdersTableWithData />,
},
{
id: 2,
tab: 'Trades',
content: (
<>
<h2>Trades Content</h2>
</>
),
},
]
}

function useAddressParam(): string {
const { address } = useParams<{ address: string }>()

return address
}

const OrdersTableWidget: React.FC = () => {
const ownerAddress = useAddressParam()
const { orders, isLoading, error } = useGetOrders(ownerAddress)
const ordersTableState = useMemo(() => {
if (isLoading && orders.length === 0) return OrdersTableState.Loading
else if (error) return OrdersTableState.Error

return OrdersTableState.Loaded
}, [isLoading, orders.length, error])

return (
<OrdersTableContext.Provider value={{ orders, kind: ordersTableState, error }}>
<Wrapper>
<ExplorerTabs tabItems={tabItems(isLoading ? OrdersTableState.Loading : OrdersTableState.Loaded)} />
</Wrapper>
</OrdersTableContext.Provider>
)
}

export default OrdersTableWidget
128 changes: 128 additions & 0 deletions src/apps/explorer/components/OrdersTableWidget/useGetOrders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { useState, useCallback, useEffect } from 'react'
import { subMinutes, getTime } from 'date-fns'

import { Network } from 'types'
import { useMultipleErc20 } from 'hooks/useErc20'
import { getOrders, Order, RawOrder } from 'api/operator'
import { useNetworkId } from 'state/network'
import { transformOrder } from 'utils'
import { ORDERS_HISTORY_MINUTES_AGO, ORDERS_QUERY_INTERVAL } from 'apps/explorer/const'

/**
*
* Merge new RawOrders consulted, that may have changed status
*
* @param previousOrders List of orders
* @param newOrdersFetched List of fetched block order that could have changed
*/
export function mergeNewOrders(previousOrders: Order[], newOrdersFetched: RawOrder[]): Order[] {
if (newOrdersFetched.length === 0) return previousOrders

// find the order up to which it is to be replaced
const lastOrder = newOrdersFetched[newOrdersFetched.length - 1]
const positionLastOrder = previousOrders.map((o) => o.uid).indexOf(lastOrder.uid)
if (positionLastOrder === -1) {
return newOrdersFetched.map((order) => transformOrder(order)).concat(previousOrders)
}

const slicedOrders: Order[] = previousOrders.slice(positionLastOrder + 1)
return newOrdersFetched.map((order) => transformOrder(order)).concat(slicedOrders)
}

function isObjectEmpty(object: Record<string, unknown>): boolean {
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
for (const key in object) {
if (key) return false
}

return true
}

type Result = {
orders: Order[]
error: string
isLoading: boolean
}

export function useGetOrders(ownerAddress: string): Result {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState('')
const [orders, setOrders] = useState<Order[]>([])
const networkId = useNetworkId() || undefined
const [erc20Addresses, setErc20Addresses] = useState<string[]>([])
const { value: valueErc20s, isLoading: areErc20Loading } = useMultipleErc20({ networkId, addresses: erc20Addresses })
const [mountNewOrders, setMountNewOrders] = useState(false)

const fetchOrders = useCallback(
async (network: Network, owner: string, minTimeHistoryTimeStamp = 0): Promise<void> => {
setIsLoading(true)
setError('')

try {
const ordersFetched = await getOrders({ networkId: network, owner, minValidTo: minTimeHistoryTimeStamp })
const newErc20Addresses = ordersFetched.reduce((accumulator: string[], element) => {
const updateAccumulator = (tokenAddress: string): void => {
Copy link
Contributor

Choose a reason for hiding this comment

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

What about using a Set (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) for this?

const newErc20Address = new Set<string>()
ordersFetched.forEach(order => {
  newErc20Address.add(order.buyToken)
  newErc20Address.add(order.sellToken)
})

Copy link
Contributor Author

@henrypalacios henrypalacios Sep 9, 2021

Choose a reason for hiding this comment

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

I love the Set, it would be much more readable however but I went for the reduce for performance.
Do you think it makes up for it or should I go for the Set? https://jsben.ch/ognIg

Copy link
Contributor

Choose a reason for hiding this comment

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

Wow, no idea it had such a performance impact. Guess they make mose sense in languages where Sets are first class citizens.

I prefer sets for readability but maybe we should stick with reducer due to performance as you suggest.

@anxolin @W3stside FYI

if (accumulator.indexOf(tokenAddress) === -1) {
accumulator.push(tokenAddress)
}
}
updateAccumulator(element.buyToken)
updateAccumulator(element.sellToken)

return accumulator
}, [])

setErc20Addresses(newErc20Addresses)
// For the moment it is neccesary to sort by date
ordersFetched.sort((a, b) => +new Date(b.creationDate) - +new Date(a.creationDate))

setOrders((previousOrders) => mergeNewOrders(previousOrders, ordersFetched))
setMountNewOrders(true)
} catch (e) {
const msg = `Failed to fetch orders`
console.error(msg, e)
setError(msg)
} finally {
setIsLoading(false)
}
},
[],
)

useEffect(() => {
if (!networkId) {
return
}
const getOrUpdateOrders = (minHistoryTime?: number): Promise<void> =>
fetchOrders(networkId, ownerAddress, minHistoryTime)

getOrUpdateOrders()

const intervalId: NodeJS.Timeout = setInterval(() => {
const minutesAgoTimestamp = getTime(subMinutes(new Date(), ORDERS_HISTORY_MINUTES_AGO))
getOrUpdateOrders(Math.floor(minutesAgoTimestamp / 1000))
}, ORDERS_QUERY_INTERVAL)

return (): void => {
clearInterval(intervalId)
}
}, [fetchOrders, networkId, ownerAddress])

useEffect(() => {
if (areErc20Loading || isObjectEmpty(valueErc20s) || !mountNewOrders) {
return
}

const newOrders = orders.map((order) => {
order.buyToken = valueErc20s[order.buyTokenAddress] || order.buyToken
order.sellToken = valueErc20s[order.sellTokenAddress] || order.sellToken

return order
})

setOrders(newOrders)
setMountNewOrders(false)
}, [valueErc20s, networkId, areErc20Loading, mountNewOrders, orders])

return { orders, error, isLoading }
}
2 changes: 2 additions & 0 deletions src/apps/explorer/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { AnalyticsDimension } from 'types'

/** Explorer app constants */
export const ORDER_QUERY_INTERVAL = 10000 // in ms
export const ORDERS_QUERY_INTERVAL = 10000 // in ms
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We had agreed on 30min but I have set it to 10min to test.

export const ORDERS_HISTORY_MINUTES_AGO = 10 // in minutes

export const DISPLAY_TEXT_COPIED_CHECK = 1000 // in ms

Expand Down
4 changes: 3 additions & 1 deletion src/apps/explorer/pages/UserDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react'

const UserDetails: React.FC = () => <h3>Placeholder Page</h3>
import OrdersTableWidget from '../components/OrdersTableWidget'

const UserDetails: React.FC = () => <OrdersTableWidget />

export default UserDetails
12 changes: 11 additions & 1 deletion src/components/common/StyledUserDetailsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import styled from 'styled-components'

import { SimpleTable, Props as SimpleTableProps } from 'components/common/SimpleTable'

interface Props {
export interface Props {
showBorderTable?: boolean
}

Expand All @@ -11,6 +11,7 @@ export type StyledUserDetailsTableProps = SimpleTableProps & Props
const StyledUserDetailsTable = styled(SimpleTable)<StyledUserDetailsTableProps>`
border: ${({ theme, showBorderTable }): string => (showBorderTable ? `0.1rem solid ${theme.borderPrimary}` : 'none')};
border-radius: 0.4rem;
margin-top: 0;

tr td {
&:not(:first-of-type) {
Expand Down Expand Up @@ -38,6 +39,9 @@ const StyledUserDetailsTable = styled(SimpleTable)<StyledUserDetailsTableProps>`
gap: 6px;
}

thead {
position: inherit;
}
thead tr {
width: 100%;
}
Expand All @@ -53,6 +57,10 @@ const StyledUserDetailsTable = styled(SimpleTable)<StyledUserDetailsTableProps>`
span.wrap-datedisplay > span:last-of-type {
display: flex;
}

tbody tr td.row-td-empty {
grid-column: 1 / span all;
}
`

export const EmptyItemWrapper = styled.div`
Expand All @@ -62,6 +70,8 @@ export const EmptyItemWrapper = styled.div`
align-items: center;
justify-content: center;
display: flex;
width: 100%;
font-size: ${({ theme }): string => theme.fontSizeDefault};
`

export default StyledUserDetailsTable
Loading