@layerzerolabs/protocol-stellar-v2 1.2.8 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layerzerolabs/protocol-stellar-v2",
3
- "version": "1.2.8",
3
+ "version": "1.2.10",
4
4
  "private": false,
5
5
  "license": "LZBL-1.3",
6
6
  "files": [
@@ -9,24 +9,18 @@
9
9
  "message-libs",
10
10
  "upgrader",
11
11
  "workers",
12
- "scripts",
13
12
  "Cargo.toml",
14
13
  "rust-toolchain.toml",
15
14
  "rustfmt.toml",
16
15
  "clippy.toml"
17
16
  ],
18
17
  "dependencies": {
19
- "@layerzerolabs/common-utils-macros-stellar-contracts": "1.2.8",
20
- "@layerzerolabs/common-utils-stellar-contracts": "1.2.8"
18
+ "@layerzerolabs/common-utils-macros-stellar-contracts": "1.2.10",
19
+ "@layerzerolabs/common-utils-stellar-contracts": "1.2.10"
21
20
  },
22
21
  "devDependencies": {
23
- "@types/node": "^22.18.6",
24
- "fflate": "^0.8.3",
25
- "toml": "^3.0.0",
26
- "tsx": "^4.21.0",
27
- "@layerzerolabs/build-utils-rust": "1.2.8",
28
- "@layerzerolabs/vm-tooling-stellar": "1.2.8",
29
- "@layerzerolabs/typescript-configuration": "1.2.8"
22
+ "@layerzerolabs/build-utils-rust": "1.2.10",
23
+ "@layerzerolabs/vm-tooling-stellar": "1.2.10"
30
24
  },
31
25
  "publishConfig": {
32
26
  "access": "public",
@@ -45,7 +39,7 @@
45
39
  "@layerzerolabs/common-utils-macros-stellar-contracts": "workspace:*"
46
40
  },
47
41
  "scripts": {
48
- "build": "pnpm package:source && tsx scripts/build-verifiable.ts",
42
+ "build": "pnpm package:source && pnpm exec lz-tool extra stellar verifiable-build --stellar-version 25.1.0 --archive .artifacts/.incoming/contracts-source.zip --package endpoint-v2:endpoint-v2/Cargo.toml --package uln302:message-libs/uln-302/Cargo.toml --package treasury:message-libs/treasury/Cargo.toml --package simple-message-lib:message-libs/simple-message-lib/Cargo.toml --package blocked-message-lib:message-libs/blocked-message-lib/Cargo.toml --package layerzero-views:layerzero-views/Cargo.toml --package upgrader:upgrader/Cargo.toml --package executor:workers/executor/Cargo.toml --package executor-helper:workers/executor-helper/Cargo.toml --package executor-fee-lib:workers/executor-fee-lib/Cargo.toml --package dvn:workers/dvn/Cargo.toml --package dvn-fee-lib:workers/dvn-fee-lib/Cargo.toml --package price-feed:workers/price-feed/Cargo.toml",
49
43
  "build:contracts": "pnpm resolve-dependencies && pnpm exec lz-tool stellar contract build",
50
44
  "build:contracts:native": "pnpm resolve-dependencies && pnpm exec lz-tool --docker-platform native stellar contract build",
51
45
  "build:native": "pnpm build:contracts:native",
@@ -1,444 +0,0 @@
1
- import { unzipSync } from 'fflate';
2
- import { spawn } from 'node:child_process';
3
- import { createHash } from 'node:crypto';
4
- import {
5
- chmod,
6
- copyFile,
7
- mkdir,
8
- mkdtemp,
9
- readdir,
10
- readFile,
11
- rename,
12
- rm,
13
- stat,
14
- writeFile,
15
- } from 'node:fs/promises';
16
- import { tmpdir } from 'node:os';
17
- import path from 'node:path';
18
- import { parseArgs } from 'node:util';
19
- import { parse as parseToml } from 'toml';
20
-
21
- const STELLAR_VERSION = '25.1.0';
22
- const ARTIFACTS_DIR = '.artifacts';
23
- /** Staged by `pnpm package:source`; not the published SEP-58 artifact set. */
24
- const INCOMING_ARCHIVE_REL = path.join(ARTIFACTS_DIR, '.incoming', 'contracts-source.zip');
25
- /** Published next to *.wasm via atomic publishArtifacts. */
26
- const SOURCE_ARCHIVE_NAME = 'contracts-source.zip';
27
- const WASM_TARGET_DIR = path.join('target', 'wasm32v1-none', 'release');
28
-
29
- /**
30
- * Protocol + worker cdylib contracts still in this package. Paths are relative to the
31
- * extracted package-source root (the crate basename folder, e.g. `contracts/`).
32
- *
33
- * Non-protocol OApp contracts (oapp, oft, oft-core, sac-manager, counter, …) were
34
- * moved out of this package and are intentionally omitted.
35
- */
36
- const CONTRACTS: ReadonlyArray<{ packageName: string; manifestPath: string }> = [
37
- { packageName: 'endpoint-v2', manifestPath: 'endpoint-v2/Cargo.toml' },
38
- { packageName: 'uln302', manifestPath: 'message-libs/uln-302/Cargo.toml' },
39
- { packageName: 'treasury', manifestPath: 'message-libs/treasury/Cargo.toml' },
40
- {
41
- packageName: 'simple-message-lib',
42
- manifestPath: 'message-libs/simple-message-lib/Cargo.toml',
43
- },
44
- {
45
- packageName: 'blocked-message-lib',
46
- manifestPath: 'message-libs/blocked-message-lib/Cargo.toml',
47
- },
48
- { packageName: 'layerzero-views', manifestPath: 'layerzero-views/Cargo.toml' },
49
- { packageName: 'upgrader', manifestPath: 'upgrader/Cargo.toml' },
50
- { packageName: 'executor', manifestPath: 'workers/executor/Cargo.toml' },
51
- {
52
- packageName: 'executor-helper',
53
- manifestPath: 'workers/executor-helper/Cargo.toml',
54
- },
55
- {
56
- packageName: 'executor-fee-lib',
57
- manifestPath: 'workers/executor-fee-lib/Cargo.toml',
58
- },
59
- { packageName: 'dvn', manifestPath: 'workers/dvn/Cargo.toml' },
60
- { packageName: 'dvn-fee-lib', manifestPath: 'workers/dvn-fee-lib/Cargo.toml' },
61
- { packageName: 'price-feed', manifestPath: 'workers/price-feed/Cargo.toml' },
62
- ];
63
-
64
- interface ParsedArgs {
65
- outputDir: string;
66
- }
67
-
68
- const pathExists = async (filePath: string): Promise<boolean> => {
69
- try {
70
- await stat(filePath);
71
- return true;
72
- } catch (err) {
73
- if ((err as { code?: string }).code === 'ENOENT') {
74
- return false;
75
- }
76
- throw err;
77
- }
78
- };
79
-
80
- const printUsage = (): void => {
81
- console.error(
82
- 'Usage: tsx scripts/build-verifiable.ts [--output-dir <path>]\n' +
83
- '\n' +
84
- `Requires ${INCOMING_ARCHIVE_REL} from \`pnpm package:source\`.\n` +
85
- `Builds ${CONTRACTS.length} protocol/worker contracts sequentially.\n` +
86
- 'Embeds SEP-58 source_sha256 (content-addressed; source_uri is out of band).\n' +
87
- `Atomically writes ${SOURCE_ARCHIVE_NAME} + *.wasm to --output-dir ` +
88
- `(default: ${ARTIFACTS_DIR}/) only after a successful build.`,
89
- );
90
- };
91
-
92
- const parseCliArgs = (argv: string[], packageRoot: string): ParsedArgs | null => {
93
- try {
94
- const { values } = parseArgs({
95
- args: argv.filter((arg) => arg !== '--'),
96
- options: {
97
- 'output-dir': { type: 'string', default: ARTIFACTS_DIR },
98
- },
99
- strict: true,
100
- });
101
-
102
- const outputDirRaw = values['output-dir'];
103
- if (typeof outputDirRaw !== 'string' || outputDirRaw.length === 0) {
104
- printUsage();
105
- return null;
106
- }
107
-
108
- return { outputDir: path.resolve(packageRoot, outputDirRaw) };
109
- } catch (error) {
110
- console.error(error instanceof Error ? error.message : error);
111
- printUsage();
112
- return null;
113
- }
114
- };
115
-
116
- interface RustToolchainToml {
117
- toolchain?: {
118
- channel?: string;
119
- };
120
- }
121
-
122
- /** Read the pinned Rust channel from rust-toolchain.toml so it cannot drift from the crate. */
123
- const readRustVersion = async (packageRoot: string): Promise<string> => {
124
- const toolchainPath = path.join(packageRoot, 'rust-toolchain.toml');
125
- const parsed = parseToml(await readFile(toolchainPath, 'utf8')) as RustToolchainToml;
126
- const channel = parsed.toolchain?.channel;
127
- if (typeof channel !== 'string') {
128
- throw new Error(`Missing 'toolchain.channel' in ${toolchainPath}`);
129
- }
130
- return channel;
131
- };
132
-
133
- const extractZip = async (bytes: Uint8Array, destDir: string): Promise<void> => {
134
- const files = unzipSync(bytes);
135
- const destRoot = path.resolve(destDir);
136
- for (const [relPath, content] of Object.entries(files)) {
137
- if (relPath.endsWith('/')) continue;
138
- const outPath = path.resolve(destRoot, relPath);
139
- if (outPath !== destRoot && !outPath.startsWith(destRoot + path.sep)) {
140
- throw new Error(`Zip entry escapes destination: ${relPath}`);
141
- }
142
- await mkdir(path.dirname(outPath), { recursive: true });
143
- await writeFile(outPath, content);
144
- }
145
- };
146
-
147
- const runPnpm = (args: string[], cwd: string): Promise<number> =>
148
- new Promise((resolve, reject) => {
149
- const child = spawn('pnpm', args, { cwd, stdio: 'inherit' });
150
- child.on('error', reject);
151
- child.on('close', (code) => resolve(code ?? 1));
152
- });
153
-
154
- /**
155
- * Make a bind-mounted tree deletable by the host after the official image wrote files as a
156
- * non-host UID. Uses root only for cleanup — never for the verifiable build itself.
157
- */
158
- const relaxDockerOwnedPermissions = (dir: string, rustVersion: string): Promise<void> =>
159
- new Promise((resolve, reject) => {
160
- const image = `stellar/stellar-cli:${STELLAR_VERSION}-rust${rustVersion}-slim-bookworm`;
161
- const child = spawn(
162
- 'docker',
163
- [
164
- 'run',
165
- '--rm',
166
- '--user',
167
- '0:0',
168
- '--entrypoint',
169
- 'chmod',
170
- '-v',
171
- `${dir}:/cleanup`,
172
- image,
173
- '-R',
174
- 'a+rwX',
175
- '/cleanup',
176
- ],
177
- { stdio: 'inherit' },
178
- );
179
- child.on('error', reject);
180
- child.on('close', (code) => {
181
- if (code === 0) {
182
- resolve();
183
- } else {
184
- reject(new Error(`Failed to chmod temp dir via docker (exit ${code ?? 1})`));
185
- }
186
- });
187
- });
188
-
189
- const removeTempDir = async (tempDir: string, rustVersion: string): Promise<void> => {
190
- try {
191
- await rm(tempDir, { recursive: true, force: true });
192
- } catch (error) {
193
- const code = (error as { code?: string }).code;
194
- if (code !== 'EACCES' && code !== 'EPERM') {
195
- throw error;
196
- }
197
- // Container-created target/ files aren't owned by the host — chmod as root, then retry.
198
- await relaxDockerOwnedPermissions(tempDir, rustVersion);
199
- await rm(tempDir, { recursive: true, force: true });
200
- }
201
- };
202
-
203
- const wasmArtifactName = (packageName: string): string =>
204
- `${packageName.replaceAll('-', '_')}.wasm`;
205
-
206
- const persistWasm = async (
207
- buildRoot: string,
208
- outDir: string,
209
- packageName: string,
210
- ): Promise<void> => {
211
- const wasmName = wasmArtifactName(packageName);
212
- const builtPath = path.join(buildRoot, WASM_TARGET_DIR, wasmName);
213
- if (!(await pathExists(builtPath))) {
214
- throw new Error(`Expected WASM not found after build: ${builtPath}`);
215
- }
216
-
217
- await mkdir(outDir, { recursive: true });
218
- const outPath = path.join(outDir, wasmName);
219
- await copyFile(builtPath, outPath);
220
- console.info(`Staged WASM: ${outPath}`);
221
- };
222
-
223
- const persistSourceArchive = async (archiveBytes: Uint8Array, outDir: string): Promise<void> => {
224
- await mkdir(outDir, { recursive: true });
225
- const outPath = path.join(outDir, SOURCE_ARCHIVE_NAME);
226
- // Write from the in-memory archive so publish can safely replace `.artifacts/`
227
- // even when the input zip lived under that same directory.
228
- await writeFile(outPath, archiveBytes);
229
- console.info(`Staged source archive: ${outPath}`);
230
- };
231
-
232
- /**
233
- * Atomically replace the output dir with the staged artifact set.
234
- * Writes to a sibling temp dir, then renames into place so readers never see a
235
- * partial update and obsolete .wasm files from prior runs are dropped.
236
- */
237
- const publishArtifacts = async (stagingDir: string, outDir: string): Promise<void> => {
238
- const parentDir = path.dirname(outDir);
239
- const outBase = path.basename(outDir);
240
- await mkdir(parentDir, { recursive: true });
241
-
242
- const nextDir = await mkdtemp(path.join(parentDir, `${outBase}.next-`));
243
- const backupDir = path.join(parentDir, `${outBase}.prev`);
244
- const entries = await readdir(stagingDir);
245
-
246
- try {
247
- for (const entry of entries) {
248
- await copyFile(path.join(stagingDir, entry), path.join(nextDir, entry));
249
- }
250
-
251
- // Recover from a previous interrupted swap before touching .prev.
252
- // If outDir is gone but .prev remains, that backup is the only good copy.
253
- if (!(await pathExists(outDir)) && (await pathExists(backupDir))) {
254
- await rename(backupDir, outDir);
255
- console.info(`Restored ${outDir} from leftover ${backupDir}`);
256
- } else if (await pathExists(backupDir)) {
257
- await rm(backupDir, { recursive: true, force: true });
258
- }
259
-
260
- if (await pathExists(outDir)) {
261
- await rename(outDir, backupDir);
262
- }
263
- await rename(nextDir, outDir);
264
- // Publish already succeeded; leftover .prev must not fail the build.
265
- await rm(backupDir, { recursive: true, force: true }).catch((error) => {
266
- console.warn(`Failed to remove leftover ${backupDir} after publish:`, error);
267
- });
268
- } catch (error) {
269
- // nextDir is consumed on successful rename(nextDir, outDir); otherwise remove it.
270
- await rm(nextDir, { recursive: true, force: true }).catch(() => undefined);
271
- // Restore previous output dir if the swap failed mid-flight.
272
- if (!(await pathExists(outDir)) && (await pathExists(backupDir))) {
273
- await rename(backupDir, outDir).catch(() => undefined);
274
- }
275
- throw error;
276
- }
277
-
278
- for (const entry of entries) {
279
- console.info(`Published: ${path.join(outDir, entry)}`);
280
- }
281
- };
282
-
283
- const buildContract = async ({
284
- packageRoot,
285
- buildRoot,
286
- stagingDir,
287
- contract,
288
- sourceSha256,
289
- rustVersion,
290
- }: {
291
- packageRoot: string;
292
- buildRoot: string;
293
- stagingDir: string;
294
- contract: { packageName: string; manifestPath: string };
295
- sourceSha256: string;
296
- rustVersion: string;
297
- }): Promise<number> => {
298
- const verifiableArgs = [
299
- 'exec',
300
- 'lz-tool',
301
- 'extra',
302
- 'stellar',
303
- 'verifiable-build',
304
- '--stellar-version',
305
- STELLAR_VERSION,
306
- '--rust-version',
307
- rustVersion,
308
- '--source-dir',
309
- buildRoot,
310
- // metadata passthrough to docker run command
311
- '--',
312
- // Actual build flags (must match bldopt so verify rebuild is byte-identical).
313
- '--manifest-path',
314
- contract.manifestPath,
315
- '--package',
316
- contract.packageName,
317
- '--optimize',
318
- // SEP-58: record the same flags as bldopt for `contract verify` replay.
319
- '--meta',
320
- `bldopt=--manifest-path=${contract.manifestPath}`,
321
- '--meta',
322
- `bldopt=--package=${contract.packageName}`,
323
- '--meta',
324
- 'bldopt=--optimize',
325
- // SEP-58 content-addressed source pin (source_uri is optional / out of band).
326
- '--meta',
327
- `source_sha256=${sourceSha256}`,
328
- ];
329
-
330
- console.info(`\n=== Building ${contract.packageName} (${contract.manifestPath}) ===`);
331
- console.info(`Running: pnpm ${verifiableArgs.join(' ')}`);
332
- const code = await runPnpm(verifiableArgs, packageRoot);
333
- if (code === 0) {
334
- await persistWasm(buildRoot, stagingDir, contract.packageName);
335
- }
336
- return code;
337
- };
338
-
339
- const main = async (): Promise<number> => {
340
- const packageRoot = process.cwd();
341
- const parsed = parseCliArgs(process.argv.slice(2), packageRoot);
342
- if (!parsed) {
343
- return 1;
344
- }
345
-
346
- const zipPath = path.join(packageRoot, INCOMING_ARCHIVE_REL);
347
-
348
- if (!(await pathExists(zipPath))) {
349
- console.error(`Incoming archive not found: ${zipPath}`);
350
- console.error('Run `pnpm package:source` first to create the archive.');
351
- return 1;
352
- }
353
-
354
- const rustVersion = await readRustVersion(packageRoot);
355
- const outDir = parsed.outputDir;
356
-
357
- // Local archive is the SEP-58 source pin (source_sha256); upload/hosting is out of band.
358
- // Kept under .artifacts/.incoming so a failed verifiable build cannot desync the published
359
- // .artifacts/contracts-source.zip + *.wasm pair (publishArtifacts is atomic on success).
360
- console.info(`Reading incoming archive: ${zipPath}`);
361
- const archiveBytes = new Uint8Array(await readFile(zipPath));
362
- if (archiveBytes.length === 0) {
363
- console.error(`Archive is empty: ${zipPath}`);
364
- return 1;
365
- }
366
- const sourceSha256 = createHash('sha256').update(archiveBytes).digest('hex');
367
- console.info(`source_sha256=${sourceSha256}`);
368
- console.info(`rust_version=${rustVersion} (from rust-toolchain.toml)`);
369
- console.info(`stellar_version=${STELLAR_VERSION}`);
370
- console.info(`outputDir=${outDir}`);
371
- console.info(`Contracts to build: ${CONTRACTS.length}`);
372
-
373
- const tempDir = await mkdtemp(path.join(tmpdir(), 'stellar-build-verifiable-'));
374
- const stagingDir = path.join(tempDir, 'staging');
375
- let exitCode = 0;
376
-
377
- try {
378
- console.info(`Extracting archive to: ${tempDir}`);
379
- await extractZip(archiveBytes, tempDir);
380
-
381
- // package-source zips cwd's parent and keeps only `<basename>/…`, so the
382
- // archive's single top-level folder matches the crate directory name.
383
- const rootDirName = path.basename(packageRoot);
384
- const buildRoot = path.join(tempDir, rootDirName);
385
- if (!(await pathExists(buildRoot))) {
386
- console.error(
387
- `Expected top-level ${rootDirName}/ directory not found after extraction`,
388
- );
389
- return 1;
390
- }
391
- // mkdtemp is 0700; the official stellar-cli image runs as a non-host UID and must
392
- // create cargo target dirs under the bind-mounted source. Do not pass --user (that
393
- // would diverge from what external verifiers run). buildRoot is always the extracted
394
- // temp tree (never the package cwd).
395
- await chmod(buildRoot, 0o777);
396
- console.info(`Build root: ${buildRoot}`);
397
- console.info(`Staging artifacts in: ${stagingDir}`);
398
-
399
- await persistSourceArchive(archiveBytes, stagingDir);
400
-
401
- for (const contract of CONTRACTS) {
402
- const code = await buildContract({
403
- packageRoot,
404
- buildRoot,
405
- stagingDir,
406
- contract,
407
- sourceSha256,
408
- rustVersion,
409
- });
410
- if (code !== 0) {
411
- console.error(`Verifiable build failed for ${contract.packageName} (exit ${code})`);
412
- exitCode = code;
413
- break;
414
- }
415
- }
416
-
417
- if (exitCode === 0) {
418
- await publishArtifacts(stagingDir, outDir);
419
- } else {
420
- console.error(
421
- 'Leaving output dir unchanged; staged artifacts were discarded with the temp dir.',
422
- );
423
- }
424
- } finally {
425
- // Artifacts may already be published; cleanup must never invert a successful exit.
426
- await removeTempDir(tempDir, rustVersion).catch((error) => {
427
- console.warn(`Temp dir cleanup failed (build result unchanged): ${tempDir}`, error);
428
- });
429
- }
430
-
431
- if (exitCode === 0) {
432
- console.info(`\nAll ${CONTRACTS.length} contracts built successfully.`);
433
- console.info(`Artifacts written to: ${outDir}`);
434
- }
435
-
436
- return exitCode;
437
- };
438
-
439
- main()
440
- .then((code) => process.exit(code))
441
- .catch((error: unknown) => {
442
- console.error(error);
443
- process.exit(1);
444
- });