diff --git a/api/src/models/api/nova/commitment/ILatestSlotCommitmentsResponse.ts b/api/src/models/api/nova/commitment/ILatestSlotCommitmentsResponse.ts new file mode 100644 index 000000000..c2818376d --- /dev/null +++ b/api/src/models/api/nova/commitment/ILatestSlotCommitmentsResponse.ts @@ -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[]; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index 776b6cecc..de1ed70db 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -268,5 +268,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" }, ]; diff --git a/api/src/routes/nova/commitment/latest/get.ts b/api/src/routes/nova/commitment/latest/get.ts new file mode 100644 index 000000000..784fdaff6 --- /dev/null +++ b/api/src/routes/nova/commitment/latest/get.ts @@ -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 { + const networkService = ServiceFactory.get("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(`feed-${request.network}`); + const slotCommitments = feedService.getLatestSlotCommitments; + + return { slotCommitments }; +} diff --git a/api/src/services/nova/feed/novaFeed.ts b/api/src/services/nova/feed/novaFeed.ts index 1fc1a2447..e7b7a8a44 100644 --- a/api/src/services/nova/feed/novaFeed.ts +++ b/api/src/services/nova/feed/novaFeed.ts @@ -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). @@ -25,6 +29,11 @@ export class NovaFeed { */ private _mqttClient: Client; + /** + * The latest slot commitments cache. + */ + private readonly latestSlotCommitmentCache: ISlotCommitmentWrapper[] = []; + /** * The network in context. */ @@ -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. @@ -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(cls: ClassConstructor, serializedMessage: string): T { @@ -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 { + 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; + } + } + } } diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index 0fc9b4456..4a3c31637 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -185,6 +185,25 @@ export class NovaApiService { } } + /** + * Get the delegation output details. + * @param delegationId The delegationId to get the output details for. + * @returns The delegation output details. + */ + public async delegationDetails(delegationId: string): Promise { + try { + const delegationOutputId = await this.client.delegationOutputId(delegationId); + + if (delegationOutputId) { + const outputResponse = await this.outputDetails(delegationOutputId); + + return outputResponse.error ? { error: outputResponse.error } : { output: outputResponse.output }; + } + } catch { + return { message: "Delegation output not found" }; + } + } + /** * Get controlled Foundry output id by controller Account address * @param accountAddress The bech32 account address to get the controlled Foundries for. diff --git a/api/src/utils/nova/searchExecutor.ts b/api/src/utils/nova/searchExecutor.ts index bb3785fc8..e2dad915e 100644 --- a/api/src/utils/nova/searchExecutor.ts +++ b/api/src/utils/nova/searchExecutor.ts @@ -97,6 +97,21 @@ export class SearchExecutor { ); } + if (searchQuery.delegationId) { + promises.push( + this.executeQuery( + this.apiService.delegationDetails(searchQuery.delegationId), + (response) => { + promisesResult = { + output: response.output, + error: response.error || response.message, + }; + }, + "Delegation id fetch failed", + ), + ); + } + if (searchQuery.transactionId) { promises.push( this.executeQuery( diff --git a/client/src/app/components/nova/landing/LandingSlotSection.scss b/client/src/app/components/nova/landing/LandingSlotSection.scss index c680a3bc6..8e2b963c4 100644 --- a/client/src/app/components/nova/landing/LandingSlotSection.scss +++ b/client/src/app/components/nova/landing/LandingSlotSection.scss @@ -17,7 +17,8 @@ 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; @@ -25,6 +26,10 @@ background-color: $gray-5; border-radius: 4px; + &.basic { + grid-template-columns: none; + } + &.transparent { background-color: transparent; } @@ -32,6 +37,19 @@ &: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; + } } } } diff --git a/client/src/app/components/nova/landing/LandingSlotSection.tsx b/client/src/app/components/nova/landing/LandingSlotSection.tsx index 678e825a2..6da6b6847 100644 --- a/client/src/app/components/nova/landing/LandingSlotSection.tsx +++ b/client/src/app/components/nova/landing/LandingSlotSection.tsx @@ -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; } @@ -15,13 +18,33 @@ const LandingSlotSection: React.FC = () => {

Latest Slots

-
{currentSlot}
-
- {latestSlots?.map((slot) => ( -
- {slot} +
+
{currentSlotIndex}
- ))} + + {latestSlotIndexes?.map((slot) => { + const commitmentWrapper = latestSlotCommitments?.find((commitment) => commitment.slotCommitment.slot === slot) ?? null; + const commitmentId = !commitmentWrapper ? ( + + ) : ( + + ); + const referenceManaCost = !commitmentWrapper ? ( + + ) : ( + commitmentWrapper.slotCommitment.referenceManaCost.toString() + ); + const slotStatus = !commitmentWrapper ? "pending" : commitmentWrapper.status; + + return ( +
+
{slot}
+
{commitmentId}
+
{referenceManaCost}
+
{slotStatus}
+
+ ); + })}
); diff --git a/client/src/app/lib/enums/index.ts b/client/src/app/lib/enums/index.ts deleted file mode 100644 index 877581b6e..000000000 --- a/client/src/app/lib/enums/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./slot-state.enums"; diff --git a/client/src/app/lib/enums/slot-state.enums.ts b/client/src/app/lib/enums/slot-state.enums.ts deleted file mode 100644 index 3a190a546..000000000 --- a/client/src/app/lib/enums/slot-state.enums.ts +++ /dev/null @@ -1,16 +0,0 @@ -export enum SlotState { - /** - * The slot is pending. - */ - Pending = "pending", - - /** - * The slot is committed. - */ - Committed = "committed", - - /** - * The slot is finalized. - */ - Finalized = "finalized", -} diff --git a/client/src/app/routes/nova/SlotPage.tsx b/client/src/app/routes/nova/SlotPage.tsx index a8d6aed2e..56d538e1e 100644 --- a/client/src/app/routes/nova/SlotPage.tsx +++ b/client/src/app/routes/nova/SlotPage.tsx @@ -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.Pending]: PillStatus.Pending, - [SlotState.Committed]: PillStatus.Success, - [SlotState.Finalized]: PillStatus.Success, -}; - export default function SlotPage({ match: { params: { network, slotIndex }, @@ -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() ?? "-", }, ]; @@ -59,11 +47,6 @@ export default function SlotPage({

Slot

- {parsedSlotIndex && ( -
- -
- )} {parsedSlotIndex ? (
diff --git a/client/src/features/visualizer-threejs/CameraControls.tsx b/client/src/features/visualizer-threejs/CameraControls.tsx index c6f422394..a8353cac9 100644 --- a/client/src/features/visualizer-threejs/CameraControls.tsx +++ b/client/src/features/visualizer-threejs/CameraControls.tsx @@ -1,45 +1,33 @@ import { CameraControls as DreiCameraControls } from "@react-three/drei"; -import React, { useEffect } from "react"; +import { getCameraAngles } from "./utils"; +import React, { useEffect, useState } from "react"; import { useThree } from "@react-three/fiber"; import { CanvasElement } from "./enums"; -import { useConfigStore } from "./store"; +import { useTangleStore, useConfigStore } from "./store"; import { VISUALIZER_PADDINGS } from "./constants"; -import { getCameraAngles } from "./utils"; const CAMERA_ANGLES = getCameraAngles(); const CameraControls = () => { const controls = React.useRef(null); + const [shouldLockZoom, setShouldLockZoom] = useState(false); const scene = useThree((state) => state.scene); + const zoom = useTangleStore((state) => state.zoom); const mesh = scene.getObjectByName(CanvasElement.TangleWrapperMesh) as THREE.Mesh | undefined; const canvasDimensions = useConfigStore((state) => state.dimensions); - /** - * Locks the camera zoom to the current zoom value. - */ - function lockCameraZoom(controls: DreiCameraControls) { - const zoom = controls.camera.zoom; - controls.maxZoom = zoom; - controls.minZoom = zoom; - } - - /** - * Unlocks the camera zoom for free movement. - */ - function unlockCameraZoom(controls: DreiCameraControls) { - controls.maxZoom = Infinity; - controls.minZoom = 0.01; - } - /** * Fits the camera to the TangleMesh. */ function fitCameraToTangle(controls: DreiCameraControls | null, mesh?: THREE.Mesh) { if (controls && mesh) { - unlockCameraZoom(controls); + const previousZoom = controls.camera.zoom; + controls.minZoom = 0.01; + controls.maxZoom = Infinity; controls.fitToBox(mesh, false, { ...VISUALIZER_PADDINGS }); - lockCameraZoom(controls); + controls.minZoom = previousZoom; + controls.maxZoom = previousZoom; } } @@ -53,7 +41,9 @@ const CameraControls = () => { const camera = controls.current?.camera; const renderVerticalScene = canvasDimensions.width < canvasDimensions.height; const cameraUp: [number, number, number] = renderVerticalScene ? [1, 0, 0] : [0, 1, 0]; + setShouldLockZoom(false); camera.up.set(...cameraUp); + setShouldLockZoom(true); } }, [canvasDimensions, controls, mesh]); @@ -70,6 +60,16 @@ const CameraControls = () => { }; }, [controls, mesh]); + /** + * Locks the camera zoom to the current zoom value. + */ + useEffect(() => { + if (controls.current) { + controls.current.maxZoom = shouldLockZoom ? zoom : Infinity; + controls.current.minZoom = shouldLockZoom ? zoom : 0.01; + } + }, [controls.current, shouldLockZoom, zoom]); + return ; }; diff --git a/client/src/features/visualizer-threejs/Emitter.tsx b/client/src/features/visualizer-threejs/Emitter.tsx index fba26e0c7..b19a07765 100644 --- a/client/src/features/visualizer-threejs/Emitter.tsx +++ b/client/src/features/visualizer-threejs/Emitter.tsx @@ -4,7 +4,14 @@ import React, { RefObject, Dispatch, SetStateAction, useEffect, useRef, useLayou import * as THREE from "three"; import { useConfigStore, useTangleStore } from "./store"; import { useRenderTangle } from "./useRenderTangle"; -import { getTangleDistances, getEmitterPositions, generateRandomPeriods, generateRandomAmplitudes } from "./utils"; +import { + getTangleDistances, + getEmitterPositions, + generateRandomPeriods, + generateRandomAmplitudes, + generateRandomTiltings, + getCurrentTiltValue, +} from "./utils"; import { CanvasElement } from "./enums"; import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer"; import { EMITTER_DEPTH, EMITTER_HEIGHT, EMITTER_WIDTH } from "./constants"; @@ -36,14 +43,19 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte const randomSinusoidAmplitudes = useConfigStore((state) => state.randomSinusoidAmplitudes); const setRandomSinusoidAmplitudes = useConfigStore((state) => state.setRandomSinusoidAmplitudes); + const randomTilts = useConfigStore((state) => state.randomTilts); + const setRandomTilts = useConfigStore((state) => state.setRandomTilts); + const tangleWrapperRef = useRef(null); useLayoutEffect(() => { const { periods, sum: periodsSum } = generateRandomPeriods(); const amplitudes = generateRandomAmplitudes(); + const tiltings = generateRandomTiltings(); setSinusoidRandomPeriods(periods); setSinusoidPeriodsSum(periodsSum); setRandomSinusoidAmplitudes(amplitudes); + setRandomTilts(tiltings); }, []); useEffect(() => { @@ -68,6 +80,7 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte */ useFrame(() => { const currentAnimationTime = getVisualizerTimeDiff(); + const currentTilt = getCurrentTiltValue(currentAnimationTime, randomTilts); const { x, y } = getEmitterPositions({ currentAnimationTime, periods: sinusoidRandomPeriods, @@ -79,6 +92,7 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte if (emitterRef.current) { emitterRef.current.position.x = x; emitterRef.current.position.y = y; + emitterRef.current.rotation.z = THREE.MathUtils.degToRad(currentTilt); } if (tangleWrapperRef.current) { diff --git a/client/src/features/visualizer-threejs/VisualizerInstance.tsx b/client/src/features/visualizer-threejs/VisualizerInstance.tsx index 26f62570f..ff59a791d 100644 --- a/client/src/features/visualizer-threejs/VisualizerInstance.tsx +++ b/client/src/features/visualizer-threejs/VisualizerInstance.tsx @@ -24,13 +24,13 @@ import { Wrapper } from "./wrapper/Wrapper"; import { CanvasElement } from "./enums"; import { useGetThemeMode } from "~/helpers/hooks/useGetThemeMode"; import { TSelectFeedItemNova } from "~/app/types/visualizer.types"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { BasicBlockBody, Utils, type IBlockMetadata, type BlockState, type SlotIndex, type BlockId } from "@iota/sdk-wasm-nova/web"; +import { BasicBlockBody, Utils, type IBlockMetadata, type BlockState, type SlotIndex } from "@iota/sdk-wasm-nova/web"; import { IFeedBlockData } from "~/models/api/nova/feed/IFeedBlockData"; import CameraControls from "./CameraControls"; -import "./Visualizer.scss"; import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer"; import { getBlockInitPosition, getBlockTargetPosition } from "./blockPositions"; +import { getCurrentTiltValue } from "./utils"; +import "./Visualizer.scss"; const features = { statsEnabled: false, @@ -76,6 +76,7 @@ const VisualizerInstance: React.FC> = const sinusoidPeriodsSum = useConfigStore((s) => s.sinusoidPeriodsSum); const sinusoidRandomPeriods = useConfigStore((s) => s.sinusoidRandomPeriods); const sinusoidRandomAmplitudes = useConfigStore((s) => s.randomSinusoidAmplitudes); + const randomTilts = useConfigStore((state) => state.randomTilts); const selectedFeedItem: TSelectFeedItemNova = clickedInstanceId ? blockMetadata.get(clickedInstanceId) ?? null : null; const resetConfigState = useTangleStore((s) => s.resetConfigState); @@ -208,7 +209,8 @@ const VisualizerInstance: React.FC> = periodsSum: sinusoidPeriodsSum, sinusoidAmplitudes: sinusoidRandomAmplitudes, }); - const targetPosition = getBlockTargetPosition(initPosition, bps); + const blockTiltFactor = getCurrentTiltValue(currentAnimationTime, randomTilts); + const targetPosition = getBlockTargetPosition(initPosition, bps, blockTiltFactor); bpsCounter.addBlock(); @@ -227,11 +229,10 @@ const VisualizerInstance: React.FC> = if (blockWeakParents.length > 0) { addToEdgeQueue(blockData.blockId, blockWeakParents); } - addBlock({ id: blockData.blockId, color: PENDING_BLOCK_COLOR, - blockAddedTimestamp: getCurrentAnimationTime(), + blockAddedTimestamp: currentAnimationTime, targetPosition, initPosition, }); diff --git a/client/src/features/visualizer-threejs/blockPositions.ts b/client/src/features/visualizer-threejs/blockPositions.ts index a01f521a6..7f398e038 100644 --- a/client/src/features/visualizer-threejs/blockPositions.ts +++ b/client/src/features/visualizer-threejs/blockPositions.ts @@ -1,29 +1,22 @@ -import { EMITTER_WIDTH, EMITTER_X_POSITION_MULTIPLIER } from "./constants"; -import { ISinusoidalPositionParams } from "./interfaces"; -import { getEmitterPositions, getGenerateDynamicYZPosition, getTangleDistances, randomIntFromInterval } from "./utils"; +import { ISinusoidalPositionParams, IThreeDimensionalPosition } from "./interfaces"; +import { getEmitterPositions, getTangleDistances, getBlockPositionGenerator } from "./utils"; -const generateYZPositions = getGenerateDynamicYZPosition(); +const generateBlockTargetPosition = getBlockPositionGenerator(); -interface IPos { - x: number; - y: number; - z: number; +export function getBlockTargetPosition( + initPosition: IThreeDimensionalPosition, + bps: number, + tiltDegress: number, +): IThreeDimensionalPosition { + return generateBlockTargetPosition(bps, initPosition, tiltDegress); } -export function getBlockTargetPosition(initPosition: IPos, bps: number): IPos { - const { y, z } = generateYZPositions(bps, initPosition); - const emitterMinX = initPosition.x - EMITTER_WIDTH / 2; - const emitterMaxX = initPosition.x + EMITTER_WIDTH / 2; - - const minX = emitterMinX - (emitterMaxX - emitterMinX) * EMITTER_X_POSITION_MULTIPLIER; - const maxX = emitterMaxX + (emitterMaxX - emitterMinX) * EMITTER_X_POSITION_MULTIPLIER; - - const x = randomIntFromInterval(minX, maxX); - - return { x, y, z }; -} - -export function getBlockInitPosition({ currentAnimationTime, periods, periodsSum, sinusoidAmplitudes }: ISinusoidalPositionParams): IPos { +export function getBlockInitPosition({ + currentAnimationTime, + periods, + periodsSum, + sinusoidAmplitudes, +}: ISinusoidalPositionParams): IThreeDimensionalPosition { const { xTangleDistance } = getTangleDistances(); const { x: xEmitterPos, y, z } = getEmitterPositions({ currentAnimationTime, periods, periodsSum, sinusoidAmplitudes }); const x = xEmitterPos + xTangleDistance / 2; diff --git a/client/src/features/visualizer-threejs/constants.ts b/client/src/features/visualizer-threejs/constants.ts index cc588c7f8..6e78c4962 100644 --- a/client/src/features/visualizer-threejs/constants.ts +++ b/client/src/features/visualizer-threejs/constants.ts @@ -19,7 +19,6 @@ export const ZOOM_DEFAULT = 2; export const TIME_DIFF_COUNTER = 250; export const SECOND = 1000; export const DATA_SENDER_TIME_INTERVAL = 500; -export const ANIMATION_TIME_SECONDS = 3; // colors export const PENDING_BLOCK_COLOR = new Color("#A6C3FC"); @@ -35,7 +34,7 @@ export const BLOCK_STATE_TO_COLOR = new Map([ ]); // emitter -export const EMITTER_SPEED_MULTIPLIER = 80; +export const EMITTER_SPEED_MULTIPLIER = 150; export const EMITTER_PADDING_RIGHT = 150; export const VISUALIZER_SAFE_ZONE = 150; @@ -72,10 +71,10 @@ export const EMITTER_HEIGHT = 250; export const EMITTER_DEPTH = 250; // conic emitter -export const MIN_TANGLE_RADIUS = 100; -export const MAX_TANGLE_RADIUS = 300; +export const MIN_TANGLE_RADIUS = 200; +export const MAX_TANGLE_RADIUS = 600; -export const MIN_BLOCKS_PER_SECOND = 100; +export const MIN_BLOCKS_PER_SECOND = 150; export const MAX_BLOCKS_PER_SECOND = 250; export const MIN_BLOCK_NEAR_RADIUS = 20; @@ -83,7 +82,8 @@ export const MIN_BLOCK_NEAR_RADIUS = 20; export const MAX_POINT_RETRIES = 10; export const MAX_PREV_POINTS = 20; -export const EMITTER_X_POSITION_MULTIPLIER = 3; +export const SPRAY_DISTANCE = 400; +export const SPRAY_ANIMATION_DURATION = SPRAY_DISTANCE / EMITTER_SPEED_MULTIPLIER; /* Values for randomizing the tangle */ export const NUMBER_OF_RANDOM_PERIODS = 100; @@ -93,3 +93,8 @@ export const MAX_SINUSOID_PERIOD = 8; export const NUMBER_OF_RANDOM_AMPLITUDES = 100; export const MIN_SINUSOID_AMPLITUDE = 100; export const MAX_SINUSOID_AMPLITUDE = 200; + +export const NUMBER_OF_RANDOM_TILTINGS = 100; +export const TILT_DURATION_SECONDS = 4; +export const MAX_TILT_FACTOR_DEGREES = 16; +export const MIN_TILT_FACTOR_DEGREES = 1; diff --git a/client/src/features/visualizer-threejs/interfaces.ts b/client/src/features/visualizer-threejs/interfaces.ts index 81bae2dd5..732fce9b1 100644 --- a/client/src/features/visualizer-threejs/interfaces.ts +++ b/client/src/features/visualizer-threejs/interfaces.ts @@ -5,12 +5,21 @@ export interface ICameraAngles { maxAzimuthAngle: number; } +export interface ITwoDimensionalPosition { + x: number; + y: number; +} + export interface IThreeDimensionalPosition { x: number; y: number; z: number; } +export interface IThreeDimensionalPositionWithTilt extends IThreeDimensionalPosition { + tiltFactor: number; +} + export interface ITimeBasedPositionParams { currentAnimationTime: number; } diff --git a/client/src/features/visualizer-threejs/store/config.ts b/client/src/features/visualizer-threejs/store/config.ts index d505f5177..8c5d9ce9d 100644 --- a/client/src/features/visualizer-threejs/store/config.ts +++ b/client/src/features/visualizer-threejs/store/config.ts @@ -23,6 +23,9 @@ interface ConfigState { randomSinusoidAmplitudes: number[]; setRandomSinusoidAmplitudes: (randomizedAmplitudes: number[]) => void; + + randomTilts: number[]; + setRandomTilts: (randomTilts: number[]) => void; } export const useConfigStore = create((set) => ({ @@ -110,4 +113,15 @@ export const useConfigStore = create((set) => ({ randomSinusoidAmplitudes: randomizedAmplitudes, })); }, + + /** + * Randomized tilts for the tangle. + */ + randomTilts: [], + setRandomTilts: (randomTilts) => { + set((state) => ({ + ...state, + randomTilts, + })); + }, })); diff --git a/client/src/features/visualizer-threejs/store/tangle.ts b/client/src/features/visualizer-threejs/store/tangle.ts index 4fb7e376d..5044b9914 100644 --- a/client/src/features/visualizer-threejs/store/tangle.ts +++ b/client/src/features/visualizer-threejs/store/tangle.ts @@ -1,7 +1,7 @@ import { Color } from "three"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; -import { ZOOM_DEFAULT, ANIMATION_TIME_SECONDS } from "../constants"; +import { ZOOM_DEFAULT, EMITTER_SPEED_MULTIPLIER, SPRAY_DISTANCE } from "../constants"; import { IFeedBlockData } from "~models/api/nova/feed/IFeedBlockData"; import { IThreeDimensionalPosition } from "../interfaces"; import { BlockId, SlotIndex } from "@iota/sdk-wasm-nova/web"; @@ -104,7 +104,8 @@ export const useTangleStore = create()( }); for (const [key, value] of state.blockIdToAnimationPosition) { - if (value.elapsedTime > ANIMATION_TIME_SECONDS) { + const animationTime = SPRAY_DISTANCE / EMITTER_SPEED_MULTIPLIER; + if (value.elapsedTime > animationTime) { state.blockIdToAnimationPosition.delete(key); } } diff --git a/client/src/features/visualizer-threejs/useRenderTangle.tsx b/client/src/features/visualizer-threejs/useRenderTangle.tsx index 14f8c3b27..4da8ff476 100644 --- a/client/src/features/visualizer-threejs/useRenderTangle.tsx +++ b/client/src/features/visualizer-threejs/useRenderTangle.tsx @@ -1,7 +1,7 @@ -import { useFrame, useThree } from "@react-three/fiber"; -import { useEffect, useRef } from "react"; +import { useThree } from "@react-three/fiber"; +import { useEffect, useRef, useState } from "react"; import * as THREE from "three"; -import { ANIMATION_TIME_SECONDS, MAX_BLOCK_INSTANCES, NODE_SIZE_DEFAULT } from "./constants"; +import { SPRAY_ANIMATION_DURATION, MAX_BLOCK_INSTANCES, NODE_SIZE_DEFAULT } from "./constants"; import { useMouseMove } from "./hooks/useMouseMove"; import { IBlockState, IBlockAnimationPosition, useConfigStore, useTangleStore } from "./store"; import { useRenderEdges } from "./useRenderEdges"; @@ -15,7 +15,8 @@ const INITIAL_SPHERE_SCALE = 0.7; export const useRenderTangle = () => { const tangleMeshRef = useRef(new THREE.InstancedMesh(SPHERE_GEOMETRY, SPHERE_MATERIAL, MAX_BLOCK_INSTANCES)); - const objectIndexRef = useRef(0); + const [updateAnimationPositionQueue, setUpdateAnimationPositionQueue] = useState>(new Map()); + const objectIndexRef = useRef(1); const { scene } = useThree(); const isPlaying = useConfigStore((s) => s.isPlaying); @@ -28,6 +29,8 @@ export const useRenderTangle = () => { const blockIdToIndex = useTangleStore((s) => s.blockIdToIndex); const updateBlockIdToIndex = useTangleStore((s) => s.updateBlockIdToIndex); const blockIdToAnimationPosition = useTangleStore((s) => s.blockIdToAnimationPosition); + const updateBlockIdToAnimationPosition = useTangleStore((s) => s.updateBlockIdToAnimationPosition); + const getVisualizerTimeDiff = useVisualizerTimer(); const assignBlockToMesh = (block: IBlockState) => { @@ -42,10 +45,10 @@ export const useRenderTangle = () => { // Reuses old indexes when MAX_INSTANCES is reached // This also makes it so that old nodes are removed - if (objectIndexRef.current < MAX_BLOCK_INSTANCES - 1) { + if (objectIndexRef.current < MAX_BLOCK_INSTANCES) { objectIndexRef.current += 1; } else { - objectIndexRef.current = 0; + objectIndexRef.current = 1; } return block.id; @@ -146,33 +149,49 @@ export const useRenderTangle = () => { /** * Spray animation */ - useFrame(() => { - const isPlaying = useConfigStore.getState().isPlaying; - - if (!isPlaying) { - return; - } - - const blockIdToAnimationPosition = useTangleStore.getState().blockIdToAnimationPosition; - const updateBlockIdToAnimationPosition = useTangleStore.getState().updateBlockIdToAnimationPosition; - + useEffect(() => { const updatedAnimationPositions: Map = new Map(); + const updateAnimationPositionQueue: Map = new Map(); + const SPRAY_FRAMES_PER_SECOND = 24; + + const interval = setInterval(() => { + blockIdToAnimationPosition.forEach((properties, blockId) => { + const { initPosition, targetPosition, blockAddedTimestamp } = properties; + const currentAnimationTime = getVisualizerTimeDiff(); + const elapsedTime = currentAnimationTime - blockAddedTimestamp; + const animationAlpha = Math.min(elapsedTime / SPRAY_ANIMATION_DURATION, 1); + const targetPositionVector = new THREE.Vector3(); + + targetPositionVector.lerpVectors(positionToVector(initPosition), positionToVector(targetPosition), animationAlpha); + updatedAnimationPositions.set(blockId, { initPosition, elapsedTime, targetPosition, blockAddedTimestamp }); + + const index = blockIdToIndex.get(blockId); + if (index) { + if (isPlaying) { + updateInstancedMeshPosition(tangleMeshRef.current, index, targetPositionVector); + } else { + updateAnimationPositionQueue.set(index, targetPositionVector); + } + } + }); + }, 1000 / SPRAY_FRAMES_PER_SECOND); - blockIdToAnimationPosition.forEach(({ initPosition, targetPosition, blockAddedTimestamp }, blockId) => { - const currentAnimationTime = getVisualizerTimeDiff(); - const elapsedTime = currentAnimationTime - blockAddedTimestamp; - const positionBasedOnTime = Math.min(elapsedTime / ANIMATION_TIME_SECONDS, 1); - const targetPositionVector = new THREE.Vector3(); - - targetPositionVector.lerpVectors(positionToVector(initPosition), positionToVector(targetPosition), positionBasedOnTime); - updatedAnimationPositions.set(blockId, { initPosition, elapsedTime, targetPosition, blockAddedTimestamp }); + updateBlockIdToAnimationPosition(updatedAnimationPositions); + setUpdateAnimationPositionQueue(updateAnimationPositionQueue); - const index = blockIdToIndex.get(blockId); - if (index) { - updateInstancedMeshPosition(tangleMeshRef.current, index, targetPositionVector); - } - }); + return () => clearInterval(interval); + }, [blockIdToAnimationPosition, isPlaying]); - updateBlockIdToAnimationPosition(updatedAnimationPositions); - }); + /** + * Update animation positions after unpausing + */ + useEffect(() => { + if (isPlaying) { + updateAnimationPositionQueue.forEach((position, index) => { + updateInstancedMeshPosition(tangleMeshRef.current, index, position); + }); + updateAnimationPositionQueue.clear(); + setUpdateAnimationPositionQueue(updateAnimationPositionQueue); + } + }, [isPlaying, updateAnimationPositionQueue]); }; diff --git a/client/src/features/visualizer-threejs/utils.ts b/client/src/features/visualizer-threejs/utils.ts index bba6c9ec2..a77a28edd 100644 --- a/client/src/features/visualizer-threejs/utils.ts +++ b/client/src/features/visualizer-threejs/utils.ts @@ -1,13 +1,9 @@ -import { Vector3 } from "three"; +import { Vector3, MathUtils } from "three"; import { - BLOCK_STEP_PX, MIN_BLOCKS_PER_SECOND, MAX_BLOCKS_PER_SECOND, MIN_TANGLE_RADIUS, MAX_TANGLE_RADIUS, - MIN_BLOCK_NEAR_RADIUS, - MAX_PREV_POINTS, - MAX_POINT_RETRIES, MAX_BLOCK_INSTANCES, EMITTER_SPEED_MULTIPLIER, CAMERA_X_AXIS_MOVEMENT, @@ -20,8 +16,16 @@ import { NUMBER_OF_RANDOM_AMPLITUDES, MIN_SINUSOID_AMPLITUDE, MAX_SINUSOID_AMPLITUDE, + NUMBER_OF_RANDOM_TILTINGS, + TILT_DURATION_SECONDS, + SPRAY_DISTANCE, + MAX_PREV_POINTS, + MAX_POINT_RETRIES, + MIN_BLOCK_NEAR_RADIUS, + MIN_TILT_FACTOR_DEGREES, + MAX_TILT_FACTOR_DEGREES, } from "./constants"; -import type { ICameraAngles, ISinusoidalPositionParams, IThreeDimensionalPosition } from "./interfaces"; +import type { ICameraAngles, ISinusoidalPositionParams, IThreeDimensionalPosition, ITwoDimensionalPosition } from "./interfaces"; /** * Generates a random number within a specified range. @@ -69,16 +73,6 @@ interface IBlockTanglePosition { z: number; } -/** - * Calculates the distance between two points. - * @returns the distance between two points. - */ -function distanceBetweenPoints(point1: IBlockTanglePosition, point2: IBlockTanglePosition): number { - const { z: z1, y: y1 } = point1; - const { z: z2, y: y2 } = point2; - return Math.sqrt((y2 - y1) ** 2 + (z2 - z1) ** 2); -} - /** * Calculates the radius of the circle based on the blocks per second. * @returns the radius of the circle. @@ -94,28 +88,8 @@ function getLinearRadius(bps: number): number { return radius; } -/** - * Generates a random point on a circle. - * @returns the random point on a circle. - */ -function getDynamicRandomYZPoints( - bps: number, - initialPosition: IThreeDimensionalPosition = { - x: 0, - y: 0, - z: 0, - }, -): IBlockTanglePosition { - const theta = Math.random() * (2 * Math.PI); - - const maxRadius = getLinearRadius(bps); - const randomFactor = Math.random(); - const radius = randomFactor * maxRadius; - - const y = radius * Math.cos(theta) + initialPosition.y; - const z = radius * Math.sin(theta) + initialPosition.z; - - return { y, z }; +function distanceBetweenPoints(point1: IBlockTanglePosition, point2: IBlockTanglePosition): number { + return Math.sqrt(Math.pow(point1.y - point2.y, 2) + Math.pow(point1.z - point2.z, 2)); } /** @@ -130,6 +104,20 @@ function pointPassesAllChecks(point: IBlockTanglePosition, prevPoints: IBlockTan return prevPoints.some((prevPoint) => distanceBetweenPoints(point, prevPoint) > MIN_BLOCK_NEAR_RADIUS); } +export function getBlockPositionGenerator(): ( + bps: number, + initialPosition: IThreeDimensionalPosition, + tiltDegress: number, +) => IThreeDimensionalPosition { + const prevPoints: IBlockTanglePosition[] = []; + + return (bps: number, initialPosition: IThreeDimensionalPosition, tiltDegress: number) => { + const point = generateAValidRandomPoint(bps, initialPosition, prevPoints, tiltDegress); + prevPoints.push({ y: point.y, z: point.z }); + return point; + }; +} + /** * Retries to generate a point until it passes all the checks. * @returns the point that passes all the checks. @@ -138,18 +126,20 @@ function generateAValidRandomPoint( bps: number, initialPosition: IThreeDimensionalPosition, prevPoints: IBlockTanglePosition[], -): IBlockTanglePosition { - let trialPoint: IBlockTanglePosition; + tiltDegress: number, +): IThreeDimensionalPosition { + let trialPoint: IThreeDimensionalPosition; let passAllChecks = false; let retries = 0; do { - trialPoint = getDynamicRandomYZPoints(bps, initialPosition); + trialPoint = generateRandomXYZPoints(bps, initialPosition, tiltDegress); passAllChecks = pointPassesAllChecks(trialPoint, prevPoints); retries++; } while (!passAllChecks && retries < MAX_POINT_RETRIES); prevPoints.push(trialPoint); + if (prevPoints.length > MAX_PREV_POINTS) { prevPoints.shift(); } @@ -158,23 +148,30 @@ function generateAValidRandomPoint( } /** - * Gets a function to generate a random point on a circle. - * @returns the function to generate the random point on a circle. + * Generates a random point on a circle. + * @returns the random point on a circle. */ -export function getGenerateDynamicYZPosition(): typeof getDynamicRandomYZPoints { - const prevPoints: IBlockTanglePosition[] = []; - - return (bps: number, initialPosition: IThreeDimensionalPosition = { x: 0, y: 0, z: 0 }): IBlockTanglePosition => { - const validPoint = generateAValidRandomPoint(bps, initialPosition, prevPoints); +export function generateRandomXYZPoints( + bps: number, + initialPosition: IThreeDimensionalPosition, + tiltDegrees: number, +): IThreeDimensionalPosition { + const tiltRad = MathUtils.degToRad(-tiltDegrees); + const opposite = SPRAY_DISTANCE * Math.sin(tiltRad); + const adjacent = SPRAY_DISTANCE * Math.cos(tiltRad); + const circumferenceCenter: ITwoDimensionalPosition = { + x: initialPosition.x - adjacent, + y: initialPosition.y + opposite, + }; - const randomYNumber = randomNumberFromInterval(0, BLOCK_STEP_PX / 20); - const randomZNumber = randomNumberFromInterval(0, BLOCK_STEP_PX / 20); + const _radius = getLinearRadius(bps); + const randomFactor = Math.random(); + const radius = _radius * randomFactor; - validPoint.y += randomYNumber; - validPoint.z += randomZNumber; + const y = circumferenceCenter.y + radius * Math.cos(radius); + const z = initialPosition.z + radius * Math.sin(radius); - return validPoint; - }; + return { x: circumferenceCenter.x, y, z }; } /** @@ -343,5 +340,47 @@ export function generateRandomAmplitudes(): number[] { currentAmplitude = getNextAmplitudeWithVariation(currentAmplitude); amplitudes.push(currentAmplitude); } + return amplitudes; } + +export function generateRandomTiltings(): number[] { + let previousValue: number; + + const tilts: number[] = Array.from({ length: NUMBER_OF_RANDOM_TILTINGS }, () => { + let randomTilt = randomIntFromInterval(MIN_TILT_FACTOR_DEGREES, MAX_TILT_FACTOR_DEGREES); + + if ((previousValue < 0 && randomTilt < 0) || (previousValue > 0 && randomTilt > 0)) { + randomTilt *= -1; + } + + previousValue = randomTilt; + + return randomTilt; + }); + return tilts; +} + +export function getCurrentTiltValue(animationTime: number, tilts: number[]): number { + const tiltAnimationDuration = TILT_DURATION_SECONDS * 2; // Multiplied by 2 so it goes back to the initial position + const totalIntervalDuration = tilts.length * tiltAnimationDuration; // The total duration of the random tilts + + const currentTiltAnimationSeconds = animationTime % tiltAnimationDuration; + const currentAnimationSecondsInInterval = animationTime % totalIntervalDuration; + + const currentTiltIndex = Math.floor(currentAnimationSecondsInInterval / tiltAnimationDuration); + const tilt = tilts[currentTiltIndex]; + + // Calculate the proportion of the current animation time within the half-duration + const proportionOfHalfDuration = currentTiltAnimationSeconds / (tiltAnimationDuration / 2); + let currentTilt; + + if (currentTiltAnimationSeconds <= tiltAnimationDuration / 2) { + currentTilt = tilt * proportionOfHalfDuration; + } else { + // We subtract from 2 to reverse the effect after the peak + currentTilt = tilt * (2 - proportionOfHalfDuration); + } + + return currentTilt; +} diff --git a/client/src/helpers/nova/hooks/useSlotsFeed.ts b/client/src/helpers/nova/hooks/useSlotsFeed.ts index 626250c0b..fc39aa8c4 100644 --- a/client/src/helpers/nova/hooks/useSlotsFeed.ts +++ b/client/src/helpers/nova/hooks/useSlotsFeed.ts @@ -1,53 +1,98 @@ import moment from "moment"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { ServiceFactory } from "~/factories/serviceFactory"; +import { useIsMounted } from "~/helpers/hooks/useIsMounted"; +import { ISlotCommitmentWrapper } from "~/models/api/nova/ILatestSlotCommitmentsResponse"; +import { NOVA } from "~/models/config/protocolVersion"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; +import { useNetworkInfoNova } from "../networkInfo"; import { useNovaTimeConvert } from "./useNovaTimeConvert"; const DEFAULT_SLOT_LIMIT = 10; +const MAX_LATEST_SLOT_COMMITMENTS = 20; + +const CHECK_SLOT_INDEX_INTERVAL = 950; +const CHECK_SLOT_COMMITMENTS_INTERVAL = 3500; export default function useSlotsFeed(slotsLimit: number = DEFAULT_SLOT_LIMIT): { - currentSlot: number | null; + currentSlotIndex: number | null; currentSlotProgressPercent: number | null; - latestSlots: number[] | null; + latestSlotIndexes: number[] | null; + latestSlotCommitments: ISlotCommitmentWrapper[]; } { + const isMounted = useIsMounted(); + const { name: network } = useNetworkInfoNova((s) => s.networkInfo); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); const { unixTimestampToSlotIndex, slotIndexToUnixTimeRange } = useNovaTimeConvert(); - const [currentSlot, setCurrentSlot] = useState(null); - const [latestSlots, setLatestSlots] = useState(null); + const [currentSlotIndex, setCurrentSlotIndex] = useState(null); + const [latestSlotIndexes, setLatestSlotIndexes] = useState(null); + + const [latestSlotCommitments, setLatestSlotCommitments] = useState([]); + const [currentSlotProgressPercent, setCurrentSlotProgressPercent] = useState(null); - const [slotTimeUpdateHandle, setSlotTimeUpdateHandle] = useState(null); - const checkCurrentSlot = () => { + const [slotIndexCheckerHandle, setSlotIndexCheckerHandle] = useState(null); + const [slotCommitmentsCheckerHandle, setSlotCommitmentsCheckerHandle] = useState(null); + + const checkCurrentSlotIndex = () => { if (unixTimestampToSlotIndex && slotIndexToUnixTimeRange) { const now = moment().unix(); const currentSlotIndex = unixTimestampToSlotIndex(now); const slotTimeRange = slotIndexToUnixTimeRange(currentSlotIndex); const slotProgressPercent = Math.trunc(((now - slotTimeRange.from) / (slotTimeRange.to - 1 - slotTimeRange.from)) * 100); - setCurrentSlot(currentSlotIndex); - setCurrentSlotProgressPercent(slotProgressPercent); - setLatestSlots(Array.from({ length: slotsLimit - 1 }, (_, i) => currentSlotIndex - 1 - i)); + + if (isMounted) { + setCurrentSlotIndex(currentSlotIndex); + setCurrentSlotProgressPercent(slotProgressPercent); + setLatestSlotIndexes(Array.from({ length: slotsLimit - 1 }, (_, i) => currentSlotIndex - 1 - i)); + } } }; + const getLatestSlotCommitments = useCallback(async () => { + if (apiClient) { + const latestSlotCommitments = await apiClient.latestSlotCommitments(network); + if (isMounted && latestSlotCommitments.slotCommitments && latestSlotCommitments.slotCommitments.length > 0) { + setLatestSlotCommitments(latestSlotCommitments.slotCommitments.slice(0, MAX_LATEST_SLOT_COMMITMENTS)); + } + } + }, [network]); + useEffect(() => { - if (slotTimeUpdateHandle === null) { - checkCurrentSlot(); - const intervalTimerHandle = setInterval(() => { - checkCurrentSlot(); - }, 950); + if (slotIndexCheckerHandle === null) { + getLatestSlotCommitments(); + checkCurrentSlotIndex(); + + const slotCommitmentCheckerHandle = setInterval(() => { + getLatestSlotCommitments(); + }, CHECK_SLOT_COMMITMENTS_INTERVAL); + + const slotIndexIntervalHandle = setInterval(() => { + checkCurrentSlotIndex(); + }, CHECK_SLOT_INDEX_INTERVAL); - setSlotTimeUpdateHandle(intervalTimerHandle); + setSlotCommitmentsCheckerHandle(slotCommitmentCheckerHandle); + setSlotIndexCheckerHandle(slotIndexIntervalHandle); } return () => { - if (slotTimeUpdateHandle) { - clearInterval(slotTimeUpdateHandle); + if (slotCommitmentsCheckerHandle) { + clearInterval(slotCommitmentsCheckerHandle); } - setSlotTimeUpdateHandle(null); - setCurrentSlot(null); + + if (slotIndexCheckerHandle) { + clearInterval(slotIndexCheckerHandle); + } + + setSlotCommitmentsCheckerHandle(null); + setSlotIndexCheckerHandle(null); + setCurrentSlotIndex(null); setCurrentSlotProgressPercent(null); - setLatestSlots(null); + setLatestSlotIndexes(null); + setLatestSlotCommitments([]); }; }, []); - return { currentSlot, currentSlotProgressPercent, latestSlots }; + return { currentSlotIndex, currentSlotProgressPercent, latestSlotIndexes, latestSlotCommitments }; } diff --git a/client/src/models/api/nova/ILatestSlotCommitmentsResponse.ts b/client/src/models/api/nova/ILatestSlotCommitmentsResponse.ts new file mode 100644 index 000000000..68f04a973 --- /dev/null +++ b/client/src/models/api/nova/ILatestSlotCommitmentsResponse.ts @@ -0,0 +1,16 @@ +import { SlotCommitment } from "@iota/sdk-wasm-nova/web"; +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[]; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index 81152f01f..afa1ebbe3 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -37,6 +37,7 @@ import { ICongestionRequest } from "~/models/api/nova/ICongestionRequest"; import { ICongestionResponse } from "~/models/api/nova/ICongestionResponse"; import { IAccountValidatorDetailsRequest } from "~/models/api/nova/IAccountValidatorDetailsRequest"; import { IAccountValidatorDetailsResponse } from "~/models/api/nova/IAccountValidatorDetailsResponse"; +import { ILatestSlotCommitmentResponse } from "~/models/api/nova/ILatestSlotCommitmentsResponse"; /** * Class to handle api communications on nova. @@ -172,6 +173,15 @@ export class NovaApiClient extends ApiClient { ); } + /** + * Get the latest slot commitments. + * @param network The network in context. + * @returns The latest slot commitments response. + */ + public async latestSlotCommitments(network: string): Promise { + return this.callApi(`nova/commitment/latest/${network}`, "get"); + } + /** * Get the account congestion. * @param request The request to send.