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,