Skip to content

Commit 02c499d

Browse files
authored
feat(sealevel-sdk, cli): svm sdk minimal core deployments impl (#8385)
1 parent 5ac20e8 commit 02c499d

49 files changed

Lines changed: 2052 additions & 176 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test-cli-e2e.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ on:
2020
type: string
2121
has_rebalancer:
2222
type: string
23+
has_svm:
24+
type: string
2325

2426
env:
2527
TURBO_TELEMETRY_DISABLED: 1
@@ -389,6 +391,61 @@ jobs:
389391
env:
390392
CLI_E2E_TEST: ${{ matrix.test }}
391393

394+
# ── SVM (Sealevel) ──────────────────────────────────
395+
396+
cli-svm-e2e-smoke:
397+
runs-on: depot-ubuntu-24.04
398+
if: (inputs.run_cli_e2e == 'true' || inputs.has_svm == 'true') && inputs.ci_tier == 'smoke'
399+
timeout-minutes: 15
400+
strategy:
401+
fail-fast: false
402+
matrix:
403+
test:
404+
- core-deploy
405+
steps:
406+
- uses: actions/checkout@v6
407+
with:
408+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
409+
submodules: recursive
410+
persist-credentials: false
411+
- name: install-hyperlane-cli
412+
uses: ./.github/actions/install-cli
413+
with:
414+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
415+
- name: Checkout registry
416+
uses: ./.github/actions/checkout-registry
417+
- name: CLI sealevel e2e tests (${{ matrix.test }})
418+
run: pnpm -C typescript/cli test:sealevel:e2e
419+
env:
420+
CLI_E2E_TEST: ${{ matrix.test }}
421+
422+
cli-svm-e2e-matrix:
423+
runs-on: depot-ubuntu-24.04
424+
if: (inputs.run_cli_e2e == 'true' || inputs.has_svm == 'true') && inputs.ci_tier == 'full'
425+
timeout-minutes: 15
426+
strategy:
427+
fail-fast: false
428+
matrix:
429+
test:
430+
- core-deploy
431+
- core-apply
432+
steps:
433+
- uses: actions/checkout@v6
434+
with:
435+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
436+
submodules: recursive
437+
persist-credentials: false
438+
- name: install-hyperlane-cli
439+
uses: ./.github/actions/install-cli
440+
with:
441+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
442+
- name: Checkout registry
443+
uses: ./.github/actions/checkout-registry
444+
- name: CLI sealevel e2e tests (${{ matrix.test }})
445+
run: pnpm -C typescript/cli test:sealevel:e2e
446+
env:
447+
CLI_E2E_TEST: ${{ matrix.test }}
448+
392449
# ── Package-specific targeted tests ─────────────────
393450
# Run only when peripheral packages change, testing just the
394451
# specific CLI commands that use them. Full coverage in merge queue.

.github/workflows/test-sdk-e2e.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ jobs:
129129
test:
130130
- ism
131131
- hook
132+
- mailbox
133+
- validator-announce
132134
- native-token
133135
- synthetic-token
134136
- collateral-token
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
owner: 6ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWt
2+
defaultIsm:
3+
type: testIsm
4+
defaultHook:
5+
type: merkleTreeHook
6+
requiredHook:
7+
type: merkleTreeHook
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
owner: 6ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWt
2+
defaultIsm:
3+
type: testIsm
4+
defaultHook:
5+
type: interchainGasPaymaster
6+
owner: 6ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWt
7+
beneficiary: 6ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWt
8+
oracleKey: 6ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWt
9+
overhead: {}
10+
oracleConfig: {}
11+
requiredHook:
12+
type: merkleTreeHook

typescript/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"test:cosmosnative:e2e": "./src/tests/cosmosnative/run-e2e-test.sh",
3838
"test:radix:e2e": "./src/tests/radix/run-e2e-test.sh",
3939
"test:aleo:e2e": "./src/tests/aleo/run-e2e-test.sh",
40+
"test:sealevel:e2e": "./src/tests/sealevel/run-e2e-test.sh",
4041
"test:tron:e2e": "./src/tests/ethereum/run-tron-e2e-test.sh",
4142
"version:update": "echo \"export const VERSION = '$npm_package_version';\" > src/version.ts",
4243
"prepack": "pnpm bundle"

typescript/cli/src/tests/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const TEST_CHAIN_NAMES_BY_PROTOCOL = {
3636
CHAIN_NAME_3: 'hyp3',
3737
},
3838
[ProtocolType.Sealevel]: {
39+
CHAIN_NAME_1: 'svmlocal1',
3940
UNSUPPORTED_CHAIN: 'sealevel1',
4041
},
4142
[ProtocolType.Radix]: {
@@ -69,6 +70,7 @@ export const CORE_CONFIG_PATH_BY_PROTOCOL = {
6970
[ProtocolType.CosmosNative]: './examples/cosmosnative/core-config.yaml',
7071
[ProtocolType.Radix]: './examples/radix/core-config.yaml',
7172
[ProtocolType.Aleo]: './examples/aleo/core-config.yaml',
73+
[ProtocolType.Sealevel]: './examples/sealevel/core-config.yaml',
7274
} as const satisfies ProtocolMap<string>;
7375

7476
export const CROSS_CHAIN_CORE_CONFIG_PATH_BY_PROTOCOL = {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extensions": ["ts"],
3+
"file": ["src/tests/sealevel/e2e-test.setup.ts"],
4+
"node-option": ["import=tsx/esm"]
5+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { expect } from 'chai';
2+
3+
import { type CoreConfig } from '@hyperlane-xyz/sdk';
4+
import { ProtocolType } from '@hyperlane-xyz/utils';
5+
6+
import { readYamlOrJson, writeYamlOrJson } from '../../../utils/files.js';
7+
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
8+
import {
9+
BURN_ADDRESS_BY_PROTOCOL,
10+
CORE_READ_CONFIG_PATH_BY_PROTOCOL,
11+
HYP_KEY_BY_PROTOCOL,
12+
REGISTRY_PATH,
13+
} from '../../constants.js';
14+
15+
// Uses merkleTreeHook as defaultHook to match what core read returns on SVM,
16+
// avoiding a hook mismatch that would cause apply to redeploy the IGP.
17+
const CORE_APPLY_CONFIG_PATH = './examples/sealevel/core-config-apply.yaml';
18+
19+
// SVM deploys programs from bytes (~90+ write-chunk transactions per program),
20+
// so the suite needs a generous timeout.
21+
const SVM_DEPLOY_TIMEOUT = 600_000;
22+
23+
describe('hyperlane core apply (Sealevel E2E tests)', async function () {
24+
this.timeout(SVM_DEPLOY_TIMEOUT);
25+
26+
const hyperlaneCore = new HyperlaneE2ECoreTestCommands(
27+
ProtocolType.Sealevel,
28+
'svmlocal1',
29+
REGISTRY_PATH,
30+
CORE_APPLY_CONFIG_PATH,
31+
CORE_READ_CONFIG_PATH_BY_PROTOCOL.sealevel.CHAIN_NAME_1,
32+
);
33+
34+
// Deploy once before all tests to avoid repeated ~7min deploys
35+
before(async function () {
36+
const coreConfig: CoreConfig = await readYamlOrJson(CORE_APPLY_CONFIG_PATH);
37+
writeYamlOrJson(
38+
CORE_READ_CONFIG_PATH_BY_PROTOCOL.sealevel.CHAIN_NAME_1,
39+
coreConfig,
40+
);
41+
42+
hyperlaneCore.setCoreInputPath(
43+
CORE_READ_CONFIG_PATH_BY_PROTOCOL.sealevel.CHAIN_NAME_1,
44+
);
45+
46+
await hyperlaneCore.deploy(HYP_KEY_BY_PROTOCOL.sealevel);
47+
});
48+
49+
describe('hyperlane core apply (mailbox updates)', function () {
50+
it('should update the mailbox owner to the specified one', async () => {
51+
const coreConfig: CoreConfig = await readYamlOrJson(
52+
CORE_APPLY_CONFIG_PATH,
53+
);
54+
55+
coreConfig.owner = BURN_ADDRESS_BY_PROTOCOL.sealevel;
56+
writeYamlOrJson(
57+
CORE_READ_CONFIG_PATH_BY_PROTOCOL.sealevel.CHAIN_NAME_1,
58+
coreConfig,
59+
);
60+
61+
await hyperlaneCore.apply(HYP_KEY_BY_PROTOCOL.sealevel);
62+
63+
const derivedCoreConfig = await hyperlaneCore.readConfig();
64+
expect(derivedCoreConfig.owner).to.equal(
65+
BURN_ADDRESS_BY_PROTOCOL.sealevel,
66+
);
67+
});
68+
});
69+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { expect } from 'chai';
2+
3+
import { type ChainAddresses } from '@hyperlane-xyz/registry';
4+
import {
5+
type CoreConfig,
6+
type DerivedCoreConfig,
7+
HookType,
8+
IsmType,
9+
} from '@hyperlane-xyz/sdk';
10+
import {
11+
ProtocolType,
12+
assert,
13+
isValidAddressSealevel,
14+
} from '@hyperlane-xyz/utils';
15+
import { SealevelIgpHookReader, createRpc } from '@hyperlane-xyz/sealevel-sdk';
16+
17+
import { readYamlOrJson, writeYamlOrJson } from '../../../utils/files.js';
18+
import { HyperlaneE2ECoreTestCommands } from '../../commands/core.js';
19+
import {
20+
CORE_ADDRESSES_PATH_BY_PROTOCOL,
21+
CORE_CONFIG_PATH_BY_PROTOCOL,
22+
CORE_READ_CONFIG_PATH_BY_PROTOCOL,
23+
HYP_DEPLOYER_ADDRESS_BY_PROTOCOL,
24+
HYP_KEY_BY_PROTOCOL,
25+
REGISTRY_PATH,
26+
TEST_CHAIN_METADATA_BY_PROTOCOL,
27+
} from '../../constants.js';
28+
29+
// SVM deploys programs from bytes (~90+ write-chunk transactions per program),
30+
// so the suite needs a generous timeout.
31+
const SVM_DEPLOY_TIMEOUT = 600_000;
32+
33+
describe('hyperlane core deploy (Sealevel E2E tests)', async function () {
34+
this.timeout(SVM_DEPLOY_TIMEOUT);
35+
36+
const hyperlaneCore = new HyperlaneE2ECoreTestCommands(
37+
ProtocolType.Sealevel,
38+
'svmlocal1',
39+
REGISTRY_PATH,
40+
CORE_CONFIG_PATH_BY_PROTOCOL.sealevel,
41+
CORE_READ_CONFIG_PATH_BY_PROTOCOL.sealevel.CHAIN_NAME_1,
42+
);
43+
44+
it('should create a core deployment with the signer as the mailbox owner', async () => {
45+
const coreConfig: CoreConfig = await readYamlOrJson(
46+
CORE_CONFIG_PATH_BY_PROTOCOL.sealevel,
47+
);
48+
49+
writeYamlOrJson(
50+
CORE_READ_CONFIG_PATH_BY_PROTOCOL.sealevel.CHAIN_NAME_1,
51+
coreConfig,
52+
);
53+
hyperlaneCore.setCoreInputPath(
54+
CORE_READ_CONFIG_PATH_BY_PROTOCOL.sealevel.CHAIN_NAME_1,
55+
);
56+
57+
await hyperlaneCore.deploy(HYP_KEY_BY_PROTOCOL.sealevel);
58+
59+
// Validate core read (mailbox-level assertions)
60+
const derivedCoreConfig: DerivedCoreConfig =
61+
await hyperlaneCore.readConfig();
62+
63+
expect(derivedCoreConfig.owner).to.equal(
64+
HYP_DEPLOYER_ADDRESS_BY_PROTOCOL.sealevel,
65+
);
66+
expect(derivedCoreConfig.proxyAdmin?.owner).to.be.undefined;
67+
68+
const deployedDefaultIsm = derivedCoreConfig.defaultIsm;
69+
assert(
70+
deployedDefaultIsm.type === IsmType.TEST_ISM,
71+
`Expected deployed defaultIsm to be of type ${IsmType.TEST_ISM}`,
72+
);
73+
74+
// Validate the registry has the deployed addresses
75+
const addresses: ChainAddresses = await readYamlOrJson(
76+
CORE_ADDRESSES_PATH_BY_PROTOCOL.sealevel.CHAIN_NAME_1,
77+
);
78+
expect(isValidAddressSealevel(addresses.interchainGasPaymaster)).to.be.true;
79+
expect(isValidAddressSealevel(addresses.mailbox)).to.be.true;
80+
expect(isValidAddressSealevel(addresses.validatorAnnounce)).to.be.true;
81+
82+
// Validate the IGP hook was properly deployed via direct hook read
83+
const rpc = createRpc(
84+
TEST_CHAIN_METADATA_BY_PROTOCOL.sealevel.CHAIN_NAME_1.rpcUrl,
85+
);
86+
// Zero salt matches DEFAULT_IGP_SALT used by the CLI core deploy flow.
87+
const igpReader = new SealevelIgpHookReader(rpc, new Uint8Array(32));
88+
assert(
89+
addresses.interchainGasPaymaster,
90+
'Expected interchainGasPaymaster address to be defined',
91+
);
92+
const igpArtifact = await igpReader.read(addresses.interchainGasPaymaster);
93+
assert(
94+
igpArtifact.config.type === HookType.INTERCHAIN_GAS_PAYMASTER,
95+
`Expected hook to be of type ${HookType.INTERCHAIN_GAS_PAYMASTER}, got ${igpArtifact.config.type}`,
96+
);
97+
});
98+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import fs from 'fs';
2+
3+
import {
4+
type SolanaTestValidator,
5+
airdropSol,
6+
getPreloadedPrograms,
7+
runSolanaNode,
8+
} from '@hyperlane-xyz/sealevel-sdk/testing';
9+
import { createRpc } from '@hyperlane-xyz/sealevel-sdk';
10+
11+
import {
12+
HYP_DEPLOYER_ADDRESS_BY_PROTOCOL,
13+
REGISTRY_PATH,
14+
TEST_CHAIN_METADATA_BY_PROTOCOL,
15+
TEST_CHAIN_NAMES_BY_PROTOCOL,
16+
} from '../constants.js';
17+
18+
// Longer timeout for setup: validator startup + airdrop
19+
const SETUP_TIMEOUT_MS = 180_000;
20+
21+
let validator: SolanaTestValidator | undefined;
22+
let programCleanup: (() => void) | undefined;
23+
24+
before(async function () {
25+
this.timeout(SETUP_TIMEOUT_MS);
26+
27+
// Clean up existing sealevel chain addresses
28+
const sealevelChains = TEST_CHAIN_NAMES_BY_PROTOCOL.sealevel;
29+
Object.values(sealevelChains).forEach((name) => {
30+
const path = `${REGISTRY_PATH}/chains/${name}/addresses.yaml`;
31+
32+
if (fs.existsSync(path)) {
33+
fs.rmSync(path, { recursive: true, force: true });
34+
}
35+
});
36+
37+
// Write the Token-2022 override programs to temp files and start the validator.
38+
// Core programs (mailbox, ISM, hooks, VA) are deployed from embedded bytes
39+
// by the writers during `hyperlane core deploy`.
40+
const { programs, cleanup } = getPreloadedPrograms([]);
41+
programCleanup = cleanup;
42+
43+
try {
44+
validator = await runSolanaNode(
45+
TEST_CHAIN_METADATA_BY_PROTOCOL.sealevel.CHAIN_NAME_1,
46+
programs,
47+
);
48+
49+
// Fund the deployer
50+
const rpc = createRpc(
51+
TEST_CHAIN_METADATA_BY_PROTOCOL.sealevel.CHAIN_NAME_1.rpcUrl,
52+
);
53+
await airdropSol(
54+
rpc,
55+
HYP_DEPLOYER_ADDRESS_BY_PROTOCOL.sealevel,
56+
50_000_000_000n,
57+
);
58+
} catch (error: unknown) {
59+
cleanup();
60+
throw error;
61+
}
62+
});
63+
64+
// Reset the test registry for each test invocation
65+
beforeEach(() => {
66+
const deploymentPaths = `${REGISTRY_PATH}/deployments/warp_routes`;
67+
68+
if (fs.existsSync(deploymentPaths)) {
69+
fs.rmSync(deploymentPaths, { recursive: true, force: true });
70+
}
71+
});
72+
73+
after(async function () {
74+
this.timeout(SETUP_TIMEOUT_MS);
75+
76+
await validator?.stop();
77+
programCleanup?.();
78+
});

0 commit comments

Comments
 (0)