openzoo 0.51.2 → 0.51.4

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.
@@ -251,8 +251,10 @@ export async function build(root = '.', opts = {}) {
251
251
  log(`reading ${root} → ${deployment.source}${deployment.framework ? ` (${deployment.framework})` : ''}: ${deployment.functions.length} function(s), ${deployment.staticFiles.length} static file(s)`);
252
252
  for (const n of deployment.notes) { log(`note: ${n}`); notes.push(n); }
253
253
 
254
+ const target = opts.target === 'shared' ? 'shared' : 'program';
254
255
  const transmute = opts.transmute || await loadTransmute();
255
- const result = await transmute(deployment, { name, runtimePath, arch: opts.arch, root, outDir });
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 });
256
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})');
257
259
  const manifest = { name, ...(result.manifest || {}) };
258
260
  if (!manifest.version) manifest.version = 1;
@@ -295,7 +297,7 @@ export async function build(root = '.', opts = {}) {
295
297
  const soSize = soPath ? fs.statSync(soPath).size : null;
296
298
  writeJson(path.join(zooDir, 'build.json'), {
297
299
  name, crateName, root, outDir, arch, soPath, soSize, soName: soNameFor(crateName), builtAt: new Date().toISOString(),
298
- runtimePath, cargo: cargo ? { ok: cargo.ok, missing: !!cargo.missing } : { skipped: true },
300
+ runtimePath, cargo: cargo ? { ok: cargo.ok, missing: !!cargo.missing } : { skipped: true }, target: 'program',
299
301
  });
300
302
 
301
303
  // Cost sheet.
@@ -309,3 +311,71 @@ export async function build(root = '.', opts = {}) {
309
311
  log(`next: openzoo-transmute deploy ${path.relative(process.cwd(), outDir) || outDir} --cluster <localnet|devnet|mainnet>`);
310
312
  return { outDir, zooDir, crateDir, manifest, report, soPath, arch, costEstimate, deployment, notes, staticPlan };
311
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
+ }
@@ -14,20 +14,25 @@ import path from 'node:path';
14
14
  import { PublicKey } from '@solana/web3.js';
15
15
  import { readDeployment } from './vercel.js';
16
16
  import { build } from './build.js';
17
- import { deploy, resolveOutDir, clusterLabel } from './deploy.js';
17
+ import { deploy, deployRuntime, resolveOutDir, clusterLabel } from './deploy.js';
18
18
  import { startGateway, DEFAULT_PORT } from './gateway.js';
19
19
  import { startHub, DEFAULT_HUB_PORT } from './hub.js';
20
- import { connect, getProgramInfo, readManifest } from './solana.js';
20
+ import { connect, getProgramInfo, getSiteInfo, readManifest } from './solana.js';
21
21
  import { loadWallet, rpcUrl } from './wallet.js';
22
22
  import { MANIFEST_PATH } from './wire.js';
23
23
 
24
24
  export const USAGE = `openzoo-transmute — Vercel app → Solana program + asset accounts
25
25
 
26
26
  usage:
27
- openzoo-transmute build [dir] [--out .zoo-out] [--name <crate>] [--arch v0|v3] [--cluster <c>] [--skip-cargo]
28
- openzoo-transmute deploy [dir|outDir] [--cluster mainnet|devnet|localnet|<url>] [--keypair <path>] [--yes] [--program <id>]
29
- [--concurrency 4] [--skip-assets] [--force] [--headroom 1.5]
30
- openzoo-transmute serve <programId> [--cluster <c>] [--port ${DEFAULT_PORT}] [--keypair <path>] [--host 127.0.0.1] [--quiet]
27
+ openzoo-transmute build [dir] [--target shared|program] [--out .zoo-out] [--name <crate>] [--arch v0|v3] [--cluster <c>] [--skip-cargo]
28
+ --target shared (default): bytecode for the shared zoo-vm runtime — a site is data accounts only (≈0.05–0.1 SOL)
29
+ --target program: your own Pinocchio program per site (≈1 SOL+)
30
+ openzoo-transmute deploy [dir|outDir] [--cluster mainnet|devnet|localnet|<url>] [--keypair <path>] [--yes] [--runtime <zoo-vm id>]
31
+ [--program <id>] [--concurrency 4] [--skip-assets] [--force] [--headroom 1.5]
32
+ openzoo-transmute serve <siteId|programId> [--runtime <zoo-vm id>] [--cluster <c>] [--port ${DEFAULT_PORT}] [--keypair <path>] [--host 127.0.0.1] [--quiet]
33
+ openzoo-transmute runtime deploy [--cluster <c>] [--keypair <path>] [--yes] [--program-keypair zoo-vm-keypair.json] [--so <path>]
34
+ deploy (or upgrade) the shared zoo-vm runtime once per cluster
35
+ openzoo-transmute runtime status <zoo-vm id> [--cluster <c>]
31
36
  openzoo-transmute hub [--cluster mainnet] [--port 8080] [--host 0.0.0.0] [--public-url <https://…>]
32
37
  hosted explorer for EVERY program on the cluster (/s/<programId>), read-only
33
38
  openzoo-transmute inspect [dir] print the Vercel model + eligibility report
@@ -35,7 +40,7 @@ usage:
35
40
  openzoo-transmute help
36
41
 
37
42
  clusters: mainnet (default, needs --yes to deploy), devnet, testnet, localnet, or an RPC URL.
38
- env: OPENZOO_CLUSTER, OPENZOO_RPC (mainnet RPC), OPENZOO_KEYPAIR / OPENZOO_WALLET (signer).
43
+ env: OPENZOO_CLUSTER, OPENZOO_RPC (mainnet RPC), OPENZOO_KEYPAIR / OPENZOO_WALLET (signer), OPENZOO_VM_PROGRAM (shared runtime id).
39
44
  `;
40
45
 
41
46
  /** `--k v`, `--k=v`, `--flag`, `--no-flag`; everything else is positional. */
@@ -114,26 +119,49 @@ async function tryTransmute(deployment) {
114
119
  }
115
120
  }
116
121
 
122
+ export const DEFAULT_TARGET = 'shared';
123
+
124
+ /** `--runtime` is the zoo-vm program id (shared) — or, for `--target program`, a zoo-host checkout path. */
125
+ function runtimeFlag(f) {
126
+ const v = f.runtime;
127
+ if (typeof v !== 'string') return { runtime: process.env.OPENZOO_VM_PROGRAM || null, runtimePath: f.runtimePath || undefined };
128
+ if (fs.existsSync(v) && fs.existsSync(path.join(v, 'Cargo.toml'))) return { runtime: process.env.OPENZOO_VM_PROGRAM || null, runtimePath: v };
129
+ new PublicKey(v);
130
+ return { runtime: v, runtimePath: f.runtimePath || undefined };
131
+ }
132
+
117
133
  async function cmdBuild(p, f, log) {
118
- const r = await build(p[0] || '.', { out: f.out, name: f.name, arch: f.arch, cluster: f.cluster, runtimePath: f.runtime, skipCargo: !!f.skipCargo, log });
119
- if (f.json) log(JSON.stringify({ outDir: r.outDir, soPath: r.soPath, arch: r.arch, report: r.report, costEstimate: { sol: r.costEstimate.sol, totalSol: r.costEstimate.totalSol, source: r.costEstimate.source } }, null, 2));
134
+ const target = f.target || process.env.OPENZOO_TARGET || DEFAULT_TARGET;
135
+ if (!['shared', 'program'].includes(target)) throw new Error(`--target must be shared or program (got ${target})`);
136
+ const { runtime, runtimePath } = runtimeFlag(f);
137
+ const r = await build(p[0] || '.', { out: f.out, name: f.name, arch: f.arch, cluster: f.cluster, target, runtime, runtimePath, skipCargo: !!f.skipCargo, log });
138
+ if (f.json) log(JSON.stringify({ outDir: r.outDir, target: r.target || 'program', soPath: r.soPath, codePath: r.codePath || null, codeSize: r.codeSize || null, arch: r.arch, report: r.report, costEstimate: { sol: r.costEstimate.sol, totalSol: r.costEstimate.totalSol, source: r.costEstimate.source } }, null, 2));
120
139
  return r.report.ineligible.length && !r.report.eligible.length && r.manifest.routes.length === 0 && r.deployment.functions.length ? 2 : 0;
121
140
  }
122
141
 
123
142
  async function cmdDeploy(p, f, log) {
124
- const r = await deploy({ outDir: p[0] || '.', cluster: f.cluster, keypair: f.keypair, programId: f.program, yes: !!f.yes, concurrency: f.concurrency ? Number(f.concurrency) : undefined, skipAssets: !!f.skipAssets, force: !!f.force, log });
143
+ const { runtime } = runtimeFlag(f);
144
+ const r = await deploy({ outDir: p[0] || '.', cluster: f.cluster, keypair: f.keypair, programId: f.program, runtime, yes: !!f.yes, concurrency: f.concurrency ? Number(f.concurrency) : undefined, skipAssets: !!f.skipAssets, force: !!f.force, headroom: f.headroom, log });
125
145
  if (f.json) log(JSON.stringify(r, null, 2));
126
146
  return 0;
127
147
  }
128
148
 
129
149
  async function cmdServe(p, f, log) {
130
150
  let programId = p[0];
151
+ let { runtime } = runtimeFlag(f);
131
152
  if (!programId) {
132
153
  // Fall back to the last deploy in ./ or ./.zoo-out.
133
- try { programId = JSON.parse(fs.readFileSync(path.join(resolveOutDir('.'), '.zoo', 'deploy.json'), 'utf8')).programId; } catch { /* none */ }
134
- if (!programId) throw new Error('usage: serve <programId> (no .zoo/deploy.json found to infer it from)');
154
+ try {
155
+ const d = JSON.parse(fs.readFileSync(path.join(resolveOutDir('.'), '.zoo', 'deploy.json'), 'utf8'));
156
+ programId = d.target === 'shared' ? d.site : d.programId;
157
+ if (d.target === 'shared' && !runtime) runtime = d.runtime;
158
+ } catch { /* none */ }
159
+ if (!programId) throw new Error('usage: serve <siteId|programId> [--runtime <zoo-vm id>] (no .zoo/deploy.json found to infer it from)');
135
160
  }
136
161
  new PublicKey(programId); // validate
162
+ // Shared runtime: the positional is the site id, the program is zoo-vm.
163
+ let site = null;
164
+ if (runtime) { site = programId; programId = runtime; }
137
165
  let keypair = null, walletPath = null;
138
166
  if (f.keypair !== false) {
139
167
  try { const w = loadWallet({ keypair: typeof f.keypair === 'string' ? f.keypair : undefined }); keypair = w.keypair; walletPath = w.path; }
@@ -142,10 +170,10 @@ async function cmdServe(p, f, log) {
142
170
  const port = f.port != null ? Number(f.port) : DEFAULT_PORT;
143
171
  const host = f.host || '127.0.0.1';
144
172
  if (keypair && !/^(127\.0\.0\.1|localhost|::1|\[::1\])$/.test(host)) log(`warning: binding ${host} with a signer: anyone who can reach this gateway can send transactions paid by ${keypair.publicKey.toBase58()}; use --no-keypair unless that is intended`);
145
- const gw = await startGateway({ programId, cluster: f.cluster, port, host, keypair, log, quiet: !!f.quiet });
173
+ const gw = await startGateway({ programId, site, cluster: f.cluster, port, host, keypair, log, quiet: !!f.quiet });
146
174
  const m = gw.state.manifest;
147
175
  log(`openzoo-transmute gateway → ${gw.url}/ (explorer ${gw.url}/.zoo/, manifest ${gw.url}${MANIFEST_PATH})`);
148
- log(`program ${programId} on ${gw.state.cluster} · ${m.routes.length} function(s), ${m.static ? m.static.length : '?'} asset(s) · signer ${keypair ? keypair.publicKey.toBase58() + (walletPath ? ` (${walletPath})` : '') : 'none (reads only)'}`);
176
+ log(`${site ? `site ${site} on runtime ${programId}` : `program ${programId}`} on ${gw.state.cluster} · ${m.routes.length} function(s), ${m.static ? m.static.length : '?'} asset(s) · signer ${keypair ? keypair.publicKey.toBase58() + (walletPath ? ` (${walletPath})` : '') : 'none (reads only)'}`);
149
177
  for (const r of m.routes) log(` ${(r.methods && r.methods.length ? r.methods.join('|') : 'ANY').padEnd(16)} ${r.routePath}`);
150
178
  const stop = () => gw.close().then(() => process.exit(0));
151
179
  process.once('SIGINT', stop);
@@ -165,10 +193,24 @@ async function cmdInspect(p, f, log) {
165
193
  }
166
194
 
167
195
  async function cmdStatus(p, f, log) {
168
- if (!p[0]) throw new Error('usage: status <programId> [--cluster <c>]');
169
- const programId = new PublicKey(p[0]);
196
+ if (!p[0]) throw new Error('usage: status <siteId|programId> [--runtime <zoo-vm id>] [--cluster <c>]');
197
+ const { runtime } = runtimeFlag(f);
170
198
  const rpc = rpcUrl(f.cluster);
171
199
  const connection = connect(rpc);
200
+ if (runtime) {
201
+ const site = new PublicKey(p[0]);
202
+ const [info, manifest] = await Promise.all([getSiteInfo(connection, new PublicKey(runtime), site), readManifest(connection, new PublicKey(runtime), site).catch(() => null)]);
203
+ const out = { site: site.toBase58(), runtime, cluster: clusterLabel(f.cluster), rpc: rpc.replace(/\?.*$/, ''), exists: info.exists, authority: info.authority ? info.authority.toBase58() : null, account: info.pda.toBase58(), manifest: manifest ? { name: manifest.name, routes: manifest.routes?.length ?? 0, static: manifest.static?.length ?? 0, deployedAt: manifest.deployedAt } : null };
204
+ if (f.json) { log(JSON.stringify(out, null, 2)); return info.exists ? 0 : 1; }
205
+ log(`site ${out.site} on runtime ${runtime} (${out.cluster})`);
206
+ if (!info.exists) { log('status NOT DEPLOYED'); return 1; }
207
+ log(`authority ${out.authority}`);
208
+ log(`account ${out.account}`);
209
+ if (manifest) { log(`manifest ${manifest.name || ''} · ${out.manifest.routes} function(s), ${out.manifest.static} asset(s) · deployed ${manifest.deployedAt || '?'}`); for (const r of manifest.routes || []) log(` ${(r.methods && r.methods.length ? r.methods.join('|') : 'ANY').padEnd(16)} ${r.routePath}`); }
210
+ else log(`manifest none at ${MANIFEST_PATH}`);
211
+ return 0;
212
+ }
213
+ const programId = new PublicKey(p[0]);
172
214
  const [info, slot, manifest] = await Promise.all([getProgramInfo(connection, programId), connection.getSlot('confirmed'), readManifest(connection, programId).catch(() => null)]);
173
215
  const out = {
174
216
  programId: programId.toBase58(), cluster: clusterLabel(f.cluster), rpc: rpc.replace(/\?.*$/, ''), slot,
@@ -215,13 +257,27 @@ async function cmdHub(p, f, log) {
215
257
  return 0;
216
258
  }
217
259
 
260
+ async function cmdRuntime(p, f, log) {
261
+ const sub = p[0];
262
+ if (sub === 'deploy') {
263
+ const r = await deployRuntime({ cluster: f.cluster, keypair: f.keypair, yes: !!f.yes, programKeypair: f.programKeypair, so: f.so, headroom: f.headroom, log });
264
+ if (f.json) log(JSON.stringify(r, null, 2));
265
+ return 0;
266
+ }
267
+ if (sub === 'status') {
268
+ if (!p[1]) throw new Error('usage: runtime status <zoo-vm id> [--cluster <c>]');
269
+ return cmdStatus([p[1]], { ...f, runtime: undefined }, log);
270
+ }
271
+ throw new Error('usage: runtime deploy|status');
272
+ }
273
+
218
274
  export async function run(argv = process.argv.slice(2), io = {}) {
219
275
  const log = io.log ?? console.log;
220
276
  const error = io.error ?? console.error;
221
277
  const { cmd, positionals, flags } = parseArgs(argv);
222
278
  if (flags.version) { log(JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version); return 0; }
223
279
  if (flags.help || cmd === 'help' || cmd === '--help') { log(USAGE); return 0; }
224
- const commands = { build: cmdBuild, deploy: cmdDeploy, serve: cmdServe, inspect: cmdInspect, status: cmdStatus, hub: cmdHub };
280
+ const commands = { build: cmdBuild, deploy: cmdDeploy, serve: cmdServe, inspect: cmdInspect, status: cmdStatus, hub: cmdHub, runtime: cmdRuntime };
225
281
  const fn = commands[cmd];
226
282
  if (!fn) { error(`unknown command: ${cmd}\n`); error(USAGE); process.exitCode = 1; return 1; }
227
283
  try {