From 4cfd712d21cc1cc2fea520465905fd9fbd4ec0dc Mon Sep 17 00:00:00 2001 From: Xeonus Date: Wed, 17 Jun 2026 11:48:09 +0200 Subject: [PATCH 1/3] chore: fix array parsing from balancer/backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What was wrong _to_json only rewrote spreads of inline array literals (...[ ] → [ ]). When balancer/backend added workerJobs: [...activeChainWorkerJobsGeneric, ...] (spreads of imported identifiers), those tokens reached json.loads and raised JSONDecodeError for every chain. Subgraph.get_subgraph_url_from_backend_config() swallowed it (bare try/except → None) and silently fell back to stale studio.thegraph.com URLs — which regressed the bal_addresses scheduled PR with outdated sources This change fixes the issue by adding two regexes right before the existing one to properly handle the new data structure in the backend. --- bal_tools/ts_config_loader.py | 7 ++++ tests/test_ts_config_loader.py | 61 +++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/bal_tools/ts_config_loader.py b/bal_tools/ts_config_loader.py index 3c9704a..4d8a9cb 100644 --- a/bal_tools/ts_config_loader.py +++ b/bal_tools/ts_config_loader.py @@ -176,6 +176,13 @@ def handle_spread_with_map(text): obj = handle_spread_with_map(obj) + # Drop spreads of imported identifiers we can't resolve, e.g. + # workerJobs: [...activeChainWorkerJobsGeneric, ...activeChainWorkerJobsV2] + # These reference imported arrays that aren't available to the parser. + obj = re.sub(r"\.\.\.[A-Za-z_$][\w$]*\s*,?", "", obj) + # Remove any leftover leading comma the removal above may produce ([, ...]) + obj = re.sub(r"\[\s*,", "[", obj) + # Then handle simple spread operators: ...[array] -> [array] obj = re.sub(r"\.\.\.\s*\[", "[", obj) diff --git a/tests/test_ts_config_loader.py b/tests/test_ts_config_loader.py index 6cba766..5fbc047 100644 --- a/tests/test_ts_config_loader.py +++ b/tests/test_ts_config_loader.py @@ -6,7 +6,55 @@ """ import requests -from bal_tools.ts_config_loader import ts_config_loader +from bal_tools.ts_config_loader import ts_config_loader, _to_json, _extract_object_literal + + +def test_spread_of_imported_identifiers(): + """Regression: spreads of imported identifiers (e.g. workerJobs: + [...activeChainWorkerJobsGeneric]) must not break JSON parsing. + + The backend network configs import worker-job arrays and spread them into + `workerJobs`. The parser can't resolve those imports, so it should drop the + spreads and still produce a valid object rather than raising JSONDecodeError. + """ + import json + + ts = """ +import { activeChainWorkerJobsGeneric, activeChainWorkerJobsV2 } from './worker-jobs'; + +export default { + chain: { + slug: 'mychain', + }, + subgraphs: { + balancer: `https://example.com/v2-mychain-smol/latest/gn`, + gauge: `https://example.com/balancer-gauges-mychain/latest/gn`, + }, + workerJobs: [...activeChainWorkerJobsGeneric, ...activeChainWorkerJobsV2], +}; +""" + parsed = json.loads(_to_json(_extract_object_literal(ts))) + assert parsed["workerJobs"] == [] + assert parsed["subgraphs"]["balancer"].endswith("v2-mychain-smol/latest/gn") + assert parsed["chain"]["slug"] == "mychain" + + +def test_spread_of_import_mixed_with_literal(): + """A spread of an import mixed with real array entries should drop only the + unresolved spread and keep the literal values.""" + import json + + ts = """ +import { extraJobs } from './worker-jobs'; + +export default { + stakingServices: ['gauge', ...extraJobs], + workerJobs: [...extraJobs, 'literalJob'], +}; +""" + parsed = json.loads(_to_json(_extract_object_literal(ts))) + assert parsed["stakingServices"] == ["gauge"] + assert parsed["workerJobs"] == ["literalJob"] def test_all_backend_configs_load(): @@ -32,19 +80,30 @@ def test_all_backend_configs_load(): failed_configs = [] + loaded_any = False for config_file in config_files: chain = config_file.replace(".ts", "") url = f"https://raw.githubusercontent.com/balancer/backend/refs/heads/v3-main/config/{config_file}" + # The config/ directory also holds helper modules (e.g. worker-jobs.ts, + # types.ts, chain-id-to-chain.ts) that aren't network configs and have no + # `export default` literal. Skip those; only network configs are parseable. + raw = requests.get(url).text + if "export default" not in raw: + continue + try: # Should not raise any exceptions config = ts_config_loader(url) assert isinstance( config, dict ), f"Config for {chain} should be a dictionary" + loaded_any = True except Exception as e: failed_configs.append((chain, str(e))) + assert loaded_any, "No network configs were loaded" + assert ( len(failed_configs) == 0 ), f"Failed to load {len(failed_configs)} configs: {failed_configs}" From 02ba0f081591acfe3d6684d857fb0452b9553129 Mon Sep 17 00:00:00 2001 From: Xeonus <496505+Xeonus@users.noreply.github.com> Date: Wed, 17 Jun 2026 09:49:16 +0000 Subject: [PATCH 2/3] style: ci lint with `black` --- tests/test_ts_config_loader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_ts_config_loader.py b/tests/test_ts_config_loader.py index 5fbc047..8b91d74 100644 --- a/tests/test_ts_config_loader.py +++ b/tests/test_ts_config_loader.py @@ -6,7 +6,11 @@ """ import requests -from bal_tools.ts_config_loader import ts_config_loader, _to_json, _extract_object_literal +from bal_tools.ts_config_loader import ( + ts_config_loader, + _to_json, + _extract_object_literal, +) def test_spread_of_imported_identifiers(): From 019415e6807fa4676fdcfaa7c209db1b52dd6a56 Mon Sep 17 00:00:00 2001 From: Xeonus Date: Wed, 17 Jun 2026 11:53:02 +0200 Subject: [PATCH 3/3] refactor: remove stakeDAO test --- tests/test_ecosystem.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 tests/test_ecosystem.py diff --git a/tests/test_ecosystem.py b/tests/test_ecosystem.py deleted file mode 100644 index 8707be9..0000000 --- a/tests/test_ecosystem.py +++ /dev/null @@ -1,9 +0,0 @@ -from bal_tools.ecosystem import StakeDAO - - -def test_calculate_dynamic_min_incentive(): - sd = StakeDAO() - result = sd.calculate_dynamic_min_incentive() - - assert isinstance(result, int) - assert result > 0