Skip to content
Closed
8 changes: 8 additions & 0 deletions .changeset/nasty-olives-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@hyperlane-xyz/rebalancer': patch
'@hyperlane-xyz/deploy-sdk': patch
'@hyperlane-xyz/utils': minor
'@hyperlane-xyz/cli': patch
---

Changed `readJson()` and `readYamlOrJson()` to return `T | null` on empty JSON files, for consistency with YAML behavior and simplified error handling. All call sites updated with explicit null checks using `assert()` or null coalescing.
Comment on lines 1 to 8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Double-check the version bump level. The PR description calls this a breaking change; a patch bump might understate the impact depending on your changeset policy.

🤖 Prompt for AI Agents
In @.changeset/nasty-olives-hide.md around lines 1 - 7, The changeset claims
breaking behavior for readJson() and readYamlOrJson() now returning T | null;
verify the correct semver level for the three packages
'@hyperlane-xyz/deploy-sdk', '@hyperlane-xyz/utils', and '@hyperlane-xyz/cli'
and update the changeset if necessary (patch → minor or major) per our
repository policy; ensure the changeset metadata and any changelog entries
reflect that callers now must handle null from readJson/readYamlOrJson and bump
the package versions accordingly.

5 changes: 3 additions & 2 deletions typescript/cli/src/commands/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
DeployedCoreAddressesSchema,
normalizeConfig,
} from '@hyperlane-xyz/sdk';
import { diffObjMerge } from '@hyperlane-xyz/utils';
import { assert, diffObjMerge } from '@hyperlane-xyz/utils';

import {
createCoreDeployConfig,
Expand Down Expand Up @@ -238,7 +238,8 @@ export const check: CommandModuleWithContext<{
handler: async ({ context, chain, mailbox, config: configFilePath }) => {
logCommandHeader('Hyperlane Core Check');

const expectedCoreConfig: CoreConfig = await readYamlOrJson(configFilePath);
const expectedCoreConfig = await readYamlOrJson<CoreConfig>(configFilePath);
assert(expectedCoreConfig, `Empty core config file at ${configFilePath}`);
const onChainCoreConfig = await executeCoreRead({
context,
chain,
Expand Down
11 changes: 9 additions & 2 deletions typescript/cli/src/config/submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
type AnnotatedEV5Transaction,
type ChainName,
} from '@hyperlane-xyz/sdk';
import { type ProtocolType, errorToString } from '@hyperlane-xyz/utils';
import { type ProtocolType, assert, errorToString } from '@hyperlane-xyz/utils';

import { type WriteCommandContext } from '../context/types.js';
import { getSubmitterByStrategy } from '../deploy/warp.js';
Expand Down Expand Up @@ -58,5 +58,12 @@ export async function runSubmit({
export function getTransactions(
transactionsFilepath: string,
): AnnotatedEV5Transaction[] {
return readYamlOrJson<AnnotatedEV5Transaction[]>(transactionsFilepath.trim());
const transactions = readYamlOrJson<AnnotatedEV5Transaction[]>(
transactionsFilepath.trim(),
);
assert(
transactions,
`Empty transactions file at ${transactionsFilepath.trim()}`,
);
return transactions;
}
8 changes: 7 additions & 1 deletion typescript/cli/src/fork/fork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,15 @@ export async function runForkCommand({
);

let port = basePort;
// Wrapper to handle null returns from readYamlOrJson
const readYamlOrJsonNonNull = <T>(path: string): T => {
const result = readYamlOrJson<T>(path);
assert(result, `Empty config file at ${path}`);
return result;
};
const parsedForkConfig = forkedChainConfigByChainFromRaw(
forkConfig,
readYamlOrJson,
readYamlOrJsonNonNull,
);
const chainMetadataOverrides: ChainMap<{
blocks: ChainMetadata['blocks'];
Expand Down
19 changes: 9 additions & 10 deletions typescript/cli/src/submitters/EV5FileSubmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
import {
type Annotated,
type ProtocolType,
assert,
rootLogger,
} from '@hyperlane-xyz/utils';

Expand Down Expand Up @@ -60,15 +59,15 @@ export class EV5FileSubmitter
const allTxs = [...txs];

// Attempt to append transactions to existing filepath.
try {
const maybeExistingTxs = readYamlOrJson(filepath); // Can throw if file is empty
assert(
Array.isArray(maybeExistingTxs),
`Target filepath ${filepath} has existing data, but is not an array. Overwriting.`,
);
allTxs.unshift(...maybeExistingTxs);
} catch (e) {
this.logger.debug(`Invalid transactions read from ${filepath}: ${e}`);
const maybeExistingTxs = readYamlOrJson(filepath);
if (maybeExistingTxs !== null) {
if (!Array.isArray(maybeExistingTxs)) {
this.logger.debug(
`Target filepath ${filepath} has existing data, but is not an array. Overwriting.`,
);
} else {
allTxs.unshift(...maybeExistingTxs);
}
}

writeYamlOrJson(filepath, allTxs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { expect } from 'chai';
import { type CoreConfig, type HookConfig, HookType } from '@hyperlane-xyz/sdk';
import { ProtocolType, normalizeConfig } from '@hyperlane-xyz/utils';

import { readYamlOrJson, writeYamlOrJson } from '../../../utils/files.js';
import {
readYamlOrJsonOrThrow,
writeYamlOrJson,
} from '../../../utils/files.js';
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
import {
CORE_CONFIG_PATH_BY_PROTOCOL,
Expand All @@ -28,7 +31,7 @@ describe('hyperlane core apply hooks (Aleo E2E tests)', async function () {

// Reset the core deploy config before each test
beforeEach(async function () {
const coreConfig: CoreConfig = await readYamlOrJson(
const coreConfig = readYamlOrJsonOrThrow<CoreConfig>(
CORE_CONFIG_PATH_BY_PROTOCOL.aleo,
);
writeYamlOrJson(
Expand Down Expand Up @@ -74,7 +77,7 @@ describe('hyperlane core apply hooks (Aleo E2E tests)', async function () {
for (const hookConfig of Object.values(testCases)) {
for (const hookField of hookFields) {
it(`should update the ${hookField} to a ${hookConfig.type}`, async () => {
const coreConfig: CoreConfig = await readYamlOrJson(
const coreConfig = readYamlOrJsonOrThrow<CoreConfig>(
CORE_CONFIG_PATH_BY_PROTOCOL.aleo,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
} from '@hyperlane-xyz/sdk';
import { ProtocolType, normalizeConfig } from '@hyperlane-xyz/utils';

import { readYamlOrJson, writeYamlOrJson } from '../../../utils/files.js';
import {
readYamlOrJsonOrThrow,
writeYamlOrJson,
} from '../../../utils/files.js';
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
import {
CORE_CONFIG_PATH_BY_PROTOCOL,
Expand All @@ -33,7 +36,7 @@ describe('hyperlane core apply ism (Aleo E2E tests)', async function () {

// Reset the core deploy config before each test
beforeEach(async function () {
const coreConfig: CoreConfig = await readYamlOrJson(
const coreConfig = readYamlOrJsonOrThrow<CoreConfig>(
CORE_CONFIG_PATH_BY_PROTOCOL.aleo,
);
writeYamlOrJson(
Expand Down Expand Up @@ -70,7 +73,7 @@ describe('hyperlane core apply ism (Aleo E2E tests)', async function () {

for (const ismConfig of Object.values(testCases)) {
it(`should update the defaultIsm to a ${ismConfig.type}`, async () => {
const coreConfig: CoreConfig = await readYamlOrJson(
const coreConfig = readYamlOrJsonOrThrow<CoreConfig>(
CORE_CONFIG_PATH_BY_PROTOCOL.aleo,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { expect } from 'chai';
import { type CoreConfig } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';

import { readYamlOrJson, writeYamlOrJson } from '../../../utils/files.js';
import {
readYamlOrJsonOrThrow,
writeYamlOrJson,
} from '../../../utils/files.js';
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
import {
BURN_ADDRESS_BY_PROTOCOL,
Expand All @@ -28,7 +31,7 @@ describe('hyperlane core apply mailbox (Aleo E2E tests)', async function () {

// Reset the core deploy config before each test
beforeEach(async function () {
const coreConfig: CoreConfig = await readYamlOrJson(
const coreConfig = readYamlOrJsonOrThrow<CoreConfig>(
CORE_CONFIG_PATH_BY_PROTOCOL.aleo,
);
writeYamlOrJson(
Expand All @@ -44,7 +47,7 @@ describe('hyperlane core apply mailbox (Aleo E2E tests)', async function () {
});

it(`should update the mailbox owner to the specified one`, async () => {
const coreConfig: CoreConfig = await readYamlOrJson(
const coreConfig = readYamlOrJsonOrThrow<CoreConfig>(
CORE_CONFIG_PATH_BY_PROTOCOL.aleo,
);

Expand Down
7 changes: 5 additions & 2 deletions typescript/cli/src/tests/aleo/core/core-deploy.e2e-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
normalizeAddressEvm,
} from '@hyperlane-xyz/utils';

import { readYamlOrJson, writeYamlOrJson } from '../../../utils/files.js';
import {
readYamlOrJsonOrThrow,
writeYamlOrJson,
} from '../../../utils/files.js';
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
import {
BURN_ADDRESS_BY_PROTOCOL,
Expand Down Expand Up @@ -136,7 +139,7 @@ describe('hyperlane core deploy (Aleo E2E tests)', async function () {

for (const { description, expect } of testCases) {
it(description, async () => {
const coreConfig: CoreConfig = await readYamlOrJson(
const coreConfig = readYamlOrJsonOrThrow<CoreConfig>(
CORE_CONFIG_PATH_BY_PROTOCOL.aleo,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type ChainAddresses } from '@hyperlane-xyz/registry';
import { TokenType } from '@hyperlane-xyz/sdk';
import { ProtocolType, assert } from '@hyperlane-xyz/utils';

import { readYamlOrJson } from '../../../utils/files.js';
import { readYamlOrJsonOrThrow } from '../../../utils/files.js';
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
import { HyperlaneE2EWarpTestCommands } from '../../commands/warp.js';
import {
Expand Down Expand Up @@ -61,7 +61,7 @@ describe('hyperlane warp apply Hook updates (Aleo E2E tests)', async function ()
before(async function () {
await hyperlaneCore1.deploy(HYP_KEY_BY_PROTOCOL.aleo);

chain1CoreAddress = readYamlOrJson(
chain1CoreAddress = readYamlOrJsonOrThrow(
`${REGISTRY_PATH}/chains/${TEST_CHAIN_NAMES_BY_PROTOCOL.aleo.CHAIN_NAME_1}/addresses.yaml`,
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type ChainAddresses } from '@hyperlane-xyz/registry';
import { TokenType } from '@hyperlane-xyz/sdk';
import { ProtocolType, assert } from '@hyperlane-xyz/utils';

import { readYamlOrJson } from '../../../utils/files.js';
import { readYamlOrJsonOrThrow } from '../../../utils/files.js';
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
import { HyperlaneE2EWarpTestCommands } from '../../commands/warp.js';
import {
Expand Down Expand Up @@ -61,7 +61,7 @@ describe('hyperlane warp apply ISM updates (Aleo E2E tests)', async function ()
before(async function () {
await hyperlaneCore1.deploy(HYP_KEY_BY_PROTOCOL.aleo);

chain1CoreAddress = readYamlOrJson(
chain1CoreAddress = readYamlOrJsonOrThrow(
`${REGISTRY_PATH}/chains/${TEST_CHAIN_NAMES_BY_PROTOCOL.aleo.CHAIN_NAME_1}/addresses.yaml`,
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
} from '@hyperlane-xyz/sdk';
import { type Address, ProtocolType, assert } from '@hyperlane-xyz/utils';

import { readYamlOrJson, writeYamlOrJson } from '../../../utils/files.js';
import {
readYamlOrJsonOrThrow,
writeYamlOrJson,
} from '../../../utils/files.js';
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
import { HyperlaneE2EWarpTestCommands } from '../../commands/warp.js';
import {
Expand Down Expand Up @@ -78,10 +81,10 @@ describe('hyperlane warp apply ownership (Aleo E2E tests)', async function () {
hyperlaneCore2.deploy(HYP_KEY_BY_PROTOCOL.aleo),
]);

chain1CoreAddress = readYamlOrJson(
chain1CoreAddress = readYamlOrJsonOrThrow(
`${REGISTRY_PATH}/chains/${TEST_CHAIN_NAMES_BY_PROTOCOL.aleo.CHAIN_NAME_1}/addresses.yaml`,
);
chain2CoreAddress = readYamlOrJson(
chain2CoreAddress = readYamlOrJsonOrThrow(
`${REGISTRY_PATH}/chains/${TEST_CHAIN_NAMES_BY_PROTOCOL.aleo.CHAIN_NAME_2}/addresses.yaml`,
);
});
Expand Down Expand Up @@ -176,8 +179,10 @@ describe('hyperlane warp apply ownership (Aleo E2E tests)', async function () {
outputPath: WARP_READ_OUTPUT_PATH,
});

const updatedWarpDeployConfig: DerivedWarpRouteDeployConfig =
readYamlOrJson(WARP_READ_OUTPUT_PATH);
const updatedWarpDeployConfig =
readYamlOrJsonOrThrow<DerivedWarpRouteDeployConfig>(
WARP_READ_OUTPUT_PATH,
);

expect(
updatedWarpDeployConfig[TEST_CHAIN_NAMES_BY_PROTOCOL.aleo.CHAIN_NAME_1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
} from '@hyperlane-xyz/sdk';
import { ProtocolType, assert } from '@hyperlane-xyz/utils';

import { readYamlOrJson, writeYamlOrJson } from '../../../utils/files.js';
import {
readYamlOrJsonOrThrow,
writeYamlOrJson,
} from '../../../utils/files.js';
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
import { HyperlaneE2EWarpTestCommands } from '../../commands/warp.js';
import {
Expand Down Expand Up @@ -80,10 +83,10 @@ describe('hyperlane warp apply route extension (Aleo E2E tests)', async function
hyperlaneCore2.deploy(HYP_KEY_BY_PROTOCOL.aleo),
]);

chain1CoreAddress = readYamlOrJson(
chain1CoreAddress = readYamlOrJsonOrThrow(
`${REGISTRY_PATH}/chains/${TEST_CHAIN_NAMES_BY_PROTOCOL.aleo.CHAIN_NAME_1}/addresses.yaml`,
);
chain2CoreAddress = readYamlOrJson(
chain2CoreAddress = readYamlOrJsonOrThrow(
`${REGISTRY_PATH}/chains/${TEST_CHAIN_NAMES_BY_PROTOCOL.aleo.CHAIN_NAME_2}/addresses.yaml`,
);

Expand Down Expand Up @@ -139,8 +142,10 @@ describe('hyperlane warp apply route extension (Aleo E2E tests)', async function
outputPath: WARP_READ_OUTPUT_PATH,
});

const updatedWarpDeployConfig: DerivedWarpRouteDeployConfig =
readYamlOrJson(WARP_READ_OUTPUT_PATH);
const updatedWarpDeployConfig =
readYamlOrJsonOrThrow<DerivedWarpRouteDeployConfig>(
WARP_READ_OUTPUT_PATH,
);

for (const chainName of Object.keys(warpDeployConfig)) {
assertWarpRouteConfig(
Expand Down
13 changes: 8 additions & 5 deletions typescript/cli/src/tests/aleo/warp/warp-deploy.e2e-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
} from '@hyperlane-xyz/sdk';
import { ProtocolType, assert } from '@hyperlane-xyz/utils';

import { readYamlOrJson, writeYamlOrJson } from '../../../utils/files.js';
import {
readYamlOrJsonOrThrow,
writeYamlOrJson,
} from '../../../utils/files.js';
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
import { HyperlaneE2EWarpTestCommands } from '../../commands/warp.js';
import {
Expand Down Expand Up @@ -82,10 +85,10 @@ describe('hyperlane warp deploy (Aleo E2E tests)', async function () {
hyperlaneCore2.deploy(HYP_KEY_BY_PROTOCOL.aleo),
]);

chain1CoreAddress = readYamlOrJson(
chain1CoreAddress = readYamlOrJsonOrThrow<ChainAddresses>(
`${REGISTRY_PATH}/chains/${TEST_CHAIN_NAMES_BY_PROTOCOL.aleo.CHAIN_NAME_1}/addresses.yaml`,
);
chain2CoreAddress = readYamlOrJson(
chain2CoreAddress = readYamlOrJsonOrThrow<ChainAddresses>(
`${REGISTRY_PATH}/chains/${TEST_CHAIN_NAMES_BY_PROTOCOL.aleo.CHAIN_NAME_2}/addresses.yaml`,
);

Expand Down Expand Up @@ -137,7 +140,7 @@ describe('hyperlane warp deploy (Aleo E2E tests)', async function () {
outputPath: WARP_READ_OUTPUT_PATH,
});

const config: DerivedWarpRouteDeployConfig = readYamlOrJson(
const config = readYamlOrJsonOrThrow<DerivedWarpRouteDeployConfig>(
WARP_READ_OUTPUT_PATH,
);

Expand Down Expand Up @@ -174,7 +177,7 @@ describe('hyperlane warp deploy (Aleo E2E tests)', async function () {
outputPath: WARP_READ_OUTPUT_PATH,
});

const config: DerivedWarpRouteDeployConfig = readYamlOrJson(
const config = readYamlOrJsonOrThrow<DerivedWarpRouteDeployConfig>(
WARP_READ_OUTPUT_PATH,
);

Expand Down
Loading
Loading