Skip to content

Commit

Permalink
Feat: Add infrastructure for nova mqtt feed (#920)
Browse files Browse the repository at this point in the history
* feat: Add support for NovaFeed in subscribe/unsubscribe endpoint

* feat: Add infrastructure for nova (nodeInfo, novaApiClient, novaFeedClient)

* chore(local-nova-sdk): Add step in script to rename the nodejs&wasm bindings packages before install

* feat: Fix instantiation of sdk Client from new version

* feat: novaFeedClient works [WiP]. Visualizer 2.0 switched to nova networks.

* feat: Move stardust NodeInfoService to stardust folder

* feat: Add separate nodeInfoService for nova

* fix: Fix imports and build

* fix: Fix NetworkSwitcher enum
  • Loading branch information
msarcev authored Dec 13, 2023
1 parent af95c35 commit c039b5b
Show file tree
Hide file tree
Showing 28 changed files with 495 additions and 65 deletions.
2 changes: 1 addition & 1 deletion api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 29 additions & 15 deletions api/src/initServices.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
);
});
});
}

/**
Expand Down
8 changes: 8 additions & 0 deletions api/src/models/api/nova/INodeInfoResponse.ts
Original file line number Diff line number Diff line change
@@ -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;

9 changes: 9 additions & 0 deletions api/src/models/api/nova/feed/IFeedUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Block } from "@iota/sdk-nova";

type IFeedBlockUpdate = Block;

export interface IFeedUpdate {
subscriptionId: string;
block?: IFeedBlockUpdate;
}

13 changes: 12 additions & 1 deletion api/src/routes/feed/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -67,6 +68,16 @@ export async function subscribe(
)
);
}
} else if (networkConfig.protocolVersion === NOVA) {
const service = ServiceFactory.get<NovaFeed>(`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."
Expand Down
5 changes: 4 additions & 1 deletion api/src/routes/feed/unsubscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<StardustFeed>(`feed-${request.network}`);
service?.unsubscribeBlocks(request.subscriptionId);
} else {
return {
error: "Network protocol not supported for feed."
Expand Down
13 changes: 9 additions & 4 deletions api/src/routes/node/info.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -16,18 +17,22 @@ import { ValidationHelper } from "../../utils/validationHelper";
export async function info(
_: IConfiguration,
request: INetworkBoundGetRequest
): Promise<INodeInfoResponse> {
): Promise<IStardustNodeInfoResponse | INovaNodeInfoResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();

ValidationHelper.oneOf(request.network, networks, "network");
const networkConfig = networkService.get(request.network);

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

const nodeService = ServiceFactory.get<NodeInfoService>(`node-info-${request.network}`);

return nodeService.getNodeInfo();
}

39 changes: 32 additions & 7 deletions api/src/services/nova/feed/novaFeed.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -12,32 +14,55 @@ export class NovaFeed {
* The block feed subscribers (downstream).
*/
protected readonly blockSubscribers: {
[id: string]: (data: Record<string, unknown>) => Promise<void>;
[id: string]: (data: IFeedUpdate) => Promise<void>;
};

/**
* Mqtt service for data (upstream).
*/
private readonly _mqttClient: Client;

/**
* The network in context.
*/
private readonly network: string;

/**
* Creates a new instance of NovaFeed.
* @param networkId The network id.
*/
constructor(networkId: string) {
logger.debug("[NovaFeed] Constructing a Nova Feed");
this.blockSubscribers = {};
this._mqttClient = ServiceFactory.get<Client>(`mqtt-${networkId}`);
this.network = networkId;
this._mqttClient = ServiceFactory.get<Client>(`client-${networkId}`);
const nodeInfoService = ServiceFactory.get<NodeInfoService>(`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<void>): Promise<void> {
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.
*/
Expand All @@ -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<Record<string, unknown>> = {
block
};
Expand Down
44 changes: 44 additions & 0 deletions api/src/services/nova/nodeInfoService.ts
Original file line number Diff line number Diff line change
@@ -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<NodeInfoService> {
const apiClient = ServiceFactory.get<Client>(`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;
}
}
2 changes: 1 addition & 1 deletion client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions client/script/postinstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions client/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouteComponentProps<AppRouteProps>> = (
Expand Down Expand Up @@ -48,7 +48,7 @@ const App: React.FC<RouteComponentProps<AppRouteProps>> = (
const identityResolverEnabled = networkConfig?.identityResolverEnabled ?? true;
const currentNetworkName = networkConfig?.network;
const isShimmer = isShimmerUiTheme(networkConfig?.uiTheme);
const nodeService = ServiceFactory.get<NodeInfoService>("node-info");
const nodeService = ServiceFactory.get<NodeInfoServiceStardust>("node-info-stardust");
const nodeInfo = networkConfig?.network ? nodeService.get(networkConfig?.network) : null;
const withNetworkContext = networkContextWrapper(currentNetworkName, nodeInfo, networkConfig?.uiTheme);
scrollToTop();
Expand Down
4 changes: 2 additions & 2 deletions client/src/app/AppUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? (
Expand Down
5 changes: 3 additions & 2 deletions client/src/app/components/NetworkSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
};

/**
Expand Down
Loading

0 comments on commit c039b5b

Please sign in to comment.