diff --git a/packages/page-staking-async/src/CommandCenter/ah.tsx b/packages/page-staking-async/src/CommandCenter/ah.tsx
index b01193cecd..e8cb3e1a70 100644
--- a/packages/page-staking-async/src/CommandCenter/ah.tsx
+++ b/packages/page-staking-async/src/CommandCenter/ah.tsx
@@ -3,208 +3,301 @@
import type { ReactNode } from 'react';
import type { ApiPromise } from '@polkadot/api';
-import type { IAhOutput } from './index.js';
+import type { EnhancedEvent, IAhOutput } from './index.js';
import React from 'react';
import { Link } from 'react-router-dom';
-import { CardSummary, Expander, Icon, MarkWarning, Spinner, styled, Tooltip } from '@polkadot/react-components';
+import { CardSummary, Expander, Spinner, styled } from '@polkadot/react-components';
import { Event as EventDisplay } from '@polkadot/react-params';
-import { formatNumber } from '@polkadot/util';
+import { formatBalance, formatNumber } from '@polkadot/util';
import { useTranslation } from '../translate.js';
interface Props {
children: ReactNode;
ahApi?: ApiPromise;
- ahOutput: IAhOutput[];
+ ahOutput?: IAhOutput;
+ ahEvents: EnhancedEvent[];
ahUrl: string;
isRelayChain: boolean
}
-function AssetHubSection ({ ahApi, ahOutput, ahUrl, children, isRelayChain }: Props) {
+function AssetHubSection ({ ahApi, ahEvents, ahOutput, ahUrl, children, isRelayChain }: Props) {
const { t } = useTranslation();
return (
-
-
- {t('Asset Hub chain')}
- {children}
-
- {!ahApi &&
}
-
- {ahOutput.map((ah) => {
- const uniquePages = Array.from(new Set(ah.multiblock.snapshotRange.map((p) => Number(p.toString())))).sort((a, b) => a - b);
- const [minPage, maxPage] = [uniquePages[0], uniquePages[uniquePages.length - 1]];
+
+
+
+ {t('Asset Hub')}
+ {children}
+
+ {!ahApi && }
+ {ahOutput && (
+
+
+ {!isRelayChain
+ ? #{formatNumber(ahOutput.finalizedBlock)}
+ : (
+
+ #{formatNumber(ahOutput.finalizedBlock)}
+ )
+ }
+
+
+
{t('Staking')}
+
+
+ {ahOutput.staking.currentEra}
+
+
+ {ahOutput.staking.activeEra.index}
+ {ahOutput.staking.activeEra.duration && ` (${ahOutput.staking.activeEra.duration})`}
+
+
+ {ahOutput.staking.erasStartSessionIndex || 'None'}
+ {ahOutput.rcClient.eraDepth && `, Era-depth: ${ahOutput.rcClient.eraDepth} sessions`}
+
+
+ {ahOutput.staking.bondedEras && ahOutput.staking.bondedEras.length > 0
+ ? (() => {
+ const firstEra = ahOutput.staking.bondedEras[0];
+ const lastEra = ahOutput.staking.bondedEras[ahOutput.staking.bondedEras.length - 1];
+
+ return `[(${firstEra[0].toString()}@${firstEra[1].toString()}) -> (${lastEra[0].toString()}@${lastEra[1].toString()})]`;
+ })()
+ : 'None'}
+
+
+ {ahOutput.staking.unprunedEras}
+
+
+ {ahOutput.staking.forcing || 'None'}
+
+
+
+
+
{t('Staking RC Client')}
+
+
+ End={ahOutput.rcClient.lastSessionReportEndIndex}, Start={ahOutput.rcClient.lastSessionIndex}
+
+
+
+
+
{t('Election')}
+
+
+ {ahOutput.multiblock.phase}
+
+
+ {ahOutput.multiblock.round}
+
+
+ {ahOutput.multiblock.signedSubmissions}
+
+
+ {ahOutput.multiblock.queuedScore || 'None'}
+
+
+ {ahOutput.multiblock.snapshotRange.length > 0
+ ? (() => {
+ const uniquePages = Array.from(new Set(ahOutput.multiblock.snapshotRange.map((p) => Number(p.toString())))).sort((a, b) => a - b);
+ const [minPage, maxPage] = [uniquePages[0], uniquePages[uniquePages.length - 1]];
+
+ return `${minPage} → ${maxPage}`;
+ })()
+ : 'None'}
+
+
+
+
+
{t('Validators/Nominators')}
+
+
+ {ahOutput.staking.validatorCount || 'None'}
+
+
+ {ahOutput.staking.validatorCandidates
+ ? `${ahOutput.staking.validatorCandidates}${ahOutput.staking.maxValidatorsCount ? ` (max: ${ahOutput.staking.maxValidatorsCount})` : ''}`
+ : 'None'}
+
+
+ {ahOutput.staking.nominatorCandidates
+ ? `${ahOutput.staking.nominatorCandidates}${ahOutput.staking.maxNominatorsCount ? ` (max: ${ahOutput.staking.maxNominatorsCount})` : ''}`
+ : 'None'}
+
+
+ {ahOutput.staking.minNominatorBond || ahOutput.staking.minValidatorBond || ahOutput.staking.minNominatorActiveStake
+ ? `${ahOutput.staking.minNominatorBond ? `N: ${formatBalance(ahOutput.staking.minNominatorBond, { forceUnit: '-', withSi: true })}` : ''}${ahOutput.staking.minValidatorBond ? ` / V: ${formatBalance(ahOutput.staking.minValidatorBond, { forceUnit: '-', withSi: true })}` : ''}${ahOutput.staking.minNominatorActiveStake ? ` / Active: ${formatBalance(ahOutput.staking.minNominatorActiveStake, { forceUnit: '-', withSi: true })}` : ''}`
+ : 'None'}
+
+
+
+
+
{t('Bags List')}
+
+
+ {/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
+ {ahOutput.bagsList ? ahOutput.bagsList.allNodes : 'None'}
+
+
+ {/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
+ {ahOutput.bagsList ? ahOutput.bagsList.lock : 'None'}
+
+
+
+
+ )}
+
+
+ {t('Asset Hub Events')}
+ {ahEvents.length === 0 && (
+
+ {t('No relevant events in recent blocks')}
+
+ )}
+ {ahEvents.map((enhancedEvent, index) => {
+ const { blockNumber, event, weight } = enhancedEvent;
+ const eventName = `${event.section}.${event.method}`;
return (
-
-
-
-
+
+ {eventName}
+
+ {weight && [{weight}]}
{!isRelayChain
- ? #{formatNumber(ah.finalizedBlock)}
+ ? #{formatNumber(blockNumber)}
: (
- #{formatNumber(ah.finalizedBlock)}
+ #{formatNumber(blockNumber)}
)
}
-
-
-
-
-
-
- {ah.staking.currentEra}
-
-
- {ah.staking.erasStartSessionIndex}
-
-
- {ah.staking.activeEra.index}
-
-
-
- {ah.multiblock.phase}
-
- {ah.multiblock.queuedScore &&
-
- {ah.multiblock.queuedScore}
- }
-
- {ah.multiblock.signedSubmissions}
-
- {!!ah.multiblock.snapshotRange.length &&
-
- {`${minPage} → ${maxPage}`}
- }
-
-
-
- {ah.rcClient.lastSessionReportEndIndex}
-
-
-
-
-
-
{t('events')}
- {ah.events.map((event) => {
- const eventName = `${event.section}.${event.method}`;
-
- return (
-
-
-
-
- );
- })}
- {ah.events.length === 0 &&
-
-
-
-
-
- }
-
-
+ >
+ }
+ summaryMeta={event.meta}
+ >
+
+
);
})}
-
-
);
+
+
+ );
}
-const StyledSection = styled.section`
- margin-block: 1rem;
- overflow: auto;
- white-space: nowrap;
+const StyledColumn = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+`;
- .warning {
- max-width: fit-content;
- margin-left: 0;
+const StyledInfoBox = styled.div`
+ background: var(--bg-table);
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+
+ h2 {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ font-size: var(--font-size-h2);
+ font-weight: 500;
}
- .ui--Labelled-content {
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-normal);
- }
+ .info-content {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
- .assethub__chain {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- place-items: start;
- background: var(--bg-table);
- margin-bottom: 0.35rem;
- padding: 0.8rem 1rem;
- border-radius: 0.5rem;
+ .block-info {
+ font-size: var(--font-size-h3);
+ font-weight: 600;
+ }
- .details {
- display: flex;
- align-items: center;
- gap: 0.8rem;
-
- .session__summary {
- display: flex;
- align-items: center;
-
- h4 {
- font-weight: 400;
- font-size: medium;
- }
- }
-
- .staking__summary {
- display: flex;
- justify-content: end;
- }
-
- .multiblock__summary, .rcClient__summary {
- display: flex;
- justify-content: end;
- margin-top: 1.5rem;
+ .section {
+ h4 {
+ font-size: var(--font-size-base);
+ font-weight: 500;
+ color: var(--color-text-secondary);
+ text-transform: uppercase;
+ margin-bottom: 0.5rem;
+ letter-spacing: 0.05em;
}
}
- .events__summary {
- display: grid;
+ .stats {
+ display: flex;
+ flex-wrap: wrap;
gap: 1rem;
- justify-self: center;
- h3 {
- font-weight: 500;
- font-size: var(--font-size-h2);
+ }
+
+ .ui--Labelled {
+ label {
+ font-size: medium !important;
+ text-align: left !important;
}
- .warning__tooltip {
- display: flex;
- align-items: center;
- gap: 0.5rem;
+
+ .ui--Labelled-content {
+ font-size: small !important;
+ text-align: left !important;
+ }
+ }
+ }
+`;
+
+const StyledEventsBox = styled.div`
+ background: var(--bg-table);
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+ max-height: 500px;
+ overflow-y: auto;
+
+ h3 {
+ margin-bottom: 1rem;
+ font-size: var(--font-size-h3);
+ font-weight: 500;
+ }
+
+ .no-events {
+ color: var(--color-text-secondary);
+ font-style: italic;
+ padding: 1rem;
+ text-align: center;
+ }
+
+ .ui--Expander {
+ margin-bottom: 0.5rem;
+ position: relative;
+
+ .absolute {
+ position: absolute;
+ right: 0.5rem;
+ top: 0.72rem;
+ white-space: nowrap;
+
+ .weight {
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-small);
+ margin-right: 0.5rem;
}
}
}
diff --git a/packages/page-staking-async/src/CommandCenter/index.tsx b/packages/page-staking-async/src/CommandCenter/index.tsx
index 9233467996..eb94b99534 100644
--- a/packages/page-staking-async/src/CommandCenter/index.tsx
+++ b/packages/page-staking-async/src/CommandCenter/index.tsx
@@ -2,20 +2,28 @@
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
-import type { AccountId32, Event } from '@polkadot/types/interfaces';
+import type { AccountId32, Event, Hash } from '@polkadot/types/interfaces';
+import type { FrameSupportDispatchPerDispatchClassWeight, PolkadotRuntimeParachainsConfigurationHostConfiguration } from '@polkadot/types/lookup';
import type { IEventData, ITuple } from '@polkadot/types/types';
import type { u32, Vec } from '@polkadot/types-codec';
import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { Dropdown, styled } from '@polkadot/react-components';
+import { Button, Dropdown, Input, MarkWarning, styled } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { getApi } from '@polkadot/react-hooks/ctx/StakingAsync';
+import { formatBalance } from '@polkadot/util';
import AssetHubSection from './ah.js';
import RelaySection from './relay.js';
-const MAX_EVENTS = 25;
+// Enhanced event type with metadata
+export interface EnhancedEvent {
+ event: Event;
+ blockNumber: number;
+ blockHash: string;
+ weight?: string;
+}
export interface IRcOutput {
finalizedBlock: number,
@@ -27,21 +35,42 @@ export interface IRcOutput {
stakingAhClient: {
mode: string
hasNextActiveId?: number,
- hasQueuedInClient?: [number, AccountId32[]]
+ hasQueuedInClient?: [number, AccountId32[]],
+ validatorPoints: number
},
- events: Event[]
+ staking: {
+ forceEra?: string,
+ validatorCount?: number,
+ electionPhase?: string
+ },
+ parachainConfig?: {
+ maxDownwardMessageSize: number,
+ maxUpwardMessageSize: number
+ }
}
export interface IAhOutput {
finalizedBlock: number,
staking: {
currentEra: number,
- activeEra: {index: number, start: string},
+ activeEra: {index: number, start: string, duration?: string},
erasStartSessionIndex?: number,
- bondedEras: Vec>
+ bondedEras: Vec>,
+ unprunedEras: string,
+ validatorCount?: number,
+ validatorCandidates?: number,
+ nominatorCandidates?: number,
+ maxValidatorsCount?: number,
+ maxNominatorsCount?: number,
+ minNominatorBond?: string,
+ minValidatorBond?: string,
+ minNominatorActiveStake?: string,
+ forcing?: string
},
rcClient: {
- lastSessionReportEndIndex: string
+ lastSessionReportEndIndex: string,
+ lastSessionIndex: number,
+ eraDepth?: number
},
multiblock: {
phase: string,
@@ -50,14 +79,45 @@ export interface IAhOutput {
queuedScore: string|null,
signedSubmissions: number
},
- events: Event[]
+ bagsList?: {
+ allNodes: number,
+ lock: string
+ }
}
+// Filter functions for relevant events
+const filterRcEvents = (e: IEventData): boolean => {
+ const ahClientEvents = (e: IEventData) =>
+ e.section === 'stakingAhClient';
+ const sessionEvents = (e: IEventData) =>
+ e.section === 'session' || e.section === 'historical';
+
+ return ahClientEvents(e) || sessionEvents(e);
+};
+
+const filterAhEvents = (e: IEventData): boolean => {
+ const election = (e: IEventData) =>
+ e.section === 'multiBlockElection' ||
+ e.section === 'multiBlockElectionVerifier' ||
+ e.section === 'multiBlockElectionSigned' ||
+ e.section === 'multiBlockElectionUnsigned';
+ const rcClient = (e: IEventData) => e.section === 'stakingRcClient';
+ const staking = (e: IEventData) =>
+ e.section === 'staking' &&
+ (e.method === 'EraPaid' ||
+ e.method === 'SessionRotated' ||
+ e.method === 'PagedElectionProceeded');
+
+ return election(e) || rcClient(e) || staking(e);
+};
+
const commandCenterHandler = async (
rcApi: ApiPromise,
ahApi: ApiPromise,
- setRcOutout: React.Dispatch>,
- setAhOutout: React.Dispatch>
+ setRcOutput: React.Dispatch>,
+ setAhOutput: React.Dispatch>,
+ setRcEvents: React.Dispatch>,
+ setAhEvents: React.Dispatch>
): Promise => {
await rcApi.rpc.chain.subscribeFinalizedHeads(async (header) => {
// --- RC:
@@ -77,64 +137,110 @@ const commandCenterHandler = async (
// Operating mode of the client.
const mode = await rcApi.query.stakingAhClient.mode();
+ // Staking info from RC if available
+ const forceEra = rcApi.query.staking?.forceEra ? await rcApi.query.staking.forceEra() : undefined;
+ const validatorCount = rcApi.query.staking?.validatorCount ? await rcApi.query.staking.validatorCount() : undefined;
+ const electionPhase = rcApi.query.electionProviderMultiPhase?.currentPhase ? await rcApi.query.electionProviderMultiPhase.currentPhase() : undefined;
+
+ // Validator points
+ const validatorPointsKeys = await rcApi.query.stakingAhClient.validatorPoints.keys();
+
+ // Parachain config
+ let parachainConfig;
+
+ try {
+ const configuration = await rcApi.query.configuration.activeConfig() as PolkadotRuntimeParachainsConfigurationHostConfiguration;
+
+ parachainConfig = {
+ maxDownwardMessageSize: configuration.maxDownwardMessageSize?.toNumber() || 0,
+ maxUpwardMessageSize: configuration.maxUpwardMessageSize?.toNumber() || 0
+ };
+ } catch {
+ parachainConfig = undefined;
+ }
+
// Events that we are interested in from RC:
const eventsOfInterest = (await (await rcApi.at(header.hash.toHex())).query.system.events())
.map((e) => e.event)
- .filter((e) => {
- const ahClientEvents = (e: IEventData) =>
- e.section === 'stakingAhClient';
- const sessionEvents = (e: IEventData) =>
- e.section === 'session' || e.section === 'historical';
+ .filter((e) => filterRcEvents(e.data));
- return ahClientEvents(e.data) || sessionEvents(e.data);
- });
+ // Wrap events with metadata
+ const enhancedEvents: EnhancedEvent[] = eventsOfInterest.map((event) => ({
+ blockHash: header.hash.toString(),
+ blockNumber: header.number.toNumber(),
+ event
+ }));
- setRcOutout((prev) => {
- const newItem = {
- events: eventsOfInterest,
- finalizedBlock: header.number.toNumber(),
- session: {
- hasQueuedInSession: hasQueuedInSession.isTrue,
- historicalRange: historicalRange.isSome
- ? [historicalRange.unwrap()[0].toNumber(), historicalRange.unwrap()[1].toNumber()] as [number, number]
- : undefined,
- index: index.toNumber()
- },
- stakingAhClient: {
- hasNextActiveId: hasNextActiveId.isEmpty
- ? undefined
- : rcApi.createType('Option', hasNextActiveId).unwrap().toNumber(),
- hasQueuedInClient: (() => {
- const parsed = rcApi.createType('Option<(u32,Vec)>', hasQueuedInClient);
+ setRcOutput({
+ finalizedBlock: header.number.toNumber(),
+ parachainConfig,
+ session: {
+ hasQueuedInSession: hasQueuedInSession.isTrue,
+ historicalRange: historicalRange.isSome
+ ? [historicalRange.unwrap()[0].toNumber(), historicalRange.unwrap()[1].toNumber()] as [number, number]
+ : undefined,
+ index: index.toNumber()
+ },
+ staking: {
+ electionPhase: electionPhase?.toString(),
+ forceEra: forceEra?.toString(),
+ validatorCount: validatorCount?.toNumber()
+ },
+ stakingAhClient: {
+ hasNextActiveId: hasNextActiveId.isEmpty
+ ? undefined
+ : rcApi.createType('Option', hasNextActiveId).unwrap().toNumber(),
+ hasQueuedInClient: (() => {
+ const parsed = rcApi.createType('Option<(u32,Vec)>', hasQueuedInClient);
- return parsed.isNone ? undefined : [parsed.unwrap()[0].toNumber(), parsed.unwrap()[1]] as [number, AccountId32[]];
- })(),
- mode: mode.toString()
- }
- };
-
- return [newItem, ...prev]
- .reduce((acc, curr) => {
- if (!acc.find((item) => item.finalizedBlock === curr.finalizedBlock)) {
- acc.push(curr);
- }
-
- return acc;
- }, [] as typeof prev)
- .slice(0, MAX_EVENTS);
+ return parsed.isNone ? undefined : [parsed.unwrap()[0].toNumber(), parsed.unwrap()[1]] as [number, AccountId32[]];
+ })(),
+ mode: mode.toString(),
+ validatorPoints: validatorPointsKeys.length
+ }
});
+
+ if (enhancedEvents.length > 0) {
+ setRcEvents(enhancedEvents);
+ }
});
await ahApi.rpc.chain.subscribeFinalizedHeads(async (header) => {
// the current planned era
- const currentEra = await rcApi.query.session.currentIndex();
+ const currentEra = (await ahApi.query.staking.currentEra()).unwrap().toNumber();
// the active era
const activeEra = (await ahApi.query.staking.activeEra()).unwrap();
+ const activeEraDuration = activeEra.start.isSome
+ ? (() => {
+ const startTime = activeEra.start.unwrap().toNumber();
+ const durationMs = Date.now() - startTime;
+ const hours = Math.floor(durationMs / 3600000);
+ const minutes = Math.floor((durationMs % 3600000) / 60000);
+ const seconds = Math.floor((durationMs % 60000) / 1000);
+
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+ })()
+ : undefined;
+
// the starting index of the active era
const bondedEras = await ahApi.query.staking.bondedEras();
const activeEraStartSessionIndex = bondedEras.find(([e]) => e.eq(activeEra.index))?.[1];
- // the starting index of the active era
- // const erasStartSessionIndex = await ahApi.query.staking.erasStartSessionIndex(activeEra.unwrap().index);
+
+ // Unpruned eras
+ const unprunedEras = ahApi.query.staking.eraPruningState
+ ? (await ahApi.query.staking.eraPruningState.entries()).map(([k]) => k.args[0]).sort().join(', ')
+ : 'unimplemented!';
+
+ // Additional staking info
+ const validatorCount = ahApi.query.staking.validatorCount ? await ahApi.query.staking.validatorCount() : undefined;
+ const validatorCandidates = ahApi.query.staking.counterForValidators ? await ahApi.query.staking.counterForValidators() : undefined;
+ const nominatorCandidates = ahApi.query.staking.counterForNominators ? await ahApi.query.staking.counterForNominators() : undefined;
+ const maxValidatorsCount = ahApi.query.staking.maxValidatorsCount ? await ahApi.query.staking.maxValidatorsCount() : undefined;
+ const maxNominatorsCount = ahApi.query.staking.maxNominatorsCount ? await ahApi.query.staking.maxNominatorsCount() : undefined;
+ const minNominatorBond = ahApi.query.staking.minNominatorBond ? await ahApi.query.staking.minNominatorBond() : undefined;
+ const minValidatorBond = ahApi.query.staking.minValidatorBond ? await ahApi.query.staking.minValidatorBond() : undefined;
+ const minNominatorActiveStake = ahApi.query.staking.minimumActiveStake ? await ahApi.query.staking.minimumActiveStake() : undefined;
+ const forcing = ahApi.query.staking.forceEra ? await ahApi.query.staking.forceEra() : undefined;
// the basic state of the election provider
const phase = await ahApi.query.multiBlockElection.currentPhase();
@@ -150,57 +256,130 @@ const commandCenterHandler = async (
await ahApi.query.multiBlockElectionSigned.sortedScores(round);
// The client
- const lastSessionReportEndIndex = await ahApi.query.stakingRcClient.lastSessionReportEndingIndex();
+ const lastSessionReportEndIndexRaw = await ahApi.query.stakingRcClient.lastSessionReportEndingIndex();
+ const lastSessionReportEndIndex = ahApi.createType('Option', lastSessionReportEndIndexRaw);
+ const lastSessionIndex = lastSessionReportEndIndex.isSome ? lastSessionReportEndIndex.unwrap().toNumber() + 1 : 0;
+ const eraDepth = activeEraStartSessionIndex ? lastSessionIndex - activeEraStartSessionIndex.toNumber() : undefined;
- // Events that we are interested in from RC:
+ // Bags list
+ let bagsList;
+
+ try {
+ const allNodes = await ahApi.query.voterList.counterForListNodes();
+ const lock = await ahApi.query.voterList.lock();
+
+ bagsList = {
+ allNodes: allNodes.toNumber(),
+ lock: lock.toU8a().toString()
+ };
+ } catch {
+ bagsList = undefined;
+ }
+
+ // Events that we are interested in from AH:
const eventsOfInterest = (await (await ahApi.at(header.hash.toHex())).query.system.events())
.map((e) => e.event)
- .filter((e) => {
- const election = (e: IEventData) => e.section === 'multiBlockElection' || e.section === 'multiBlockElectionVerifier' || e.section === 'multiBlockElectionSigned' || e.section === 'multiBlockElectionUnsigned';
- const rcClient = (e: IEventData) => e.section === 'stakingRcClient';
- const staking = (e: IEventData) => e.section === 'staking' && (e.method === 'EraPaid' || e.method === 'SessionRotated' || e.method === 'PagedElectionProceeded');
+ .filter((e) => filterAhEvents(e.data));
- return election(e.data) || rcClient(e.data) || staking(e.data);
- });
+ // Get block weight
+ const weight = await (await ahApi.at(header.hash)).query.system.blockWeight();
- setAhOutout((prev) => {
- const parsedQueuedScore = ahApi.createType('Option', queuedScore);
+ const formatWeight = (w: FrameSupportDispatchPerDispatchClassWeight) => {
+ const normalRef = w.normal?.refTime?.toBigInt() || 0n;
+ const operationalRef = w.operational?.refTime?.toBigInt() || 0n;
+ const mandatoryRef = w.mandatory?.refTime?.toBigInt() || 0n;
+ const totalRef = normalRef + operationalRef + mandatoryRef;
- const newItem = {
- events: eventsOfInterest,
- finalizedBlock: header.number.toNumber(),
- multiblock: {
- phase: phase.toString(),
- queuedScore: parsedQueuedScore.isSome ? parsedQueuedScore.unwrap().toString() : null,
- round: ahApi.createType('u32', round).toNumber(),
- signedSubmissions: ahApi.createType(
- 'Vec<(AccountId32,SpNposElectionsElectionScore)>',
- signedSubmissions
- ).length,
- snapshotRange: snapshotRange.map((a) => a.toString())
- },
- rcClient: { lastSessionReportEndIndex: lastSessionReportEndIndex.toString() },
- staking: {
- activeEra: {
- index: activeEra.index.toNumber(),
- start: activeEra.toString()
- },
- bondedEras,
- currentEra: currentEra.toNumber(),
- erasStartSessionIndex: activeEraStartSessionIndex?.toNumber()
+ const normalProof = w.normal?.proofSize?.toBigInt() || 0n;
+ const operationalProof = w.operational?.proofSize?.toBigInt() || 0n;
+ const mandatoryProof = w.mandatory?.proofSize?.toBigInt() || 0n;
+ const totalProof = normalProof + operationalProof + mandatoryProof;
+
+ return `${(Number(totalRef) / 1_000_000_000_000).toFixed(2)}s / ${(Number(totalProof) / 1024).toFixed(0)}KB`;
+ };
+
+ // Wrap events with metadata including weight
+ const enhancedEvents: EnhancedEvent[] = eventsOfInterest.map((event) => ({
+ blockHash: header.hash.toString(),
+ blockNumber: header.number.toNumber(),
+ event,
+ weight: formatWeight(weight)
+ }));
+
+ const parsedQueuedScore = ahApi.createType('Option', queuedScore);
+ const formattedQueuedScore = parsedQueuedScore.isSome
+ ? (() => {
+ const score = parsedQueuedScore.unwrap();
+ const minimalStake = score.minimalStake?.toString() || '0';
+ const formattedMinStake = formatBalance(minimalStake, { forceUnit: '-', withSi: true });
+
+ return `minStake: ${formattedMinStake}, ...`;
+ })()
+ : null;
+
+ // Format phase to be more readable (e.g., "Unsigned(1)" instead of {"unsigned":1})
+ const formattedPhase = (() => {
+ const phaseStr = phase.toString();
+
+ try {
+ const phaseJson = JSON.parse(phaseStr) as Record;
+
+ if (typeof phaseJson === 'object' && phaseJson !== null) {
+ const key = Object.keys(phaseJson)[0];
+ const value = phaseJson[key];
+
+ return `${key.charAt(0).toUpperCase() + key.slice(1)}(${value})`;
}
- };
+ } catch {
+ // If not JSON, return as is
+ }
- return [newItem, ...prev]
- .reduce((acc, curr) => {
- if (!acc.find((item) => item.finalizedBlock === curr.finalizedBlock)) {
- acc.push(curr);
- }
+ return phaseStr;
+ })();
- return acc;
- }, [] as typeof prev)
- .slice(0, MAX_EVENTS);
+ setAhOutput({
+ bagsList,
+ finalizedBlock: header.number.toNumber(),
+ multiblock: {
+ phase: formattedPhase,
+ queuedScore: formattedQueuedScore,
+ round: ahApi.createType('u32', round).toNumber(),
+ signedSubmissions: ahApi.createType(
+ 'Vec<(AccountId32,SpNposElectionsElectionScore)>',
+ signedSubmissions
+ ).length,
+ snapshotRange: snapshotRange.map((a) => a.toString())
+ },
+ rcClient: {
+ eraDepth,
+ lastSessionIndex,
+ lastSessionReportEndIndex: lastSessionReportEndIndex.isSome ? lastSessionReportEndIndex.unwrap().toString() : '0'
+ },
+ staking: {
+ activeEra: {
+ duration: activeEraDuration,
+ index: activeEra.index.toNumber(),
+ start: activeEra.toString()
+ },
+ bondedEras,
+ currentEra,
+ erasStartSessionIndex: activeEraStartSessionIndex?.toNumber(),
+ forcing: forcing?.toString(),
+ maxNominatorsCount: maxNominatorsCount?.isSome ? maxNominatorsCount.unwrap().toNumber() : undefined,
+ maxValidatorsCount: maxValidatorsCount?.isSome ? maxValidatorsCount.unwrap().toNumber() : undefined,
+ minNominatorActiveStake: minNominatorActiveStake?.toString(),
+ minNominatorBond: minNominatorBond?.toString(),
+ minValidatorBond: minValidatorBond?.toString(),
+ nominatorCandidates: nominatorCandidates?.toNumber(),
+ unprunedEras,
+ validatorCandidates: validatorCandidates?.toNumber(),
+ validatorCount: validatorCount?.toNumber()
+ }
});
+
+ if (enhancedEvents.length > 0) {
+ setAhEvents(enhancedEvents);
+ }
});
};
@@ -215,8 +394,10 @@ interface Props {
function CommandCenter ({ ahApi: initialAhApi, ahEndPoints, isRelayChain, rcApi: initialRcApi, rcEndPoints }: Props) {
const { api, apiUrl } = useApi();
- const [rcOutput, setRcOutput] = useState([]);
- const [ahOutput, setAhOutput] = useState([]);
+ const [rcOutput, setRcOutput] = useState(undefined);
+ const [ahOutput, setAhOutput] = useState(undefined);
+ const [rcEvents, setRcEvents] = useState([]);
+ const [ahEvents, setAhEvents] = useState([]);
const [rcUrl, setRcUrl] = useState(undefined);
const [ahUrl, setAhUrl] = useState(undefined);
@@ -224,23 +405,236 @@ function CommandCenter ({ ahApi: initialAhApi, ahEndPoints, isRelayChain, rcApi:
const [ahApi, setAhApi] = useState(initialAhApi);
const [rcApi, setRcApi] = useState(initialRcApi);
- const rcEndPointOptions = useRef(rcEndPoints.map((e) => ({ text: e, value: e })));
- const ahEndPointOptions = useRef(ahEndPoints.map((e) => ({ text: e, value: e })));
+ const [isLoadingHistory, setIsLoadingHistory] = useState(false);
+ const [rcLowestBlock, setRcLowestBlock] = useState(null);
+ const [ahLowestBlock, setAhLowestBlock] = useState(null);
+ const [loadingProgress, setLoadingProgress] = useState<{ rc: number; ah: number } | null>(null);
+
+ const [customRcUrl, setCustomRcUrl] = useState('ws://127.0.0.1:9944');
+ const [customAhUrl, setCustomAhUrl] = useState('ws://127.0.0.1:9944');
+ const [showCustomRcInput, setShowCustomRcInput] = useState(false);
+ const [showCustomAhInput, setShowCustomAhInput] = useState(false);
+
+ const CUSTOM_OPTION = 'custom';
+ const rcEndPointOptions = useRef([...rcEndPoints.map((e) => ({ text: e, value: e })), { text: 'Custom endpoint...', value: CUSTOM_OPTION }]);
+ const ahEndPointOptions = useRef([...ahEndPoints.map((e) => ({ text: e, value: e })), { text: 'Custom endpoint...', value: CUSTOM_OPTION }]);
+
+ // Load historical events from RC
+ const loadRcHistoricalEvents = useCallback(async (blocksToLoad: number) => {
+ if (!rcApi) {
+ return;
+ }
+
+ let currentBlockHash: Hash;
+
+ if (rcLowestBlock === null) {
+ const finalizedHead = await rcApi.rpc.chain.getFinalizedHead();
+
+ currentBlockHash = finalizedHead;
+ } else if (rcLowestBlock === 0) {
+ return;
+ } else {
+ currentBlockHash = await rcApi.rpc.chain.getBlockHash(rcLowestBlock);
+ }
+
+ let blocksProcessed = 0;
+
+ while (blocksProcessed < blocksToLoad) {
+ try {
+ const block = await rcApi.rpc.chain.getBlock(currentBlockHash);
+ const blockNumber = block.block.header.number.toNumber();
+
+ if (blockNumber === 0) {
+ setRcLowestBlock(0);
+ break;
+ }
+
+ const events = await (await rcApi.at(currentBlockHash)).query.system.events();
+ const relevantEvents = events
+ .map((e) => e.event)
+ .filter((e) => filterRcEvents(e.data));
+
+ if (relevantEvents.length > 0) {
+ const enhancedEvents: EnhancedEvent[] = relevantEvents.map((event) => ({
+ blockHash: currentBlockHash.toString(),
+ blockNumber,
+ event
+ }));
+
+ setRcEvents((prev) => {
+ // Insert new events and sort by block number (highest first)
+ const combined = [...prev, ...enhancedEvents];
+
+ return combined.sort((a, b) => b.blockNumber - a.blockNumber);
+ });
+ }
+
+ setRcLowestBlock(blockNumber - 1);
+ currentBlockHash = block.block.header.parentHash;
+ blocksProcessed++;
+
+ // Update progress
+ const remaining = blocksToLoad - blocksProcessed;
+
+ setLoadingProgress((prev) => ({ ah: prev?.ah ?? 0, rc: remaining }));
+ } catch (error) {
+ console.error(`RC: Error at block ${currentBlockHash.toHex()}:`, error);
+ break;
+ }
+ }
+ }, [rcApi, rcLowestBlock]);
+
+ // Load historical events from AH
+ const loadAhHistoricalEvents = useCallback(async (blocksToLoad: number) => {
+ if (!ahApi) {
+ return;
+ }
+
+ let currentBlockHash: Hash;
+
+ if (ahLowestBlock === null) {
+ const finalizedHead = await ahApi.rpc.chain.getFinalizedHead();
+
+ currentBlockHash = finalizedHead;
+ } else if (ahLowestBlock === 0) {
+ return;
+ } else {
+ currentBlockHash = await ahApi.rpc.chain.getBlockHash(ahLowestBlock);
+ }
+
+ let blocksProcessed = 0;
+
+ while (blocksProcessed < blocksToLoad) {
+ try {
+ const block = await ahApi.rpc.chain.getBlock(currentBlockHash);
+ const blockNumber = block.block.header.number.toNumber();
+
+ if (blockNumber === 0) {
+ setAhLowestBlock(0);
+ break;
+ }
+
+ const events = await (await ahApi.at(currentBlockHash)).query.system.events();
+ const relevantEvents = events
+ .map((e) => e.event)
+ .filter((e) => filterAhEvents(e.data));
+
+ if (relevantEvents.length > 0) {
+ // Get block weight
+ const weight = await (await ahApi.at(currentBlockHash)).query.system.blockWeight();
+
+ const formatWeight = (w: FrameSupportDispatchPerDispatchClassWeight) => {
+ const normalRef = w.normal?.refTime?.toBigInt() || 0n;
+ const operationalRef = w.operational?.refTime?.toBigInt() || 0n;
+ const mandatoryRef = w.mandatory?.refTime?.toBigInt() || 0n;
+ const totalRef = normalRef + operationalRef + mandatoryRef;
+
+ const normalProof = w.normal?.proofSize?.toBigInt() || 0n;
+ const operationalProof = w.operational?.proofSize?.toBigInt() || 0n;
+ const mandatoryProof = w.mandatory?.proofSize?.toBigInt() || 0n;
+ const totalProof = normalProof + operationalProof + mandatoryProof;
+
+ return `${(Number(totalRef) / 1_000_000_000_000).toFixed(2)}s / ${(Number(totalProof) / 1024).toFixed(0)}KB`;
+ };
+
+ const enhancedEvents: EnhancedEvent[] = relevantEvents.map((event) => ({
+ blockHash: currentBlockHash.toString(),
+ blockNumber,
+ event,
+ weight: formatWeight(weight)
+ }));
+
+ setAhEvents((prev) => {
+ // Insert new events and sort by block number (highest first)
+ const combined = [...prev, ...enhancedEvents];
+
+ return combined.sort((a, b) => b.blockNumber - a.blockNumber);
+ });
+ }
+
+ setAhLowestBlock(blockNumber - 1);
+ currentBlockHash = block.block.header.parentHash;
+ blocksProcessed++;
+
+ // Update progress
+ const remaining = blocksToLoad - blocksProcessed;
+
+ setLoadingProgress((prev) => ({ ah: remaining, rc: prev?.rc ?? 0 }));
+ } catch (error) {
+ console.error(`AH: Error at block ${currentBlockHash.toHex()}:`, error);
+ break;
+ }
+ }
+ }, [ahApi, ahLowestBlock]);
+
+ // Load historical events (600 blocks at a time)
+ const _loadHistoricalEvents = useCallback(async () => {
+ if (isLoadingHistory || (!rcApi && !ahApi)) {
+ return;
+ }
+
+ setIsLoadingHistory(true);
+ setLoadingProgress({ ah: 600, rc: 600 });
+
+ try {
+ await Promise.all([
+ loadRcHistoricalEvents(600),
+ loadAhHistoricalEvents(600)
+ ]);
+
+ console.log('Historical events loaded');
+ } catch (error) {
+ console.error('Error loading historical events:', error);
+ } finally {
+ setIsLoadingHistory(false);
+ setLoadingProgress(null);
+ }
+ }, [isLoadingHistory, rcApi, ahApi, loadRcHistoricalEvents, loadAhHistoricalEvents]);
const _onSelectAhUrl = useCallback((newAhUrl: string) => {
- if (newAhUrl !== ahUrl) {
+ if (newAhUrl === CUSTOM_OPTION) {
+ setShowCustomAhInput(true);
+ } else if (newAhUrl !== ahUrl) {
ahApi?.disconnect().catch(console.log);
+ setShowCustomAhInput(false);
setAhUrl(newAhUrl);
}
}, [ahApi, ahUrl]);
const _onSelectRcUrl = useCallback((newRcUrl: string) => {
- if (newRcUrl !== rcUrl) {
+ if (newRcUrl === CUSTOM_OPTION) {
+ setShowCustomRcInput(true);
+ } else if (newRcUrl !== rcUrl) {
rcApi?.disconnect().catch(console.log);
+ setShowCustomRcInput(false);
setRcUrl(newRcUrl);
}
}, [rcApi, rcUrl]);
+ const _onCustomRcUrlChange = useCallback((value: string) => {
+ setCustomRcUrl(value);
+ }, []);
+
+ const _onCustomAhUrlChange = useCallback((value: string) => {
+ setCustomAhUrl(value);
+ }, []);
+
+ const _onApplyCustomRcUrl = useCallback(() => {
+ if (customRcUrl?.trim()) {
+ rcApi?.disconnect().catch(console.log);
+ setRcUrl(customRcUrl.trim());
+ setShowCustomRcInput(false);
+ }
+ }, [customRcUrl, rcApi]);
+
+ const _onApplyCustomAhUrl = useCallback(() => {
+ if (customAhUrl?.trim()) {
+ ahApi?.disconnect().catch(console.log);
+ setAhUrl(customAhUrl.trim());
+ setShowCustomAhInput(false);
+ }
+ }, [customAhUrl, ahApi]);
+
useEffect(() => {
if (isRelayChain) {
setRcUrl(apiUrl);
@@ -258,8 +652,10 @@ function CommandCenter ({ ahApi: initialAhApi, ahEndPoints, isRelayChain, rcApi:
useEffect(() => {
setRcApi(undefined);
setAhApi(undefined);
- setRcOutput([]);
- setAhOutput([]);
+ setRcOutput(undefined);
+ setAhOutput(undefined);
+ setRcEvents([]);
+ setAhEvents([]);
if (isRelayChain) {
setRcApi(api);
@@ -277,43 +673,166 @@ function CommandCenter ({ ahApi: initialAhApi, ahEndPoints, isRelayChain, rcApi:
}, [ahUrl, api, isRelayChain, rcUrl]);
useEffect(() => {
- ahApi && rcApi && commandCenterHandler(rcApi, ahApi, setRcOutput, setAhOutput).catch(console.log);
+ ahApi && rcApi && commandCenterHandler(rcApi, ahApi, setRcOutput, setAhOutput, setRcEvents, setAhEvents).catch(console.log);
}, [ahApi, rcApi]);
+ // Auto-load 600 blocks on page load
+ useEffect(() => {
+ if (ahApi && rcApi && rcLowestBlock === null && ahLowestBlock === null) {
+ _loadHistoricalEvents().catch(console.error);
+ }
+ }, [ahApi, rcApi, rcLowestBlock, ahLowestBlock, _loadHistoricalEvents]);
+
return (
+
+
+
+
+
+ {loadingProgress && (
+
+ Loading historical events... RC: {loadingProgress.rc} blocks left | AH: {loadingProgress.ah} blocks left
+
+ )}
+ {!loadingProgress && (rcLowestBlock !== null || ahLowestBlock !== null) && (
+
+ {rcLowestBlock !== null && `RC: Indexed to Block #${rcLowestBlock}`}
+ {rcLowestBlock !== null && ahLowestBlock !== null && ' | '}
+ {ahLowestBlock !== null && `AH: Indexed to Block #${ahLowestBlock}`}
+
+ )}
+
-
+
+
+ {showCustomRcInput && !isRelayChain && (
+
+
+
+
+ )}
+
-
+
+
+ {showCustomAhInput && isRelayChain && (
+
+
+
+
+ )}
+
);
}
+const StyledEndpointControls = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+`;
+
+const StyledCustomInput = styled.div`
+ display: flex;
+ gap: 0.5rem;
+ align-items: flex-end;
+
+ > div {
+ flex: 1;
+ }
+
+ button {
+ margin-bottom: 0.25rem;
+ }
+`;
+
+const StyledWarningBanner = styled.div`
+ grid-column: 1 / -1;
+ padding: 1rem 1.5rem;
+ background: var(--bg-table);
+ border-radius: 0.5rem;
+ border: 2px solid var(--color-warning);
+
+ .ui--MarkWarning {
+ margin: 0;
+ font-size: var(--font-size-h3);
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+`;
+
+const StyledButtonContainer = styled.div`
+ grid-column: 1 / -1;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem;
+ background: var(--bg-table);
+ border-radius: 0.5rem;
+
+ .history-info {
+ font-size: var(--font-size-small);
+ color: var(--color-text-secondary);
+
+ &.progress {
+ color: var(--color-text);
+ font-weight: 500;
+ }
+ }
+`;
+
const StyledDiv = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
@@ -322,7 +841,7 @@ const StyledDiv = styled.div`
.dropdown {
min-width: max-content !important;
}
-
+
@media screen and (max-width: 1200px){
grid-template-columns: repeat(1, 1fr);
}
diff --git a/packages/page-staking-async/src/CommandCenter/relay.tsx b/packages/page-staking-async/src/CommandCenter/relay.tsx
index 6e6750230b..db1c22ce1a 100644
--- a/packages/page-staking-async/src/CommandCenter/relay.tsx
+++ b/packages/page-staking-async/src/CommandCenter/relay.tsx
@@ -3,12 +3,12 @@
import type { ReactNode } from 'react';
import type { ApiPromise } from '@polkadot/api';
-import type { IRcOutput } from './index.js';
+import type { EnhancedEvent, IRcOutput } from './index.js';
import React from 'react';
import { Link } from 'react-router-dom';
-import { CardSummary, Expander, Icon, MarkWarning, Spinner, styled, Tooltip } from '@polkadot/react-components';
+import { CardSummary, Expander, Spinner, styled } from '@polkadot/react-components';
import { Event as EventDisplay } from '@polkadot/react-params';
import { formatNumber } from '@polkadot/util';
@@ -17,170 +17,241 @@ import { useTranslation } from '../translate.js';
interface Props {
children: ReactNode;
rcApi?: ApiPromise;
- rcOutput: IRcOutput[];
+ rcOutput?: IRcOutput;
+ rcEvents: EnhancedEvent[];
rcUrl: string;
isRelayChain: boolean
}
-function RelaySection ({ children, isRelayChain, rcApi, rcOutput, rcUrl }: Props) {
+function RelaySection ({ children, isRelayChain, rcApi, rcEvents, rcOutput, rcUrl }: Props) {
const { t } = useTranslation();
return (
-
-
- {t('Relay chain')}
- {children}
-
- {!rcApi &&
}
-
- {rcOutput.map((rc) => {
- return (
-
-
-
-
- {isRelayChain
- ? #{formatNumber(rc.finalizedBlock)}
- : (
-
- #{formatNumber(rc.finalizedBlock)}
- )
- }
-
-
- #{formatNumber(rc.session.index)}
+
+
+
+ {t('Relay Chain')}
+ {children}
+
+ {!rcApi && }
+ {rcOutput && (
+
+
+ {isRelayChain
+ ? #{formatNumber(rcOutput.finalizedBlock)}
+ : (
+
+ #{formatNumber(rcOutput.finalizedBlock)}
+ )
+ }
+
+
+
{t('Session Info')}
+
+
+ #{formatNumber(rcOutput.session.index)}
+
+
+ {rcOutput.session.hasQueuedInSession ? 'Yes' : 'No'}
+
+ {rcOutput.session.historicalRange &&
+
+ {`#${formatNumber(rcOutput.session.historicalRange[0])} → #${formatNumber(rcOutput.session.historicalRange[1])}`}
- {rc.session.historicalRange &&
-
- {`${rc.session.historicalRange?.[0]} → ${rc.session.historicalRange?.[1]}`}
-
- }
-
-
- {rc.stakingAhClient.hasQueuedInClient &&
-
-
-
- {rc.stakingAhClient.hasQueuedInClient[0]}
-
-
- {rc.stakingAhClient.hasQueuedInClient[1].length}
-
-
}
-
-
-
-
{t('events')}
- {rc.events.map((event) => {
- const eventName = `${event.section}.${event.method}`;
-
- return (
-
-
-
-
- );
- })}
- {rc.events.length === 0 &&
-
-
-
-
-
}
+
+
{t('Staking AH Client')}
+
+
+ {rcOutput.stakingAhClient.mode}
+
+
+ {rcOutput.stakingAhClient.hasNextActiveId !== undefined ? rcOutput.stakingAhClient.hasNextActiveId : 'None'}
+
+
+ {rcOutput.stakingAhClient.validatorPoints}
+
+
+
+
+ {rcOutput.stakingAhClient.hasQueuedInClient ? rcOutput.stakingAhClient.hasQueuedInClient[0] : 'None'}
+
+
+ {rcOutput.stakingAhClient.hasQueuedInClient ? rcOutput.stakingAhClient.hasQueuedInClient[1].length : 'None'}
+
+
+
+
+
{t('Staking/Elections')}
+
+
+ {rcOutput.staking.forceEra || 'None'}
+
+
+ {rcOutput.staking.electionPhase || 'None'}
+
+
+ {rcOutput.staking.validatorCount || 'None'}
+
+
+
+
+
{t('Parachain Config')}
+
+
+ {rcOutput.parachainConfig ? formatNumber(rcOutput.parachainConfig.maxDownwardMessageSize) : 'None'}
+
+
+ {rcOutput.parachainConfig ? formatNumber(rcOutput.parachainConfig.maxUpwardMessageSize) : 'None'}
+
+
+
+
+ )}
+
+
+ {t('Relay Chain Events')}
+ {rcEvents.length === 0 && (
+
+ {t('No relevant events in recent blocks')}
+
+ )}
+ {rcEvents.map((enhancedEvent, index) => {
+ const { blockNumber, event } = enhancedEvent;
+ const eventName = `${event.section}.${event.method}`;
+
+ return (
+
+ {eventName}
+
+ {isRelayChain
+ ? #{formatNumber(blockNumber)}
+ : (
+
+ #{formatNumber(blockNumber)}
+ )
+ }
+
+ >
+ }
+ summaryMeta={event.meta}
+ >
+
+
);
})}
-
- );
+
+
+ );
}
-const StyledSection = styled.section`
- margin-block: 1rem;
- overflow: auto;
+const StyledColumn = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+`;
- .warning {
- max-width: fit-content;
- margin-left: 0;
- }
+const StyledInfoBox = styled.div`
+ background: var(--bg-table);
+ padding: 1.5rem;
+ border-radius: 0.5rem;
- .ui--Labelled-content {
- font-size: var(--font-size-h3);
- font-weight: var(--font-weight-normal);
- }
-
- .warning__tooltip {
+ h2 {
display: flex;
align-items: center;
- gap: 0.5rem;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ font-size: var(--font-size-h2);
+ font-weight: 500;
}
- .relay__chain {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- background: var(--bg-table);
- margin-bottom: 0.35rem;
- padding: 0.8rem 1rem;
- border-radius: 0.5rem;
+ .info-content {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
- .details {
- display: flex;
- flex-direction: column;
- gap: 0.8rem;
+ .block-info {
+ font-size: var(--font-size-h3);
+ font-weight: 600;
+ }
- .session__summary {
- display: flex;
- align-items: center;
-
- h4 {
- font-weight: 400;
- font-size: medium;
- }
- }
-
- .stakingAhClient__summary {
- .stakingAhClient__hasQueuedInClient {
- display: flex;
- justify-content: space-evenly;
- }
+ .section {
+ h4 {
+ font-size: var(--font-size-base);
+ font-weight: 500;
+ color: var(--color-text-secondary);
+ text-transform: uppercase;
+ margin-bottom: 0.5rem;
+ letter-spacing: 0.05em;
}
}
- .events__summary {
- justify-self: center;
- h3 {
- font-weight: 500;
- font-size: var(--font-size-h2);
+ .stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ }
+ }
+
+ .ui--Labelled {
+ label {
+ font-size: medium !important;
+ text-align: left !important;
}
+
+ .ui--Labelled-content {
+ font-size: small !important;
+ text-align: left !important;
+ }
+ }
+`;
+
+const StyledEventsBox = styled.div`
+ background: var(--bg-table);
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+ max-height: 500px;
+ overflow-y: auto;
+
+ h3 {
+ margin-bottom: 1rem;
+ font-size: var(--font-size-h3);
+ font-weight: 500;
+ }
+
+ .no-events {
+ color: var(--color-text-secondary);
+ font-style: italic;
+ padding: 1rem;
+ text-align: center;
+ }
+
+ .ui--Expander {
+ margin-bottom: 0.5rem;
+ position: relative;
+
+ .absolute {
+ position: absolute;
+ right: 0.5rem;
+ top: 0.72rem;
+ white-space: nowrap;
}
}
`;