Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add slot page #1163

Merged
merged 11 commits into from
Feb 26, 2024
11 changes: 11 additions & 0 deletions api/src/models/api/nova/ISlotRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ISlotRequest {
/**
* The network to search on.
*/
network: string;

/**
* The slot index to get the details for.
*/
slotIndex: string;
}
9 changes: 9 additions & 0 deletions api/src/models/api/nova/ISlotResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// eslint-disable-next-line import/no-unresolved
import { SlotCommitment } from "@iota/sdk-nova";

export interface ISlotResponse {
msarcev marked this conversation as resolved.
Show resolved Hide resolved
/**
* The deserialized slot.
*/
slot?: SlotCommitment;
}
1 change: 1 addition & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,5 @@ export const routes: IRoute[] = [
},
{ path: "/nova/block/:network/:blockId", method: "get", folder: "nova/block", func: "get" },
{ path: "/nova/block/metadata/:network/:blockId", method: "get", folder: "nova/block/metadata", func: "get" },
{ path: "/nova/slot/:network/:slotIndex", method: "get", folder: "nova/slot", func: "get" },
];
30 changes: 30 additions & 0 deletions api/src/routes/nova/slot/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ServiceFactory } from "../../../factories/serviceFactory";
import { ISlotRequest } from "../../../models/api/nova/ISlotRequest";
import { ISlotResponse } from "../../../models/api/nova/ISlotResponse";
import { IConfiguration } from "../../../models/configuration/IConfiguration";
import { NOVA } from "../../../models/db/protocolVersion";
import { NetworkService } from "../../../services/networkService";
import { NovaApiService } from "../../../services/nova/novaApiService";
import { ValidationHelper } from "../../../utils/validationHelper";

/**
* Fetch the block from the network.
* @param _ The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(_: IConfiguration, request: ISlotRequest): Promise<ISlotResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.numberFromString(request.slotIndex, "slotIndex");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const novaApiService = ServiceFactory.get<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.getSlotCommitment(Number(request.slotIndex));
}
16 changes: 16 additions & 0 deletions api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse";
import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse";
import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse";
import { ISearchResponse } from "../../models/api/nova/ISearchResponse";
import { ISlotResponse } from "../../models/api/nova/ISlotResponse";
import { INetwork } from "../../models/db/INetwork";
import { HexHelper } from "../../utils/hexHelper";
import { SearchExecutor } from "../../utils/nova/searchExecutor";
Expand Down Expand Up @@ -261,6 +262,21 @@ export class NovaApiService {
return manaRewardsResponse ? { outputId, manaRewards: manaRewardsResponse } : { outputId, message: "Rewards data not found" };
}

/**
* Get the slot commitment.
* @param slotIndex The slot index to get the commitment for.
* @returns The slot commitment.
*/
public async getSlotCommitment(slotIndex: number): Promise<ISlotResponse> {
try {
const slot = await this.client.getCommitmentByIndex(slotIndex);

return { slot };
} catch (e) {
logger.error(`Failed fetching slot with slot index ${slotIndex}. Cause: ${e}`);
}
}

/**
* Find item on the stardust network.
* @param query The query to use for finding items.
Expand Down
30 changes: 30 additions & 0 deletions client/src/app/components/nova/PageDataRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";
import classNames from "classnames";
import TruncatedId from "../stardust/TruncatedId";

export interface IPageDataRow {
label: string;
value?: string | number;
highlighted?: boolean;
truncatedId?: {
id: string;
link?: string;
showCopyButton?: boolean;
};
}
const PageDataRow = ({ label, value, truncatedId, highlighted }: IPageDataRow): React.JSX.Element => {
return (
<div className="section--data">
<div className="label">{label}</div>
<div className={classNames("value code", { highlighted })}>
{truncatedId ? (
<TruncatedId id={truncatedId.id} link={truncatedId.link} showCopyButton={truncatedId.showCopyButton} />
) : (
value
)}
</div>
</div>
);
};

export default PageDataRow;
41 changes: 41 additions & 0 deletions client/src/app/components/nova/StatusPill.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@import "./../../../scss/fonts";
@import "./../../../scss/mixins";
@import "./../../../scss/media-queries";
@import "./../../../scss/variables";

.status-pill {
@include font-size(12px);

display: flex;
align-items: center;
margin-right: 8px;
padding: 6px 12px;
border: 0;
border-radius: 6px;
outline: none;
color: $gray-midnight;
font-family: $inter;
font-weight: 500;
letter-spacing: 0.5px;
white-space: nowrap;

@include phone-down {
height: 32px;
}

&.status__ {
&confirmed {
background-color: var(--message-confirmed-bg);
color: $mint-green-7;
}

&conflicting {
background-color: var(--message-conflicting-bg);
}

&pending {
background-color: var(--light-bg);
color: #8493ad;
}
}
}
33 changes: 33 additions & 0 deletions client/src/app/components/nova/StatusPill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";
import classNames from "classnames";
import { PillState } from "~/app/lib/ui/enums";
import "./StatusPill.scss";

interface IStatusPill {
/**
* Label for the status.
*/
label: string;
/**
* The status of the pill.
*/
state: PillState;
}

const StatusPill: React.FC<IStatusPill> = ({ label, state }): React.JSX.Element => (
<>
{label && (
<div
className={classNames("status-pill", {
status__confirmed: state === PillState.Confirmed,
status__conflicting: state === PillState.Rejected,
status__pending: state === PillState.Pending,
})}
>
<span className="capitalize-text">{label}</span>
</div>
)}
</>
);

export default StatusPill;
1 change: 1 addition & 0 deletions client/src/app/lib/enums/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./slot-state.enums";
11 changes: 11 additions & 0 deletions client/src/app/lib/enums/slot-state.enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum SlotState {
/**
* The slot is pending.
*/
Pending = "pending",

/**
* The slot is finalized.
*/
Finalized = "finalized",
}
1 change: 1 addition & 0 deletions client/src/app/lib/ui/enums/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./pill-state.enum";
16 changes: 16 additions & 0 deletions client/src/app/lib/ui/enums/pill-state.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export enum PillState {
/**
* The status is pending.
*/
Pending = "pending",

/**
* The status is confirmed.
*/
Confirmed = "confirmed",

/**
* The status is rejected.
*/
Rejected = "rejected",
}
2 changes: 2 additions & 0 deletions client/src/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import StardustOutputPage from "./routes/stardust/OutputPage";
import NovaBlockPage from "./routes/nova/Block";
import NovaOutputPage from "./routes/nova/OutputPage";
import NovaSearch from "./routes/nova/Search";
import NovaSlotPage from "./routes/nova/SlotPage";
import StardustSearch from "./routes/stardust/Search";
import StardustStatisticsPage from "./routes/stardust/statistics/StatisticsPage";
import StardustTransactionPage from "./routes/stardust/TransactionPage";
Expand Down Expand Up @@ -178,6 +179,7 @@ const buildAppRoutes = (protocolVersion: string, withNetworkContext: (wrappedCom
<Route path="/:network/block/:blockId" key={keys.next().value} component={NovaBlockPage} />,
<Route path="/:network/output/:outputId" key={keys.next().value} component={NovaOutputPage} />,
<Route path="/:network/search/:query?" key={keys.next().value} component={NovaSearch} />,
<Route path="/:network/slot/:slotIndex" key={keys.next().value} component={NovaSlotPage} />,
];

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

.slot-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;
}

.slot-page--header {
display: flex;
flex-direction: column;
align-items: flex-start;

.header--title {
margin-bottom: 8px;
}

.header--status {
display: flex;
}
}

.section {
padding-top: 44px;

.section--header {
margin-top: 44px;
}

.card--content__output {
margin-top: 20px;
}
}
}
}
}
87 changes: 87 additions & 0 deletions client/src/app/routes/nova/SlotPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from "react";
import useSlotCommitment from "~/helpers/nova/hooks/useSlotCommitment";
import StatusPill from "~/app/components/nova/StatusPill";
import PageDataRow, { IPageDataRow } from "~/app/components/nova/PageDataRow";
import Modal from "~/app/components/Modal";
import mainHeaderMessage from "~assets/modals/nova/slot/main-header.json";
import NotFound from "~/app/components/NotFound";
import { SlotState } from "~/app/lib/enums";
import { RouteComponentProps } from "react-router-dom";
import { PillState } from "~/app/lib/ui/enums";
import "./SlotPage.scss";

const SLOT_STATE_TO_PILL_STATE: Record<SlotState, PillState> = {
[SlotState.Pending]: PillState.Pending,
[SlotState.Finalized]: PillState.Confirmed,
};

export default function SlotPage({
match: {
params: { network, slotIndex },
},
}: RouteComponentProps<{
network: string;
slotIndex: string;
}>): React.JSX.Element {
const { slotCommitment } = useSlotCommitment(network, slotIndex);

const parsedSlotIndex = parseSlotIndex(slotIndex);
const slotState = slotCommitment ? SlotState.Finalized : SlotState.Pending;
const pillState: PillState = SLOT_STATE_TO_PILL_STATE[slotState];

const dataRows: IPageDataRow[] = [
{
label: "Slot Index",
value: slotCommitment?.slot || parsedSlotIndex,
highlighted: true,
},
{
label: "RMC",
value: slotCommitment?.referenceManaCost.toString(),
},
];

function parseSlotIndex(slotIndex: string): number | undefined {
const slotIndexNum = parseInt(slotIndex, 10);
if (isNaN(slotIndexNum)) {
return;
}
return slotIndexNum;
}

return (
<section className="slot-page">
<div className="wrapper">
<div className="inner">
<div className="slot-page--header">
<div className="header--title row middle">
<h1>Slot</h1>
<Modal icon="info" data={mainHeaderMessage} />
</div>
{slotCommitment && (
<div className="header--status">
<StatusPill state={pillState} label={slotState} />
</div>
)}
</div>
{parsedSlotIndex && slotCommitment ? (
<div className="section">
<div className="section--header row row--tablet-responsive middle space-between">
<div className="row middle">
<h2>General</h2>
</div>
</div>
{dataRows.map((dataRow, index) => {
if (dataRow.value || dataRow.truncatedId) {
return <PageDataRow key={index} {...dataRow} />;
}
})}
</div>
) : (
<NotFound query={slotIndex} searchTarget="slot" />
)}
</div>
</div>
</section>
);
}
Loading
Loading