diff --git a/__tests__/TestHelper.js b/__tests__/TestHelper.js index 5861dd00..6adcce7b 100644 --- a/__tests__/TestHelper.js +++ b/__tests__/TestHelper.js @@ -365,52 +365,26 @@ 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' + }, + 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 ef90327c..087cad49 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 8e051ec3..a1d465dd 100644 --- a/__tests__/infrastructure/NodeService.spec.js +++ b/__tests__/infrastructure/NodeService.spec.js @@ -1,80 +1,83 @@ -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 = [ { roles: 1, - peerStatus: generateNodePeerStatus(true), + restVersion: null, + isHealthy: null, + isSslEnabled: null, ...nodeCommonField }, { - roles: 2, - apiStatus: generateNodeApiStatus(false), + roles: 1, // Peer node (light) + restVersion: '2.4.4', + isHealthy: null, + isSslEnabled: true, ...nodeCommonField }, { roles: 3, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(false), + restVersion: '2.4.4', + isHealthy: true, + isSslEnabled: true, ...nodeCommonField }, { - roles: 3, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true), - ...nodeCommonField - }, - { - roles: 3, - peerStatus: generateNodePeerStatus(false), - apiStatus: generateNodeApiStatus(false), - ...nodeCommonField - }, - { - roles: 5, - peerStatus: generateNodePeerStatus(true), + roles: 5, // Peer Voting node (light) + restVersion: '2.4.4', + isHealthy: null, + isSslEnabled: true, ...nodeCommonField }, { roles: 5, - peerStatus: generateNodePeerStatus(false), + restVersion: null, + isHealthy: null, + isSslEnabled: null, ...nodeCommonField }, { roles: 7, - peerStatus: generateNodePeerStatus(true), - apiStatus: generateNodeApiStatus(true), + restVersion: '2.4.4', + isHealthy: true, + isSslEnabled: true, ...nodeCommonField } ]; 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 runNodeWatchFailResponseTests = (nodeWatchMethod, NodeServiceMethod) => { + it('throws error when node watch fail response', async () => { // Arrange: - const error = new Error(`Statistics service ${statisticServiceMethod} error`); + const error = new Error(`node watch ${nodeWatchMethod} error`); - http.statisticServiceRestClient = jest.fn().mockImplementation(() => { - return { - [statisticServiceMethod]: jest.fn().mockRejectedValue(error) - }; - }); + jest.spyOn(NodeWatchService, nodeWatchMethod).mockRejectedValue(error); // Act + Assert: await expect(NodeService[NodeServiceMethod]()).rejects.toThrow(error); @@ -82,13 +85,9 @@ describe('Node Service', () => { }; 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 +95,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'); + runNodeWatchFailResponseTests('getNodes', 'getAvailableNodes'); }); 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 +179,117 @@ 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: { - apiStatus: { - isAvailable: undefined - }, - rolesRaw: 1 - } - }); - }); - - it('returns api node status and chain info when api status is present', async () => { - await assertNodeStatus(statisticServiceNodeResponse[1], { - peerStatus: {}, - apiStatus: expectedAPIStatus, - chainInfo: expectedChainInfoStatus, - mapInfo: { + it('returns API node status, chain info and map info', async () => { + await assertNodeStatus( + { + ...nodeWatchServiceNodeResponse[2], + geoLocation: geoLocationCommonField + }, + { apiStatus: { - isAvailable: false + connectionStatus: true, + isHttpsEnabled: true, + restVersion: '2.4.4' }, - rolesRaw: 2 + 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 chain info, api and peer node status when both status is present', async () => { - await assertNodeStatus(statisticServiceNodeResponse[2], { - peerStatus: expectedPeerStatus, - 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: 3 - } + mapInfo: {} }); }); - const runLightRestNodeTests = roles => { - it(`returns roles ${roles} node status and light rest status`, async () => { - // Arrange: - const lightNodeResponse = { - roles, - peerStatus: generateNodePeerStatus(true), + const runLightRestNodeTests = lightNode => { + it(`returns roles ${lightNode.roles} node status and light rest status`, async () => { + await assertNodeStatus(lightNode, { apiStatus: { - ...generateNodeApiStatus(true), - nodeStatus: undefined + lightNodeStatus: true, + isHttpsEnabled: true, + restVersion: '2.4.4' }, - ...nodeCommonField - }; - - const expectedLightAPIStatus = { - ...expectedAPIStatus, - lightNodeStatus: true, - connectionStatus: true - }; - delete expectedLightAPIStatus.databaseStatus; - delete expectedLightAPIStatus.apiNodeStatus; - - await assertNodeStatus(lightNodeResponse, { - peerStatus: expectedPeerStatus, - apiStatus: expectedLightAPIStatus, chainInfo: expectedChainInfoStatus, - mapInfo: { - apiStatus: { - isAvailable: true - }, - rolesRaw: roles - } + mapInfo: {} }); }); }; - [1, 4, 5].forEach(roles => runLightRestNodeTests(roles)); + [ + nodeWatchServiceNodeResponse[1], + nodeWatchServiceNodeResponse[3] + ].forEach(lightNode => runLightRestNodeTests(lightNode)); + + runNodeWatchFailResponseTests('getNodeByMainPublicKey', 'getNodeInfo'); + }); - runStatisticServiceFailResponseTests('getNode', 'getNodeInfo'); + 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 + } + ]); + }); }); }); diff --git a/__tests__/infrastructure/NodeWatchService.spec.js b/__tests__/infrastructure/NodeWatchService.spec.js new file mode 100644 index 00000000..75c7b4b4 --- /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 036dc694..a3ab3969 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/src/components/NodesMap.vue b/src/components/NodesMap.vue index 33c3f89f..e97823f5 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 42a52b5f..8298ca49 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/config/default.json b/src/config/default.json index f8f51b57..34a3a9a0 100644 --- a/src/config/default.json +++ b/src/config/default.json @@ -2,7 +2,8 @@ "apiNodePort": 3001, "endpoints": { "marketData": "https://min-api.cryptocompare.com/", - "statisticsService": "https://symbol.services" + "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 5874d91a..11467299 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", diff --git a/src/config/i18n/es.json b/src/config/i18n/es.json index 004309cc..d857ef55 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", diff --git a/src/config/i18n/ja.json b/src/config/i18n/ja.json index f5c4898e..bc9a7166 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": "ネットワーク識別子", diff --git a/src/config/i18n/ko.json b/src/config/i18n/ko.json index 251b813b..d893cd6b 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": "네트워크 구분자", diff --git a/src/config/i18n/pt.json b/src/config/i18n/pt.json index ac834451..420e8cde 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", diff --git a/src/config/i18n/ru.json b/src/config/i18n/ru.json index 6589034c..f02bcd7f 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": "Идентификатор сети", diff --git a/src/config/i18n/ua.json b/src/config/i18n/ua.json index 84987133..f0b9ed19 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", diff --git a/src/config/i18n/zh.json b/src/config/i18n/zh.json index c7a6cbe6..dfae3780 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", diff --git a/src/config/key-redirects.json b/src/config/key-redirects.json index 93fce999..f4993d85 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 4e9f43b5..96cfa6b2 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 6b081076..e6890838 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 a12c81a5..5865b3b9 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('node watch getNodes error'); } }; @@ -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('node watch getNodeByMainPublicKey error'); } }; @@ -304,7 +287,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 +296,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 00000000..3198ce97 --- /dev/null +++ b/src/infrastructure/NodeWatchService.js @@ -0,0 +1,33 @@ +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 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/index.js b/src/infrastructure/index.js index 459e0a19..7f4ba43a 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 d5b1d611..09b5ae0d 100644 --- a/src/store/node.js +++ b/src/store/node.js @@ -57,10 +57,9 @@ 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,