openzoo 0.51.4 → 0.51.5

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.
Files changed (50) hide show
  1. package/bin/openzoo.js +0 -37
  2. package/lib/config.js +34 -0
  3. package/lib/pay.js +60 -4
  4. package/package.json +4 -7
  5. package/transmute/README.md +0 -385
  6. package/transmute/bin/openzoo-transmute.js +0 -7
  7. package/transmute/docs/OPENZOO_CLI_PATCH.md +0 -139
  8. package/transmute/docs/VERCEL_TO_SOLANA.md +0 -381
  9. package/transmute/lib/build.js +0 -381
  10. package/transmute/lib/cli.js +0 -293
  11. package/transmute/lib/compile/bytecode.js +0 -591
  12. package/transmute/lib/compile/hostids.js +0 -11
  13. package/transmute/lib/compile/index.js +0 -159
  14. package/transmute/lib/compile/ir.js +0 -1634
  15. package/transmute/lib/compile/parse.js +0 -753
  16. package/transmute/lib/compile/rust.js +0 -781
  17. package/transmute/lib/deploy.js +0 -357
  18. package/transmute/lib/eligibility.js +0 -211
  19. package/transmute/lib/gateway.js +0 -710
  20. package/transmute/lib/hub.js +0 -290
  21. package/transmute/lib/solana.js +0 -387
  22. package/transmute/lib/vercel.js +0 -296
  23. package/transmute/lib/wallet.js +0 -41
  24. package/transmute/lib/wire.js +0 -208
  25. package/transmute/prebuilt/zoo_vm.v0.so +0 -0
  26. package/transmute/runtime/zoo-host/Cargo.lock +0 -588
  27. package/transmute/runtime/zoo-host/Cargo.toml +0 -30
  28. package/transmute/runtime/zoo-host/examples/vmrun.rs +0 -43
  29. package/transmute/runtime/zoo-host/src/assets.rs +0 -214
  30. package/transmute/runtime/zoo-host/src/ctx.rs +0 -350
  31. package/transmute/runtime/zoo-host/src/fmt.rs +0 -188
  32. package/transmute/runtime/zoo-host/src/helpers.rs +0 -245
  33. package/transmute/runtime/zoo-host/src/json.rs +0 -263
  34. package/transmute/runtime/zoo-host/src/kv.rs +0 -206
  35. package/transmute/runtime/zoo-host/src/lib.rs +0 -137
  36. package/transmute/runtime/zoo-host/src/site.rs +0 -94
  37. package/transmute/runtime/zoo-host/src/val.rs +0 -1050
  38. package/transmute/runtime/zoo-host/src/vm.rs +0 -902
  39. package/transmute/runtime/zoo-host/src/wire.rs +0 -239
  40. package/transmute/runtime/zoo-vm/Cargo.lock +0 -596
  41. package/transmute/runtime/zoo-vm/Cargo.toml +0 -23
  42. package/transmute/runtime/zoo-vm/src/lib.rs +0 -92
  43. package/transmute/site/.env.production +0 -2
  44. package/transmute/site/api/echo.js +0 -10
  45. package/transmute/site/api/hello.js +0 -12
  46. package/transmute/site/api/hits.js +0 -13
  47. package/transmute/site/api/time.js +0 -5
  48. package/transmute/site/package.json +0 -6
  49. package/transmute/site/public/app.js +0 -16
  50. package/transmute/site/public/index.html +0 -96
@@ -1,381 +0,0 @@
1
- // `build`: Vercel deployment model → Rust crate → .so, plus the cost sheet.
2
- //
3
- // readDeployment(root) → transmute() → <out>/.zoo/{crate/, manifest.json,
4
- // report.json, static-plan.json, build.json} → cargo build-sbf → .so
5
- //
6
- // The compiler (lib/compile/index.js) owns the JS→Rust translation; this
7
- // module owns the filesystem layout, the cargo invocation and the rent
8
- // estimate so `deploy` can pick the artifacts up without re-reading the
9
- // source repo.
10
- import fs from 'node:fs';
11
- import os from 'node:os';
12
- import path from 'node:path';
13
- import { spawn } from 'node:child_process';
14
- import { fileURLToPath } from 'node:url';
15
- import { readDeployment } from './vercel.js';
16
- import { ASSET_FIXED_HEADER, MANIFEST_PATH } from './wire.js';
17
- import { connect } from './solana.js';
18
- import { rpcUrl } from './wallet.js';
19
-
20
- const HERE = path.dirname(fileURLToPath(import.meta.url));
21
- export const RUNTIME_PATH = path.resolve(HERE, '..', 'runtime', 'zoo-host');
22
- export const SOLANA_BIN = path.join(os.homedir(), '.local', 'share', 'solana', 'install', 'active_release', 'bin');
23
- export const INSTALL_HINT = 'sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"';
24
- export const DEFAULT_ARCH = 'v0';
25
- export const ARCHES = ['v0', 'v1', 'v2', 'v3', 'v4'];
26
- /** Rent rule of thumb: 3480 lamports per byte-year × 2 years = 6960 lamports/byte ≈ 6.96 SOL/MB. */
27
- export const LAMPORTS_PER_BYTE = 6960;
28
- export const ACCOUNT_OVERHEAD_BYTES = 128;
29
- export const LAMPORTS_PER_SOL = 1_000_000_000;
30
- /** BPF upgradeable loader account sizes. */
31
- export const PROGRAM_ACCOUNT_BYTES = 36;
32
- export const PROGRAMDATA_HEADER_BYTES = 45;
33
- export const BUFFER_HEADER_BYTES = 37;
34
-
35
- // ---------------------------------------------------------------- naming
36
-
37
- /** A crate name Cargo accepts, derived from the project directory (or --name). */
38
- export function crateNameFor(root, name) {
39
- const base = (name || path.basename(path.resolve(root)) || 'site').toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
40
- const safe = /^[a-z]/.test(base) ? base : 'site-' + base;
41
- return safe || 'site';
42
- }
43
- export function soNameFor(crateName) { return crateName.replace(/-/g, '_') + '.so'; }
44
-
45
- /** Parse `[package] name` out of a Cargo.toml (falls back to `fallback`). */
46
- export function packageNameOf(cargoToml, fallback) {
47
- const m = String(cargoToml || '').match(/\[package\][^[]*?\bname\s*=\s*"([^"]+)"/);
48
- return m ? m[1] : fallback;
49
- }
50
-
51
- // ---------------------------------------------------------------- cargo
52
-
53
- /** Process env with the Solana toolchain on PATH (`cargo build-sbf` lives there). */
54
- export function solanaEnv(extra = {}) {
55
- const sep = path.delimiter;
56
- const cur = process.env.PATH || '';
57
- const parts = cur.split(sep);
58
- const PATH = parts.includes(SOLANA_BIN) ? cur : [SOLANA_BIN, path.join(os.homedir(), '.cargo', 'bin'), cur].filter(Boolean).join(sep);
59
- return { ...process.env, PATH, ...extra };
60
- }
61
-
62
- /** Absolute path of `cargo-build-sbf` on the given PATH, or null. */
63
- export function findCargoBuildSbf(env = solanaEnv()) {
64
- const exe = process.platform === 'win32' ? 'cargo-build-sbf.exe' : 'cargo-build-sbf';
65
- for (const dir of (env.PATH || '').split(path.delimiter)) {
66
- if (!dir) continue;
67
- const p = path.join(dir, exe);
68
- try { fs.accessSync(p, fs.constants.X_OK); return p; } catch { /* next */ }
69
- }
70
- return null;
71
- }
72
-
73
- /**
74
- * Run `cargo build-sbf --arch <arch>` in `crateDir`. Never throws for a
75
- * missing toolchain: returns `{ok:false, missing:true}` with the install hint.
76
- * @returns {Promise<{ok:boolean, missing?:boolean, soPath:string|null, arch:string, code?:number, output:string, error?:string}>}
77
- */
78
- export async function runCargoBuildSbf(crateDir, { arch = DEFAULT_ARCH, outDir, log = console.log, env = solanaEnv(), cargo = 'cargo', extraArgs = [] } = {}) {
79
- if (!ARCHES.includes(arch)) throw new Error(`unknown --arch ${arch} (expected one of ${ARCHES.join(', ')})`);
80
- const sbfOut = outDir || path.join(crateDir, 'target', 'deploy');
81
- if (!findCargoBuildSbf(env) && cargo === 'cargo') {
82
- log(`cargo-build-sbf not found on PATH (looked in ${SOLANA_BIN}).`);
83
- log(`install the Solana toolchain with:\n ${INSTALL_HINT}\nthen re-run \`build\`; continuing without a .so.`);
84
- return { ok: false, missing: true, soPath: null, arch, output: '', error: 'cargo-build-sbf not installed' };
85
- }
86
- const args = ['build-sbf', '--arch', arch, '--sbf-out-dir', sbfOut, ...extraArgs];
87
- log(`$ ${cargo} ${args.join(' ')} (in ${crateDir})`);
88
- const t0 = Date.now();
89
- const res = await new Promise((resolve) => {
90
- let output = '';
91
- let child;
92
- try {
93
- child = spawn(cargo, args, { cwd: crateDir, env, stdio: ['ignore', 'pipe', 'pipe'] });
94
- } catch (e) { return resolve({ code: -1, output: '', error: String(e?.message || e) }); }
95
- const onData = (d) => { const s = d.toString(); output += s; for (const line of s.split('\n')) if (line.trim()) log(' ' + line.trimEnd()); };
96
- child.stdout.on('data', onData);
97
- child.stderr.on('data', onData);
98
- child.on('error', (e) => resolve({ code: -1, output, error: String(e?.message || e) }));
99
- child.on('close', (code) => resolve({ code, output }));
100
- });
101
- if (res.error && /ENOENT/.test(res.error)) {
102
- log(`\`${cargo}\` is not installed. Install the Solana toolchain with:\n ${INSTALL_HINT}`);
103
- return { ok: false, missing: true, soPath: null, arch, output: res.output, error: res.error };
104
- }
105
- if (res.code !== 0) return { ok: false, soPath: null, arch, code: res.code, output: res.output, error: res.error || `cargo build-sbf exited with ${res.code}` };
106
- const soPath = findSo(sbfOut, res.output);
107
- log(`built ${soPath ? path.relative(process.cwd(), soPath) : '(no .so found?)'} in ${((Date.now() - t0) / 1000).toFixed(1)}s`);
108
- return { ok: !!soPath, soPath, arch, code: 0, output: res.output, error: soPath ? undefined : `no .so found in ${sbfOut}` };
109
- }
110
-
111
- function findSo(dir, output = '') {
112
- const m = output.match(/\b(\/\S+\.so)\b/);
113
- if (m && fs.existsSync(m[1])) return m[1];
114
- if (!fs.existsSync(dir)) return null;
115
- const sos = fs.readdirSync(dir).filter((f) => f.endsWith('.so')).map((f) => path.join(dir, f)).sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
116
- return sos[0] || null;
117
- }
118
-
119
- // ---------------------------------------------------------------- cost
120
-
121
- function accountBytesFor(kind, size, contentType = '') {
122
- switch (kind) {
123
- case 'asset': return ASSET_FIXED_HEADER + Buffer.byteLength(contentType.slice(0, 120)) + size;
124
- case 'program': return PROGRAM_ACCOUNT_BYTES;
125
- case 'programdata': return PROGRAMDATA_HEADER_BYTES + size;
126
- case 'buffer': return BUFFER_HEADER_BYTES + size;
127
- default: return size;
128
- }
129
- }
130
-
131
- /** Reach the cluster within `timeoutMs`; null when unreachable. */
132
- export async function probeConnection(cluster, { timeoutMs = 2500 } = {}) {
133
- let url;
134
- try { url = rpcUrl(cluster); } catch { return null; }
135
- const connection = connect(url);
136
- try {
137
- await Promise.race([connection.getSlot('confirmed'), new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), timeoutMs).unref?.())]);
138
- return connection;
139
- } catch { return null; }
140
- }
141
-
142
- /**
143
- * Rent the deployment will lock. Uses the cluster's real rent parameters
144
- * when a connection is given, else the 6.96 SOL/MB rule of thumb.
145
- * @param {{staticFiles:Array<{path:string,size:number,contentType?:string}>, soSize?:number|null, manifestBytes?:number, connection?:object|null, upgrade?:boolean}} o
146
- */
147
- export async function estimateCost({ staticFiles = [], soSize = null, manifestBytes = 0, connection = null, upgrade = false }) {
148
- const items = [];
149
- if (soSize != null) {
150
- if (upgrade) items.push({ label: 'program upgrade buffer (refunded on success)', kind: 'buffer', bytes: accountBytesFor('buffer', soSize), transient: true });
151
- else {
152
- items.push({ label: 'program account', kind: 'program', bytes: accountBytesFor('program', 0) });
153
- items.push({ label: `program data (.so = ${soSize.toLocaleString()} B)`, kind: 'programdata', bytes: accountBytesFor('programdata', soSize) });
154
- }
155
- } else {
156
- items.push({ label: 'program (no .so built yet)', kind: 'program', bytes: 0, unknown: true });
157
- }
158
- for (const f of staticFiles) items.push({ label: f.path, kind: 'asset', bytes: accountBytesFor('asset', f.size, f.contentType || '') });
159
- items.push({ label: MANIFEST_PATH + ' (manifest)', kind: 'asset', bytes: accountBytesFor('asset', manifestBytes, 'application/json; charset=utf-8') });
160
-
161
- let source = 'rule-of-thumb';
162
- const rentFor = new Map();
163
- if (connection) {
164
- try {
165
- const sizes = [...new Set(items.filter((i) => !i.unknown).map((i) => i.bytes))];
166
- await Promise.all(sizes.map(async (n) => rentFor.set(n, await connection.getMinimumBalanceForRentExemption(n))));
167
- source = 'rpc';
168
- } catch { rentFor.clear(); }
169
- }
170
- let lamports = 0, transientLamports = 0;
171
- for (const it of items) {
172
- it.lamports = it.unknown ? 0 : (rentFor.get(it.bytes) ?? (ACCOUNT_OVERHEAD_BYTES + it.bytes) * LAMPORTS_PER_BYTE);
173
- it.sol = it.lamports / LAMPORTS_PER_SOL;
174
- if (it.transient) transientLamports += it.lamports; else lamports += it.lamports;
175
- }
176
- const feeLamports = 5000 * (1 + Math.ceil((soSize || 0) / 900) + staticFiles.reduce((n, f) => n + 1 + Math.ceil(f.size / 900), 0) + 2);
177
- return {
178
- source, rpc: connection?.rpcEndpoint || null, items,
179
- lamports, sol: lamports / LAMPORTS_PER_SOL,
180
- transientLamports, transientSol: transientLamports / LAMPORTS_PER_SOL,
181
- feeLamports, feeSol: feeLamports / LAMPORTS_PER_SOL,
182
- totalLamports: lamports + transientLamports + feeLamports,
183
- totalSol: (lamports + transientLamports + feeLamports) / LAMPORTS_PER_SOL,
184
- unknownProgram: soSize == null,
185
- };
186
- }
187
-
188
- /** Render the cost estimate as a plain-text table. */
189
- export function formatCostTable(est, { maxRows = 40 } = {}) {
190
- const rows = est.items.map((i) => [i.label, i.unknown ? '?' : i.bytes.toLocaleString(), i.unknown ? '?' : i.sol.toFixed(4)]);
191
- const shown = rows.length > maxRows ? [...rows.slice(0, maxRows), [`… ${rows.length - maxRows} more files`, '', '']] : rows;
192
- shown.push(['rent total', '', est.sol.toFixed(4)]);
193
- if (est.transientLamports) shown.push(['+ transient (refunded)', '', est.transientSol.toFixed(4)]);
194
- shown.push(['+ tx fees (approx.)', '', est.feeSol.toFixed(4)]);
195
- shown.push(['TOTAL', '', est.totalSol.toFixed(4)]);
196
- const w0 = Math.min(60, Math.max(...shown.map((r) => r[0].length), 4));
197
- const w1 = Math.max(...shown.map((r) => r[1].length), 5);
198
- const lines = [` ${'item'.padEnd(w0)} ${'bytes'.padStart(w1)} ${'SOL'.padStart(8)}`];
199
- for (const [a, b, c] of shown) lines.push(` ${a.length > w0 ? a.slice(0, w0 - 1) + '…' : a.padEnd(w0)} ${b.padStart(w1)} ${c.padStart(8)}`);
200
- lines.push(` (rent per ${est.source === 'rpc' ? est.rpc.replace(/\?.*$/, '') : 'the 6.96 SOL/MB rule of thumb'}${est.unknownProgram ? '; program rent unknown until the .so is built' : ''})`);
201
- return lines.join('\n');
202
- }
203
-
204
- // ---------------------------------------------------------------- build
205
-
206
- async function loadTransmute() {
207
- try {
208
- const mod = await import('./compile/index.js');
209
- if (typeof mod.transmute !== 'function') throw new Error('lib/compile/index.js does not export transmute()');
210
- return mod.transmute;
211
- } catch (e) {
212
- if (e?.code === 'ERR_MODULE_NOT_FOUND' && /compile\/index\.js/.test(String(e.message))) {
213
- throw new Error('the compiler (lib/compile/index.js) is not present in this install of openzoo-transmute');
214
- }
215
- throw e;
216
- }
217
- }
218
-
219
- function writeJson(p, obj) { fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n'); }
220
-
221
- /** Point the generated Cargo.toml at a zoo-host checkout that exists on this machine. */
222
- export function fixRuntimePath(cargoToml, runtimePath) {
223
- const rt = runtimePath.replace(/\\/g, '/');
224
- const re = /(zoo-host\s*=\s*\{[^}]*?\bpath\s*=\s*")([^"]*)(")/;
225
- const m = cargoToml.match(re);
226
- if (!m) return cargoToml.includes('zoo-host') ? cargoToml : cargoToml + `\n[dependencies.zoo-host]\npath = "${rt}"\n`;
227
- if (path.isAbsolute(m[2]) && fs.existsSync(path.join(m[2], 'Cargo.toml'))) return cargoToml;
228
- return cargoToml.replace(re, `$1${rt}$3`);
229
- }
230
-
231
- /**
232
- * Build a Vercel-shaped repo into `<out>/.zoo/`.
233
- *
234
- * @param {string} root the app's directory
235
- * @param {{out?:string, name?:string, arch?:string, cluster?:string, runtimePath?:string, skipCargo?:boolean,
236
- * transmute?:Function, log?:Function, cargo?:string, connection?:object|null}} opts
237
- * @returns {Promise<{outDir:string, zooDir:string, crateDir:string, manifest:object, report:object, soPath:string|null, arch:string, costEstimate:object, deployment:object, notes:string[]}>}
238
- */
239
- export async function build(root = '.', opts = {}) {
240
- const log = opts.log ?? console.log;
241
- root = path.resolve(root);
242
- if (!fs.existsSync(root)) throw new Error(`${root}: no such directory`);
243
- const outDir = path.resolve(opts.out || path.join(root, '.zoo-out'));
244
- const zooDir = path.join(outDir, '.zoo');
245
- const crateDir = path.join(zooDir, 'crate');
246
- const notes = [];
247
-
248
- const deployment = readDeployment(root);
249
- const name = crateNameFor(root, opts.name);
250
- const runtimePath = path.resolve(opts.runtimePath || RUNTIME_PATH);
251
- log(`reading ${root} → ${deployment.source}${deployment.framework ? ` (${deployment.framework})` : ''}: ${deployment.functions.length} function(s), ${deployment.staticFiles.length} static file(s)`);
252
- for (const n of deployment.notes) { log(`note: ${n}`); notes.push(n); }
253
-
254
- const target = opts.target === 'shared' ? 'shared' : 'program';
255
- const transmute = opts.transmute || await loadTransmute();
256
- const result = await transmute(deployment, { name, runtimePath, arch: opts.arch, root, outDir, target });
257
- if (target === 'shared') return buildShared({ root, outDir, zooDir, deployment, name, result, notes, log, opts });
258
- if (!result || !result.crate || !result.crate['Cargo.toml'] || !result.crate['src/lib.rs']) throw new Error('transmute() returned no crate (expected {crate:{"Cargo.toml","src/lib.rs"}, manifest, report})');
259
- const manifest = { name, ...(result.manifest || {}) };
260
- if (!manifest.version) manifest.version = 1;
261
- if (!manifest.framework) manifest.framework = deployment.framework;
262
- if (!Array.isArray(manifest.routes)) manifest.routes = [];
263
- if (!Array.isArray(manifest.static)) manifest.static = deployment.staticFiles.map((f) => ({ path: f.path, contentType: f.contentType, size: f.size }));
264
- if (!manifest.config) manifest.config = deployment.config;
265
- const report = { eligible: [], ineligible: [], warnings: [], ...(result.report || {}) };
266
-
267
- // Lay the crate out.
268
- fs.rmSync(crateDir, { recursive: true, force: true });
269
- fs.mkdirSync(crateDir, { recursive: true });
270
- for (const [rel, content] of Object.entries(result.crate)) {
271
- const p = path.join(crateDir, rel);
272
- fs.mkdirSync(path.dirname(p), { recursive: true });
273
- fs.writeFileSync(p, rel === 'Cargo.toml' ? fixRuntimePath(String(content), runtimePath) : content);
274
- }
275
- const crateName = packageNameOf(result.crate['Cargo.toml'], name);
276
- const byPath = new Map(deployment.staticFiles.map((f) => [f.path, f]));
277
- const staticPlan = manifest.static.map((s) => ({ ...s, file: byPath.get(s.path)?.file || s.file || null }));
278
- writeJson(path.join(zooDir, 'manifest.json'), manifest);
279
- writeJson(path.join(zooDir, 'report.json'), report);
280
- writeJson(path.join(zooDir, 'static-plan.json'), staticPlan);
281
- log(`wrote ${path.relative(process.cwd(), zooDir) || zooDir}/{crate/, manifest.json, report.json, static-plan.json}`);
282
- log(`functions: ${report.eligible.length} eligible, ${report.ineligible.length} ineligible${report.warnings.length ? `, ${report.warnings.length} warning(s)` : ''}`);
283
- for (const i of report.ineligible) log(` ✗ ${i.name}: ${i.reason}${i.file ? ` (${path.relative(root, i.file)}${i.line ? ':' + i.line : ''})` : ''}`);
284
- for (const w of report.warnings) log(` ! ${typeof w === 'string' ? w : w.message || JSON.stringify(w)}`);
285
-
286
- // Compile.
287
- let arch = opts.arch || DEFAULT_ARCH;
288
- if (!opts.arch) { const n = `no --arch given: building for ${DEFAULT_ARCH} (mainnet); \`deploy\` re-detects the cluster's SBPF version and rebuilds if needed`; notes.push(n); log(`note: ${n}`); }
289
- let soPath = null, cargo = null;
290
- if (opts.skipCargo) { notes.push('cargo build skipped (--skip-cargo)'); log('skipping cargo build-sbf (--skip-cargo)'); }
291
- else {
292
- cargo = await runCargoBuildSbf(crateDir, { arch, outDir: path.join(zooDir, 'deploy'), log, cargo: opts.cargo, env: opts.env });
293
- if (cargo.ok) soPath = cargo.soPath;
294
- else if (cargo.missing) notes.push(`cargo-build-sbf not installed; install with: ${INSTALL_HINT}`);
295
- else throw Object.assign(new Error(`cargo build-sbf failed (${cargo.error})\n${cargo.output.split('\n').filter((l) => /error/i.test(l)).slice(0, 20).join('\n')}`), { output: cargo.output });
296
- }
297
- const soSize = soPath ? fs.statSync(soPath).size : null;
298
- writeJson(path.join(zooDir, 'build.json'), {
299
- name, crateName, root, outDir, arch, soPath, soSize, soName: soNameFor(crateName), builtAt: new Date().toISOString(),
300
- runtimePath, cargo: cargo ? { ok: cargo.ok, missing: !!cargo.missing } : { skipped: true }, target: 'program',
301
- });
302
-
303
- // Cost sheet.
304
- const connection = opts.connection !== undefined ? opts.connection : await probeConnection(opts.cluster || 'localnet');
305
- const manifestBytes = Buffer.byteLength(JSON.stringify({ ...manifest, programId: 'x'.repeat(44), deployedAt: new Date().toISOString() }));
306
- const costEstimate = await estimateCost({ staticFiles: staticPlan, soSize, manifestBytes, connection });
307
- log('');
308
- log(formatCostTable(costEstimate));
309
- log('');
310
- if (soPath) log(`.so: ${soPath} (${soSize.toLocaleString()} B, ${arch})`);
311
- log(`next: openzoo-transmute deploy ${path.relative(process.cwd(), outDir) || outDir} --cluster <localnet|devnet|mainnet>`);
312
- return { outDir, zooDir, crateDir, manifest, report, soPath, arch, costEstimate, deployment, notes, staticPlan };
313
- }
314
-
315
- // ---------------------------------------------------------------- shared runtime
316
-
317
- /** Bytes of the site account the shared runtime creates per site. */
318
- export const SITE_ACCOUNT_BYTES = 66;
319
-
320
- /** Rent for a shared-runtime site: site account + code asset + assets + manifest (no program). */
321
- export async function estimateSharedCost({ staticFiles = [], codeSize = 0, manifestBytes = 0, connection = null }) {
322
- const est = await estimateCost({ staticFiles, soSize: null, manifestBytes, connection });
323
- est.items = est.items.filter((i) => i.kind !== 'program');
324
- const rent = async (n) => { if (connection) { try { return await connection.getMinimumBalanceForRentExemption(n); } catch {} } return (ACCOUNT_OVERHEAD_BYTES + n) * LAMPORTS_PER_BYTE; };
325
- const codeBytes = accountBytesFor('asset', codeSize, 'application/x-zoo-bytecode');
326
- const extra = [
327
- { label: 'site account (shared runtime)', kind: 'site', bytes: SITE_ACCOUNT_BYTES, lamports: await rent(SITE_ACCOUNT_BYTES) },
328
- { label: `/.zoo/code.bin (bytecode = ${codeSize.toLocaleString()} B)`, kind: 'asset', bytes: codeBytes, lamports: await rent(codeBytes) },
329
- ];
330
- for (const it of extra) it.sol = it.lamports / LAMPORTS_PER_SOL;
331
- est.items = [...extra, ...est.items];
332
- est.lamports += extra.reduce((n, i) => n + i.lamports, 0);
333
- est.sol = est.lamports / LAMPORTS_PER_SOL;
334
- est.feeLamports = 5000 * (2 + Math.ceil(codeSize / 900) + staticFiles.reduce((n, f) => n + 1 + Math.ceil(f.size / 900), 0) + 2);
335
- est.feeSol = est.feeLamports / LAMPORTS_PER_SOL;
336
- est.totalLamports = est.lamports + est.transientLamports + est.feeLamports;
337
- est.totalSol = est.totalLamports / LAMPORTS_PER_SOL;
338
- est.unknownProgram = false;
339
- est.shared = true;
340
- return est;
341
- }
342
-
343
- /** `build --target shared`: bytecode module instead of a crate; nothing to compile. */
344
- async function buildShared({ root, outDir, zooDir, deployment, name, result, notes, log, opts }) {
345
- if (!result || !result.code || !result.code.length) throw new Error('transmute() returned no bytecode module for --target shared');
346
- const manifest = { name, ...(result.manifest || {}), target: 'shared' };
347
- if (!manifest.version) manifest.version = 1;
348
- if (!manifest.framework) manifest.framework = deployment.framework;
349
- if (!Array.isArray(manifest.routes)) manifest.routes = [];
350
- if (!Array.isArray(manifest.static)) manifest.static = deployment.staticFiles.map((f) => ({ path: f.path, contentType: f.contentType, size: f.size }));
351
- if (!manifest.config) manifest.config = deployment.config;
352
- const runtime = opts.runtime || process.env.OPENZOO_VM_PROGRAM || null;
353
- if (runtime) manifest.runtime = runtime;
354
- const report = { eligible: [], ineligible: [], warnings: [], ...(result.report || {}) };
355
-
356
- fs.rmSync(path.join(zooDir, 'crate'), { recursive: true, force: true });
357
- fs.mkdirSync(zooDir, { recursive: true });
358
- const codePath = path.join(zooDir, 'code.bin');
359
- fs.writeFileSync(codePath, result.code);
360
- const byPath = new Map(deployment.staticFiles.map((f) => [f.path, f]));
361
- const staticPlan = manifest.static.map((s) => ({ ...s, file: byPath.get(s.path)?.file || s.file || null }));
362
- writeJson(path.join(zooDir, 'manifest.json'), manifest);
363
- writeJson(path.join(zooDir, 'report.json'), report);
364
- writeJson(path.join(zooDir, 'static-plan.json'), staticPlan);
365
- const codeSize = result.code.length;
366
- writeJson(path.join(zooDir, 'build.json'), { name, root, outDir, target: 'shared', codePath, codeSize, runtime, builtAt: new Date().toISOString(), cargo: { skipped: true } });
367
- log(`wrote ${path.relative(process.cwd(), zooDir) || zooDir}/{code.bin (${codeSize.toLocaleString()} B bytecode), manifest.json, report.json, static-plan.json}`);
368
- log(`functions: ${report.eligible.length} eligible, ${report.ineligible.length} ineligible${report.warnings.length ? `, ${report.warnings.length} warning(s)` : ''}`);
369
- for (const i of report.ineligible) log(` ✗ ${i.name}: ${i.reason}${i.file ? ` (${path.relative(root, i.file)}${i.line ? ':' + i.line : ''})` : ''}`);
370
- for (const w of report.warnings) log(` ! ${typeof w === 'string' ? w : w.message || JSON.stringify(w)}`);
371
- if (!runtime) { const n = 'no --runtime given: pass --runtime <zoo-vm program id> (or OPENZOO_VM_PROGRAM) at deploy time'; notes.push(n); log(`note: ${n}`); }
372
-
373
- const connection = opts.connection !== undefined ? opts.connection : await probeConnection(opts.cluster || 'localnet');
374
- const manifestBytes = Buffer.byteLength(JSON.stringify({ ...manifest, runtime: 'x'.repeat(44), site: 'x'.repeat(44), deployedAt: new Date().toISOString() }));
375
- const costEstimate = await estimateSharedCost({ staticFiles: staticPlan, codeSize, manifestBytes, connection });
376
- log('');
377
- log(formatCostTable(costEstimate));
378
- log('');
379
- log(`next: openzoo-transmute deploy ${path.relative(process.cwd(), outDir) || outDir} --cluster mainnet${runtime ? '' : ' --runtime <id>'}`);
380
- return { outDir, zooDir, crateDir: null, manifest, report, soPath: null, codePath, codeSize, arch: null, target: 'shared', costEstimate, deployment, notes, staticPlan };
381
- }