Skip to content

Commit fb38809

Browse files
committed
fix: canonicalize error-code constants across daemon, bot client, and tests
Adds DAEMON_CODES to constants.ts (BROADCAST_DEADLINE, CREDENTIAL_DAEMON_UNAVAILABLE) and MasterPasswordError.code static property, replacing 12+ literal sites with single-source-of-truth references. Follows the same pattern as the DAEMON_ERRORS fix in 0b2fdca — shared string + no canonical symbol = silent breakage. ## BROADCAST_DEADLINE Files: credential-daemon.ts, modules/dexbot_credential_client.ts, tests/test_uncertain_broadcast.ts - 7 production sites (error creation, broadcastErr.code checks, sendError calls) and 3 test mocks now reference DAEMON_CODES.BROADCAST_DEADLINE instead of the literal. - Blast radius: 4 sender call-sites + 1 receiver + 1 receiver-fallback prefix + 3 mocks. ## CREDENTIAL_DAEMON_UNAVAILABLE Files: modules/dexbot_class.ts, tests/test_fill_replay_guards.ts - Wrapped error assignment and predicate check in _ensureCredentialDaemonWritable and _isCredentialDaemonError reference DAEMON_CODES.CREDENTIAL_DAEMON_UNAVAILABLE. - Test mock updated to match. ## MASTER_PASSWORD_FAILED Files: modules/chain_keys.ts, tests/test_chain_keys_master_password_failure.ts, tests/test_dexbot_start_master_password_failure_output.ts - MasterPasswordError exposes static .code property; constructor and isMasterPasswordFailure predicate reference MasterPasswordError.code instead of the literal. Tests import and reference the static. ## Testing Notes - test_uncertain_broadcast.ts (all 21 tests): PASS - test_chain_keys_master_password_failure.ts: PASS - test_dexbot_start_master_password_failure_output.ts: PASS - test_fill_replay_guards.ts: pre-existing failure (100 !== 102.5), unrelated to these changes - Build: clean compile
1 parent 9199b85 commit fb38809

11 files changed

Lines changed: 46 additions & 27 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file.
1010
- Existing installations with a pre-existing `UPDATER.ACTIVE: true` in `general.settings.json` are unaffected; the new default only applies to fresh installs or users who remove the `UPDATER` key from their settings.
1111
- **Fix**: exit code 2 → 0 on "already up to date" — PM2's `cron_restart` treats non-zero exits as failures, causing false-positive error logs on every successful no-op tick (`scripts/update.ts:262`).
1212
- **Fix**: restore stashed local changes after `git pull` with ref-specific pop — uses the exact stash ref (matched by message) instead of bare `git stash pop`, preventing cross-contamination with other stash entries. Also checks `git status --porcelain` for unmerged paths after pop and logs the error object on failure (`scripts/update.ts:299-307`).
13+
- **Fix**: align DAEMON_ERRORS string checks with canonical constants — `DaemonKeyStore` in `key_store.ts` used locally-hardcoded strings (`'SESSION_EXPIRED'`, `'SOURCE_AUTH_DENIED'`) that did not match the actual daemon error values from `constants.ts:520`, causing session refresh/retry to silently never trigger. Removed the local shadow; test mocks updated to match (`0b2fdca`).
14+
- **Fix**: canonicalize BROADCAST_DEADLINE, CREDENTIAL_DAEMON_UNAVAILABLE, and MASTER_PASSWORD_FAILED — adds `DAEMON_CODES` to `constants.ts` (modeled after `DAEMON_ERRORS`) and `MasterPasswordError.code` static property, replacing 12+ literal sites across the credential daemon, bot client, `dexbot_class`, `chain_keys`, and 4 test files with single-source-of-truth references to prevent silent recovery-path breakage from string drift.
1315

1416
## [1.0.1] - 2026-06-24 - Bootstrap Fill Pipeline, AccountOrders Simplification & Timer Guard
1517

credential-daemon.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const { path } = require('./modules/path_api');
7171
const os = require('os');
7272
const { randomBytes } = require('./modules/crypto/sync');
7373
const chainKeys = require('./modules/chain_keys');
74-
const { TIMING, NODE_MANAGEMENT, DAEMON_ERRORS } = require('./modules/constants');
74+
const { TIMING, NODE_MANAGEMENT, DAEMON_ERRORS, DAEMON_CODES } = require('./modules/constants');
7575
const { readGeneralSettings } = require('./modules/general_settings');
7676
const { orderNodesForSettings } = require('./modules/node_health_cache');
7777
const credentialPolicy = require('./modules/credential_policy');
@@ -393,9 +393,9 @@ async function broadcastWithRetry(accountName: any, privateKey: any, broadcastFn
393393
const deadlinePromise = new Promise((_, reject) => {
394394
deadlineTimer = setTimeout(() => {
395395
const err: any = new Error(
396-
`BROADCAST_DEADLINE:inner broadcast deadline ${innerDeadlineMs}ms exceeded`
396+
`${DAEMON_CODES.BROADCAST_DEADLINE}:inner broadcast deadline ${innerDeadlineMs}ms exceeded`
397397
);
398-
err.code = 'BROADCAST_DEADLINE';
398+
err.code = DAEMON_CODES.BROADCAST_DEADLINE;
399399
err.uncertain = true;
400400
err.accountName = accountName;
401401
err.startedAt = startedAt;
@@ -809,7 +809,7 @@ function processRequest(requestStr: string, socket: any) {
809809
(client: any) => client.broadcast(operation)
810810
);
811811
} catch (broadcastErr: any) {
812-
if (broadcastErr && broadcastErr.code === 'BROADCAST_DEADLINE') {
812+
if (broadcastErr && broadcastErr.code === DAEMON_CODES.BROADCAST_DEADLINE) {
813813
appendAuditLog({
814814
event: 'sign_timeout',
815815
accountName,
@@ -828,7 +828,7 @@ function processRequest(requestStr: string, socket: any) {
828828
return sendError(
829829
socket,
830830
'chain status uncertain after inner deadline',
831-
'BROADCAST_DEADLINE'
831+
DAEMON_CODES.BROADCAST_DEADLINE
832832
);
833833
}
834834
throw broadcastErr;
@@ -914,7 +914,7 @@ function processRequest(requestStr: string, socket: any) {
914914
(client: any) => executeOperationsWithClient(client, operations)
915915
);
916916
} catch (broadcastErr: any) {
917-
if (broadcastErr && broadcastErr.code === 'BROADCAST_DEADLINE') {
917+
if (broadcastErr && broadcastErr.code === DAEMON_CODES.BROADCAST_DEADLINE) {
918918
appendAuditLog({
919919
event: 'sign_timeout',
920920
accountName,
@@ -930,7 +930,7 @@ function processRequest(requestStr: string, socket: any) {
930930
return sendError(
931931
socket,
932932
'chain status uncertain after inner deadline',
933-
'BROADCAST_DEADLINE'
933+
DAEMON_CODES.BROADCAST_DEADLINE
934934
);
935935
}
936936
throw broadcastErr;

docs/EVOLUTION.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ Consolidated the market adapter with split data sources (Kibana, native API), AM
6464

6565
**Jun 22**: Browser-safe surface enforcement completed with lazy require wrappers and storage adapter path fix. Credit runtime extended with `disallowedDealIds` filter for 1.22.x BitShares compatibility and `ratio``outputWeight` rename with backward-compat shim. Doc sweep across 15 files.
6666

67+
**Jun 25**: DAEMON_ERRORS retry-path fix — `DaemonKeyStore` session-expiry retry never fired due to locally-hardcoded mismatch with canonical constants. Canonical error-code hardening — `DAEMON_CODES` added to `constants.ts` for `BROADCAST_DEADLINE`/`CREDENTIAL_DAEMON_UNAVAILABLE`, `MasterPasswordError.code` static property, replacing 12+ literal sites with single-source-of-truth references.
68+
6769
---
6870

6971
## Architecture Evolution
@@ -157,10 +159,10 @@ Removed all 89 @ts-nocheck directives, added type annotations across 67 files. R
157159
First stable release. Profile validation, logging overhaul, AMA delta threshold, on-chain authority resolution, credential hardening (8 finding groups), centralization of project-root/fs/math/magic-number utilities, error-path hardening, doc sweep. Post-release: headless unlock mode, Credit/MPA Claw bridge, credit runtime fixes, credential daemon hardening, test auto-discovery, settings merge consolidation, shared-account fund registry, credit/MPA collateral proportional allocation, vendored uPlot library, node config editor, analyze-git Chart.js→uPlot migration, fund registry whitelist fixes. Browser compatibility: six portable abstractions (`StorageAdapter`, `CryptoProvider`, `Config`, `PATHS`, `ProcessDiscovery`, `KeyStore`), `env.ts`, `Runtime` singleton, `path_api.ts`, pure-JS crypto fallbacks, `ecc.browser.ts`, `ecc_selector.ts`, lazy `ws`/`pm2` loading, browser bundle verification, 1288-line test suite, 15 newly browser-safe modules, 28-check dist-level verification. Credit runtime multi-asset collateral fixes and stale reborrow guard. I/O pipeline centralization through StorageAdapter. Final browser-compat gaps closed. Browser-safe enforcement, storage adapter path fix, `disallowedDealIds` filter, `outputWeight` rename, doc sweep.
158160

159161
### v1.0.0 → v1.0.1 (4 commits)
160-
Bootstrap fill pipeline refactored to use the standard same-side replacement path via `_processFillsWithBatching`, eliminating the only cross-side rotation in the codebase. AccountOrders simplified to one bot per file, removing the `{bots: {[key]: ...}}` wrapper that caused doubled-entry bugs during bot-id migration. Deferred maintenance timer guard prevents rare null/undefined spread errors during periodic blockchain fetches.
162+
Bootstrap fill pipeline refactored to same-side replacement, AccountOrders simplified to one bot per file, deferred maintenance timer guard.
161163

162-
### v1.0.1 → v1.0.2 (3 changes)
163-
Auto-update disabled by default (`ACTIVE: true``false`) — explicit operator opt-in required for unattended code changes on financial systems. Update script hardened: exit code 2 → 0 on "already up to date" stops PM2 false-positive error logging, and `git stash pop` added after pull to restore previously stashed uncommitted work.
164+
### v1.0.1 → v1.0.2 (4 commits)
165+
Auto-update disabled by default, update script hardened, DAEMON_ERRORS retry-path fix, and canonical error-code hardening via `DAEMON_CODES`/`MasterPasswordError.code` static.
164166

165167
---
166168

modules/chain_keys.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,11 +534,12 @@ function verifyCurrentPassword(password, accountsData) {
534534
}
535535

536536
class MasterPasswordError extends Error {
537+
static code = 'MASTER_PASSWORD_FAILED';
537538
code: string;
538539
constructor(message: string) {
539540
super(message);
540541
this.name = 'MasterPasswordError';
541-
this.code = 'MASTER_PASSWORD_FAILED';
542+
this.code = MasterPasswordError.code;
542543
}
543544
}
544545

@@ -548,7 +549,7 @@ class MasterPasswordError extends Error {
548549
* @returns {boolean} True if the error indicates a master password failure
549550
*/
550551
function isMasterPasswordFailure(err) {
551-
return !!(err && (err instanceof MasterPasswordError || err.code === 'MASTER_PASSWORD_FAILED'));
552+
return !!(err && (err instanceof MasterPasswordError || err.code === MasterPasswordError.code));
552553
}
553554

554555
const MASTER_PASSWORD_MAX_ATTEMPTS = CREDENTIAL_PROMPTS.MAX_MASTER_PASSWORD_ATTEMPTS;

modules/constants.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,16 @@ const DAEMON_ERRORS = Object.freeze({
522522
SOURCE_AUTH_DENIED: 'invalid source authentication',
523523
});
524524

525+
// DAEMON_CODES: Canonical error-code constants shared between the credential
526+
// daemon, the bot client, and tests. Every code value is identical to its
527+
// property name so that a receiver comparing against e.g.
528+
// err.code === DAEMON_CODES.BROADCAST_DEADLINE stays in sync with
529+
// senders that assign the same symbol.
530+
const DAEMON_CODES = Object.freeze({
531+
BROADCAST_DEADLINE: 'BROADCAST_DEADLINE',
532+
CREDENTIAL_DAEMON_UNAVAILABLE: 'CREDENTIAL_DAEMON_UNAVAILABLE',
533+
});
534+
525535
// Interactive credential-prompt limits
526536
// (modules/chain_keys.ts master-password authentication, etc.).
527537
// Capped to prevent infinite stdin loops on a corrupted vault or a forgotten
@@ -1594,4 +1604,4 @@ Object.freeze(MARKET_ADAPTER.AMAS);
15941604
Object.freeze(MARKET_ADAPTER);
15951605
Object.freeze(CREDENTIAL_PROMPTS);
15961606

1597-
export = { ORDER_TYPES, ORDER_STATES, REBALANCE_STATES, COW_ACTIONS, DEFAULT_CONFIG, TIMING, GRID_LIMITS, LOG_LEVEL, LOGGING_CONFIG, INCREMENT_BOUNDS, FEE_PARAMETERS, CR_ZONES, DEFAULT_TARGET_CR, API_LIMITS, FILL_PROCESSING, MAINTENANCE, NODE_MANAGEMENT, PIPELINE_TIMING, UPDATER, LAUNCHER, COW_PERFORMANCE, NATIVE_CLIENT, MARKET_ADAPTER, BUILD_DIR, BTS_PRECISION, DAEMON_ERRORS, CREDENTIAL_PROMPTS };
1607+
export = { ORDER_TYPES, ORDER_STATES, REBALANCE_STATES, COW_ACTIONS, DEFAULT_CONFIG, TIMING, GRID_LIMITS, LOG_LEVEL, LOGGING_CONFIG, INCREMENT_BOUNDS, FEE_PARAMETERS, CR_ZONES, DEFAULT_TARGET_CR, API_LIMITS, FILL_PROCESSING, MAINTENANCE, NODE_MANAGEMENT, PIPELINE_TIMING, UPDATER, LAUNCHER, COW_PERFORMANCE, NATIVE_CLIENT, MARKET_ADAPTER, BUILD_DIR, BTS_PRECISION, DAEMON_ERRORS, DAEMON_CODES, CREDENTIAL_PROMPTS };

modules/dexbot_class.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const {
9999
MAINTENANCE,
100100
GRID_LIMITS,
101101
FILL_PROCESSING,
102+
DAEMON_CODES,
102103
} = require('./constants');
103104
const { PATHS, getRecalculateTriggerFile } = require('./paths');
104105
const { attemptResumePersistedGridByPriceMatch, decideStartupGridAction, reconcileGridOrders } = require('./order/grid_reconcile');
@@ -3336,7 +3337,7 @@ class DEXBot {
33363337
this._suspendGridPersistenceForCredentialOutage(message);
33373338
this.manager?.logger?.log?.(`[CREDENTIAL] ${message}. Write operations paused; re-unlock with node pm2.`, 'error');
33383339
const wrapped: any = new Error(message);
3339-
wrapped.code = 'CREDENTIAL_DAEMON_UNAVAILABLE';
3340+
wrapped.code = DAEMON_CODES.CREDENTIAL_DAEMON_UNAVAILABLE;
33403341
wrapped.cause = err;
33413342
throw wrapped;
33423343
}
@@ -3349,7 +3350,7 @@ class DEXBot {
33493350
*/
33503351
_isCredentialDaemonError(err) {
33513352
if (!err) return false;
3352-
if (err.code === 'CREDENTIAL_DAEMON_UNAVAILABLE') return true;
3353+
if (err.code === DAEMON_CODES.CREDENTIAL_DAEMON_UNAVAILABLE) return true;
33533354
const message = String(err.message || '');
33543355
return /Credential daemon|Daemon connection failed|daemon .*unavailable|dexbot-cred-daemon\.sock|ECONNREFUSED|ENOENT/.test(message);
33553356
}

modules/dexbot_credential_client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ try {
1010
const { getStorage } = require('./storage');
1111
const storage = getStorage();
1212
const { createHmac } = require('./crypto/sync');
13-
const { TIMING } = require('./constants');
13+
const { TIMING, DAEMON_CODES } = require('./constants');
1414
const {
1515
getCredentialReadyFilePath,
1616
getCredentialSocketPath,
@@ -256,8 +256,8 @@ function executeOperationsViaCredentialDaemon(accountName: string, operations: a
256256
// the uncertainOnTimeout flag.
257257
if (
258258
isBroadcast &&
259-
(errCode === 'BROADCAST_DEADLINE' ||
260-
(typeof errMsg === 'string' && errMsg.startsWith('BROADCAST_DEADLINE')))
259+
(errCode === DAEMON_CODES.BROADCAST_DEADLINE ||
260+
(typeof errMsg === 'string' && errMsg.startsWith(DAEMON_CODES.BROADCAST_DEADLINE)))
261261
) {
262262
throw new BroadcastUncertainError(
263263
`Credential daemon broadcast uncertain: ${errCode || errMsg}`,

tests/test_chain_keys_master_password_failure.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ console.log('Running chain_keys master password failure tests');
66
const masterPasswordError = new chainKeys.MasterPasswordError('Incorrect master password after 3 attempts.');
77
assert.strictEqual(chainKeys.isMasterPasswordFailure(masterPasswordError), true, 'MasterPasswordError instances should be recognized');
88
assert.strictEqual(
9-
chainKeys.isMasterPasswordFailure({ code: 'MASTER_PASSWORD_FAILED', message: 'Incorrect master password after 3 attempts.' }),
9+
chainKeys.isMasterPasswordFailure({ code: chainKeys.MasterPasswordError.code, message: 'Incorrect master password after 3 attempts.' }),
1010
true,
1111
'plain errors carrying MASTER_PASSWORD_FAILED should be recognized'
1212
);

tests/test_dexbot_start_master_password_failure_output.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const systemPath = require.resolve('../modules/order/utils/system');
1313
const accountBotsPath = require.resolve('../modules/account_bots');
1414
const bitsharesClientPath = require.resolve('../modules/bitshares_client');
1515

16+
// Import real MasterPasswordError before the chain_keys stub replaces it
17+
const { MasterPasswordError } = require('../modules/chain_keys');
18+
1619
const originalDexbotModule = require.cache[dexbotPath];
1720
const originalBotSettings = require.cache[botSettingsPath];
1821
const originalDexbotClass = require.cache[dexbotClassPath];
@@ -74,7 +77,7 @@ function installStubs() {
7477

7578
const masterPasswordError = new Error('Incorrect master password after 3 attempts.');
7679
masterPasswordError.name = 'MasterPasswordError';
77-
(masterPasswordError as any).code = 'MASTER_PASSWORD_FAILED';
80+
(masterPasswordError as any).code = MasterPasswordError.code;
7881

7982
class StubSharedDEXBot {
8083
[key: string]: any;
@@ -101,7 +104,7 @@ function installStubs() {
101104
authenticate: async () => 'test-password',
102105
isDaemonReady: () => false,
103106
isDaemonResponsive: async () => false,
104-
isMasterPasswordFailure: (err) => !!(err && (err.code === 'MASTER_PASSWORD_FAILED' || err.name === 'MasterPasswordError')),
107+
isMasterPasswordFailure: (err) => !!(err && (err.code === MasterPasswordError.code || err.name === 'MasterPasswordError')),
105108
});
106109
setCachedModule(gracefulShutdownPath, {
107110
setupGracefulShutdown: () => {},

tests/test_fill_replay_guards.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const assert = require('assert');
33
const DEXBot = require('../modules/dexbot_class');
44
const chainOrders = require('../modules/chain_orders');
55
const { OrderManager } = require('../modules/order');
6-
const { ORDER_STATES, ORDER_TYPES, TIMING } = require('../modules/constants');
6+
const { ORDER_STATES, ORDER_TYPES, TIMING, DAEMON_CODES } = require('../modules/constants');
77
const { buildFillKey } = require('../modules/order/utils/order');
88
const {
99
ProcessedFillStore,
@@ -389,7 +389,7 @@ async function runTests() {
389389
};
390390
bot._executeBatchIfNeeded = async () => {
391391
const err = new Error('Credential daemon unavailable before COW batch broadcast: ENOENT');
392-
(err as any).code = 'CREDENTIAL_DAEMON_UNAVAILABLE';
392+
(err as any).code = DAEMON_CODES.CREDENTIAL_DAEMON_UNAVAILABLE;
393393
throw err;
394394
};
395395
bot._triggerStateRecoverySync = async () => {

0 commit comments

Comments
 (0)