Skip to content

Commit 0afed46

Browse files
committed
fix(sync): report invalid chain config instead of crashing contract-state sync
Wrap chain config / connections.json loading in a ConfigLoadError that names the file and the underlying parse/ENOENT error, so a malformed config no longer surfaces as a bare SyntaxError stack trace. syncContractState() now returns 'synced' | 'disabled' | 'config-error'; an invalid config is reported with an actionable message and no longer aborts 'sync all' (voters/accounts/proposals are still reported as completed). Standalone 'sync contract-state' still exits non-zero on config error.
1 parent 8747a39 commit 0afed46

2 files changed

Lines changed: 76 additions & 18 deletions

File tree

src/cli/hyp-control.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { request } from 'undici';
33
import { IndexerController } from './controller-client/controller.client.js';
44
import { printHeapStats, printMemoryUsage, printUsageMap } from './stats.control.js';
55
import { AccountSynchronizer } from './sync-modules/sync-accounts.js';
6-
import { ContractStateSynchronizer } from './sync-modules/sync-contract-state.js';
6+
import { ConfigLoadError, ContractStateSynchronizer } from './sync-modules/sync-contract-state.js';
77
import { ProposalSynchronizer } from './sync-modules/sync-proposals.js';
88
import { VoterSynchronizer } from './sync-modules/sync-voters.js';
99
import { QueueManager } from './queue-manager/queue.manager.js';
@@ -59,15 +59,27 @@ async function syncProposals(chain: string, host?: string) {
5959
await syncWithPauseResume(chain, 'table-proposals', new ProposalSynchronizer(chain), host);
6060
}
6161

62-
async function syncContractState(chain: string, host?: string, contract?: string, table?: string) {
63-
const contractStateSynchronizer = new ContractStateSynchronizer(chain);
62+
type ContractStateSyncResult = 'synced' | 'disabled' | 'config-error';
63+
64+
async function syncContractState(chain: string, host?: string, contract?: string, table?: string): Promise<ContractStateSyncResult> {
65+
let contractStateSynchronizer: ContractStateSynchronizer;
66+
try {
67+
contractStateSynchronizer = new ContractStateSynchronizer(chain);
68+
} catch (error) {
69+
if (error instanceof ConfigLoadError) {
70+
console.error(`\n❌ Contract state sync skipped: ${error.message}`);
71+
console.error(` Fix the config file above and re-run the sync.`);
72+
return 'config-error';
73+
}
74+
throw error;
75+
}
6476
// Check if contract state is enabled before proceeding
6577
if (!contractStateSynchronizer.isEnabled()) {
6678
console.log(`Contract state synchronization is not enabled for chain: ${chain}`);
67-
return false; // Return false to indicate no sync was performed
79+
return 'disabled';
6880
}
6981
await syncWithPauseResume(chain, 'dynamic-table', contractStateSynchronizer, host, contract, table);
70-
return true; // Return true to indicate sync was performed
82+
return 'synced';
7183
}
7284

7385
async function stopIndexer(chain: string, host?: string) {
@@ -550,14 +562,17 @@ async function getScalingInfo(chain: string, host?: string) {
550562
.description('Sync contract state for a specific chain')
551563
.action(async (chain: string, contract?: string, table?: string, args?: any) => {
552564
try {
553-
const syncPerformed = await syncContractState(chain, args.host, contract, table);
554-
// Only show completion message if sync was actually performed
555-
if (syncPerformed) {
565+
const result = await syncContractState(chain, args.host, contract, table);
566+
if (result === 'synced') {
556567
console.log('Sync completed for contractState');
557-
} else {
568+
process.exit(0);
569+
} else if (result === 'disabled') {
558570
console.log('Contract state synchronization skipped - feature is disabled in config');
571+
process.exit(0);
572+
} else {
573+
// config-error: actionable message already printed by syncContractState
574+
process.exit(1);
559575
}
560-
process.exit(0);
561576
} catch (error) {
562577
console.error('Error syncing contract state:', error);
563578
process.exit(1);
@@ -571,11 +586,16 @@ async function getScalingInfo(chain: string, host?: string) {
571586
await syncVoters(chain);
572587
await syncAccounts(chain, undefined, undefined);
573588
await syncProposals(chain);
574-
const contractStateSynced = await syncContractState(chain);
589+
const contractStateResult = await syncContractState(chain);
575590

576-
console.log(`Sync completed for all components`);
577-
if (!contractStateSynced) {
578-
console.log(`Note: Contract state sync was skipped (feature is disabled in config)`);
591+
if (contractStateResult === 'config-error') {
592+
console.log(`Sync completed for voters, accounts and proposals.`);
593+
console.log(`Note: Contract state sync was skipped due to an invalid config (see error above).`);
594+
} else {
595+
console.log(`Sync completed for all components`);
596+
if (contractStateResult === 'disabled') {
597+
console.log(`Note: Contract state sync was skipped (feature is disabled in config)`);
598+
}
579599
}
580600
} catch (error) {
581601
console.error('Error during sync:', error);

src/cli/sync-modules/sync-contract-state.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,39 @@ import { join } from 'node:path';
55
import { cargo } from 'async';
66
import { findAndValidatePrimaryKey } from '../utils/check-primary-key.js';
77

8+
/**
9+
* Raised when a JSON config file is missing or cannot be parsed.
10+
* Carries the offending path so callers can produce an actionable message
11+
* instead of a raw SyntaxError/ENOENT stack trace.
12+
*/
13+
export class ConfigLoadError extends Error {
14+
constructor(public readonly path: string, message: string) {
15+
super(message);
16+
this.name = 'ConfigLoadError';
17+
}
18+
}
19+
20+
/**
21+
* Read and parse a JSON config file, turning fs/JSON failures into a
22+
* ConfigLoadError that names the file and the underlying problem.
23+
*/
24+
function readJsonConfig<T>(path: string, label: string): T {
25+
let raw: string;
26+
try {
27+
raw = readFileSync(path, 'utf-8');
28+
} catch (err: any) {
29+
if (err?.code === 'ENOENT') {
30+
throw new ConfigLoadError(path, `${label} not found: ${path}`);
31+
}
32+
throw new ConfigLoadError(path, `Unable to read ${label} (${path}): ${err?.message ?? err}`);
33+
}
34+
try {
35+
return JSON.parse(raw) as T;
36+
} catch (err: any) {
37+
throw new ConfigLoadError(path, `Invalid JSON in ${label} (${path}): ${err?.message ?? err}`);
38+
}
39+
}
40+
841
interface ChainConfig {
942
features: {
1043
contract_state: {
@@ -43,19 +76,24 @@ export class ContractStateSynchronizer {
4376
private loadConfig(): ChainConfig {
4477
const configDir = join(import.meta.dirname, '../../../config/chains');
4578
const configPath = join(configDir, `${this.chain}.config.json`);
46-
return JSON.parse(readFileSync(configPath, 'utf-8'));
79+
return readJsonConfig<ChainConfig>(configPath, `chain config for "${this.chain}"`);
4780
}
4881

4982
private loadConnections() {
5083
const configDir = join(import.meta.dirname, '../../../config');
51-
return JSON.parse(readFileSync(join(configDir, 'connections.json'), 'utf-8'));
84+
const connectionsPath = join(configDir, 'connections.json');
85+
return readJsonConfig<any>(connectionsPath, 'connections.json');
5286
}
5387

5488
private createAPIClient(): APIClient {
5589
const connections = this.loadConnections();
56-
const endpoint = connections.chains[this.chain].http;
90+
const chainConn = connections.chains?.[this.chain];
91+
if (!chainConn) {
92+
throw new Error(`Chain "${this.chain}" not found in connections.json`);
93+
}
94+
const endpoint = chainConn.http;
5795
if (!endpoint) {
58-
throw new Error('No HTTP Endpoint!');
96+
throw new Error(`No HTTP endpoint configured for chain "${this.chain}" in connections.json`);
5997
}
6098
return new APIClient({ url: endpoint });
6199
}

0 commit comments

Comments
 (0)