edge-currency-plugins 3.15.0 → 3.17.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 +51 -49
- package/lib/common/plugin/EngineEmitter.js +10 -0
- package/lib/common/plugin/PluginState.js +87 -1
- package/lib/common/plugin/types.js +11 -0
- package/lib/common/utxobased/engine/ServerStates.js +257 -9
- package/lib/common/utxobased/engine/UtxoEngineProcessor.js +147 -22
- package/lib/common/utxobased/info/all.js +6 -0
- package/lib/common/utxobased/info/bitcoin.js +1 -1
- package/lib/common/utxobased/info/ecashbitcoin.js +90 -0
- package/lib/common/utxobased/info/ecashbitcoinbetanet.js +91 -0
- package/lib/common/utxobased/keymanager/keymanager.js +6 -1
- package/lib/common/utxobased/network/Blockbook.js +9 -2
- package/lib/common/utxobased/network/BlockbookElectrum.js +243 -77
- package/lib/common/utxobased/network/Socket.js +34 -22
- package/lib/common/utxobased/network/electrumApi.js +49 -1
- package/lib/src/common/plugin/EngineEmitter.d.ts +7 -2
- package/lib/src/common/plugin/PluginState.d.ts +30 -0
- package/lib/src/common/plugin/types.d.ts +10 -0
- package/lib/src/common/utxobased/engine/ServerStates.d.ts +32 -0
- package/lib/src/common/utxobased/engine/UtxoEngineProcessor.d.ts +5 -1
- package/lib/src/common/utxobased/info/all.d.ts +2 -0
- package/lib/src/common/utxobased/info/ecashbitcoin.d.ts +3 -0
- package/lib/src/common/utxobased/info/ecashbitcoinbetanet.d.ts +3 -0
- package/lib/src/common/utxobased/network/Blockbook.d.ts +6 -1
- package/lib/src/common/utxobased/network/BlockbookElectrum.d.ts +2 -0
- package/lib/src/common/utxobased/network/Socket.d.ts +4 -0
- package/lib/src/common/utxobased/network/electrumApi.d.ts +21 -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/lib/test/common/utxobased/engine/UtxoEngineProcessor.spec.d.ts +1 -0
- package/lib/test/common/utxobased/keymanager/coins/ecashbitcointransactiontest.spec.d.ts +1 -0
- package/lib/test/common/utxobased/network/BlockbookElectrum.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');
|
|
@@ -15,6 +16,9 @@ var _Blockbook = require('../network/Blockbook');
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
var _blockbookApi = require('../network/blockbookApi');
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
18
22
|
var _BlockbookElectrum = require('../network/BlockbookElectrum');
|
|
19
23
|
var _Deferred = require('../network/Deferred'); var _Deferred2 = _interopRequireDefault(_Deferred);
|
|
20
24
|
|
|
@@ -23,6 +27,27 @@ var _socketQueue = require('../network/socketQueue');
|
|
|
23
27
|
var _constants = require('./constants');
|
|
24
28
|
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* What the last REST health probe said about a connected server:
|
|
32
|
+
* - `pending`: the probe has not answered yet.
|
|
33
|
+
* - `healthy`: Blockbook reported `inSync: true`.
|
|
34
|
+
* - `unhealthy`: Blockbook reported `inSync: false`.
|
|
35
|
+
* - `unknown`: the server has no HTTP twin to probe, or the probe failed
|
|
36
|
+
* (network error, non-200, unparseable body). Never treated as unhealthy.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
26
51
|
|
|
27
52
|
|
|
28
53
|
|
|
@@ -115,6 +140,33 @@ var _constants = require('./constants');
|
|
|
115
140
|
*/
|
|
116
141
|
const BROADCAST_ATTEMPT_TIMEOUT_MS = 30000; exports.BROADCAST_ATTEMPT_TIMEOUT_MS = BROADCAST_ATTEMPT_TIMEOUT_MS
|
|
117
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Upper bound on a REST health probe. The probe runs off the socket keepalive
|
|
145
|
+
* and must never hold the connection's timer for long.
|
|
146
|
+
*/
|
|
147
|
+
const HEALTH_PROBE_TIMEOUT_MS = 10000; exports.HEALTH_PROBE_TIMEOUT_MS = HEALTH_PROBE_TIMEOUT_MS
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Probes of one server closer together than this share a single request,
|
|
151
|
+
* across every wallet of the plugin. The connect path and the first
|
|
152
|
+
* keepalive can land within milliseconds of each other, and N wallets each
|
|
153
|
+
* run their own keepalive; one answer serves them all.
|
|
154
|
+
*/
|
|
155
|
+
const HEALTH_PROBE_MIN_INTERVAL_MS = 10000; exports.HEALTH_PROBE_MIN_INTERVAL_MS = HEALTH_PROBE_MIN_INTERVAL_MS
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The part of Blockbook's REST `GET /api/` response the health probe reads.
|
|
159
|
+
* The WebSocket `getInfo` message does not carry `inSync`, so the probe has
|
|
160
|
+
* to go over HTTP. Blockbook itself masks the first five seconds of every
|
|
161
|
+
* resync, so a `false` here means the indexer has been behind its backend
|
|
162
|
+
* for longer than that, or the backend RPC is failing.
|
|
163
|
+
*/
|
|
164
|
+
const asBlockbookHealth = _cleaners.asObject.call(void 0, {
|
|
165
|
+
blockbook: _cleaners.asObject.call(void 0, {
|
|
166
|
+
inSync: _cleaners.asBoolean
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
118
170
|
const withTimeout = async (
|
|
119
171
|
promise,
|
|
120
172
|
ms,
|
|
@@ -145,9 +197,146 @@ const withTimeout = async (
|
|
|
145
197
|
} = config
|
|
146
198
|
const { serverConfigs = [] } = pluginInfo.engineInfo
|
|
147
199
|
const { broadcastTimeoutMs = exports.BROADCAST_ATTEMPT_TIMEOUT_MS } = config
|
|
200
|
+
const {
|
|
201
|
+
healthProbeMinIntervalMs = exports.HEALTH_PROBE_MIN_INTERVAL_MS,
|
|
202
|
+
healthProbeTimeoutMs = exports.HEALTH_PROBE_TIMEOUT_MS,
|
|
203
|
+
keepAliveMs,
|
|
204
|
+
wakeUpMs
|
|
205
|
+
} = config
|
|
148
206
|
log('Making server states')
|
|
149
207
|
|
|
150
208
|
const serverStatesCache = {}
|
|
209
|
+
|
|
210
|
+
// Edge's own Blockbook servers expose both a socket and a REST endpoint on
|
|
211
|
+
// the same host. Index the REST URIs by host so a socket URI can find its
|
|
212
|
+
// twin for the health probe. Third-party sockets (Trezor, NOWNodes) have no
|
|
213
|
+
// twin here and are never probed.
|
|
214
|
+
const httpUriByHost = {}
|
|
215
|
+
for (const serverConfig of serverConfigs) {
|
|
216
|
+
if (serverConfig.type !== 'blockbook') continue
|
|
217
|
+
for (const httpUri of serverConfig.uris) {
|
|
218
|
+
const { host } = _urijs.parse.call(void 0, httpUri)
|
|
219
|
+
if (host != null) httpUriByHost[host] = httpUri
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const findHttpTwin = (uri) => {
|
|
224
|
+
const { host } = _urijs.parse.call(void 0, uri)
|
|
225
|
+
return host == null ? undefined : httpUriByHost[host]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const probeServerHealth = async (uri) => {
|
|
229
|
+
const httpUri = findHttpTwin(uri)
|
|
230
|
+
if (httpUri == null) return 'unknown'
|
|
231
|
+
const probe = async () => {
|
|
232
|
+
const response = await io.fetchCors(`${httpUri}/api/`, { method: 'GET' })
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
log.warn(`${uri} health probe: HTTP ${response.status}`)
|
|
235
|
+
return 'unknown'
|
|
236
|
+
}
|
|
237
|
+
const { blockbook } = asBlockbookHealth(await response.json())
|
|
238
|
+
return blockbook.inSync ? 'healthy' : 'unhealthy'
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
// The bound covers the body too; a server can send its headers and
|
|
242
|
+
// then stall:
|
|
243
|
+
return await withTimeout(
|
|
244
|
+
probe(),
|
|
245
|
+
healthProbeTimeoutMs,
|
|
246
|
+
`Timeout for health probe to ${httpUri}`
|
|
247
|
+
)
|
|
248
|
+
} catch (error) {
|
|
249
|
+
log.warn(`${uri} health probe failed: ${String(error)}`)
|
|
250
|
+
return 'unknown'
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Disconnects a server on purpose. On platforms that deliver a close event
|
|
256
|
+
* for a locally closed socket (the app's WebView does) the socket's close
|
|
257
|
+
* path also runs, scoring the server down and throwing its in-flight
|
|
258
|
+
* requests. The refill is scheduled here regardless, so replacing the
|
|
259
|
+
* connection never depends on that event arriving.
|
|
260
|
+
*/
|
|
261
|
+
const dropServer = (uri, reason) => {
|
|
262
|
+
const serverState = serverStatesCache[uri]
|
|
263
|
+
if (serverState == null) return
|
|
264
|
+
log.warn(`${uri} dropped: ${reason}`)
|
|
265
|
+
// Keep the refill from picking it straight back; its score is still
|
|
266
|
+
// near the top since it answered every ping:
|
|
267
|
+
pluginState.quarantineServer(uri)
|
|
268
|
+
_utils.removeItem.call(void 0, serverStatesCache, uri)
|
|
269
|
+
serverState.blockbook.disconnect().catch((error) => {
|
|
270
|
+
log.error(`${uri} disconnect failed: ${String(error)}`)
|
|
271
|
+
})
|
|
272
|
+
reconnect()
|
|
273
|
+
// Let the engine re-query anything this server answered:
|
|
274
|
+
engineEmitter.emit(_EngineEmitter.EngineEvent.SERVER_DROPPED, uri)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Whether some other connection could carry the wallet if `uri` went
|
|
279
|
+
* away: open, and not known to be out of sync. A peer whose probe has not
|
|
280
|
+
* answered yet does not count; it may be about to fail the same way.
|
|
281
|
+
*/
|
|
282
|
+
const hasUsablePeer = (uri) =>
|
|
283
|
+
Object.entries(serverStatesCache).some(
|
|
284
|
+
([otherUri, state]) =>
|
|
285
|
+
otherUri !== uri &&
|
|
286
|
+
state.blockbook.isConnected &&
|
|
287
|
+
(state.health === 'healthy' || state.health === 'unknown')
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Drops every server known to be out of sync, as long as the wallet keeps
|
|
292
|
+
* at least one usable connection. A stale server that is the only leg is
|
|
293
|
+
* kept: behind the chain beats no data at all. It is re-evaluated on every
|
|
294
|
+
* probe result, so it goes as soon as a usable peer shows up.
|
|
295
|
+
*
|
|
296
|
+
* Only one stale leg is kept. Two stale servers would otherwise keep each
|
|
297
|
+
* other, filling every connection slot so the refill never reaches a
|
|
298
|
+
* server that could replace them.
|
|
299
|
+
*/
|
|
300
|
+
const dropUnhealthyServers = () => {
|
|
301
|
+
let isStaleLegKept = false
|
|
302
|
+
for (const [uri, state] of Object.entries(serverStatesCache)) {
|
|
303
|
+
if (state.health !== 'unhealthy') continue
|
|
304
|
+
if (!hasUsablePeer(uri) && !isStaleLegKept) {
|
|
305
|
+
log.warn(
|
|
306
|
+
`${uri} reports inSync: false but is the only usable connection`
|
|
307
|
+
)
|
|
308
|
+
isStaleLegKept = true
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
dropServer(uri, 'Blockbook reports inSync: false')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const checkServerHealth = (uri) => {
|
|
316
|
+
const serverState = serverStatesCache[uri]
|
|
317
|
+
if (serverState == null) return
|
|
318
|
+
pluginState
|
|
319
|
+
.shareServerProbe(
|
|
320
|
+
uri,
|
|
321
|
+
healthProbeMinIntervalMs,
|
|
322
|
+
async () => await probeServerHealth(uri)
|
|
323
|
+
)
|
|
324
|
+
.then(health => {
|
|
325
|
+
const serverState = serverStatesCache[uri]
|
|
326
|
+
// The connection may have gone away while the probe was in flight:
|
|
327
|
+
if (serverState == null) return
|
|
328
|
+
if (serverState.health !== health) {
|
|
329
|
+
log(`${uri} health: ${serverState.health} -> ${health}`)
|
|
330
|
+
}
|
|
331
|
+
serverState.health = health
|
|
332
|
+
// This result may make this server droppable, or make it the peer
|
|
333
|
+
// that lets a previously kept stale server go:
|
|
334
|
+
dropUnhealthyServers()
|
|
335
|
+
})
|
|
336
|
+
.catch((error) => {
|
|
337
|
+
log.error(`${uri} health check failed: ${String(error)}`)
|
|
338
|
+
})
|
|
339
|
+
}
|
|
151
340
|
let isEngineOn = true
|
|
152
341
|
let serverList = []
|
|
153
342
|
let reconnectCounter = 0
|
|
@@ -181,6 +370,8 @@ const withTimeout = async (
|
|
|
181
370
|
const queryTime = Date.now() - queryDate
|
|
182
371
|
log(`${uri} returned healthCheck in ${queryTime}ms`)
|
|
183
372
|
pluginState.serverScoreUp(uri, queryTime)
|
|
373
|
+
// Periodic probe, once per keepalive:
|
|
374
|
+
checkServerHealth(uri)
|
|
184
375
|
}
|
|
185
376
|
)
|
|
186
377
|
engineEmitter.on(
|
|
@@ -199,7 +390,11 @@ const withTimeout = async (
|
|
|
199
390
|
_EngineEmitter.EngineEvent.BLOCK_HEIGHT_CHANGED,
|
|
200
391
|
(uri, blockHeight) => {
|
|
201
392
|
log(`${uri} block height changed to ${blockHeight}`)
|
|
202
|
-
serverStatesCache[uri]
|
|
393
|
+
const serverState = serverStatesCache[uri]
|
|
394
|
+
// The connection may already be gone, as the sibling handler above
|
|
395
|
+
// allows for:
|
|
396
|
+
if (serverState == null) return
|
|
397
|
+
serverState.blockHeight = blockHeight
|
|
203
398
|
}
|
|
204
399
|
)
|
|
205
400
|
|
|
@@ -207,18 +402,26 @@ const withTimeout = async (
|
|
|
207
402
|
|
|
208
403
|
|
|
209
404
|
|
|
210
|
-
const makeServerStatesCacheEntry = (
|
|
405
|
+
const makeServerStatesCacheEntry = (
|
|
406
|
+
uri,
|
|
407
|
+
blockbook
|
|
408
|
+
) => ({
|
|
211
409
|
blockbook,
|
|
212
410
|
blockSubscriptionStatus: 'unsubscribed',
|
|
213
411
|
txids: new Set(),
|
|
214
412
|
addresses: new Set(),
|
|
215
|
-
blockHeight: 0
|
|
413
|
+
blockHeight: 0,
|
|
414
|
+
health: findHttpTwin(uri) == null ? 'unknown' : 'pending'
|
|
216
415
|
})
|
|
217
416
|
|
|
218
417
|
const reconnect = () => {
|
|
219
418
|
if (isEngineOn) {
|
|
220
419
|
log(`attempting server reconnect number ${reconnectCounter}`)
|
|
221
420
|
const reconnectionDelay = Math.max(5, reconnectCounter++) * 1000
|
|
421
|
+
// One pending reconnect at a time. A drop and the close event it
|
|
422
|
+
// causes both land here, and a timer that was overwritten rather than
|
|
423
|
+
// cleared would outlive stop() and switch the engine back on.
|
|
424
|
+
clearTimeout(reconnectTimer)
|
|
222
425
|
reconnectTimer = setTimeout(() => {
|
|
223
426
|
clearTimeout(reconnectTimer)
|
|
224
427
|
instance.refillServers()
|
|
@@ -244,6 +447,9 @@ const withTimeout = async (
|
|
|
244
447
|
// Skip reconnecting to an existing connection
|
|
245
448
|
if (serverStatesCache[uri] != null) continue
|
|
246
449
|
|
|
450
|
+
// The list may predate a quarantine:
|
|
451
|
+
if (pluginState.isServerQuarantined(uri)) continue
|
|
452
|
+
|
|
247
453
|
// Validate the URI of server to make sure it is valid
|
|
248
454
|
const parsed = _urijs.parse.call(void 0, uri)
|
|
249
455
|
if (
|
|
@@ -254,7 +460,7 @@ const withTimeout = async (
|
|
|
254
460
|
continue
|
|
255
461
|
}
|
|
256
462
|
|
|
257
|
-
//
|
|
463
|
+
// Randomize the URI picking
|
|
258
464
|
chanceToBePicked -= chanceToBePicked > 0.5 ? 0.25 : 0
|
|
259
465
|
if (Math.random() > chanceToBePicked) {
|
|
260
466
|
serverList.push(uri)
|
|
@@ -308,7 +514,7 @@ const withTimeout = async (
|
|
|
308
514
|
}
|
|
309
515
|
|
|
310
516
|
// Create a new blockbook instance based on the URI scheme
|
|
311
|
-
if (
|
|
517
|
+
if (_BlockbookElectrum.isElectrumUri.call(void 0, uri)) {
|
|
312
518
|
// Electrum wrapper
|
|
313
519
|
blockbook = _BlockbookElectrum.makeBlockbookElectrum.call(void 0, {
|
|
314
520
|
asAddress: pluginInfo.engineInfo.asBlockbookAddress,
|
|
@@ -331,17 +537,25 @@ const withTimeout = async (
|
|
|
331
537
|
log,
|
|
332
538
|
taskGeneratorFn,
|
|
333
539
|
socketEmitter,
|
|
334
|
-
walletId: walletInfo.id
|
|
540
|
+
walletId: walletInfo.id,
|
|
541
|
+
keepAliveMs,
|
|
542
|
+
wakeUpMs
|
|
335
543
|
})
|
|
336
544
|
}
|
|
337
545
|
|
|
338
546
|
// Make new ServerStates instance
|
|
339
|
-
serverStatesCache[uri] = makeServerStatesCacheEntry(blockbook)
|
|
547
|
+
serverStatesCache[uri] = makeServerStatesCacheEntry(uri, blockbook)
|
|
340
548
|
|
|
341
549
|
// Initialize blockbook connection for server
|
|
342
550
|
blockbook
|
|
343
551
|
.connect()
|
|
344
552
|
.then(async () => {
|
|
553
|
+
// This connection is now open and counts as a usable peer, which
|
|
554
|
+
// may be what a stale server kept for lack of one was waiting on.
|
|
555
|
+
// Done here rather than on the socket's open event, which fires
|
|
556
|
+
// before the connect promise marks the blockbook connected:
|
|
557
|
+
dropUnhealthyServers()
|
|
558
|
+
|
|
345
559
|
// Fetch block height from blockbook server
|
|
346
560
|
const startTime = Date.now()
|
|
347
561
|
const { bestHeight: blockHeight } = await blockbook.fetchInfo()
|
|
@@ -356,11 +570,29 @@ const withTimeout = async (
|
|
|
356
570
|
// Increment server score using response time
|
|
357
571
|
const responseTime = Date.now() - startTime
|
|
358
572
|
pluginState.serverScoreUp(uri, responseTime)
|
|
573
|
+
|
|
574
|
+
// On-connect probe. The keepalive may or may not have fired by
|
|
575
|
+
// now, depending on how fast the socket opened, so this is the
|
|
576
|
+
// one place that guarantees a probe before the server does work:
|
|
577
|
+
checkServerHealth(uri)
|
|
359
578
|
})
|
|
360
579
|
.catch(e => {
|
|
361
580
|
log.error(`${JSON.stringify(e.message)}`)
|
|
362
581
|
})
|
|
363
582
|
}
|
|
583
|
+
|
|
584
|
+
// A quarantined server is withheld from the candidate list, so a wallet
|
|
585
|
+
// left short of connections would otherwise stay that way until an
|
|
586
|
+
// unrelated close event. Keep trying while a quarantine is in force; the
|
|
587
|
+
// server comes back into the list when it expires. The reconnect delay
|
|
588
|
+
// grows with each empty pass, so this does not spin.
|
|
589
|
+
if (
|
|
590
|
+
Object.keys(serverStatesCache).length < _constants.MAX_CONNECTIONS &&
|
|
591
|
+
serverList.length === 0 &&
|
|
592
|
+
pluginState.hasQuarantinedServers()
|
|
593
|
+
) {
|
|
594
|
+
reconnect()
|
|
595
|
+
}
|
|
364
596
|
}
|
|
365
597
|
|
|
366
598
|
const deferredWithServerScoring = (
|
|
@@ -581,6 +813,10 @@ const withTimeout = async (
|
|
|
581
813
|
return serverStatesCache[uri].blockHeight
|
|
582
814
|
},
|
|
583
815
|
|
|
816
|
+
getServerHealth(uri) {
|
|
817
|
+
return _optionalChain([serverStatesCache, 'access', _2 => _2[uri], 'optionalAccess', _3 => _3.health])
|
|
818
|
+
},
|
|
819
|
+
|
|
584
820
|
getServerList() {
|
|
585
821
|
return serverList
|
|
586
822
|
},
|
|
@@ -630,6 +866,18 @@ const withTimeout = async (
|
|
|
630
866
|
return true
|
|
631
867
|
},
|
|
632
868
|
|
|
869
|
+
serverCanGetAddressHistory(uri, address) {
|
|
870
|
+
// Electrum only reports how many transactions an address has, so the
|
|
871
|
+
// history goes to a connected Blockbook server whenever there is one.
|
|
872
|
+
// Without one, the counts still let the wallet finish syncing:
|
|
873
|
+
const hasBlockbook = Object.entries(serverStatesCache).some(
|
|
874
|
+
([otherUri, state]) =>
|
|
875
|
+
!_BlockbookElectrum.isElectrumUri.call(void 0, otherUri) && state.blockbook.isConnected
|
|
876
|
+
)
|
|
877
|
+
if (_BlockbookElectrum.isElectrumUri.call(void 0, uri) && hasBlockbook) return false
|
|
878
|
+
return instance.serverCanGetAddress(uri, address)
|
|
879
|
+
},
|
|
880
|
+
|
|
633
881
|
serverIsAwareOfAddress(uri, address) {
|
|
634
882
|
const serverState = serverStatesCache[uri]
|
|
635
883
|
if (serverState == null) return false
|
|
@@ -674,7 +922,7 @@ const withTimeout = async (
|
|
|
674
922
|
serverState.addresses.delete(address)
|
|
675
923
|
}
|
|
676
924
|
// Reject the deferredAddressSub promise if provided
|
|
677
|
-
_optionalChain([deferredAddressSub, 'optionalAccess',
|
|
925
|
+
_optionalChain([deferredAddressSub, 'optionalAccess', _4 => _4.reject, 'call', _5 => _5(err)])
|
|
678
926
|
})
|
|
679
927
|
|
|
680
928
|
blockbook.watchAddresses(
|