Skip to content

Commit 3a0afdf

Browse files
xeno097claude
andauthored
feat(svm-provider): svm provider warp artifact api impl (#8219)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent f798837 commit 3a0afdf

40 files changed

Lines changed: 83908 additions & 69 deletions

.github/workflows/test.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ jobs:
143143
has_tron: ${{ steps.decide.outputs.has_tron }}
144144
has_relayer: ${{ steps.decide.outputs.has_relayer }}
145145
has_rebalancer: ${{ steps.decide.outputs.has_rebalancer }}
146+
has_svm: ${{ steps.decide.outputs.has_svm }}
146147
runs-on: ubuntu-latest
147148
steps:
148149
- uses: actions/checkout@v6
@@ -181,6 +182,7 @@ jobs:
181182
echo "has_tron=true" >> $GITHUB_OUTPUT
182183
echo "has_relayer=true" >> $GITHUB_OUTPUT
183184
echo "has_rebalancer=true" >> $GITHUB_OUTPUT
185+
echo "has_svm=true" >> $GITHUB_OUTPUT
184186
exit 0
185187
fi
186188
@@ -192,6 +194,7 @@ jobs:
192194
echo "has_tron=false" >> $GITHUB_OUTPUT
193195
echo "has_relayer=false" >> $GITHUB_OUTPUT
194196
echo "has_rebalancer=false" >> $GITHUB_OUTPUT
197+
echo "has_svm=false" >> $GITHUB_OUTPUT
195198
exit 0
196199
fi
197200
@@ -200,6 +203,7 @@ jobs:
200203
has_aleo=$(echo "$CHANGED_FILES" | grep -qE '^typescript/aleo-sdk/' && echo true || echo false)
201204
has_cosmos=$(echo "$CHANGED_FILES" | grep -qE '^typescript/(cosmos-sdk|cosmos-types)/' && echo true || echo false)
202205
has_tron=$(echo "$CHANGED_FILES" | grep -qE '^typescript/tron-sdk/' && echo true || echo false)
206+
has_svm=$(echo "$CHANGED_FILES" | grep -qE '^(rust/sealevel/|typescript/svm-sdk/)' && echo true || echo false)
203207
204208
# Package-specific changes (run only their targeted CLI e2e tests)
205209
has_relayer=$(echo "$CHANGED_FILES" | grep -qE '^typescript/relayer/' && echo true || echo false)
@@ -222,6 +226,9 @@ jobs:
222226
echo "CLI changes: $has_cli_changes"
223227
echo "VM-specific — radix=$has_radix aleo=$has_aleo cosmos=$has_cosmos tron=$has_tron"
224228
echo "Package-specific — relayer=$has_relayer rebalancer=$has_rebalancer"
229+
echo "has_svm=$has_svm" >> $GITHUB_OUTPUT
230+
231+
echo "VM-specific — radix=$has_radix aleo=$has_aleo cosmos=$has_cosmos tron=$has_tron svm=$has_svm"
225232
226233
- name: Decide what to run
227234
id: decide
@@ -244,6 +251,7 @@ jobs:
244251
echo "has_tron=false" >> $GITHUB_OUTPUT
245252
echo "has_relayer=false" >> $GITHUB_OUTPUT
246253
echo "has_rebalancer=false" >> $GITHUB_OUTPUT
254+
echo "has_svm=false" >> $GITHUB_OUTPUT
247255
exit 0
248256
fi
249257
@@ -257,6 +265,7 @@ jobs:
257265
echo "has_tron=true" >> $GITHUB_OUTPUT
258266
echo "has_relayer=true" >> $GITHUB_OUTPUT
259267
echo "has_rebalancer=true" >> $GITHUB_OUTPUT
268+
echo "has_svm=true" >> $GITHUB_OUTPUT
260269
exit 0
261270
fi
262271
@@ -269,6 +278,7 @@ jobs:
269278
echo "has_tron=${{ steps.check-vm.outputs.has_tron }}" >> $GITHUB_OUTPUT
270279
echo "has_relayer=${{ steps.check-vm.outputs.has_relayer }}" >> $GITHUB_OUTPUT
271280
echo "has_rebalancer=${{ steps.check-vm.outputs.has_rebalancer }}" >> $GITHUB_OUTPUT
281+
echo "has_svm=${{ steps.check-vm.outputs.has_svm }}" >> $GITHUB_OUTPUT
272282
273283
pnpm-test-run:
274284
runs-on: depot-ubuntu-24.04
@@ -478,6 +488,22 @@ jobs:
478488
exit 1
479489
fi
480490
491+
svm-program-bytes:
492+
runs-on: ubuntu-latest
493+
needs: [change-detection]
494+
if: needs.change-detection.outputs.has_svm == 'true'
495+
steps:
496+
- uses: actions/checkout@v6
497+
with:
498+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
499+
500+
- uses: actions/setup-node@v6
501+
with:
502+
node-version-file: .nvmrc
503+
504+
- name: Check program-bytes.ts staleness
505+
run: node typescript/svm-sdk/scripts/check-program-bytes-hash.mjs
506+
481507
env-tests:
482508
needs: [change-detection]
483509
uses: ./.github/workflows/test-env.yml

rust/sealevel/programs/build-programs.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ PROGRAM_TYPE="${1:-all}"
1212
SOLANA_CLI_VERSION_FOR_BUILDING_PROGRAMS="3.0.14"
1313

1414
# The paths to the programs
15-
CORE_PROGRAM_PATHS=("mailbox" "ism/multisig-ism-message-id" "validator-announce" "hyperlane-sealevel-igp")
15+
CORE_PROGRAM_PATHS=("mailbox" "ism/multisig-ism-message-id" "ism/test-ism" "validator-announce" "hyperlane-sealevel-igp")
1616
TOKEN_PROGRAM_PATHS=("hyperlane-sealevel-token" "hyperlane-sealevel-token-collateral" "hyperlane-sealevel-token-native")
1717

1818
build_program () {

typescript/provider-sdk/src/warp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ interface BaseWarpArtifactConfig {
133133
name?: string;
134134
symbol?: string;
135135
decimals?: number;
136+
scale?: number;
136137
}
137138

138139
export interface CollateralWarpArtifactConfig extends BaseWarpArtifactConfig {
@@ -145,6 +146,7 @@ export interface SyntheticWarpArtifactConfig extends BaseWarpArtifactConfig {
145146
name: string;
146147
symbol: string;
147148
decimals: number;
149+
metadataUri?: string;
148150
}
149151

150152
export interface NativeWarpArtifactConfig extends BaseWarpArtifactConfig {

typescript/svm-sdk/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
dist
2+
src/generated/

typescript/svm-sdk/package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,22 @@
2828
}
2929
},
3030
"scripts": {
31-
"build": "rm -rf ./dist && tsc",
31+
"build": "node scripts/generate-core-addresses.mjs && rm -rf ./dist && tsc",
32+
"program:build": "(cd ../../rust/sealevel/programs && bash build-programs.sh)",
33+
"program:generate": "node scripts/generate-program-bytes.mjs",
34+
"program:check": "node scripts/check-program-bytes-hash.mjs",
3235
"format": "oxfmt --write ./src",
3336
"lint": "eslint -c ./eslint.config.mjs",
3437
"clean": "rm -rf ./dist ./cache",
3538
"dev": "tsc --watch",
3639
"test": "mocha --require tsx --timeout 300000 'src/tests/**/*.e2e-test.ts'",
40+
"test:unit": "mocha --require tsx --timeout 10000 'src/tests/**/*.unit-test.ts'",
3741
"test:ism": "mocha --require tsx --timeout 300000 'src/tests/ism.e2e-test.ts'",
38-
"test:hook": "mocha --require tsx --timeout 300000 'src/tests/hook.e2e-test.ts'"
42+
"test:hook": "mocha --require tsx --timeout 300000 'src/tests/hook.e2e-test.ts'",
43+
"test:native-token": "mocha --require tsx --timeout 300000 'src/tests/native-token.e2e-test.ts'",
44+
"test:synthetic-token": "mocha --require tsx --timeout 300000 'src/tests/synthetic-token.e2e-test.ts'",
45+
"test:collateral-token": "mocha --require tsx --timeout 300000 'src/tests/collateral-token.e2e-test.ts'",
46+
"test:read-token": "mocha --require tsx --timeout 300000 'src/tests/read-token.e2e-test.ts'"
3947
},
4048
"dependencies": {
4149
"@hyperlane-xyz/provider-sdk": "workspace:*",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env node
2+
/* eslint-disable no-console */
3+
/* eslint-disable import/no-nodejs-modules */
4+
import { readFileSync } from 'node:fs';
5+
import { dirname, join } from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
8+
import { computeSealevelSourceHash } from './sealevel-source-hash.mjs';
9+
10+
const __dirname = dirname(fileURLToPath(import.meta.url));
11+
const PROGRAM_BYTES_FILE = join(__dirname, '../src/hyperlane/program-bytes.ts');
12+
13+
let content;
14+
try {
15+
content = readFileSync(PROGRAM_BYTES_FILE, 'utf-8');
16+
} catch {
17+
console.error(
18+
'ERROR: program-bytes.ts not found. Regenerate with:\n' +
19+
' pnpm -C typescript/svm-sdk program:build\n' +
20+
' pnpm -C typescript/svm-sdk program:generate',
21+
);
22+
process.exit(1);
23+
}
24+
25+
const match = content.match(/SEALEVEL_SOURCE_HASH\s*=\s*'([a-f0-9]{64})'/);
26+
if (!match) {
27+
console.error(
28+
'ERROR: SEALEVEL_SOURCE_HASH not found in program-bytes.ts.\n' +
29+
'The file may predate the staleness check. Regenerate with:\n' +
30+
' pnpm -C typescript/svm-sdk program:build\n' +
31+
' pnpm -C typescript/svm-sdk program:generate',
32+
);
33+
process.exit(1);
34+
}
35+
36+
const embeddedHash = match[1];
37+
const currentHash = computeSealevelSourceHash();
38+
39+
if (embeddedHash === currentHash) {
40+
console.log('program-bytes.ts is up to date with Rust sealevel sources.');
41+
process.exit(0);
42+
}
43+
44+
console.error('ERROR: program-bytes.ts is STALE.');
45+
console.error(` Embedded hash: ${embeddedHash}`);
46+
console.error(` Current hash: ${currentHash}`);
47+
console.error('');
48+
console.error(
49+
'Rust sealevel sources changed but program-bytes.ts was not regenerated.\n' +
50+
'To fix:\n' +
51+
' pnpm -C typescript/svm-sdk program:build\n' +
52+
' pnpm -C typescript/svm-sdk program:generate',
53+
);
54+
process.exit(1);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env node
2+
/* eslint-disable no-console */
3+
/* eslint-disable import/no-nodejs-modules */
4+
import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
5+
import { dirname, join } from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
8+
const __dirname = dirname(fileURLToPath(import.meta.url));
9+
const ENVIRONMENTS_DIR = join(
10+
__dirname,
11+
'../../../rust/sealevel/environments/mainnet3',
12+
);
13+
const OUTPUT_FILE = join(__dirname, '../src/generated/core-addresses.ts');
14+
15+
/** Snake_case key to camelCase. */
16+
function camelCase(s) {
17+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
18+
}
19+
20+
const chains = {};
21+
22+
for (const entry of readdirSync(ENVIRONMENTS_DIR, { withFileTypes: true })) {
23+
if (!entry.isDirectory()) continue;
24+
25+
const programIdsPath = join(
26+
ENVIRONMENTS_DIR,
27+
entry.name,
28+
'core',
29+
'program-ids.json',
30+
);
31+
32+
let raw;
33+
try {
34+
raw = JSON.parse(readFileSync(programIdsPath, 'utf-8'));
35+
} catch {
36+
continue; // no core/program-ids.json for this entry
37+
}
38+
39+
const camelCased = {};
40+
for (const [key, value] of Object.entries(raw)) {
41+
camelCased[camelCase(key)] = value;
42+
}
43+
44+
chains[entry.name] = camelCased;
45+
}
46+
47+
const chainNames = Object.keys(chains).sort();
48+
49+
if (chainNames.length === 0) {
50+
console.error('No valid chains found in', ENVIRONMENTS_DIR);
51+
process.exit(1);
52+
}
53+
54+
const interfaceFields = Object.keys(chains[chainNames[0]])
55+
.map((k) => ` ${k}: string;`)
56+
.join('\n');
57+
58+
const entries = chainNames
59+
.map((chain) => {
60+
const fields = Object.entries(chains[chain])
61+
.map(([k, v]) => ` ${k}: '${v}',`)
62+
.join('\n');
63+
return ` ${chain}: {\n${fields}\n },`;
64+
})
65+
.join('\n');
66+
67+
const tsContent = `/**
68+
* Auto-generated SVM core deployment addresses from rust/sealevel/environments/mainnet3.
69+
* DO NOT EDIT — regenerate with: node scripts/generate-core-addresses.mjs
70+
*/
71+
72+
export interface SvmCoreAddresses {
73+
${interfaceFields}
74+
}
75+
76+
export const SVM_CORE_ADDRESSES: Record<string, SvmCoreAddresses> = {
77+
${entries}
78+
};
79+
`;
80+
81+
mkdirSync(dirname(OUTPUT_FILE), { recursive: true });
82+
writeFileSync(OUTPUT_FILE, tsContent);
83+
console.log(
84+
`Generated ${OUTPUT_FILE} with ${chainNames.length} chains: ${chainNames.join(', ')}`,
85+
);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env node
2+
/* eslint-disable no-console */
3+
/* eslint-disable import/no-nodejs-modules */
4+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
5+
import { dirname, join } from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
8+
import { computeSealevelSourceHash } from './sealevel-source-hash.mjs';
9+
10+
const __filename = fileURLToPath(import.meta.url);
11+
const __dirname = dirname(__filename);
12+
13+
const PROGRAMS_DIR = join(__dirname, '../../../rust/sealevel/target/deploy');
14+
const OUTPUT_FILE = join(__dirname, '../src/hyperlane/program-bytes.ts');
15+
16+
/** All Sealevel programs — keys match PROGRAM_BINARIES in testing/setup.ts. */
17+
const PROGRAMS = {
18+
mailbox: 'hyperlane_sealevel_mailbox.so',
19+
igp: 'hyperlane_sealevel_igp.so',
20+
multisigIsm: 'hyperlane_sealevel_multisig_ism_message_id.so',
21+
testIsm: 'hyperlane_sealevel_test_ism.so',
22+
validatorAnnounce: 'hyperlane_sealevel_validator_announce.so',
23+
tokenSynthetic: 'hyperlane_sealevel_token.so',
24+
tokenNative: 'hyperlane_sealevel_token_native.so',
25+
tokenCollateral: 'hyperlane_sealevel_token_collateral.so',
26+
};
27+
28+
console.log('🔧 Generating program bytes from .so files...\n');
29+
30+
const programBytes = {};
31+
32+
for (const [key, filename] of Object.entries(PROGRAMS)) {
33+
const path = join(PROGRAMS_DIR, filename);
34+
try {
35+
const bytes = readFileSync(path);
36+
programBytes[key] = Array.from(bytes);
37+
console.log(` ✅ ${key}: ${bytes.length.toLocaleString()} bytes`);
38+
} catch {
39+
console.error(` ❌ ${key}: required .so file not found at ${path}`);
40+
process.exit(1);
41+
}
42+
}
43+
44+
const sourceHash = computeSealevelSourceHash();
45+
console.log(`\n Source hash: ${sourceHash}`);
46+
47+
const entries = Object.entries(programBytes)
48+
.map(([key, bytes]) => ` ${key}: new Uint8Array([${bytes}]),`)
49+
.join('\n');
50+
51+
const tsContent = `/**
52+
* Auto-generated program bytes from compiled .so binaries.
53+
* DO NOT EDIT — regenerate with:
54+
* pnpm -C typescript/svm-sdk program:build
55+
* pnpm -C typescript/svm-sdk program:generate
56+
*/
57+
58+
/** SHA-256 of Rust sealevel sources at generation time. Used for CI staleness detection. */
59+
export const SEALEVEL_SOURCE_HASH = '${sourceHash}';
60+
61+
export const HYPERLANE_SVM_PROGRAM_BYTES = {
62+
${entries}
63+
} as const;
64+
65+
export type HyperlaneSvmProgramBytesKey = keyof typeof HYPERLANE_SVM_PROGRAM_BYTES;
66+
`;
67+
68+
mkdirSync(dirname(OUTPUT_FILE), { recursive: true });
69+
writeFileSync(OUTPUT_FILE, tsContent);
70+
console.log(`\n✨ Generated ${OUTPUT_FILE}\n`);

0 commit comments

Comments
 (0)