From 628787d107783bceeedc8b8b325a317af3e68d64 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 04:13:28 +0800 Subject: [PATCH 01/17] [explorer] task: rename nodePublicKey to mainPublicKey --- __tests__/store/api.spec.js | 2 +- src/components/NodesMap.vue | 2 +- src/components/tables/TableView.vue | 3 +-- src/config/i18n/en-us.json | 2 +- src/config/i18n/es.json | 2 +- src/config/i18n/ja.json | 2 +- src/config/i18n/ko.json | 2 +- src/config/i18n/pt.json | 2 +- src/config/i18n/ru.json | 2 +- src/config/i18n/ua.json | 2 +- src/config/i18n/zh.json | 2 +- src/config/key-redirects.json | 2 +- src/config/pages/node-detail.json | 2 +- src/config/pages/node-list.json | 4 ++-- src/infrastructure/NodeService.js | 2 +- 15 files changed, 16 insertions(+), 17 deletions(-) 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/src/components/NodesMap.vue b/src/components/NodesMap.vue index 33c3f89fb..7b3d4cbaf 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') + '' + ''; 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/config/i18n/en-us.json b/src/config/i18n/en-us.json index 5874d91af..11467299a 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 004309cc0..d857ef556 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 f5c4898e3..bc9a71662 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 251b813b6..d893cd6b2 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 ac834451a..420e8cde8 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 6589034ce..f02bcd7f2 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 84987133a..f0b9ed198 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 c7a6cbe63..dfae37806 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 93fce9993..6df495d3d 100644 --- a/src/config/key-redirects.json +++ b/src/config/key-redirects.json @@ -63,5 +63,5 @@ "namespaceArtifactId": "namespaces", "node": "nodes", - "nodePublicKey": "nodes" + "mainPublicKey": "nodes" } \ No newline at end of file diff --git a/src/config/pages/node-detail.json b/src/config/pages/node-detail.json index 4e9f43b5f..9cf1f164e 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" ] }, 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..c0a626822 100644 --- a/src/infrastructure/NodeService.js +++ b/src/infrastructure/NodeService.js @@ -304,7 +304,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 From 819aeb19b33f810cdb1856edb85996d08419d994 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 04:35:18 +0800 Subject: [PATCH 02/17] [explorer] task: added node watch services --- .../infrastructure/NodeWatchService.spec.js | 84 +++++++++++++++++++ src/infrastructure/NodeWatchService.js | 30 +++++++ src/infrastructure/index.js | 2 + 3 files changed, 116 insertions(+) create mode 100644 __tests__/infrastructure/NodeWatchService.spec.js create mode 100644 src/infrastructure/NodeWatchService.js diff --git a/__tests__/infrastructure/NodeWatchService.spec.js b/__tests__/infrastructure/NodeWatchService.spec.js new file mode 100644 index 000000000..76f2361ea --- /dev/null +++ b/__tests__/infrastructure/NodeWatchService.spec.js @@ -0,0 +1,84 @@ +import globalConfig from '../../src/config/globalConfig'; +import { NodeWatchService } from '../../src/infrastructure'; +import Axios from 'axios'; + +jest.mock('axios'); + +describe('Node Watch Service', () => { + 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 and limit', async () => { + // Act + const result = await NodeWatchService.getNodes(true, 2); + + // Assert + expect(result).toEqual([...mockApiResponse, ...mockPeerResponse]); + expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/api?only_ssl=true&limit=2`); + expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/peer?only_ssl=true&limit=2`); + }); + + it('handles errors when fetching nodes', async () => { + // Arrange + Axios.get.mockRejectedValue(new Error('Network error')); + + // Act + Assert + await expect(NodeWatchService.getNodes()).rejects.toThrow('Network error'); + }); + }); + + 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}`); + }); + + it('handles errors when fetching node by main public key', async () => { + // Arrange + const mainPublicKey = '1234567890abcdef'; + Axios.get.mockRejectedValue(new Error('Network error')); + + // Act + Assert + await expect(NodeWatchService.getNodeByMainPublicKey(mainPublicKey)).rejects.toThrow('Network error'); + }); + }); +}); \ No newline at end of file diff --git a/src/infrastructure/NodeWatchService.js b/src/infrastructure/NodeWatchService.js new file mode 100644 index 000000000..e51e6d9c4 --- /dev/null +++ b/src/infrastructure/NodeWatchService.js @@ -0,0 +1,30 @@ +import globalConfig from '../config/globalConfig'; +import Axios from 'axios'; + +class NodeWatchService { + static async getNodes(onlySSL = false, limit = 0) { + try { + const [apiNodesResponse, peerNodesResponse] = await Promise.all([ + Axios.get(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/api?only_ssl=` + onlySSL + '&limit=' + limit), + Axios.get(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/peer?only_ssl=` + onlySSL + '&limit=' + limit) + ]); + + return [...apiNodesResponse.data, ...peerNodesResponse.data]; + } catch (error) { + console.error('Error fetching nodes:', error); + throw error; + } + } + + static async getNodeByMainPublicKey(mainPublicKey) { + try { + const response = await Axios.get(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/mainPublicKey/${mainPublicKey}`); + return response.data; + } catch (error) { + console.error('Error fetching node by main public key:', error); + throw error; + } + } +} + +export default NodeWatchService; 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, From 2633df0fe325c5db99898bb246df16d17976775a Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 04:41:13 +0800 Subject: [PATCH 03/17] [explorer] task: updated node maps --- __tests__/components/NodesMap.spec.js | 10 ++++------ src/components/NodesMap.vue | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/__tests__/components/NodesMap.spec.js b/__tests__/components/NodesMap.spec.js index ef90327c6..f648055cf 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/src/components/NodesMap.vue b/src/components/NodesMap.vue index 7b3d4cbaf..c3d99c165 100644 --- a/src/components/NodesMap.vue +++ b/src/components/NodesMap.vue @@ -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: From 6b4c2d3233144b612da822a28bb67b0895378f7e Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 04:41:49 +0800 Subject: [PATCH 04/17] [explorer] task: updated unit test --- __tests__/TestHelper.js | 64 ++++++++++++----------------------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/__tests__/TestHelper.js b/__tests__/TestHelper.js index 5861dd001..6adcce7b8 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' } }; From dd5ff079c276abbba6eadc79949529a5ed91ca24 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 04:42:51 +0800 Subject: [PATCH 05/17] [explorer] task: removed peer status card table --- src/config/pages/node-detail.json | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/config/pages/node-detail.json b/src/config/pages/node-detail.json index 9cf1f164e..94dc65d0b 100644 --- a/src/config/pages/node-detail.json +++ b/src/config/pages/node-detail.json @@ -84,20 +84,6 @@ "lastStatusCheck" ] }, - { - "layoutOptions": "adaptive", - "type": "CardTable", - "title": "nodePeerStatusTitle", - "managerGetter": "node/info", - "dataGetter": "node/peerStatus", - "errorMessage": "nodeDetailError", - "pagination": "none", - "hideEmptyData": true, - "fields": [ - "connectionStatus", - "lastStatusCheck" - ] - }, { "layoutOptions": "full-width", "type": "CardTable", From f5f79f06b43887c3ed3fb201b576b153c6e1ae3c Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 04:59:40 +0800 Subject: [PATCH 06/17] [explorer] task: refactor unit test helper and update getAPINodeList --- __tests__/infrastructure/NodeService.spec.js | 104 ++++++++++++------- src/infrastructure/NodeService.js | 22 +--- 2 files changed, 71 insertions(+), 55 deletions(-) diff --git a/__tests__/infrastructure/NodeService.spec.js b/__tests__/infrastructure/NodeService.spec.js index 8e051ec3d..ca88e1e1c 100644 --- a/__tests__/infrastructure/NodeService.spec.js +++ b/__tests__/infrastructure/NodeService.spec.js @@ -1,72 +1,77 @@ -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 () => { // Arrange: const error = new Error(`Statistics service ${statisticServiceMethod} error`); @@ -342,8 +347,35 @@ describe('Node Service', () => { }); }; - [1, 4, 5].forEach(roles => runLightRestNodeTests(roles)); + describe('getAPINodeList', () => { + it('returns a list of API nodes', async () => { + // Arrange: + jest.spyOn(NodeWatchService, 'getNodes').mockResolvedValue(nodeWatchServiceNodeResponse); + + // Act: + const apiNodeList = await NodeService.getAPINodeList(); - runStatisticServiceFailResponseTests('getNode', 'getNodeInfo'); + // 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/src/infrastructure/NodeService.js b/src/infrastructure/NodeService.js index c0a626822..6d6795dbe 100644 --- a/src/infrastructure/NodeService.js +++ b/src/infrastructure/NodeService.js @@ -313,32 +313,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); return nodes + .filter(node => node.isHealthy && node.isSslEnabled) .map(nodeInfo => this.formatNodeInfo(nodeInfo)) .sort((a, b) => a.friendlyName.localeCompare(b.friendlyName)); }; From d5228b1ff7c0d08e43efe231bfa07ecc0f82358d Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 05:00:49 +0800 Subject: [PATCH 07/17] [explorer] task: more unit test helper update --- __tests__/infrastructure/NodeService.spec.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/__tests__/infrastructure/NodeService.spec.js b/__tests__/infrastructure/NodeService.spec.js index ca88e1e1c..dcd61a7ff 100644 --- a/__tests__/infrastructure/NodeService.spec.js +++ b/__tests__/infrastructure/NodeService.spec.js @@ -72,14 +72,12 @@ describe('Node Service', () => { networkGenerationHashSeed: '57F7DA205008026C776CB6AED843393F04CD458E0AA2D9F1D5F31A402072B2D6' }; + 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); From dc217ae0b950828a20b25b7e3e60c58d994c82d4 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 05:09:03 +0800 Subject: [PATCH 08/17] [explorer] task: rename hostDetail to geoLocation --- src/config/pages/node-detail.json | 2 +- src/store/node.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/pages/node-detail.json b/src/config/pages/node-detail.json index 94dc65d0b..b939ead37 100644 --- a/src/config/pages/node-detail.json +++ b/src/config/pages/node-detail.json @@ -49,7 +49,7 @@ "type": "CardTable", "title": "nodeHostDetailTitle", "managerGetter": "node/info", - "dataGetter": "node/hostDetail", + "dataGetter": "node/geoLocation", "hideDependOnGetter": "node/hostInfoManager", "errorMessage": "nodeDetailError", "pagination": "none", diff --git a/src/store/node.js b/src/store/node.js index d5b1d6118..306e06ffe 100644 --- a/src/store/node.js +++ b/src/store/node.js @@ -60,7 +60,7 @@ export default { 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, From 9fcac339072528cf3811087937fb97443e0f4f55 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 05:09:38 +0800 Subject: [PATCH 09/17] [explorer] task: removed unused field --- src/config/pages/node-detail.json | 12 +++--------- src/store/node.js | 1 - 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/config/pages/node-detail.json b/src/config/pages/node-detail.json index b939ead37..96cfa6b20 100644 --- a/src/config/pages/node-detail.json +++ b/src/config/pages/node-detail.json @@ -54,15 +54,12 @@ "errorMessage": "nodeDetailError", "pagination": "none", "fields": [ - "ip", - "organization", - "as", "continent", "country", "region", "city", - "district", - "zip" + "isp" + ] }, { @@ -76,12 +73,9 @@ "hideEmptyData": true, "fields": [ "connectionStatus", - "databaseStatus", - "apiNodeStatus", "lightNodeStatus", "isHttpsEnabled", - "restVersion", - "lastStatusCheck" + "restVersion" ] }, { diff --git a/src/store/node.js b/src/store/node.js index 306e06ffe..09b5ae0d2 100644 --- a/src/store/node.js +++ b/src/store/node.js @@ -57,7 +57,6 @@ 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, geoLocation: state => state.info?.data?.geoLocation, From c9529f31c849341c4bd6c032b8789f7a3f01c78c Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 05:11:59 +0800 Subject: [PATCH 10/17] [explorer] task: migrate statistic service to node watch --- __tests__/infrastructure/NodeService.spec.js | 270 +++++++------------ src/infrastructure/NodeService.js | 227 ++++++++-------- 2 files changed, 198 insertions(+), 299 deletions(-) diff --git a/__tests__/infrastructure/NodeService.spec.js b/__tests__/infrastructure/NodeService.spec.js index dcd61a7ff..4b1dd2bc6 100644 --- a/__tests__/infrastructure/NodeService.spec.js +++ b/__tests__/infrastructure/NodeService.spec.js @@ -85,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(); @@ -99,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 }); @@ -226,125 +179,88 @@ 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: {} }); }); }; + [ + nodeWatchServiceNodeResponse[1], + nodeWatchServiceNodeResponse[3] + ].forEach(lightNode => runLightRestNodeTests(lightNode)); + + runNodeWatchFailResponseTests('getNodeByMainPublicKey', 'getNodeInfo'); + }); + describe('getAPINodeList', () => { it('returns a list of API nodes', async () => { // Arrange: @@ -374,6 +290,6 @@ describe('Node Service', () => { rolesRaw: 7 } ]); - }); + }); }) }); diff --git a/src/infrastructure/NodeService.js b/src/infrastructure/NodeService.js index 6d6795dbe..8810ef24c 100644 --- a/src/infrastructure/NodeService.js +++ b/src/infrastructure/NodeService.js @@ -16,10 +16,10 @@ * */ +import { NodeWatchService } from '../infrastructure'; import http from './http'; import Constants from '../config/constants'; import helper from '../helper'; -import moment from 'moment'; import * as symbol from 'symbol-sdk'; class NodeService { @@ -67,27 +67,40 @@ 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. @@ -95,22 +108,14 @@ class NodeService { */ 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 + }; - // Api status + formattedNode.apiStatus = {}; + + // 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: null != restVersion, + 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'); } }; From bdf412b852ebd52ea1a09c3945a0d552eb07a816 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Tue, 15 Jul 2025 05:17:25 +0800 Subject: [PATCH 11/17] [explorer] task: added nodeWatch config endpoint --- src/config/default.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/default.json b/src/config/default.json index f8f51b574..34a3a9a0a 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", From fb38b71d59afdb280c6707ee01bec915060c984a Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Thu, 17 Jul 2025 03:40:27 +0800 Subject: [PATCH 12/17] [explorer] task: added order params to query random node list --- .../infrastructure/NodeWatchService.spec.js | 8 +++---- src/infrastructure/NodeService.js | 2 +- src/infrastructure/NodeWatchService.js | 23 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/__tests__/infrastructure/NodeWatchService.spec.js b/__tests__/infrastructure/NodeWatchService.spec.js index 76f2361ea..b4e45cff6 100644 --- a/__tests__/infrastructure/NodeWatchService.spec.js +++ b/__tests__/infrastructure/NodeWatchService.spec.js @@ -37,14 +37,14 @@ describe('Node Watch Service', () => { expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/peer?only_ssl=false&limit=0`); }); - it('fetches nodes with SSL filtering and limit', async () => { + it('fetches nodes with SSL filtering, limit 2 and order random', async () => { // Act - const result = await NodeWatchService.getNodes(true, 2); + const result = await NodeWatchService.getNodes(true, 2, 'random'); // Assert expect(result).toEqual([...mockApiResponse, ...mockPeerResponse]); - expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/api?only_ssl=true&limit=2`); - expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/peer?only_ssl=true&limit=2`); + expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/api?only_ssl=true&limit=2&order=random`); + expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/peer?only_ssl=true&limit=2&order=random`); }); it('handles errors when fetching nodes', async () => { diff --git a/src/infrastructure/NodeService.js b/src/infrastructure/NodeService.js index 8810ef24c..304fa2b7e 100644 --- a/src/infrastructure/NodeService.js +++ b/src/infrastructure/NodeService.js @@ -302,7 +302,7 @@ class NodeService { */ static getAPINodeList = async () => { // get 30 ssl ready nodes from node watch service - const nodes = await NodeWatchService.getNodes(true, 30); + const nodes = await NodeWatchService.getNodes(true, 30, 'random'); return nodes .filter(node => node.isHealthy && node.isSslEnabled) diff --git a/src/infrastructure/NodeWatchService.js b/src/infrastructure/NodeWatchService.js index e51e6d9c4..ab15d8402 100644 --- a/src/infrastructure/NodeWatchService.js +++ b/src/infrastructure/NodeWatchService.js @@ -2,27 +2,26 @@ import globalConfig from '../config/globalConfig'; import Axios from 'axios'; class NodeWatchService { - static async getNodes(onlySSL = false, limit = 0) { - try { + 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([ - Axios.get(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/api?only_ssl=` + onlySSL + '&limit=' + limit), - Axios.get(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/peer?only_ssl=` + onlySSL + '&limit=' + limit) + this.get(`/api/symbol/nodes/api?${params}`), + this.get(`/api/symbol/nodes/peer?${params}`) ]); return [...apiNodesResponse.data, ...peerNodesResponse.data]; - } catch (error) { - console.error('Error fetching nodes:', error); - throw error; - } } static async getNodeByMainPublicKey(mainPublicKey) { + + static async get(route) { try { - const response = await Axios.get(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/mainPublicKey/${mainPublicKey}`); - return response.data; + const response = await Axios.get(`${globalConfig.endpoints.nodeWatch}${route}`); + return response; } catch (error) { - console.error('Error fetching node by main public key:', error); - throw error; + console.error('Error fetching nodes:', error); + throw Error(`Error fetching from ${route}`); } } } From bcfe000638cd8ff23d11fceaf89a7fb02ee131d8 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Thu, 17 Jul 2025 03:41:07 +0800 Subject: [PATCH 13/17] [explorer] task: refactor on node watch services --- .../infrastructure/NodeWatchService.spec.js | 27 +++++++++---------- src/infrastructure/NodeWatchService.js | 10 ++++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/__tests__/infrastructure/NodeWatchService.spec.js b/__tests__/infrastructure/NodeWatchService.spec.js index b4e45cff6..b5bf6cd52 100644 --- a/__tests__/infrastructure/NodeWatchService.spec.js +++ b/__tests__/infrastructure/NodeWatchService.spec.js @@ -5,6 +5,16 @@ 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' }, @@ -47,13 +57,7 @@ describe('Node Watch Service', () => { expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/peer?only_ssl=true&limit=2&order=random`); }); - it('handles errors when fetching nodes', async () => { - // Arrange - Axios.get.mockRejectedValue(new Error('Network error')); - - // Act + Assert - await expect(NodeWatchService.getNodes()).rejects.toThrow('Network error'); - }); + runNodeWatchThrowErrorTests('getNodes', undefined, 'Error fetching from /api/symbol/nodes/api?only_ssl=false&limit=0'); }); describe('getNodeByMainPublicKey', () => { @@ -72,13 +76,6 @@ describe('Node Watch Service', () => { expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/mainPublicKey/${mainPublicKey}`); }); - it('handles errors when fetching node by main public key', async () => { - // Arrange - const mainPublicKey = '1234567890abcdef'; - Axios.get.mockRejectedValue(new Error('Network error')); - - // Act + Assert - await expect(NodeWatchService.getNodeByMainPublicKey(mainPublicKey)).rejects.toThrow('Network error'); - }); + runNodeWatchThrowErrorTests('getNodeByMainPublicKey', '1234567890abcdef', 'Error fetching from /api/symbol/nodes/mainPublicKey/1234567890abcdef'); }); }); \ No newline at end of file diff --git a/src/infrastructure/NodeWatchService.js b/src/infrastructure/NodeWatchService.js index ab15d8402..3198ce972 100644 --- a/src/infrastructure/NodeWatchService.js +++ b/src/infrastructure/NodeWatchService.js @@ -5,15 +5,19 @@ 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([ + 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]; + 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 { From 0c8b0ff535e33e9e5f53ef9d09904c3b16cd673c Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Thu, 17 Jul 2025 03:55:47 +0800 Subject: [PATCH 14/17] [explorer] task: lint fix --- __tests__/infrastructure/NodeService.spec.js | 2 +- src/infrastructure/NodeService.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/infrastructure/NodeService.spec.js b/__tests__/infrastructure/NodeService.spec.js index 4b1dd2bc6..44ed6bafa 100644 --- a/__tests__/infrastructure/NodeService.spec.js +++ b/__tests__/infrastructure/NodeService.spec.js @@ -291,5 +291,5 @@ describe('Node Service', () => { } ]); }); - }) + }); }); diff --git a/src/infrastructure/NodeService.js b/src/infrastructure/NodeService.js index 304fa2b7e..fbeff084d 100644 --- a/src/infrastructure/NodeService.js +++ b/src/infrastructure/NodeService.js @@ -16,10 +16,10 @@ * */ -import { NodeWatchService } from '../infrastructure'; import http from './http'; import Constants from '../config/constants'; import helper from '../helper'; +import { NodeWatchService } from '../infrastructure'; import * as symbol from 'symbol-sdk'; class NodeService { From 55c31b6c468fcd95cee5305dea8e499d59d17933 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Wed, 20 Aug 2025 23:04:33 +0800 Subject: [PATCH 15/17] [explorer] fix: lint issue --- __tests__/infrastructure/NodeWatchService.spec.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/__tests__/infrastructure/NodeWatchService.spec.js b/__tests__/infrastructure/NodeWatchService.spec.js index b5bf6cd52..183fc4e59 100644 --- a/__tests__/infrastructure/NodeWatchService.spec.js +++ b/__tests__/infrastructure/NodeWatchService.spec.js @@ -52,9 +52,10 @@ describe('Node Watch Service', () => { 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(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/api?only_ssl=true&limit=2&order=random`); - expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/peer?only_ssl=true&limit=2&order=random`); + 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'); @@ -76,6 +77,10 @@ describe('Node Watch Service', () => { expect(Axios.get).toHaveBeenCalledWith(`${globalConfig.endpoints.nodeWatch}/api/symbol/nodes/mainPublicKey/${mainPublicKey}`); }); - runNodeWatchThrowErrorTests('getNodeByMainPublicKey', '1234567890abcdef', 'Error fetching from /api/symbol/nodes/mainPublicKey/1234567890abcdef'); + runNodeWatchThrowErrorTests( + 'getNodeByMainPublicKey', + '1234567890abcdef', + 'Error fetching from /api/symbol/nodes/mainPublicKey/1234567890abcdef' + ); }); }); \ No newline at end of file From ffd2005afe38c17bc7c6b96ad214e40f0a1e1518 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Fri, 22 Aug 2025 22:24:35 +0800 Subject: [PATCH 16/17] [explorer] fix: rename and typo --- __tests__/components/NodesMap.spec.js | 8 ++++---- __tests__/infrastructure/NodeService.spec.js | 2 +- src/components/NodesMap.vue | 4 ++-- src/infrastructure/NodeService.js | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/__tests__/components/NodesMap.spec.js b/__tests__/components/NodesMap.spec.js index f648055cf..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, isAPInode) => { +const setupStoreMount = (role, isApiNode) => { const nodeModule = { namespaced: true }; @@ -36,7 +36,7 @@ const setupStoreMount = (role, isAPInode) => { const propsData = { nodes: [{ rolesRaw: role, - isAPInode, + isApiNode, coordinates: { latitude: 1, longitude: 2 @@ -56,9 +56,9 @@ localVue.use(Vuex); describe('NodesMap', () => { describe('addMarkers', () => { - const assertMarkerIcon = (role, isAPInode, expectedIcon) => { + const assertMarkerIcon = (role, isApiNode, expectedIcon) => { // Arrange: - const wrapper = setupStoreMount(role, isAPInode); + const wrapper = setupStoreMount(role, isApiNode); // Act: wrapper.vm.addMarkers(); diff --git a/__tests__/infrastructure/NodeService.spec.js b/__tests__/infrastructure/NodeService.spec.js index 44ed6bafa..a1d465dd7 100644 --- a/__tests__/infrastructure/NodeService.spec.js +++ b/__tests__/infrastructure/NodeService.spec.js @@ -225,7 +225,7 @@ describe('Node Service', () => { longitude: 20.000 }, rolesRaw: 3, - isAPInode: true + isApiNode: true } } ); diff --git a/src/components/NodesMap.vue b/src/components/NodesMap.vue index c3d99c165..e97823f53 100644 --- a/src/components/NodesMap.vue +++ b/src/components/NodesMap.vue @@ -175,7 +175,7 @@ export default { switch (node.rolesRaw) { case 1: - icon = node.isAPInode ? 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.isAPInode ? iconVotingLight : iconVoting; + icon = node.isApiNode ? iconVotingLight : iconVoting; break; case 6: case 7: diff --git a/src/infrastructure/NodeService.js b/src/infrastructure/NodeService.js index fbeff084d..5865b3b9b 100644 --- a/src/infrastructure/NodeService.js +++ b/src/infrastructure/NodeService.js @@ -103,7 +103,7 @@ class NodeService { }; /** - * Get available node list from statistic service. + * Get available node list from node watch service. * @returns {array} NodeInfo[] */ static getAvailableNodes = async () => { @@ -156,7 +156,7 @@ class NodeService { longitude: node.geoLocation.lon }, location: node.geoLocation.city + ', ' + node.geoLocation.region + ', ' + node.geoLocation.country, - isAPInode: null != node.restVersion + isApiNode: null != node.restVersion }; delete node.geoLocation; } @@ -204,7 +204,7 @@ class NodeService { } else { if (null != restVersion) { formattedNode.apiStatus = { - lightNodeStatus: null != restVersion, + lightNodeStatus: true, isHttpsEnabled, restVersion }; @@ -222,7 +222,7 @@ class NodeService { longitude: lon }, rolesRaw: formattedNode.rolesRaw, - isAPInode: null != formattedNode.restVersion + isApiNode: null != formattedNode.restVersion }; } else { formattedNode.mapInfo = {}; From bb8bec5917c1a5d8bd0ea8cb4eb03c4cd15d8443 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Fri, 22 Aug 2025 22:24:51 +0800 Subject: [PATCH 17/17] [explorer] fix: lint new line --- __tests__/infrastructure/NodeWatchService.spec.js | 2 +- src/config/key-redirects.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/infrastructure/NodeWatchService.spec.js b/__tests__/infrastructure/NodeWatchService.spec.js index 183fc4e59..75c7b4b4b 100644 --- a/__tests__/infrastructure/NodeWatchService.spec.js +++ b/__tests__/infrastructure/NodeWatchService.spec.js @@ -83,4 +83,4 @@ describe('Node Watch Service', () => { 'Error fetching from /api/symbol/nodes/mainPublicKey/1234567890abcdef' ); }); -}); \ No newline at end of file +}); diff --git a/src/config/key-redirects.json b/src/config/key-redirects.json index 6df495d3d..f4993d853 100644 --- a/src/config/key-redirects.json +++ b/src/config/key-redirects.json @@ -64,4 +64,4 @@ "node": "nodes", "mainPublicKey": "nodes" -} \ No newline at end of file +}