From e9a80cb506b6771ebbfdf102e4dcfa0e43347f0c Mon Sep 17 00:00:00 2001 From: Nathanael Liu Date: Thu, 15 Aug 2024 10:57:44 -0400 Subject: [PATCH] Order processor UI (#208) * wip: order processor ui * updates * Update src/pages/processor.tsx * fix sidebar * update order card * restructure apis * add cocos indexer to env * fix timeago * order pages * hooks * order details modal * order processor table * update fetching orders * remove link to the extrinsic id * show not processed orders only * disable pages under secondary market on kusama * center account links in purchase history table * purchase type * update parachain table * fix contributed amount * fix sidebar responsive * fix: relay token for order rewards * fix order id --------- Co-authored-by: Sergej Sakac <73715684+Szegoo@users.noreply.github.com> Co-authored-by: cuteolaf --- .env.example | 1 + next.config.js | 2 + src/apis/accounts.ts | 34 ++++ src/apis/index.ts | 151 +--------------- src/apis/orders.ts | 77 ++++++++ src/apis/sales.ts | 118 ++++++++++++ src/components/Elements/Address/index.tsx | 10 +- .../Layout/Sidebar/index.module.scss | 14 +- src/components/Layout/Sidebar/index.tsx | 27 ++- .../Modals/OrderDetails/index.module.scss | 5 + .../Orders/Modals/OrderDetails/index.tsx | 68 +++++++ src/components/Orders/Modals/index.ts | 1 + src/components/Orders/OrderCard/index.tsx | 16 +- .../Tables/OrderProcessorTable/index.tsx | 170 ++++++++++++++++++ .../Tables/ParachainTable/index.tsx | 9 +- .../Tables/PurchaseHistoryTable/index.tsx | 28 +-- src/components/Tables/index.ts | 1 + src/consts/index.ts | 2 + src/contexts/orders/index.tsx | 110 ++++++++---- src/hooks/index.ts | 1 + src/hooks/order/index.ts | 1 + src/hooks/order/processed.ts | 79 ++++++++ src/models/orders/index.ts | 25 ++- src/models/regions/sale.ts | 2 +- src/pages/marketplace.tsx | 11 ++ src/pages/{orders.tsx => orders/index.tsx} | 21 ++- src/pages/orders/processor.tsx | 69 +++++++ src/utils/functions/formatting.ts | 2 +- 28 files changed, 822 insertions(+), 233 deletions(-) create mode 100644 src/apis/accounts.ts create mode 100644 src/apis/orders.ts create mode 100644 src/apis/sales.ts create mode 100644 src/components/Orders/Modals/OrderDetails/index.module.scss create mode 100644 src/components/Orders/Modals/OrderDetails/index.tsx create mode 100644 src/components/Tables/OrderProcessorTable/index.tsx create mode 100644 src/hooks/order/index.ts create mode 100644 src/hooks/order/processed.ts rename src/pages/{orders.tsx => orders/index.tsx} (87%) create mode 100644 src/pages/orders/processor.tsx diff --git a/.env.example b/.env.example index e522b0b4..9c293bc3 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ WS_REGIONX_COCOS_CHAIN="WSS endpoint of the regionx chain" ROCOCO_CORETIME_INDEXER="Subquery indexer for Rococo Coretime" KUSAMA_CORETIME_INDEXER="Subquery indexer for Kusama Coretime" +COCOS_INDEXER="Subquery indexer for RegionX Cocos" ROCOCO_CORETIME_DICT="Subquery dictionary for Rococo Coretime" KUSAMA_CORETIME_DICT="Subquery dictionary for Kusama Coretime" diff --git a/next.config.js b/next.config.js index aba8338e..aae4cefa 100644 --- a/next.config.js +++ b/next.config.js @@ -9,6 +9,8 @@ const nextConfig = { WS_KUSAMA_CORETIME_CHAIN: process.env.WS_KUSAMA_CORETIME_CHAIN || '', WS_WESTEND_CORETIME_CHAIN: process.env.WS_WESTEND_CORETIME_CHAIN || '', + COCOS_INDEXER: process.env.COCOS_INDEXER || '', + ROCOCO_CORETIME_INDEXER: process.env.ROCOCO_CORETIME_INDEXER || '', KUSAMA_CORETIME_INDEXER: process.env.KUSAMA_CORETIME_INDEXER || '', SUBSCAN_CORETIME_WESTEND_INDEXER: diff --git a/src/apis/accounts.ts b/src/apis/accounts.ts new file mode 100644 index 00000000..7da438d6 --- /dev/null +++ b/src/apis/accounts.ts @@ -0,0 +1,34 @@ +import { fetchGraphql } from '@/utils/fetchGraphql'; + +import { API_CORETIME_DICT } from '@/consts'; +import { Address, ApiResponse, NetworkType } from '@/models'; + +export const fetchAccountExtrinsics = async ( + network: NetworkType, + address: Address, + after: string | null, + orderBy = 'BLOCK_HEIGHT_DESC' +): Promise => { + const query = `{ + extrinsics( + after: ${after} + filter: {signer: {equalTo: "${address}"}} + orderBy: ${orderBy} + ) { + nodes { + id + module + call + blockHeight + success + timestamp + } + pageInfo { + hasNextPage + endCursor + } + totalCount + } + }`; + return fetchGraphql(API_CORETIME_DICT[network], query); +}; diff --git a/src/apis/index.ts b/src/apis/index.ts index 7a320f1c..06f44c9b 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -1,148 +1,3 @@ -import { API_CORETIME_DICT, API_CORETIME_INDEXER } from '@/consts'; -import { Address, ApiResponse, NetworkType } from '@/models'; - -import { fetchGraphql } from '../utils/fetchGraphql'; - -export const fetchBurnInfo = async ( - network: NetworkType -): Promise => { - const query = `{ - stats { - nodes { - id - saleCycle - totalBurn - } - } - sales( - orderBy: HEIGHT_DESC, - first: 2 - ) { - nodes { - burn - } - } - }`; - return fetchGraphql(API_CORETIME_INDEXER[network], query); -}; - -export const fetchPurchaseHistoryData = async ( - network: NetworkType, - regionBegin: number, - after: string | null, - orderBy = 'HEIGHT_DESC' -): Promise => { - const query = `{ - purchases( - after: ${after} - filter: {begin: {equalTo: ${regionBegin}}} - orderBy: ${orderBy} - ) { - nodes { - account - core - extrinsicId - height - price - purchaseType - timestamp - } - pageInfo { - hasNextPage - endCursor - } - totalCount - } - }`; - return fetchGraphql(API_CORETIME_INDEXER[network], query); -}; - -export const fetchSaleDetailsData = async ( - network: NetworkType, - saleCycle: number, - after: string | null, - orderBy = 'HEIGHT_DESC' -): Promise => { - const query = `{ - purchases( - filter: {saleCycle: {equalTo: ${saleCycle}}} - after: ${after ? `"${after}"` : null} - orderBy: ${orderBy} - ) { - nodes { - account - core - extrinsicId - height - price - purchaseType - timestamp - } - pageInfo { - hasNextPage - endCursor - } - totalCount - } - }`; - return fetchGraphql(API_CORETIME_INDEXER[network], query); -}; - -export const fetchSalesHistoryData = async ( - network: NetworkType, - after: string | null -): Promise => { - const query = `{ - sales( - after: ${after}, - orderBy: SALE_CYCLE_DESC - ) { - nodes { - saleCycle - regionBegin - regionEnd - height - saleEnd - timestamp - tsSaleEnd - startPrice - endPrice - } - pageInfo { - hasNextPage - endCursor - } - } - }`; - return fetchGraphql(API_CORETIME_INDEXER[network], query); -}; - -export const fetchAccountExtrinsics = async ( - network: NetworkType, - address: Address, - after: string | null, - orderBy = 'BLOCK_HEIGHT_DESC' -): Promise => { - const query = `{ - extrinsics( - after: ${after} - filter: {signer: {equalTo: "${address}"}} - orderBy: ${orderBy} - ) { - nodes { - id - module - call - blockHeight - success - timestamp - } - pageInfo { - hasNextPage - endCursor - } - totalCount - } - }`; - return fetchGraphql(API_CORETIME_DICT[network], query); -}; +export * from './accounts'; +export * from './orders'; +export * from './sales'; diff --git a/src/apis/orders.ts b/src/apis/orders.ts new file mode 100644 index 00000000..55fc944d --- /dev/null +++ b/src/apis/orders.ts @@ -0,0 +1,77 @@ +import { fetchGraphql } from '@/utils/fetchGraphql'; + +import { API_COCOS_INDEXER } from '@/consts'; +import { Address, ApiResponse } from '@/models'; + +export const fetchContribution = async ( + orderId: number, + address: Address | undefined, + after: string | null +): Promise => { + const query = `{ + orderContributions( + after: ${after} + filter: {orderId: {equalTo: ${orderId}}, account: {equalTo: "${address}"}} + ) { + nodes { + amount + } + pageInfo { + hasNextPage + endCursor + } + } + }`; + return fetchGraphql(API_COCOS_INDEXER, query); +}; + +export const fetchOrders = async ( + after: string | null +): Promise => { + const query = `{ + orders(after: ${after}) { + nodes { + orderId + begin + end + creator + exist + coreOccupancy + contribution + paraId + processed + } + pageInfo { + hasNextPage + endCursor + } + } + }`; + return fetchGraphql(API_COCOS_INDEXER, query); +}; + +export const fetchProcessedOrders = async ( + after: string | null, + order = 'ORDER_ID_ASC' +): Promise => { + const query = `{ + processedOrders(after: ${after}, orderBy: ${order}) { + nodes { + orderId + height + extrinsicId + timestamp + begin + core + mask + seller + reward + } + pageInfo { + hasNextPage + endCursor + } + } + }`; + return fetchGraphql(API_COCOS_INDEXER, query); +}; diff --git a/src/apis/sales.ts b/src/apis/sales.ts new file mode 100644 index 00000000..f111dec1 --- /dev/null +++ b/src/apis/sales.ts @@ -0,0 +1,118 @@ +import { fetchGraphql } from '@/utils/fetchGraphql'; + +import { API_CORETIME_INDEXER } from '@/consts'; +import { ApiResponse, NetworkType } from '@/models'; + +export const fetchBurnInfo = async ( + network: NetworkType +): Promise => { + const query = `{ + stats { + nodes { + id + saleCycle + totalBurn + } + } + sales( + orderBy: HEIGHT_DESC, + first: 2 + ) { + nodes { + burn + } + } + }`; + return fetchGraphql(API_CORETIME_INDEXER[network], query); +}; + +export const fetchPurchaseHistoryData = async ( + network: NetworkType, + regionBegin: number, + after: string | null, + orderBy = 'HEIGHT_DESC' +): Promise => { + const query = `{ + purchases( + after: ${after} + filter: {begin: {equalTo: ${regionBegin}}} + orderBy: ${orderBy} + ) { + nodes { + account + core + extrinsicId + height + price + purchaseType + timestamp + } + pageInfo { + hasNextPage + endCursor + } + totalCount + } + }`; + return fetchGraphql(API_CORETIME_INDEXER[network], query); +}; + +export const fetchSaleDetailsData = async ( + network: NetworkType, + saleCycle: number, + after: string | null, + orderBy = 'HEIGHT_DESC' +): Promise => { + const query = `{ + purchases( + filter: {saleCycle: {equalTo: ${saleCycle}}} + after: ${after ? `"${after}"` : null} + orderBy: ${orderBy} + ) { + nodes { + account + core + extrinsicId + height + price + purchaseType + timestamp + } + pageInfo { + hasNextPage + endCursor + } + totalCount + } + }`; + return fetchGraphql(API_CORETIME_INDEXER[network], query); +}; + +export const fetchSalesHistoryData = async ( + network: NetworkType, + after: string | null +): Promise => { + const query = `{ + sales( + after: ${after}, + orderBy: SALE_CYCLE_DESC + ) { + nodes { + saleCycle + regionBegin + regionEnd + height + saleEnd + timestamp + tsSaleEnd + startPrice + endPrice + } + pageInfo { + hasNextPage + endCursor + } + } + }`; + return fetchGraphql(API_CORETIME_INDEXER[network], query); +}; diff --git a/src/components/Elements/Address/index.tsx b/src/components/Elements/Address/index.tsx index 9a7c06f7..ec049795 100644 --- a/src/components/Elements/Address/index.tsx +++ b/src/components/Elements/Address/index.tsx @@ -10,6 +10,7 @@ interface AddressProps { isShort?: boolean; isCopy?: boolean; size?: number; + center?: boolean; } export const Address = ({ @@ -17,6 +18,7 @@ export const Address = ({ isShort, isCopy, size = 32, + center, }: AddressProps) => { const { toastInfo } = useToast(); @@ -31,7 +33,13 @@ export const Address = ({ }; return ( - + diff --git a/src/components/Layout/Sidebar/index.module.scss b/src/components/Layout/Sidebar/index.module.scss index b2b4183b..351c73ba 100644 --- a/src/components/Layout/Sidebar/index.module.scss +++ b/src/components/Layout/Sidebar/index.module.scss @@ -3,6 +3,7 @@ flex-direction: column; width: 15rem; background-color: white; + height: 100vh; } .logoContainer { @@ -27,14 +28,25 @@ .menuContainer { display: flex; flex-direction: column; + height: calc(100vh - 5rem); + font-size: 1rem; + padding: 2rem 1.5rem; +} + +.menuItems { + display: flex; + flex-direction: column; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #7e8591 #efefef; flex-grow: 1; - gap: 1rem; } .statusContainer { display: flex; flex-direction: column; gap: 0.5rem; + margin-top: 1rem; } .menuIcon { diff --git a/src/components/Layout/Sidebar/index.tsx b/src/components/Layout/Sidebar/index.tsx index b9ade3a9..07708585 100644 --- a/src/components/Layout/Sidebar/index.tsx +++ b/src/components/Layout/Sidebar/index.tsx @@ -4,6 +4,7 @@ import GridViewIcon from '@mui/icons-material/GridView'; import HistoryIcon from '@mui/icons-material/History'; import HomeIcon from '@mui/icons-material/Home'; import ListOutlinedIcon from '@mui/icons-material/ListOutlined'; +import RepeatOutlinedIcon from '@mui/icons-material/RepeatOutlined'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import SwapHorizOutlinedIcon from '@mui/icons-material/SwapHorizOutlined'; import { Box, Stack, Typography, useTheme } from '@mui/material'; @@ -151,6 +152,12 @@ export const Sidebar = () => { enabled: enableRegionX(network), icon: , }, + { + label: 'Order Processor', + route: '/orders/processor', + enabled: enableRegionX(network), + icon: , + }, ], }; @@ -160,7 +167,7 @@ export const Sidebar = () => { }; return ( -
+ { logo - - + + {Object.entries(menu).map(([label, submenu], index) => ( { ))} -
+ { label='RegionX chain' /> )} -
+
-
+ ); }; diff --git a/src/components/Orders/Modals/OrderDetails/index.module.scss b/src/components/Orders/Modals/OrderDetails/index.module.scss new file mode 100644 index 00000000..2fd0374a --- /dev/null +++ b/src/components/Orders/Modals/OrderDetails/index.module.scss @@ -0,0 +1,5 @@ +.container { + display: flex; + flex-direction: column; + gap: 0.75rem; +} diff --git a/src/components/Orders/Modals/OrderDetails/index.tsx b/src/components/Orders/Modals/OrderDetails/index.tsx new file mode 100644 index 00000000..8336252a --- /dev/null +++ b/src/components/Orders/Modals/OrderDetails/index.tsx @@ -0,0 +1,68 @@ +import { + Box, + Dialog, + DialogActions, + DialogContent, + DialogProps, + Typography, + useTheme, +} from '@mui/material'; + +import { ActionButton } from '@/components/Elements'; + +import { useOrders } from '@/contexts/orders'; + +import styles from './index.module.scss'; +import { OrderCard } from '../../OrderCard'; + +interface OrderDetailsModalProps extends DialogProps { + onClose: () => void; + orderId: number; +} + +export const OrderDetailsModal = ({ + open, + onClose, + orderId, +}: OrderDetailsModalProps) => { + const { orders } = useOrders(); + const order = orders.find((item) => item.orderId === orderId); + + const theme = useTheme(); + + return ( + + + + + Order #{orderId + 1} + + + + {!order ? ( + Failed to get the order details + ) : ( + + )} + + + + + + + + + ); +}; diff --git a/src/components/Orders/Modals/index.ts b/src/components/Orders/Modals/index.ts index 239ea22e..a91d986d 100644 --- a/src/components/Orders/Modals/index.ts +++ b/src/components/Orders/Modals/index.ts @@ -1,2 +1,3 @@ export * from './Contribution'; export * from './OrderCreation'; +export * from './OrderDetails'; diff --git a/src/components/Orders/OrderCard/index.tsx b/src/components/Orders/OrderCard/index.tsx index 3e40d50c..f5b81e63 100644 --- a/src/components/Orders/OrderCard/index.tsx +++ b/src/components/Orders/OrderCard/index.tsx @@ -32,7 +32,7 @@ export const OrderCard = ({ state: { decimals, symbol }, } = useRelayApi(); const { network } = useNetwork(); - const { requirements, paraId } = order; + const { begin, end, coreOccupancy, paraId } = order; const logo = chainData[network][order.paraId]?.logo; @@ -62,7 +62,7 @@ export const OrderCard = ({ - {requirements.begin} + {begin} @@ -70,13 +70,13 @@ export const OrderCard = ({ - {requirements.end} + {end} - Contributed + Contribution {`${getBalanceString( order.contribution.toString(), @@ -90,13 +90,17 @@ export const OrderCard = ({ Core Occupancy - {`${((requirements.coreOccupancy / 57600) * 100).toFixed(2)} %`} + {`${((coreOccupancy / 57600) * 100).toFixed(2)} %`} diff --git a/src/components/Tables/OrderProcessorTable/index.tsx b/src/components/Tables/OrderProcessorTable/index.tsx new file mode 100644 index 00000000..f07d6742 --- /dev/null +++ b/src/components/Tables/OrderProcessorTable/index.tsx @@ -0,0 +1,170 @@ +import { + Button, + Paper, + Stack, + Table, + TableBody, + TableContainer, + TableFooter, + TableHead, + TablePagination, + TableRow, + Tooltip, +} from '@mui/material'; +import { useState } from 'react'; + +import { + getBalanceString, + getRelativeTimeString, + getTimeStringLong, +} from '@/utils/functions'; + +import { Address, Link } from '@/components/Elements'; +import { OrderDetailsModal } from '@/components/Orders'; + +import { SUSBCAN_CORETIME_URL } from '@/consts'; +import { useRelayApi } from '@/contexts/apis'; +import { useNetwork } from '@/contexts/network'; +import { OrderItem } from '@/models'; + +import { StyledTableCell, StyledTableRow } from '../common'; + +interface OrderProcessorTableProps { + data: OrderItem[]; +} + +export const OrderProcessorTable = ({ data }: OrderProcessorTableProps) => { + const { network } = useNetwork(); + const { + state: { decimals, symbol }, + } = useRelayApi(); + + const [activeOrderId, setActiveOrderId] = useState(null); + + // table pagination + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + + const handleChangePage = ( + _event: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = ( + event: React.ChangeEvent + ) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + return ( + <> + + + + + + Order Id + Extrinsic + Who + Reward + Timestamp + + + + {(rowsPerPage > 0 + ? data.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ) + : data + ).map( + ( + { orderId, height, extrinsicId, account, reward, timestamp }, + index + ) => ( + + + + + + {height}-{extrinsicId} + + + +
+ + + + {getBalanceString(reward.toString(), decimals, symbol)} + + + +

{getRelativeTimeString(timestamp)}

+
+
+ + ) + )} + +
+
+ + + + + + + +
+
+
+ {activeOrderId !== null ? ( + setActiveOrderId(null)} + orderId={activeOrderId} + /> + ) : ( + <> + )} + + ); +}; diff --git a/src/components/Tables/ParachainTable/index.tsx b/src/components/Tables/ParachainTable/index.tsx index d78ca14b..25cc0f76 100644 --- a/src/components/Tables/ParachainTable/index.tsx +++ b/src/components/Tables/ParachainTable/index.tsx @@ -158,7 +158,7 @@ export const ParachainTable = ({ : parachains ).map(({ id, name, state, watching, logo, homepage }, index) => ( - + {id} @@ -188,7 +188,12 @@ export const ParachainTable = ({ )}
- + diff --git a/src/components/Tables/PurchaseHistoryTable/index.tsx b/src/components/Tables/PurchaseHistoryTable/index.tsx index 06b14980..7f069264 100644 --- a/src/components/Tables/PurchaseHistoryTable/index.tsx +++ b/src/components/Tables/PurchaseHistoryTable/index.tsx @@ -9,6 +9,7 @@ import { TablePagination, TableRow, Tooltip, + useTheme, } from '@mui/material'; import { useState } from 'react'; @@ -32,6 +33,8 @@ interface PurchaseHistoryTableProps { } export const PurchaseHistoryTable = ({ data }: PurchaseHistoryTableProps) => { + const theme = useTheme(); + const { network } = useNetwork(); const { state: { symbol, decimals }, @@ -95,17 +98,22 @@ export const PurchaseHistoryTable = ({ data }: PurchaseHistoryTableProps) => { - + window.open( + `${SUSBCAN_CORETIME_URL[network]}/account/${address}`, + '_blank' + ) + } > -
- +
+ {core} diff --git a/src/components/Tables/index.ts b/src/components/Tables/index.ts index 14540ceb..efa60e88 100644 --- a/src/components/Tables/index.ts +++ b/src/components/Tables/index.ts @@ -1,4 +1,5 @@ export * from './common'; +export * from './OrderProcessorTable'; export * from './ParachainTable'; export * from './PurchaseHistoryTable'; export * from './SalesHistoryTable'; diff --git a/src/consts/index.ts b/src/consts/index.ts index c44810f3..5f8c66d7 100644 --- a/src/consts/index.ts +++ b/src/consts/index.ts @@ -16,6 +16,8 @@ export const API_CORETIME_DICT = { [NetworkType.NONE]: '', }; +export const API_COCOS_INDEXER = process.env.COCOS_INDEXER ?? ''; + export const SUSBCAN_CORETIME_URL = { [NetworkType.ROCOCO]: 'https://coretime-rococo.subscan.io', [NetworkType.KUSAMA]: 'https://coretime-kusama.subscan.io', diff --git a/src/contexts/orders/index.tsx b/src/contexts/orders/index.tsx index 4f7cf726..6309379a 100644 --- a/src/contexts/orders/index.tsx +++ b/src/contexts/orders/index.tsx @@ -6,13 +6,14 @@ import { useState, } from 'react'; -import { getOrderAccount } from '@/utils/order'; - -import { ContextStatus, OnChainOrder, Order, RELAY_ASSET_ID } from '@/models'; +import { fetchContribution, fetchOrders as fetchOrdersApi } from '@/apis'; +import { ApiResponse, ContextStatus, Order } from '@/models'; import { useAccounts } from '../account'; -import { useRegionXApi } from '../apis'; -import { ApiState } from '../apis/types'; + +interface Contribution { + amount: string; +} interface OrderData { status: ContextStatus; @@ -41,59 +42,92 @@ const OrderProvider = ({ children }: Props) => { const { state: { activeAccount }, } = useAccounts(); - const { - state: { api, apiState }, - } = useRegionXApi(); const fetchOrders = useCallback(async () => { - if (!api || apiState !== ApiState.READY) { - return; - } + const getContribution = async (orderId: number) => { + let finished = false; + let after: string | null = null; + + let sum = 0; + while (!finished) { + const res: ApiResponse = await fetchContribution( + orderId, + activeAccount?.address, + after + ); + + const { + status, + data: { + orderContributions: { nodes, pageInfo }, + }, + } = res; + + if (status !== 200 || nodes === null) break; + + sum += nodes.reduce( + (acc: number, item: Contribution) => acc + parseInt(item.amount), + 0 + ); + + finished = !pageInfo.hasNextPage; + after = pageInfo.endCursor; + } + return sum; + }; + try { setStatus(ContextStatus.LOADING); // fetch orders - const orderEntries = await api.query.orders.orders.entries(); - const records: Order[] = []; + let finished = false; + let after: string | null = null; + + const result = []; + while (!finished) { + const res: ApiResponse = await fetchOrdersApi(after); - for await (const [key, value] of orderEntries) { - const [orderId] = key.toHuman() as [number]; - const orderAccount = getOrderAccount(api, orderId); + const { + status, + data: { + orders: { nodes, pageInfo }, + }, + } = res; - const obj = value.toJSON() as OnChainOrder; + if (status !== 200 || nodes === null) break; - const totalContribution = ( - ( - await api.query.tokens.accounts( - orderAccount.toString(), - RELAY_ASSET_ID + result.push(...nodes); + + finished = !pageInfo.hasNextPage; + after = pageInfo.endCursor; + } + if (!finished) { + setStatus(ContextStatus.ERROR); + } else { + setOrders( + await Promise.all( + result.map( + async (item) => + ({ + ...item, + totalContribution: parseInt(item.contribution), + contribution: await getContribution(item.orderId), + } as Order) ) - ).toJSON() as any - ).free; - const contribution = ( - await api.query.orders.contributions(orderId, activeAccount?.address) - ).toJSON() as number; - - records.push({ - ...obj, - orderId, - contribution, - totalContribution, - } as Order); + ) + ); } - setOrders(records); setStatus(ContextStatus.LOADED); } catch (e) { setStatus(ContextStatus.ERROR); setOrders([]); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [api, apiState, activeAccount]); + }, [activeAccount]); useEffect(() => { fetchOrders(); - }, [api, apiState, activeAccount, fetchOrders]); + }, [activeAccount, fetchOrders]); return ( diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 4fff6d01..32ad0c45 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,4 +1,5 @@ export * from './accountExtrinsics'; +export * from './order'; export * from './parasInfo'; export * from './renewableParas'; export * from './sale'; diff --git a/src/hooks/order/index.ts b/src/hooks/order/index.ts new file mode 100644 index 00000000..0c346416 --- /dev/null +++ b/src/hooks/order/index.ts @@ -0,0 +1 @@ +export * from './processed'; diff --git a/src/hooks/order/processed.ts b/src/hooks/order/processed.ts new file mode 100644 index 00000000..677b1864 --- /dev/null +++ b/src/hooks/order/processed.ts @@ -0,0 +1,79 @@ +import { useEffect, useState } from 'react'; + +import { fetchProcessedOrders } from '@/apis'; +import { ApiResponse, OrderItem } from '@/models'; + +export const useProcessedOrders = () => { + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + const [isError, setError] = useState(false); + + useEffect(() => { + const asyncFetchData = async () => { + setData([]); + setError(false); + setLoading(false); + + try { + setLoading(true); + let finished = false; + let after: string | null = null; + + const result = []; + while (!finished) { + const res: ApiResponse = await fetchProcessedOrders(after); + + const { status, data } = res; + if (status !== 200) break; + + if (data.processedOrders.nodes !== null) + result.push(...data.processedOrders.nodes); + + finished = !data.processedOrders.pageInfo.hasNextPage; + after = data.processedOrders.pageInfo.endCursor; + } + if (!finished) { + setError(true); + } else { + setData( + result.map( + ({ + orderId, + height, + extrinsicId, + timestamp, + begin, + core, + mask, + seller, + reward, + }) => + ({ + orderId, + height, + extrinsicId, + timestamp: new Date(Number(timestamp)), + begin, + core, + mask, + account: seller, + reward, + } as OrderItem) + ) + ); + } + } catch { + setError(true); + } finally { + setLoading(false); + } + }; + asyncFetchData(); + }, []); + + return { + loading, + data, + isError, + }; +}; diff --git a/src/models/orders/index.ts b/src/models/orders/index.ts index dd25e1e4..6580aa00 100644 --- a/src/models/orders/index.ts +++ b/src/models/orders/index.ts @@ -4,20 +4,27 @@ import { Address, ParaId } from '../common'; type PartsOf57600 = number; -export type Requirements = { +export type Order = { + orderId: number; begin: Timeslice; end: Timeslice; - coreOccupancy: PartsOf57600; -}; - -export type OnChainOrder = { creator: Address; + exist: boolean; + coreOccupancy: PartsOf57600; + contribution: number; paraId: ParaId; - requirements: Requirements; + totalContribution: number; + processed: boolean; }; -export type Order = OnChainOrder & { +export type OrderItem = { orderId: number; - totalContribution: number; - contribution: number; + height: number; + extrinsicId: number; + timestamp: Date; + begin: number; + core: number; + mask: string; + account: string; + reward: number; }; diff --git a/src/models/regions/sale.ts b/src/models/regions/sale.ts index ba240610..734c6d23 100644 --- a/src/models/regions/sale.ts +++ b/src/models/regions/sale.ts @@ -106,7 +106,7 @@ export type PurchaseHistoryItem = { extrinsicId: string; timestamp: Date; price: number; - type: string; + type: PurchaseType; }; export type SalesHistoryItem = { diff --git a/src/pages/marketplace.tsx b/src/pages/marketplace.tsx index 3f2e29ec..cbfe4b8e 100644 --- a/src/pages/marketplace.tsx +++ b/src/pages/marketplace.tsx @@ -13,9 +13,11 @@ import { useTheme } from '@mui/material/styles'; import { OnChainRegionId, Region } from 'coretime-utils'; import { useConfirm } from 'material-ui-confirm'; import moment from 'moment'; +import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { useSubmitExtrinsic } from '@/hooks/submitExtrinsic'; +import { enableRegionX } from '@/utils/functions'; import { Balance, @@ -28,6 +30,7 @@ import { useAccounts } from '@/contexts/account'; import { useRegionXApi } from '@/contexts/apis/RegionXApi'; import { ApiState } from '@/contexts/apis/types'; import { useMarket } from '@/contexts/market'; +import { useNetwork } from '@/contexts/network'; import { useToast } from '@/contexts/toast'; import { ContextStatus, Listing, MarketFilterOptions } from '@/models'; @@ -63,7 +66,9 @@ const sortOptions: Option[] = [ const Marketplace = () => { const confirm = useConfirm(); const theme = useTheme(); + const router = useRouter(); + const { network } = useNetwork(); const { state: { activeAccount, activeSigner }, } = useAccounts(); @@ -141,6 +146,12 @@ const Marketplace = () => { }).then(() => unlistRegion(region.getOnChainRegionId())); }; + useEffect(() => { + if (!enableRegionX(network)) { + router.push('/'); + } + }, [network, router]); + useEffect(() => { const checkConditions = (listing: Listing): boolean => { const { region, beginTimestamp, endTimestamp, currentPrice } = listing; diff --git a/src/pages/orders.tsx b/src/pages/orders/index.tsx similarity index 87% rename from src/pages/orders.tsx rename to src/pages/orders/index.tsx index 82a371e2..a60a2b85 100644 --- a/src/pages/orders.tsx +++ b/src/pages/orders/index.tsx @@ -9,8 +9,11 @@ import { Typography, useTheme, } from '@mui/material'; +import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; +import { enableRegionX } from '@/utils/functions'; + import { ActionButton, Balance, @@ -19,12 +22,20 @@ import { OrderCreationModal, } from '@/components'; +import { useAccounts } from '@/contexts/account'; +import { useNetwork } from '@/contexts/network'; import { useOrders } from '@/contexts/orders'; import { ContextStatus, Order } from '@/models'; const OrderDashboard = () => { const theme = useTheme(); + const router = useRouter(); + + const { network } = useNetwork(); const { orders, status } = useOrders(); + const { + state: { activeAccount }, + } = useAccounts(); const [orderCreationModalOpen, openOrderCreationModal] = useState(false); const [expiredOnly, watchExpired] = useState(false); @@ -34,8 +45,13 @@ const OrderDashboard = () => { const [contributionModal, openContributionModal] = useState(false); useEffect(() => { - // TODO: expiry - setOrdersToShow(orders); + if (!enableRegionX(network)) { + router.push('/'); + } + }, [network, router]); + + useEffect(() => { + setOrdersToShow(orders.filter(({ processed }) => !processed)); }, [orders]); return ( @@ -116,6 +132,7 @@ const OrderDashboard = () => { selectOrder(order); openContributionModal(true); }} + disabled={activeAccount === null} > Contribute diff --git a/src/pages/orders/processor.tsx b/src/pages/orders/processor.tsx new file mode 100644 index 00000000..0715617c --- /dev/null +++ b/src/pages/orders/processor.tsx @@ -0,0 +1,69 @@ +import { + Backdrop, + Box, + Card, + CircularProgress, + Stack, + Typography, + useTheme, +} from '@mui/material'; +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +import { useProcessedOrders } from '@/hooks/order'; +import { enableRegionX } from '@/utils/functions'; + +import { OrderProcessorTable } from '@/components'; + +import { useRegionXApi } from '@/contexts/apis'; +import { ApiState } from '@/contexts/apis/types'; +import { useNetwork } from '@/contexts/network'; + +const OrderProcessor = () => { + const theme = useTheme(); + const router = useRouter(); + + const { network } = useNetwork(); + const { + state: { apiState }, + } = useRegionXApi(); + + const { data: processedOrderData, loading: loadingProcessedOrders } = + useProcessedOrders(); + + useEffect(() => { + if (!enableRegionX(network)) { + router.push('/'); + } + }, [network, router]); + + return apiState !== ApiState.READY || loadingProcessedOrders ? ( + + + + ) : ( + + + + + + Order Processor UI + + + See all the orders that were fulfilled + + + + + + + ); +}; + +export default OrderProcessor; diff --git a/src/utils/functions/formatting.ts b/src/utils/functions/formatting.ts index 4185243e..0dd5b836 100644 --- a/src/utils/functions/formatting.ts +++ b/src/utils/functions/formatting.ts @@ -2,7 +2,7 @@ import TimeAgo from 'javascript-time-ago'; import en from 'javascript-time-ago/locale/en'; import moment from 'moment'; -TimeAgo.addDefaultLocale(en); +TimeAgo.addLocale(en); export const getTimeStringShort = (timestamp: number | Date): string => { return moment(timestamp).format('MMM DD HH:mm');