mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-09 20:11:10 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { CoretimeInformation } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
|
||||
import { useCoretimeInformation } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { createGet, estimateTime } from './utils/index.js';
|
||||
|
||||
interface CoretimeContextProps {
|
||||
coretimeInfo: CoretimeInformation | null;
|
||||
get: ReturnType<typeof createGet> | null;
|
||||
saleStartDate: string | null
|
||||
saleEndDate: string | null
|
||||
currentRegionEnd: number | null
|
||||
currentRegionStart: number | null
|
||||
}
|
||||
|
||||
const initState = {
|
||||
coretimeInfo: null,
|
||||
currentRegionEnd: null,
|
||||
currentRegionStart: null,
|
||||
get: null,
|
||||
saleEndDate: null,
|
||||
saleStartDate: null
|
||||
};
|
||||
|
||||
const CoretimeContext = createContext<CoretimeContextProps>(initState);
|
||||
|
||||
export const CoretimeProvider = ({ api,
|
||||
children,
|
||||
isApiReady }: {
|
||||
children: ReactNode;
|
||||
api: ApiPromise;
|
||||
isApiReady: boolean;
|
||||
}) => {
|
||||
const coretimeInfo = useCoretimeInformation(api, isApiReady);
|
||||
const get = useMemo(() => {
|
||||
if (coretimeInfo?.constants) {
|
||||
return createGet(coretimeInfo.constants);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [coretimeInfo?.constants]);
|
||||
|
||||
const currentRegionEnd = useMemo(() => coretimeInfo ? coretimeInfo?.salesInfo.regionEnd - coretimeInfo?.config.regionLength : 0, [coretimeInfo]);
|
||||
const currentRegionStart = useMemo(() => coretimeInfo ? coretimeInfo.salesInfo.regionEnd - coretimeInfo?.config.regionLength * 2 : 0, [coretimeInfo]);
|
||||
|
||||
const saleStartDate = useMemo(() => get && coretimeInfo && estimateTime(currentRegionStart, get.blocks.relay(coretimeInfo?.status?.lastTimeslice), coretimeInfo.constants.relay)?.formattedDate, [currentRegionStart, coretimeInfo, get]);
|
||||
const saleEndDate = useMemo(() => get && coretimeInfo && estimateTime(currentRegionEnd, get.blocks.relay(coretimeInfo?.status?.lastTimeslice), coretimeInfo.constants.relay)?.formattedDate, [currentRegionEnd, coretimeInfo, get]);
|
||||
|
||||
const value = useMemo(() => {
|
||||
if (!coretimeInfo || !currentRegionEnd || !currentRegionStart || !get || !saleEndDate || !saleStartDate) {
|
||||
return initState;
|
||||
}
|
||||
|
||||
return {
|
||||
coretimeInfo: coretimeInfo ?? null,
|
||||
currentRegionEnd,
|
||||
currentRegionStart,
|
||||
get,
|
||||
saleEndDate,
|
||||
saleStartDate
|
||||
};
|
||||
}, [coretimeInfo, currentRegionEnd, currentRegionStart, get, saleEndDate, saleStartDate]);
|
||||
|
||||
return (
|
||||
<CoretimeContext.Provider value={value}>
|
||||
{children}
|
||||
</CoretimeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCoretimeContext = () => useContext(CoretimeContext);
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { TabItem } from '@pezkuwi/react-components/types';
|
||||
import type { RelayName } from './types.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Spinner, Tabs } from '@pezkuwi/react-components';
|
||||
import { useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Overview from './Overview/index.js';
|
||||
import Sale from './Sale/index.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
api: ApiPromise;
|
||||
isApiReady: boolean;
|
||||
}
|
||||
|
||||
function createItemsRef (t: (key: string, options?: { replace: Record<string, unknown> }) => string): TabItem[] {
|
||||
return [
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Overview')
|
||||
},
|
||||
{
|
||||
name: 'sale',
|
||||
text: t('Sale')
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function CoretimePage ({ api, basePath, className, isApiReady }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const itemsRef = useRef(createItemsRef(t));
|
||||
const relayName = useCall<string>(isApiReady && api?.rpc.system.chain)?.toString().toLowerCase() as RelayName;
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
items={itemsRef.current}
|
||||
/>
|
||||
{!relayName
|
||||
? <div><Spinner /></div>
|
||||
: <Routes>
|
||||
<Route path={basePath}>
|
||||
<Route
|
||||
element={<Overview relayName={relayName} />}
|
||||
index
|
||||
/>
|
||||
<Route
|
||||
element={<Sale relayName={relayName} />}
|
||||
path='sale'
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CoretimePage);
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ChainInformation } from '@pezkuwi/react-hooks/types';
|
||||
import type { ActiveFilters } from '../types.js';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, Dropdown, Input } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { FilterType, useBlocksSort, useSearchFilter, useTypeFilter } from './filters/index.js';
|
||||
|
||||
interface Props {
|
||||
chainInfo: Record<number, ChainInformation>;
|
||||
data: number[];
|
||||
onFilter: (data: number[]) => void;
|
||||
}
|
||||
|
||||
function Filters ({ chainInfo, data: initialData, onFilter }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [activeFilters, setActiveFilters] = useState<ActiveFilters>({
|
||||
search: [],
|
||||
type: []
|
||||
});
|
||||
|
||||
const { apply: applyBlocksSort, direction, onApply: onApplySort, reset: resetSort } = useBlocksSort({
|
||||
chainInfo,
|
||||
data: initialData,
|
||||
onFilter: (data) => handleFilter(data, FilterType.BLOCKS)
|
||||
});
|
||||
|
||||
const { apply: applySearchFilter, onApply: onApplySearch, reset: resetSearch, searchValue } = useSearchFilter({
|
||||
data: initialData,
|
||||
onFilter: (data) => handleFilter(data, FilterType.SEARCH)
|
||||
});
|
||||
|
||||
const { apply: applyTypeFilter, onApply: onApplyType, reset: resetType, selectedType, typeOptions } = useTypeFilter({
|
||||
chainInfo,
|
||||
data: initialData,
|
||||
onFilter: (data) => handleFilter(data, FilterType.TYPE)
|
||||
});
|
||||
|
||||
/**
|
||||
* 1. Applies additional filtering already present in the filters
|
||||
* 2. Performs filtering based on the filter type
|
||||
*/
|
||||
const handleFilter = useCallback((
|
||||
filteredData: number[],
|
||||
filterType: FilterType
|
||||
): void => {
|
||||
let resultData = filteredData;
|
||||
|
||||
if (filterType !== FilterType.SEARCH) {
|
||||
resultData = applySearchFilter(resultData, activeFilters.search);
|
||||
}
|
||||
|
||||
if (filterType !== FilterType.TYPE) {
|
||||
resultData = applyTypeFilter(resultData, activeFilters.type);
|
||||
}
|
||||
|
||||
if (filterType !== FilterType.BLOCKS && direction) {
|
||||
resultData = applyBlocksSort(resultData, direction);
|
||||
}
|
||||
|
||||
if (filterType !== FilterType.BLOCKS) {
|
||||
setActiveFilters((prev) => ({
|
||||
...prev,
|
||||
[filterType]: filteredData.length === initialData.length ? [] : filteredData
|
||||
}));
|
||||
}
|
||||
|
||||
onFilter(resultData);
|
||||
}, [initialData, onFilter, activeFilters, direction, applyBlocksSort, applyTypeFilter, applySearchFilter]);
|
||||
|
||||
const resetAllFilters = useCallback(() => {
|
||||
resetSearch();
|
||||
resetType();
|
||||
resetSort();
|
||||
setActiveFilters({ search: [], type: [] });
|
||||
onFilter(initialData);
|
||||
}, [initialData, onFilter, resetSearch, resetType, resetSort]);
|
||||
|
||||
const hasActiveFilters = searchValue || selectedType || direction;
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', flexDirection: 'row', gap: '10px' }}>
|
||||
<div style={{ minWidth: '250px' }}>
|
||||
<Input
|
||||
aria-label={t('Search by teyrchain id or name')}
|
||||
className='full isSmall'
|
||||
label={t('Search')}
|
||||
onChange={onApplySearch}
|
||||
placeholder={t('teyrchain id or name')}
|
||||
value={searchValue}
|
||||
/>
|
||||
</div>
|
||||
<Dropdown
|
||||
className='isSmall'
|
||||
label={t('type')}
|
||||
onChange={onApplyType}
|
||||
options={typeOptions}
|
||||
placeholder='select type'
|
||||
value={selectedType}
|
||||
/>
|
||||
<div style={{ height: '20px' }}>
|
||||
<Button
|
||||
icon={direction ? (direction === 'DESC' ? 'arrow-up' : 'arrow-down') : 'sort'}
|
||||
label={t('blocks')}
|
||||
onClick={onApplySort}
|
||||
/>
|
||||
</div>
|
||||
{hasActiveFilters && (
|
||||
<div style={{ height: '20px' }}>
|
||||
<Button
|
||||
icon='times'
|
||||
label={t('Reset filters')}
|
||||
onClick={resetAllFilters}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Filters;
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BrokerStatus, CoreDescription, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord, RegionInfo } from '@pezkuwi/react-hooks/types';
|
||||
import type { RelayName } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { BN } from '@pezkuwi/util';
|
||||
|
||||
import { useCoretimeContext } from '../CoretimeContext.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { FirstCycleStart } from '../utils/index.js';
|
||||
|
||||
interface Props {
|
||||
coreDscriptors?: CoreDescription[];
|
||||
saleInfo: PalletBrokerSaleInfoRecord
|
||||
config: PalletBrokerConfigRecord,
|
||||
region: RegionInfo[],
|
||||
status: BrokerStatus,
|
||||
teyrchainCount: number
|
||||
relayName: RelayName,
|
||||
}
|
||||
|
||||
function Summary ({ config, teyrchainCount, relayName, status }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { coretimeInfo, currentRegionEnd, currentRegionStart, saleEndDate, saleStartDate } = useCoretimeContext();
|
||||
|
||||
const saleNumber = useMemo(() => {
|
||||
if (relayName && currentRegionEnd) {
|
||||
return Math.floor(
|
||||
(currentRegionEnd - FirstCycleStart.timeslice.coretime[relayName]) / config.regionLength
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [currentRegionEnd, relayName, config]);
|
||||
|
||||
const timeslicesSinceCycleStart = useMemo(() => currentRegionEnd && new BN(config?.regionLength).sub((new BN(currentRegionEnd)).sub(new BN(status.lastTimeslice))), [status, config, currentRegionEnd]);
|
||||
|
||||
return (
|
||||
<SummaryBox>
|
||||
<section>
|
||||
{status &&
|
||||
<CardSummary label={t('sale number')}>
|
||||
<div>
|
||||
{saleNumber}
|
||||
</div>
|
||||
</CardSummary>
|
||||
}
|
||||
<CardSummary label={t('timeslice')}>
|
||||
{status?.lastTimeslice}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('teyrchains')}>
|
||||
{teyrchainCount && teyrchainCount}
|
||||
</CardSummary>
|
||||
{config && status && currentRegionEnd && saleEndDate && saleStartDate && timeslicesSinceCycleStart && coretimeInfo?.constants &&
|
||||
<>
|
||||
<CardSummary
|
||||
className='media--800'
|
||||
label={t('timeslice progress')}
|
||||
progress={{
|
||||
hideGraph: true,
|
||||
hideValue: false,
|
||||
isBlurred: false,
|
||||
total: new BN(config?.regionLength),
|
||||
value: timeslicesSinceCycleStart,
|
||||
withTime: false
|
||||
}}
|
||||
/>
|
||||
<CardSummary
|
||||
label={t('cycle')}
|
||||
progress={{
|
||||
total: new BN(config.regionLength).mul(new BN(coretimeInfo?.constants.relay.blocksPerTimeslice)),
|
||||
value: timeslicesSinceCycleStart.mul(new BN(coretimeInfo?.constants.relay.blocksPerTimeslice)),
|
||||
withTime: true
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
}
|
||||
</section>
|
||||
<section className='media--1200'>
|
||||
<CardSummary label={t('sale dates')}>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px' }}>{saleStartDate}</div>
|
||||
<div style={{ fontSize: '14px' }}>{saleEndDate}</div>
|
||||
</div>
|
||||
</CardSummary>
|
||||
<CardSummary label={t('sale ts')}>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px' }}>{currentRegionStart}</div>
|
||||
<div style={{ fontSize: '14px' }}>{currentRegionEnd}</div>
|
||||
</div>
|
||||
</CardSummary>
|
||||
|
||||
</section>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export * from '../../types.js';
|
||||
export * from './useBlockSort.js';
|
||||
export * from './useSearchFilter.js';
|
||||
export * from './useTypeFilter.js';
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ChainInformation } from '@pezkuwi/react-hooks/types';
|
||||
import type { ChainInfoFilterProps, SortDirection } from '../../types.js';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export function sortByBlocks (data: number[], chainInfo: Record<number, ChainInformation>, direction: SortDirection): number[] {
|
||||
if (!data || !chainInfo || !direction) {
|
||||
return data || [];
|
||||
}
|
||||
|
||||
const filteredData = data.filter((block) => !!chainInfo[block]?.workTaskInfo[0]);
|
||||
|
||||
return [...filteredData].sort((a, b) => {
|
||||
const aInfo = chainInfo[a]?.workTaskInfo[0];
|
||||
const bInfo = chainInfo[b]?.workTaskInfo[0];
|
||||
|
||||
return direction === 'DESC'
|
||||
? bInfo.lastBlock - aInfo.lastBlock
|
||||
: aInfo.lastBlock - bInfo.lastBlock;
|
||||
});
|
||||
}
|
||||
|
||||
const getNextSortState = (current: SortDirection): SortDirection =>
|
||||
({ '': 'DESC', ASC: '', DESC: 'ASC' } as const)[current];
|
||||
|
||||
export function useBlocksSort ({ chainInfo, data, onFilter }: ChainInfoFilterProps) {
|
||||
const [direction, setDirection] = useState<SortDirection>('');
|
||||
|
||||
const apply = useCallback((data: number[], sort: SortDirection): number[] => {
|
||||
return sort
|
||||
? sortByBlocks(data, chainInfo, sort)
|
||||
: data;
|
||||
}, [chainInfo]);
|
||||
|
||||
const onApply = useCallback(() => {
|
||||
const nextDirection = getNextSortState(direction);
|
||||
|
||||
setDirection(nextDirection);
|
||||
onFilter(nextDirection ? sortByBlocks(data, chainInfo, nextDirection) : data);
|
||||
}, [data, chainInfo, onFilter, direction]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setDirection('');
|
||||
onFilter(data || []);
|
||||
}, [data, onFilter]);
|
||||
|
||||
return {
|
||||
apply,
|
||||
direction,
|
||||
onApply,
|
||||
reset
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useRelayEndpoints } from '@pezkuwi/react-hooks/useParaEndpoints';
|
||||
|
||||
interface UseSearchFilterProps {
|
||||
data: number[];
|
||||
onFilter: (data: number[]) => void;
|
||||
}
|
||||
|
||||
export function useSearchFilter ({ data, onFilter }: UseSearchFilterProps) {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const endpoints = useRelayEndpoints();
|
||||
const endPointsMap = useMemo(() =>
|
||||
Object.fromEntries(
|
||||
endpoints
|
||||
.filter((e) => e?.text && e.paraId)
|
||||
.map((e) => [
|
||||
React.isValidElement(e.text) ? '' : String(e.text),
|
||||
e.paraId
|
||||
])
|
||||
),
|
||||
[endpoints]
|
||||
);
|
||||
|
||||
const apply = useCallback((data: number[], activeSearch: number[]): number[] => {
|
||||
return activeSearch.length > 0
|
||||
? data.filter((id) => activeSearch.includes(id))
|
||||
: data;
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setSearchValue('');
|
||||
onFilter(data);
|
||||
}, [data, onFilter]);
|
||||
|
||||
const onInputChange = useCallback((v: string) => {
|
||||
setSearchValue(v);
|
||||
const searchLower = v.trim().toLowerCase();
|
||||
|
||||
if (!searchLower) {
|
||||
onFilter(data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingIds = new Set<number>();
|
||||
|
||||
for (const item of data) {
|
||||
const itemStr = item.toString().toLowerCase();
|
||||
|
||||
if (itemStr.includes(searchLower)) {
|
||||
matchingIds.add(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(endPointsMap)) {
|
||||
if (key.toLowerCase().includes(searchLower) && value === item) {
|
||||
matchingIds.add(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filteredData = Array.from(matchingIds);
|
||||
|
||||
onFilter(apply(data, filteredData));
|
||||
}, [data, endPointsMap, onFilter, apply]);
|
||||
|
||||
return {
|
||||
apply,
|
||||
onApply: onInputChange,
|
||||
reset,
|
||||
searchValue
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { FlagColor } from '@pezkuwi/react-components/types';
|
||||
import type { ChainInfoFilterProps } from '../../types.js';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Tag } from '@pezkuwi/react-components';
|
||||
import { CoreTimeTypes } from '@pezkuwi/react-hooks/constants';
|
||||
|
||||
import { coretimeTypeColours } from '../../utils/index.js';
|
||||
|
||||
const coretimeTypes = Object.keys(CoreTimeTypes).filter((key) => isNaN(Number(key)));
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
text: 'All',
|
||||
value: 'All'
|
||||
},
|
||||
...coretimeTypes.map((type) => ({
|
||||
text: (
|
||||
<Tag
|
||||
color={coretimeTypeColours[CoreTimeTypes[type as keyof typeof CoreTimeTypes]] as FlagColor}
|
||||
label={type}
|
||||
/>
|
||||
),
|
||||
value: CoreTimeTypes[type as keyof typeof CoreTimeTypes].toString()
|
||||
}))
|
||||
];
|
||||
|
||||
export function useTypeFilter ({ chainInfo, data, onFilter }: ChainInfoFilterProps) {
|
||||
const [selectedType, setSelectedType] = useState<string>('');
|
||||
const [activeType, setActiveType] = useState<number[]>([]);
|
||||
|
||||
const apply = useCallback((data: number[], activeType: number[]): number[] => {
|
||||
return activeType.length > 0
|
||||
? data.filter((id) => activeType.includes(id))
|
||||
: data;
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setSelectedType('');
|
||||
setActiveType([]);
|
||||
onFilter(data);
|
||||
}, [data, onFilter]);
|
||||
|
||||
const onDropDownChange = useCallback((v: string) => {
|
||||
setSelectedType(v);
|
||||
|
||||
if (!v || v === 'All') {
|
||||
setActiveType([]);
|
||||
onFilter(data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredData = data.filter((paraId) => {
|
||||
const taskInfo = chainInfo[paraId]?.workTaskInfo;
|
||||
|
||||
return taskInfo?.length > 0 && taskInfo[0].type.toString() === v;
|
||||
});
|
||||
|
||||
setActiveType(filteredData);
|
||||
onFilter(apply(data, filteredData));
|
||||
}, [chainInfo, data, onFilter, apply]);
|
||||
|
||||
return {
|
||||
activeType,
|
||||
apply,
|
||||
onApply: onDropDownChange,
|
||||
reset,
|
||||
selectedType,
|
||||
typeOptions
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { RelayName } from '../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useCoretimeContext } from '../CoretimeContext.js';
|
||||
import TeyrchainsTable from '../TeyrchainsTable.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
relayName: RelayName
|
||||
}
|
||||
|
||||
function Overview ({ className, relayName }: Props): React.ReactElement<Props> {
|
||||
const { coretimeInfo } = useCoretimeContext();
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
{coretimeInfo && (
|
||||
<Summary
|
||||
config={coretimeInfo?.config}
|
||||
teyrchainCount={coretimeInfo.taskIds?.length || 0}
|
||||
region={coretimeInfo?.region}
|
||||
relayName={relayName}
|
||||
saleInfo={coretimeInfo?.salesInfo}
|
||||
status={coretimeInfo?.status}
|
||||
/>
|
||||
)}
|
||||
{!!coretimeInfo &&
|
||||
<TeyrchainsTable
|
||||
coretimeInfo={coretimeInfo}
|
||||
relayName={relayName}
|
||||
/>
|
||||
}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Overview);
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { FlagColor } from '@pezkuwi/react-components/types';
|
||||
import type { ChainWorkTaskInformation, LegacyLease } from '@pezkuwi/react-hooks/types';
|
||||
import type { RelayName } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { MarkWarning, ParaLink, styled, Tag } from '@pezkuwi/react-components';
|
||||
import { ParaLinkType } from '@pezkuwi/react-components/ParaLink';
|
||||
import { ChainRenewalStatus, CoreTimeTypes } from '@pezkuwi/react-hooks/constants';
|
||||
import { BN, formatBalance, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { coretimeTypeColours, estimateTime } from './utils/index.js';
|
||||
import { useCoretimeContext } from './CoretimeContext.js';
|
||||
|
||||
interface Props {
|
||||
id: number
|
||||
chainRecord: ChainWorkTaskInformation
|
||||
regionEnd: number
|
||||
regionBegin: number
|
||||
lastCommittedTimeslice: number
|
||||
lease: LegacyLease | undefined
|
||||
highlight?: boolean
|
||||
relayName: RelayName
|
||||
}
|
||||
|
||||
interface StyledCellProps {
|
||||
$p: boolean;
|
||||
$width?: string;
|
||||
}
|
||||
|
||||
const StyledCell = styled.td<StyledCellProps>`
|
||||
&& {
|
||||
background-color: ${({ $p }) => ($p ? '#F9FAFB' : undefined)};
|
||||
width: ${({ $width }) => $width};
|
||||
}
|
||||
height: 55px;
|
||||
`;
|
||||
|
||||
const StyledMarkWarning = styled(MarkWarning)`
|
||||
width: fit-content;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
&.mark {
|
||||
margin: 0 0 0 1rem;
|
||||
display: inline;
|
||||
}
|
||||
`;
|
||||
|
||||
const EXPIRES_IN_DAYS = 7;
|
||||
|
||||
function Row ({ chainRecord, highlight = false, id, lastCommittedTimeslice, lease, regionBegin, regionEnd, relayName }: Props): React.ReactElement<Props> {
|
||||
// Group status checks
|
||||
const { renewalStatus } = chainRecord;
|
||||
const isRenewed = renewalStatus === ChainRenewalStatus.Renewed;
|
||||
const isEligible = renewalStatus === ChainRenewalStatus.Eligible;
|
||||
const chainRegionEnd = isRenewed ? regionEnd : regionBegin;
|
||||
|
||||
const renewalLink = isEligible && (
|
||||
<a
|
||||
href={`https://app.regionx.tech/renew?network=${relayName}¶Id=${id}&core=${chainRecord?.workload?.core}`}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Renew on RegionX
|
||||
</a>
|
||||
);
|
||||
|
||||
const renewalValue = isRenewed
|
||||
? chainRecord.renewalStatusMessage
|
||||
: (isEligible ? renewalLink : '-');
|
||||
|
||||
const targetTimeslice = lease?.until || chainRegionEnd;
|
||||
const showEstimates = !!targetTimeslice && Object.values(CoreTimeTypes)[chainRecord.type] !== CoreTimeTypes.Reservation;
|
||||
const { coretimeInfo, get } = useCoretimeContext();
|
||||
|
||||
const estimatedTime = showEstimates && get && coretimeInfo && estimateTime(targetTimeslice, get.blocks.relay(lastCommittedTimeslice), coretimeInfo?.constants?.relay);
|
||||
|
||||
const isWithinWeek = !!estimatedTime && new Date(estimatedTime.timestamp).getTime() - Date.now() < EXPIRES_IN_DAYS * 24 * 60 * 60 * 1000;
|
||||
const isReservation = chainRecord.type === CoreTimeTypes.Reservation;
|
||||
|
||||
return (
|
||||
<React.Fragment key={`${id}`}>
|
||||
<StyledCell
|
||||
$p={highlight}
|
||||
$width='150px'
|
||||
>{id}</StyledCell>
|
||||
<StyledCell
|
||||
$p={highlight}
|
||||
$width='150px'
|
||||
className='media--800'
|
||||
>{<ParaLink id={new BN(id)} />}</StyledCell>
|
||||
<StyledCell $p={highlight}>{chainRecord?.workload?.core}</StyledCell>
|
||||
<StyledCell $p={highlight}>
|
||||
<Tag
|
||||
color={coretimeTypeColours[chainRecord.type] as FlagColor}
|
||||
label={Object.values(CoreTimeTypes)[chainRecord.type]}
|
||||
/>
|
||||
</StyledCell>
|
||||
<StyledCell
|
||||
$p={highlight}
|
||||
className='media--800'
|
||||
>{showEstimates && chainRecord?.lastBlock && relayName &&
|
||||
<a
|
||||
href={`https://${relayName.split(' ')[0]}.subscan.io/block/${chainRecord?.lastBlock}`}
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>{formatNumber(chainRecord?.lastBlock)}
|
||||
</a>}
|
||||
</StyledCell>
|
||||
<StyledCell
|
||||
$p={highlight}
|
||||
className='media--1000'
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<span>
|
||||
{estimatedTime && estimatedTime?.formattedDate}
|
||||
{!!isWithinWeek && !isReservation && chainRecord?.renewalStatus !== ChainRenewalStatus.Renewed && (
|
||||
<StyledMarkWarning
|
||||
content='Expires Soon'
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</StyledCell>
|
||||
<StyledCell
|
||||
$p={highlight}
|
||||
className='media--1200'
|
||||
>{renewalValue}</StyledCell>
|
||||
<StyledCell
|
||||
$p={highlight}
|
||||
className='media--1200'
|
||||
>{chainRecord?.renewal ? formatBalance(chainRecord.renewal?.price.toString()) : ''}</StyledCell>
|
||||
<StyledCell
|
||||
$p={highlight}
|
||||
className='media--800'
|
||||
>{<div style={{ alignItems: 'center', columnGap: '12px', display: 'flex', flexDirection: 'row', justifyContent: 'left' }}>
|
||||
<ParaLink
|
||||
id={new BN(id)}
|
||||
showLogo={false}
|
||||
type={ParaLinkType.SUBSCAN}
|
||||
/>
|
||||
<div style={{ marginBottom: '2px' }}>
|
||||
<ParaLink
|
||||
id={new BN(id)}
|
||||
showLogo={false}
|
||||
type={ParaLinkType.HOME}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
</StyledCell>
|
||||
{highlight && <StyledCell $p={highlight} />}
|
||||
</React.Fragment>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Row);
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PhaseConfig } from '../types.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { styled, Table } from '@pezkuwi/react-components';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
phaseInfo: PhaseConfig['config'][keyof PhaseConfig['config']]
|
||||
}
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
border-collapse: collapse;
|
||||
|
||||
tr:last-child {
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
`;
|
||||
|
||||
function SaleTable ({ phaseInfo }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[],
|
||||
[t('Dates'), 'start media--800'],
|
||||
[t('Blocks (relay)'), 'start'],
|
||||
[t('Timeslices'), 'start']
|
||||
]);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '500px' }}>
|
||||
<StyledTable
|
||||
emptySpinner={false}
|
||||
header={headerRef.current}
|
||||
isSplit={false}
|
||||
>
|
||||
<tr>
|
||||
<td style={{ width: '100px' }}>Start</td>
|
||||
<td>{phaseInfo.start.date}</td>
|
||||
<td>{formatNumber(phaseInfo.start.blocks.relay)}</td>
|
||||
<td>{formatNumber(phaseInfo.start.ts)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ width: '100px' }}>End</td>
|
||||
<td>{phaseInfo.end.date}</td>
|
||||
<td>{formatNumber(phaseInfo.end.blocks.relay)}</td>
|
||||
<td>{formatNumber(phaseInfo.end.ts)}</td>
|
||||
</tr>
|
||||
|
||||
</StyledTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(SaleTable);
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { RelayName, SaleParameters } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-components';
|
||||
|
||||
import { PhaseName } from '../constants.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import PhaseTable from './PhaseTable.js';
|
||||
import { SubscanModuleCallUrl } from './SubscanModuleCallUrl.js';
|
||||
|
||||
const ResponsiveContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
gap: 10rem;
|
||||
margin-top: 2rem;
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.h3`
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const LinkWithLogo = ({ alt, href, logo }: { href: string, logo: string, alt: string }) => {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<img
|
||||
alt={alt}
|
||||
height={25}
|
||||
src={logo}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const providers = {
|
||||
lastic: {
|
||||
alt: 'Lastic',
|
||||
href: (chainName: string) => `https://www.lastic.xyz/${chainName}/bulkcore1`,
|
||||
logo: 'https://www.lastic.xyz/_next/image?url=%2Fassets%2FImages%2FLogos%2Flastic-logo.png&w=384&q=100'
|
||||
},
|
||||
regionx: {
|
||||
alt: 'RegionX',
|
||||
href: (chainName: string) => `https://app.regionx.tech/?network=${chainName}`,
|
||||
logo: 'https://app.regionx.tech/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Flogo.8f0fd171.png&w=3840&q=75'
|
||||
},
|
||||
subscan: {
|
||||
alt: 'Subscan',
|
||||
href: (chainName: string) => `https://coretime-${chainName}.subscan.io/coretime_dashboard`,
|
||||
logo: 'https://www.subscan.io/_next/image?url=%2Fwebsite%2Flogo-light.png&w=256&q=75'
|
||||
}
|
||||
};
|
||||
|
||||
const phases = {
|
||||
[PhaseName.Renewals]: {
|
||||
description: 'In this phase, core owners can renew existing cores at a fixed price to ensure continued operation in the next region. No new core purchases are permitted.',
|
||||
name: 'Interlude/Renewals phase'
|
||||
},
|
||||
[PhaseName.PriceDiscovery]: {
|
||||
description: 'The period during which cores are available for both purchase and renewal. The price decreases linearly over time.',
|
||||
name: 'Price Discovery phase'
|
||||
},
|
||||
[PhaseName.FixedPrice]: {
|
||||
description: 'The period during which cores are available for both purchase and renewal. The price remains fixed towards the end of the sales period.',
|
||||
name: 'Fixed price phase'
|
||||
}
|
||||
};
|
||||
|
||||
const dotLakeUrl = 'https://data.parity.io/coretime';
|
||||
|
||||
const SaleDetailsView = ({ chosenSaleNumber, relayName, saleParams }: { saleParams: SaleParameters, chosenSaleNumber: number, relayName: RelayName }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const subscanPriceGraphUrl = useMemo(() =>
|
||||
`https://coretime-${relayName}.subscan.io/coretime_dashboard`
|
||||
, [relayName]);
|
||||
|
||||
if (chosenSaleNumber === -1 || !saleParams) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer>
|
||||
<div>
|
||||
<Title>Sale phases</Title>
|
||||
<div style={{ display: 'grid', gap: '1rem', gridTemplateRows: '1fr 1fr 1fr', minWidth: '200px' }}>
|
||||
{!saleParams?.phaseConfig &&
|
||||
<div>
|
||||
<p>{t(`This sale is of unusual length of ${saleParams.currentRegion.end.ts - saleParams.currentRegion.start.ts} timeslices, hence the regular phases are not applicable.`)}</p>
|
||||
<p>{t(`Sale start timeslice: ${saleParams.currentRegion.start.ts}`)}</p>
|
||||
<p>{t(`Sale end timeslice: ${saleParams.currentRegion.end.ts}`)}</p>
|
||||
</div>
|
||||
}
|
||||
{saleParams?.phaseConfig && Object.entries(phases).map(([phase, { description, name }]) => (
|
||||
<div key={phase}>
|
||||
<h4>{t(name)}</h4>
|
||||
<p style={{ maxWidth: '600px', opacity: '0.8' }}>{t(description)}</p>
|
||||
{saleParams?.phaseConfig &&
|
||||
<PhaseTable
|
||||
phaseInfo={saleParams?.phaseConfig.config[phase as keyof typeof saleParams.phaseConfig.config]}
|
||||
/>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Title>{t('Region for sale ')}</Title>
|
||||
<p style={{ maxWidth: '600px', opacity: '0.8' }}>{t('Region is an asset of Coretime. It signifies the upcoming sales period within which a core can be secured by purchasing coretime. Acquiring coretime grants access to a core for the duration of that specific region.')}</p>
|
||||
{saleParams?.regionForSale && <PhaseTable phaseInfo={saleParams?.regionForSale} />}
|
||||
<Title>{t('Subscan Links')}</Title>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<a
|
||||
href={subscanPriceGraphUrl}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>Sale Purchase Graph</a>
|
||||
<SubscanModuleCallUrl
|
||||
chainName={relayName}
|
||||
chosenSaleNumber={chosenSaleNumber}
|
||||
currentRegion={saleParams.currentRegion}
|
||||
urlTitle='Sale Purchase Transactions'
|
||||
/>
|
||||
<SubscanModuleCallUrl
|
||||
call={'renew'}
|
||||
chainName={relayName}
|
||||
chosenSaleNumber={chosenSaleNumber}
|
||||
currentRegion={saleParams.currentRegion}
|
||||
urlTitle='Sale Renewal Transactions'
|
||||
/>
|
||||
</div>
|
||||
<Title>{t('DotLake Coretime Dashboard')}</Title>
|
||||
<a
|
||||
href={dotLakeUrl}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>Dot Lake</a>
|
||||
<Title>{t('Coretime providers')}</Title>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
{Object.entries(providers).map(([provider, { alt, href, logo }]) => (
|
||||
<LinkWithLogo
|
||||
alt={alt}
|
||||
href={href(relayName)}
|
||||
key={provider}
|
||||
logo={logo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaleDetailsView;
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SaleParameters } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { constructSubscanQuery } from '../utils/index.js';
|
||||
|
||||
function formatDate (input: string) {
|
||||
const date = new Date(input + ' UTC');
|
||||
|
||||
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
||||
}
|
||||
|
||||
export const SubscanModuleCallUrl = ({ call = 'purchase', chainName, chosenSaleNumber, currentRegion, module = 'broker', urlTitle }: { chosenSaleNumber: number, currentRegion: SaleParameters['currentRegion'], chainName: string, urlTitle: string, module?: string, call?: string }) => {
|
||||
const url = useMemo(() => constructSubscanQuery(formatDate(currentRegion.start.date), formatDate(currentRegion.end.date), chainName, module, call), [currentRegion, chainName, module, call]);
|
||||
|
||||
if (chosenSaleNumber === -1 || !currentRegion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>{urlTitle}</a>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { BrokerStatus, CoreDescription, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord, RegionInfo } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { BN, formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useCoretimeContext } from '../CoretimeContext.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { getCurrentRegionStartEndTs } from '../utils/index.js';
|
||||
|
||||
interface Props {
|
||||
api: ApiPromise | null,
|
||||
coreDscriptors?: CoreDescription[];
|
||||
saleInfo: PalletBrokerSaleInfoRecord
|
||||
config: PalletBrokerConfigRecord,
|
||||
region: RegionInfo[],
|
||||
status: BrokerStatus,
|
||||
saleNumber: number,
|
||||
}
|
||||
|
||||
function Summary ({ config, saleInfo, saleNumber, status }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { currentRegionEndTs, currentRegionStartTs } = getCurrentRegionStartEndTs(saleInfo, config.regionLength);
|
||||
const { get, saleEndDate, saleStartDate } = useCoretimeContext();
|
||||
|
||||
return (
|
||||
<SummaryBox>
|
||||
<section>
|
||||
{status &&
|
||||
<CardSummary label={t('sale number')}>
|
||||
<div>
|
||||
{saleNumber > -1 ? saleNumber : '-'}
|
||||
</div>
|
||||
</CardSummary>
|
||||
}
|
||||
<CardSummary label={t('sold/offered')}>
|
||||
{`${saleInfo?.coresSold} / ${saleInfo?.coresOffered}`}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('sale end')}>
|
||||
<div>{saleEndDate}</div>
|
||||
</CardSummary>
|
||||
<CardSummary label={t('last block')}>
|
||||
<div>{get && formatNumber(get.blocks.relay(currentRegionEndTs))}</div>
|
||||
</CardSummary>
|
||||
<CardSummary label={t('last timeslice')}>
|
||||
<div>{formatNumber(currentRegionEndTs)}</div>
|
||||
</CardSummary>
|
||||
{config && status &&
|
||||
<CardSummary
|
||||
className='media--800'
|
||||
label={t('sale progress')}
|
||||
progress={{
|
||||
isBlurred: false,
|
||||
total: new BN(config?.regionLength),
|
||||
value: new BN(config?.regionLength - (currentRegionEndTs - status.lastTimeslice)),
|
||||
withTime: false
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</section>
|
||||
<section className='media--1200'>
|
||||
{status &&
|
||||
(<CardSummary label={t('current region dates')}>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px' }}>{saleStartDate}</div>
|
||||
<div style={{ fontSize: '14px' }}>{saleEndDate}</div>
|
||||
</div>
|
||||
</CardSummary>)
|
||||
}
|
||||
{status &&
|
||||
<CardSummary label={t('region ts')}>
|
||||
<div>
|
||||
<div style={{ fontSize: '14px' }}>{currentRegionStartTs}</div>
|
||||
<div style={{ fontSize: '14px' }}>{currentRegionEndTs}</div>
|
||||
</div>
|
||||
</CardSummary>
|
||||
}
|
||||
</section>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CoretimeInformation } from '@pezkuwi/react-hooks/types';
|
||||
import type { BlockNumber } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { PhaseName } from '../../constants.js';
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import { formatBNToBalance, getCorePriceAt } from '../../utils/sale.js';
|
||||
import { WhiteBox } from '../../WhiteBox.js';
|
||||
|
||||
export const Cores = ({ color, phaseName, salesInfo }: { phaseName: string, salesInfo: CoretimeInformation['salesInfo'], color: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { apiCoretime } = useApi();
|
||||
const bestNumberFinalized = useCall<BlockNumber>(apiCoretime?.derive.chain.bestNumberFinalized);
|
||||
const coretimePrice = useMemo(() => bestNumberFinalized && salesInfo && getCorePriceAt(bestNumberFinalized.toNumber(), salesInfo), [salesInfo, bestNumberFinalized]);
|
||||
|
||||
const formattedCoretimePrice = useMemo(() => {
|
||||
return !coretimePrice ? null : formatBNToBalance(coretimePrice);
|
||||
}, [coretimePrice]);
|
||||
|
||||
const CoresWrapper = styled(WhiteBox)`
|
||||
justify-self: flex-end;
|
||||
|
||||
@media (max-width: 1150px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<CoresWrapper>
|
||||
<p style={{ fontSize: '16px', fontWeight: 'bold' }}>Cores</p>
|
||||
{phaseName === PhaseName.Renewals
|
||||
? (
|
||||
<h4>{t('Cores cannot be purchased now')}</h4>
|
||||
)
|
||||
: (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div>
|
||||
<p style={{ fontSize: '14px', marginBottom: '0.15rem', opacity: '0.8' }}>{t('current price')}</p>
|
||||
<p style={{ color: `${color}`, fontSize: '20px' }}>{formattedCoretimePrice}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style={{ fontSize: '14px', marginBottom: '0.15rem', opacity: '0.8' }}>{t('available cores')}</p>
|
||||
<p style={{ fontSize: '20px' }}> {salesInfo.coresOffered - salesInfo.coresSold}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</CoresWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SaleParameters } from 'page-coretime/src/types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-components';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import { WhiteBox } from '../../WhiteBox.js';
|
||||
|
||||
const RegionWrapper = styled(WhiteBox)`
|
||||
justify-self: center;
|
||||
|
||||
@media (min-width: 1150px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Region = ({ regionForSale }: { regionForSale: SaleParameters['regionForSale'] }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<RegionWrapper>
|
||||
<p style={{ fontSize: '16px', fontWeight: 'bold' }}>{t('Region for sale')}</p>
|
||||
{regionForSale &&
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div>
|
||||
<p style={{ fontSize: '14px', marginBottom: '0.15rem', opacity: '0.8' }}>period</p>
|
||||
<p style={{ fontSize: '20px' }}>{regionForSale.start.date} - {regionForSale.end.date}</p>
|
||||
</div>
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<p style={{ fontSize: '14px', marginBottom: '0.15rem', opacity: '0.8' }}>{t('relay chain blocks')}</p>
|
||||
<p style={{ fontSize: '20px' }}>{formatNumber(regionForSale.start.blocks.relay)} - {formatNumber(regionForSale.end.blocks.relay)}</p>
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
</RegionWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SaleParameters } from 'page-coretime/src/types.js';
|
||||
import type { ProgressBarSection } from '@pezkuwi/react-components/types';
|
||||
import type { CoretimeInformation } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { CardSummary, ProgressBar, styled, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import { formatBNToBalance, getCorePriceAt, getSaleProgress } from '../../utils/sale.js';
|
||||
import { WhiteBox } from '../../WhiteBox.js';
|
||||
|
||||
interface TimelineProps {
|
||||
phaseName: string;
|
||||
saleParams: SaleParameters;
|
||||
coretimeInfo: {
|
||||
salesInfo: CoretimeInformation['salesInfo'];
|
||||
status: CoretimeInformation['status'];
|
||||
};
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const Timeline = ({ color, coretimeInfo: { salesInfo, status }, phaseName, saleParams }: TimelineProps) => {
|
||||
const { t } = useTranslation();
|
||||
const progressValues = useMemo(() => saleParams && salesInfo.regionBegin &&
|
||||
getSaleProgress(
|
||||
status.lastTimeslice,
|
||||
saleParams.currentRegion.start.ts,
|
||||
saleParams.interlude.ts,
|
||||
saleParams.leadin.ts,
|
||||
salesInfo.regionBegin),
|
||||
[saleParams, status.lastTimeslice, salesInfo.regionBegin]);
|
||||
|
||||
const coretimePriceStart = useMemo(() => salesInfo && getCorePriceAt(salesInfo.saleStart, salesInfo), [salesInfo]);
|
||||
|
||||
const startPrice = useMemo(() => coretimePriceStart && formatBNToBalance(coretimePriceStart), [coretimePriceStart]);
|
||||
const endPrice = useMemo(() => salesInfo?.endPrice && formatBNToBalance(salesInfo.endPrice), [salesInfo?.endPrice]);
|
||||
|
||||
return (
|
||||
<TimelineWrapper>
|
||||
<p style={{ fontSize: '16px', fontWeight: 'bold' }}>{t('Sale timeline')}</p>
|
||||
<StyledSummaryBox>
|
||||
<GridLayout>
|
||||
{phaseName && <>
|
||||
<CardSummary label='current phase'>{phaseName}</CardSummary>
|
||||
<CardSummary label='current phase end'>{saleParams?.phaseConfig?.config[phaseName as keyof typeof saleParams.phaseConfig.config].end.date}</CardSummary>
|
||||
<CardSummary label='last phase block'>{formatNumber(saleParams?.phaseConfig?.config[phaseName as keyof typeof saleParams.phaseConfig.config].end.blocks.relay)}</CardSummary>
|
||||
</>}
|
||||
<CardSummary label='start price'>{startPrice}</CardSummary>
|
||||
<CardSummary label='fixed price'>{endPrice}</CardSummary>
|
||||
</GridLayout>
|
||||
</StyledSummaryBox>
|
||||
<ProgressBar
|
||||
color={color}
|
||||
sections={progressValues as ProgressBarSection[] ?? []}
|
||||
/>
|
||||
</TimelineWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const TimelineWrapper = styled(WhiteBox)`
|
||||
justify-self: flex-start;
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSummaryBox = styled(SummaryBox)`
|
||||
margin-top: 0;
|
||||
`;
|
||||
|
||||
const GridLayout = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 0.5rem;
|
||||
row-gap: 1rem;
|
||||
width: 100%;
|
||||
|
||||
/* Override CardSummary styling to align text left and remove padding */
|
||||
article {
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
|
||||
> .ui--Labelled {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { RelayName, SaleParameters } from '../types.js';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Dropdown, styled } from '@pezkuwi/react-components';
|
||||
import { defaultHighlight } from '@pezkuwi/react-components/styles';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useCoretimeContext } from '../CoretimeContext.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { getSaleParameters } from '../utils/sale.js';
|
||||
import { Cores } from './boxes/Cores.js';
|
||||
import { Region } from './boxes/Region.js';
|
||||
import { Timeline } from './boxes/Timeline.js';
|
||||
import SaleDetailsView from './SaleDetailsView.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
relayName: RelayName
|
||||
}
|
||||
|
||||
const ResponsiveGrid = styled.div`
|
||||
display: grid;
|
||||
align-items: stretch;
|
||||
gap: 2rem;
|
||||
grid-template-rows: auto auto;
|
||||
margin-top: 4rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1150px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
> *:nth-child(3) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1150px) {
|
||||
grid-template-columns: 1fr 1fr 3fr;
|
||||
}
|
||||
`;
|
||||
|
||||
function Sale ({ relayName }: Props): React.ReactElement<Props> {
|
||||
const { coretimeInfo } = useCoretimeContext();
|
||||
const { api, apiEndpoint, isApiReady } = useApi();
|
||||
const { t } = useTranslation();
|
||||
const lastCommittedTimeslice = coretimeInfo?.status?.lastTimeslice;
|
||||
const [chosenSaleNumber, setChosenSaleNumber] = useState<number>(-1);
|
||||
const [saleParams, setSaleParams] = useState<SaleParameters | null>(null);
|
||||
const [selectedSaleParams, setSelectedSaleParams] = useState<SaleParameters | null>(null);
|
||||
const apiColor = apiEndpoint?.ui.color || defaultHighlight;
|
||||
|
||||
const saleNumberOptions = useMemo(() => [
|
||||
{
|
||||
text: t('Pick a sale number'),
|
||||
value: -1
|
||||
},
|
||||
...Array.from({ length: saleParams?.saleNumber ?? 0 }, (_, i) => ({
|
||||
text: `sale #${i + 1}`,
|
||||
value: i
|
||||
})).reverse()
|
||||
], [saleParams?.saleNumber, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (coretimeInfo && !saleParams) {
|
||||
setSaleParams(getSaleParameters(
|
||||
coretimeInfo,
|
||||
relayName,
|
||||
lastCommittedTimeslice ?? 0
|
||||
));
|
||||
}
|
||||
}, [coretimeInfo, saleParams, lastCommittedTimeslice, relayName]);
|
||||
|
||||
const phaseName = useMemo(() => saleParams?.phaseConfig?.currentPhaseName, [saleParams]);
|
||||
|
||||
const onDropDownChange = useCallback((value: number) => {
|
||||
setChosenSaleNumber(value);
|
||||
|
||||
if (value !== -1) {
|
||||
if (!coretimeInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedSaleParams(getSaleParameters(coretimeInfo, relayName, lastCommittedTimeslice ?? 0, value));
|
||||
}
|
||||
}, [coretimeInfo, relayName, lastCommittedTimeslice]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{coretimeInfo && !!saleParams?.saleNumber &&
|
||||
<Summary
|
||||
api={isApiReady ? api : null}
|
||||
config={coretimeInfo?.config}
|
||||
region={coretimeInfo?.region}
|
||||
saleInfo={coretimeInfo?.salesInfo}
|
||||
saleNumber={saleParams?.saleNumber}
|
||||
status={coretimeInfo?.status}
|
||||
/>}
|
||||
<ResponsiveGrid>
|
||||
{phaseName && coretimeInfo &&
|
||||
<Cores
|
||||
color={apiColor}
|
||||
phaseName={phaseName}
|
||||
salesInfo={coretimeInfo?.salesInfo}
|
||||
/>}
|
||||
{saleParams?.regionForSale && <Region regionForSale={saleParams.regionForSale} />}
|
||||
{phaseName && coretimeInfo && saleParams &&
|
||||
<Timeline
|
||||
color={apiColor}
|
||||
coretimeInfo={coretimeInfo}
|
||||
phaseName={phaseName}
|
||||
saleParams={saleParams}
|
||||
/>}
|
||||
<div style={{ borderRadius: '4px', gridColumn: '1 / -1', justifySelf: 'center', padding: '24px', width: '100%' }}>
|
||||
<p style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '1rem' }}>{t('Sale information')}</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div style={{ maxWidth: '300px' }}>
|
||||
<Dropdown
|
||||
className='isSmall'
|
||||
label={t('Pick a sale number')}
|
||||
onChange={onDropDownChange}
|
||||
options={saleNumberOptions}
|
||||
value={chosenSaleNumber}
|
||||
/>
|
||||
</div>
|
||||
{saleParams && selectedSaleParams &&
|
||||
<SaleDetailsView
|
||||
chosenSaleNumber={chosenSaleNumber}
|
||||
relayName={relayName}
|
||||
saleParams={selectedSaleParams}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
</ResponsiveGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sale;
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ChainInformation, ChainWorkTaskInformation } from '@pezkuwi/react-hooks/types';
|
||||
import type { RelayName } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ExpandButton } from '@pezkuwi/react-components';
|
||||
import { useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Row from './Row.js';
|
||||
|
||||
interface Props {
|
||||
chain: ChainInformation
|
||||
regionEnd: number
|
||||
regionBegin: number
|
||||
lastCommittedTimeslice: number
|
||||
relayName: RelayName
|
||||
}
|
||||
|
||||
function TeyrchainTableRow ({ chain, lastCommittedTimeslice, regionBegin, regionEnd, relayName }: Props): React.ReactElement<Props> {
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||
const info = chain.workTaskInfo;
|
||||
const firstRecord = chain.workTaskInfo[0];
|
||||
const multiple = info.length > 1;
|
||||
const expandedContent = info.slice(1);
|
||||
|
||||
if (!firstRecord) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const renderRow = (record: ChainWorkTaskInformation, idx: number, highlight = false) =>
|
||||
<>
|
||||
<Row
|
||||
chainRecord={record}
|
||||
highlight={highlight}
|
||||
id={chain.id}
|
||||
lastCommittedTimeslice={lastCommittedTimeslice}
|
||||
lease={chain.lease}
|
||||
regionBegin={regionBegin}
|
||||
regionEnd={regionEnd}
|
||||
relayName={relayName}
|
||||
/>
|
||||
{idx === 0 && <td style={{ paddingRight: '2rem', textAlign: 'right', verticalAlign: 'top' }}>
|
||||
|
||||
{!!multiple &&
|
||||
<ExpandButton
|
||||
expanded={isExpanded}
|
||||
onClick={toggleIsExpanded}
|
||||
/>
|
||||
}
|
||||
</td>}
|
||||
</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
className={`isExpanded isFirst ${isExpanded ? '' : 'isLast'}`}
|
||||
key={chain.id}
|
||||
>
|
||||
{renderRow(firstRecord, 0)}
|
||||
</tr>
|
||||
{isExpanded && expandedContent?.map((infoRow, idx) =>
|
||||
<tr key={`${chain.id}${idx}`}>
|
||||
{renderRow(infoRow, idx + 1, true)}
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TeyrchainTableRow);
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { RelayName } from './types.js';
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { type CoretimeInformation } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import Filters from './Overview/Filters.js';
|
||||
import TeyrchainTableRow from './TeyrchainTableRow.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
coretimeInfo: CoretimeInformation
|
||||
relayName: RelayName
|
||||
}
|
||||
|
||||
function TeyrchainsTable ({ coretimeInfo, relayName }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('teyrchains'), 'start'],
|
||||
[t('name'), 'start media--800'],
|
||||
[t('core number'), 'start'],
|
||||
[t('type'), 'start'],
|
||||
[t('last block'), 'start media--800'],
|
||||
[t('end date (approx)'), 'start media--1000'],
|
||||
[t('renewal'), 'start media--1200'],
|
||||
[t('renewal price'), 'start media--1200'],
|
||||
[t('links'), 'start media--800'],
|
||||
[t('other cores'), 'end']
|
||||
]);
|
||||
|
||||
const [taskIds, setTaskIds] = useState<number[]>([]);
|
||||
|
||||
const onFilter = useCallback((filteredData: number[]) => {
|
||||
setTaskIds(filteredData);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (coretimeInfo?.taskIds) {
|
||||
setTaskIds(coretimeInfo?.taskIds);
|
||||
}
|
||||
}, [coretimeInfo?.taskIds]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Filters
|
||||
chainInfo={coretimeInfo?.chainInfo}
|
||||
data={coretimeInfo?.taskIds}
|
||||
onFilter={onFilter}
|
||||
/>
|
||||
<Table
|
||||
emptySpinner={false}
|
||||
header={headerRef.current}
|
||||
isSplit={false}
|
||||
>
|
||||
{taskIds?.map((taskId: number) => {
|
||||
const chain = coretimeInfo.chainInfo[taskId];
|
||||
|
||||
if (!chain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TeyrchainTableRow
|
||||
chain={chain}
|
||||
key={chain.id}
|
||||
lastCommittedTimeslice={coretimeInfo.status.lastCommittedTimeslice}
|
||||
regionBegin={coretimeInfo.salesInfo.regionBegin}
|
||||
regionEnd={coretimeInfo.salesInfo.regionEnd}
|
||||
relayName={relayName}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TeyrchainsTable);
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { type ReactNode } from 'react';
|
||||
|
||||
import { useTheme } from '@pezkuwi/react-hooks';
|
||||
|
||||
export const WhiteBox = ({ children, style }: { children: ReactNode, style?: React.CSSProperties }) => {
|
||||
const { theme } = useTheme();
|
||||
const backgroundColor = theme === 'dark' ? '#333' : 'white';
|
||||
|
||||
return <div style={{ backgroundColor, borderRadius: '4px', display: 'flex', flexDirection: 'column', justifyItems: 'center', minWidth: '250px', padding: '24px', ...style }}>
|
||||
{children}
|
||||
</div>;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export const PhaseName = {
|
||||
FixedPrice: 'Fixed Price',
|
||||
PriceDiscovery: 'Price Discovery',
|
||||
Renewals: 'Renewals'
|
||||
} as const;
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { CoretimeProvider } from './CoretimeContext.js';
|
||||
import CoretimePage from './CoretimePage.js';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function CoretimeApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { api, isApiReady } = useApi();
|
||||
|
||||
return (
|
||||
<CoretimeProvider
|
||||
api={api}
|
||||
isApiReady={isApiReady}
|
||||
>
|
||||
<CoretimePage
|
||||
api={api}
|
||||
basePath={basePath}
|
||||
className={className}
|
||||
isApiReady={isApiReady}
|
||||
/>
|
||||
</CoretimeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CoretimeApp);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useTranslation as useTranslationBase } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): { t: (key: string, options?: { replace: Record<string, unknown> }) => string } {
|
||||
return useTranslationBase('app-coretime');
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ChainInformation } from '@pezkuwi/react-hooks/types';
|
||||
import type { PhaseName } from './constants.js';
|
||||
|
||||
export interface PhaseInfo {
|
||||
name?: string;
|
||||
start: {
|
||||
date: string | null;
|
||||
blocks: {
|
||||
relay: number;
|
||||
coretime: number;
|
||||
};
|
||||
ts: number;
|
||||
}
|
||||
end: {
|
||||
date: string | null;
|
||||
blocks: {
|
||||
relay: number;
|
||||
coretime: number;
|
||||
};
|
||||
ts: number;
|
||||
}
|
||||
}
|
||||
|
||||
type PhaseNameType = typeof PhaseName[keyof typeof PhaseName];
|
||||
|
||||
export interface PhaseConfig {
|
||||
currentPhaseName: PhaseNameType;
|
||||
config: Record<PhaseNameType, PhaseInfo >;
|
||||
}
|
||||
|
||||
export interface PhaseProgress {
|
||||
value: number;
|
||||
total: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SaleParameters {
|
||||
currentRegion: {
|
||||
start: { date: string, ts: number; blocks: { coretime: number, relay: number } };
|
||||
end: { date: string, ts: number; blocks: { coretime: number, relay: number } };
|
||||
};
|
||||
regionForSale: {
|
||||
start: { date: string, ts: number; blocks: { coretime: number, relay: number } };
|
||||
end: { date: string | null, ts: number; blocks: { coretime: number, relay: number } };
|
||||
};
|
||||
saleNumber: number;
|
||||
interlude: { ts: number; blocks: number };
|
||||
leadin: { ts: number; blocks: number };
|
||||
phaseConfig: PhaseConfig | null;
|
||||
}
|
||||
|
||||
export interface SaleDetails {
|
||||
saleNumber: number;
|
||||
relay: {
|
||||
start: {
|
||||
block: number;
|
||||
ts: number;
|
||||
},
|
||||
end: {
|
||||
block: number;
|
||||
ts: number;
|
||||
},
|
||||
},
|
||||
coretime: {
|
||||
start: {
|
||||
block: number;
|
||||
},
|
||||
end: {
|
||||
block: number;
|
||||
}
|
||||
}
|
||||
date: {
|
||||
start: string | null;
|
||||
end: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RegionInfo {
|
||||
regionBegin: number;
|
||||
regionEnd: number;
|
||||
}
|
||||
|
||||
export type RelayName = 'dicle' | 'pezkuwi' | 'paseo testnet' | 'zagros'
|
||||
|
||||
export interface GetResponse {
|
||||
blocks: {
|
||||
coretime: (ts: number) => number;
|
||||
relay: (ts: number) => number;
|
||||
};
|
||||
timeslices: {
|
||||
coretime: (blocks: number) => number;
|
||||
relay: (blocks: number) => number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BaseFilterProps {
|
||||
data: number[];
|
||||
onFilter: (data: number[]) => void;
|
||||
}
|
||||
|
||||
export interface ChainInfoFilterProps extends BaseFilterProps {
|
||||
chainInfo: Record<number, ChainInformation>;
|
||||
}
|
||||
|
||||
export type SortDirection = 'DESC' | 'ASC' | '';
|
||||
|
||||
export enum FilterType {
|
||||
BLOCKS = 'blocks',
|
||||
SEARCH = 'search',
|
||||
TYPE = 'type'
|
||||
}
|
||||
|
||||
export interface ActiveFilters {
|
||||
search: number[];
|
||||
type: number[];
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ChainBlockConstants, ChainConstants, CoretimeInformation } from '@pezkuwi/react-hooks/types';
|
||||
import type { GetResponse, RegionInfo, RelayName } from '../types.js';
|
||||
|
||||
import { CoreTimeTypes } from '@pezkuwi/react-hooks/constants';
|
||||
import { BN } from '@pezkuwi/util';
|
||||
|
||||
type FirstCycleStartType = Record<
|
||||
'block' | 'timeslice',
|
||||
Record<
|
||||
'coretime',
|
||||
Record<RelayName, number>
|
||||
>
|
||||
>;
|
||||
|
||||
// Blocks on the Coretime Chain
|
||||
export const FirstCycleStart: FirstCycleStartType = {
|
||||
block: {
|
||||
coretime: {
|
||||
dicle: 53793,
|
||||
'paseo testnet': 22316,
|
||||
pezkuwi: 100988,
|
||||
zagros: 7363
|
||||
}
|
||||
},
|
||||
timeslice: {
|
||||
coretime: {
|
||||
dicle: 284920,
|
||||
'paseo testnet': 38469,
|
||||
pezkuwi: 282525,
|
||||
zagros: 245402
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const coretimeTypeColours: Record<string, string> = {
|
||||
[CoreTimeTypes.Reservation]: 'orange',
|
||||
[CoreTimeTypes.Lease]: 'blue',
|
||||
[CoreTimeTypes['Bulk Coretime']]: 'pink'
|
||||
};
|
||||
|
||||
export function formatDate (date: Date, time = false) {
|
||||
const day = date.getDate();
|
||||
const month = date.toLocaleString('default', { month: 'short' });
|
||||
const year = date.getFullYear();
|
||||
|
||||
if (time) {
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
return `${day} ${month} ${year}, ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
return `${day} ${month} ${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives you a date for the target timeslice
|
||||
*
|
||||
* Relay chain info:
|
||||
* blockTime = 6000 ms
|
||||
* BlocksPerTimeslice = 80
|
||||
* Default Regoin = 5040 timeslices
|
||||
*
|
||||
* Calculation:
|
||||
* TargetBlock = TargetTimeslice * BlocksPerTimeslice
|
||||
* Block Time Difference = |TargetBlock - latest Block| * blockTime
|
||||
*
|
||||
* Estimate timestamp =
|
||||
* if targetBlock is before the latestBlock
|
||||
* now minus block time difference
|
||||
* else
|
||||
* now plus block time difference
|
||||
*/
|
||||
export const estimateTime = (
|
||||
targetTimeslice: string | number,
|
||||
latestBlock: number,
|
||||
{ blocksPerTimeslice: blocksPerTs, blocktimeMs }: ChainBlockConstants
|
||||
): { timestamp: number, formattedDate: string } | null => {
|
||||
if (!latestBlock || !targetTimeslice) {
|
||||
console.error('Invalid input: one or more inputs are missing');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const now = new BN(Date.now());
|
||||
const blockTime = new BN(blocktimeMs); // Average block time in milliseconds (6 seconds)
|
||||
const blocksPerTimeslice = new BN(blocksPerTs);
|
||||
const targetBlock = new BN(Number(targetTimeslice)).mul(blocksPerTimeslice);
|
||||
const latestBlockBN = new BN(latestBlock);
|
||||
const blockDifference = targetBlock.sub(latestBlockBN);
|
||||
const timeDifference = blockDifference.mul(blockTime);
|
||||
const estTimestamp = now.add(timeDifference);
|
||||
|
||||
return {
|
||||
formattedDate: formatDate(new Date(estTimestamp.toNumber())),
|
||||
timestamp: estTimestamp.toNumber()
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in calculation:', error);
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory function to create helper functions for converting timeslices to blocks and vice versa.
|
||||
*
|
||||
* @returns An object containing blocks and timeslices conversion functions.
|
||||
*/
|
||||
export const createGet = (constants: ChainConstants): GetResponse => ({
|
||||
blocks: {
|
||||
/**
|
||||
* Convert timeslices to Coretime blocks.
|
||||
*
|
||||
* @param ts - Number of timeslices.
|
||||
* @returns Number of Coretime blocks.
|
||||
*/
|
||||
coretime: (ts: number) => {
|
||||
return ts * constants.coretime.blocksPerTimeslice;
|
||||
},
|
||||
/**
|
||||
* Convert timeslices to Relay blocks.
|
||||
*
|
||||
* @param ts - Number of timeslices.
|
||||
* @returns Number of Relay blocks.
|
||||
*/
|
||||
relay: (ts: number) => {
|
||||
return ts * constants.relay.blocksPerTimeslice;
|
||||
}
|
||||
},
|
||||
timeslices: {
|
||||
/**
|
||||
* Convert Coretime blocks to timeslices.
|
||||
*
|
||||
* @param blocks - Number of Coretime blocks.
|
||||
* @returns Number of timeslices.
|
||||
*/
|
||||
coretime: (blocks: number) => {
|
||||
return blocks / constants.coretime.blocksPerTimeslice;
|
||||
},
|
||||
/**
|
||||
* Convert Relay blocks to timeslices.
|
||||
*
|
||||
* @param blocks - Number of Relay blocks.
|
||||
* @returns Number of timeslices.
|
||||
*/
|
||||
relay: (blocks: number) => {
|
||||
return blocks / constants.relay.blocksPerTimeslice;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the start and end of the current region
|
||||
* broker.saleInfo call returns the start/end of the next region always
|
||||
*
|
||||
* The end of the current region is the start of the next region, which is returned by broker.saleInfo call
|
||||
*
|
||||
* @param saleInfo - The sale information
|
||||
* @param config - The broker configuration
|
||||
*
|
||||
* @returns The start and end of the current region
|
||||
*/
|
||||
export const getCurrentRegionStartEndTs = (saleInfo: RegionInfo, regionLength: number) => {
|
||||
return {
|
||||
currentRegionEndTs: saleInfo.regionBegin,
|
||||
currentRegionStartTs: saleInfo.regionBegin - regionLength
|
||||
};
|
||||
};
|
||||
|
||||
export const getAvailableNumberOfCores = (coretimeInfo: CoretimeInformation) =>
|
||||
Number(coretimeInfo?.salesInfo?.coresOffered) - Number(coretimeInfo?.salesInfo.coresSold);
|
||||
|
||||
export const constructSubscanQuery = (dateStart: string, dateEnd: string, chainName: string, module = 'broker', call = 'purchase') => {
|
||||
const page = 1;
|
||||
const pageSize = 25;
|
||||
const signed = 'all';
|
||||
const baseURL = `https://coretime-${chainName}.subscan.io/extrinsic`;
|
||||
|
||||
return `${baseURL}?page=${page}&time_dimension=date&page_size=${pageSize}&module=${module}&signed=${signed}&call=${call}&date_start=${dateStart}&date_end=${dateEnd}`;
|
||||
};
|
||||
@@ -0,0 +1,292 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-coretime authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ChainConstants, PalletBrokerConfigRecord, PalletBrokerSaleInfoRecord } from '@pezkuwi/react-hooks/types';
|
||||
import type { GetResponse, PhaseConfig, RegionInfo, RelayName, SaleParameters } from '../types.js';
|
||||
|
||||
import { type ProgressBarSection } from '@pezkuwi/react-components/types';
|
||||
import { BN, formatBalance } from '@pezkuwi/util';
|
||||
|
||||
import { PhaseName } from '../constants.js';
|
||||
import { createGet, estimateTime, FirstCycleStart, getCurrentRegionStartEndTs } from './index.js';
|
||||
|
||||
// We are scaling everything to avoid floating point precision issues.
|
||||
const SCALE = new BN(10000);
|
||||
|
||||
/**
|
||||
* Formats a BN value to a human-readable balance string with proper units
|
||||
*
|
||||
* @param num - The BN value to format
|
||||
* @returns A formatted string with the balance value and unit
|
||||
*/
|
||||
export const formatBNToBalance = (num: BN) => formatBalance(num, { forceUnit: formatBalance.getDefaults().unit, withAll: true, withUnit: true });
|
||||
|
||||
export const leadinFactorAt = (scaledWhen: BN): BN => {
|
||||
const scaledHalf = SCALE.div(new BN(2)); // 0.5 scaled to 10000
|
||||
|
||||
if (scaledWhen.lte(scaledHalf)) {
|
||||
// First half of the graph, steeper slope
|
||||
return SCALE.mul(new BN(100)).sub(scaledWhen.mul(new BN(180)));
|
||||
} else {
|
||||
// Second half of the graph, flatter slope
|
||||
return SCALE.mul(new BN(19)).sub(scaledWhen.mul(new BN(18)));
|
||||
}
|
||||
};
|
||||
|
||||
export const getCorePriceAt = (blockNow: number | null, saleInfo: PalletBrokerSaleInfoRecord | undefined): BN => {
|
||||
if (!saleInfo || !blockNow) {
|
||||
return new BN(0);
|
||||
}
|
||||
|
||||
const { endPrice, leadinLength, saleStart } = saleInfo;
|
||||
|
||||
// Explicit conversion to BN
|
||||
const blockNowBn = new BN(blockNow);
|
||||
const saleStartBn = new BN(saleStart);
|
||||
const leadinLengthBn = new BN(leadinLength);
|
||||
|
||||
// Elapsed time since the start of the sale, constrained to not exceed the total lead-in period
|
||||
const elapsedTimeSinceSaleStart = blockNowBn.sub(saleStartBn);
|
||||
const cappedElapsedTime = elapsedTimeSinceSaleStart.lt(leadinLengthBn)
|
||||
? elapsedTimeSinceSaleStart
|
||||
: leadinLengthBn;
|
||||
|
||||
const scaledProgress = cappedElapsedTime.mul(new BN(10000)).div(leadinLengthBn);
|
||||
/**
|
||||
* Progress is a normalized value between 0 and 1, where:
|
||||
*
|
||||
* 0 means the sale just started.
|
||||
* 1 means the sale is at the end of the lead-in period.
|
||||
*
|
||||
* We are scaling it to avoid floating point precision issues.
|
||||
*/
|
||||
const leadinFactor = leadinFactorAt(scaledProgress);
|
||||
const scaledPrice = leadinFactor.mul(endPrice).div(SCALE);
|
||||
|
||||
return scaledPrice;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current sale number
|
||||
*
|
||||
* @param currentRegionEnd - The end of the current region
|
||||
* @param chainName - The name of the chain
|
||||
* @param config - broker.configuration call response
|
||||
*
|
||||
* @returns The current sale number
|
||||
*/
|
||||
export const getCurrentSaleNumber = (
|
||||
currentRegionEnd: number,
|
||||
relayName: RelayName,
|
||||
config: Pick<PalletBrokerConfigRecord, 'interludeLength' | 'leadinLength' | 'regionLength'>
|
||||
): number => {
|
||||
if (!relayName || !currentRegionEnd) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return Math.ceil((currentRegionEnd - FirstCycleStart.timeslice.coretime[relayName]) / config.regionLength);
|
||||
};
|
||||
|
||||
export const determinePhaseName = (
|
||||
lastCommittedTimeslice: number,
|
||||
currentRegionStart: number,
|
||||
interludeLengthTs: number,
|
||||
leadInLengthTs: number): typeof PhaseName[keyof typeof PhaseName] => {
|
||||
const progress = lastCommittedTimeslice - currentRegionStart;
|
||||
|
||||
if (progress < interludeLengthTs) {
|
||||
return PhaseName.Renewals;
|
||||
}
|
||||
|
||||
if (progress < interludeLengthTs + leadInLengthTs) {
|
||||
return PhaseName.PriceDiscovery;
|
||||
}
|
||||
|
||||
return PhaseName.FixedPrice;
|
||||
};
|
||||
|
||||
export const getSaleProgress = (
|
||||
lastCommittedTimeslice: number | undefined,
|
||||
currentRegionStartTs: number,
|
||||
interludeLengthTs: number,
|
||||
leadInLengthTs: number,
|
||||
regionBegin: number): ProgressBarSection[] => {
|
||||
if (!lastCommittedTimeslice || !currentRegionStartTs || !interludeLengthTs || !leadInLengthTs || !regionBegin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const progress = lastCommittedTimeslice - currentRegionStartTs;
|
||||
|
||||
return [
|
||||
{
|
||||
label: PhaseName.Renewals,
|
||||
total: interludeLengthTs,
|
||||
value: Math.min(progress, interludeLengthTs)
|
||||
},
|
||||
{
|
||||
label: PhaseName.PriceDiscovery,
|
||||
total: leadInLengthTs,
|
||||
value: Math.min(Math.max(progress - interludeLengthTs, 0), leadInLengthTs)
|
||||
},
|
||||
{
|
||||
label: PhaseName.FixedPrice,
|
||||
total: regionBegin - currentRegionStartTs - interludeLengthTs - leadInLengthTs,
|
||||
value: Math.max(progress - interludeLengthTs - leadInLengthTs, 0)
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const makeConfig = (startTs: number, endTs: number, get: GetResponse, getDate: (ts: number) => string | null, phaseName?: typeof PhaseName[keyof typeof PhaseName]) => {
|
||||
return {
|
||||
end: {
|
||||
blocks: {
|
||||
coretime: get.blocks.coretime(endTs),
|
||||
relay: get.blocks.relay(endTs)
|
||||
},
|
||||
date: getDate(endTs),
|
||||
ts: endTs
|
||||
},
|
||||
name: phaseName ?? '',
|
||||
start: {
|
||||
blocks: {
|
||||
coretime: get.blocks.coretime(startTs),
|
||||
relay: get.blocks.relay(startTs)
|
||||
},
|
||||
date: getDate(startTs),
|
||||
ts: startTs
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
const getPhaseConfiguration = (
|
||||
currentRegionStart: number,
|
||||
regionLength: number,
|
||||
interludeLengthTs: number,
|
||||
leadInLengthTs: number,
|
||||
lastCommittedTimeslice: number,
|
||||
constants: ChainConstants
|
||||
): PhaseConfig => {
|
||||
const renewalsEndTs = currentRegionStart + interludeLengthTs;
|
||||
const priceDiscoveryEndTs = renewalsEndTs + leadInLengthTs;
|
||||
const fixedPriceLengthTs = regionLength - interludeLengthTs - leadInLengthTs;
|
||||
const fixedPriceEndTs = priceDiscoveryEndTs + fixedPriceLengthTs;
|
||||
const get = createGet(constants);
|
||||
const getDate = (ts: number) => estimateTime(ts, get.blocks.relay(lastCommittedTimeslice), constants.relay)?.formattedDate ?? null;
|
||||
|
||||
return {
|
||||
config: {
|
||||
[PhaseName.FixedPrice]: makeConfig(priceDiscoveryEndTs, fixedPriceEndTs, get, getDate, PhaseName.FixedPrice),
|
||||
[PhaseName.PriceDiscovery]: makeConfig(renewalsEndTs, priceDiscoveryEndTs, get, getDate, PhaseName.PriceDiscovery),
|
||||
[PhaseName.Renewals]: makeConfig(currentRegionStart, renewalsEndTs, get, getDate, PhaseName.Renewals)
|
||||
},
|
||||
currentPhaseName: determinePhaseName(lastCommittedTimeslice, currentRegionStart, interludeLengthTs, leadInLengthTs)
|
||||
};
|
||||
};
|
||||
|
||||
export const getSaleParameters = (
|
||||
{ config, constants, salesInfo }: {salesInfo: RegionInfo, config: Pick<PalletBrokerConfigRecord, 'interludeLength' | 'leadinLength' | 'regionLength'>, constants: ChainConstants},
|
||||
relayName: RelayName,
|
||||
lastCommittedTimeslice: number,
|
||||
chosenSaleNumber = -1
|
||||
): SaleParameters => {
|
||||
const get = createGet(constants);
|
||||
const interludeLengthTs = get.timeslices.coretime(config.interludeLength);
|
||||
const leadInLengthTs = get.timeslices.coretime(config.leadinLength);
|
||||
let { currentRegionEndTs, currentRegionStartTs } = getCurrentRegionStartEndTs(salesInfo, config.regionLength);
|
||||
const getDate = (ts: number) => estimateTime(ts, get.blocks.relay(lastCommittedTimeslice), constants.relay)?.formattedDate ?? null;
|
||||
const saleNumber = getCurrentSaleNumber(currentRegionEndTs, relayName, config);
|
||||
|
||||
let currentRegionInfo: SaleParameters['currentRegion'];
|
||||
|
||||
if (chosenSaleNumber !== -1) {
|
||||
// A hack for Dicle as one of the sales had an unusual length
|
||||
// checked against Subscan historical sales
|
||||
if (relayName === 'dicle') {
|
||||
const irregularRegionLength = 848;
|
||||
|
||||
if (chosenSaleNumber === 0) {
|
||||
currentRegionStartTs = FirstCycleStart.timeslice.coretime[relayName];
|
||||
currentRegionEndTs = currentRegionStartTs + config.regionLength;
|
||||
} else if (chosenSaleNumber === 1) {
|
||||
currentRegionStartTs = FirstCycleStart.timeslice.coretime[relayName] + config.regionLength;
|
||||
// that particular sale #2 was only 848 blocks long
|
||||
currentRegionEndTs = currentRegionStartTs + irregularRegionLength;
|
||||
} else {
|
||||
currentRegionStartTs = FirstCycleStart.timeslice.coretime[relayName] + config.regionLength * (chosenSaleNumber - 1) + irregularRegionLength;
|
||||
currentRegionEndTs = currentRegionStartTs + config.regionLength;
|
||||
}
|
||||
} else {
|
||||
currentRegionStartTs = FirstCycleStart.timeslice.coretime[relayName] + config.regionLength * chosenSaleNumber;
|
||||
currentRegionEndTs = currentRegionStartTs + config.regionLength;
|
||||
}
|
||||
|
||||
currentRegionInfo = {
|
||||
end: {
|
||||
blocks: {
|
||||
// the coretime blocks cannot be calculated as historically the regions are not 201,600 blocks long, they deviate from 2,212 to 1,417 blocks
|
||||
coretime: 0,
|
||||
relay: get.blocks.relay(currentRegionEndTs)
|
||||
},
|
||||
date: getDate(currentRegionEndTs) ?? '',
|
||||
ts: currentRegionEndTs
|
||||
},
|
||||
start: {
|
||||
blocks: {
|
||||
|
||||
coretime: 0,
|
||||
relay: get.blocks.relay(currentRegionStartTs)
|
||||
},
|
||||
date: getDate(currentRegionStartTs) ?? '',
|
||||
ts: currentRegionStartTs
|
||||
}
|
||||
};
|
||||
} else {
|
||||
currentRegionInfo = makeConfig(currentRegionStartTs, currentRegionEndTs, get, getDate) as SaleParameters['currentRegion'];
|
||||
}
|
||||
|
||||
let phaseConfig: PhaseConfig | null = null;
|
||||
|
||||
if (currentRegionEndTs - currentRegionStartTs === config.regionLength) {
|
||||
phaseConfig = getPhaseConfiguration(
|
||||
currentRegionStartTs,
|
||||
config.regionLength,
|
||||
interludeLengthTs,
|
||||
leadInLengthTs,
|
||||
lastCommittedTimeslice,
|
||||
constants
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
currentRegion: currentRegionInfo,
|
||||
interlude: {
|
||||
blocks: config.interludeLength,
|
||||
ts: interludeLengthTs
|
||||
},
|
||||
leadin: {
|
||||
blocks: config.leadinLength,
|
||||
ts: leadInLengthTs
|
||||
},
|
||||
phaseConfig,
|
||||
regionForSale: {
|
||||
end: {
|
||||
blocks: {
|
||||
coretime: 0,
|
||||
relay: currentRegionInfo.end.blocks.relay + get.blocks.relay(config.regionLength)
|
||||
},
|
||||
date: estimateTime(currentRegionInfo.end.ts + config.regionLength, get.blocks.relay(lastCommittedTimeslice), constants.relay)?.formattedDate ?? null,
|
||||
ts: currentRegionInfo.end.ts + config.regionLength
|
||||
},
|
||||
start: {
|
||||
blocks: {
|
||||
coretime: 0,
|
||||
relay: currentRegionInfo.end.blocks.relay
|
||||
},
|
||||
date: currentRegionInfo.end.date,
|
||||
ts: currentRegionInfo.end.ts
|
||||
}
|
||||
},
|
||||
saleNumber
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user