diff --git a/api/package-lock.json b/api/package-lock.json index 5f0ef8b1e..6fc4f6198 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -63,7 +63,7 @@ } }, "../iota-sdk/bindings/nodejs": { - "name": "@iota/sdk", + "name": "@iota/sdk-nova", "version": "1.1.3", "hasInstallScript": true, "license": "Apache-2.0", diff --git a/api/src/initServices.ts b/api/src/initServices.ts index 7c95782d9..58937f5c3 100644 --- a/api/src/initServices.ts +++ b/api/src/initServices.ts @@ -1,6 +1,6 @@ import { MqttClient as ChrysalisMqttClient } from "@iota/mqtt.js"; import { Client as StardustClient } from "@iota/sdk"; -import { initLogger, Client as NovaMqttClient } from "@iota/sdk-nova"; +import { initLogger, Client as NovaClient } from "@iota/sdk-nova"; import { ServiceFactory } from "./factories/serviceFactory"; import logger from "./logger"; import { IConfiguration } from "./models/configuration/IConfiguration"; @@ -22,10 +22,11 @@ import { ZmqService } from "./services/legacy/zmqService"; import { LocalStorageService } from "./services/localStorageService"; import { NetworkService } from "./services/networkService"; import { NovaFeed } from "./services/nova/feed/novaFeed"; +import { NodeInfoService as NodeInfoServiceNova } from "./services/nova/nodeInfoService"; import { ChronicleService } from "./services/stardust/chronicleService"; import { StardustFeed } from "./services/stardust/feed/stardustFeed"; import { InfluxDBService } from "./services/stardust/influx/influxDbService"; -import { NodeInfoService } from "./services/stardust/nodeInfoService"; +import { NodeInfoService as NodeInfoServiceStardust } from "./services/stardust/nodeInfoService"; import { StardustStatsService } from "./services/stardust/stats/stardustStatsService"; // iota-sdk debug @@ -203,7 +204,7 @@ function initStardustServices(networkConfig: INetwork): void { } // eslint-disable-next-line no-void - void NodeInfoService.build(networkConfig).then(nodeInfoService => { + void NodeInfoServiceStardust.build(networkConfig).then(nodeInfoService => { ServiceFactory.register( `node-info-${networkConfig.network}`, () => nodeInfoService @@ -241,19 +242,32 @@ function initStardustServices(networkConfig: INetwork): void { function initNovaServices(networkConfig: INetwork): void { logger.verbose(`Initializing Nova services for ${networkConfig.network}`); - const mqttInstance = new NovaMqttClient( - { nodes: [networkConfig.feedEndpoint], brokerOptions: { useWs: true }, ignoreNodeHealth: true } - ); - ServiceFactory.register( - `mqtt-${networkConfig.network}`, - () => mqttInstance - ); + // eslint-disable-next-line no-void + void NovaClient.create({ + nodes: [networkConfig.provider], + brokerOptions: { useWs: true }, + // Needed only for now in local development (NOT FOR PROD) + ignoreNodeHealth: true + }).then(novaClient => { + ServiceFactory.register( + `client-${networkConfig.network}`, + () => novaClient + ); - const feedInstance = new NovaFeed(networkConfig.network); - ServiceFactory.register( - `feed-${networkConfig.network}`, - () => feedInstance - ); + // eslint-disable-next-line no-void + void NodeInfoServiceNova.build(networkConfig).then(nodeInfoService => { + ServiceFactory.register( + `node-info-${networkConfig.network}`, + () => nodeInfoService + ); + + const feedInstance = new NovaFeed(networkConfig.network); + ServiceFactory.register( + `feed-${networkConfig.network}`, + () => feedInstance + ); + }); + }); } /** diff --git a/api/src/models/api/nova/INodeInfoResponse.ts b/api/src/models/api/nova/INodeInfoResponse.ts new file mode 100644 index 000000000..2ca694651 --- /dev/null +++ b/api/src/models/api/nova/INodeInfoResponse.ts @@ -0,0 +1,8 @@ +import { INodeInfo } from "@iota/sdk-nova"; +import { IResponse } from "../IResponse"; + +/** + * The response with node info for a specific network. + */ +export type INodeInfoResponse = INodeInfo & IResponse; + diff --git a/api/src/models/api/nova/feed/IFeedUpdate.ts b/api/src/models/api/nova/feed/IFeedUpdate.ts new file mode 100644 index 000000000..b4aa0c8f3 --- /dev/null +++ b/api/src/models/api/nova/feed/IFeedUpdate.ts @@ -0,0 +1,9 @@ +import { Block } from "@iota/sdk-nova"; + +type IFeedBlockUpdate = Block; + +export interface IFeedUpdate { + subscriptionId: string; + block?: IFeedBlockUpdate; +} + diff --git a/api/src/routes/feed/subscribe.ts b/api/src/routes/feed/subscribe.ts index 54c73af19..94561b275 100644 --- a/api/src/routes/feed/subscribe.ts +++ b/api/src/routes/feed/subscribe.ts @@ -4,10 +4,11 @@ import logger from "../../logger"; import { IFeedSubscribeRequest } from "../../models/api/IFeedSubscribeRequest"; import { IFeedSubscribeResponse } from "../../models/api/IFeedSubscribeResponse"; import { IConfiguration } from "../../models/configuration/IConfiguration"; -import { CHRYSALIS, LEGACY, STARDUST } from "../../models/db/protocolVersion"; +import { CHRYSALIS, LEGACY, NOVA, STARDUST } from "../../models/db/protocolVersion"; import { IItemsService as IItemsServiceChrysalis } from "../../models/services/chrysalis/IItemsService"; import { IItemsService as IItemsServiceLegacy } from "../../models/services/legacy/IItemsService"; import { NetworkService } from "../../services/networkService"; +import { NovaFeed } from "../../services/nova/feed/novaFeed"; import { StardustFeed } from "../../services/stardust/feed/stardustFeed"; import { ValidationHelper } from "../../utils/validationHelper"; @@ -67,6 +68,16 @@ export async function subscribe( ) ); } + } else if (networkConfig.protocolVersion === NOVA) { + const service = ServiceFactory.get(`feed-${request.network}`); + if (service) { + await service.subscribeBlocks( + socket.id, + async data => { + socket.emit("block", data); + } + ); + } } else { return { error: "Network protocol not supported for feed." diff --git a/api/src/routes/feed/unsubscribe.ts b/api/src/routes/feed/unsubscribe.ts index caa43bb75..6b39dcb2f 100644 --- a/api/src/routes/feed/unsubscribe.ts +++ b/api/src/routes/feed/unsubscribe.ts @@ -4,7 +4,7 @@ import logger from "../../logger"; import { IFeedUnsubscribeRequest } from "../../models/api/IFeedUnsubscribeRequest"; import { IResponse } from "../../models/api/IResponse"; import { IConfiguration } from "../../models/configuration/IConfiguration"; -import { CHRYSALIS, LEGACY, STARDUST } from "../../models/db/protocolVersion"; +import { CHRYSALIS, LEGACY, NOVA, STARDUST } from "../../models/db/protocolVersion"; import { IItemsService as IItemsServiceChrysalis } from "../../models/services/chrysalis/IItemsService"; import { IItemsService as IItemsServiceLegacy } from "../../models/services/legacy/IItemsService"; import { NetworkService } from "../../services/networkService"; @@ -55,6 +55,9 @@ export async function unsubscribe( service?.unsubscribeBlocks(request.subscriptionId); service?.unsubscribeMilestones(request.subscriptionId); } + } else if (networkConfig.protocolVersion === NOVA) { + const service = ServiceFactory.get(`feed-${request.network}`); + service?.unsubscribeBlocks(request.subscriptionId); } else { return { error: "Network protocol not supported for feed." diff --git a/api/src/routes/node/info.ts b/api/src/routes/node/info.ts index 8e50ed601..844a7f285 100644 --- a/api/src/routes/node/info.ts +++ b/api/src/routes/node/info.ts @@ -1,8 +1,9 @@ import { ServiceFactory } from "../../factories/serviceFactory"; import { INetworkBoundGetRequest } from "../../models/api/INetworkBoundGetRequest"; -import { INodeInfoResponse } from "../../models/api/stardust/INodeInfoResponse"; +import { INodeInfoResponse as INovaNodeInfoResponse } from "../../models/api/nova/INodeInfoResponse"; +import { INodeInfoResponse as IStardustNodeInfoResponse } from "../../models/api/stardust/INodeInfoResponse"; import { IConfiguration } from "../../models/configuration/IConfiguration"; -import { STARDUST } from "../../models/db/protocolVersion"; +import { NOVA, STARDUST } from "../../models/db/protocolVersion"; import { NetworkService } from "../../services/networkService"; import { NodeInfoService } from "../../services/stardust/nodeInfoService"; import { ValidationHelper } from "../../utils/validationHelper"; @@ -16,14 +17,17 @@ import { ValidationHelper } from "../../utils/validationHelper"; export async function info( _: IConfiguration, request: INetworkBoundGetRequest -): Promise { +): 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 !== STARDUST) { + if ( + networkConfig.protocolVersion !== STARDUST && + networkConfig.protocolVersion !== NOVA + ) { return {}; } @@ -31,3 +35,4 @@ export async function info( return nodeService.getNodeInfo(); } + diff --git a/api/src/services/nova/feed/novaFeed.ts b/api/src/services/nova/feed/novaFeed.ts index fa23a2a38..d510ca570 100644 --- a/api/src/services/nova/feed/novaFeed.ts +++ b/api/src/services/nova/feed/novaFeed.ts @@ -1,7 +1,9 @@ -import { BasicBlock, Client, IBlockMetadata } from "@iota/sdk-nova"; +import { Block, Client, IBlockMetadata } from "@iota/sdk-nova"; import { ClassConstructor, plainToInstance } from "class-transformer"; import { ServiceFactory } from "../../../factories/serviceFactory"; import logger from "../../../logger"; +import { IFeedUpdate } from "../../../models/api/nova/feed/IFeedUpdate"; +import { NodeInfoService } from "../nodeInfoService"; /** * Wrapper class around Nova MqttClient. @@ -12,7 +14,7 @@ export class NovaFeed { * The block feed subscribers (downstream). */ protected readonly blockSubscribers: { - [id: string]: (data: Record) => Promise; + [id: string]: (data: IFeedUpdate) => Promise; }; /** @@ -20,6 +22,11 @@ export class NovaFeed { */ private readonly _mqttClient: Client; + /** + * The network in context. + */ + private readonly network: string; + /** * Creates a new instance of NovaFeed. * @param networkId The network id. @@ -27,17 +34,35 @@ export class NovaFeed { constructor(networkId: string) { logger.debug("[NovaFeed] Constructing a Nova Feed"); this.blockSubscribers = {}; - this._mqttClient = ServiceFactory.get(`mqtt-${networkId}`); + this.network = networkId; + this._mqttClient = ServiceFactory.get(`client-${networkId}`); + const nodeInfoService = ServiceFactory.get(`node-info-${networkId}`); - logger.debug(`[NovaFeed] Mqtt is ${JSON.stringify(this._mqttClient)}`); - - if (this._mqttClient) { + if (this._mqttClient && nodeInfoService) { this.connect(); } else { throw new Error(`Failed to build novaFeed instance for ${networkId}`); } } + /** + * Subscribe to the blocks nova feed. + * @param id The id of the subscriber. + * @param callback The callback to call with data for the event. + */ + public async subscribeBlocks(id: string, callback: (data: IFeedUpdate) => Promise): Promise { + this.blockSubscribers[id] = callback; + } + + /** + * Unsubscribe from the blocks feed. + * @param subscriptionId The id to unsubscribe. + */ + public unsubscribeBlocks(subscriptionId: string): void { + logger.debug(`[NovaFeed] Removing subscriber ${subscriptionId} from blocks (${this.network})`); + delete this.blockSubscribers[subscriptionId]; + } + /** * Connects the callbacks for upstream data. */ @@ -46,7 +71,7 @@ export class NovaFeed { // eslint-disable-next-line no-void void this._mqttClient.listenMqtt(["blocks"], (_, message) => { try { - const block: BasicBlock = this.parseMqttPayloadMessage(BasicBlock, message); + const block: Block = this.parseMqttPayloadMessage(Block, message); const update: Partial> = { block }; diff --git a/api/src/services/nova/nodeInfoService.ts b/api/src/services/nova/nodeInfoService.ts new file mode 100644 index 000000000..723d41bf8 --- /dev/null +++ b/api/src/services/nova/nodeInfoService.ts @@ -0,0 +1,44 @@ +import { Client, INodeInfo } from "@iota/sdk-nova"; +import { NodeInfoError } from "../../errors/nodeInfoError"; +import { ServiceFactory } from "../../factories/serviceFactory"; +import { INetwork } from "../../models/db/INetwork"; + +/** + * Class to handle Nova protocol node info. + */ +export class NodeInfoService { + /** + * The network configuration. + */ + protected readonly _network: INetwork; + + /** + * The node and token info. + */ + protected _nodeInfo: INodeInfo; + + /** + * Create a new instance of NodeInfoService. + * @param network The network config. + * @param nodeInfo The fetched node info + */ + private constructor(network: INetwork, nodeInfo: INodeInfo) { + this._network = network; + this._nodeInfo = nodeInfo; + } + + public static async build(network: INetwork): Promise { + const apiClient = ServiceFactory.get(`client-${network.network}`); + + try { + const response = await apiClient.getInfo(); + return new NodeInfoService(network, response.nodeInfo); + } catch (err) { + throw new NodeInfoError(`Failed to fetch node info for "${network.network}" with error:\n${err}`); + } + } + + public getNodeInfo(): INodeInfo { + return this._nodeInfo; + } +} diff --git a/client/package-lock.json b/client/package-lock.json index 31b4cf0fa..cc8e515b3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -101,7 +101,7 @@ } }, "../iota-sdk/bindings/wasm": { - "name": "@iota/sdk-wasm", + "name": "@iota/sdk-wasm-nova", "version": "1.1.1", "license": "Apache-2.0", "dependencies": { diff --git a/client/script/postinstall.sh b/client/script/postinstall.sh index e7986d9ef..7ddc6ed5e 100755 --- a/client/script/postinstall.sh +++ b/client/script/postinstall.sh @@ -7,6 +7,8 @@ mkdir -p "$TARGET" # stardust cp "$NODE_MODULES/@iota/sdk-wasm/web/wasm/iota_sdk_wasm_bg.wasm" "$TARGET/iota_sdk_stardust_wasm_bg.wasm" +# nova +cp "$NODE_MODULES/@iota/sdk-wasm-nova/web/wasm/iota_sdk_wasm_bg.wasm" "$TARGET/iota_sdk_nova_wasm_bg.wasm" # identity cp "$NODE_MODULES/@iota/identity-wasm/web/identity_wasm_bg.wasm" "$TARGET/identity_wasm_bg.wasm" diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx index d72336ffd..d684ec8c2 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -20,7 +20,7 @@ import { INetwork } from "~models/config/INetwork"; import { MAINNET } from "~models/config/networkType"; import { STARDUST } from "~models/config/protocolVersion"; import { NetworkService } from "~services/networkService"; -import { NodeInfoService } from "~services/nodeInfoService"; +import { NodeInfoService as NodeInfoServiceStardust } from "~services/stardust/nodeInfoService"; import "./App.scss"; const App: React.FC> = ( @@ -48,7 +48,7 @@ const App: React.FC> = ( const identityResolverEnabled = networkConfig?.identityResolverEnabled ?? true; const currentNetworkName = networkConfig?.network; const isShimmer = isShimmerUiTheme(networkConfig?.uiTheme); - const nodeService = ServiceFactory.get("node-info"); + const nodeService = ServiceFactory.get("node-info-stardust"); const nodeInfo = networkConfig?.network ? nodeService.get(networkConfig?.network) : null; const withNetworkContext = networkContextWrapper(currentNetworkName, nodeInfo, networkConfig?.uiTheme); scrollToTop(); diff --git a/client/src/app/AppUtils.tsx b/client/src/app/AppUtils.tsx index 08baa7403..67f6c6ba0 100644 --- a/client/src/app/AppUtils.tsx +++ b/client/src/app/AppUtils.tsx @@ -4,11 +4,11 @@ import NetworkContext from "./context/NetworkContext"; import { INetwork } from "~models/config/INetwork"; import { ALPHANET, CHRYSALIS_MAINNET, DEVNET, LEGACY_MAINNET, MAINNET, NetworkType, SHIMMER, TESTNET } from "~models/config/networkType"; import { IOTA_UI, Theme } from "~models/config/uiTheme"; -import { IReducedNodeInfo } from "~services/nodeInfoService"; +import { IStardustNodeInfo } from "~services/stardust/nodeInfoService"; export const networkContextWrapper = ( currentNetwork: string | undefined, - nodeInfo: IReducedNodeInfo | null, + nodeInfo: IStardustNodeInfo | null, uiTheme: Theme | undefined ) => function withNetworkContext(wrappedComponent: ReactNode) { return currentNetwork && nodeInfo ? ( diff --git a/client/src/app/components/NetworkSwitcher.tsx b/client/src/app/components/NetworkSwitcher.tsx index b8afdfe77..f718cf09a 100644 --- a/client/src/app/components/NetworkSwitcher.tsx +++ b/client/src/app/components/NetworkSwitcher.tsx @@ -5,13 +5,14 @@ import MainnetIcon from "~assets/mainnet.svg?react"; import { NetworkSwitcherProps } from "./NetworkSwitcherProps"; import { getNetworkOrder } from "~helpers/networkHelper"; import { MAINNET } from "~models/config/networkType"; -import { CHRYSALIS, LEGACY, STARDUST } from "~models/config/protocolVersion"; +import { CHRYSALIS, LEGACY, NOVA, STARDUST } from "~models/config/protocolVersion"; import "./NetworkSwitcher.scss"; const PROTOCOL_VERIONS_TO_LABEL = { [LEGACY]: "Legacy", [CHRYSALIS]: "Chrysalis", - [STARDUST]: "Stardust" + [STARDUST]: "Stardust", + [NOVA]: "Nova" }; /** diff --git a/client/src/app/routes.tsx b/client/src/app/routes.tsx index dde1f7194..3f7e9c9cd 100644 --- a/client/src/app/routes.tsx +++ b/client/src/app/routes.tsx @@ -35,12 +35,12 @@ import StardustOutputPage from "./routes/stardust/OutputPage"; import StardustSearch from "./routes/stardust/Search"; import StardustStatisticsPage from "./routes/stardust/statistics/StatisticsPage"; import StardustTransactionPage from "./routes/stardust/TransactionPage"; -// import { Visualizer as StardustVisualizer } from "./routes/stardust/Visualizer"; -import StardustVisualizer from "../features/visualizer-threejs/VisualizerInstance"; +import { Visualizer as StardustVisualizer } from "./routes/stardust/Visualizer"; +import NovaVisualizer from "../features/visualizer-threejs/VisualizerInstance"; import StreamsV0 from "./routes/StreamsV0"; import { StreamsV0RouteProps } from "./routes/StreamsV0RouteProps"; import { VisualizerRouteProps } from "./routes/VisualizerRouteProps"; -import { CHRYSALIS, LEGACY, STARDUST } from "~models/config/protocolVersion"; +import { CHRYSALIS, LEGACY, NOVA, STARDUST } from "~models/config/protocolVersion"; /** * Generator for keys in routes. Gives an incremented value on every next(). @@ -208,6 +208,13 @@ const buildAppRoutes = ( /> ]; + const novaRoutes = [ + + ]; + return ( {commonRoutes} @@ -220,6 +227,9 @@ const buildAppRoutes = ( {protocolVersion === STARDUST && ( withNetworkContext(stardustRoutes) )} + {protocolVersion === NOVA && ( + novaRoutes + )} ); }; diff --git a/client/src/features/visualizer-threejs/VisualizerInstance.tsx b/client/src/features/visualizer-threejs/VisualizerInstance.tsx index 69bac25b7..463a939e2 100644 --- a/client/src/features/visualizer-threejs/VisualizerInstance.tsx +++ b/client/src/features/visualizer-threejs/VisualizerInstance.tsx @@ -15,7 +15,7 @@ import { VisualizerRouteProps } from "../../app/routes/VisualizerRouteProps"; import { ServiceFactory } from "../../factories/serviceFactory"; import { useNetworkConfig } from "../../helpers/hooks/useNetworkConfig"; import { IFeedBlockData } from "../../models/api/stardust/feed/IFeedBlockData"; -import { StardustFeedClient } from "../../services/stardust/stardustFeedClient"; +import { NovaFeedClient } from "../../services/nova/novaFeedClient"; import { Wrapper } from "./wrapper/Wrapper"; import "./Visualizer.scss"; import { IFeedBlockMetadata } from "~/models/api/stardust/feed/IFeedBlockMetadata"; @@ -57,7 +57,7 @@ const VisualizerInstance: React.FC> = const indexToBlockId = useTangleStore(s => s.indexToBlockId); const emitterRef = useRef(null); - const feedServiceRef = useRef(null); + const feedServiceRef = useRef(null); /** * Pause on tab or window change @@ -171,7 +171,7 @@ const VisualizerInstance: React.FC> = if (!runListeners) { return; } - feedServiceRef.current = ServiceFactory.get( + feedServiceRef.current = ServiceFactory.get( `feed-${network}` ); setIsPlaying(true); diff --git a/client/src/index.tsx b/client/src/index.tsx index e6a065165..4c6ce16e8 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -2,7 +2,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ // needed for features from @iota/sdk which use reflection (decorators) import "reflect-metadata"; -import initStardustSdk from "@iota/sdk-wasm/web"; +import initSdkStardust from "@iota/sdk-wasm/web"; +import initSdkNova from "@iota/sdk-wasm-nova/web"; import React from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter, Route, RouteComponentProps } from "react-router-dom"; @@ -10,7 +11,7 @@ import App from "~app/App"; import { AppRouteProps } from "~app/AppRouteProps"; import { ServiceFactory } from "~factories/serviceFactory"; import "./index.scss"; -import { CHRYSALIS, LEGACY, STARDUST } from "~models/config/protocolVersion"; +import { CHRYSALIS, LEGACY, NOVA, STARDUST } from "~models/config/protocolVersion"; import { ChrysalisApiClient } from "~services/chrysalis/chrysalisApiClient"; import { ChrysalisFeedClient } from "~services/chrysalis/chrysalisFeedClient"; import { ChrysalisTangleCacheService } from "~services/chrysalis/chrysalisTangleCacheService"; @@ -21,20 +22,24 @@ import { LegacyFeedClient } from "~services/legacy/legacyFeedClient"; import { LegacyTangleCacheService } from "~services/legacy/legacyTangleCacheService"; import { LocalStorageService } from "~services/localStorageService"; import { NetworkService } from "~services/networkService"; -import { NodeInfoService } from "~services/nodeInfoService"; +import { NodeInfoService as NodeInfoServiceStardust } from "~services/stardust/nodeInfoService"; +import { NodeInfoService as NodeInfoServiceNova } from "~services/nova/nodeInfoService"; import { SettingsService } from "~services/settingsService"; import { StardustApiClient } from "~services/stardust/stardustApiClient"; import { StardustFeedClient } from "~services/stardust/stardustFeedClient"; +import { NovaApiClient } from "./services/nova/novaApiClient"; +import { TokenRegistryClient } from "~services/stardust/tokenRegistryClient"; import "@fontsource/ibm-plex-mono"; import "@fontsource/material-icons"; -import { TokenRegistryClient } from "~services/stardust/tokenRegistryClient"; +import { NovaFeedClient } from "./services/nova/novaFeedClient"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const apiEndpoint = (window as any).env.API_ENDPOINT; initialiseServices().then(async () => { // load the wasm - await initStardustSdk("/wasm/iota_sdk_stardust_wasm_bg.wasm"); + await initSdkStardust("/wasm/iota_sdk_stardust_wasm_bg.wasm"); + await initSdkNova("/wasm/iota_sdk_nova_wasm_bg.wasm"); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const container = document.querySelector("#root")!; @@ -58,6 +63,7 @@ async function initialiseServices(): Promise { ServiceFactory.register(`api-client-${LEGACY}`, () => new LegacyApiClient(apiEndpoint)); ServiceFactory.register(`api-client-${CHRYSALIS}`, () => new ChrysalisApiClient(apiEndpoint)); ServiceFactory.register(`api-client-${STARDUST}`, () => new StardustApiClient(apiEndpoint)); + ServiceFactory.register(`api-client-${NOVA}`, () => new NovaApiClient(apiEndpoint)); ServiceFactory.register("settings", () => new SettingsService()); ServiceFactory.register("local-storage", () => new LocalStorageService()); @@ -69,9 +75,13 @@ async function initialiseServices(): Promise { await networkService.buildCache(); ServiceFactory.register("network", () => networkService); - const nodeInfoService = new NodeInfoService(); - await nodeInfoService.buildCache(); - ServiceFactory.register("node-info", () => nodeInfoService); + const nodeInfoServiceStardust = new NodeInfoServiceStardust(); + await nodeInfoServiceStardust.buildCache(); + ServiceFactory.register("node-info-stardust", () => nodeInfoServiceStardust); + + const nodeInfoServiceNova = new NodeInfoServiceNova(); + await nodeInfoServiceNova.buildCache(); + ServiceFactory.register("node-info-nova", () => nodeInfoServiceNova); ServiceFactory.register("currency", () => new CurrencyService(apiEndpoint)); ServiceFactory.register(`tangle-cache-${LEGACY}`, () => new LegacyTangleCacheService()); @@ -103,6 +113,13 @@ async function initialiseServices(): Promise { ); break; } + case NOVA: { + ServiceFactory.register( + `feed-${netConfig.network}`, + serviceName => new NovaFeedClient(apiEndpoint, serviceName.slice(5)) + ); + break; + } default: } } diff --git a/client/src/models/api/nova/INodeInfoResponse.ts b/client/src/models/api/nova/INodeInfoResponse.ts new file mode 100644 index 000000000..4eeeff633 --- /dev/null +++ b/client/src/models/api/nova/INodeInfoResponse.ts @@ -0,0 +1,9 @@ +import { INodeInfo } from "@iota/sdk-wasm-nova/web"; +import { IResponse } from "../IResponse"; + +/** + * The response with node info for a specific network. + */ +export type INodeInfoResponse = INodeInfo & IResponse + + diff --git a/client/src/models/api/nova/feed/IFeedBlockData.ts b/client/src/models/api/nova/feed/IFeedBlockData.ts new file mode 100644 index 000000000..0a8983b06 --- /dev/null +++ b/client/src/models/api/nova/feed/IFeedBlockData.ts @@ -0,0 +1,14 @@ +import { Block } from "@iota/sdk-wasm-nova/web"; + +export interface IFeedBlockData { + /** + * The block id. + */ + blockId: string; + + /** + * The block. + */ + block: Block +} + diff --git a/client/src/models/api/nova/feed/IFeedSubscribeRequest.ts b/client/src/models/api/nova/feed/IFeedSubscribeRequest.ts new file mode 100644 index 000000000..81a0c5a49 --- /dev/null +++ b/client/src/models/api/nova/feed/IFeedSubscribeRequest.ts @@ -0,0 +1,14 @@ +export type IFeedSelect = "block"; + +export interface IFeedSubscribeRequest { + /** + * The network in context for the request. + */ + network: string; + + /** + * The specific feed to subscribe too. + */ + feedSelect: IFeedSelect; +} + diff --git a/client/src/models/api/nova/feed/IFeedUpdate.ts b/client/src/models/api/nova/feed/IFeedUpdate.ts new file mode 100644 index 000000000..3b32e1217 --- /dev/null +++ b/client/src/models/api/nova/feed/IFeedUpdate.ts @@ -0,0 +1,9 @@ +import { Block } from "@iota/sdk-wasm-nova/web"; + +type IFeedBlockUpdate = Block; + +export interface IFeedUpdate { + subscriptionId: string; + block?: IFeedBlockUpdate; +} + diff --git a/client/src/models/api/stardust/feed/IFeedSubscribeRequest.ts b/client/src/models/api/stardust/feed/IFeedSubscribeRequest.ts index a12bbb6d6..c8a3103f2 100644 --- a/client/src/models/api/stardust/feed/IFeedSubscribeRequest.ts +++ b/client/src/models/api/stardust/feed/IFeedSubscribeRequest.ts @@ -7,7 +7,7 @@ export interface IFeedSubscribeRequest { network: string; /** - * The specific feed to subscribe too (expected only on stardust feed). + * The specific feed to subscribe too (expected only on stardust/nova feeds). */ feedSelect: IFeedSelect; } diff --git a/client/src/services/nova/nodeInfoService.ts b/client/src/services/nova/nodeInfoService.ts new file mode 100644 index 000000000..07b1e123c --- /dev/null +++ b/client/src/services/nova/nodeInfoService.ts @@ -0,0 +1,44 @@ +import { ServiceFactory } from "~/factories/serviceFactory"; +import { NetworkService } from "../networkService"; +import { NovaApiClient } from "./novaApiClient"; +import { INodeInfoResponse } from "~models/api/nova/INodeInfoResponse" +import { NOVA } from "~/models/config/protocolVersion"; + +/** + * Service to handle base token info on nova. + */ +export class NodeInfoService { + /** + * Cache of the base taken infos. + */ + private _cache: { [network: string]: INodeInfoResponse } = {}; + + /** + * Get the base token info by network. + * @param network The name of the network. + * @returns The base token info. + */ + public get(network: string): INodeInfoResponse { + return this._cache[network]; + } + + /** + * Build the cache of base token infos. + */ + public async buildCache(): Promise { + const networksService = ServiceFactory.get("network"); + const novaNetworks = networksService.networks().filter(n => n.protocolVersion === NOVA); + + for (const networkDetails of novaNetworks) { + const apiClient = ServiceFactory.get(`api-client-${NOVA}`); + const network = networkDetails.network; + const response: INodeInfoResponse = await apiClient.nodeInfo({ network }); + const { baseToken, status, protocolParameters } = response; + + if (baseToken && status && protocolParameters) { + this._cache[network] = response; + } + } + } +} + diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts new file mode 100644 index 000000000..b318c0b19 --- /dev/null +++ b/client/src/services/nova/novaApiClient.ts @@ -0,0 +1,20 @@ +import { INetworkBoundGetRequest } from "~/models/api/INetworkBoundGetRequest"; +import { INodeInfoResponse } from "~/models/api/nova/INodeInfoResponse"; +import { ApiClient } from "../apiClient"; + +/** + * Class to handle api communications on nova. + */ +export class NovaApiClient extends ApiClient { + /** + * Perform a request to get the base token info for the network. + * @param request The Base token request. + * @returns The response from the request. + */ + public async nodeInfo(request: INetworkBoundGetRequest): Promise { + return this.callApi( + `node-info/${request.network}`, + "get" + ); + } +} diff --git a/client/src/services/nova/novaFeedClient.ts b/client/src/services/nova/novaFeedClient.ts new file mode 100644 index 000000000..b17206daf --- /dev/null +++ b/client/src/services/nova/novaFeedClient.ts @@ -0,0 +1,166 @@ +import { + Block +} from "@iota/sdk-wasm-nova/web"; +import { io, Socket } from "socket.io-client"; +import { ServiceFactory } from "~/factories/serviceFactory"; +import { IFeedSubscribeResponse } from "~/models/api/IFeedSubscribeResponse"; +import { IFeedBlockData } from "~/models/api/nova/feed/IFeedBlockData"; +import { IFeedSubscribeRequest } from "~/models/api/nova/feed/IFeedSubscribeRequest"; +import { IFeedUpdate } from "~/models/api/nova/feed/IFeedUpdate"; +import { INodeInfoResponse } from "~/models/api/nova/INodeInfoResponse"; +import { IFeedBlockMetadata } from "~/models/api/stardust/feed/IFeedBlockMetadata"; +import { IFeedUnsubscribeRequest } from "~/models/api/stardust/feed/IFeedUnsubscribeRequest"; +import { INetwork } from "~/models/config/INetwork"; +import { NetworkService } from "../networkService"; +import { NodeInfoService } from "./nodeInfoService"; + +export class NovaFeedClient { + /** + * Network configuration. + */ + protected readonly _networkConfig?: INetwork; + + /** + * Network node info. + */ + protected readonly _nodeInfo?: INodeInfoResponse; + + /** + * Socket endpoint + */ + protected readonly endpoint: string; + + /** + * The web socket to communicate on. + */ + private socket: Socket | null = null; + + /** + * The subscription id for blocks feed. + */ + private blockSubscriptionId?: string; + + /** + * Create a new instance of StardustFeedClient. + * @param endpoint The endpoint for the api. + * @param networkId The network configurations. + */ + constructor(endpoint: string, networkId: string) { + this.endpoint = endpoint; + const networkService = ServiceFactory.get("network"); + const theNetworkConfig = networkService.get(networkId); + const nodeService = ServiceFactory.get("node-info-nova"); + const nodeInfo = theNetworkConfig?.network ? nodeService.get(theNetworkConfig?.network) : null; + + if (theNetworkConfig && nodeInfo) { + this._networkConfig = theNetworkConfig; + this._nodeInfo = nodeInfo + } else { + console.error("[NovaFeedClient] Couldn't initialize client for network", networkId); + } + } + + /** + * Subscribe to the feed of blocks. + * @param onBlockDataCallback the callback for block data updates. + */ + public subscribeBlocks( + onBlockDataCallback?: (blockData: IFeedBlockData) => void, + // TODO Support metadata update + onMetadataUpdatedCallback?: (metadataUpdate: { [id: string]: IFeedBlockMetadata }) => void + ) { + this.socket = io(this.endpoint, { upgrade: true, transports: ["websocket"] }); + + // If reconnect fails then also try polling mode. + this.socket.on("reconnect_attempt", () => { + if (this.socket) { + this.socket.io.opts.transports = ["polling", "websocket"]; + } + }); + + try { + if (!this.blockSubscriptionId && this._networkConfig?.network && this.socket) { + const subscribeRequest: IFeedSubscribeRequest = { + network: this._networkConfig.network, + feedSelect: "block" + }; + + this.socket.on("subscribe", (subscribeResponse: IFeedSubscribeResponse) => { + if (subscribeResponse.error) { + console.log( + "Failed subscribing to feed", + this._networkConfig?.network, + subscribeResponse.error + ); + } else { + this.blockSubscriptionId = subscribeResponse.subscriptionId; + } + }); + + this.socket.on("block", async (update: IFeedUpdate) => { + if (update.subscriptionId === this.blockSubscriptionId) { + if (update.block) { + const block: IFeedBlockData = this.buildFeedBlockData(update.block); + console.log("[NovaFeedClient] New block", block); + onBlockDataCallback?.(block); + } + } + }); + + this.socket.emit("subscribe", subscribeRequest); + } + } catch (error) { + console.log("Failed subscribing to block feed", this._networkConfig?.network, error); + } + } + + /** + * Perform a request to unsubscribe to block feed events. + */ + public async unsubscribeBlocks(): Promise { + let success = false; + try { + if (this.blockSubscriptionId && this._networkConfig?.network && this.socket) { + const unsubscribeRequest: IFeedUnsubscribeRequest = { + network: this._networkConfig.network, + subscriptionId: this.blockSubscriptionId, + feedSelect: "block" + }; + + this.socket.on("unsubscribe", () => { }); + this.socket.emit("unsubscribe", unsubscribeRequest); + success = true; + } + } catch { + success = false; + console.error("[NovaFeedClient] Could not unsubscribe blocks"); + } finally { + this.socket?.disconnect(); + this.blockSubscriptionId = undefined; + this.socket = null; + } + + return success; + } + + /** + * Build the block data object. + * @param block The item source. + * @returns The feed item. + */ + private buildFeedBlockData(block: Block): IFeedBlockData { + const blockId = "unknown" + // TODO Figure out how to use Protocol parameters from SDK to build blockId + // + // const latestProtocolParameters = this._nodeInfo?.protocolParameters.at(-1)?.parameters ?? null + // if (latestProtocolParameters) { + // console.log(latestProtocolParameters) + // blockId = Utils.blockId(block, latestProtocolParameters); + // } + + return { + blockId, + block + }; + } +} diff --git a/client/src/services/nodeInfoService.ts b/client/src/services/stardust/nodeInfoService.ts similarity index 76% rename from client/src/services/nodeInfoService.ts rename to client/src/services/stardust/nodeInfoService.ts index 47d284237..b2b025239 100644 --- a/client/src/services/nodeInfoService.ts +++ b/client/src/services/stardust/nodeInfoService.ts @@ -1,14 +1,14 @@ import { INodeInfoBaseToken, IRent } from "@iota/sdk-wasm/web"; -import { StardustApiClient } from "./stardust/stardustApiClient"; +import { StardustApiClient } from "../stardust/stardustApiClient"; import { ServiceFactory } from "~factories/serviceFactory"; -import { INodeInfoResponse } from "~models/api/stardust/INodeInfoResponse"; +import { INodeInfoResponse as IStardustInfoResponse } from "~models/api/stardust/INodeInfoResponse"; import { STARDUST } from "~models/config/protocolVersion"; import { NetworkService } from "~services/networkService"; /** - * The reduced node info fields relevant for Explorer. + * The reduced stardust node info fields relevant for Explorer. */ -export interface IReducedNodeInfo { +export interface IStardustNodeInfo { /** * The base token info of the node. */ @@ -28,20 +28,20 @@ export interface IReducedNodeInfo { } /** - * Service to handle base token info. + * Service to handle base token info on stardust. */ export class NodeInfoService { /** * Cache of the base taken infos. */ - private _cache: { [network: string]: IReducedNodeInfo } = {}; + private _cache: { [network: string]: IStardustNodeInfo } = {}; /** * Get the base token info by network. * @param network The name of the network. * @returns The base token info. */ - public get(network: string): IReducedNodeInfo { + public get(network: string): IStardustNodeInfo { return this._cache[network]; } @@ -55,7 +55,7 @@ export class NodeInfoService { for (const networkDetails of stardustNetworks) { const apiClient = ServiceFactory.get(`api-client-${STARDUST}`); const network = networkDetails.network; - const response: INodeInfoResponse = await apiClient.nodeInfo({ network }); + const response: IStardustInfoResponse = await apiClient.nodeInfo({ network }); const { baseToken, protocolVersion, bech32Hrp, rentStructure } = response; if (baseToken && protocolVersion && bech32Hrp && rentStructure) { diff --git a/client/src/services/stardust/stardustFeedClient.ts b/client/src/services/stardust/stardustFeedClient.ts index 56a56f8d4..070628e36 100644 --- a/client/src/services/stardust/stardustFeedClient.ts +++ b/client/src/services/stardust/stardustFeedClient.ts @@ -66,7 +66,7 @@ export class StardustFeedClient { private cacheTrimTimer: NodeJS.Timer | null = null; /** - * Create a new instance of TransactionsClient. + * Create a new instance of StardustFeedClient. * @param endpoint The endpoint for the api. * @param networkId The network configurations. */ @@ -80,7 +80,7 @@ export class StardustFeedClient { if (theNetworkConfig) { this._networkConfig = theNetworkConfig; } else { - console.error("[FeedClient] Couldn't initialize client for network", networkId); + console.error("[StardustFeedClient] Couldn't initialize client for network", networkId); } this.setupCacheTrimJob(); @@ -122,6 +122,7 @@ export class StardustFeedClient { this.blockSubscriptionId = subscribeResponse.subscriptionId; } }); + this.socket.on("block", async (update: IFeedUpdate) => { if (update.subscriptionId === this.blockSubscriptionId) { if (update.blockMetadata) { @@ -231,7 +232,7 @@ export class StardustFeedClient { } } catch { success = false; - console.error("[FeedClient] Could not unsubscribe blocks"); + console.error("[StardustFeedClient] Could not unsubscribe blocks"); } finally { this.socket?.disconnect(); this.blockSubscriptionId = undefined; @@ -260,7 +261,7 @@ export class StardustFeedClient { } } catch { success = false; - console.error("[FeedClient] Could not unsubscribe milestones"); + console.error("[StardustFeedClient] Could not unsubscribe milestones"); } finally { this.socket?.disconnect(); this.milestoneSubscriptionId = undefined; diff --git a/setup_nova.sh b/setup_nova.sh index 981288845..d74927ba6 100755 --- a/setup_nova.sh +++ b/setup_nova.sh @@ -14,13 +14,17 @@ fi echo "Checking out nova-sdk commit $TARGET_COMMIT" git checkout "$TARGET_COMMIT" -echo "Building nodejs bindings" cd "./bindings/nodejs" +echo "Renaming nodejs sdk (sdk-nova)" +sed -i '' '2s/.*/ \"name\": \"@iota\/sdk-nova\",/' package.json +echo "Building nodejs bindings" yarn yarn build -echo "Building wasm bindings" cd "../wasm" +echo "Renaming wask sdk (sdk-wasm-nova)" +sed -i '' '2s/.*/ \"name\": \"@iota\/sdk-wasm-nova\",/' package.json +echo "Building wasm bindings" yarn yarn build