Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 69 additions & 17 deletions src/components/ChainDataSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -728,35 +728,87 @@ function EthereumChainSections({
);
}

/// Convert planck (u128 wire-string) to a human-readable DOT figure.
/// Uses BigInt to avoid Number's 53-bit precision ceiling — top
/// validators have stake well above 2^53 planck.
function planckToDot(planckStr: string): string {
try {
const planck = BigInt(planckStr);
const denom = 10_000_000_000n; // 10^10
const whole = planck / denom;
const frac = planck % denom;
// Show 4 fractional digits — beyond that the value is noise for UI.
const fracStr = (frac / 1_000_000n).toString().padStart(4, '0');
return `${whole.toLocaleString()}.${fracStr}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The planckToDot function currently mixes whole.toLocaleString() (which uses locale-specific thousands separators) with a hardcoded . decimal separator. In locales where the thousands separator is a dot (e.g., German), this produces ambiguous output like 1.234.5678.

To maintain consistency with the localized integer formatting used elsewhere in the file (via formatNumber), you should dynamically determine the decimal separator or use a fixed locale for both parts to ensure the output is clear and correctly formatted.

    const sep = new Intl.NumberFormat().formatToParts(1.1).find(p => p.type === 'decimal')?.value ?? '.';
    return `${whole.toLocaleString()}${sep}${fracStr}`;

} catch {
return planckStr;
}
}

function PolkadotChainSections({
data,
isMobile,
}: {
data: PolkadotChainData;
isMobile: boolean;
}) {
// Replaces the demoted dot_not_elected feed event. Badge renders
// only when the worker has populated chain_data.is_elected — during
// the deploy gap window where worker is_elected hasn't reached prod
// yet, undefined would render falsy and incorrectly show "No". Better
// to render nothing than to show wrong data.
// Replaces the demoted dot_not_elected feed event. Render only when
// the worker has populated chain_data.is_elected — during the deploy
// gap window where worker is_elected hasn't reached prod yet,
// undefined would render falsy and incorrectly show "No". Better to
// render nothing than to show wrong data.
if (typeof data.is_elected !== 'boolean') {
return null;
}
return (
<Section title="Active Set" isMobile={isMobile}>
<Field
label="Currently Elected"
value={
<span style={{ color: data.is_elected ? 'var(--color-accent)' : 'var(--color-danger)' }}>
{data.is_elected ? 'Yes' : 'No'}
</span>
}
/>
{data.observed_at_block != null && (
<Field label="Observed at Block" value={formatNumber(data.observed_at_block)} />
<>
<Section title="Active Set" isMobile={isMobile}>
<Field
label="Currently Elected"
value={
<span style={{ color: data.is_elected ? 'var(--color-accent)' : 'var(--color-danger)' }}>
{data.is_elected ? 'Yes' : 'No'}
</span>
}
/>
{data.observed_at_block != null && (
<Field label="Observed at Block" value={formatNumber(data.observed_at_block)} />
)}
</Section>

{data.validator_prefs && (
<Section title="Validator Preferences" isMobile={isMobile}>
<Field
label="Commission"
value={`${(data.validator_prefs.commission_bps / 100).toFixed(2)}%`}
/>
<Field
label="Accepting Nominations"
value={
<span
style={{
color: data.validator_prefs.blocked ? 'var(--color-danger)' : 'var(--color-accent)',
}}
>
{data.validator_prefs.blocked ? 'No (blocked)' : 'Yes'}
</span>
}
/>
</Section>
)}
</Section>

{data.era_exposure && (
<Section title="Era Exposure" isMobile={isMobile}>
<Field label="Era" value={formatNumber(data.era_exposure.era_index)} />
<Field label="Total Stake" value={`${planckToDot(data.era_exposure.total_planck)} DOT`} />
<Field
label="Validator Own Stake"
value={`${planckToDot(data.era_exposure.own_planck)} DOT`}
/>
<Field label="Nominators" value={formatNumber(data.era_exposure.nominator_count)} />
</Section>
)}
</>
);
}

Expand Down
20 changes: 20 additions & 0 deletions src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,29 @@ export interface EthereumChainData {
slashed: boolean;
}

export interface PolkadotValidatorPrefs {
/// Commission expressed in basis points (0..=10000) — converted from
/// the chain's Perbill so the wire format matches Solana's commission
/// field. Frontend formats as `bps / 100` to display percent.
commission_bps: number;
blocked: boolean;
}

export interface PolkadotEraExposure {
era_index: number;
/// Planck values are emitted as strings — JS Number can't represent
/// u128 losslessly. Frontend converts to DOT via BigInt division by
/// 10^10.
total_planck: string;
own_planck: string;
nominator_count: number;
}

export interface PolkadotChainData {
is_elected: boolean;
observed_at_block: number;
validator_prefs: PolkadotValidatorPrefs | null;
era_exposure: PolkadotEraExposure | null;
}

// --- Scan Analysis ---
Expand Down
Loading