diff --git a/.github/workflows/nova-build-temp.yaml b/.github/workflows/nova-build-temp.yaml index 2508da903..7b718a706 100644 --- a/.github/workflows/nova-build-temp.yaml +++ b/.github/workflows/nova-build-temp.yaml @@ -6,7 +6,7 @@ on: TARGET_COMMIT: description: "Target Commit Hash for the SDK" required: false - default: "8f0ff5e1e899a0d960ddfea09237739a88c3bcf1" + default: "fc9f0f56bb5cfc146993e53aa9656ded220734e1" environment: type: choice description: "Select the environment to deploy to" diff --git a/api/src/models/api/nova/ICongestionRequest.ts b/api/src/models/api/nova/ICongestionRequest.ts new file mode 100644 index 000000000..00e845db6 --- /dev/null +++ b/api/src/models/api/nova/ICongestionRequest.ts @@ -0,0 +1,11 @@ +export interface ICongestionRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The account id to get the congestion for. + */ + accountId: string; +} diff --git a/api/src/models/api/nova/ICongestionResponse.ts b/api/src/models/api/nova/ICongestionResponse.ts new file mode 100644 index 000000000..9e7fff964 --- /dev/null +++ b/api/src/models/api/nova/ICongestionResponse.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { CongestionResponse } from "@iota/sdk-nova"; +import { IResponse } from "./IResponse"; + +export interface ICongestionResponse extends IResponse { + /** + * The Account Congestion. + */ + congestion?: CongestionResponse; +} diff --git a/api/src/routes.ts b/api/src/routes.ts index cfd81b27f..60a25ca9d 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -248,6 +248,12 @@ export const routes: IRoute[] = [ folder: "nova/transaction", func: "get", }, + { + path: "/nova/account/congestion/:network/:accountId", + method: "get", + folder: "nova/account/congestion", + func: "get", + }, { 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" }, ]; diff --git a/api/src/routes/nova/account/congestion/get.ts b/api/src/routes/nova/account/congestion/get.ts new file mode 100644 index 000000000..4ab680766 --- /dev/null +++ b/api/src/routes/nova/account/congestion/get.ts @@ -0,0 +1,30 @@ +import { ServiceFactory } from "../../../../factories/serviceFactory"; +import { ICongestionRequest } from "../../../../models/api/nova/ICongestionRequest"; +import { ICongestionResponse } from "../../../../models/api/nova/ICongestionResponse"; +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"; + +/** + * Get Congestion for Account address + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: ICongestionRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.accountId, "accountId"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.getAccountCongestion(request.accountId); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index 9a51450bb..06bc6c426 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -11,6 +11,7 @@ import { IAddressDetailsResponse } from "../../models/api/nova/IAddressDetailsRe import { IAnchorDetailsResponse } from "../../models/api/nova/IAnchorDetailsResponse"; import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse"; import { IBlockResponse } from "../../models/api/nova/IBlockResponse"; +import { ICongestionResponse } from "../../models/api/nova/ICongestionResponse"; import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse"; import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse"; import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse"; @@ -275,6 +276,25 @@ export class NovaApiService { }; } + /** + * Get Congestion for Account + * @param accountId The account address to get the congestion for. + * @returns The Congestion. + */ + public async getAccountCongestion(accountId: string): Promise { + try { + const response = await this.client.getAccountCongestion(accountId); + + if (response) { + return { + congestion: response, + }; + } + } catch { + return { message: "Account congestion not found" }; + } + } + /** * Get the output mana rewards. * @param outputId The outputId to get the rewards for. diff --git a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx index 29410a48d..e548927c5 100644 --- a/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx +++ b/client/src/app/components/nova/address/section/AddressPageTabbedSections.tsx @@ -1,6 +1,8 @@ import React, { useState } from "react"; import associatedOuputsMessage from "~assets/modals/stardust/address/associated-outputs.json"; import foundriesMessage from "~assets/modals/stardust/alias/foundries.json"; +import stateMessage from "~assets/modals/stardust/alias/state.json"; +import bicMessage from "~assets/modals/nova/account/bic.json"; import TabbedSection from "../../../hoc/TabbedSection"; import AssociatedOutputs from "./association/AssociatedOutputs"; import nativeTokensMessage from "~assets/modals/stardust/address/assets-in-wallet.json"; @@ -12,6 +14,8 @@ import AssetsTable from "./native-tokens/AssetsTable"; import { IImplicitAccountCreationAddressState } from "~/helpers/nova/hooks/useImplicitAccountCreationAddressState"; import { AddressType } from "@iota/sdk-wasm-nova/web"; import AccountFoundriesSection from "./account/AccountFoundriesSection"; +import AccountBlockIssuanceSection from "./account/AccountBlockIssuanceSection"; +import AnchorStateSection from "./anchor/AnchorStateSection"; enum DEFAULT_TABS { AssocOutputs = "Outputs", @@ -19,9 +23,14 @@ enum DEFAULT_TABS { } enum ACCOUNT_TABS { + BlockIssuance = "Block Issuance", Foundries = "Foundries", } +enum ANCHOR_TABS { + State = "State", +} + const buildDefaultTabsOptions = (tokensCount: number, associatedOutputCount: number) => ({ [DEFAULT_TABS.AssocOutputs]: { disabled: associatedOutputCount === 0, @@ -37,13 +46,33 @@ const buildDefaultTabsOptions = (tokensCount: number, associatedOutputCount: num }, }); -const buildAccountAddressTabsOptions = (foundriesCount: number, isAccountFoundriesLoading: boolean) => ({ +const buildAccountAddressTabsOptions = ( + isBlockIssuer: boolean, + isCongestionLoading: boolean, + foundriesCount: number, + isAccountFoundriesLoading: boolean, +) => ({ [ACCOUNT_TABS.Foundries]: { disabled: foundriesCount === 0, hidden: foundriesCount === 0, isLoading: isAccountFoundriesLoading, infoContent: foundriesMessage, }, + [ACCOUNT_TABS.BlockIssuance]: { + disabled: !isBlockIssuer, + hidden: !isBlockIssuer, + isLoading: isCongestionLoading, + infoContent: bicMessage, + }, +}); + +const buildAnchorAddressTabsOptions = (isAnchorStateTabDisabled: boolean, isAnchorDetailsLoading: boolean) => ({ + [ANCHOR_TABS.State]: { + disabled: isAnchorStateTabDisabled, + hidden: isAnchorStateTabDisabled, + isLoading: isAnchorDetailsLoading, + infoContent: stateMessage, + }, }); interface IAddressPageTabbedSectionsProps { @@ -78,6 +107,11 @@ export const AddressPageTabbedSections: React.FC, , + ] + : null; + let tabEnums = DEFAULT_TABS; const defaultTabsOptions = buildDefaultTabsOptions(tokensCount, outputCount); let tabOptions = defaultTabsOptions; @@ -92,17 +136,30 @@ export const AddressPageTabbedSections: React.FC = ({ blockIssuerFeature, congestion }) => { + return ( +
+
+
+
Current Slot
+
{congestion?.slot}
+
+
+
Block Issuance Credit
+
{congestion?.blockIssuanceCredits.toString()}
+
+
+
Referenced Mana Cost
+
{congestion?.referenceManaCost.toString()}
+
+
+
Expiry Slot
+
{blockIssuerFeature?.expirySlot}
+
+
+ {blockIssuerFeature?.blockIssuerKeys.map((key) => ( + + Public Key: +
+ +
+
+ ))} +
+
+
+ ); +}; + +export default AccountBlockIssuanceSection; diff --git a/client/src/app/components/nova/address/section/anchor/AnchorStateSection.tsx b/client/src/app/components/nova/address/section/anchor/AnchorStateSection.tsx new file mode 100644 index 000000000..ffc910a9f --- /dev/null +++ b/client/src/app/components/nova/address/section/anchor/AnchorStateSection.tsx @@ -0,0 +1,37 @@ +import { AnchorOutput, FeatureType, StateMetadataFeature } from "@iota/sdk-wasm-nova/web"; +import React from "react"; +import DataToggle from "~/app/components/DataToggle"; + +interface AnchorStateSectionProps { + /** + * The Anchor Output + */ + readonly output: AnchorOutput | null; +} + +const AnchorStateSection: React.FC = ({ output }) => { + const stateMetadata = output?.features?.find((feature) => feature.type === FeatureType.StateMetadata) as StateMetadataFeature; + + return ( +
+
+
+
State Index
+
+ {output?.stateIndex} +
+
+ {Object.entries(stateMetadata.entries).map(([key, value], index) => ( +
+
{key}
+
+ +
+
+ ))} +
+
+ ); +}; + +export default AnchorStateSection; diff --git a/client/src/assets/modals/nova/account/bic.json b/client/src/assets/modals/nova/account/bic.json new file mode 100644 index 000000000..3b875279d --- /dev/null +++ b/client/src/assets/modals/nova/account/bic.json @@ -0,0 +1,11 @@ +{ + "title": "Block Issuance Credit", + "description": "

(BIC) is the form of Mana used as an anti-spam mechanism to the block issuance process.

", + "links": [ + { + "label": "Read more", + "href": "https://wiki.iota.org/learn/protocols/iota2.0/core-concepts/mana/#block-issuance-credits-bic", + "isExternal": true + } + ] +} diff --git a/client/src/features/visualizer-threejs/CameraControls.tsx b/client/src/features/visualizer-threejs/CameraControls.tsx index 2419d39dc..18c36e95e 100644 --- a/client/src/features/visualizer-threejs/CameraControls.tsx +++ b/client/src/features/visualizer-threejs/CameraControls.tsx @@ -3,34 +3,56 @@ import { getCameraAngles } from "./utils"; import React, { useEffect } from "react"; import { useThree } from "@react-three/fiber"; import { CanvasElement } from "./enums"; -import { useTangleStore } from "./store"; import { VISUALIZER_PADDINGS } from "./constants"; +const CAMERA_ANGLES = getCameraAngles(); + const CameraControls = () => { - const [shouldLockZoom, setShouldLockZoom] = React.useState(false); const controls = React.useRef(null); - const CAMERA_ANGLES = getCameraAngles(); + const scene = useThree((state) => state.scene); + const mesh = scene.getObjectByName(CanvasElement.TangleWrapperMesh) as THREE.Mesh | undefined; - const zoom = useTangleStore((s) => s.zoom); - const get = useThree((state) => state.get); - const mesh = get().scene.getObjectByName(CanvasElement.TangleWrapperMesh); + /** + * Locks the camera zoom to the current zoom value. + */ + function lockCameraZoom(controls: DreiCameraControls) { + const zoom = controls.camera.zoom; + controls.maxZoom = zoom; + controls.minZoom = zoom; + } - // Set fixed zoom - useEffect(() => { - if (controls.current && shouldLockZoom) { - controls.current.maxZoom = zoom; - controls.current.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); + controls.fitToBox(mesh, false, { ...VISUALIZER_PADDINGS }); + controls.setOrbitPoint(0, 0, 0); + lockCameraZoom(controls); } - }, [controls, zoom, shouldLockZoom]); + } - // Fix to TangleMesh + /** + * Fit camera to TangleMesh on mount and on window resize. + */ useEffect(() => { - if (controls.current && mesh) { - controls.current.fitToBox(mesh, false, { ...VISUALIZER_PADDINGS }); - controls.current.setOrbitPoint(0, 0, 0); - setShouldLockZoom(true); - } + const adjustCamera = () => fitCameraToTangle(controls.current, mesh); + adjustCamera(); + + window.addEventListener("resize", adjustCamera); + return () => { + window.removeEventListener("resize", adjustCamera); + }; }, [controls, mesh]); return ; diff --git a/client/src/features/visualizer-threejs/Emitter.tsx b/client/src/features/visualizer-threejs/Emitter.tsx index 767dfee56..fba26e0c7 100644 --- a/client/src/features/visualizer-threejs/Emitter.tsx +++ b/client/src/features/visualizer-threejs/Emitter.tsx @@ -1,10 +1,10 @@ /* eslint-disable react/no-unknown-property */ import { useFrame, useThree } from "@react-three/fiber"; -import React, { RefObject, Dispatch, SetStateAction, useEffect, useRef } from "react"; +import React, { RefObject, Dispatch, SetStateAction, useEffect, useRef, useLayoutEffect } from "react"; import * as THREE from "three"; import { useConfigStore, useTangleStore } from "./store"; import { useRenderTangle } from "./useRenderTangle"; -import { getTangleDistances, getEmitterPositions } from "./utils"; +import { getTangleDistances, getEmitterPositions, generateRandomPeriods, generateRandomAmplitudes } from "./utils"; import { CanvasElement } from "./enums"; import useVisualizerTimer from "~/helpers/nova/hooks/useVisualizerTimer"; import { EMITTER_DEPTH, EMITTER_HEIGHT, EMITTER_WIDTH } from "./constants"; @@ -28,8 +28,24 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte const setIsPlaying = useConfigStore((state) => state.setIsPlaying); const setInitialTime = useConfigStore((state) => state.setInitialTime); + const sinusoidPeriodsSum = useConfigStore((state) => state.sinusoidPeriodsSum); + const setSinusoidPeriodsSum = useConfigStore((state) => state.setSinusoidPeriodsSum); + const sinusoidRandomPeriods = useConfigStore((state) => state.sinusoidRandomPeriods); + const setSinusoidRandomPeriods = useConfigStore((state) => state.setSinusoidRandomPeriods); + + const randomSinusoidAmplitudes = useConfigStore((state) => state.randomSinusoidAmplitudes); + const setRandomSinusoidAmplitudes = useConfigStore((state) => state.setRandomSinusoidAmplitudes); + const tangleWrapperRef = useRef(null); + useLayoutEffect(() => { + const { periods, sum: periodsSum } = generateRandomPeriods(); + const amplitudes = generateRandomAmplitudes(); + setSinusoidRandomPeriods(periods); + setSinusoidPeriodsSum(periodsSum); + setRandomSinusoidAmplitudes(amplitudes); + }, []); + useEffect(() => { setZoom(currentZoom); }, [currentZoom]); @@ -52,7 +68,12 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte */ useFrame(() => { const currentAnimationTime = getVisualizerTimeDiff(); - const { x, y } = getEmitterPositions(currentAnimationTime); + const { x, y } = getEmitterPositions({ + currentAnimationTime, + periods: sinusoidRandomPeriods, + periodsSum: sinusoidPeriodsSum, + sinusoidAmplitudes: randomSinusoidAmplitudes, + }); if (isPlaying) { if (emitterRef.current) { @@ -84,7 +105,7 @@ const Emitter: React.FC = ({ setRunListeners, emitterRef }: Emitte {/* Emitter Mesh */} - + ); diff --git a/client/src/features/visualizer-threejs/VisualizerInstance.tsx b/client/src/features/visualizer-threejs/VisualizerInstance.tsx index 751d8e187..26f62570f 100644 --- a/client/src/features/visualizer-threejs/VisualizerInstance.tsx +++ b/client/src/features/visualizer-threejs/VisualizerInstance.tsx @@ -44,6 +44,7 @@ const VisualizerInstance: React.FC> = }) => { const [networkConfig] = useNetworkConfig(network); const themeMode = useGetThemeMode(); + const getCurrentAnimationTime = useVisualizerTimer(); const [runListeners, setRunListeners] = React.useState(false); @@ -72,14 +73,16 @@ const VisualizerInstance: React.FC> = const addToConfirmedBlocksSlot = useTangleStore((s) => s.addToConfirmedBlocksBySlot); const removeConfirmedBlocksSlot = useTangleStore((s) => s.removeConfirmedBlocksSlot); + const sinusoidPeriodsSum = useConfigStore((s) => s.sinusoidPeriodsSum); + const sinusoidRandomPeriods = useConfigStore((s) => s.sinusoidRandomPeriods); + const sinusoidRandomAmplitudes = useConfigStore((s) => s.randomSinusoidAmplitudes); + const selectedFeedItem: TSelectFeedItemNova = clickedInstanceId ? blockMetadata.get(clickedInstanceId) ?? null : null; const resetConfigState = useTangleStore((s) => s.resetConfigState); const emitterRef = useRef(null); const [feedService, setFeedService] = React.useState(ServiceFactory.get(`feed-${network}`)); - const getCurrentAnimationTime = useVisualizerTimer(); - /** * Pause on tab or window change */ @@ -199,7 +202,12 @@ const VisualizerInstance: React.FC> = if (blockData) { const currentAnimationTime = getCurrentAnimationTime(); const bps = bpsCounter.getBPS(); - const initPosition = getBlockInitPosition(currentAnimationTime); + const initPosition = getBlockInitPosition({ + currentAnimationTime, + periods: sinusoidRandomPeriods, + periodsSum: sinusoidPeriodsSum, + sinusoidAmplitudes: sinusoidRandomAmplitudes, + }); const targetPosition = getBlockTargetPosition(initPosition, bps); bpsCounter.addBlock(); diff --git a/client/src/features/visualizer-threejs/blockPositions.ts b/client/src/features/visualizer-threejs/blockPositions.ts index c38e0cab2..a01f521a6 100644 --- a/client/src/features/visualizer-threejs/blockPositions.ts +++ b/client/src/features/visualizer-threejs/blockPositions.ts @@ -1,4 +1,5 @@ import { EMITTER_WIDTH, EMITTER_X_POSITION_MULTIPLIER } from "./constants"; +import { ISinusoidalPositionParams } from "./interfaces"; import { getEmitterPositions, getGenerateDynamicYZPosition, getTangleDistances, randomIntFromInterval } from "./utils"; const generateYZPositions = getGenerateDynamicYZPosition(); @@ -22,9 +23,9 @@ export function getBlockTargetPosition(initPosition: IPos, bps: number): IPos { return { x, y, z }; } -export function getBlockInitPosition(currentAnimationTime: number): IPos { +export function getBlockInitPosition({ currentAnimationTime, periods, periodsSum, sinusoidAmplitudes }: ISinusoidalPositionParams): IPos { const { xTangleDistance } = getTangleDistances(); - const { x: xEmitterPos, y, z } = getEmitterPositions(currentAnimationTime); + const { x: xEmitterPos, y, z } = getEmitterPositions({ currentAnimationTime, periods, periodsSum, sinusoidAmplitudes }); const x = xEmitterPos + xTangleDistance / 2; return { x, y, z }; diff --git a/client/src/features/visualizer-threejs/constants.ts b/client/src/features/visualizer-threejs/constants.ts index 37b399ec5..cc588c7f8 100644 --- a/client/src/features/visualizer-threejs/constants.ts +++ b/client/src/features/visualizer-threejs/constants.ts @@ -26,8 +26,8 @@ export const PENDING_BLOCK_COLOR = new Color("#A6C3FC"); export const ACCEPTED_BLOCK_COLOR = new Color("#0101AB"); export const CONFIRMED_BLOCK_COLOR = new Color("#0000DB"); export const FINALIZED_BLOCK_COLOR = new Color("#0101FF"); -// TODO Remove accepted state once is added to the SDK (missing) -export const BLOCK_STATE_TO_COLOR = new Map([ + +export const BLOCK_STATE_TO_COLOR = new Map([ ["pending", PENDING_BLOCK_COLOR], ["accepted", ACCEPTED_BLOCK_COLOR], ["confirmed", CONFIRMED_BLOCK_COLOR], @@ -72,7 +72,6 @@ export const EMITTER_HEIGHT = 250; export const EMITTER_DEPTH = 250; // conic emitter - export const MIN_TANGLE_RADIUS = 100; export const MAX_TANGLE_RADIUS = 300; @@ -86,7 +85,11 @@ export const MAX_PREV_POINTS = 20; export const EMITTER_X_POSITION_MULTIPLIER = 3; -export const MAX_SINUSOIDAL_AMPLITUDE = 200; -export const SINUSOIDAL_AMPLITUDE_ACCUMULATOR = 30; -export const INITIAL_SINUSOIDAL_AMPLITUDE = 80; -export const HALF_WAVE_PERIOD_SECONDS = 5; +/* Values for randomizing the tangle */ +export const NUMBER_OF_RANDOM_PERIODS = 100; +export const MIN_SINUSOID_PERIOD = 5; +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; diff --git a/client/src/features/visualizer-threejs/interfaces.ts b/client/src/features/visualizer-threejs/interfaces.ts index 0efef94cb..81bae2dd5 100644 --- a/client/src/features/visualizer-threejs/interfaces.ts +++ b/client/src/features/visualizer-threejs/interfaces.ts @@ -10,3 +10,13 @@ export interface IThreeDimensionalPosition { y: number; z: number; } + +export interface ITimeBasedPositionParams { + currentAnimationTime: number; +} + +export interface ISinusoidalPositionParams extends ITimeBasedPositionParams { + periods: number[]; + periodsSum: number; + sinusoidAmplitudes: number[]; +} diff --git a/client/src/features/visualizer-threejs/store/config.ts b/client/src/features/visualizer-threejs/store/config.ts index 02956518b..d505f5177 100644 --- a/client/src/features/visualizer-threejs/store/config.ts +++ b/client/src/features/visualizer-threejs/store/config.ts @@ -15,6 +15,14 @@ interface ConfigState { initialTime: number | null; setInitialTime: (initialTime: number) => void; + + sinusoidPeriodsSum: number; + setSinusoidPeriodsSum: (totalPeriodsSum: number) => void; + sinusoidRandomPeriods: number[]; + setSinusoidRandomPeriods: (randomizedPeriods: number[]) => void; + + randomSinusoidAmplitudes: number[]; + setRandomSinusoidAmplitudes: (randomizedAmplitudes: number[]) => void; } export const useConfigStore = create((set) => ({ @@ -73,4 +81,33 @@ export const useConfigStore = create((set) => ({ initialTime, })); }, + + /** + * Randomized periods for the tangle. + */ + sinusoidPeriodsSum: 0, + setSinusoidPeriodsSum: (totalPeriodsSum) => { + set((state) => ({ + ...state, + sinusoidPeriodsSum: totalPeriodsSum, + })); + }, + sinusoidRandomPeriods: [], + setSinusoidRandomPeriods: (randomizedPeriods) => { + set((state) => ({ + ...state, + sinusoidRandomPeriods: randomizedPeriods, + })); + }, + + /** + * Randomized amplitudes for the tangle. + */ + randomSinusoidAmplitudes: [], + setRandomSinusoidAmplitudes: (randomizedAmplitudes) => { + set((state) => ({ + ...state, + randomSinusoidAmplitudes: randomizedAmplitudes, + })); + }, })); diff --git a/client/src/features/visualizer-threejs/utils.ts b/client/src/features/visualizer-threejs/utils.ts index ddf39952b..bba6c9ec2 100644 --- a/client/src/features/visualizer-threejs/utils.ts +++ b/client/src/features/visualizer-threejs/utils.ts @@ -8,18 +8,20 @@ import { MIN_BLOCK_NEAR_RADIUS, MAX_PREV_POINTS, MAX_POINT_RETRIES, - HALF_WAVE_PERIOD_SECONDS, MAX_BLOCK_INSTANCES, EMITTER_SPEED_MULTIPLIER, - MAX_SINUSOIDAL_AMPLITUDE, CAMERA_X_AXIS_MOVEMENT, CAMERA_Y_AXIS_MOVEMENT, CAMERA_X_OFFSET, CAMERA_Y_OFFSET, - SINUSOIDAL_AMPLITUDE_ACCUMULATOR, - INITIAL_SINUSOIDAL_AMPLITUDE, + NUMBER_OF_RANDOM_PERIODS, + MIN_SINUSOID_PERIOD, + MAX_SINUSOID_PERIOD, + NUMBER_OF_RANDOM_AMPLITUDES, + MIN_SINUSOID_AMPLITUDE, + MAX_SINUSOID_AMPLITUDE, } from "./constants"; -import { ICameraAngles, IThreeDimensionalPosition } from "./interfaces"; +import type { ICameraAngles, ISinusoidalPositionParams, IThreeDimensionalPosition } from "./interfaces"; /** * Generates a random number within a specified range. @@ -191,7 +193,7 @@ export function getTangleDistances(): { const maxXDistance = MAX_BLOCK_DISTANCE; /* Max Y Distance will be multiplied by 2 to position blocks in the negative and positive Y axis */ - const maxYDistance = MAX_TANGLE_RADIUS * 2 + MAX_SINUSOIDAL_AMPLITUDE * 2; + const maxYDistance = MAX_TANGLE_RADIUS * 2 + MAX_SINUSOID_AMPLITUDE * 2; /* TODO: add sinusoidal distances */ @@ -231,16 +233,24 @@ export function getCameraAngles(): ICameraAngles { } /** - * Calculates the sinusoidal position for the emitter based on the current animation time. + * Calculates the sinusoidal position for the emitter based on the current animation time, + * considering random periods. * @returns the sinusoidal position */ -export function calculateSinusoidalAmplitude(currentAnimationTime: number): number { - const wavePeriod = HALF_WAVE_PERIOD_SECONDS * 2; - const currentWaveCount = Math.floor(currentAnimationTime / wavePeriod); - const accumulatedAmplitude = currentWaveCount * SINUSOIDAL_AMPLITUDE_ACCUMULATOR; - const currentAmplitude = Math.min(INITIAL_SINUSOIDAL_AMPLITUDE + accumulatedAmplitude, MAX_SINUSOIDAL_AMPLITUDE); +export function calculateSinusoidalAmplitude({ + currentAnimationTime, + periods, + periodsSum, + sinusoidAmplitudes, +}: ISinusoidalPositionParams): number { + const elapsedTime = currentAnimationTime % periodsSum; + const { index, period, accumulatedTime } = getCurrentPeriodValues(currentAnimationTime, periods, periodsSum); - const yPosition = currentAmplitude * Math.sin((2 * Math.PI * currentAnimationTime) / wavePeriod); + const startTimeOfCurrentPeriod = accumulatedTime - period; + const timeInCurrentPeriod = elapsedTime - startTimeOfCurrentPeriod; + const currentAmplitude = sinusoidAmplitudes[index]; + + const yPosition = currentAmplitude * Math.sin((2 * Math.PI * timeInCurrentPeriod) / period); return yPosition; } @@ -257,9 +267,14 @@ export function calculateEmitterPositionX(currentAnimationTime: number): number * Calculates the emitter position based on the current animation time. * @returns the emitter X,Y,Z positions */ -export function getEmitterPositions(currentAnimationTime: number): IThreeDimensionalPosition { +export function getEmitterPositions({ + currentAnimationTime, + periods, + periodsSum, + sinusoidAmplitudes, +}: ISinusoidalPositionParams): IThreeDimensionalPosition { const x = calculateEmitterPositionX(currentAnimationTime); - const y = calculateSinusoidalAmplitude(currentAnimationTime); + const y = calculateSinusoidalAmplitude({ currentAnimationTime, periods, periodsSum, sinusoidAmplitudes }); return { x, y, z: 0 }; } @@ -271,3 +286,62 @@ export function getEmitterPositions(currentAnimationTime: number): IThreeDimensi export function positionToVector(position: IThreeDimensionalPosition) { return new Vector3(position.x, position.y, position.z); } + +export function generateRandomPeriods(): { periods: number[]; sum: number } { + let sum = 0; + const periods = Array.from({ length: NUMBER_OF_RANDOM_PERIODS }, () => { + const period = Number(randomNumberFromInterval(MIN_SINUSOID_PERIOD, MAX_SINUSOID_PERIOD).toFixed(4)); + sum += period; + return period; + }); + return { periods, sum }; +} + +type PeriodResult = { + period: number; + accumulatedTime: number; + index: number; +}; + +function getCurrentPeriodValues(animationTime: number, periods: number[], totalSum: number): PeriodResult { + const effectiveTime = animationTime % totalSum; + + let accumulatedTime = 0; + + for (let i = 0; i < periods.length; i++) { + const period = periods[i]; + accumulatedTime += period; + if (effectiveTime < accumulatedTime) { + return { index: i, period, accumulatedTime }; + } + } + + return { index: 0, period: periods[0], accumulatedTime: 0 }; +} + +function getNextAmplitudeWithVariation(currentAmplitude: number = 0): number { + const variation = (2 * MIN_SINUSOID_AMPLITUDE) / 3; + const randomAmplitudeVariation = randomNumberFromInterval(-variation, variation); + + let newAmplitude = currentAmplitude + randomAmplitudeVariation; + + if (newAmplitude > MAX_SINUSOID_AMPLITUDE) { + newAmplitude = currentAmplitude - Math.abs(randomAmplitudeVariation); + } else if (newAmplitude < MIN_SINUSOID_AMPLITUDE) { + newAmplitude = currentAmplitude + Math.abs(randomAmplitudeVariation); + } + + newAmplitude = Math.max(MIN_SINUSOID_AMPLITUDE, Math.min(newAmplitude, MAX_SINUSOID_AMPLITUDE)); + + return newAmplitude; +} + +export function generateRandomAmplitudes(): number[] { + const amplitudes: number[] = []; + let currentAmplitude: number = 0; + for (let i = 0; i < NUMBER_OF_RANDOM_AMPLITUDES; i++) { + currentAmplitude = getNextAmplitudeWithVariation(currentAmplitude); + amplitudes.push(currentAmplitude); + } + return amplitudes; +} diff --git a/client/src/helpers/nova/hooks/useAccountAddressState.ts b/client/src/helpers/nova/hooks/useAccountAddressState.ts index a92f679db..09c5ec62d 100644 --- a/client/src/helpers/nova/hooks/useAccountAddressState.ts +++ b/client/src/helpers/nova/hooks/useAccountAddressState.ts @@ -1,5 +1,12 @@ import { Reducer, useEffect, useReducer } from "react"; -import { AccountAddress, AccountOutput, OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { + AccountAddress, + AccountOutput, + BlockIssuerFeature, + CongestionResponse, + FeatureType, + OutputResponse, +} from "@iota/sdk-wasm-nova/web"; import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; import { useAccountDetails } from "./useAccountDetails"; import { useLocation, useParams } from "react-router-dom"; @@ -9,18 +16,22 @@ import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; import { useAccountControlledFoundries } from "./useAccountControlledFoundries"; +import { useAccountCongestion } from "./useAccountCongestion"; export interface IAccountAddressState { addressDetails: IAddressDetails | null; accountOutput: AccountOutput | null; totalBalance: number | null; availableBalance: number | null; + blockIssuerFeature: BlockIssuerFeature | null; addressBasicOutputs: OutputResponse[] | null; foundries: string[] | null; + congestion: CongestionResponse | null; isAccountDetailsLoading: boolean; isAssociatedOutputsLoading: boolean; isBasicOutputsLoading: boolean; isFoundriesLoading: boolean; + isCongestionLoading: boolean; } const initialState = { @@ -28,12 +39,15 @@ const initialState = { accountOutput: null, totalBalance: null, availableBalance: null, + blockIssuerFeature: null, addressBasicOutputs: null, foundries: null, + congestion: null, isAccountDetailsLoading: true, isAssociatedOutputsLoading: false, isBasicOutputsLoading: false, isFoundriesLoading: false, + isCongestionLoading: false, }; /** @@ -56,6 +70,7 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, accountOutput); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [foundries, isFoundriesLoading] = useAccountControlledFoundries(network, state.addressDetails); + const { congestion, isLoading: isCongestionLoading } = useAccountCongestion(network, state.addressDetails?.hex ?? null); useEffect(() => { const locationState = location.state as IAddressPageLocationProps; @@ -70,17 +85,42 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres }, []); useEffect(() => { - setState({ + let updatedState: Partial = { accountOutput, isAccountDetailsLoading, totalBalance, availableBalance, foundries, + congestion, addressBasicOutputs, isBasicOutputsLoading, isFoundriesLoading, - }); - }, [accountOutput, totalBalance, availableBalance, addressBasicOutputs, isAccountDetailsLoading, isBasicOutputsLoading]); + isCongestionLoading, + }; + + if (accountOutput && !state.blockIssuerFeature) { + const blockIssuerFeature = accountOutput?.features?.find( + (feature) => feature.type === FeatureType.BlockIssuer, + ) as BlockIssuerFeature; + if (blockIssuerFeature) { + updatedState = { + ...updatedState, + blockIssuerFeature, + }; + } + } + + setState(updatedState); + }, [ + accountOutput, + totalBalance, + availableBalance, + addressBasicOutputs, + congestion, + isAccountDetailsLoading, + isBasicOutputsLoading, + isCongestionLoading, + ]); return [state, setState]; }; diff --git a/client/src/helpers/nova/hooks/useAccountCongestion.ts b/client/src/helpers/nova/hooks/useAccountCongestion.ts new file mode 100644 index 000000000..7ca8eafd9 --- /dev/null +++ b/client/src/helpers/nova/hooks/useAccountCongestion.ts @@ -0,0 +1,48 @@ +import { CongestionResponse } from "@iota/sdk-wasm-nova/web"; +import { useEffect, useState } from "react"; +import { ServiceFactory } from "~/factories/serviceFactory"; +import { useIsMounted } from "~/helpers/hooks/useIsMounted"; +import { NOVA } from "~/models/config/protocolVersion"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; + +/** + * Fetch account congestion + * @param network The Network in context + * @param accountId The account id + * @returns The output response and loading bool. + */ +export function useAccountCongestion( + network: string, + accountId: string | null, +): { congestion: CongestionResponse | null; isLoading: boolean } { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [congestion, setAccountCongestion] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + if (accountId) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .getAccountCongestion({ + network, + accountId, + }) + .then((response) => { + if (!response?.error && isMounted) { + setAccountCongestion(response.congestion ?? null); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, accountId]); + + return { congestion, isLoading }; +} diff --git a/client/src/models/api/nova/ICongestionRequest.ts b/client/src/models/api/nova/ICongestionRequest.ts new file mode 100644 index 000000000..00e845db6 --- /dev/null +++ b/client/src/models/api/nova/ICongestionRequest.ts @@ -0,0 +1,11 @@ +export interface ICongestionRequest { + /** + * The network to search on. + */ + network: string; + + /** + * The account id to get the congestion for. + */ + accountId: string; +} diff --git a/client/src/models/api/nova/ICongestionResponse.ts b/client/src/models/api/nova/ICongestionResponse.ts new file mode 100644 index 000000000..cb176a6f3 --- /dev/null +++ b/client/src/models/api/nova/ICongestionResponse.ts @@ -0,0 +1,11 @@ +/* eslint-disable import/no-unresolved */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { CongestionResponse } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "./IResponse"; + +export interface ICongestionResponse extends IResponse { + /** + * The Account Congestion. + */ + congestion?: CongestionResponse; +} diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index acd1b1610..5c03dbf0b 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -31,6 +31,8 @@ import { IFoundriesResponse } from "~/models/api/nova/foundry/IFoundriesResponse import { IFoundriesRequest } from "~/models/api/nova/foundry/IFoundriesRequest"; import { ITransactionDetailsRequest } from "~/models/api/nova/ITransactionDetailsRequest"; import { ITransactionDetailsResponse } from "~/models/api/nova/ITransactionDetailsResponse"; +import { ICongestionRequest } from "~/models/api/nova/ICongestionRequest"; +import { ICongestionResponse } from "~/models/api/nova/ICongestionResponse"; /** * Class to handle api communications on nova. @@ -157,6 +159,15 @@ export class NovaApiClient extends ApiClient { ); } + /** + * Get the account congestion. + * @param request The request to send. + * @returns The response from the request. + */ + public async getAccountCongestion(request: ICongestionRequest): Promise { + return this.callApi(`nova/account/congestion/${request.network}/${request.accountId}`, "get"); + } + /** * Get the output mana rewards. * @param request The request to send. diff --git a/setup_nova.sh b/setup_nova.sh index 7fdf8c22f..b69fcd007 100755 --- a/setup_nova.sh +++ b/setup_nova.sh @@ -1,6 +1,6 @@ #!/bin/bash SDK_DIR="iota-sdk" -TARGET_COMMIT="bbe86bbc2ef7a38b88768b78142519e0817c370e" +TARGET_COMMIT="fc9f0f56bb5cfc146993e53aa9656ded220734e1" if [ ! -d "$SDK_DIR" ]; then git clone -b 2.0 git@github.com:iotaledger/iota-sdk.git @@ -35,5 +35,3 @@ rm package.json.bak echo "Building wasm bindings" yarn yarn build - -