openzoo 0.51.4 → 0.51.6

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 (55) hide show
  1. package/README.md +12 -1
  2. package/bin/openzoo.js +21 -46
  3. package/lib/chatgpt-install.js +106 -0
  4. package/lib/chatgpt.js +108 -90
  5. package/lib/config.js +34 -0
  6. package/lib/modelcommand.js +443 -0
  7. package/lib/pay.js +60 -4
  8. package/lib/proxy.js +39 -0
  9. package/package.json +4 -7
  10. package/transmute/README.md +0 -385
  11. package/transmute/bin/openzoo-transmute.js +0 -7
  12. package/transmute/docs/OPENZOO_CLI_PATCH.md +0 -139
  13. package/transmute/docs/VERCEL_TO_SOLANA.md +0 -381
  14. package/transmute/lib/build.js +0 -381
  15. package/transmute/lib/cli.js +0 -293
  16. package/transmute/lib/compile/bytecode.js +0 -591
  17. package/transmute/lib/compile/hostids.js +0 -11
  18. package/transmute/lib/compile/index.js +0 -159
  19. package/transmute/lib/compile/ir.js +0 -1634
  20. package/transmute/lib/compile/parse.js +0 -753
  21. package/transmute/lib/compile/rust.js +0 -781
  22. package/transmute/lib/deploy.js +0 -357
  23. package/transmute/lib/eligibility.js +0 -211
  24. package/transmute/lib/gateway.js +0 -710
  25. package/transmute/lib/hub.js +0 -290
  26. package/transmute/lib/solana.js +0 -387
  27. package/transmute/lib/vercel.js +0 -296
  28. package/transmute/lib/wallet.js +0 -41
  29. package/transmute/lib/wire.js +0 -208
  30. package/transmute/prebuilt/zoo_vm.v0.so +0 -0
  31. package/transmute/runtime/zoo-host/Cargo.lock +0 -588
  32. package/transmute/runtime/zoo-host/Cargo.toml +0 -30
  33. package/transmute/runtime/zoo-host/examples/vmrun.rs +0 -43
  34. package/transmute/runtime/zoo-host/src/assets.rs +0 -214
  35. package/transmute/runtime/zoo-host/src/ctx.rs +0 -350
  36. package/transmute/runtime/zoo-host/src/fmt.rs +0 -188
  37. package/transmute/runtime/zoo-host/src/helpers.rs +0 -245
  38. package/transmute/runtime/zoo-host/src/json.rs +0 -263
  39. package/transmute/runtime/zoo-host/src/kv.rs +0 -206
  40. package/transmute/runtime/zoo-host/src/lib.rs +0 -137
  41. package/transmute/runtime/zoo-host/src/site.rs +0 -94
  42. package/transmute/runtime/zoo-host/src/val.rs +0 -1050
  43. package/transmute/runtime/zoo-host/src/vm.rs +0 -902
  44. package/transmute/runtime/zoo-host/src/wire.rs +0 -239
  45. package/transmute/runtime/zoo-vm/Cargo.lock +0 -596
  46. package/transmute/runtime/zoo-vm/Cargo.toml +0 -23
  47. package/transmute/runtime/zoo-vm/src/lib.rs +0 -92
  48. package/transmute/site/.env.production +0 -2
  49. package/transmute/site/api/echo.js +0 -10
  50. package/transmute/site/api/hello.js +0 -12
  51. package/transmute/site/api/hits.js +0 -13
  52. package/transmute/site/api/time.js +0 -5
  53. package/transmute/site/package.json +0 -6
  54. package/transmute/site/public/app.js +0 -16
  55. 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
- }