dexbot 1.4.7 → 1.4.8
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/README.md +1 -0
- package/dist/credential-daemon.d.ts.map +1 -1
- package/dist/credential-daemon.js +416 -92
- package/dist/credential-daemon.js.map +1 -1
- package/dist/dexbot.d.ts.map +1 -1
- package/dist/dexbot.js +61 -27
- package/dist/dexbot.js.map +1 -1
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +14 -24
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
- package/dist/modules/bitshares-native/transport.js +21 -0
- package/dist/modules/bitshares-native/transport.js.map +1 -1
- package/dist/modules/broadcast_failure.d.ts +21 -0
- package/dist/modules/broadcast_failure.d.ts.map +1 -0
- package/dist/modules/broadcast_failure.js +95 -0
- package/dist/modules/broadcast_failure.js.map +1 -0
- package/dist/modules/chain_keys.d.ts +2 -2
- package/dist/modules/chain_keys.d.ts.map +1 -1
- package/dist/modules/chain_keys.js +30 -56
- package/dist/modules/chain_keys.js.map +1 -1
- package/dist/modules/chain_orders.d.ts +79 -1
- package/dist/modules/chain_orders.d.ts.map +1 -1
- package/dist/modules/chain_orders.js +154 -10
- package/dist/modules/chain_orders.js.map +1 -1
- package/dist/modules/constants.d.ts +1 -0
- package/dist/modules/constants.d.ts.map +1 -1
- package/dist/modules/constants.js +21 -7
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/daemon_node_health.d.ts +33 -0
- package/dist/modules/daemon_node_health.d.ts.map +1 -0
- package/dist/modules/daemon_node_health.js +120 -0
- package/dist/modules/daemon_node_health.js.map +1 -0
- package/dist/modules/dexbot_class.d.ts +12 -3
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +19 -5
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +46 -1
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +754 -64
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_credential_client.d.ts +15 -0
- package/dist/modules/dexbot_credential_client.d.ts.map +1 -1
- package/dist/modules/dexbot_credential_client.js +114 -141
- package/dist/modules/dexbot_credential_client.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +45 -15
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +5 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +237 -84
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_startup_runtime.js +95 -33
- package/dist/modules/dexbot_startup_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +24 -2
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/key_store.d.ts.map +1 -1
- package/dist/modules/key_store.js +5 -4
- package/dist/modules/key_store.js.map +1 -1
- package/dist/modules/launcher/credential_daemon.d.ts.map +1 -1
- package/dist/modules/launcher/credential_daemon.js +15 -3
- package/dist/modules/launcher/credential_daemon.js.map +1 -1
- package/dist/modules/node_failure_ledger.d.ts +39 -0
- package/dist/modules/node_failure_ledger.d.ts.map +1 -0
- package/dist/modules/node_failure_ledger.js +88 -0
- package/dist/modules/node_failure_ledger.js.map +1 -0
- package/dist/modules/node_health_cache.d.ts +2 -1
- package/dist/modules/node_health_cache.d.ts.map +1 -1
- package/dist/modules/node_health_cache.js +25 -0
- package/dist/modules/node_health_cache.js.map +1 -1
- package/dist/modules/node_manager.d.ts +3 -0
- package/dist/modules/node_manager.d.ts.map +1 -1
- package/dist/modules/node_manager.js +41 -10
- package/dist/modules/node_manager.js.map +1 -1
- package/dist/modules/order/accounting.d.ts +1 -1
- package/dist/modules/order/accounting.d.ts.map +1 -1
- package/dist/modules/order/accounting.js +219 -101
- package/dist/modules/order/accounting.js.map +1 -1
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +66 -14
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile.js +130 -56
- package/dist/modules/order/grid_reconcile.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +2 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +333 -54
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +97 -4
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +377 -92
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/strategy.d.ts.map +1 -1
- package/dist/modules/order/strategy.js +24 -1
- package/dist/modules/order/strategy.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts +16 -3
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +329 -247
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +79 -1
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +18 -0
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +44 -1
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +91 -5
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +5 -1
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +7 -5
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/order/utils/withPoolRef.d.ts.map +1 -1
- package/dist/modules/order/utils/withPoolRef.js +39 -4
- package/dist/modules/order/utils/withPoolRef.js.map +1 -1
- package/dist/modules/socket_json_client.d.ts +14 -0
- package/dist/modules/socket_json_client.d.ts.map +1 -0
- package/dist/modules/socket_json_client.js +100 -0
- package/dist/modules/socket_json_client.js.map +1 -0
- package/package.json +2 -1
- package/scripts/clear-all.sh +4 -4
- package/scripts/clear-logs.sh +4 -4
|
@@ -104,13 +104,15 @@ const { assertPrivatePathSecurity, ensureCredentialRuntimeDirSync, getCredential
|
|
|
104
104
|
const { buildSessionAccountCache, loadDaemonPrivateKey, } = require('./modules/credential_session_cache');
|
|
105
105
|
const { fetchBootstrapPassword } = require('./modules/launcher/credential_bootstrap');
|
|
106
106
|
const { normalizeBootstrapCredential } = require('./modules/launcher/credential_secret');
|
|
107
|
-
const Logger = require('./modules/logger');
|
|
107
|
+
const Logger = require('./modules/logger').default;
|
|
108
108
|
const { ensureDir, safeUnlink } = require('./modules/utils/fs_utils');
|
|
109
109
|
const { Config } = require('./modules/config');
|
|
110
110
|
const daemonLogger = new Logger('credential-daemon');
|
|
111
111
|
// Resolve project root — handles running from dist/ (compiled) vs source
|
|
112
112
|
const { PATHS } = require('./modules/paths');
|
|
113
113
|
const { getErrorMessage } = require('./modules/utils/errors');
|
|
114
|
+
const { sleep } = require('./modules/order/utils/system');
|
|
115
|
+
const { classifyBroadcastFailure } = require('./modules/broadcast_failure');
|
|
114
116
|
// Unix sockets are required; only Unix-like systems are supported
|
|
115
117
|
const RUNTIME_DIR = getCredentialRuntimeDir({ root: PATHS.PROJECT_ROOT });
|
|
116
118
|
const SOCKET_PATH = getCredentialSocketPath({ root: PATHS.PROJECT_ROOT, runtimeDir: RUNTIME_DIR });
|
|
@@ -136,7 +138,7 @@ let auditPruneIntervalTimer = null;
|
|
|
136
138
|
// Signing client cache: key = `${accountName}:${keyFingerprint(wif)}`, value = full signing client + createdAt.
|
|
137
139
|
// Key rotation: loadDaemonPrivateKey re-reads from vault on every call. If the WIF changes the
|
|
138
140
|
// fingerprint changes → cache miss → new signing client created with the current key. No staleness.
|
|
139
|
-
// Cleared on transport reconnect (see
|
|
141
|
+
// Cleared on transport reconnect (see broadcastWithDeadline). TTL-pruned (30 min) in pruneStaleSigningClients.
|
|
140
142
|
const signingClientCache = new Map();
|
|
141
143
|
function debugLog(message, err = null) {
|
|
142
144
|
const suffix = err && getErrorMessage(err) ? `: ${getErrorMessage(err)}` : '';
|
|
@@ -396,107 +398,366 @@ async function executeOperationsWithClient(client, operations) {
|
|
|
396
398
|
operation_results: operationResults,
|
|
397
399
|
};
|
|
398
400
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Build a typed BROADCAST_DEADLINE error describing an uncertain broadcast.
|
|
403
|
+
* The bot maps this code to BroadcastUncertainError and runs verify-before-
|
|
404
|
+
* retry (chain read + adoption) instead of re-broadcasting.
|
|
405
|
+
*/
|
|
406
|
+
function buildUncertainError(accountName, startedAt, detail) {
|
|
407
|
+
const err = new Error(`${DAEMON_CODES.BROADCAST_DEADLINE}:${detail}`);
|
|
408
|
+
err.code = DAEMON_CODES.BROADCAST_DEADLINE;
|
|
409
|
+
err.uncertain = true;
|
|
410
|
+
err.accountName = accountName;
|
|
411
|
+
err.startedAt = startedAt;
|
|
412
|
+
err.ageMs = Date.now() - startedAt;
|
|
413
|
+
return err;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Serialize daemon broadcasts: the daemon shares ONE _nativeChainClient and
|
|
417
|
+
* signing-client cache across all socket clients, so concurrent
|
|
418
|
+
* broadcastWithDeadline calls would stomp each other's node pinning, connect
|
|
419
|
+
* state, and cache clearing (a pre-existing hazard, widened by per-node
|
|
420
|
+
* pinning). Broadcasts are queued as a promise chain; non-broadcast requests
|
|
421
|
+
* are unaffected.
|
|
422
|
+
*
|
|
423
|
+
* The broadcast deadline guard (createBroadcastGuard) is created by the
|
|
424
|
+
* request handler BEFORE the queue wait, so the deadline timer covers queue
|
|
425
|
+
* wait + broadcast work together: total daemon wall time per request is
|
|
426
|
+
* capped at CREDENTIAL_DAEMON_INNER_DEADLINE_MS, well inside the bot's
|
|
427
|
+
* CREDENTIAL_BROADCAST_TIMEOUT_MS outer socket window. A broadcast that is
|
|
428
|
+
* still queued when the deadline fires aborts via guard.isFired() before it
|
|
429
|
+
* starts, and a client socket that dies (bot outer timeout destroys its end,
|
|
430
|
+
* crash, restart) fires the guard too — so a broadcast can never land on
|
|
431
|
+
* chain AFTER the bot already verified chain absence and re-broadcast the
|
|
432
|
+
* same operation.
|
|
433
|
+
*/
|
|
434
|
+
let broadcastChain = Promise.resolve();
|
|
435
|
+
function serializeBroadcast(fn, getWork) {
|
|
436
|
+
const run = broadcastChain.then(fn, fn);
|
|
437
|
+
// The chain must wait for the WORK to settle, not the deadline-raced
|
|
438
|
+
// result: a deadline-aborted broadcast keeps running in the background
|
|
439
|
+
// (the guard only aborts it at its next checkpoint), and starting the
|
|
440
|
+
// next queued broadcast while that zombie still touches the shared
|
|
441
|
+
// _nativeChainClient would stomp its node pinning / transport state.
|
|
442
|
+
// getWork() is consulted AFTER run settles, when the zombie (if any) is
|
|
443
|
+
// known; the transport disconnect + guard checkpoints terminate it fast,
|
|
444
|
+
// so the queue wait stays well inside the next request's inner deadline.
|
|
445
|
+
broadcastChain = Promise.resolve(getWork ? run.then(() => getWork(), () => getWork()) : run)
|
|
446
|
+
.then(() => undefined, () => undefined);
|
|
447
|
+
return run;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Broadcast deadline guard shared between the queue wait and the broadcast
|
|
451
|
+
* work. The timer starts at request receipt — before the serializeBroadcast
|
|
452
|
+
* queue wait — so queued broadcasts cannot outlive the bot's outer socket
|
|
453
|
+
* window. Firing the guard (deadline exceeded, or the requesting socket
|
|
454
|
+
* died) aborts the queued/in-flight broadcast: the work checks isFired()
|
|
455
|
+
* before every attempt, so a late broadcast can never land after the bot
|
|
456
|
+
* already verified chain absence and re-broadcast the operation.
|
|
457
|
+
*/
|
|
458
|
+
function createBroadcastGuard(accountName, startedAt, deadlineMs) {
|
|
459
|
+
let fired = false;
|
|
460
|
+
let timer = null;
|
|
461
|
+
let rejectGuard = null;
|
|
462
|
+
const promise = new Promise((_, reject) => {
|
|
463
|
+
rejectGuard = reject;
|
|
464
|
+
timer = setTimeout(() => {
|
|
465
|
+
fired = true;
|
|
466
|
+
reject(buildUncertainError(accountName, startedAt, `inner broadcast deadline ${deadlineMs}ms exceeded`));
|
|
467
|
+
}, deadlineMs);
|
|
468
|
+
});
|
|
469
|
+
// The guard promise is raced only once the serialized broadcast starts;
|
|
470
|
+
// while the request sits in the queue a deadline rejection would
|
|
471
|
+
// otherwise surface as an unhandled rejection.
|
|
472
|
+
promise.catch(() => { });
|
|
473
|
+
return {
|
|
474
|
+
isFired: () => fired,
|
|
475
|
+
promise,
|
|
476
|
+
// The candidate in play when the guard fires, set by the broadcast
|
|
477
|
+
// work loop (null until a candidate is attempted, or for a queued
|
|
478
|
+
// deadline that never started). The request handler echoes it in the
|
|
479
|
+
// BROADCAST_DEADLINE reply so the bot blames the node that actually
|
|
480
|
+
// caused the outcome — never its original preference blindly.
|
|
481
|
+
currentNode: null,
|
|
482
|
+
fire: (reason) => {
|
|
483
|
+
if (fired)
|
|
484
|
+
return;
|
|
485
|
+
fired = true;
|
|
486
|
+
if (timer) {
|
|
487
|
+
clearTimeout(timer);
|
|
488
|
+
timer = null;
|
|
489
|
+
}
|
|
490
|
+
rejectGuard(buildUncertainError(accountName, startedAt, reason));
|
|
491
|
+
},
|
|
492
|
+
clearTimer: () => {
|
|
493
|
+
if (timer) {
|
|
494
|
+
clearTimeout(timer);
|
|
495
|
+
timer = null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function resolveInnerDeadlineMs() {
|
|
501
|
+
return Number.isFinite(Number(TIMING?.CREDENTIAL_DAEMON_INNER_DEADLINE_MS))
|
|
502
|
+
? Number(TIMING.CREDENTIAL_DAEMON_INNER_DEADLINE_MS)
|
|
503
|
+
: 20000;
|
|
504
|
+
}
|
|
505
|
+
async function broadcastWithDeadline(accountName, privateKey, broadcastFn, nodeUrl = null, opts = {}) {
|
|
506
|
+
// Deadline-capped broadcast: each node gets CREDENTIAL_DAEMON_BROADCAST_RETRIES
|
|
507
|
+
// attempts pinned to it (the transport sweeps ONLY the pinned node), and
|
|
508
|
+
// only when they ALL fail with failures that provably never reached the
|
|
509
|
+
// chain (pre-transmit: connection setup, WebSocket not open, frame send
|
|
510
|
+
// errors) does the daemon report the node failure to the node health
|
|
511
|
+
// ledger and rotate to the next best node from the health cache. An
|
|
512
|
+
// uncertain failure — RPC timeout, or the connection dropped while a
|
|
513
|
+
// response was pending — may have landed, and re-signing it would
|
|
514
|
+
// duplicate the transaction on chain (each re-signing produces a new
|
|
515
|
+
// transaction ID), so it is NEVER retried on any node: it is reported as
|
|
516
|
+
// BROADCAST_DEADLINE so the bot layer verifies chain inclusion before any
|
|
517
|
+
// re-broadcast (executeWithRetryOnUncertain, startup adoption). The inner
|
|
518
|
+
// deadline caps the TOTAL wall time across all attempts — pre-transmit
|
|
519
|
+
// failures are fast (no RPC wait), so retries never hang the bot — and
|
|
520
|
+
// guarantees we
|
|
521
|
+
// reply well before the bot's outer socket timer
|
|
402
522
|
// (CREDENTIAL_BROADCAST_TIMEOUT_MS) fires. If we don't reply in time, the
|
|
403
523
|
// bot raises BroadcastUncertainError and enters the recovery path.
|
|
404
524
|
// See: modules/dexbot_credential_client.ts BroadcastUncertainError.
|
|
405
|
-
const innerDeadlineMs =
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const maxRetries = TIMING?.CREDENTIAL_DAEMON_BROADCAST_RETRIES ?? 2;
|
|
525
|
+
const innerDeadlineMs = resolveInnerDeadlineMs();
|
|
526
|
+
const maxRetries = Number.isFinite(Number(TIMING?.CREDENTIAL_DAEMON_BROADCAST_RETRIES))
|
|
527
|
+
? Number(TIMING.CREDENTIAL_DAEMON_BROADCAST_RETRIES)
|
|
528
|
+
: 3;
|
|
529
|
+
const retryBackoffMs = Number.isFinite(Number(TIMING?.CREDENTIAL_DAEMON_BROADCAST_BACKOFF_MS))
|
|
530
|
+
? Number(TIMING.CREDENTIAL_DAEMON_BROADCAST_BACKOFF_MS)
|
|
531
|
+
: 1000;
|
|
532
|
+
const startedAt = opts.startedAt ?? Date.now();
|
|
533
|
+
// The guard covers the queue wait too when the request handler creates
|
|
534
|
+
// it before serializeBroadcast (deadline timer started at request
|
|
535
|
+
// receipt); without one it starts here and covers only the broadcast
|
|
536
|
+
// work. isFired() lets the retry loop abort before any late background
|
|
537
|
+
// broadcast, and the finally block resets the transport even while the
|
|
538
|
+
// broadcast is still in flight.
|
|
539
|
+
const guard = opts.guard ?? createBroadcastGuard(accountName, startedAt, innerDeadlineMs);
|
|
540
|
+
const deadlinePromise = guard.promise;
|
|
422
541
|
const work = (async () => {
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
542
|
+
// Pin ALL attempts to ONE node at a time: up to `attemptsPerNode`
|
|
543
|
+
// attempts on the best candidate, and only when those all fail with
|
|
544
|
+
// provably-untransmitted errors do we report the node failure and
|
|
545
|
+
// rotate to the next best node.
|
|
546
|
+
const attemptsPerNode = Math.max(1, maxRetries);
|
|
547
|
+
let lastErr = null;
|
|
548
|
+
const exhaustedNodes = [];
|
|
549
|
+
// Build the ordered candidate list: the bot-supplied nodeUrl is the
|
|
550
|
+
// PREFERRED node (attempted first), followed by the healthy node set
|
|
551
|
+
// ordered best-first by the shared health cache. Re-reads the cache
|
|
552
|
+
// so bot-side blacklists are reflected immediately; daemon-side
|
|
553
|
+
// blacklists (daemonNodeHealth) are excluded until their cooldown
|
|
554
|
+
// expires.
|
|
555
|
+
const buildCandidates = () => {
|
|
556
|
+
refreshNodeList();
|
|
557
|
+
const baseList = _nativeNodeList.length > 0 ? _nativeNodeList : NODE_MANAGEMENT.DEFAULT_NODES;
|
|
558
|
+
const list = nodeUrl ? [nodeUrl, ...baseList.filter((n) => n !== nodeUrl)] : [...baseList];
|
|
559
|
+
return list.filter((n) => !exhaustedNodes.includes(n) && !daemonNodeHealth.isBlacklisted(n));
|
|
560
|
+
};
|
|
561
|
+
// Candidate pool: healthy nodes best-first. When every available node
|
|
562
|
+
// is daemon-blacklisted (or the health cache is empty/absent), fall
|
|
563
|
+
// back to the configured defaults as a LAST RESORT — rebuilt the same
|
|
564
|
+
// way on every rotation, so each rotation still gets its next
|
|
565
|
+
// best-effort node: failing the broadcast outright would stall the
|
|
566
|
+
// order until the bot's next cycle, and a stale blacklist (node
|
|
567
|
+
// recovered inside its cooldown window) would otherwise go unused.
|
|
568
|
+
// Ordering: never-failed/recovered nodes first, then blacklisted ones
|
|
569
|
+
// cooldown-soonest-first.
|
|
570
|
+
let lastResortWarned = false;
|
|
571
|
+
const buildCandidatePool = () => {
|
|
572
|
+
const pool = buildCandidates();
|
|
573
|
+
if (pool.length > 0)
|
|
574
|
+
return pool;
|
|
575
|
+
const state = daemonNodeHealth.getState();
|
|
576
|
+
const fallback = NODE_MANAGEMENT.DEFAULT_NODES
|
|
577
|
+
.filter((n) => !exhaustedNodes.includes(n))
|
|
578
|
+
.sort((a, b) => (state.get(a)?.blacklistedUntil ?? -Infinity) - (state.get(b)?.blacklistedUntil ?? -Infinity));
|
|
579
|
+
if (fallback.length > 0 && !lastResortWarned) {
|
|
580
|
+
lastResortWarned = true;
|
|
581
|
+
daemonLogger.warn?.(`[credential-daemon] All healthy candidates blacklisted; falling back to last-resort node(s) (never-failed first, then cooldown-soonest)`);
|
|
582
|
+
}
|
|
583
|
+
return fallback;
|
|
584
|
+
};
|
|
585
|
+
// candidates[0] is always the next fresh node: every rotation pushes
|
|
586
|
+
// the exhausted node onto the ledger and rebuilds the pool, so the
|
|
587
|
+
// pool shrinks until it is empty.
|
|
588
|
+
let candidates = buildCandidatePool();
|
|
589
|
+
// The candidate in play when the deadline fires, exposed to the
|
|
590
|
+
// request handler (via the guard) so the BROADCAST_DEADLINE reply can
|
|
591
|
+
// name the node that actually caused it — never the bot's original
|
|
592
|
+
// preference, which may be innocent after daemon-side rotation.
|
|
593
|
+
if (opts.guard)
|
|
594
|
+
opts.guard.currentNode = null;
|
|
595
|
+
while (candidates.length > 0) {
|
|
596
|
+
const candidate = candidates[0];
|
|
597
|
+
if (opts.guard)
|
|
598
|
+
opts.guard.currentNode = candidate;
|
|
599
|
+
for (let attempt = 1; attempt <= attemptsPerNode; attempt++) {
|
|
600
|
+
// Abort as soon as the guard fired (inner deadline won the
|
|
601
|
+
// race, or the requesting socket died): the bot has been told
|
|
602
|
+
// the outcome is uncertain and may verify + re-create — a
|
|
603
|
+
// late background broadcast would duplicate the order.
|
|
604
|
+
if (guard.isFired()) {
|
|
605
|
+
throw buildUncertainError(accountName, startedAt, 'inner broadcast deadline exceeded');
|
|
606
|
+
}
|
|
607
|
+
if (attempt > 1) {
|
|
608
|
+
await sleep(retryBackoffMs);
|
|
609
|
+
if (guard.isFired()) {
|
|
610
|
+
throw buildUncertainError(accountName, startedAt, 'inner broadcast deadline exceeded');
|
|
442
611
|
}
|
|
443
|
-
signingClientCache.clear();
|
|
444
612
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
613
|
+
let phase = 'connect';
|
|
614
|
+
try {
|
|
615
|
+
const currentNode = _nativeChainClient.transport?.getNodeUrl?.();
|
|
616
|
+
if (_nativeChainClient.getStatus() !== 'connected' || currentNode !== candidate) {
|
|
617
|
+
phase = 'connect';
|
|
618
|
+
// Pin the attempt to the current candidate: the transport
|
|
619
|
+
// sweeps ONLY the pinned node, so all attempts genuinely
|
|
620
|
+
// use the SAME node before any rotation.
|
|
621
|
+
_nativeChainClient.setNodes([candidate]);
|
|
622
|
+
await _nativeChainClient.connect();
|
|
623
|
+
// The guard may have fired while the connect handshake was
|
|
624
|
+
// in flight (slow node + inner deadline). Abort here —
|
|
625
|
+
// before any signing-client work — so the sign+broadcast
|
|
626
|
+
// never happens after the bot was told BROADCAST_DEADLINE:
|
|
627
|
+
// a late background broadcast would duplicate the order.
|
|
628
|
+
if (guard.isFired()) {
|
|
629
|
+
throw buildUncertainError(accountName, startedAt, 'inner broadcast deadline exceeded during connect');
|
|
630
|
+
}
|
|
631
|
+
// Transport reconnected — dispose all stale signing clients (heap-dump safety)
|
|
632
|
+
// then clear the cache so new clients use the fresh transport.
|
|
633
|
+
for (const [, entry] of signingClientCache) {
|
|
634
|
+
if (typeof entry.signingClient.dispose === 'function') {
|
|
455
635
|
try {
|
|
456
|
-
|
|
636
|
+
entry.signingClient.dispose();
|
|
457
637
|
}
|
|
458
638
|
catch (_) { }
|
|
459
639
|
}
|
|
460
|
-
signingClientCache.delete(k);
|
|
461
640
|
}
|
|
641
|
+
signingClientCache.clear();
|
|
642
|
+
}
|
|
643
|
+
const fp = keyFingerprint(String(privateKey));
|
|
644
|
+
const cacheKey = accountName + ':' + fp;
|
|
645
|
+
let cached = signingClientCache.get(cacheKey);
|
|
646
|
+
if (!cached) {
|
|
647
|
+
// Proactively evict stale entries for the same account (different fingerprint)
|
|
648
|
+
// so key rotation never leaves dead entries in the map.
|
|
649
|
+
const stalePrefix = accountName + ':';
|
|
650
|
+
for (const [k, v] of signingClientCache) {
|
|
651
|
+
if (k !== cacheKey && k.startsWith(stalePrefix)) {
|
|
652
|
+
if (typeof v.signingClient.dispose === 'function') {
|
|
653
|
+
try {
|
|
654
|
+
v.signingClient.dispose();
|
|
655
|
+
}
|
|
656
|
+
catch (_) { }
|
|
657
|
+
}
|
|
658
|
+
signingClientCache.delete(k);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
pruneStaleSigningClients();
|
|
662
|
+
// Pre-convert WIF string → Buffer so dispose() can actually zero the bytes.
|
|
663
|
+
// The WIF string goes out of scope at function return and is GC'd (non-deterministic),
|
|
664
|
+
// but the Buffer passed to createSigningClient is filled with 0 by dispose() on eviction/shutdown.
|
|
665
|
+
const { createSigningClient, wifToBuffer } = require('./modules/bitshares-native');
|
|
666
|
+
const keyBuffer = wifToBuffer(String(privateKey));
|
|
667
|
+
const signingClient = createSigningClient(_nativeChainClient, accountName, keyBuffer);
|
|
668
|
+
cached = { signingClient, createdAt: Date.now() };
|
|
669
|
+
signingClientCache.set(cacheKey, cached);
|
|
670
|
+
}
|
|
671
|
+
const client = cached.signingClient.client;
|
|
672
|
+
await client.initPromise;
|
|
673
|
+
// The guard may have fired while the signing client fetched
|
|
674
|
+
// account/order metadata. Abort before the broadcast RPC so
|
|
675
|
+
// nothing is transmitted post-deadline — a zombie broadcast
|
|
676
|
+
// landing after the bot verified chain absence would duplicate
|
|
677
|
+
// the operation.
|
|
678
|
+
if (guard.isFired()) {
|
|
679
|
+
throw buildUncertainError(accountName, startedAt, 'inner broadcast deadline exceeded during signing client init');
|
|
462
680
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const signingClient = createSigningClient(_nativeChainClient, accountName, keyBuffer);
|
|
470
|
-
cached = { signingClient, createdAt: Date.now() };
|
|
471
|
-
signingClientCache.set(cacheKey, cached);
|
|
681
|
+
phase = 'broadcast';
|
|
682
|
+
const result = await broadcastFn(client);
|
|
683
|
+
// Success — restore the full node list so other users of
|
|
684
|
+
// the shared client don't inherit the single-node pin.
|
|
685
|
+
_nativeChainClient.setNodes(_nativeNodeList.length > 0 ? _nativeNodeList : NODE_MANAGEMENT.DEFAULT_NODES);
|
|
686
|
+
return result;
|
|
472
687
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
688
|
+
catch (err) {
|
|
689
|
+
// Guard-fired aborts (deadline during connect/init) must
|
|
690
|
+
// propagate as uncertain immediately — they are never
|
|
691
|
+
// retryable, even during the connect phase, and a retry
|
|
692
|
+
// would just re-enter the same deadlined attempt.
|
|
693
|
+
if (err?.code === DAEMON_CODES.BROADCAST_DEADLINE || err?.uncertain === true) {
|
|
694
|
+
throw err;
|
|
695
|
+
}
|
|
696
|
+
// Connect-phase failures are always pre-transmit: no broadcast
|
|
697
|
+
// has been attempted yet. Broadcast-phase failures are only
|
|
698
|
+
// retryable when the RPC frame provably never reached the wire.
|
|
699
|
+
const cls = phase === 'connect' ? 'retryable' : classifyBroadcastFailure(err);
|
|
700
|
+
if (cls === 'retryable') {
|
|
701
|
+
lastErr = err;
|
|
702
|
+
const retryLabel = attempt < attemptsPerNode ? 'retrying same node' : 'exhausted; rotating to next best node';
|
|
703
|
+
debugLog(`Broadcast attempt ${attempt}/${attemptsPerNode} on ${candidate} failed pre-transmit (${getErrorMessage(err)}); ${retryLabel}`);
|
|
704
|
+
// Reset the transport so the next attempt reconnects
|
|
705
|
+
// cleanly against the pinned node.
|
|
706
|
+
try {
|
|
707
|
+
_nativeChainClient.disconnect();
|
|
708
|
+
}
|
|
709
|
+
catch (_) { }
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (cls === 'uncertain') {
|
|
713
|
+
// The transaction may have landed — never re-sign on ANY
|
|
714
|
+
// node. Report the typed uncertain reply so the bot's
|
|
715
|
+
// verify-before-retry machinery checks chain inclusion
|
|
716
|
+
// before any re-create.
|
|
717
|
+
throw buildUncertainError(accountName, startedAt, getErrorMessage(err));
|
|
718
|
+
}
|
|
719
|
+
// 'definite': the chain rejected the transaction or it could
|
|
720
|
+
// not be built — nothing landed; the error propagates as-is.
|
|
479
721
|
throw err;
|
|
480
|
-
debugLog(`Broadcast failed (attempt ${attempt}), reconnecting: ${getErrorMessage(err)}`);
|
|
481
|
-
const staleKey = accountName + ':' + keyFingerprint(String(privateKey));
|
|
482
|
-
const staleEntry = signingClientCache.get(staleKey);
|
|
483
|
-
if (staleEntry && typeof staleEntry.signingClient.dispose === 'function') {
|
|
484
|
-
staleEntry.signingClient.dispose();
|
|
485
|
-
}
|
|
486
|
-
signingClientCache.delete(staleKey);
|
|
487
|
-
try {
|
|
488
|
-
_nativeChainClient.disconnect();
|
|
489
722
|
}
|
|
490
|
-
catch (_) { }
|
|
491
723
|
}
|
|
724
|
+
// All attempts on this candidate failed pre-transmit: nothing ever
|
|
725
|
+
// reached the chain via this node. Report the failure so the node
|
|
726
|
+
// health ledger counts it toward blacklisting, then rotate to the
|
|
727
|
+
// next best node (re-reading the health cache first).
|
|
728
|
+
exhaustedNodes.push(candidate);
|
|
729
|
+
daemonNodeHealth.reportFailure(candidate, getErrorMessage(lastErr) || 'pre-transmit broadcast failure');
|
|
730
|
+
candidates = buildCandidatePool();
|
|
731
|
+
}
|
|
732
|
+
// All candidates failed pre-transmit (or none were usable): nothing
|
|
733
|
+
// ever reached the chain.
|
|
734
|
+
if (!lastErr) {
|
|
735
|
+
lastErr = new Error('Broadcast not attempted: no usable node candidates (all blacklisted or unavailable)');
|
|
492
736
|
}
|
|
737
|
+
throw lastErr;
|
|
493
738
|
})();
|
|
739
|
+
// Expose the WORK promise (not the deadline-raced result) to the broadcast
|
|
740
|
+
// queue: serializeBroadcast chains the next queued broadcast on it, so a
|
|
741
|
+
// deadline-aborted zombie cannot overlap the next broadcast on the shared
|
|
742
|
+
// transport. The handler reads it back via opts.workRef.
|
|
743
|
+
if (opts.workRef && typeof opts.workRef === 'object') {
|
|
744
|
+
opts.workRef.work = work;
|
|
745
|
+
}
|
|
494
746
|
try {
|
|
495
747
|
return await Promise.race([work, deadlinePromise]);
|
|
496
748
|
}
|
|
497
749
|
finally {
|
|
498
|
-
|
|
499
|
-
|
|
750
|
+
guard.clearTimer();
|
|
751
|
+
if (guard.isFired()) {
|
|
752
|
+
// The guard won while the broadcast may still be in flight —
|
|
753
|
+
// drop the connection so the next request starts clean. The
|
|
754
|
+
// transport abort also kills any in-flight connect handshake, so
|
|
755
|
+
// the zombie cannot resurrect a connection afterwards.
|
|
756
|
+
try {
|
|
757
|
+
_nativeChainClient.disconnect();
|
|
758
|
+
}
|
|
759
|
+
catch (_) { }
|
|
760
|
+
}
|
|
500
761
|
// Prune stale signing clients on every broadcast, not just on cache
|
|
501
762
|
// miss, so long-running daemons don't accumulate stale entries when
|
|
502
763
|
// accounts always hit the cache (same WIF, no rotation).
|
|
@@ -525,6 +786,14 @@ function refreshNodeList() {
|
|
|
525
786
|
}
|
|
526
787
|
}
|
|
527
788
|
}
|
|
789
|
+
/**
|
|
790
|
+
* Daemon-local node health ledger: blacklists persistently failing nodes so
|
|
791
|
+
* the broadcast path rotates away from them and the shared health cache is
|
|
792
|
+
* updated when a node hits the failure threshold. See
|
|
793
|
+
* modules/daemon_node_health.ts for semantics.
|
|
794
|
+
*/
|
|
795
|
+
const { createNodeHealthLedger } = require('./modules/daemon_node_health');
|
|
796
|
+
const daemonNodeHealth = createNodeHealthLedger({ logger: daemonLogger });
|
|
528
797
|
function getCredentialDaemonNodeRefreshIntervalMs(settings) {
|
|
529
798
|
const configured = settings?.NODES?.credentialDaemonRefreshIntervalMs
|
|
530
799
|
?? settings?.NODES?.CREDENTIAL_DAEMON_NODE_REFRESH_INTERVAL_MS
|
|
@@ -720,9 +989,22 @@ async function initialize() {
|
|
|
720
989
|
*/
|
|
721
990
|
function handleConnection(socket) {
|
|
722
991
|
let buffer = '';
|
|
992
|
+
// Broadcast deadline guards for requests on this socket. Fired when the
|
|
993
|
+
// socket dies (bot outer timeout destroys its end, crash, restart): the
|
|
994
|
+
// queued/in-flight broadcast for a client that can no longer receive the
|
|
995
|
+
// reply must abort — otherwise it can land on chain after the bot
|
|
996
|
+
// verified chain absence and re-broadcast the same operation.
|
|
997
|
+
const activeGuards = new Set();
|
|
998
|
+
const abortSocketGuards = () => {
|
|
999
|
+
for (const guard of activeGuards) {
|
|
1000
|
+
guard.fire('client socket closed before broadcast reply');
|
|
1001
|
+
}
|
|
1002
|
+
activeGuards.clear();
|
|
1003
|
+
};
|
|
723
1004
|
socket.setTimeout(TIMING.CREDENTIAL_DAEMON_SOCKET_TIMEOUT_MS);
|
|
724
1005
|
socket.on('timeout', () => {
|
|
725
1006
|
daemonLogger.debug?.('[credential-daemon] Socket timeout — client idle');
|
|
1007
|
+
abortSocketGuards();
|
|
726
1008
|
socket.destroy();
|
|
727
1009
|
});
|
|
728
1010
|
socket.on('data', (data) => {
|
|
@@ -738,7 +1020,7 @@ function handleConnection(socket) {
|
|
|
738
1020
|
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
739
1021
|
for (const line of lines) {
|
|
740
1022
|
if (line.trim()) {
|
|
741
|
-
processRequest(line.trim(), socket);
|
|
1023
|
+
processRequest(line.trim(), socket, activeGuards);
|
|
742
1024
|
}
|
|
743
1025
|
}
|
|
744
1026
|
}
|
|
@@ -747,12 +1029,17 @@ function handleConnection(socket) {
|
|
|
747
1029
|
}
|
|
748
1030
|
});
|
|
749
1031
|
socket.on('end', () => {
|
|
1032
|
+
abortSocketGuards();
|
|
750
1033
|
socket.destroy();
|
|
751
1034
|
});
|
|
752
1035
|
socket.on('error', (error) => {
|
|
753
1036
|
daemonLogger.debug?.('[credential-daemon] Socket error: ' + getErrorMessage(error));
|
|
1037
|
+
abortSocketGuards();
|
|
754
1038
|
socket.destroy();
|
|
755
1039
|
});
|
|
1040
|
+
socket.on('close', () => {
|
|
1041
|
+
abortSocketGuards();
|
|
1042
|
+
});
|
|
756
1043
|
}
|
|
757
1044
|
/**
|
|
758
1045
|
* Process incoming credential request from client.
|
|
@@ -762,7 +1049,7 @@ function handleConnection(socket) {
|
|
|
762
1049
|
* @param {string} requestStr - JSON string with {type, accountName}
|
|
763
1050
|
* @param {net.Socket} socket - Client socket to send response
|
|
764
1051
|
*/
|
|
765
|
-
function processRequest(requestStr, socket) {
|
|
1052
|
+
function processRequest(requestStr, socket, activeGuards = new Set()) {
|
|
766
1053
|
if (daemonShuttingDown)
|
|
767
1054
|
return;
|
|
768
1055
|
// The outer try/catch handles JSON parse errors and any synchronous throws.
|
|
@@ -862,9 +1149,20 @@ function processRequest(requestStr, socket) {
|
|
|
862
1149
|
}
|
|
863
1150
|
const privateKey = await loadCurrentPrivateKey(accountName);
|
|
864
1151
|
const broadcastNodeUrl = typeof request.nodeUrl === 'string' ? request.nodeUrl : null;
|
|
1152
|
+
// Start the broadcast deadline NOW, before the
|
|
1153
|
+
// serializeBroadcast queue wait, so total daemon wall time
|
|
1154
|
+
// per request stays inside the bot's outer socket window.
|
|
1155
|
+
// If the deadline fires while queued (burst concurrency),
|
|
1156
|
+
// the queued broadcast aborts via guard.isFired() before
|
|
1157
|
+
// it starts — it can never land after the bot verified
|
|
1158
|
+
// chain absence and re-broadcast the same operation.
|
|
1159
|
+
const broadcastStartedAt = Date.now();
|
|
1160
|
+
const broadcastGuard = createBroadcastGuard(accountName, broadcastStartedAt, resolveInnerDeadlineMs());
|
|
1161
|
+
activeGuards.add(broadcastGuard);
|
|
1162
|
+
const broadcastWorkRef = { work: null };
|
|
865
1163
|
let signResult;
|
|
866
1164
|
try {
|
|
867
|
-
signResult = await
|
|
1165
|
+
signResult = await serializeBroadcast(() => broadcastWithDeadline(accountName, privateKey, (client) => client.broadcast(operation), broadcastNodeUrl, { guard: broadcastGuard, startedAt: broadcastStartedAt, workRef: broadcastWorkRef }), () => broadcastWorkRef.work || Promise.resolve());
|
|
868
1166
|
}
|
|
869
1167
|
catch (broadcastErr) {
|
|
870
1168
|
if (broadcastErr && broadcastErr.code === DAEMON_CODES.BROADCAST_DEADLINE) {
|
|
@@ -872,7 +1170,7 @@ function processRequest(requestStr, socket) {
|
|
|
872
1170
|
event: 'sign_timeout',
|
|
873
1171
|
accountName,
|
|
874
1172
|
sessionId,
|
|
875
|
-
nodeUrl: broadcastNodeUrl,
|
|
1173
|
+
nodeUrl: broadcastGuard.currentNode || broadcastNodeUrl,
|
|
876
1174
|
opCount: 1,
|
|
877
1175
|
opTypes: [operation && operation.op_name].filter(Boolean),
|
|
878
1176
|
ageMs: broadcastErr.ageMs,
|
|
@@ -883,11 +1181,19 @@ function processRequest(requestStr, socket) {
|
|
|
883
1181
|
});
|
|
884
1182
|
// Tell the bot the chain state is uncertain so it
|
|
885
1183
|
// can run the recovery path (read chain, match by
|
|
886
|
-
// fingerprint, adopt or discard).
|
|
887
|
-
|
|
1184
|
+
// fingerprint, adopt or discard). Name the node
|
|
1185
|
+
// actually in play at the deadline (the bot's
|
|
1186
|
+
// preferred node may be innocent after daemon-side
|
|
1187
|
+
// rotation, and a queued deadline involves none).
|
|
1188
|
+
return sendError(socket, 'chain status uncertain after inner deadline', DAEMON_CODES.BROADCAST_DEADLINE, broadcastGuard.currentNode
|
|
1189
|
+
? { nodeUrl: broadcastGuard.currentNode }
|
|
1190
|
+
: {});
|
|
888
1191
|
}
|
|
889
1192
|
throw broadcastErr;
|
|
890
1193
|
}
|
|
1194
|
+
finally {
|
|
1195
|
+
activeGuards.delete(broadcastGuard);
|
|
1196
|
+
}
|
|
891
1197
|
appendAuditLog({
|
|
892
1198
|
event: 'sign_allowed',
|
|
893
1199
|
accountName,
|
|
@@ -955,9 +1261,20 @@ function processRequest(requestStr, socket) {
|
|
|
955
1261
|
}
|
|
956
1262
|
const privateKey = await loadCurrentPrivateKey(accountName);
|
|
957
1263
|
const broadcastNodeUrl = typeof request.nodeUrl === 'string' ? request.nodeUrl : null;
|
|
1264
|
+
// Start the broadcast deadline NOW, before the
|
|
1265
|
+
// serializeBroadcast queue wait, so total daemon wall time
|
|
1266
|
+
// per request stays inside the bot's outer socket window.
|
|
1267
|
+
// If the deadline fires while queued (burst concurrency),
|
|
1268
|
+
// the queued broadcast aborts via guard.isFired() before
|
|
1269
|
+
// it starts — it can never land after the bot verified
|
|
1270
|
+
// chain absence and re-broadcast the same operation.
|
|
1271
|
+
const broadcastStartedAt = Date.now();
|
|
1272
|
+
const broadcastGuard = createBroadcastGuard(accountName, broadcastStartedAt, resolveInnerDeadlineMs());
|
|
1273
|
+
activeGuards.add(broadcastGuard);
|
|
1274
|
+
const broadcastWorkRef = { work: null };
|
|
958
1275
|
let signResult;
|
|
959
1276
|
try {
|
|
960
|
-
signResult = await
|
|
1277
|
+
signResult = await serializeBroadcast(() => broadcastWithDeadline(accountName, privateKey, (client) => executeOperationsWithClient(client, operations), broadcastNodeUrl, { guard: broadcastGuard, startedAt: broadcastStartedAt, workRef: broadcastWorkRef }), () => broadcastWorkRef.work || Promise.resolve());
|
|
961
1278
|
}
|
|
962
1279
|
catch (broadcastErr) {
|
|
963
1280
|
if (broadcastErr && broadcastErr.code === DAEMON_CODES.BROADCAST_DEADLINE) {
|
|
@@ -965,7 +1282,7 @@ function processRequest(requestStr, socket) {
|
|
|
965
1282
|
event: 'sign_timeout',
|
|
966
1283
|
accountName,
|
|
967
1284
|
sessionId,
|
|
968
|
-
nodeUrl: broadcastNodeUrl,
|
|
1285
|
+
nodeUrl: broadcastGuard.currentNode || broadcastNodeUrl,
|
|
969
1286
|
opCount: operations.length,
|
|
970
1287
|
opTypes: operations.map((o) => o && o.op_name).filter(Boolean),
|
|
971
1288
|
ageMs: broadcastErr.ageMs,
|
|
@@ -974,10 +1291,15 @@ function processRequest(requestStr, socket) {
|
|
|
974
1291
|
: null,
|
|
975
1292
|
timestamp: new Date().toISOString(),
|
|
976
1293
|
});
|
|
977
|
-
return sendError(socket, 'chain status uncertain after inner deadline', DAEMON_CODES.BROADCAST_DEADLINE
|
|
1294
|
+
return sendError(socket, 'chain status uncertain after inner deadline', DAEMON_CODES.BROADCAST_DEADLINE, broadcastGuard.currentNode
|
|
1295
|
+
? { nodeUrl: broadcastGuard.currentNode }
|
|
1296
|
+
: {});
|
|
978
1297
|
}
|
|
979
1298
|
throw broadcastErr;
|
|
980
1299
|
}
|
|
1300
|
+
finally {
|
|
1301
|
+
activeGuards.delete(broadcastGuard);
|
|
1302
|
+
}
|
|
981
1303
|
appendAuditLog({
|
|
982
1304
|
event: 'sign_allowed',
|
|
983
1305
|
accountName,
|
|
@@ -1017,12 +1339,14 @@ function sendSuccess(socket, data) {
|
|
|
1017
1339
|
* @param {net.Socket} socket - Client socket
|
|
1018
1340
|
* @param {string} message - Error message
|
|
1019
1341
|
* @param {number} code - Error code
|
|
1342
|
+
* @param {Object} [extra] - Extra fields merged into the response (e.g. nodeUrl)
|
|
1020
1343
|
*/
|
|
1021
|
-
function sendError(socket, message, code = null) {
|
|
1344
|
+
function sendError(socket, message, code = null, extra = {}) {
|
|
1022
1345
|
const response = JSON.stringify({
|
|
1023
1346
|
success: false,
|
|
1024
1347
|
error: message,
|
|
1025
|
-
...(code ? { code } : {})
|
|
1348
|
+
...(code ? { code } : {}),
|
|
1349
|
+
...extra
|
|
1026
1350
|
});
|
|
1027
1351
|
socket.write(response + '\n');
|
|
1028
1352
|
socket.end();
|