-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.js
366 lines (318 loc) · 12.7 KB
/
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import Fastify from 'fastify';
import ky from 'ky';
import dns from 'dns/promises';
import geoip from 'geoip-lite';
import { checkHyperionHealth } from './nodes/hyperion.js';
import { checkAtomicHealth } from './nodes/atomic.js';
import { checkLightApiHealth } from './nodes/lightapi.js';
import { checkIpfsHealth } from './nodes/ipfs.js';
const fastify = Fastify({ logger: true });
const PORT = process.env.PORT || 3000;
const HEALTH_CHECK_INTERVAL = process.env.HEALTH_CHECK_INTERVAL || 520000; // Default 520 seconds
const TIMEOUT_DURATION = process.env.TIMEOUT_DURATION || 10000;
let nextHealthCheckTime = Date.now() + HEALTH_CHECK_INTERVAL;
const NODE_LIST_REFRESH_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
const API_URL = 'api.sengine.co';
// Add this constant near the top of the file, with other constants
const API_VERSION = '1.0.2';
// Add this function to log the countdown
const logCountdown = () => {
const now = Date.now();
const timeRemaining = Math.max(0, nextHealthCheckTime - now);
const secondsRemaining = Math.ceil(timeRemaining / 1000);
console.log(`Time until next health check: ${secondsRemaining} seconds`);
};
// In-memory list of healthy nodes for mainnet and testnet
let healthyNodes = {
hyperion: {
mainnet: [],
testnet: []
},
atomic: {
mainnet: [],
testnet: []
},
lightapi: {
mainnet: [],
testnet: []
},
ipfs: {
mainnet: [],
testnet: []
}
};
let hyperionMainnetNodes = [];
let hyperionTestnetNodes = [];
let atomicMainnetNodes = [];
let atomicTestnetNodes = [];
let lightApiMainnetNodes = [];
let lightApiTestnetNodes = [];
let ipfsMainnetNodes = [];
let ipfsTestnetNodes = [];
// Fetch list of nodes from custom URLs
const fetchNodeList = async () => {
try {
const fetchWithFallback = async (url) => {
try {
return await ky.get(`https://${url}`).json();
} catch (error) {
console.log(`HTTPS request failed for ${url}, falling back to HTTP`);
return await ky.get(`http://${url}`).json();
}
};
// Fetch Hyperion nodes
const hyperionNodeList = await fetchWithFallback(`${API_URL}/nodes/hyperion`);
hyperionMainnetNodes = hyperionNodeList
.filter(node => node.network === 'mainnet')
.map(node => ({
url: node.https_node_url,
historyfull: node.historyfull
}));
hyperionTestnetNodes = hyperionNodeList
.filter(node => node.network === 'testnet')
.map(node => ({
url: node.https_node_url,
historyfull: node.historyfull
}));
// Fetch Atomic nodes
const atomicNodeList = await fetchWithFallback(`${API_URL}/nodes/atomic`);
atomicMainnetNodes = atomicNodeList.filter(node => node.network === 'mainnet').map(node => ({ url: node.https_node_url }));
atomicTestnetNodes = atomicNodeList.filter(node => node.network === 'testnet').map(node => ({ url: node.https_node_url }));
// Fetch Light API nodes
const lightApiNodeList = await fetchWithFallback(`${API_URL}/nodes/light-api`);
lightApiMainnetNodes = lightApiNodeList
.filter(node => node.network === 'mainnet')
.map(node => ({ url: node.https_node_url }));
lightApiTestnetNodes = lightApiNodeList
.filter(node => node.network === 'testnet')
.map(node => ({ url: node.https_node_url }));
// Fetch IPFS nodes
const ipfsNodeList = await fetchWithFallback(`${API_URL}/nodes/ipfs`);
ipfsMainnetNodes = ipfsNodeList
.filter(node => node.network === 'mainnet')
.map(node => ({ url: node.https_node_url }));
ipfsTestnetNodes = ipfsNodeList
.filter(node => node.network === 'testnet')
.map(node => ({ url: node.https_node_url }));
fastify.log.info('Node list updated.');
} catch (error) {
fastify.log.error('Failed to fetch node list:', error);
}
};
// Fetch the head block from nodes and get the latest one
const fetchLatestHeadBlock = async (nodes) => {
try {
const headBlocks = await Promise.all(nodes.slice(0, 3).map(async (node) => {
try {
const response = await ky.get(`${node.url}/v1/chain/get_info`, { timeout: TIMEOUT_DURATION }).json();
console.log(`Head block number for node ${node.url}: ${response.head_block_num}`);
return response.head_block_num;
} catch (error) {
fastify.log.error(`Failed to fetch head block from ${node.url}:`, error.message);
return null;
}
}));
const latestHeadBlock = Math.max(...headBlocks.filter(Boolean));
console.log(`Latest head block number selected: ${latestHeadBlock}`);
return latestHeadBlock;
} catch (error) {
fastify.log.error('Failed to fetch latest head block:', error.message);
return null;
}
};
// Update health checks for all mainnet nodes
const updateHealthChecks = async () => {
if (!hyperionMainnetNodes.length || !atomicMainnetNodes.length ||
!hyperionTestnetNodes.length || !atomicTestnetNodes.length ||
!lightApiMainnetNodes.length || !lightApiTestnetNodes.length ||
!ipfsMainnetNodes.length || !ipfsTestnetNodes.length) {
fastify.log.warn('Node list is empty. Fetching node list...');
await fetchNodeList();
}
const healthyHyperionMainnetNodes = [];
const healthyHyperionTestnetNodes = [];
const healthyAtomicMainnetNodes = [];
const healthyAtomicTestnetNodes = [];
const healthyLightApiMainnetNodes = [];
const healthyLightApiTestnetNodes = [];
const healthyIpfsMainnetNodes = [];
const healthyIpfsTestnetNodes = [];
for (const node of hyperionMainnetNodes) {
const isHealthy = await checkHyperionHealth(node, TIMEOUT_DURATION);
if (isHealthy) {
healthyHyperionMainnetNodes.push(node);
}
}
for (const node of hyperionTestnetNodes) {
const isHealthy = await checkHyperionHealth(node, TIMEOUT_DURATION);
if (isHealthy) {
healthyHyperionTestnetNodes.push(node);
}
}
for (const node of atomicMainnetNodes) {
const isHealthy = await checkAtomicHealth(node);
if (isHealthy) {
healthyAtomicMainnetNodes.push(node);
}
}
for (const node of atomicTestnetNodes) {
const isHealthy = await checkAtomicHealth(node);
if (isHealthy) {
healthyAtomicTestnetNodes.push(node);
}
}
// Light API health checks
for (const node of lightApiMainnetNodes) {
const isHealthy = await checkLightApiHealth(node, TIMEOUT_DURATION);
if (isHealthy) {
healthyLightApiMainnetNodes.push(node);
}
}
for (const node of lightApiTestnetNodes) {
const isHealthy = await checkLightApiHealth(node, TIMEOUT_DURATION);
if (isHealthy) {
healthyLightApiTestnetNodes.push(node);
}
}
// IPFS health checks
for (const node of ipfsMainnetNodes) {
const isHealthy = await checkIpfsHealth(node, TIMEOUT_DURATION);
if (isHealthy) {
healthyIpfsMainnetNodes.push(node);
}
}
for (const node of ipfsTestnetNodes) {
const isHealthy = await checkIpfsHealth(node, TIMEOUT_DURATION);
if (isHealthy) {
healthyIpfsTestnetNodes.push(node);
}
}
healthyNodes.hyperion.mainnet = healthyHyperionMainnetNodes;
healthyNodes.hyperion.testnet = healthyHyperionTestnetNodes;
healthyNodes.atomic.mainnet = healthyAtomicMainnetNodes;
healthyNodes.atomic.testnet = healthyAtomicTestnetNodes;
healthyNodes.lightapi.mainnet = healthyLightApiMainnetNodes;
healthyNodes.lightapi.testnet = healthyLightApiTestnetNodes;
healthyNodes.ipfs.mainnet = healthyIpfsMainnetNodes;
healthyNodes.ipfs.testnet = healthyIpfsTestnetNodes;
fastify.log.info(`Health check completed. Healthy Hyperion nodes: mainnet ${healthyHyperionMainnetNodes.length}, testnet ${healthyHyperionTestnetNodes.length}`);
fastify.log.info(`Healthy Atomic nodes: mainnet ${healthyAtomicMainnetNodes.length}, testnet ${healthyAtomicTestnetNodes.length}`);
fastify.log.info(`Healthy Light API nodes: mainnet ${healthyLightApiMainnetNodes.length}, testnet ${healthyLightApiTestnetNodes.length}`);
fastify.log.info(`Healthy IPFS nodes: mainnet ${healthyIpfsMainnetNodes.length}, testnet ${healthyIpfsTestnetNodes.length}`);
console.log('Updated healthyNodes:', JSON.stringify(healthyNodes, null, 2));
nextHealthCheckTime = Date.now() + HEALTH_CHECK_INTERVAL;
console.log(`Next health check scheduled for: ${new Date(nextHealthCheckTime).toISOString()}`);
};
// Set up the interval for health checks
setInterval(async () => {
await updateHealthChecks();
}, HEALTH_CHECK_INTERVAL);
// Set up a more frequent interval for logging the countdown
setInterval(logCountdown, 10000); // Log every 10 seconds
// Initial health check
updateHealthChecks();
// Schedule daily node list refresh for both mainnet and testnet
setInterval(async () => {
await fetchNodeList();
await updateHealthChecks(); // Perform health checks after refreshing the node list
}, NODE_LIST_REFRESH_INTERVAL);
fetchNodeList(); // Initial fetch
// Route to get healthy nodes based on type, network, and geolocation
fastify.get('/nodes', (request, reply) => {
const {
type = 'hyperion',
network = 'mainnet',
count = 3,
historyfull = 'true',
streaming = 'true',
atomicassets = 'true',
atomicmarket = 'true'
} = request.query;
const ip = request.ip;
const userGeo = geoip.lookup(ip) || {};
const userRegion = userGeo.region || '';
const userCountry = userGeo.country || '';
console.log('Request received for /nodes');
console.log('Query parameters:', { type, network, count, historyfull, streaming, atomicassets, atomicmarket });
console.log('User geo:', { region: userRegion, country: userCountry });
let nodesList = healthyNodes[type][network] || [];
console.log(`Initial nodes list for ${type} ${network}:`, nodesList);
// Apply filters
if (type === 'hyperion') {
if (historyfull === 'true') {
nodesList = nodesList.filter(node => node.historyfull === true);
}
// When historyfull is false, we don't filter based on historyfull
if (streaming !== undefined) {
nodesList = nodesList.filter(node => node.streaming?.enable === (streaming === 'true'));
}
} else if (type === 'atomic') {
// Default is both atomicassets and atomicmarket are true
const filterAtomicAssets = atomicassets !== undefined ? (atomicassets === 'true') : true;
const filterAtomicMarket = atomicmarket !== undefined ? (atomicmarket === 'true') : true;
nodesList = nodesList.filter(node => {
if (filterAtomicAssets && filterAtomicMarket) {
return node.atomic.atomicassets && node.atomic.atomicmarket;
} else if (filterAtomicAssets) {
return node.atomic.atomicassets;
} else if (filterAtomicMarket) {
return node.atomic.atomicmarket;
}
return true; // If neither is specified, don't filter
});
}
console.log(`Filtered nodes list:`, nodesList);
if (!nodesList.length) {
console.log(`No healthy ${type} nodes available for ${network} with the specified filters.`);
return reply.status(503).send({ message: `No healthy ${type} nodes available for ${network} with the specified filters.` });
}
// Sort nodes by proximity to user
nodesList.sort((a, b) => {
if (a.country === userCountry && b.country !== userCountry) return -1;
if (b.country === userCountry && a.country !== userCountry) return 1;
if (a.region === userRegion && b.region !== userRegion) return -1;
if (b.region === userRegion && a.region !== userRegion) return 1;
return 0;
});
// Prepare the response
const response = nodesList.slice(0, parseInt(count, 10)).map(node => ({
url: node.url,
region: node.region,
country: node.country,
timezone: node.timezone,
historyfull: node.historyfull,
streaming: node.streaming,
atomic: node.atomic
}));
console.log('Responding with:', response);
reply.send(response);
});
fastify.get('/health', (request, reply) => {
const totalHyperionNodes = healthyNodes.hyperion.mainnet.length + healthyNodes.hyperion.testnet.length;
const totalAtomicNodes = healthyNodes.atomic.mainnet.length + healthyNodes.atomic.testnet.length;
const totalLightApiNodes = healthyNodes.lightapi.mainnet.length + healthyNodes.lightapi.testnet.length;
const totalIpfsNodes = healthyNodes.ipfs.mainnet.length + healthyNodes.ipfs.testnet.length;
const isHealthy = totalHyperionNodes >= 3 && totalAtomicNodes >= 3 &&
totalLightApiNodes >= 3 && totalIpfsNodes >= 3;
reply.send({
version: API_VERSION,
status: isHealthy ? 'healthy' : 'unhealthy',
nodes: {
hyperion: totalHyperionNodes,
atomic: totalAtomicNodes,
lightapi: totalLightApiNodes,
ipfs: totalIpfsNodes
}
});
});
// Start server
const start = async () => {
try {
await fastify.listen({ port: PORT, host: '0.0.0.0' });
fastify.log.info(`Server running at http://localhost:${PORT}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();