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 commitment id in slots feed #1182

Merged
merged 13 commits into from
Feb 26, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { SlotCommitment } from "@iota/sdk-nova";
import { IResponse } from "../../IResponse";

export enum SlotCommitmentStatus {
Committed = "committed",
Finalized = "finalized",
}

export interface ISlotCommitmentWrapper {
status: SlotCommitmentStatus;
slotCommitment: SlotCommitment;
}

export interface ILatestSlotCommitmentResponse extends IResponse {
slotCommitments: ISlotCommitmentWrapper[];
}
6 changes: 6 additions & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,5 +256,11 @@ 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/commitment/latest/:network",
method: "get",
folder: "nova/commitment/latest",
func: "get",
},
{ path: "/nova/slot/:network/:slotIndex", method: "get", folder: "nova/slot", func: "get" },
];
30 changes: 30 additions & 0 deletions api/src/routes/nova/commitment/latest/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ServiceFactory } from "../../../../factories/serviceFactory";
import { ILatestSlotCommitmentResponse } from "../../../../models/api/nova/commitment/ILatestSlotCommitmentsResponse";
import { IConfiguration } from "../../../../models/configuration/IConfiguration";
import { NOVA } from "../../../../models/db/protocolVersion";
import { NetworkService } from "../../../../services/networkService";
import { NovaFeed } from "../../../../services/nova/feed/novaFeed";
import { ValidationHelper } from "../../../../utils/validationHelper";

/**
* Get the latest slot commitments.
* @param _ The configuration.
* @param request The request.
* @param request.network The network in context.
* @returns The response.
*/
export async function get(_: IConfiguration, request: { network: string }): Promise<ILatestSlotCommitmentResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return { error: "Endpoint available only on Nova networks.", slotCommitments: [] };
}

const feedService = ServiceFactory.get<NovaFeed>(`feed-${request.network}`);
const slotCommitments = feedService.getLatestSlotCommitments;

return { slotCommitments };
}
59 changes: 59 additions & 0 deletions api/src/services/nova/feed/novaFeed.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { Block, Client, IBlockMetadata, SlotCommitment } from "@iota/sdk-nova";
import { ClassConstructor, plainToInstance } from "class-transformer";
import { ServiceFactory } from "../../../factories/serviceFactory";
import logger from "../../../logger";
import { ISlotCommitmentWrapper, SlotCommitmentStatus } from "../../../models/api/nova/commitment/ILatestSlotCommitmentsResponse";
import { IFeedUpdate } from "../../../models/api/nova/feed/IFeedUpdate";
import { INetwork } from "../../../models/db/INetwork";
import { NodeInfoService } from "../nodeInfoService";

const LATEST_SLOT_COMMITMENT_LIMIT = 30;

/**
* Wrapper class around Nova MqttClient.
* Streaming blocks from mqtt (upstream) to explorer-client connections (downstream).
Expand All @@ -25,6 +29,11 @@ export class NovaFeed {
*/
private _mqttClient: Client;

/**
* The latest slot commitments cache.
*/
private readonly latestSlotCommitmentCache: ISlotCommitmentWrapper[] = [];

/**
* The network in context.
*/
Expand Down Expand Up @@ -54,6 +63,14 @@ export class NovaFeed {
});
}

/**
* Get the latest slot commitment cache state.
* @returns The latest slot commitments.
*/
public get getLatestSlotCommitments() {
return this.latestSlotCommitmentCache;
}

/**
* Subscribe to the blocks nova feed.
* @param id The id of the subscriber.
Expand Down Expand Up @@ -124,10 +141,26 @@ export class NovaFeed {

// eslint-disable-next-line no-void
void this.broadcastBlock(update);

// eslint-disable-next-line no-void
void this.updateLatestSlotCommitmentCache(slotCommitment, true);
} catch {
logger.error("[NovaFeed]: Failed broadcasting finalized slot downstream.");
}
});

// eslint-disable-next-line no-void
void this._mqttClient.listenMqtt(["commitments/latest"], async (_, message) => {
try {
const deserializedMessage: { topic: string; payload: string } = JSON.parse(message);
const slotCommitment: SlotCommitment = JSON.parse(deserializedMessage.payload);

// eslint-disable-next-line no-void
void this.updateLatestSlotCommitmentCache(slotCommitment, false);
} catch {
logger.error("[NovaFeed]: Failed broadcasting commited slot downstream.");
}
});
}

private parseMqttPayloadMessage<T>(cls: ClassConstructor<T>, serializedMessage: string): T {
Expand Down Expand Up @@ -159,4 +192,30 @@ export class NovaFeed {
}
}
}

/**
* Updates the slot commitment cache.
* @param newSlotCommitment The new slot commitment.
* @param isFinalized Did the SlotCommitment get emitted from the 'commitments/finalized' topic or not ('commitments/latest').
*/
private async updateLatestSlotCommitmentCache(newSlotCommitment: SlotCommitment, isFinalized: boolean): Promise<void> {
if (!this.latestSlotCommitmentCache.map((commitment) => commitment.slotCommitment.slot).includes(newSlotCommitment.slot)) {
this.latestSlotCommitmentCache.unshift({
slotCommitment: newSlotCommitment,
status: isFinalized ? SlotCommitmentStatus.Finalized : SlotCommitmentStatus.Committed,
});

if (this.latestSlotCommitmentCache.length > LATEST_SLOT_COMMITMENT_LIMIT) {
this.latestSlotCommitmentCache.pop();
}
} else if (isFinalized) {
const commitmentToUpdate = this.latestSlotCommitmentCache.find(
(commitment) => commitment.slotCommitment.slot === newSlotCommitment.slot,
);

if (commitmentToUpdate) {
commitmentToUpdate.status = SlotCommitmentStatus.Finalized;
}
}
}
}
20 changes: 19 additions & 1 deletion client/src/app/components/nova/landing/LandingSlotSection.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,39 @@
margin: 0 20px 20px;

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

&.basic {
grid-template-columns: none;
}

&.transparent {
background-color: transparent;
}

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

.slot__index,
.slot__commitment-id,
.slot__rmc,
.slot__status {
display: flex;
margin: 0 auto;
justify-content: center;
}

.slot__commitment-id {
width: 220px;
}
}
}
}
41 changes: 32 additions & 9 deletions client/src/app/components/nova/landing/LandingSlotSection.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React from "react";
import useSlotsFeed from "~/helpers/nova/hooks/useSlotsFeed";
import "./LandingSlotSection.scss";
import ProgressBar from "./ProgressBar";
import { Utils } from "@iota/sdk-wasm-nova/web";
import Spinner from "../../Spinner";
import TruncatedId from "../../stardust/TruncatedId";
import "./LandingSlotSection.scss";

const LandingSlotSection: React.FC = () => {
const { currentSlot, currentSlotProgressPercent, latestSlots } = useSlotsFeed();
const { currentSlotIndex, currentSlotProgressPercent, latestSlotIndexes, latestSlotCommitments } = useSlotsFeed();

if (currentSlot === null || currentSlotProgressPercent === null) {
if (currentSlotIndex === null || currentSlotProgressPercent === null) {
return null;
}

Expand All @@ -15,13 +18,33 @@ const LandingSlotSection: React.FC = () => {
<h2 className="slots-section__header">Latest Slots</h2>
<div className="slots-feed__wrapper">
<ProgressBar progress={currentSlotProgressPercent} showLabel={false}>
<div className="slots-feed__item transparent">{currentSlot}</div>
</ProgressBar>
{latestSlots?.map((slot) => (
<div key={`slot-key-${slot}`} className="slots-feed__item">
{slot}
<div className="slots-feed__item transparent basic">
<div className="slot__index">{currentSlotIndex}</div>
</div>
))}
</ProgressBar>
{latestSlotIndexes?.map((slot) => {
const commitmentWrapper = latestSlotCommitments?.find((commitment) => commitment.slotCommitment.slot === slot) ?? null;
const commitmentId = !commitmentWrapper ? (
<Spinner compact />
) : (
<TruncatedId id={Utils.computeSlotCommitmentId(commitmentWrapper.slotCommitment)} showCopyButton />
);
const referenceManaCost = !commitmentWrapper ? (
<Spinner compact />
) : (
commitmentWrapper.slotCommitment.referenceManaCost.toString()
);
const slotStatus = !commitmentWrapper ? "pending" : commitmentWrapper.status;

return (
<div key={`slot-key-${slot}`} className="slots-feed__item">
<div className="slot__index">{slot}</div>
<div className="slot__commitment-id">{commitmentId}</div>
<div className="slot__rmc">{referenceManaCost}</div>
<div className="slot__status">{slotStatus}</div>
</div>
);
})}
</div>
</div>
);
Expand Down
1 change: 0 additions & 1 deletion client/src/app/lib/enums/index.ts

This file was deleted.

16 changes: 0 additions & 16 deletions client/src/app/lib/enums/slot-state.enums.ts

This file was deleted.

21 changes: 2 additions & 19 deletions client/src/app/routes/nova/SlotPage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import React from "react";
import useuseSlotDetails from "~/helpers/nova/hooks/useSlotDetails";
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 { PillStatus } from "~/app/lib/ui/enums";
import "./SlotPage.scss";

const SLOT_STATE_TO_PILL_STATUS: Record<SlotState, PillStatus> = {
[SlotState.Pending]: PillStatus.Pending,
[SlotState.Committed]: PillStatus.Success,
[SlotState.Finalized]: PillStatus.Success,
};

export default function SlotPage({
match: {
params: { network, slotIndex },
Expand All @@ -27,18 +18,15 @@ export default function SlotPage({
const { slotCommitment } = useuseSlotDetails(network, slotIndex);

const parsedSlotIndex = parseSlotIndex(slotIndex);
const slotState = slotCommitment ? SlotState.Finalized : SlotState.Pending;
const pillStatus: PillStatus = SLOT_STATE_TO_PILL_STATUS[slotState];

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

Expand All @@ -59,11 +47,6 @@ export default function SlotPage({
<h1>Slot</h1>
<Modal icon="info" data={mainHeaderMessage} />
</div>
{parsedSlotIndex && (
<div className="header--status">
<StatusPill status={pillStatus} label={slotState} />
</div>
)}
</div>
{parsedSlotIndex ? (
<div className="section">
Expand Down
Loading
Loading