Skip to content

Commit

Permalink
Feat: Add validators page (#1200)
Browse files Browse the repository at this point in the history
* feat: Add validators endpoint. Add validators collections job with ValidatorService.

* feat: Add committee cache and validators/stats endpoint

* feat: Add inCommittee flag to IValidatorsResponse

* fix: Change stakers field to validators now that is fixed

* feat: Add validator stats endpoint

* feat: Fix validator stats computation

* feat: Add Validators route/page (WiP)

* feat: Add validators table (with hardcoded data)

* feat: Update IValidatorsResponse to match api

* feat: Remove hardcoded validator from page and rehook the hook

* fix: Ignore eslint unresolved nova SDK import

* chore: Ignore more eslint no-unsafe for SDK (CI)

* feat: Align table columns with requirements

* fix: Remove rank by stake computation (as Chronicle already returns results sorted by stake)

* feat: Rename cumulative stake header to pool stake
  • Loading branch information
msarcev authored Mar 12, 2024
1 parent 38d5c11 commit ff4edb5
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 0 deletions.
2 changes: 2 additions & 0 deletions client/src/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import NovaVisualizer from "../features/visualizer-threejs/NovaVisualizer";
import StreamsV0 from "./routes/StreamsV0";
import { StreamsV0RouteProps } from "./routes/StreamsV0RouteProps";
import { VisualizerRouteProps } from "./routes/VisualizerRouteProps";
import ValidatorsPage from "./routes/nova/ValidatorsPage";
import { CHRYSALIS, LEGACY, NOVA, STARDUST } from "~models/config/protocolVersion";

/**
Expand Down Expand Up @@ -188,6 +189,7 @@ const buildAppRoutes = (protocolVersion: string, withNetworkContext: (wrappedCom
<Route path="/:network/transaction/:transactionId" key={keys.next().value} component={NovaTransactionPage} />,
<Route path="/:network/foundry/:foundryId" key={keys.next().value} component={NovaFoundryPage} />,
<Route path="/:network/statistics" key={keys.next().value} component={NovaStatisticsPage} />,
<Route path="/:network/validators" key={keys.next().value} component={ValidatorsPage} />,
];

return (
Expand Down
107 changes: 107 additions & 0 deletions client/src/app/routes/nova/ValidatorsPage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
@import "../../../scss/fonts";
@import "../../../scss/mixins";
@import "../../../scss/media-queries";
@import "../../../scss/variables";

.validators-page {
display: flex;
flex-direction: column;

.wrapper {
display: flex;
justify-content: center;

.inner {
display: flex;
flex: 1;
flex-direction: column;
max-width: $desktop-width;
margin: 40px 25px;

@include desktop-down {
flex: unset;
width: 100%;
max-width: 100%;
margin: 40px 24px;
padding-right: 24px;
padding-left: 24px;

> .row {
flex-direction: column;
}
}

@include tablet-down {
margin: 28px 0;
}

.validators-page__header {
display: flex;
flex-direction: column;
align-items: flex-start;

.header__title {
margin-bottom: 8px;
}
}

.all-validators__section {
font-family: $metropolis;
margin-top: 40px;
background-color: $gray-1;
border-radius: 8px;

.all-validators__header {
width: fit-content;
margin: 0 auto;
padding: 20px;
}

.all-validators__wrapper {
margin: 0 20px 20px;

.validator-item {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
margin: 0px 12px;
align-items: center;
line-height: 32px;
justify-content: center;
background-color: $gray-3;
border-radius: 4px;

&.table-header {
background-color: $gray-5;
}

&:not(:last-child) {
margin-bottom: 12px;
}

.validator-item__address,
.validator-item__is-candidate,
.validator-item__is-elected,
.validator-item__fixed-cost,
.validator-item__stake,
.validator-item__cumulative-stake,
.validator-item__delegators,
.validator-item__rank {
display: flex;
margin: 0 auto;
justify-content: center;
}

.validator-item__stake {
text-align: center;
line-height: 20px;
}

.validator-item__address {
width: 120px;
}
}
}
}
}
}
}
66 changes: 66 additions & 0 deletions client/src/app/routes/nova/ValidatorsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import TruncatedId from "~/app/components/stardust/TruncatedId";
import { useValidators } from "~/helpers/nova/hooks/useValidators";
import "./ValidatorsPage.scss";

const ValidatorsPage: React.FC = () => {
const { validators, error } = useValidators();

return (
<section className="validators-page">
<div className="wrapper">
<div className="inner">
<div className="validators-page__header">
<div className="header__title row middle">
<h1>Validators</h1>
</div>
</div>

{validators !== null && (
<div className="all-validators__section">
<h2 className="all-validators__header">All Validators</h2>
<div className="all-validators__wrapper">
<div className="validator-item table-header">
<div className="validator-item__address">Address</div>
<div className="validator-item__is-candidate">Candidate?</div>
<div className="validator-item__is-elected">Elected?</div>
<div className="validator-item__fixed-cost">Fixed cost</div>
<div className="validator-item__stake">Stake (Own/Delegated)</div>
<div className="validator-item__cumulative-stake">Pool stake</div>
<div className="validator-item__delegators">Delegators</div>
<div className="validator-item__rank">Rank by stake</div>
</div>
{validators.map((validatorResponse, idx) => {
const validator = validatorResponse.validator;
const inCommittee = validatorResponse.inCommittee;
const delegatedStake = validator.poolStake - validator.validatorStake;

return (
<div className="validator-item" key={`validator-${idx}`}>
<div className="validator-item__address">
<TruncatedId id={validator.address} />
</div>
<div className="validator-item__is-candidate">{(!inCommittee).toString()}</div>
<div className="validator-item__is-elected">{inCommittee.toString()}</div>
<div className="validator-item__fixed-cost">{validator.fixedCost.toString()}</div>
<div className="validator-item__stake">
{`${validator.validatorStake.toString()} / ${delegatedStake}`}
</div>
<div className="validator-item__cumulative-stake">{validator.poolStake.toString()}</div>
<div className="validator-item__delegators">???</div>
<div className="validator-item__rank">{idx + 1}</div>
</div>
);
})}
</div>
</div>
)}

{error && <p className="danger">Failed to retrieve validators. {error}</p>}
</div>
</div>
</section>
);
};

export default ValidatorsPage;
33 changes: 33 additions & 0 deletions client/src/helpers/nova/hooks/useValidators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import { ServiceFactory } from "~/factories/serviceFactory";
import { useIsMounted } from "~/helpers/hooks/useIsMounted";
import { IValidator } from "~/models/api/nova/IValidatorsResponse";
import { NOVA } from "~/models/config/protocolVersion";
import { NovaApiClient } from "~/services/nova/novaApiClient";
import { useNetworkInfoNova } from "../networkInfo";

export function useValidators(): { validators: IValidator[] | null; error: string | null } {
const isMounted = useIsMounted();
const { name: network } = useNetworkInfoNova((s) => s.networkInfo);
const [apiClient] = useState(ServiceFactory.get<NovaApiClient>(`api-client-${NOVA}`));
const [validators, setValidators] = useState<IValidator[] | null>(null);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
(async () => {
const validatorsResponse = await apiClient.getValidators({ network });

if (isMounted) {
if ((validatorsResponse.validators ?? []).length > 0) {
setValidators(validatorsResponse.validators ?? null);
}

if (validatorsResponse.error) {
setError(validatorsResponse.error);
}
}
})();
}, [network]);

return { validators, error };
}
11 changes: 11 additions & 0 deletions client/src/models/api/nova/IValidatorsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ValidatorResponse } from "@iota/sdk-wasm-nova/web";
import { IResponse } from "./IResponse";

export interface IValidator {
validator: ValidatorResponse;
inCommittee: boolean;
}

export interface IValidatorsResponse extends IResponse {
validators?: IValidator[];
}
10 changes: 10 additions & 0 deletions client/src/services/nova/novaApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { IEpochCommitteeRequest } from "~/models/api/nova/IEpochCommitteeRequest
import { IEpochCommitteeResponse } from "~/models/api/nova/IEpochCommitteeResponse";
import { IInfluxDailyResponse } from "~/models/api/nova/influx/IInfluxDailyResponse";
import { ITransactionMetadataResponse } from "~/models/api/nova/ITransactionMetadataResponse";
import { IValidatorsResponse } from "~/models/api/nova/IValidatorsResponse";

/**
* Class to handle api communications on nova.
Expand Down Expand Up @@ -276,6 +277,15 @@ export class NovaApiClient extends ApiClient {
return this.callApi<unknown, IEpochCommitteeResponse>(`nova/epoch/committee/${request.network}/${request.epochIndex}`, "get");
}

/**
* Get the current cached validators.
* @param request The request to send.
* @returns The response from the request.
*/
public async getValidators(request: INetworkBoundGetRequest): Promise<IValidatorsResponse> {
return this.callApi<unknown, IValidatorsResponse>(`nova/validators/${request.network}`, "get");
}

/**
* Get the stats.
* @param request The request to send.
Expand Down

0 comments on commit ff4edb5

Please sign in to comment.