mirror of
https://github.com/pezkuwichain/pezkuwi-sdk-ui.git
synced 2026-06-13 21:01:10 +00:00
Initial commit: Pezkuwi SDK UI
Comprehensive web interface for interacting with Pezkuwi blockchain. Features: - Blockchain explorer - Wallet management - Staking interface - Governance participation - Developer tools Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SessionInfo, Validator } from '../../types.js';
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
|
||||
import { Table, Tag } from '@pezkuwi/react-components';
|
||||
import { useToggle } from '@pezkuwi/react-hooks';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import useExposure from '../useExposure.js';
|
||||
import useHeartbeat from '../useHeartbeat.js';
|
||||
import Bottom from './Row/Bottom.js';
|
||||
import Middle from './Row/Middle.js';
|
||||
import Top from './Row/Top.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
points?: number;
|
||||
sessionInfo: SessionInfo;
|
||||
toggleFavorite: (stashId: string) => void;
|
||||
validator: Validator;
|
||||
}
|
||||
|
||||
interface PropsExpanded {
|
||||
className?: string;
|
||||
validator: Validator;
|
||||
}
|
||||
|
||||
function EntryExpanded ({ className = '' }: PropsExpanded): React.ReactElement<PropsExpanded> {
|
||||
return <td className={className} />;
|
||||
}
|
||||
|
||||
function Entry ({ className = '', points, sessionInfo, toggleFavorite, validator }: Props): React.ReactElement<Props> {
|
||||
const [isExpanded, toggleExpanded] = useToggle();
|
||||
const pointsRef = useRef<{ counter: number, points: number }>({ counter: 0, points: 0 });
|
||||
const exposure = useExposure(validator, sessionInfo);
|
||||
const heartbeat = useHeartbeat(validator, sessionInfo);
|
||||
|
||||
const pointsAnimClass = useMemo(
|
||||
(): string => {
|
||||
if (!points || pointsRef.current.points === points) {
|
||||
return '';
|
||||
}
|
||||
|
||||
pointsRef.current.points = points;
|
||||
pointsRef.current.counter = (pointsRef.current.counter + 1) % 25;
|
||||
|
||||
return `greyAnim-${pointsRef.current.counter}`;
|
||||
},
|
||||
[points, pointsRef]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Top
|
||||
className={className}
|
||||
heartbeat={heartbeat}
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
toggleFavorite={toggleFavorite}
|
||||
validator={validator}
|
||||
>
|
||||
{points && (
|
||||
<Tag
|
||||
className={`${pointsAnimClass} absolute`}
|
||||
color='lightgrey'
|
||||
label={formatNumber(points)}
|
||||
/>
|
||||
)}
|
||||
</Top>
|
||||
<Middle
|
||||
className={className}
|
||||
isExpanded={isExpanded}
|
||||
>
|
||||
<Table.Column.Balance
|
||||
className='relative'
|
||||
label={
|
||||
exposure?.waiting && (
|
||||
<>
|
||||
<span>(</span>
|
||||
<FormatBalance value={exposure.waiting.total} />
|
||||
<span>, {exposure.waiting.others.length})</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
value={exposure?.clipped?.total}
|
||||
withLoading
|
||||
/>
|
||||
</Middle>
|
||||
<Bottom
|
||||
className={className}
|
||||
isExpanded={isExpanded}
|
||||
>
|
||||
<EntryExpanded validator={validator} />
|
||||
</Bottom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Entry);
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
function Bottom ({ children, className = '', isExpanded }: Props): React.ReactElement<Props> | null {
|
||||
if (!isExpanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={`${className} isExpanded isLast`}>
|
||||
<td />
|
||||
<td />
|
||||
{children}
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Bottom);
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
function Middle ({ children, className = '', isExpanded }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<tr className={`${className} isExpanded ${isExpanded ? '' : 'isLast'} packedTop`}>
|
||||
<td />
|
||||
{children}
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Middle);
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Validator } from '../../../types.js';
|
||||
import type { UseHeartbeat } from '../../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressSmall, Table } from '@pezkuwi/react-components';
|
||||
|
||||
import Status from '../Status.js';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
heartbeat?: UseHeartbeat;
|
||||
isExpanded: boolean;
|
||||
toggleExpanded: () => void;
|
||||
toggleFavorite: (stashId: string) => void;
|
||||
validator: Validator;
|
||||
}
|
||||
|
||||
function Top ({ children, className = '', heartbeat, isExpanded, toggleExpanded, toggleFavorite, validator }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<tr className={`${className} isExpanded isFirst packedBottom`}>
|
||||
<Table.Column.Favorite
|
||||
address={validator.stashId}
|
||||
isFavorite={validator.isFavorite}
|
||||
toggle={toggleFavorite}
|
||||
/>
|
||||
<td
|
||||
className='statusInfo'
|
||||
rowSpan={2}
|
||||
>
|
||||
<Status
|
||||
heartbeat={heartbeat}
|
||||
validator={validator}
|
||||
/>
|
||||
</td>
|
||||
<td className='address relative all'>
|
||||
<AddressSmall value={validator.stashId} />
|
||||
{children}
|
||||
</td>
|
||||
<Table.Column.Expand
|
||||
isExpanded={isExpanded}
|
||||
toggle={toggleExpanded}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Top);
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Validator } from '../../types.js';
|
||||
import type { UseHeartbeat } from '../types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Badge, styled } from '@pezkuwi/react-components';
|
||||
import { useAccounts } from '@pezkuwi/react-hooks';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
heartbeat?: UseHeartbeat;
|
||||
isChilled?: boolean;
|
||||
nominators?: string[];
|
||||
validator: Validator;
|
||||
}
|
||||
|
||||
function Status ({ className, heartbeat: { authoredBlocks, isOnline } = {}, isChilled, nominators, validator: { isElected, isPara } }: Props): React.ReactElement<Props> {
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const isNominating = useMemo(
|
||||
() => nominators && nominators.some((a) => allAccounts.includes(a)),
|
||||
[allAccounts, nominators]
|
||||
);
|
||||
|
||||
const emptyBadge = (
|
||||
<Badge
|
||||
className='opaque'
|
||||
color='gray'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
{isNominating
|
||||
? (
|
||||
<Badge
|
||||
color='green'
|
||||
icon='hand-paper'
|
||||
/>
|
||||
)
|
||||
: emptyBadge
|
||||
}
|
||||
{isPara
|
||||
? (
|
||||
<Badge
|
||||
color='purple'
|
||||
icon='vector-square'
|
||||
/>
|
||||
)
|
||||
: emptyBadge
|
||||
}
|
||||
{isChilled
|
||||
? (
|
||||
<Badge
|
||||
color='red'
|
||||
icon='cancel'
|
||||
/>
|
||||
)
|
||||
: isElected
|
||||
? (
|
||||
<Badge
|
||||
color='blue'
|
||||
icon='chevron-right'
|
||||
/>
|
||||
)
|
||||
: emptyBadge
|
||||
}
|
||||
{isOnline
|
||||
? authoredBlocks
|
||||
? (
|
||||
<Badge
|
||||
color='green'
|
||||
info={
|
||||
<span className='authoredBlocks'>{authoredBlocks}</span>
|
||||
}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Badge
|
||||
color='green'
|
||||
icon='envelope'
|
||||
/>
|
||||
)
|
||||
: emptyBadge
|
||||
}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
.authoredBlocks {
|
||||
vertical-align: top;
|
||||
font-size: var(--font-percent-tiny);
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Status);
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SessionInfo, Validator } from '../../types.js';
|
||||
import type { UsePoints } from '../types.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useNextTick } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import Entry from './Entry.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
legend: React.ReactNode;
|
||||
points?: UsePoints;
|
||||
sessionInfo: SessionInfo;
|
||||
toggleFavorite: (stashId: string) => void;
|
||||
validatorsActive?: Validator[];
|
||||
}
|
||||
|
||||
function Active ({ className = '', legend, points, sessionInfo, toggleFavorite, validatorsActive }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const isNextTick = useNextTick();
|
||||
|
||||
const header = useRef<[string?, string?, number?][]>([
|
||||
// favorite, badges, details, expand
|
||||
[t('validators'), 'start', 4]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={isNextTick && validatorsActive && t('No session validators found')}
|
||||
emptySpinner={t('Retrieving session validators')}
|
||||
header={header.current}
|
||||
isSplit
|
||||
legend={legend}
|
||||
>
|
||||
{isNextTick && validatorsActive?.map((v) => (
|
||||
<Entry
|
||||
key={v.key}
|
||||
points={points?.[v.stashId]}
|
||||
sessionInfo={sessionInfo}
|
||||
toggleFavorite={toggleFavorite}
|
||||
validator={v}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Active);
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Validator } from '../../types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Bottom from '../Active/Row/Bottom.js';
|
||||
import Middle from '../Active/Row/Middle.js';
|
||||
import Top from '../Active/Row/Top.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
toggleFavorite: (stashId: string) => void;
|
||||
validator: Validator;
|
||||
}
|
||||
|
||||
interface PropsExpanded {
|
||||
className?: string;
|
||||
validator: Validator;
|
||||
}
|
||||
|
||||
function EntryExpanded ({ className = '' }: PropsExpanded): React.ReactElement<PropsExpanded> {
|
||||
return <td className={className} />;
|
||||
}
|
||||
|
||||
function Entry ({ className = '', toggleFavorite, validator }: Props): React.ReactElement<Props> {
|
||||
const [isExpanded, toggleExpanded] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Top
|
||||
className={className}
|
||||
isExpanded={isExpanded}
|
||||
toggleExpanded={toggleExpanded}
|
||||
toggleFavorite={toggleFavorite}
|
||||
validator={validator}
|
||||
/>
|
||||
<Middle
|
||||
className={className}
|
||||
isExpanded={isExpanded}
|
||||
>
|
||||
<td />
|
||||
</Middle>
|
||||
<Bottom
|
||||
className={className}
|
||||
isExpanded={isExpanded}
|
||||
>
|
||||
<EntryExpanded validator={validator} />
|
||||
</Bottom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Entry);
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SessionInfo, Validator } from '../../types.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useNextTick } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import useValidatorsWaiting from '../../useValidatorsWaiting.js';
|
||||
import Entry from './Entry.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
favorites: string[];
|
||||
legend: React.ReactNode;
|
||||
sessionInfo: SessionInfo;
|
||||
toggleFavorite: (stashId: string) => void;
|
||||
validatorsActive?: Validator[];
|
||||
}
|
||||
|
||||
function Waiting ({ className = '', favorites, legend, sessionInfo, toggleFavorite, validatorsActive }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const isNextTick = useNextTick();
|
||||
const validatorsWaiting = useValidatorsWaiting(favorites, sessionInfo, validatorsActive);
|
||||
|
||||
const header = useRef<[string?, string?, number?][]>([
|
||||
// favorite, badges, details, expand
|
||||
[t('waiting'), 'start', 4]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={isNextTick && validatorsWaiting && t('No waiting validators found')}
|
||||
emptySpinner={t('Retrieving waiting validators')}
|
||||
header={header.current}
|
||||
isSplit
|
||||
legend={legend}
|
||||
>
|
||||
{isNextTick && validatorsWaiting?.map((v) => (
|
||||
<Entry
|
||||
key={v.key}
|
||||
toggleFavorite={toggleFavorite}
|
||||
validator={v}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Waiting);
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SessionInfo } from '../types.js';
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { Button, styled, ToggleGroup } from '@pezkuwi/react-components';
|
||||
|
||||
import Legend from '../Legend.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useValidatorsActive from '../useValidatorsActive.js';
|
||||
import Active from './Active/index.js';
|
||||
import Waiting from './Waiting/index.js';
|
||||
import usePoints from './usePoints.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
favorites: string[];
|
||||
isRelay: boolean;
|
||||
sessionInfo: SessionInfo;
|
||||
toggleFavorite: (stashId: string) => void;
|
||||
}
|
||||
|
||||
function Validators ({ className = '', favorites, isRelay, sessionInfo, toggleFavorite }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [intentIndex, setIntentIndex] = useState(0);
|
||||
const validatorsActive = useValidatorsActive(favorites, sessionInfo);
|
||||
const points = usePoints(sessionInfo);
|
||||
|
||||
const intentOptions = useRef([
|
||||
{ text: t('Active'), value: 'active' },
|
||||
{ text: t('Waiting'), value: 'waiting' }
|
||||
]);
|
||||
|
||||
const legend = <Legend isRelay={isRelay} />;
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<Button.Group>
|
||||
<ToggleGroup
|
||||
onChange={setIntentIndex}
|
||||
options={intentOptions.current}
|
||||
value={intentIndex}
|
||||
/>
|
||||
</Button.Group>
|
||||
{intentIndex === 0
|
||||
? (
|
||||
<Active
|
||||
legend={legend}
|
||||
points={points}
|
||||
sessionInfo={sessionInfo}
|
||||
toggleFavorite={toggleFavorite}
|
||||
validatorsActive={validatorsActive}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Waiting
|
||||
favorites={favorites}
|
||||
legend={legend}
|
||||
sessionInfo={sessionInfo}
|
||||
toggleFavorite={toggleFavorite}
|
||||
validatorsActive={validatorsActive}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
.ui--Table table {
|
||||
td.statusInfo {
|
||||
padding: 0 0 0 0.5rem;
|
||||
vertical-align: middle;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
max-width: 3.6rem;
|
||||
min-width: 3.6rem;
|
||||
|
||||
.ui--Badge {
|
||||
margin: 0.125rem;
|
||||
|
||||
&.opaque {
|
||||
opacity: var(--opacity-gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ td.address {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Validators);
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
export type UsePoints = Record<string, number>;
|
||||
|
||||
export interface UseHeartbeat {
|
||||
authoredBlocks?: number;
|
||||
isOnline?: boolean;
|
||||
}
|
||||
|
||||
export interface UseExposureExposureEntry {
|
||||
who: string,
|
||||
value: BN
|
||||
}
|
||||
|
||||
export interface UseExposureExposure {
|
||||
others: UseExposureExposureEntry[];
|
||||
own: BN;
|
||||
total: BN;
|
||||
}
|
||||
|
||||
export interface UseExposure {
|
||||
clipped?: UseExposureExposure;
|
||||
exposure?: UseExposureExposure;
|
||||
waiting?: {
|
||||
others: UseExposureExposureEntry[];
|
||||
total: BN;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PezspStakingExposure } from '@pezkuwi/types/lookup';
|
||||
import type { SessionInfo, Validator } from '../types.js';
|
||||
import type { UseExposure, UseExposureExposure } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { BN } from '@pezkuwi/util';
|
||||
|
||||
import { useCacheMap } from '../useCache.js';
|
||||
|
||||
const OPT_EXPOSURE = {
|
||||
transform: ({ others, own, total }: PezspStakingExposure): UseExposureExposure => ({
|
||||
others: others
|
||||
.map(({ value, who }) => ({
|
||||
value: value.unwrap(),
|
||||
who: who.toString()
|
||||
}))
|
||||
.sort((a, b) => (b.value as BN).cmp(a.value)),
|
||||
own: own.unwrap(),
|
||||
total: total.unwrap()
|
||||
})
|
||||
};
|
||||
|
||||
function getResult (exposure: UseExposureExposure, clipped: UseExposureExposure): UseExposure {
|
||||
let waiting: UseExposure['waiting'];
|
||||
|
||||
const others = exposure.others.filter(({ who }) =>
|
||||
!clipped.others.find((c) =>
|
||||
who === c.who
|
||||
)
|
||||
);
|
||||
|
||||
if (others.length) {
|
||||
waiting = {
|
||||
others,
|
||||
total: others.reduce((total, { value }) => total.iadd(value), new BN(0))
|
||||
};
|
||||
}
|
||||
|
||||
return { clipped, exposure, waiting };
|
||||
}
|
||||
|
||||
function useExposureImpl ({ stashId }: Validator, { activeEra }: SessionInfo): UseExposure | undefined {
|
||||
const { api } = useApi();
|
||||
|
||||
const params = useMemo(
|
||||
() => activeEra && [activeEra, stashId],
|
||||
[activeEra, stashId]
|
||||
);
|
||||
|
||||
const fullExposure = useCall(params && api.query.staking.erasStakers, params, OPT_EXPOSURE);
|
||||
const clipExposure = useCall(params && api.query.staking.erasStakersClipped, params, OPT_EXPOSURE);
|
||||
|
||||
const result = useMemo(
|
||||
() => fullExposure && clipExposure && getResult(fullExposure, clipExposure),
|
||||
[clipExposure, fullExposure]
|
||||
);
|
||||
|
||||
return useCacheMap('useExposure', stashId, result);
|
||||
}
|
||||
|
||||
export default createNamedHook('useExposure', useExposureImpl);
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option, u32 } from '@pezkuwi/types';
|
||||
import type { Codec } from '@pezkuwi/types/types';
|
||||
import type { SessionInfo, Validator } from '../types.js';
|
||||
import type { UseHeartbeat } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { isBoolean, isNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useCacheMap } from '../useCache.js';
|
||||
|
||||
const EMPTY: UseHeartbeat = {};
|
||||
|
||||
const OPT_BLOCKS = {
|
||||
transform: (authoredBlocks: u32): number =>
|
||||
authoredBlocks.toNumber()
|
||||
};
|
||||
|
||||
const OPT_BEATS = {
|
||||
// Option<WrapperOpaque<PalletImOnlineBoundedOpaqueNetworkState>>
|
||||
transform: (receivedHeartbeats: Option<Codec>): boolean =>
|
||||
receivedHeartbeats.isSome
|
||||
};
|
||||
|
||||
function useHeartbeatImpl ({ stashId, stashIndex }: Validator, { currentSession }: SessionInfo): UseHeartbeat {
|
||||
const { api } = useApi();
|
||||
|
||||
const params = useMemo(
|
||||
() => stashIndex === -1
|
||||
? undefined
|
||||
: currentSession && ({
|
||||
authoredBlocks: [currentSession, stashId],
|
||||
receivedHeartbeats: [currentSession, stashIndex]
|
||||
}),
|
||||
[currentSession, stashId, stashIndex]
|
||||
);
|
||||
|
||||
const authoredBlocks = useCall(params && api.query.imOnline.authoredBlocks, params?.authoredBlocks, OPT_BLOCKS);
|
||||
const receivedHeartbeats = useCall(params && api.query.imOnline.receivedHeartbeats, params?.receivedHeartbeats, OPT_BEATS);
|
||||
|
||||
const result = useMemo(
|
||||
() => isNumber(authoredBlocks) && isBoolean(receivedHeartbeats) && ({
|
||||
authoredBlocks,
|
||||
isOnline: !!(authoredBlocks || receivedHeartbeats)
|
||||
}),
|
||||
[authoredBlocks, receivedHeartbeats]
|
||||
);
|
||||
|
||||
return useCacheMap('useHeartbeat', stashId, result) || EMPTY;
|
||||
}
|
||||
|
||||
export default createNamedHook('useHeartbeat', useHeartbeatImpl);
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2017-2026 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PezpalletStakingEraRewardPoints } from '@pezkuwi/types/lookup';
|
||||
import type { SessionInfo } from '../types.js';
|
||||
import type { UsePoints } from './types.js';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
import { useCacheValue } from '../useCache.js';
|
||||
|
||||
const OPT_POINTS = {
|
||||
transform: ({ individual }: PezpalletStakingEraRewardPoints): UsePoints =>
|
||||
[...individual.entries()]
|
||||
.filter(([, points]) => points.gt(BN_ZERO))
|
||||
.reduce((result: UsePoints, [stashId, points]): UsePoints => {
|
||||
result[stashId.toString()] = points.toNumber();
|
||||
|
||||
return result;
|
||||
}, {})
|
||||
};
|
||||
|
||||
function usePointsImpl ({ activeEra }: SessionInfo): UsePoints | undefined {
|
||||
const { api } = useApi();
|
||||
|
||||
const queryParams = useMemo(
|
||||
() => activeEra && [activeEra],
|
||||
[activeEra]
|
||||
);
|
||||
|
||||
const points = useCall(queryParams && api.query.staking.erasRewardPoints, queryParams, OPT_POINTS);
|
||||
|
||||
return useCacheValue('usePoints', points);
|
||||
}
|
||||
|
||||
export default createNamedHook('usePoints', usePointsImpl);
|
||||
Reference in New Issue
Block a user