edge-currency-plugins 3.15.0 → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/assets/edge-currency-plugins/edge-currency-plugins.js +48 -48
- package/lib/common/plugin/EngineEmitter.js +10 -0
- package/lib/common/plugin/PluginState.js +87 -1
- package/lib/common/utxobased/engine/ServerStates.js +240 -8
- package/lib/common/utxobased/engine/UtxoEngineProcessor.js +112 -22
- package/lib/common/utxobased/network/Blockbook.js +6 -1
- package/lib/common/utxobased/network/Socket.js +8 -2
- package/lib/src/common/plugin/EngineEmitter.d.ts +7 -2
- package/lib/src/common/plugin/PluginState.d.ts +30 -0
- package/lib/src/common/utxobased/engine/ServerStates.d.ts +31 -0
- package/lib/src/common/utxobased/engine/UtxoEngineProcessor.d.ts +3 -1
- package/lib/src/common/utxobased/network/Blockbook.d.ts +3 -0
- package/lib/src/common/utxobased/network/Socket.d.ts +4 -0
- package/lib/test/common/plugin/PluginState.spec.d.ts +1 -0
- package/lib/test/common/utxobased/engine/AddressRepair.spec.d.ts +1 -0
- package/package.json +1 -1
|
@@ -86,6 +86,11 @@ var _events = require('events');
|
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
89
94
|
|
|
90
95
|
|
|
91
96
|
|
|
@@ -105,6 +110,11 @@ var EngineEvent; (function (EngineEvent) {
|
|
|
105
110
|
const NEW_ADDRESS_TRANSACTION = 'address:transaction:changed'; EngineEvent["NEW_ADDRESS_TRANSACTION"] = NEW_ADDRESS_TRANSACTION;
|
|
106
111
|
const ADDRESSES_CHECKED = 'addresses:checked'; EngineEvent["ADDRESSES_CHECKED"] = ADDRESSES_CHECKED;
|
|
107
112
|
const TXIDS_CHANGED = 'txids:changed'; EngineEvent["TXIDS_CHANGED"] = TXIDS_CHANGED;
|
|
113
|
+
/**
|
|
114
|
+
* The engine disconnected a server on purpose because it was found to be
|
|
115
|
+
* out of sync. Data fetched through it may be behind the chain.
|
|
116
|
+
*/
|
|
117
|
+
const SERVER_DROPPED = 'server:dropped'; EngineEvent["SERVER_DROPPED"] = SERVER_DROPPED;
|
|
108
118
|
const CONNECTION_OPEN = 'connection:open'; EngineEvent["CONNECTION_OPEN"] = CONNECTION_OPEN;
|
|
109
119
|
const CONNECTION_CLOSE = 'connection:close'; EngineEvent["CONNECTION_CLOSE"] = CONNECTION_CLOSE;
|
|
110
120
|
const CONNECTION_TIMER = 'connection:timer'; EngineEvent["CONNECTION_TIMER"] = CONNECTION_TIMER;
|
|
@@ -10,9 +10,48 @@ var _memlet = require('memlet');
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
var _ServerScores = require('./ServerScores');
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* How long a server dropped for being out of sync stays out of the pick
|
|
19
|
+
* order. Long enough for a lagging indexer to catch up, short enough that a
|
|
20
|
+
* recovered server is tried again without an app restart. After expiry the
|
|
21
|
+
* server is probed again on connect and dropped again if still behind.
|
|
22
|
+
*/
|
|
23
|
+
const SERVER_QUARANTINE_MS = 5 * 60 * 1000; exports.SERVER_QUARANTINE_MS = SERVER_QUARANTINE_MS
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Runs a server's health probe, or hands back the one started for the same
|
|
27
|
+
* URI less than `minIntervalMs` ago, whether it is still in flight or has
|
|
28
|
+
* already answered.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
function makeShareServerProbe() {
|
|
37
|
+
// Server URI -> the latest probe and when it started (ms since epoch)
|
|
38
|
+
const latestProbes = new Map
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
()
|
|
42
|
+
|
|
43
|
+
return async (uri, minIntervalMs, probe) => {
|
|
44
|
+
const now = Date.now()
|
|
45
|
+
const latest = latestProbes.get(uri)
|
|
46
|
+
if (latest != null && now - latest.startTime < minIntervalMs) {
|
|
47
|
+
return await latest.result
|
|
48
|
+
}
|
|
49
|
+
const result = probe()
|
|
50
|
+
latestProbes.set(uri, { startTime: now, result })
|
|
51
|
+
return await result
|
|
52
|
+
}
|
|
53
|
+
} exports.makeShareServerProbe = makeShareServerProbe;
|
|
54
|
+
|
|
16
55
|
// The filename for ServerInfoCache data (see ServerScores.ts)
|
|
17
56
|
// Perhaps this should be in ServerScores.ts file, but that'll take some refactoring
|
|
18
57
|
const SERVER_CACHE_FILE = 'serverCache.json'
|
|
@@ -38,6 +77,21 @@ const SERVER_CACHE_FILE = 'serverCache.json'
|
|
|
38
77
|
|
|
39
78
|
|
|
40
79
|
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
41
95
|
|
|
42
96
|
|
|
43
97
|
|
|
@@ -97,6 +151,20 @@ const SERVER_CACHE_FILE = 'serverCache.json'
|
|
|
97
151
|
let engines = []
|
|
98
152
|
const memlet = _memlet.makeMemlet.call(void 0, pluginDisklet)
|
|
99
153
|
|
|
154
|
+
// Server URI -> time (ms since epoch) at which the quarantine expires
|
|
155
|
+
const quarantineExpiry = new Map()
|
|
156
|
+
|
|
157
|
+
const isServerQuarantined = (uri) => {
|
|
158
|
+
const expiry = quarantineExpiry.get(uri)
|
|
159
|
+
if (expiry == null) return false
|
|
160
|
+
if (expiry > Date.now()) return true
|
|
161
|
+
quarantineExpiry.delete(uri)
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const hasQuarantinedServers = () =>
|
|
166
|
+
Array.from(quarantineExpiry.keys()).some(isServerQuarantined)
|
|
167
|
+
|
|
100
168
|
let serverCache = {
|
|
101
169
|
customServers: {},
|
|
102
170
|
enableCustomServers: defaultSettings.enableCustomServers,
|
|
@@ -182,6 +250,9 @@ const SERVER_CACHE_FILE = 'serverCache.json'
|
|
|
182
250
|
|
|
183
251
|
return {
|
|
184
252
|
'pluginState.servers_': sanitizeServerList(selectedServerList),
|
|
253
|
+
quarantinedServers: Array.from(quarantineExpiry.keys())
|
|
254
|
+
.filter(isServerQuarantined)
|
|
255
|
+
.map(sanitizeServerUri),
|
|
185
256
|
infoServers,
|
|
186
257
|
customServers: Object.keys(serverCache.customServers).map(
|
|
187
258
|
sanitizeServerUri
|
|
@@ -213,6 +284,15 @@ const SERVER_CACHE_FILE = 'serverCache.json'
|
|
|
213
284
|
serverScores.serverScoreUp(getSelectedServerList(), uri, score)
|
|
214
285
|
},
|
|
215
286
|
|
|
287
|
+
quarantineServer(uri, durationMs = exports.SERVER_QUARANTINE_MS) {
|
|
288
|
+
log.warn(`${pluginId} - quarantining ${uri} for ${durationMs}ms`)
|
|
289
|
+
quarantineExpiry.set(uri, Date.now() + durationMs)
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
isServerQuarantined,
|
|
293
|
+
hasQuarantinedServers,
|
|
294
|
+
shareServerProbe: makeShareServerProbe(),
|
|
295
|
+
|
|
216
296
|
async clearCache() {
|
|
217
297
|
serverScores.clearServerScoreTimes()
|
|
218
298
|
serverCacheDirty = true
|
|
@@ -223,8 +303,14 @@ const SERVER_CACHE_FILE = 'serverCache.json'
|
|
|
223
303
|
numServersWanted,
|
|
224
304
|
includePatterns = []
|
|
225
305
|
) {
|
|
306
|
+
// Withhold quarantined servers before ranking, so they cannot take
|
|
307
|
+
// slots that lower-ranked healthy servers would otherwise fill:
|
|
308
|
+
const eligibleServers = {}
|
|
309
|
+
for (const [uri, serverInfo] of Object.entries(getSelectedServerList())) {
|
|
310
|
+
if (!isServerQuarantined(uri)) eligibleServers[uri] = serverInfo
|
|
311
|
+
}
|
|
226
312
|
return serverScores.getServers(
|
|
227
|
-
|
|
313
|
+
eligibleServers,
|
|
228
314
|
numServersWanted,
|
|
229
315
|
includePatterns
|
|
230
316
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _cleaners = require('cleaners');
|
|
2
|
+
|
|
2
3
|
var _urijs = require('uri-js');
|
|
3
4
|
|
|
4
5
|
var _EngineEmitter = require('../../plugin/EngineEmitter');
|
|
@@ -23,6 +24,26 @@ var _socketQueue = require('../network/socketQueue');
|
|
|
23
24
|
var _constants = require('./constants');
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* What the last REST health probe said about a connected server:
|
|
29
|
+
* - `pending`: the probe has not answered yet.
|
|
30
|
+
* - `healthy`: Blockbook reported `inSync: true`.
|
|
31
|
+
* - `unhealthy`: Blockbook reported `inSync: false`.
|
|
32
|
+
* - `unknown`: the server has no HTTP twin to probe, or the probe failed
|
|
33
|
+
* (network error, non-200, unparseable body). Never treated as unhealthy.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
26
47
|
|
|
27
48
|
|
|
28
49
|
|
|
@@ -115,6 +136,33 @@ var _constants = require('./constants');
|
|
|
115
136
|
*/
|
|
116
137
|
const BROADCAST_ATTEMPT_TIMEOUT_MS = 30000; exports.BROADCAST_ATTEMPT_TIMEOUT_MS = BROADCAST_ATTEMPT_TIMEOUT_MS
|
|
117
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Upper bound on a REST health probe. The probe runs off the socket keepalive
|
|
141
|
+
* and must never hold the connection's timer for long.
|
|
142
|
+
*/
|
|
143
|
+
const HEALTH_PROBE_TIMEOUT_MS = 10000; exports.HEALTH_PROBE_TIMEOUT_MS = HEALTH_PROBE_TIMEOUT_MS
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Probes of one server closer together than this share a single request,
|
|
147
|
+
* across every wallet of the plugin. The connect path and the first
|
|
148
|
+
* keepalive can land within milliseconds of each other, and N wallets each
|
|
149
|
+
* run their own keepalive; one answer serves them all.
|
|
150
|
+
*/
|
|
151
|
+
const HEALTH_PROBE_MIN_INTERVAL_MS = 10000; exports.HEALTH_PROBE_MIN_INTERVAL_MS = HEALTH_PROBE_MIN_INTERVAL_MS
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The part of Blockbook's REST `GET /api/` response the health probe reads.
|
|
155
|
+
* The WebSocket `getInfo` message does not carry `inSync`, so the probe has
|
|
156
|
+
* to go over HTTP. Blockbook itself masks the first five seconds of every
|
|
157
|
+
* resync, so a `false` here means the indexer has been behind its backend
|
|
158
|
+
* for longer than that, or the backend RPC is failing.
|
|
159
|
+
*/
|
|
160
|
+
const asBlockbookHealth = _cleaners.asObject.call(void 0, {
|
|
161
|
+
blockbook: _cleaners.asObject.call(void 0, {
|
|
162
|
+
inSync: _cleaners.asBoolean
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
118
166
|
const withTimeout = async (
|
|
119
167
|
promise,
|
|
120
168
|
ms,
|
|
@@ -145,9 +193,146 @@ const withTimeout = async (
|
|
|
145
193
|
} = config
|
|
146
194
|
const { serverConfigs = [] } = pluginInfo.engineInfo
|
|
147
195
|
const { broadcastTimeoutMs = exports.BROADCAST_ATTEMPT_TIMEOUT_MS } = config
|
|
196
|
+
const {
|
|
197
|
+
healthProbeMinIntervalMs = exports.HEALTH_PROBE_MIN_INTERVAL_MS,
|
|
198
|
+
healthProbeTimeoutMs = exports.HEALTH_PROBE_TIMEOUT_MS,
|
|
199
|
+
keepAliveMs,
|
|
200
|
+
wakeUpMs
|
|
201
|
+
} = config
|
|
148
202
|
log('Making server states')
|
|
149
203
|
|
|
150
204
|
const serverStatesCache = {}
|
|
205
|
+
|
|
206
|
+
// Edge's own Blockbook servers expose both a socket and a REST endpoint on
|
|
207
|
+
// the same host. Index the REST URIs by host so a socket URI can find its
|
|
208
|
+
// twin for the health probe. Third-party sockets (Trezor, NOWNodes) have no
|
|
209
|
+
// twin here and are never probed.
|
|
210
|
+
const httpUriByHost = {}
|
|
211
|
+
for (const serverConfig of serverConfigs) {
|
|
212
|
+
if (serverConfig.type !== 'blockbook') continue
|
|
213
|
+
for (const httpUri of serverConfig.uris) {
|
|
214
|
+
const { host } = _urijs.parse.call(void 0, httpUri)
|
|
215
|
+
if (host != null) httpUriByHost[host] = httpUri
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const findHttpTwin = (uri) => {
|
|
220
|
+
const { host } = _urijs.parse.call(void 0, uri)
|
|
221
|
+
return host == null ? undefined : httpUriByHost[host]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const probeServerHealth = async (uri) => {
|
|
225
|
+
const httpUri = findHttpTwin(uri)
|
|
226
|
+
if (httpUri == null) return 'unknown'
|
|
227
|
+
const probe = async () => {
|
|
228
|
+
const response = await io.fetchCors(`${httpUri}/api/`, { method: 'GET' })
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
log.warn(`${uri} health probe: HTTP ${response.status}`)
|
|
231
|
+
return 'unknown'
|
|
232
|
+
}
|
|
233
|
+
const { blockbook } = asBlockbookHealth(await response.json())
|
|
234
|
+
return blockbook.inSync ? 'healthy' : 'unhealthy'
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
// The bound covers the body too; a server can send its headers and
|
|
238
|
+
// then stall:
|
|
239
|
+
return await withTimeout(
|
|
240
|
+
probe(),
|
|
241
|
+
healthProbeTimeoutMs,
|
|
242
|
+
`Timeout for health probe to ${httpUri}`
|
|
243
|
+
)
|
|
244
|
+
} catch (error) {
|
|
245
|
+
log.warn(`${uri} health probe failed: ${String(error)}`)
|
|
246
|
+
return 'unknown'
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Disconnects a server on purpose. On platforms that deliver a close event
|
|
252
|
+
* for a locally closed socket (the app's WebView does) the socket's close
|
|
253
|
+
* path also runs, scoring the server down and throwing its in-flight
|
|
254
|
+
* requests. The refill is scheduled here regardless, so replacing the
|
|
255
|
+
* connection never depends on that event arriving.
|
|
256
|
+
*/
|
|
257
|
+
const dropServer = (uri, reason) => {
|
|
258
|
+
const serverState = serverStatesCache[uri]
|
|
259
|
+
if (serverState == null) return
|
|
260
|
+
log.warn(`${uri} dropped: ${reason}`)
|
|
261
|
+
// Keep the refill from picking it straight back; its score is still
|
|
262
|
+
// near the top since it answered every ping:
|
|
263
|
+
pluginState.quarantineServer(uri)
|
|
264
|
+
_utils.removeItem.call(void 0, serverStatesCache, uri)
|
|
265
|
+
serverState.blockbook.disconnect().catch((error) => {
|
|
266
|
+
log.error(`${uri} disconnect failed: ${String(error)}`)
|
|
267
|
+
})
|
|
268
|
+
reconnect()
|
|
269
|
+
// Let the engine re-query anything this server answered:
|
|
270
|
+
engineEmitter.emit(_EngineEmitter.EngineEvent.SERVER_DROPPED, uri)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Whether some other connection could carry the wallet if `uri` went
|
|
275
|
+
* away: open, and not known to be out of sync. A peer whose probe has not
|
|
276
|
+
* answered yet does not count; it may be about to fail the same way.
|
|
277
|
+
*/
|
|
278
|
+
const hasUsablePeer = (uri) =>
|
|
279
|
+
Object.entries(serverStatesCache).some(
|
|
280
|
+
([otherUri, state]) =>
|
|
281
|
+
otherUri !== uri &&
|
|
282
|
+
state.blockbook.isConnected &&
|
|
283
|
+
(state.health === 'healthy' || state.health === 'unknown')
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Drops every server known to be out of sync, as long as the wallet keeps
|
|
288
|
+
* at least one usable connection. A stale server that is the only leg is
|
|
289
|
+
* kept: behind the chain beats no data at all. It is re-evaluated on every
|
|
290
|
+
* probe result, so it goes as soon as a usable peer shows up.
|
|
291
|
+
*
|
|
292
|
+
* Only one stale leg is kept. Two stale servers would otherwise keep each
|
|
293
|
+
* other, filling every connection slot so the refill never reaches a
|
|
294
|
+
* server that could replace them.
|
|
295
|
+
*/
|
|
296
|
+
const dropUnhealthyServers = () => {
|
|
297
|
+
let isStaleLegKept = false
|
|
298
|
+
for (const [uri, state] of Object.entries(serverStatesCache)) {
|
|
299
|
+
if (state.health !== 'unhealthy') continue
|
|
300
|
+
if (!hasUsablePeer(uri) && !isStaleLegKept) {
|
|
301
|
+
log.warn(
|
|
302
|
+
`${uri} reports inSync: false but is the only usable connection`
|
|
303
|
+
)
|
|
304
|
+
isStaleLegKept = true
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
dropServer(uri, 'Blockbook reports inSync: false')
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const checkServerHealth = (uri) => {
|
|
312
|
+
const serverState = serverStatesCache[uri]
|
|
313
|
+
if (serverState == null) return
|
|
314
|
+
pluginState
|
|
315
|
+
.shareServerProbe(
|
|
316
|
+
uri,
|
|
317
|
+
healthProbeMinIntervalMs,
|
|
318
|
+
async () => await probeServerHealth(uri)
|
|
319
|
+
)
|
|
320
|
+
.then(health => {
|
|
321
|
+
const serverState = serverStatesCache[uri]
|
|
322
|
+
// The connection may have gone away while the probe was in flight:
|
|
323
|
+
if (serverState == null) return
|
|
324
|
+
if (serverState.health !== health) {
|
|
325
|
+
log(`${uri} health: ${serverState.health} -> ${health}`)
|
|
326
|
+
}
|
|
327
|
+
serverState.health = health
|
|
328
|
+
// This result may make this server droppable, or make it the peer
|
|
329
|
+
// that lets a previously kept stale server go:
|
|
330
|
+
dropUnhealthyServers()
|
|
331
|
+
})
|
|
332
|
+
.catch((error) => {
|
|
333
|
+
log.error(`${uri} health check failed: ${String(error)}`)
|
|
334
|
+
})
|
|
335
|
+
}
|
|
151
336
|
let isEngineOn = true
|
|
152
337
|
let serverList = []
|
|
153
338
|
let reconnectCounter = 0
|
|
@@ -181,6 +366,8 @@ const withTimeout = async (
|
|
|
181
366
|
const queryTime = Date.now() - queryDate
|
|
182
367
|
log(`${uri} returned healthCheck in ${queryTime}ms`)
|
|
183
368
|
pluginState.serverScoreUp(uri, queryTime)
|
|
369
|
+
// Periodic probe, once per keepalive:
|
|
370
|
+
checkServerHealth(uri)
|
|
184
371
|
}
|
|
185
372
|
)
|
|
186
373
|
engineEmitter.on(
|
|
@@ -199,7 +386,11 @@ const withTimeout = async (
|
|
|
199
386
|
_EngineEmitter.EngineEvent.BLOCK_HEIGHT_CHANGED,
|
|
200
387
|
(uri, blockHeight) => {
|
|
201
388
|
log(`${uri} block height changed to ${blockHeight}`)
|
|
202
|
-
serverStatesCache[uri]
|
|
389
|
+
const serverState = serverStatesCache[uri]
|
|
390
|
+
// The connection may already be gone, as the sibling handler above
|
|
391
|
+
// allows for:
|
|
392
|
+
if (serverState == null) return
|
|
393
|
+
serverState.blockHeight = blockHeight
|
|
203
394
|
}
|
|
204
395
|
)
|
|
205
396
|
|
|
@@ -207,18 +398,26 @@ const withTimeout = async (
|
|
|
207
398
|
|
|
208
399
|
|
|
209
400
|
|
|
210
|
-
const makeServerStatesCacheEntry = (
|
|
401
|
+
const makeServerStatesCacheEntry = (
|
|
402
|
+
uri,
|
|
403
|
+
blockbook
|
|
404
|
+
) => ({
|
|
211
405
|
blockbook,
|
|
212
406
|
blockSubscriptionStatus: 'unsubscribed',
|
|
213
407
|
txids: new Set(),
|
|
214
408
|
addresses: new Set(),
|
|
215
|
-
blockHeight: 0
|
|
409
|
+
blockHeight: 0,
|
|
410
|
+
health: findHttpTwin(uri) == null ? 'unknown' : 'pending'
|
|
216
411
|
})
|
|
217
412
|
|
|
218
413
|
const reconnect = () => {
|
|
219
414
|
if (isEngineOn) {
|
|
220
415
|
log(`attempting server reconnect number ${reconnectCounter}`)
|
|
221
416
|
const reconnectionDelay = Math.max(5, reconnectCounter++) * 1000
|
|
417
|
+
// One pending reconnect at a time. A drop and the close event it
|
|
418
|
+
// causes both land here, and a timer that was overwritten rather than
|
|
419
|
+
// cleared would outlive stop() and switch the engine back on.
|
|
420
|
+
clearTimeout(reconnectTimer)
|
|
222
421
|
reconnectTimer = setTimeout(() => {
|
|
223
422
|
clearTimeout(reconnectTimer)
|
|
224
423
|
instance.refillServers()
|
|
@@ -244,6 +443,9 @@ const withTimeout = async (
|
|
|
244
443
|
// Skip reconnecting to an existing connection
|
|
245
444
|
if (serverStatesCache[uri] != null) continue
|
|
246
445
|
|
|
446
|
+
// The list may predate a quarantine:
|
|
447
|
+
if (pluginState.isServerQuarantined(uri)) continue
|
|
448
|
+
|
|
247
449
|
// Validate the URI of server to make sure it is valid
|
|
248
450
|
const parsed = _urijs.parse.call(void 0, uri)
|
|
249
451
|
if (
|
|
@@ -254,7 +456,7 @@ const withTimeout = async (
|
|
|
254
456
|
continue
|
|
255
457
|
}
|
|
256
458
|
|
|
257
|
-
//
|
|
459
|
+
// Randomize the URI picking
|
|
258
460
|
chanceToBePicked -= chanceToBePicked > 0.5 ? 0.25 : 0
|
|
259
461
|
if (Math.random() > chanceToBePicked) {
|
|
260
462
|
serverList.push(uri)
|
|
@@ -331,17 +533,25 @@ const withTimeout = async (
|
|
|
331
533
|
log,
|
|
332
534
|
taskGeneratorFn,
|
|
333
535
|
socketEmitter,
|
|
334
|
-
walletId: walletInfo.id
|
|
536
|
+
walletId: walletInfo.id,
|
|
537
|
+
keepAliveMs,
|
|
538
|
+
wakeUpMs
|
|
335
539
|
})
|
|
336
540
|
}
|
|
337
541
|
|
|
338
542
|
// Make new ServerStates instance
|
|
339
|
-
serverStatesCache[uri] = makeServerStatesCacheEntry(blockbook)
|
|
543
|
+
serverStatesCache[uri] = makeServerStatesCacheEntry(uri, blockbook)
|
|
340
544
|
|
|
341
545
|
// Initialize blockbook connection for server
|
|
342
546
|
blockbook
|
|
343
547
|
.connect()
|
|
344
548
|
.then(async () => {
|
|
549
|
+
// This connection is now open and counts as a usable peer, which
|
|
550
|
+
// may be what a stale server kept for lack of one was waiting on.
|
|
551
|
+
// Done here rather than on the socket's open event, which fires
|
|
552
|
+
// before the connect promise marks the blockbook connected:
|
|
553
|
+
dropUnhealthyServers()
|
|
554
|
+
|
|
345
555
|
// Fetch block height from blockbook server
|
|
346
556
|
const startTime = Date.now()
|
|
347
557
|
const { bestHeight: blockHeight } = await blockbook.fetchInfo()
|
|
@@ -356,11 +566,29 @@ const withTimeout = async (
|
|
|
356
566
|
// Increment server score using response time
|
|
357
567
|
const responseTime = Date.now() - startTime
|
|
358
568
|
pluginState.serverScoreUp(uri, responseTime)
|
|
569
|
+
|
|
570
|
+
// On-connect probe. The keepalive may or may not have fired by
|
|
571
|
+
// now, depending on how fast the socket opened, so this is the
|
|
572
|
+
// one place that guarantees a probe before the server does work:
|
|
573
|
+
checkServerHealth(uri)
|
|
359
574
|
})
|
|
360
575
|
.catch(e => {
|
|
361
576
|
log.error(`${JSON.stringify(e.message)}`)
|
|
362
577
|
})
|
|
363
578
|
}
|
|
579
|
+
|
|
580
|
+
// A quarantined server is withheld from the candidate list, so a wallet
|
|
581
|
+
// left short of connections would otherwise stay that way until an
|
|
582
|
+
// unrelated close event. Keep trying while a quarantine is in force; the
|
|
583
|
+
// server comes back into the list when it expires. The reconnect delay
|
|
584
|
+
// grows with each empty pass, so this does not spin.
|
|
585
|
+
if (
|
|
586
|
+
Object.keys(serverStatesCache).length < _constants.MAX_CONNECTIONS &&
|
|
587
|
+
serverList.length === 0 &&
|
|
588
|
+
pluginState.hasQuarantinedServers()
|
|
589
|
+
) {
|
|
590
|
+
reconnect()
|
|
591
|
+
}
|
|
364
592
|
}
|
|
365
593
|
|
|
366
594
|
const deferredWithServerScoring = (
|
|
@@ -581,6 +809,10 @@ const withTimeout = async (
|
|
|
581
809
|
return serverStatesCache[uri].blockHeight
|
|
582
810
|
},
|
|
583
811
|
|
|
812
|
+
getServerHealth(uri) {
|
|
813
|
+
return _optionalChain([serverStatesCache, 'access', _2 => _2[uri], 'optionalAccess', _3 => _3.health])
|
|
814
|
+
},
|
|
815
|
+
|
|
584
816
|
getServerList() {
|
|
585
817
|
return serverList
|
|
586
818
|
},
|
|
@@ -674,7 +906,7 @@ const withTimeout = async (
|
|
|
674
906
|
serverState.addresses.delete(address)
|
|
675
907
|
}
|
|
676
908
|
// Reject the deferredAddressSub promise if provided
|
|
677
|
-
_optionalChain([deferredAddressSub, 'optionalAccess',
|
|
909
|
+
_optionalChain([deferredAddressSub, 'optionalAccess', _4 => _4.reject, 'call', _5 => _5(err)])
|
|
678
910
|
})
|
|
679
911
|
|
|
680
912
|
blockbook.watchAddresses(
|