diff --git a/package-lock.json b/package-lock.json index 4027d740..a34cd466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "semver": "^7.3.5", "shx": "^0.3.4", "symbol-sdk": "^2.0.6", - "symbol-statistics-service-typescript-fetch-client": "^1.1.5", "tslib": "^2.3.1", "utf8": "^3.0.0", "winston": "^3.5.1" @@ -7890,11 +7889,6 @@ "integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==", "license": "MIT" }, - "node_modules/symbol-statistics-service-typescript-fetch-client": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/symbol-statistics-service-typescript-fetch-client/-/symbol-statistics-service-typescript-fetch-client-1.1.5.tgz", - "integrity": "sha512-hcVD9jq+S2kZCYaXTDglU43y3Dn5K6W3O45Y/pMAfiuYDCdVaHczAPDE/ZKGo0kZXy69e1sn/riAzfDfn8yFBw==" - }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", diff --git a/package.json b/package.json index d5a25d1e..6c4027a8 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "semver": "^7.3.5", "shx": "^0.3.4", "symbol-sdk": "^2.0.6", - "symbol-statistics-service-typescript-fetch-client": "^1.1.5", "tslib": "^2.3.1", "utf8": "^3.0.0", "winston": "^3.5.1" diff --git a/presets/mainnet/network.yml b/presets/mainnet/network.yml index 7a76fc1a..d59e1e59 100644 --- a/presets/mainnet/network.yml +++ b/presets/mainnet/network.yml @@ -121,7 +121,7 @@ treasuryReissuanceEpochIneligibleVoterAddresses: votingUnfinalizedBlocksDuration: 0m timeSynchronizationMinImportance: 10000000000 explorerUrl: https://symbol.fyi -statisticsServiceUrl: https://symbol.services +nodeWatchUrl: https://nodewatch.symbol.tools knownRestGateways: [] knownPeers: [] restUncirculatingAccountPublicKeys: >- diff --git a/presets/shared.yml b/presets/shared.yml index a453b4de..3d3e364c 100644 --- a/presets/shared.yml +++ b/presets/shared.yml @@ -237,10 +237,8 @@ restSSLKeyFileName: 'restSSL.key' restSSLCertificateFileName: 'restSSL.crt' restNodeMetadata: _info: "Node metadata" -statisticsServicePeerFilter: '' -statisticsServicePeerLimit: 50 -statisticsServiceRestFilter: suggested -statisticsServiceRestLimit: 10 +nodeWatchPeerLimit: 50 +nodeWatchRestLimit: 10 treasuryReissuanceTransactionSignatures_has_items: false corruptAggregateTransactionHashes_has_items: false diff --git a/presets/testnet/network.yml b/presets/testnet/network.yml index 978207c4..c50dbd8b 100644 --- a/presets/testnet/network.yml +++ b/presets/testnet/network.yml @@ -52,7 +52,7 @@ votingUnfinalizedBlocksDuration: 0m timeSynchronizationMinImportance: 10000000000 faucetUrl: https://testnet.symbol.tools explorerUrl: https://testnet.symbol.fyi -statisticsServiceUrl: https://testnet.symbol.services +nodeWatchUrl: https://nodewatch.symbol.tools/testnet nemesis: mosaics: - diff --git a/src/model/ConfigPreset.ts b/src/model/ConfigPreset.ts index 2b58b4a4..122bbc4d 100644 --- a/src/model/ConfigPreset.ts +++ b/src/model/ConfigPreset.ts @@ -423,11 +423,9 @@ export interface CommonConfigPreset extends NodeConfigPreset, GatewayConfigPrese useExperimentalNativeVotingKeyGeneration?: boolean; lastKnownNetworkEpoch: number; autoUpdateVotingKeys: boolean; - statisticsServiceUrl?: string; - statisticsServicePeerLimit: number; - statisticsServicePeerFilter?: string; - statisticsServiceRestLimit: number; - statisticsServiceRestFilter?: string; + nodeWatchUrl?: string; + nodeWatchPeerLimit: number; + nodeWatchRestLimit: number; // Nested Objects inflation?: Record; diff --git a/src/service/NodeWatchService.ts b/src/service/NodeWatchService.ts new file mode 100644 index 00000000..03aa18c9 --- /dev/null +++ b/src/service/NodeWatchService.ts @@ -0,0 +1,63 @@ +import fetch from 'cross-fetch'; + +export interface NodeWatchNodeInfo { + endpoint: string; + host: string; + peerPort: number; + isHealthy: boolean | null; + publicKey: string; + friendlyName: string; + roles: number; +} + +export class NodeWatchService { + private static readonly PEER_PORT = 7900; + + constructor(private readonly baseUrl: string) {} + + public static mapResponse(response: any): NodeWatchNodeInfo { + return { + endpoint: response.endpoint, + host: NodeWatchService.hostPortFromEndpoint(response.endpoint), + peerPort: NodeWatchService.PEER_PORT, + isHealthy: response.isHealthy, + publicKey: response.mainPublicKey, + friendlyName: response.name, + roles: response.roles, + }; + } + + private static hostPortFromEndpoint(endpoint: string | null | undefined): string { + if (!endpoint) { + return ''; + } + try { + return new URL(endpoint).hostname; + } catch { + return ''; + } + } + + public async fetchNodesByType(nodeType: 'api' | 'peer', limit: number): Promise { + const params = new URLSearchParams({ + only_ssl: nodeType === 'api' ? 'true' : 'false', + limit: `${limit}`, + order: 'random', + }); + const url = `${this.baseUrl}/api/symbol/nodes/peer?${params.toString()}`; + const body = await this.requestJson(url); + if (!Array.isArray(body)) { + throw new Error('Node watch response was not a JSON array'); + } + return body.map((node: any) => NodeWatchService.mapResponse(node)); + } + + private async requestJson(url: string): Promise { + const response = await fetch(url, {}); + if (!response.ok) { + const reason = response.statusText ? `${response.status} ${response.statusText}` : `${response.status}`; + throw new Error(`Node watch request failed: HTTP ${reason}`); + } + return await response.json(); + } +} diff --git a/src/service/RemoteNodeService.ts b/src/service/RemoteNodeService.ts index 63195b35..bd2f4367 100644 --- a/src/service/RemoteNodeService.ts +++ b/src/service/RemoteNodeService.ts @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import fetch from 'cross-fetch'; import { lookup } from 'dns'; import * as _ from 'lodash'; import { firstValueFrom } from 'rxjs'; import { ChainInfo, RepositoryFactory, RepositoryFactoryHttp, RoleType } from 'symbol-sdk'; -import { Configuration, NodeApi, NodeListFilter, RequestContext } from 'symbol-statistics-service-typescript-fetch-client'; import { Logger } from '../logger'; import { ConfigPreset, PeerInfo } from '../model'; import { KnownError } from './KnownError'; +import { NodeWatchNodeInfo, NodeWatchService } from './NodeWatchService'; import { Utils } from './Utils'; export interface RepositoryInfo { @@ -127,17 +126,20 @@ export class RemoteNodeService { } const presetData = this.presetData; const urls = [...(presetData.knownRestGateways || [])]; - const statisticsServiceUrl = presetData.statisticsServiceUrl; - if (statisticsServiceUrl && !this.offline) { - const client = this.createNodeApiRestClient(statisticsServiceUrl); + const nodeWatchUrl = presetData.nodeWatchUrl; + if (nodeWatchUrl && !this.offline) { try { - const filter = presetData.statisticsServiceRestFilter as NodeListFilter; - const limit = presetData.statisticsServiceRestLimit; - const nodes = await client.getNodes(filter ? filter : undefined, limit); - urls.push(...nodes.map((n) => n.apiStatus?.restGatewayUrl).filter((url): url is string => !!url)); + const limit = presetData.nodeWatchRestLimit; + const nodes = await this.getNodeWatchNodes(nodeWatchUrl, 'api', limit); + urls.push( + ...nodes + .filter((n) => n.isHealthy === true) + .map((n) => n.endpoint) + .filter((url): url is string => !!url), + ); } catch (e) { this.logger.warn( - `There has been an error connecting to statistics ${statisticsServiceUrl}. Rest urls cannot be resolved! Error ${Utils.getMessage( + `There has been an error connecting to node watch ${nodeWatchUrl}. Rest urls cannot be resolved! Error ${Utils.getMessage( e, )}`, ); @@ -170,24 +172,23 @@ export class RemoteNodeService { public async getPeerInfos(): Promise { const presetData = this.presetData; - const statisticsServiceUrl = presetData.statisticsServiceUrl; + const nodeWatchUrl = presetData.nodeWatchUrl; const knownPeers = [...(presetData.knownPeers || [])]; - if (statisticsServiceUrl && !this.offline) { - const client = this.createNodeApiRestClient(statisticsServiceUrl); + if (nodeWatchUrl && !this.offline) { try { - const filter = presetData.statisticsServicePeerFilter as NodeListFilter; - const limit = presetData.statisticsServicePeerLimit; - const nodes = await client.getNodes(filter ? filter : undefined, limit); + const limit = presetData.nodeWatchPeerLimit; + const nodes = await this.getNodeWatchNodes(nodeWatchUrl, 'peer', limit); const peerInfos = nodes + .filter((n) => n.isHealthy !== false) .map((n): PeerInfo | undefined => { - if (!n.peerStatus?.isAvailable || !n.publicKey || !n.port || !n.friendlyName || !n.roles) { + if (!n.publicKey || !n.friendlyName || !n.roles || !n.host) { return undefined; } return { publicKey: n.publicKey, endpoint: { - host: n.host || '', - port: n.port, + host: n.host, + port: n.peerPort, }, metadata: { name: n.friendlyName, @@ -199,7 +200,7 @@ export class RemoteNodeService { knownPeers.push(...peerInfos); } catch (error) { this.logger.warn( - `There has been an error connecting to statistics ${statisticsServiceUrl}. Peers cannot be resolved! Error ${Utils.getMessage( + `There has been an error connecting to node watch ${nodeWatchUrl}. Peers cannot be resolved! Error ${Utils.getMessage( error, )}`, ); @@ -208,21 +209,9 @@ export class RemoteNodeService { return knownPeers; } - public createNodeApiRestClient(statisticsServiceUrl: string): NodeApi { - return new NodeApi( - new Configuration({ - fetchApi: fetch as any, - basePath: statisticsServiceUrl, - middleware: [ - { - pre: (context: RequestContext): Promise => { - this.logger.info(`Getting nodes information from ${context.url}`); - return Promise.resolve(); - }, - }, - ], - }), - ); + public async getNodeWatchNodes(nodeWatchUrl: string, nodeType: 'api' | 'peer', limit: number): Promise { + this.logger.info(`Getting nodes information from ${nodeWatchUrl}`); + return new NodeWatchService(nodeWatchUrl).fetchNodesByType(nodeType, limit); } public async resolveRestUrlsForServices(): Promise<{ restNodes: string[]; defaultNode: string }> { diff --git a/src/service/index.ts b/src/service/index.ts index 24f6830d..b1dc310c 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -21,6 +21,7 @@ export * from './LinkService'; export * from './MigrationService'; export * from './ModifyMultisigService'; export * from './NemgenService'; +export * from './NodeWatchService'; export * from './OSUtils'; export * from './PortService'; export * from './RemoteNodeService'; diff --git a/test/service/NodeWatchService.test.ts b/test/service/NodeWatchService.test.ts new file mode 100644 index 00000000..3b586548 --- /dev/null +++ b/test/service/NodeWatchService.test.ts @@ -0,0 +1,114 @@ +import { expect } from 'chai'; +import 'mocha'; +import { afterEach, describe, it } from 'mocha'; +import { NodeWatchService } from '../../src/service/NodeWatchService'; +import nock = require('nock'); + +const baseUrl = 'https://nodewatch.test'; + +describe('NodeWatchService', () => { + afterEach(() => { + nock.cleanAll(); + }); + + describe('mapResponse', () => { + it('maps flat NodeWatch JSON row', () => { + const mapped = NodeWatchService.mapResponse({ + endpoint: 'https://example.com:3001', + name: 'Node1', + mainPublicKey: 'MAINPUBLICKEYHEX0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF', + nodePublicKey: 'NODEPUBLICKEYHEX0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF', + isSslEnabled: true, + isHealthy: true, + restVersion: '1.0.0', + roles: 3, + }); + + expect(mapped).to.deep.equal({ + endpoint: 'https://example.com:3001', + host: 'example.com', + peerPort: 7900, + isHealthy: true, + publicKey: 'MAINPUBLICKEYHEX0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF', + friendlyName: 'Node1', + roles: 3, + }); + }); + + it('propagates isHealthy null from API', () => { + const mapped = NodeWatchService.mapResponse({ + endpoint: '', + name: 'peer-only', + mainPublicKey: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + isHealthy: null, + roles: 1, + }); + expect(mapped.isHealthy).eq(null); + expect(mapped.host).eq(''); + }); + + it('returns empty host when endpoint is not a valid URL', () => { + const mapped = NodeWatchService.mapResponse({ + endpoint: 'not-a-valid-url', + name: 'x', + mainPublicKey: 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', + isHealthy: false, + roles: 1, + }); + expect(mapped.host).eq(''); + }); + }); + + describe('fetchNodesByType', () => { + it('calls /nodes/peer with only_ssl=true for nodeType api and maps array body', async () => { + const row = { + endpoint: 'https://dual.example:3001', + name: 'dual', + mainPublicKey: 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', + isHealthy: true, + roles: 3, + }; + nock(baseUrl).get('/api/symbol/nodes/peer').query({ only_ssl: 'true', limit: '5', order: 'random' }).reply(200, [row]); + + const service = new NodeWatchService(baseUrl); + const nodes = await service.fetchNodesByType('api', 5); + + expect(nodes).to.have.lengthOf(1); + expect(nodes[0].friendlyName).eq('dual'); + expect(nodes[0].host).eq('dual.example'); + expect(nock.isDone()).eq(true); + }); + + it('calls /nodes/peer with only_ssl=false for nodeType peer', async () => { + nock(baseUrl).get('/api/symbol/nodes/peer').query({ only_ssl: 'false', limit: '1', order: 'random' }).reply(200, []); + + const service = new NodeWatchService(baseUrl); + await service.fetchNodesByType('peer', 1); + expect(nock.isDone()).eq(true); + }); + + it('throws when response body is not a JSON array', async () => { + nock(baseUrl).get('/api/symbol/nodes/peer').query(true).reply(200, { ok: false }); + + const service = new NodeWatchService(baseUrl); + try { + await service.fetchNodesByType('api', 1); + expect.fail('expected error'); + } catch (e: unknown) { + expect((e as Error).message).eq('Node watch response was not a JSON array'); + } + }); + + it('throws when HTTP status is not 2xx', async () => { + nock(baseUrl).get('/api/symbol/nodes/peer').query(true).reply(503); + + const service = new NodeWatchService(baseUrl); + try { + await service.fetchNodesByType('peer', 2); + expect.fail('expected error'); + } catch (e: unknown) { + expect((e as Error).message).eq('Node watch request failed: HTTP 503 Service Unavailable'); + } + }); + }); +}); diff --git a/test/service/RemoteNodeService.test.ts b/test/service/RemoteNodeService.test.ts index 0099b756..d69c0f5e 100644 --- a/test/service/RemoteNodeService.test.ts +++ b/test/service/RemoteNodeService.test.ts @@ -16,393 +16,90 @@ import { expect } from 'chai'; import 'mocha'; -import { it } from 'mocha'; +import { afterEach, describe, it } from 'mocha'; import { join } from 'path'; import { restore, stub } from 'sinon'; -import { NodeApi } from 'symbol-statistics-service-typescript-fetch-client'; import { ConfigPreset, LoggerFactory, LogType, YamlUtils } from '../../src'; import { ConfigLoader, Preset, RemoteNodeService } from '../../src/service'; +import { NodeWatchNodeInfo } from '../../src/service/NodeWatchService'; + const logger = LoggerFactory.getLogger(LogType.Silent); -const list = [ + +const mockNodeWatchNodes: NodeWatchNodeInfo[] = [ { - peerStatus: { - isAvailable: true, - lastStatusCheck: 1635710986117, - }, - apiStatus: { - restGatewayUrl: 'https://dual-001.testnet.symbol.dev:3001', - isAvailable: true, - lastStatusCheck: 1635710986145, - nodeStatus: { - apiNode: 'up', - db: 'up', - }, - isHttpsEnabled: true, - nodePublicKey: 'A2160AB911943082C88109DD8B65A0082EF547CA7C28F001F857112F7ADD9B3D', - chainHeight: 517611, - finalization: { - height: 517596, - epoch: 720, - point: 43, - hash: 'FD462D4133EEEC56471AAE18A6A2A3065DF69394A849D51F20286E189C46E4F5', - }, - restVersion: '2.3.8-alpha', - }, - _id: '617ef846196f2900128bb55c', - version: 16777728, - publicKey: 'E3FC28889BDE31406465167F1D9D6A16DCA1FF67A3BABFA5E5A8596478848F78', - networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155', + endpoint: 'https://nw-mock-01.example.invalid:3001', + host: 'nw-mock-01.example.invalid', + peerPort: 7900, + isHealthy: true, + publicKey: '1111111111111111111111111111111111111111111111111111111111111111', + friendlyName: '!mock-nw-01', roles: 3, - port: 7900, - networkIdentifier: 152, - host: 'dual-001.testnet.symbol.dev', - friendlyName: 'dual-001', - hostDetail: { - host: 'dual-001.testnet.symbol.dev', - coordinates: { - latitude: 39.0438, - longitude: -77.4874, - }, - location: 'Ashburn, VA, United States', - ip: '3.86.56.197', - organization: 'AWS EC2 (us-east-1)', - as: 'AS14618 Amazon.com, Inc.', - continent: 'North America', - country: 'United States', - region: 'VA', - city: 'Ashburn', - district: '', - zip: '20149', - }, - __v: 0, }, { - peerStatus: { - isAvailable: true, - lastStatusCheck: 1635710986215, - }, - apiStatus: { - restGatewayUrl: 'https://sym-test-06.opening-line.jp:3001', - isAvailable: true, - lastStatusCheck: 1635710986858, - nodeStatus: { - apiNode: 'up', - db: 'up', - }, - isHttpsEnabled: true, - nodePublicKey: '50F34D96117E020BBB48C81C719A020C40729BF3D48483751D6CA8198FFB52C9', - chainHeight: 517611, - finalization: { - height: 517596, - epoch: 720, - point: 43, - hash: 'FD462D4133EEEC56471AAE18A6A2A3065DF69394A849D51F20286E189C46E4F5', - }, - restVersion: '2.3.6', - }, - _id: '617ef846196f2900128bb55d', - version: 16777728, - publicKey: '4675E1626A35EF8B9537486D93BB6B488960712A653CB62D27404D35E92F53A9', - networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155', + endpoint: 'https://nw-mock-02.example.invalid:3001', + host: 'nw-mock-02.example.invalid', + peerPort: 7900, + isHealthy: true, + publicKey: '2222222222222222222222222222222222222222222222222222222222222222', + friendlyName: '!mock-nw-02', roles: 3, - port: 7900, - networkIdentifier: 152, - host: 'sym-test-06.opening-line.jp', - friendlyName: 'sym-test-06.opening-line.jp', - hostDetail: { - host: 'sym-test-06.opening-line.jp', - coordinates: { - latitude: 54.7091, - longitude: 25.2971, - }, - location: 'Vilnius, VL, Lithuania', - ip: '80.209.226.245', - organization: 'RACKRAY', - as: 'AS212531 Interneto vizija', - continent: 'Europe', - country: 'Lithuania', - region: 'VL', - city: 'Vilnius', - district: '', - zip: '08234', - }, - __v: 0, }, { - peerStatus: { - isAvailable: true, - lastStatusCheck: 1635710986325, - }, - _id: '617ef846196f2900128bb55e', - version: 16777728, - publicKey: '2489946E49B03D9BE040E3FD42FEBC705D001A746BD25399E2796D615B35B732', - networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155', + endpoint: 'https://nw-mock-03.example.invalid:3001', + host: 'nw-mock-03.example.invalid', + peerPort: 7900, + isHealthy: true, + publicKey: '3333333333333333333333333333333333333333333333333333333333333333', + friendlyName: '!mock-nw-03', roles: 5, - port: 7900, - networkIdentifier: 152, - host: 'peer-601.testnet.symbol.dev', - friendlyName: 'peer-601', - hostDetail: { - host: 'peer-601.testnet.symbol.dev', - coordinates: { - latitude: 1.28009, - longitude: 103.851, - }, - location: 'Singapore, , Singapore', - ip: '54.179.53.6', - organization: 'AWS EC2 (ap-southeast-1)', - as: 'AS16509 Amazon.com, Inc.', - continent: 'Asia', - country: 'Singapore', - region: '', - city: 'Singapore', - district: '', - zip: '', - }, - __v: 0, }, { - peerStatus: { - isAvailable: true, - lastStatusCheck: 1635710986299, - }, - apiStatus: { - restGatewayUrl: 'http://AMATERASU.symbol-node.com:3000', - isAvailable: true, - lastStatusCheck: 1635710987216, - nodeStatus: { - apiNode: 'up', - db: 'up', - }, - isHttpsEnabled: false, - nodePublicKey: 'B46268513DDCC2A74241E11F2A38F2FCC6CB655E7CBBD95DA6B32266B5CA88ED', - chainHeight: 517611, - finalization: { - height: 517596, - epoch: 720, - point: 43, - hash: 'FD462D4133EEEC56471AAE18A6A2A3065DF69394A849D51F20286E189C46E4F5', - }, - restVersion: '2.3.6', - }, - _id: '617ef846196f2900128bb55f', - version: 16777728, - publicKey: 'DB14A11E28CA1EF8BC45657BA3FF0879946A57D8F7370C585819365521C6449C', - networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155', + endpoint: 'http://nw-mock-04.example.invalid:3000', + host: 'nw-mock-04.example.invalid', + peerPort: 7900, + isHealthy: false, + publicKey: '4444444444444444444444444444444444444444444444444444444444444444', + friendlyName: '!mock-nw-04', roles: 3, - port: 7900, - networkIdentifier: 152, - host: 'AMATERASU.symbol-node.com', - friendlyName: 'AMATERASU.symbol-node.com(TEST)', - hostDetail: { - host: 'AMATERASU.symbol-node.com', - coordinates: { - latitude: 34.6866, - longitude: 135.8548, - }, - location: 'Nara, 29, Japan', - ip: '58.70.54.55', - organization: 'OPTAGE Inc.', - as: 'AS17511 OPTAGE Inc.', - continent: 'Asia', - country: 'Japan', - region: '29', - city: 'Nara', - district: '', - zip: '630-8211', - }, - __v: 0, }, { - peerStatus: { - isAvailable: true, - lastStatusCheck: 1635710986193, - }, - apiStatus: { - restGatewayUrl: 'https://iroha-symbolnode.com:3001', - isAvailable: true, - lastStatusCheck: 1635710986698, - nodeStatus: { - apiNode: 'up', - db: 'up', - }, - isHttpsEnabled: true, - nodePublicKey: '01438DDE96FD4816726F8B80CC012DC85FED6CDA45F9B932887A3512593CFA51', - chainHeight: 517611, - finalization: { - height: 517596, - epoch: 720, - point: 43, - hash: 'FD462D4133EEEC56471AAE18A6A2A3065DF69394A849D51F20286E189C46E4F5', - }, - restVersion: '2.3.6', - }, - _id: '617ef846196f2900128bb560', - version: 16777728, - publicKey: '26BEC23EF633936BAB5E501F03E0C374036F5FF20AC068972839357851411496', - networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155', + endpoint: 'https://nw-mock-05.example.invalid:3001', + host: 'nw-mock-05.example.invalid', + peerPort: 7900, + isHealthy: true, + publicKey: '5555555555555555555555555555555555555555555555555555555555555555', + friendlyName: '!mock-nw-05', roles: 3, - port: 7900, - networkIdentifier: 152, - host: 'iroha-symbolnode.com', - friendlyName: '168nihoheto_VDS_S', - hostDetail: { - host: 'iroha-symbolnode.com', - coordinates: { - latitude: 47.6034, - longitude: -122.3414, - }, - location: 'Seattle, WA, United States', - ip: '66.94.122.36', - organization: 'Contabo Inc', - as: 'AS40021 Contabo Inc.', - continent: 'North America', - country: 'United States', - region: 'WA', - city: 'Seattle', - district: '', - zip: '98111', - }, - __v: 0, }, { - peerStatus: { - isAvailable: true, - lastStatusCheck: 1635710986174, - }, - apiStatus: { - restGatewayUrl: 'https://dual-101.testnet.symbol.dev:3001', - isAvailable: true, - lastStatusCheck: 1635710986567, - nodeStatus: { - apiNode: 'up', - db: 'up', - }, - isHttpsEnabled: true, - nodePublicKey: 'F81F749613EF3BC10BB9670A6FAF49BFA95079898E2034255B8256FBA3FD105D', - chainHeight: 517611, - finalization: { - height: 517596, - epoch: 720, - point: 43, - hash: 'FD462D4133EEEC56471AAE18A6A2A3065DF69394A849D51F20286E189C46E4F5', - }, - restVersion: '2.3.8-alpha', - }, - _id: '617ef846196f2900128bb561', - version: 16777728, - publicKey: 'C4348215B4C417D3E4B52ACAA3D370D29DE3A5F482CAED3C9F1BE257DD2B4079', - networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155', + endpoint: 'http://nw-mock-06.example.invalid:3000', + host: 'nw-mock-06.example.invalid', + peerPort: 7900, + isHealthy: true, + publicKey: '6666666666666666666666666666666666666666666666666666666666666666', + friendlyName: '!mock-nw-06', roles: 3, - port: 7900, - networkIdentifier: 152, - host: 'dual-101.testnet.symbol.dev', - friendlyName: 'dual-101', - hostDetail: { - host: 'dual-101.testnet.symbol.dev', - coordinates: { - latitude: 37.3394, - longitude: -121.895, - }, - location: 'San Jose, CA, United States', - ip: '54.151.52.226', - organization: 'AWS EC2 (us-west-1)', - as: 'AS16509 Amazon.com, Inc.', - continent: 'North America', - country: 'United States', - region: 'CA', - city: 'San Jose', - district: '', - zip: '95141', - }, - __v: 0, }, { - peerStatus: { - isAvailable: true, - lastStatusCheck: 1635710986181, - }, - _id: '617ef846196f2900128bb565', - version: 16777728, - publicKey: 'DC7A90D0676DB3A2D963768276F606AF76541A59588B23C6C6B48D98E0AC3837', - networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155', - roles: 1, - port: 7900, - networkIdentifier: 152, - host: 'peer-301.testnet.symbol.dev', - friendlyName: 'peer-301', - hostDetail: { - host: 'peer-301.testnet.symbol.dev', - coordinates: { - latitude: 53.3498, - longitude: -6.26031, - }, - location: 'Dublin, L, Ireland', - ip: '54.77.189.25', - organization: 'AWS EC2 (eu-west-1)', - as: 'AS16509 Amazon.com, Inc.', - continent: 'Europe', - country: 'Ireland', - region: 'L', - city: 'Dublin', - district: '', - zip: 'D02', - }, - __v: 0, + endpoint: '', + host: 'nw-mock-peer-07.example.invalid', + peerPort: 7900, + isHealthy: null, + publicKey: '7777777777777777777777777777777777777777777777777777777777777777', + friendlyName: '!mock-nw-peer-07', + roles: 5, }, { - peerStatus: { - isAvailable: true, - lastStatusCheck: 1635710986140, - }, - apiStatus: { - restGatewayUrl: 'https://sym-test-02.opening-line.jp:3001', - isAvailable: true, - lastStatusCheck: 1635710986329, - nodeStatus: { - apiNode: 'up', - db: 'up', - }, - isHttpsEnabled: true, - nodePublicKey: '81448301A61412CE24F679C67136CF56DF43216EEAB3065677AA4ECFD0441B59', - chainHeight: 517611, - finalization: { - height: 517596, - epoch: 720, - point: 43, - hash: 'FD462D4133EEEC56471AAE18A6A2A3065DF69394A849D51F20286E189C46E4F5', - }, - restVersion: '2.3.6', - }, - _id: '617ef846196f2900128bb567', - version: 16777728, - publicKey: '97A7D1E1889803D4A5E3F372530EB555C495B23012807E3E94EF15A2205BC3A6', - networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155', - roles: 3, - port: 7900, - networkIdentifier: 152, - host: 'sym-test-02.opening-line.jp', - friendlyName: 'sym-test-02.opening-line.jp', - hostDetail: { - host: 'sym-test-02.opening-line.jp', - coordinates: { - latitude: 38.6327, - longitude: -90.1956, - }, - location: 'St Louis, MO, United States', - ip: '209.145.59.225', - organization: 'Contabo Inc', - as: 'AS40021 Contabo Inc.', - continent: 'North America', - country: 'United States', - region: 'MO', - city: 'St Louis', - district: 'Downtown', - zip: '63101', - }, - __v: 0, + endpoint: '', + host: 'nw-mock-peer-08.example.invalid', + peerPort: 7900, + isHealthy: null, + publicKey: '8888888888888888888888888888888888888888888888888888888888888888', + friendlyName: '!mock-nw-peer-08', + roles: 1, }, ]; + const customPresetObject = { lastKnownNetworkEpoch: 1, nodeUseRemoteAccount: true, @@ -421,6 +118,7 @@ const customPresetObject = { }, ], }; + const preset = Preset.testnet; const root = './'; const networkPresetLocation = `${root}/presets/${preset}/network.yml`; @@ -431,121 +129,94 @@ const presetData: ConfigPreset = new ConfigLoader(logger).mergePresets(sharedPre describe('RemoteNodeService', () => { afterEach(restore); - it('getRestUrls online', async () => { - stub(RemoteNodeService.prototype, 'createNodeApiRestClient').callsFake(() => { - return { - getNodes(filter: NodeFilter, limit: number) { - expect(filter).eq(presetData.statisticsServiceRestFilter); - expect(limit).eq(presetData.statisticsServiceRestLimit); - return list; - }, - } as unknown as NodeApi; + + it('getRestUrls known and healthy NodeWatch endpoints', async () => { + stub(RemoteNodeService.prototype, 'getNodeWatchNodes').callsFake(async (baseUrl, type, limit) => { + expect(baseUrl).eq(presetData.nodeWatchUrl); + expect(type).eq('api'); + expect(limit).eq(presetData.nodeWatchRestLimit); + return mockNodeWatchNodes; }); const service = new RemoteNodeService(logger, presetData, false); const urls = await service.getRestUrls(); - expect(urls).deep.eq([ + + expect(urls).to.deep.equal([ 'http://staticRest1:3000', 'https://staticRest2:3001', - 'https://dual-001.testnet.symbol.dev:3001', - 'https://sym-test-06.opening-line.jp:3001', - 'http://AMATERASU.symbol-node.com:3000', - 'https://iroha-symbolnode.com:3001', - 'https://dual-101.testnet.symbol.dev:3001', - 'https://sym-test-02.opening-line.jp:3001', + 'https://nw-mock-01.example.invalid:3001', + 'https://nw-mock-02.example.invalid:3001', + 'https://nw-mock-03.example.invalid:3001', + 'https://nw-mock-05.example.invalid:3001', + 'http://nw-mock-06.example.invalid:3000', ]); }); - it('getRestUrls offline', async () => { - stub(RemoteNodeService.prototype, 'createNodeApiRestClient').callsFake(() => { - return { - getNodes(filter: NodeFilter, limit: number) { - expect(filter).eq(presetData.statisticsServiceRestFilter); - expect(limit).eq(presetData.statisticsServiceRestLimit); - return list; - }, - } as unknown as NodeApi; - }); + it('getRestUrls offline skips NodeWatch', async () => { const service = new RemoteNodeService(logger, presetData, true); const urls = await service.getRestUrls(); - expect(urls).deep.eq(['http://staticRest1:3000', 'https://staticRest2:3001']); + expect(urls).to.deep.equal(['http://staticRest1:3000', 'https://staticRest2:3001']); }); - it('getPeerInfos online', async () => { - stub(RemoteNodeService.prototype, 'createNodeApiRestClient').callsFake(() => { - return { - getNodes(filter: NodeFilter, limit: number) { - expect(presetData.statisticsServicePeerFilter).eq(''); - expect(filter).eq(undefined); - expect(limit).eq(presetData.statisticsServicePeerLimit); - return list; - }, - } as unknown as NodeApi; + + it('getPeerInfos online merges NodeWatch peers after preset knownPeers', async () => { + stub(RemoteNodeService.prototype, 'getNodeWatchNodes').callsFake(async (baseUrl, type, limit) => { + expect(baseUrl).eq(presetData.nodeWatchUrl); + expect(type).eq('peer'); + expect(limit).eq(presetData.nodeWatchPeerLimit); + return mockNodeWatchNodes; }); const service = new RemoteNodeService(logger, presetData, false); const peerInfos = await service.getPeerInfos(); - expect(peerInfos).deep.eq([ + + expect(peerInfos).to.deep.equal([ { publicKey: 'AAAAE7EAEEAE61EF0C50B4D05931F4325F69081B1B074D31E094C4B21E8CFB3D', endpoint: { host: 'someStaticPeer', port: 7900 }, metadata: { name: 'someStaticPeer', roles: 'Peer,Api' }, }, { - publicKey: 'E3FC28889BDE31406465167F1D9D6A16DCA1FF67A3BABFA5E5A8596478848F78', - endpoint: { host: 'dual-001.testnet.symbol.dev', port: 7900 }, - metadata: { name: 'dual-001', roles: 'Peer,Api' }, - }, - { - publicKey: '4675E1626A35EF8B9537486D93BB6B488960712A653CB62D27404D35E92F53A9', - endpoint: { host: 'sym-test-06.opening-line.jp', port: 7900 }, - metadata: { name: 'sym-test-06.opening-line.jp', roles: 'Peer,Api' }, + publicKey: '1111111111111111111111111111111111111111111111111111111111111111', + endpoint: { host: 'nw-mock-01.example.invalid', port: 7900 }, + metadata: { name: '!mock-nw-01', roles: 'Peer,Api' }, }, { - publicKey: '2489946E49B03D9BE040E3FD42FEBC705D001A746BD25399E2796D615B35B732', - endpoint: { host: 'peer-601.testnet.symbol.dev', port: 7900 }, - metadata: { name: 'peer-601', roles: 'Peer,Voting' }, + publicKey: '2222222222222222222222222222222222222222222222222222222222222222', + endpoint: { host: 'nw-mock-02.example.invalid', port: 7900 }, + metadata: { name: '!mock-nw-02', roles: 'Peer,Api' }, }, { - publicKey: 'DB14A11E28CA1EF8BC45657BA3FF0879946A57D8F7370C585819365521C6449C', - endpoint: { host: 'AMATERASU.symbol-node.com', port: 7900 }, - metadata: { name: 'AMATERASU.symbol-node.com(TEST)', roles: 'Peer,Api' }, + publicKey: '3333333333333333333333333333333333333333333333333333333333333333', + endpoint: { host: 'nw-mock-03.example.invalid', port: 7900 }, + metadata: { name: '!mock-nw-03', roles: 'Peer,Voting' }, }, { - publicKey: '26BEC23EF633936BAB5E501F03E0C374036F5FF20AC068972839357851411496', - endpoint: { host: 'iroha-symbolnode.com', port: 7900 }, - metadata: { name: '168nihoheto_VDS_S', roles: 'Peer,Api' }, + publicKey: '5555555555555555555555555555555555555555555555555555555555555555', + endpoint: { host: 'nw-mock-05.example.invalid', port: 7900 }, + metadata: { name: '!mock-nw-05', roles: 'Peer,Api' }, }, { - publicKey: 'C4348215B4C417D3E4B52ACAA3D370D29DE3A5F482CAED3C9F1BE257DD2B4079', - endpoint: { host: 'dual-101.testnet.symbol.dev', port: 7900 }, - metadata: { name: 'dual-101', roles: 'Peer,Api' }, + publicKey: '6666666666666666666666666666666666666666666666666666666666666666', + endpoint: { host: 'nw-mock-06.example.invalid', port: 7900 }, + metadata: { name: '!mock-nw-06', roles: 'Peer,Api' }, }, { - publicKey: 'DC7A90D0676DB3A2D963768276F606AF76541A59588B23C6C6B48D98E0AC3837', - endpoint: { host: 'peer-301.testnet.symbol.dev', port: 7900 }, - metadata: { name: 'peer-301', roles: 'Peer' }, + publicKey: '7777777777777777777777777777777777777777777777777777777777777777', + endpoint: { host: 'nw-mock-peer-07.example.invalid', port: 7900 }, + metadata: { name: '!mock-nw-peer-07', roles: 'Peer,Voting' }, }, { - publicKey: '97A7D1E1889803D4A5E3F372530EB555C495B23012807E3E94EF15A2205BC3A6', - endpoint: { host: 'sym-test-02.opening-line.jp', port: 7900 }, - metadata: { name: 'sym-test-02.opening-line.jp', roles: 'Peer,Api' }, + publicKey: '8888888888888888888888888888888888888888888888888888888888888888', + endpoint: { host: 'nw-mock-peer-08.example.invalid', port: 7900 }, + metadata: { name: '!mock-nw-peer-08', roles: 'Peer' }, }, ]); }); - it('getPeerInfos offline', async () => { - stub(RemoteNodeService.prototype, 'createNodeApiRestClient').callsFake(() => { - return { - getNodes(filter: NodeFilter, limit: number) { - expect(filter).eq(presetData.statisticsServicePeerFilter); - expect(limit).eq(presetData.statisticsServicePeerLimit); - return list; - }, - } as unknown as NodeApi; - }); + it('getPeerInfos offline returns only preset knownPeers', async () => { const service = new RemoteNodeService(logger, presetData, true); const peerInfos = await service.getPeerInfos(); - expect(peerInfos).deep.eq([ + expect(peerInfos).to.deep.equal([ { publicKey: 'AAAAE7EAEEAE61EF0C50B4D05931F4325F69081B1B074D31E094C4B21E8CFB3D', endpoint: { host: 'someStaticPeer', port: 7900 }, @@ -553,25 +224,19 @@ describe('RemoteNodeService', () => { }, ]); }); - const assertPeersOnInvalidUrl = async (statisticsServiceUrl: string) => { - presetData.statisticsServiceUrl = statisticsServiceUrl; + + it('getPeerInfos returns only preset knownPeers when NodeWatch fails', async () => { + stub(RemoteNodeService.prototype, 'getNodeWatchNodes').rejects(new Error('node watch unavailable')); + const service = new RemoteNodeService(logger, presetData, false); const peerInfos = await service.getPeerInfos(); - // only static nodes are returned when the statistics service client fails - expect(peerInfos).deep.eq([ + + expect(peerInfos).to.deep.equal([ { publicKey: 'AAAAE7EAEEAE61EF0C50B4D05931F4325F69081B1B074D31E094C4B21E8CFB3D', endpoint: { host: 'someStaticPeer', port: 7900 }, metadata: { name: 'someStaticPeer', roles: 'Peer,Api' }, }, ]); - }; - - it('getPeerInfos unknown statisticsServiceUrl', async () => { - await assertPeersOnInvalidUrl('https://testnet.symbol.invalid'); - }); - - it('getPeerInfos invalid statisticsServiceUrl path', async () => { - await assertPeersOnInvalidUrl('https://testnet.symbol.services/invalid'); }); });