diff --git a/__tests__/TestHelper.js b/__tests__/TestHelper.js index 5861dd001..3de8ded54 100644 --- a/__tests__/TestHelper.js +++ b/__tests__/TestHelper.js @@ -365,52 +365,29 @@ const TestHelper = { innerTransactions }; }, - generateNodePeerStatus: isAvailable => { - return { - isAvailable, - lastStatusCheck: 1676809816662 - }; - }, - generateNodeApiStatus: isAvailable => { - return { - isAvailable, - nodePublicKey: '4DA6FB57FA168EEBBCB68DA4DDC8DA7BCF41EC93FB22A33DF510DB0F2670F623', - chainHeight: 2027193, - finalization: { - height: 2031992, - epoch: 1413, - point: 7, - hash: '6B687D9B689611C90A1094A7430E78914F22A2570C80D3E42D520EB08091A973' - }, - nodeStatus: { - apiNode: 'up', - db: 'up' - }, - restVersion: '2.4.2', - restGatewayUrl: 'localhost.com', - isHttpsEnabled: true - }; - }, nodeCommonField: { - version: 16777989, - publicKey: '016DC1622EE42EF9E4D215FA1112E89040DD7AED83007283725CE9BA550272F5', - networkGenerationHashSeed: '57F7DA205008026C776CB6AED843393F04CD458E0AA2D9F1D5F31A402072B2D6', - port: 7900, - networkIdentifier: 104, - host: 'node.com', - friendlyName: 'node', - lastAvailable: '2023-02-19T12:36:04.524Z', - hostDetail: {}, - location: '', - ip: '127.0.0.1', - organization: '', - as: '', - continent: '', - country: '', - region: '', - city: '', - district: '', - zip: '' + version: '1.0.3.5', + mainPublicKey: '016DC1622EE42EF9E4D215FA1112E89040DD7AED83007283725CE9BA550272F5', + endpoint: 'http://node.com:3000', + finalizedEpoch: 50, + finalizedHash: 'finalized hash', + finalizedHeight: 100, + finalizedPoint: 1, + geoLocation: null, + height: 120, + name: 'node', + restVersion: null, + isHealthy: null, + isSslEnabled: null + }, + geoLocationCommonField: { + city: 'ABC City', + continent: 'ABC', + country: 'ABC', + isp: 'ABC Online', + lat: 10.000, + lon: 20.000, + region: 'SN' } }; diff --git a/__tests__/components/NodesMap.spec.js b/__tests__/components/NodesMap.spec.js index ef90327c6..087cad49c 100644 --- a/__tests__/components/NodesMap.spec.js +++ b/__tests__/components/NodesMap.spec.js @@ -10,7 +10,7 @@ jest.mock('../../src/styles/img/connector_blue_light.png', () => 'blue-light.png jest.mock('../../src/styles/img/connector_green.png', () => 'green.png'); jest.mock('../../src/styles/img/connector_green_light.png', () => 'green-light.png'); -const setupStoreMount = (role, apiStatus) => { +const setupStoreMount = (role, isApiNode) => { const nodeModule = { namespaced: true }; @@ -36,9 +36,7 @@ const setupStoreMount = (role, apiStatus) => { const propsData = { nodes: [{ rolesRaw: role, - apiStatus: { - isAvailable: apiStatus - }, + isApiNode, coordinates: { latitude: 1, longitude: 2 @@ -58,9 +56,9 @@ localVue.use(Vuex); describe('NodesMap', () => { describe('addMarkers', () => { - const assertMarkerIcon = (role, apiStatus, expectedIcon) => { + const assertMarkerIcon = (role, isApiNode, expectedIcon) => { // Arrange: - const wrapper = setupStoreMount(role, apiStatus); + const wrapper = setupStoreMount(role, isApiNode); // Act: wrapper.vm.addMarkers(); diff --git a/__tests__/infrastructure/NodeService.spec.js b/__tests__/infrastructure/NodeService.spec.js index 8e051ec3d..8ae44b0df 100644 --- a/__tests__/infrastructure/NodeService.spec.js +++ b/__tests__/infrastructure/NodeService.spec.js @@ -1,94 +1,92 @@ -import { NodeService } from '../../src/infrastructure'; -import http from '../../src/infrastructure/http'; +import { NodeService, NodeWatchService } from '../../src/infrastructure'; import TestHelper from '../TestHelper'; describe('Node Service', () => { // Arrange: const { - generateNodePeerStatus, - generateNodeApiStatus, - nodeCommonField + nodeCommonField, + geoLocationCommonField } = TestHelper; - const statisticServiceNodeResponse = [ + const nodeWatchServiceNodeResponse = [ { + ...nodeCommonField, roles: 1, - peerStatus: generateNodePeerStatus(true), - ...nodeCommonField + restVersion: null, + isHealthy: null, + isSslEnabled: null }, { - roles: 2, - apiStatus: generateNodeApiStatus(false), - ...nodeCommonField + ...nodeCommonField, + roles: 1, // Peer node (light) + restVersion: '2.4.4', + isHealthy: null, + isSslEnabled: true }, { + ...nodeCommonField, roles: 3, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(false), - ...nodeCommonField + restVersion: '2.4.4', + isHealthy: true, + isSslEnabled: true }, { - roles: 3, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true), - ...nodeCommonField - }, - { - roles: 3, - peerStatus: generateNodePeerStatus(false), - apiStatus: generateNodeApiStatus(false), - ...nodeCommonField + ...nodeCommonField, + roles: 5, // Peer Voting node (light) + restVersion: '2.4.4', + isHealthy: null, + isSslEnabled: true }, { + ...nodeCommonField, roles: 5, - peerStatus: generateNodePeerStatus(true), - ...nodeCommonField - }, - { - roles: 5, - peerStatus: generateNodePeerStatus(false), - ...nodeCommonField + restVersion: null, + isHealthy: null, + isSslEnabled: null }, { + ...nodeCommonField, roles: 7, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true), - ...nodeCommonField + restVersion: '2.4.4', + isHealthy: true, + isSslEnabled: true } ]; const nodeFormattedCommonField = { - network: 'MAINNET', - address: 'NDY2CXBR6SK3G7UWVXZT6YQTVJKHKFMPU74ZOYY', - nodePublicKey: + network: 'TESTNET', + networkIdentifier: 152, + address: 'TDY2CXBR6SK3G7UWVXZT6YQTVJKHKFMPU6UDZ6Q', + mainPublicKey: '016DC1622EE42EF9E4D215FA1112E89040DD7AED83007283725CE9BA550272F5', - version: '1.0.3.5' + version: '1.0.3.5', + friendlyName: 'node', + finalizedEpoch: 50, + finalizedHash: 'finalized hash', + finalizedHeight: 100, + finalizedPoint: 1, + geoLocation: null, + height: 120, + host: 'node.com', + port: '3000', + networkGenerationHashSeed: '57F7DA205008026C776CB6AED843393F04CD458E0AA2D9F1D5F31A402072B2D6' }; - const runStatisticServiceFailResponseTests = (statisticServiceMethod, NodeServiceMethod) => { - it('throws error when statistic services fail response', async () => { + const runNodeServiceThrowErrorTests = (nodeServiceMethod, params, expectedError) => { + it('throws error when node service fail response', async () => { // Arrange: - const error = new Error(`Statistics service ${statisticServiceMethod} error`); - - http.statisticServiceRestClient = jest.fn().mockImplementation(() => { - return { - [statisticServiceMethod]: jest.fn().mockRejectedValue(error) - }; - }); + jest.spyOn(NodeWatchService, 'getNodes').mockRejectedValue(new Error()); + jest.spyOn(NodeWatchService, 'getNodeByMainPublicKey').mockRejectedValue(new Error()); // Act + Assert: - await expect(NodeService[NodeServiceMethod]()).rejects.toThrow(error); + await expect(NodeService[nodeServiceMethod](params)).rejects.toThrow(expectedError); }); }; describe('getAvailableNodes', () => { - it('returns available node from statistic services', async () => { + it('returns available node from node watch services', async () => { // Arrange: - http.statisticServiceRestClient = jest.fn().mockImplementation(() => { - return { - getNodes: jest.fn().mockResolvedValue(statisticServiceNodeResponse) - }; - }); + jest.spyOn(NodeWatchService, 'getNodes').mockResolvedValue(nodeWatchServiceNodeResponse); // Act: const result = await NodeService.getAvailableNodes(); @@ -96,123 +94,80 @@ describe('Node Service', () => { // Assert: expect(result).toEqual([ { - ...nodeCommonField, ...nodeFormattedCommonField, + restVersion: null, + isHealthy: null, + isHttpsEnabled: null, apiEndpoint: 'N/A', roles: 'Peer node', - rolesRaw: 1, - peerStatus: generateNodePeerStatus(true) + rolesRaw: 1 }, { - ...nodeCommonField, ...nodeFormattedCommonField, - apiEndpoint: 'localhost.com', - roles: 'Peer Api node', - rolesRaw: 3, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(false) + restVersion: '2.4.4', + isHealthy: null, + isHttpsEnabled: true, + apiEndpoint: 'http://node.com:3000', + roles: 'Peer node (light)', + rolesRaw: 1 }, { - ...nodeCommonField, ...nodeFormattedCommonField, - apiEndpoint: 'localhost.com', + restVersion: '2.4.4', + isHealthy: true, + isHttpsEnabled: true, + apiEndpoint: 'http://node.com:3000', roles: 'Peer Api node', - rolesRaw: 3, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true) + rolesRaw: 3 + }, + { + ...nodeFormattedCommonField, + restVersion: '2.4.4', + isHealthy: null, + isHttpsEnabled: true, + apiEndpoint: 'http://node.com:3000', + roles: 'Peer Voting node (light)', + rolesRaw: 5 }, { - ...nodeCommonField, ...nodeFormattedCommonField, + restVersion: null, + isHealthy: null, + isHttpsEnabled: null, apiEndpoint: 'N/A', roles: 'Peer Voting node', - rolesRaw: 5, - peerStatus: generateNodePeerStatus(true) + rolesRaw: 5 }, { - ...nodeCommonField, ...nodeFormattedCommonField, - apiEndpoint: 'localhost.com', + restVersion: '2.4.4', + isHealthy: true, + isHttpsEnabled: true, + apiEndpoint: 'http://node.com:3000', roles: 'Peer Api Voting node', - rolesRaw: 7, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true) + rolesRaw: 7 } ]); }); - it('returns available light node from statistic services', async () => { - // Arrange: - const createExpectedNode = (rolesRaw, roleName) => ({ - ...nodeCommonField, - ...nodeFormattedCommonField, - apiEndpoint: 'N/A', - roles: roleName, - rolesRaw, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true) - }); - - const statisticServiceLightNodeResponse = [ - { - roles: 1, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true), - ...nodeCommonField - }, - { - roles: 4, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true), - ...nodeCommonField - }, - { - roles: 5, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true), - ...nodeCommonField - } - ]; - - http.statisticServiceRestClient = jest.fn().mockImplementation(() => { - return { - getNodes: jest.fn().mockResolvedValue(statisticServiceLightNodeResponse) - }; - }); - - // Act: - const result = await NodeService.getAvailableNodes(); - - // Assert: - expect(result).toEqual([ - createExpectedNode(1, 'Peer node (light)'), - createExpectedNode(4, 'Voting node (light)'), - createExpectedNode(5, 'Peer Voting node (light)') - ]); - }); - - runStatisticServiceFailResponseTests('getNodes', 'getAvailableNodes'); + runNodeServiceThrowErrorTests('getAvailableNodes', undefined, 'Failed to get available nodes'); }); describe('getNodeStats', () => { it('return nodes count with 7 types of roles', async () => { // Arrange: - http.statisticServiceRestClient = jest.fn().mockImplementation(() => { - return { - getNodes: jest.fn().mockResolvedValue(statisticServiceNodeResponse) - }; - }); + jest.spyOn(NodeWatchService, 'getNodes').mockResolvedValue(nodeWatchServiceNodeResponse); // Act: const nodeStats = await NodeService.getNodeStats(); // Assert: expect(nodeStats).toEqual({ - 1: 1, + 1: 2, 2: 0, - 3: 2, + 3: 1, 4: 0, - 5: 1, + 5: 2, 6: 0, 7: 1 }); @@ -223,127 +178,189 @@ describe('Node Service', () => { // Arrange: Date.now = jest.fn(() => new Date('2023-02-21')); - const expectedPeerStatus = { - connectionStatus: true, - lastStatusCheck: '2023-02-19 12:30:16' - }; - - const expectedAPIStatus = { - apiNodeStatus: true, - connectionStatus: false, - databaseStatus: true, - isHttpsEnabled: true, - lastStatusCheck: '2023-02-21 00:00:00', - restVersion: '2.4.2' - }; - const expectedChainInfoStatus = { - height: 2027193, - finalizedHeight: 2031992, - finalizationEpoch: 1413, - finalizationPoint: 7, - finalizedHash: '6B687D9B689611C90A1094A7430E78914F22A2570C80D3E42D520EB08091A973', - lastStatusCheck: '2023-02-21 00:00:00' + height: 120, + finalizedHeight: 100, + finalizationEpoch: 50, + finalizationPoint: 1, + finalizedHash: 'finalized hash' }; const assertNodeStatus = async (node, expectedResult) => { // Arrange: - http.statisticServiceRestClient = jest.fn().mockImplementation(() => { - return { - getNode: jest.fn().mockResolvedValue(node) - }; - }); + jest.spyOn(NodeWatchService, 'getNodeByMainPublicKey').mockResolvedValue(node); // Act: - const { apiStatus, chainInfo, peerStatus, mapInfo } = - await NodeService.getNodeInfo(node.publicKey); + const { apiStatus, chainInfo, mapInfo } = + await NodeService.getNodeInfo(node.mainPublicKey); // Assert: expect(apiStatus).toEqual(expectedResult.apiStatus); expect(chainInfo).toEqual(expectedResult.chainInfo); - expect(peerStatus).toEqual(expectedResult.peerStatus); expect(mapInfo).toEqual(expectedResult.mapInfo); }; - it('returns peer node status when peer status is present', async () => { - await assertNodeStatus(statisticServiceNodeResponse[0], { - peerStatus: expectedPeerStatus, - apiStatus: {}, - chainInfo: {}, - mapInfo: { + it('returns API node status, chain info and map info', async () => { + await assertNodeStatus( + { + ...nodeWatchServiceNodeResponse[2], + geoLocation: geoLocationCommonField + }, + { apiStatus: { - isAvailable: undefined + connectionStatus: true, + isHttpsEnabled: true, + restVersion: '2.4.4' }, - rolesRaw: 1 + chainInfo: expectedChainInfoStatus, + mapInfo: { + city: 'ABC City', + continent: 'ABC', + country: 'ABC', + isp: 'ABC Online', + region: 'SN', + coordinates: { + latitude: 10.000, + longitude: 20.000 + }, + rolesRaw: 3, + isApiNode: true + } } - }); + ); }); - it('returns api node status and chain info when api status is present', async () => { - await assertNodeStatus(statisticServiceNodeResponse[1], { - peerStatus: {}, - apiStatus: expectedAPIStatus, + it('returns Peer node status info, chain info without map info', async () => { + await assertNodeStatus(nodeWatchServiceNodeResponse[0], { + apiStatus: {}, chainInfo: expectedChainInfoStatus, - mapInfo: { - apiStatus: { - isAvailable: false - }, - rolesRaw: 2 - } + mapInfo: {} }); }); - it('returns chain info, api and peer node status when both status is present', async () => { - await assertNodeStatus(statisticServiceNodeResponse[2], { - peerStatus: expectedPeerStatus, - apiStatus: expectedAPIStatus, - chainInfo: expectedChainInfoStatus, - mapInfo: { + const runLightRestNodeTests = lightNode => { + it(`returns roles ${lightNode.roles} node status and light rest status`, async () => { + await assertNodeStatus(lightNode, { apiStatus: { - isAvailable: false + lightNodeStatus: true, + isHttpsEnabled: true, + restVersion: '2.4.4' }, + chainInfo: expectedChainInfoStatus, + mapInfo: {} + }); + }); + }; + + [ + nodeWatchServiceNodeResponse[1], + nodeWatchServiceNodeResponse[3] + ].forEach(lightNode => runLightRestNodeTests(lightNode)); + + runNodeServiceThrowErrorTests('getNodeInfo', 'public key 123', 'Failed to get node info for public key public key 123'); + }); + + describe('getAPINodeList', () => { + it('returns a list of API nodes', async () => { + // Arrange: + jest.spyOn(NodeWatchService, 'getNodes').mockResolvedValue(nodeWatchServiceNodeResponse); + + // Act: + const apiNodeList = await NodeService.getAPINodeList(); + + // Assert: + expect(apiNodeList).toEqual([ + { + ...nodeFormattedCommonField, + restVersion: '2.4.4', + isHealthy: true, + isHttpsEnabled: true, + apiEndpoint: 'http://node.com:3000', + roles: 'Peer Api node', rolesRaw: 3 + }, + { + ...nodeFormattedCommonField, + restVersion: '2.4.4', + isHealthy: true, + isHttpsEnabled: true, + apiEndpoint: 'http://node.com:3000', + roles: 'Peer Api Voting node', + rolesRaw: 7 } - }); + ]); }); + }); - const runLightRestNodeTests = roles => { - it(`returns roles ${roles} node status and light rest status`, async () => { - // Arrange: - const lightNodeResponse = { - roles, - peerStatus: generateNodePeerStatus(true), - apiStatus: { - ...generateNodeApiStatus(true), - nodeStatus: undefined - }, - ...nodeCommonField - }; + describe('getNodeHeightAndFinalizedHeightStats', () => { + it('returns node height and finalized height count and group by version', async () => { + // Arrange: + const mockApiResponse = [ + { + ...nodeCommonField, + version: '1.0.3.6', + height: 120, + finalizedHeight: 100 + }, + { + ...nodeCommonField, + version: '1.0.3.6', + height: 120, + finalizedHeight: 100 + }, + { + ...nodeCommonField, + version: '1.0.3.5', + height: 120, + finalizedHeight: 99 + }, + { + ...nodeCommonField, + version: '1.0.3.5', + height: 120, + finalizedHeight: 100 + }, + { + ...nodeCommonField, + version: '1.0.3.4', + height: 120, + finalizedHeight: 100 + } + ]; - const expectedLightAPIStatus = { - ...expectedAPIStatus, - lightNodeStatus: true, - connectionStatus: true - }; - delete expectedLightAPIStatus.databaseStatus; - delete expectedLightAPIStatus.apiNodeStatus; + jest.spyOn(NodeWatchService, 'getNodes').mockResolvedValue(mockApiResponse); - await assertNodeStatus(lightNodeResponse, { - peerStatus: expectedPeerStatus, - apiStatus: expectedLightAPIStatus, - chainInfo: expectedChainInfoStatus, - mapInfo: { - apiStatus: { - isAvailable: true - }, - rolesRaw: roles - } - }); - }); - }; + // Act: + const result = await NodeService.getNodeHeightAndFinalizedHeightStats(); - [1, 4, 5].forEach(roles => runLightRestNodeTests(roles)); + // Assert: + expect(result).toEqual([ + { + 'data': [{'x': 120, 'y': 2, 'z': 2}], + 'name': '1.0.3.6 - Height' + }, + { + 'data': [{'x': 100, 'y': 2, 'z': 2}], + 'name': '1.0.3.6 - Finalized Height'}, + { + 'data': [{'x': 120, 'y': 2, 'z': 2}], + 'name': '1.0.3.5 - Height' + }, + { + 'data': [{'x': 99, 'y': 1, 'z': 1}, {'x': 100, 'y': 1, 'z': 1}], + 'name': '1.0.3.5 - Finalized Height' + }, + { + 'data': [{'x': 120, 'y': 1, 'z': 1}], + 'name': '1.0.3.4 - Height' + }, + { + 'data': [{'x': 100, 'y': 1, 'z': 1}], + 'name': '1.0.3.4 - Finalized Height' + } + ]); + }); - runStatisticServiceFailResponseTests('getNode', 'getNodeInfo'); + runNodeServiceThrowErrorTests('getNodeHeightAndFinalizedHeightStats', undefined, 'Failed to get node height stats'); }); }); diff --git a/__tests__/infrastructure/NodeWatchService.spec.js b/__tests__/infrastructure/NodeWatchService.spec.js new file mode 100644 index 000000000..75c7b4b4b --- /dev/null +++ b/__tests__/infrastructure/NodeWatchService.spec.js @@ -0,0 +1,86 @@ +import globalConfig from '../../src/config/globalConfig'; +import { NodeWatchService } from '../../src/infrastructure'; +import Axios from 'axios'; + +jest.mock('axios'); + +describe('Node Watch Service', () => { + const runNodeWatchThrowErrorTests = (nodeWatchMethod, params, expectedError) => { + it('throws error when node watch fail response', async () => { + // Arrange: + Axios.get.mockRejectedValue(new Error()); + + // Act + Assert: + await expect(NodeWatchService[nodeWatchMethod](params)).rejects.toThrow(expectedError); + }); + }; + + describe('getNodes', () => { + const mockApiResponse = [ + { id: 1, name: 'Node1' }, + { id: 2, name: 'Node2' } + ]; + + const mockPeerResponse = [ + { id: 3, name: 'PeerNode1' }, + { id: 4, name: 'PeerNode2' } + ]; + + beforeEach(() => { + Axios.get.mockClear(); + + Axios.get.mockImplementation(url => { + if (url.includes('api/symbol/nodes/api')) + return Promise.resolve({ data: mockApiResponse }); + else if (url.includes('api/symbol/nodes/peer')) + return Promise.resolve({ data: mockPeerResponse }); + + }); + }); + it('fetches nodes with default params', async () => { + // Act + const result = await NodeWatchService.getNodes(); + + // Assert + expect(result).toEqual([...mockApiResponse, ...mockPeerResponse]); + expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/api?only_ssl=false&limit=0`); + expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/peer?only_ssl=false&limit=0`); + }); + + it('fetches nodes with SSL filtering, limit 2 and order random', async () => { + // Act + const result = await NodeWatchService.getNodes(true, 2, 'random'); + + // Assert + const endpointUrl = `${globalConfig.endpoints.nodeWatch}/api/symbol/nodes`; + expect(result).toEqual([...mockApiResponse, ...mockPeerResponse]); + expect(Axios.get).toHaveBeenCalledWith(`${endpointUrl}/api?only_ssl=true&limit=2&order=random`); + expect(Axios.get).toHaveBeenCalledWith(`${endpointUrl}/peer?only_ssl=true&limit=2&order=random`); + }); + + runNodeWatchThrowErrorTests('getNodes', undefined, 'Error fetching from /api/symbol/nodes/api?only_ssl=false&limit=0'); + }); + + describe('getNodeByMainPublicKey', () => { + it('fetches node by main public key', async () => { + // Arrange + const mockResponse = { id: 1, name: 'Node1' }; + const mainPublicKey = 'publicKey123'; + + Axios.get.mockResolvedValue({ data: mockResponse }); + + // Act + const result = await NodeWatchService.getNodeByMainPublicKey(mainPublicKey); + + // Assert + expect(result).toEqual(mockResponse); + expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/mainPublicKey/${mainPublicKey}`); + }); + + runNodeWatchThrowErrorTests( + 'getNodeByMainPublicKey', + '1234567890abcdef', + 'Error fetching from /api/symbol/nodes/mainPublicKey/1234567890abcdef' + ); + }); +}); diff --git a/__tests__/store/api.spec.js b/__tests__/store/api.spec.js index 036dc6947..a3ab3969e 100644 --- a/__tests__/store/api.spec.js +++ b/__tests__/store/api.spec.js @@ -26,7 +26,7 @@ const stubMockNode = numberOfNodes => { port: 7900, networkIdentifier: 152, host: `mock_${nodeIndex}.com`, - nodePublicKey: account.publicKey, + mainPublicKey: account.publicKey, address: account.address.plain(), rolesRaw: 3, network: 'TESTNET', diff --git a/package-lock.json b/package-lock.json index 5494798a3..90ed4f714 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,6 @@ "nem-sdk": "^1.6.8", "net": "^1.0.2", "symbol-sdk": "^2.0.3", - "symbol-statistics-service-typescript-fetch-client": "^1.1.5", "tls": "^0.0.1", "url-parse": "^1.5.10", "vue": "^2.6.14", @@ -23939,11 +23938,6 @@ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "license": "Unlicense" }, - "node_modules/symbol-statistics-service-typescript-fetch-client": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/symbol-statistics-service-typescript-fetch-client/-/symbol-statistics-service-typescript-fetch-client-1.1.5.tgz", - "integrity": "sha512-hcVD9jq+S2kZCYaXTDglU43y3Dn5K6W3O45Y/pMAfiuYDCdVaHczAPDE/ZKGo0kZXy69e1sn/riAzfDfn8yFBw==" - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index bfc436d3f..eb06c82e7 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "nem-sdk": "^1.6.8", "net": "^1.0.2", "symbol-sdk": "^2.0.3", - "symbol-statistics-service-typescript-fetch-client": "^1.1.5", "tls": "^0.0.1", "url-parse": "^1.5.10", "vue": "^2.6.14", diff --git a/src/components/Chart.vue b/src/components/Chart.vue index 74d413f97..4764ceaff 100644 --- a/src/components/Chart.vue +++ b/src/components/Chart.vue @@ -88,6 +88,16 @@ export default { intYaxis: { type: Boolean, default: false + }, + // Axis range configuration for bubble charts + xaxisRange: { + type: Array, + default: () => null + }, + + yaxisRange: { + type: Array, + default: () => null } }, @@ -114,7 +124,13 @@ export default { reset: '', customIcons: [] } - } + }, + ...(this.type === 'bubble' && { + zoom: { + enabled: false, + type: 'x' + }, + }) }, stroke: { show: true, @@ -149,13 +165,18 @@ export default { dataLabels: { position: 'top' } + }, + bubble: { + zScaling: true, + minBubbleRadius: 10, + maxBubbleRadius: 50 } }, title: { display: false }, xaxis: { - type: this.xaxisType, + type: this.type === 'bubble' ? 'numeric' : this.xaxisType, axisBorder: { show: false, color: '#0998a6' @@ -168,7 +189,11 @@ export default { : val; } } - : {} + : {}, + ...(this.xaxisRange && { + min: this.xaxisRange[0], + max: this.xaxisRange[1] + }) }, yaxis: { tooltip: { @@ -186,10 +211,18 @@ export default { : val; } } - : {} + : {}, + ...(this.yaxisRange && { + min: this.yaxisRange[0], + max: this.yaxisRange[1] + }) }, tooltip: { - enabled: true + enabled: true, + z: { + formatter: () => '', + title: '' + }, }, legend: { show: true, diff --git a/src/components/NodesMap.vue b/src/components/NodesMap.vue index 33c3f89fb..e97823f53 100644 --- a/src/components/NodesMap.vue +++ b/src/components/NodesMap.vue @@ -166,7 +166,7 @@ export default { '
' + this.getNameByKey('address') + ': ' + this.formatText(node.address) + '
' + this.getNameByKey('location') + ': ' + this.formatText(node.location) + '
' + - '' + this.getNameByKey('nodeDetailTitle') + + '' + this.getNameByKey('nodeDetailTitle') + ' ' + this.getNameByKey('accountDetailTitle') + '' + ''; @@ -175,7 +175,7 @@ export default { switch (node.rolesRaw) { case 1: - icon = node.apiStatus?.isAvailable ? iconPeerLight : iconPeer; + icon = node.isApiNode ? iconPeerLight : iconPeer; break; case 2: case 3: @@ -183,7 +183,7 @@ export default { break; case 4: case 5: - icon = node.apiStatus?.isAvailable ? iconVotingLight : iconVoting; + icon = node.isApiNode ? iconVotingLight : iconVoting; break; case 6: case 7: diff --git a/src/components/tables/TableView.vue b/src/components/tables/TableView.vue index 42a52b5f4..8298ca492 100644 --- a/src/components/tables/TableView.vue +++ b/src/components/tables/TableView.vue @@ -90,7 +90,7 @@ export default { 'namespaceArtifactId', 'mosaicArtifactId', - 'nodePublicKey' + 'mainPublicKey', ], disableClickValues: [...Object.values(Constants.Message)], changeDecimalColor: [ @@ -201,7 +201,6 @@ export default { 'recipient' === key || 'publicKey' === key || 'signerPublicKey' === key || - 'nodePublicKey' === key || 'mainPublicKey' === key || 'transactionHash' === key || 'ownerAddress' === key || diff --git a/src/components/widgets/NodeHeightAndFinalizedHeightStatsWidget.vue b/src/components/widgets/NodeHeightAndFinalizedHeightStatsWidget.vue new file mode 100644 index 000000000..afa1be34b --- /dev/null +++ b/src/components/widgets/NodeHeightAndFinalizedHeightStatsWidget.vue @@ -0,0 +1,118 @@ + + + diff --git a/src/components/widgets/NodeHeightStatsWidget.vue b/src/components/widgets/NodeHeightStatsWidget.vue deleted file mode 100644 index f44b0d587..000000000 --- a/src/components/widgets/NodeHeightStatsWidget.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/src/config/default.json b/src/config/default.json index f8f51b574..5188f549c 100644 --- a/src/config/default.json +++ b/src/config/default.json @@ -2,7 +2,7 @@ "apiNodePort": 3001, "endpoints": { "marketData": "https://min-api.cryptocompare.com/", - "statisticsService": "https://symbol.services" + "nodeWatch": "https://nodewatch.symbol.tools" }, "networkConfig": { "namespaceName": "symbol.xym", diff --git a/src/config/i18n/en-us.json b/src/config/i18n/en-us.json index 5874d91af..1e7bba8c6 100644 --- a/src/config/i18n/en-us.json +++ b/src/config/i18n/en-us.json @@ -330,7 +330,7 @@ "nodeDetailTitle": "Node Detail", "nodeHostDetailTitle": "Host Detail", "nodeLocationMapTitle": "Host Location", - "nodePublicKey": "Public Key", + "mainPublicKey": "Public Key", "apiEndpoint": "API Endpoint", "networkGenerationHashSeed": "Network Generation Hash Seed", "networkIdentifier": "Network Identifier", @@ -383,8 +383,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/config/i18n/es.json b/src/config/i18n/es.json index 004309cc0..61d181c2d 100644 --- a/src/config/i18n/es.json +++ b/src/config/i18n/es.json @@ -326,7 +326,7 @@ "nodeDetailTitle": "Node Detail", "nodeHostDetailTitle": "Host Detail", "nodeLocationMapTitle": "Host Location", - "nodePublicKey": "Public Key", + "mainPublicKey": "Public Key", "apiEndpoint": "API Endpoint", "networkGenerationHashSeed": "Network Generation Hash Seed", "networkIdentifier": "Network Identifier", @@ -379,8 +379,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/config/i18n/ja.json b/src/config/i18n/ja.json index f5c4898e3..112332c20 100644 --- a/src/config/i18n/ja.json +++ b/src/config/i18n/ja.json @@ -330,7 +330,7 @@ "nodeDetailTitle": "ノード詳細", "nodeHostDetailTitle": "ホスト詳細", "nodeLocationMapTitle": "ホスト位置", - "nodePublicKey": "公開鍵", + "mainPublicKey": "公開鍵", "apiEndpoint": "APIエンドポイント", "networkGenerationHashSeed": "ネットワークジェネレーションハッシュシード", "networkIdentifier": "ネットワーク識別子", @@ -383,8 +383,7 @@ "allNodes": "合計数", "nodeCountByRoles": "ロールごとの数", "chainInfo": "チェーン情報", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "詳細を表示", "nodeCountChartTitle": "Node count over time", "timestamp": "タイムスタンプ", diff --git a/src/config/i18n/ko.json b/src/config/i18n/ko.json index 251b813b6..1b54f5d77 100644 --- a/src/config/i18n/ko.json +++ b/src/config/i18n/ko.json @@ -330,7 +330,7 @@ "nodeDetailTitle": "노드 정보보기", "nodeHostDetailTitle": "호스트 정보보기", "nodeLocationMapTitle": "호스트 위치", - "nodePublicKey": "공개키", + "mainPublicKey": "공개키", "apiEndpoint": "API 엔드포인트", "networkGenerationHashSeed": "네트워크가 생성한 시드 해시", "networkIdentifier": "네트워크 구분자", @@ -383,8 +383,7 @@ "allNodes": "총 개수", "nodeCountByRoles": "역할 개수", "chainInfo": "체인 정보", - "nodeHeightStatsTitle": "체인 높이의 총 노드 수", - "nodeFinalizedHeightStatsTitle": "최종화 높이의 총 노드 수", + "nodeHeightAndFinalizedHeightStatsTitle": "노드 높이와 최종화 높이의 수", "viewMoreStatistics": "상태 더보기", "nodeCountChartTitle": "노드 카운트 시간 초과", "timestamp": "타임스탬프", diff --git a/src/config/i18n/pt.json b/src/config/i18n/pt.json index ac834451a..323dd4379 100644 --- a/src/config/i18n/pt.json +++ b/src/config/i18n/pt.json @@ -327,7 +327,7 @@ "nodeDetailTitle": "Node Detail", "nodeHostDetailTitle": "Host Detail", "nodeLocationMapTitle": "Host Location", - "nodePublicKey": "Public Key", + "mainPublicKey": "Public Key", "apiEndpoint": "API Endpoint", "networkGenerationHashSeed": "Network Generation Hash Seed", "networkIdentifier": "Network Identifier", @@ -380,8 +380,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/config/i18n/ru.json b/src/config/i18n/ru.json index 6589034ce..b0b6a7428 100644 --- a/src/config/i18n/ru.json +++ b/src/config/i18n/ru.json @@ -330,7 +330,7 @@ "nodeDetailTitle": "Детали ноды", "nodeHostDetailTitle": "Детали хоста", "nodeLocationMapTitle": "Местонахождение Хоста", - "nodePublicKey": "Публичный ключ", + "mainPublicKey": "Публичный ключ", "apiEndpoint": "Конечная точка API интерфейса", "networkGenerationHashSeed": "Генерация сети Хеш-сид", "networkIdentifier": "Идентификатор сети", @@ -383,8 +383,7 @@ "allNodes": "Общее количество", "nodeCountByRoles": "Подсчет по ролям", "chainInfo": "Информация о сети", - "nodeHeightStatsTitle": "Количество нод по высоте цепи", - "nodeFinalizedHeightStatsTitle": "Количество нод по завершенной высоте", + "nodeHeightAndFinalizedHeightStatsTitle": "Количество нод по высоте и завершенной высоте", "viewMoreStatistics": "Просмотр Дополнительной Статистики", "nodeCountChartTitle": "Количество нод с течением времени", "timestamp": "Отметка времени", diff --git a/src/config/i18n/ua.json b/src/config/i18n/ua.json index 84987133a..87cfa0864 100644 --- a/src/config/i18n/ua.json +++ b/src/config/i18n/ua.json @@ -326,7 +326,7 @@ "nodeDetailTitle": "Node Detail", "nodeHostDetailTitle": "Host Detail", "nodeLocationMapTitle": "Host Location", - "nodePublicKey": "Public Key", + "mainPublicKey": "Public Key", "apiEndpoint": "API Endpoint", "networkGenerationHashSeed": "Network Generation Hash Seed", "networkIdentifier": "Network Identifier", @@ -379,8 +379,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/config/i18n/zh.json b/src/config/i18n/zh.json index c7a6cbe63..de3375db4 100644 --- a/src/config/i18n/zh.json +++ b/src/config/i18n/zh.json @@ -326,7 +326,7 @@ "nodeDetailTitle": "Node Detail", "nodeHostDetailTitle": "Host Detail", "nodeLocationMapTitle": "Host Location", - "nodePublicKey": "Public Key", + "mainPublicKey": "Public Key", "apiEndpoint": "API Endpoint", "networkGenerationHashSeed": "Network Generation Hash Seed", "networkIdentifier": "Network Identifier", @@ -379,8 +379,7 @@ "allNodes": "Total count", "nodeCountByRoles": "Count By Roles", "chainInfo": "Chain Info", - "nodeHeightStatsTitle": "Count of nodes by chain height", - "nodeFinalizedHeightStatsTitle": "Count of nodes by finalized height", + "nodeHeightAndFinalizedHeightStatsTitle": "Count of nodes Heights and finalized heights", "viewMoreStatistics": "View More Stats", "nodeCountChartTitle": "Node count over time", "timestamp": "Timestamp", diff --git a/src/config/key-redirects.json b/src/config/key-redirects.json index 93fce9993..f4993d853 100644 --- a/src/config/key-redirects.json +++ b/src/config/key-redirects.json @@ -63,5 +63,5 @@ "namespaceArtifactId": "namespaces", "node": "nodes", - "nodePublicKey": "nodes" -} \ No newline at end of file + "mainPublicKey": "nodes" +} diff --git a/src/config/pages/node-detail.json b/src/config/pages/node-detail.json index 4e9f43b5f..96cfa6b20 100644 --- a/src/config/pages/node-detail.json +++ b/src/config/pages/node-detail.json @@ -27,7 +27,7 @@ "networkGenerationHashSeed", "network", "networkIdentifier", - "publicKey", + "mainPublicKey", "address" ] }, @@ -49,20 +49,17 @@ "type": "CardTable", "title": "nodeHostDetailTitle", "managerGetter": "node/info", - "dataGetter": "node/hostDetail", + "dataGetter": "node/geoLocation", "hideDependOnGetter": "node/hostInfoManager", "errorMessage": "nodeDetailError", "pagination": "none", "fields": [ - "ip", - "organization", - "as", "continent", "country", "region", "city", - "district", - "zip" + "isp" + ] }, { @@ -76,26 +73,9 @@ "hideEmptyData": true, "fields": [ "connectionStatus", - "databaseStatus", - "apiNodeStatus", "lightNodeStatus", "isHttpsEnabled", - "restVersion", - "lastStatusCheck" - ] - }, - { - "layoutOptions": "adaptive", - "type": "CardTable", - "title": "nodePeerStatusTitle", - "managerGetter": "node/info", - "dataGetter": "node/peerStatus", - "errorMessage": "nodeDetailError", - "pagination": "none", - "hideEmptyData": true, - "fields": [ - "connectionStatus", - "lastStatusCheck" + "restVersion" ] }, { diff --git a/src/config/pages/node-list.json b/src/config/pages/node-list.json index 6b081076a..e6890838e 100644 --- a/src/config/pages/node-list.json +++ b/src/config/pages/node-list.json @@ -41,14 +41,14 @@ "host", "friendlyName", "roles", - "nodePublicKey", + "mainPublicKey", "chainInfo", "softwareVersion" ], "mobileFields": [ "host", "friendlyName", - "nodePublicKey" + "mainPublicKey" ] } ] diff --git a/src/infrastructure/NodeService.js b/src/infrastructure/NodeService.js index a12c81a53..739a2ec84 100644 --- a/src/infrastructure/NodeService.js +++ b/src/infrastructure/NodeService.js @@ -19,7 +19,7 @@ import http from './http'; import Constants from '../config/constants'; import helper from '../helper'; -import moment from 'moment'; +import { NodeWatchService } from '../infrastructure'; import * as symbol from 'symbol-sdk'; class NodeService { @@ -67,50 +67,55 @@ class NodeService { * @param {object} nodeInfo NodeInfoDTO. * @returns {object} readable NodeInfo. */ - static formatNodeInfo = nodeInfo => ({ - ...nodeInfo, - nodePublicKey: nodeInfo.publicKey, - address: symbol.Address.createFromPublicKey( - nodeInfo.publicKey, - nodeInfo.networkIdentifier - ).plain(), - rolesRaw: nodeInfo.roles, - roles: [1,4,5].includes(nodeInfo.roles) && nodeInfo.apiStatus?.isAvailable - ? Constants.RoleType[nodeInfo.roles] + ' (light)' - : Constants.RoleType[nodeInfo.roles], - network: Constants.NetworkType[nodeInfo.networkIdentifier], - version: helper.formatNodeVersion(nodeInfo.version), - apiEndpoint: - 2 === nodeInfo.roles || - 3 === nodeInfo.roles || - 6 === nodeInfo.roles || - 7 === nodeInfo.roles - ? nodeInfo.apiStatus.restGatewayUrl - : Constants.Message.UNAVAILABLE - }); + static formatNodeInfo = nodeInfo => { + const { hostname, port } = '' !== nodeInfo.endpoint + ? new URL(nodeInfo.endpoint) + : { hostname: 'N/A', port: 'N/A' }; + + return { + finalizedEpoch: nodeInfo.finalizedEpoch, + finalizedHash: nodeInfo.finalizedHash, + finalizedHeight: nodeInfo.finalizedHeight, + finalizedPoint: nodeInfo.finalizedPoint, + height: nodeInfo.height, + mainPublicKey: nodeInfo.mainPublicKey, + isHealthy: nodeInfo.isHealthy, + restVersion: nodeInfo.restVersion, + isHttpsEnabled: nodeInfo.isSslEnabled, + friendlyName: nodeInfo.name, + geoLocation: nodeInfo.geoLocation, + host: hostname, + version: nodeInfo.version, + port, + address: symbol.Address.createFromPublicKey( + nodeInfo.mainPublicKey, + http.networkType + ).plain(), + rolesRaw: nodeInfo.roles, + roles: [1,4,5].includes(nodeInfo.roles) && null != nodeInfo.restVersion + ? Constants.RoleType[nodeInfo.roles] + ' (light)' + : Constants.RoleType[nodeInfo.roles], + network: Constants.NetworkType[http.networkType], + networkIdentifier: http.networkType, + apiEndpoint: null != nodeInfo.restVersion ? nodeInfo.endpoint : Constants.Message.UNAVAILABLE, + networkGenerationHashSeed: http.generationHash + }; + }; /** - * Get available node list from statistic service. + * Get available node list from node watch service. * @returns {array} NodeInfo[] */ static getAvailableNodes = async () => { try { - const nodePeers = await http.statisticServiceRestClient().getNodes(); + const nodePeers = await NodeWatchService.getNodes(); return nodePeers - .filter(({ apiStatus, roles, peerStatus }) => { - if (1 === roles || 4 === roles || 5 === roles) - return peerStatus?.isAvailable; - else if (3 === roles || 6 === roles || 7 === roles) - return apiStatus?.isAvailable || peerStatus?.isAvailable; - else - return apiStatus?.isAvailable; - }) .map(nodeInfo => this.formatNodeInfo(nodeInfo)) .sort((a, b) => a.friendlyName.localeCompare(b.friendlyName)); } catch (e) { console.error(e); - throw Error('Statistics service getNodes error'); + throw Error('Failed to get available nodes'); } }; @@ -132,33 +137,28 @@ class NodeService { node['softwareVersion'] = { version: el.version }; - if (el.apiStatus) { - const { - chainHeight, - finalization, - lastStatusCheck, - restVersion, - isHttpsEnabled - } = el.apiStatus; - - node['chainInfo'] = { - chainHeight, - finalizationHeight: finalization?.height, - lastStatusCheck - }; + node['chainInfo'] = { + chainHeight: el.height, + finalizationHeight: el.finalizedHeight + }; - node['softwareVersion'] = { - ...node.softwareVersion, - restVersion, - isHttpsEnabled - }; - } else { - node['chainInfo'] = {}; - } + node['softwareVersion'] = { + ...node.softwareVersion, + restVersion: el.restVersion, + isHttpsEnabled: el.isSslEnabled + }; - if (node?.hostDetail) { - node = { ...node, ...node.hostDetail }; - delete node.hostDetail; + if (node?.geoLocation) { + node = { + ...node, + coordinates: { + latitude: node.geoLocation.lat, + longitude: node.geoLocation.lon + }, + location: node.geoLocation.city + ', ' + node.geoLocation.region + ', ' + node.geoLocation.country, + isApiNode: null != node.restVersion + }; + delete node.geoLocation; } return node; @@ -168,87 +168,70 @@ class NodeService { static getNodeInfo = async publicKey => { try { - const node = await http.statisticServiceRestClient().getNode(publicKey); + const node = await NodeWatchService.getNodeByMainPublicKey(publicKey); + const formattedNode = this.formatNodeInfo(node); - if (formattedNode?.apiStatus) { - const { - finalization, - chainHeight, - lastStatusCheck, - nodeStatus, - isAvailable, - isHttpsEnabled, - restVersion - } = formattedNode.apiStatus; + const { + finalizedEpoch, + finalizedHash, + finalizedHeight, + finalizedPoint, + height, + isHealthy, + isHttpsEnabled, + restVersion + } = formattedNode; + + // Chain info + formattedNode.chainInfo = { + height: height, + finalizedHeight: finalizedHeight, + finalizationEpoch: finalizedEpoch, + finalizationPoint: finalizedPoint, + finalizedHash: finalizedHash + }; + + formattedNode.apiStatus = {}; - // Api status + // Only API nodes have database status + if ([2, 3, 6, 7].includes(formattedNode.rolesRaw)) { formattedNode.apiStatus = { - connectionStatus: isAvailable, + connectionStatus: isHealthy || Constants.Message.UNAVAILABLE, isHttpsEnabled, - restVersion, - lastStatusCheck: moment - .utc(lastStatusCheck) - .format('YYYY-MM-DD HH:mm:ss') + restVersion }; - - // Only API nodes have database status - if ([2, 3, 6, 7].includes(node.roles)) { - formattedNode.apiStatus = { - ...formattedNode.apiStatus, - apiNodeStatus: - 'up' === nodeStatus?.apiNode || Constants.Message.UNAVAILABLE, - databaseStatus: 'up' === nodeStatus?.db || Constants.Message.UNAVAILABLE - }; - } else { + } else { + if (null != restVersion) { formattedNode.apiStatus = { - ...formattedNode.apiStatus, - lightNodeStatus: isAvailable || Constants.Message.UNAVAILABLE + lightNodeStatus: true, + isHttpsEnabled, + restVersion }; - }; - - // Chain info - formattedNode.chainInfo = { - height: chainHeight, - finalizedHeight: finalization?.height, - finalizationEpoch: finalization?.epoch, - finalizationPoint: finalization?.point, - finalizedHash: finalization?.hash, - lastStatusCheck: moment - .utc(lastStatusCheck) - .format('YYYY-MM-DD HH:mm:ss') - }; - } else { - formattedNode.apiStatus = {}; - formattedNode.chainInfo = {}; - } - - if (formattedNode?.peerStatus) { - const { isAvailable, lastStatusCheck } = formattedNode.peerStatus; + } + }; - formattedNode.peerStatus = { - connectionStatus: isAvailable, - lastStatusCheck: moment - .utc(lastStatusCheck) - .format('YYYY-MM-DD HH:mm:ss') + // Map info used for create a marker in the map + if (formattedNode?.geoLocation) { + const { lat, lon, ...geoInfo } = formattedNode.geoLocation; + + formattedNode.mapInfo = { + ...geoInfo, + coordinates: { + latitude: lat, + longitude: lon + }, + rolesRaw: formattedNode.rolesRaw, + isApiNode: null != formattedNode.restVersion }; } else { - formattedNode.peerStatus = {}; + formattedNode.mapInfo = {}; } - // Map info used for create a marker in the map - formattedNode.mapInfo = { - ...formattedNode.hostDetail, - rolesRaw: formattedNode.rolesRaw, - apiStatus: { - isAvailable: node.apiStatus?.isAvailable - } - }; - return formattedNode; } catch (e) { console.error(e); - throw Error('Statistics service getNode error'); + throw Error('Failed to get node info for public key ' + publicKey); } }; @@ -268,28 +251,84 @@ class NodeService { return nodeTypes; }; - static getNodeHeightStats = async () => { + static getNodeHeightAndFinalizedHeightStats = async () => { try { - const data = await http.statisticServiceRestClient().getNodeHeightStats(); - - return [ - { - name: 'Height', - data: data.height.map(el => ({ - x: '' + parseInt(el.value), - y: parseInt(el.count) + const data = await NodeWatchService.getNodes(); + + const nodesByVersion = {}; + + data.forEach(node => { + const {version} = node; + + // Initialize the array for this version if it doesn't exist yet + if (!nodesByVersion[version]) { + nodesByVersion[version] = { + heights: [ + { + height: node.height, + count: 1 + } + ], + finalizedHeights: [ + { + finalizedHeight: node.finalizedHeight, + count: 1 + } + ] + }; + } else { + // Check if height already exists + const existingHeight = nodesByVersion[version].heights.find(h => h.height === node.height); + if (existingHeight) { + existingHeight.count++; + } else { + nodesByVersion[version].heights.push({ + height: node.height, + count: 1 + }); + } + + // Check if finalized height already exists + const existingFinalizedHeight = nodesByVersion[version] + .finalizedHeights.find(h => h.finalizedHeight === node.finalizedHeight); + if (existingFinalizedHeight) { + existingFinalizedHeight.count++; + } else { + nodesByVersion[version].finalizedHeights.push({ + finalizedHeight: node.finalizedHeight, + count: 1 + }); + } + } + }); + + const result = []; + + Object.keys(nodesByVersion).forEach(version => { + // Add height series for this version + result.push({ + name: `${version} - Height`, + data: nodesByVersion[version].heights.map(item => ({ + x: item.height, + y: item.count, + z: item.count })) - }, - { - name: 'Finalized Height', - data: data.finalizedHeight.map(el => ({ - x: '' + parseInt(el.value), - y: parseInt(el.count) + }); + + // Add finalized height series for this version + result.push({ + name: `${version} - Finalized Height`, + data: nodesByVersion[version].finalizedHeights.map(item => ({ + x: item.finalizedHeight, + y: item.count, + z: item.count })) - } - ]; + }); + }); + + return result; } catch (e) { - throw Error('Statistics service getNodeHeightStats error: ', e); + throw Error('Failed to get node height stats'); } }; @@ -304,7 +343,7 @@ class NodeService { roles: node.roles, network: node.network, networkGenerationHashSeed: node.networkGenerationHashSeed, - nodePublicKey: node.nodePublicKey, + mainPublicKey: node.mainPublicKey, chainHeight: node.chainInfo.chainHeight, finalizationHeight: node.chainInfo.finalizationHeight, version: node.version @@ -313,32 +352,16 @@ class NodeService { return helper.convertArrayToCSV(formattedData); }; - /** - * Gets node list from statistics service. - * @param {string} filter (optional) 'preferred | suggested'. - * @param {number} limit (optional) number of records. - * @param {boolean} ssl (optional) return ssl ready node. - * @returns {array} nodes - */ - static getNodeList = async (filter, limit, ssl) => { - try { - return await http - .statisticServiceRestClient() - .getNodes(filter, limit, ssl); - } catch (e) { - throw Error('Statistics service getNodeHeightStats error: ', e); - } - }; - /** * Get API node list dataset into Vue Component. * @returns {array} API Node list object for Vue component. */ static getAPINodeList = async () => { - // get 30 ssl ready nodes from statistics service the list - const nodes = await this.getNodeList('suggested', 30, true); + // get 30 ssl ready nodes from node watch service + const nodes = await NodeWatchService.getNodes(true, 30, 'random'); return nodes + .filter(node => node.isHealthy && node.isSslEnabled) .map(nodeInfo => this.formatNodeInfo(nodeInfo)) .sort((a, b) => a.friendlyName.localeCompare(b.friendlyName)); }; diff --git a/src/infrastructure/NodeWatchService.js b/src/infrastructure/NodeWatchService.js new file mode 100644 index 000000000..609866cf0 --- /dev/null +++ b/src/infrastructure/NodeWatchService.js @@ -0,0 +1,38 @@ +import globalConfig from '../config/globalConfig'; +import Axios from 'axios'; + +class NodeWatchService { + static async getNodes(onlySSL = false, limit = 0, order = null) { + const params = `only_ssl=${onlySSL}&limit=${limit}${order ? `&order=${order}` : ''}`; + + const [apiNodesResponse, peerNodesResponse] = await Promise.all([ + this.get(`/api/symbol/nodes/api?${params}`), + this.get(`/api/symbol/nodes/peer?${params}`) + ]); + + return [...apiNodesResponse.data, ...peerNodesResponse.data]; + } + + static async getNodeByMainPublicKey(mainPublicKey) { + const response = await this.get(`/api/symbol/nodes/mainPublicKey/${mainPublicKey}`); + + return response.data; + } + + static async getNodeCount() { + const response = await this.get('/api/symbol/nodes/count'); + return response.data; + } + + static async get(route) { + try { + const response = await Axios.get(`${globalConfig.endpoints.nodeWatch}${route}`); + return response; + } catch (error) { + console.error('Error fetching nodes:', error); + throw Error(`Error fetching from ${route}`); + } + } +} + +export default NodeWatchService; diff --git a/src/infrastructure/StatisticService.js b/src/infrastructure/StatisticService.js index b07079877..5669723b4 100644 --- a/src/infrastructure/StatisticService.js +++ b/src/infrastructure/StatisticService.js @@ -16,8 +16,7 @@ * */ import Constants from '../config/constants'; -import globalConfig from '../config/globalConfig'; -import Axios from 'axios'; +import { NodeWatchService } from '../infrastructure'; class StatisticService { /** @@ -145,20 +144,12 @@ class StatisticService { } static getNodeCountSeries = async () => { - const data = await StatisticService.fetchFromStatisticsService('/timeSeries/nodeCount'); + const data = await NodeWatchService.getNodeCount(); const chartData = StatisticService.formatChartData(data, ['1', '2', '3', '4', '5', '6', '7', 'total']); return chartData.map(el => ({ ...el, name: Constants.RoleType[el.name] || el.name })); } - static fetchFromStatisticsService = async route => { - if (this.isUrlProvided()) - return (await Axios.get(globalConfig.endpoints.statisticsService + route)).data; - - else - throw Error('Statistics service endpoint is not provided'); - } - static formatChartData = (data, includeKeys) => { const aggreagatedData = {}; const isKeyIncluded = key => !includeKeys || includeKeys.includes(key); @@ -189,15 +180,6 @@ class StatisticService { return chartData; } - - static isUrlProvided () { - try { - new URL(globalConfig?.endpoints?.statisticsService); // eslint-disable-line no-new - return true; - } catch (e) { - return false; - } - } } export default StatisticService; diff --git a/src/infrastructure/http.js b/src/infrastructure/http.js index 3e2ce43bb..c99de2ce6 100644 --- a/src/infrastructure/http.js +++ b/src/infrastructure/http.js @@ -20,10 +20,6 @@ import accountLabels from '../config/accountLabels'; import globalConfig from '../config/globalConfig'; import { NamespaceService } from '../infrastructure'; import * as symbol from 'symbol-sdk'; -import { - Configuration, - NodeApi -} from 'symbol-statistics-service-typescript-fetch-client'; let NODE_URL; @@ -190,21 +186,4 @@ export default class http { static get transactionPaginationStreamer() { return new symbol.TransactionPaginationStreamer(this.createRepositoryFactory.createTransactionRepository()); } - - static statisticServiceRestClient() { - try { - const statisticsServiceUrl = globalConfig.endpoints.statisticsService; - - if (statisticsServiceUrl && statisticsServiceUrl.length) { - return new NodeApi(new Configuration({ - fetchApi: fetch, - basePath: statisticsServiceUrl - })); - } else { - throw Error('Statistics service endpoint is not provided'); - } - } catch (error) { - console.error(error); - } - } } diff --git a/src/infrastructure/index.js b/src/infrastructure/index.js index 459e0a195..7f4ba43a6 100644 --- a/src/infrastructure/index.js +++ b/src/infrastructure/index.js @@ -12,6 +12,7 @@ import MultisigService from './MultisigService'; import NamespaceService from './NamespaceService'; import NetworkService from './NetworkService'; import NodeService from './NodeService'; +import NodeWatchService from './NodeWatchService'; import ReceiptExtractor from './ReceiptExtractor'; import ReceiptService from './ReceiptService'; import RestrictionService from './RestrictionService'; @@ -20,6 +21,7 @@ import TransactionService from './TransactionService'; export { NodeService, + NodeWatchService, AccountService, MetadataService, RestrictionService, diff --git a/src/store/node.js b/src/store/node.js index d5b1d6118..de81f0d04 100644 --- a/src/store/node.js +++ b/src/store/node.js @@ -57,15 +57,13 @@ export default { getInitialized: state => state.initialized, ...getGettersFromManagers(managers), mapInfo: state => [ state.info?.data?.mapInfo ], - peerStatus: state => state.info?.data?.peerStatus, apiStatus: state => state.info?.data?.apiStatus, chainInfo: state => state.info?.data?.chainInfo, - hostDetail: state => state.info?.data?.hostDetail, + geoLocation: state => state.info?.data?.geoLocation, hostInfoManager: (state, getters) => ({ loading: getters.timeline?.loading || getters.info?.loading, - error: !StatisticService.isUrlProvided() || - getters.timeline?.error || + error: getters.timeline?.error || getters.info?.error }) }, diff --git a/src/store/statistic.js b/src/store/statistic.js index fd2298278..70a4df567 100644 --- a/src/store/statistic.js +++ b/src/store/statistic.js @@ -29,8 +29,8 @@ import { Order } from 'symbol-sdk'; const managers = [ new DataSet( - 'nodeHeightStats', - () => NodeService.getNodeHeightStats() + 'nodeHeightAndFinalizedHeightStats', + () => NodeService.getNodeHeightAndFinalizedHeightStats() ) ]; @@ -123,7 +123,7 @@ export default { async uninitialize ({ commit, dispatch, getters }) { const callback = async () => {}; - getters.nodeHeightStats?.uninitialize(); + getters.nodeHeightAndFinalizedHeightStats?.uninitialize(); await LOCK.uninitialize(callback, commit, dispatch, getters); }, @@ -134,7 +134,7 @@ export default { context.commit('setLoadingBlockTimeDifference', true); context.commit('setLoadingTransactionPerBlock', true); context.commit('setLoadingNodeCountSeries', true); - context.getters.nodeHeightStats.setStore(context).initialFetch(); + context.getters.nodeHeightAndFinalizedHeightStats.setStore(context).initialFetch(); context.commit('setError', false); try { diff --git a/src/views/Statistics.vue b/src/views/Statistics.vue index fa78acdfd..4b9d53fa4 100644 --- a/src/views/Statistics.vue +++ b/src/views/Statistics.vue @@ -33,12 +33,10 @@ - - - - - + + + @@ -50,7 +48,7 @@ import NetworkFeesWidget from '../components/widgets/NetworkFeesWidget.vue'; import NetworkRentalFeesWidget from '../components/widgets/NetworkRentalFeesWidget.vue'; import ChartBlockTimeDifference from '../components/widgets/ChartBlockTimeDifference.vue'; import ChartTransactionPerBlock from '../components/widgets/ChartTransactionPerBlock.vue'; -import NodeHeightStatsWidget from '../components/widgets/NodeHeightStatsWidget.vue'; +import NodeHeightAndFinalizedHeightStatsWidget from '../components/widgets/NodeHeightAndFinalizedHeightStatsWidget.vue'; import ChartNodeCount from '../components/widgets/ChartNodeCount.vue'; export default { @@ -60,7 +58,7 @@ export default { NetworkRentalFeesWidget, ChartBlockTimeDifference, ChartTransactionPerBlock, - NodeHeightStatsWidget, + NodeHeightAndFinalizedHeightStatsWidget, ChartNodeCount }, mounted () {