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,357 +0,0 @@
1
- // `deploy`: push a built `<out>/.zoo/` to a cluster.
2
- //
3
- // detect SBPF arch (rebuild if the .so was built for another one)
4
- // → deploy or upgrade the program (keypair kept at .zoo/program-keypair.json)
5
- // → putAsset every static file
6
- // → write the manifest as the /.zoo/manifest.json asset
7
- //
8
- // Mainnet is refused without `yes` after the cost table has been printed.
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
- import { Keypair, PublicKey } from '@solana/web3.js';
12
- import { MANIFEST_PATH, CODE_PATH } from './wire.js';
13
- import { connect, deployProgram, upgradeProgram, getProgramInfo, putAsset, detectSbpfArch, pool, assetRent } from './solana.js';
14
- import { initSite, getSiteInfo } from './solana.js';
15
- import { loadWallet, rpcUrl } from './wallet.js';
16
- import { runCargoBuildSbf, estimateCost, formatCostTable, LAMPORTS_PER_SOL, INSTALL_HINT } from './build.js';
17
- import { DEFAULT_PORT } from './gateway.js';
18
-
19
- export const MANIFEST_CONTENT_TYPE = 'application/json; charset=utf-8';
20
-
21
- function readJson(p, fallback) {
22
- try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { if (fallback !== undefined) return fallback; throw new Error(`${p}: ${e.message}`); }
23
- }
24
- function writeJson(p, obj) { fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n'); }
25
-
26
- /** Accept the app dir, its `.zoo-out`, or the `.zoo` dir itself; return the out dir. */
27
- export function resolveOutDir(dir = '.') {
28
- const d = path.resolve(dir);
29
- const candidates = [d, path.join(d, '.zoo-out'), path.dirname(d)];
30
- for (const c of candidates) if (fs.existsSync(path.join(c, '.zoo', 'manifest.json'))) return c;
31
- throw new Error(`${d}: no .zoo/manifest.json here or in .zoo-out/; run \`openzoo-transmute build\` first`);
32
- }
33
-
34
- export function isMainnet(cluster, rpc) {
35
- const c = String(cluster || process.env.OPENZOO_CLUSTER || 'mainnet');
36
- if (/^(localnet|localhost|local|devnet|testnet)$/.test(c)) return false;
37
- if (/^https?:\/\//.test(c)) return /mainnet/i.test(c) || /(fluxrpc|helius|triton|quiknode|alchemy|ankr)/i.test(c) && !/devnet|testnet/i.test(c);
38
- return /mainnet/i.test(c) || /mainnet/i.test(rpc || '');
39
- }
40
-
41
- /** Genesis hashes of the public clusters: the one signal an RPC URL cannot disguise. */
42
- export const GENESIS_HASHES = {
43
- mainnet: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d',
44
- devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
45
- testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
46
- };
47
-
48
- /** Which public cluster the connection actually serves, by genesis hash; null for a local/unknown one or when unreachable. */
49
- export async function genesisCluster(connection) {
50
- try {
51
- const h = await connection.getGenesisHash();
52
- for (const [name, hash] of Object.entries(GENESIS_HASHES)) if (h === hash) return name;
53
- } catch { /* unreachable: the URL heuristic stands alone */ }
54
- return null;
55
- }
56
-
57
- export function clusterLabel(cluster) {
58
- const c = cluster || process.env.OPENZOO_CLUSTER || 'mainnet';
59
- if (/^https?:\/\//.test(c)) return /127\.0\.0\.1|localhost/.test(c) ? 'localnet' : c;
60
- return c;
61
- }
62
-
63
- /** Load or create the site's program keypair (kept next to the build so redeploys upgrade in place). */
64
- export function programKeypairFor(zooDir, { create = true } = {}) {
65
- const p = path.join(zooDir, 'program-keypair.json');
66
- if (fs.existsSync(p)) return { keypair: Keypair.fromSecretKey(Uint8Array.from(readJson(p))), path: p, created: false };
67
- if (!create) return null;
68
- const kp = Keypair.generate();
69
- fs.writeFileSync(p, JSON.stringify([...kp.secretKey]), { mode: 0o600 });
70
- return { keypair: kp, path: p, created: true };
71
- }
72
-
73
- /**
74
- * @param {{outDir?:string, cluster?:string, keypair?:string, programId?:string, yes?:boolean, concurrency?:number,
75
- * log?:Function, connection?:object, wallet?:object, skipAssets?:boolean, force?:boolean, cargo?:string}} o
76
- * @returns {Promise<{programId:string, signature:string|null, signatures:object, assets:{uploaded:number,skipped:number,failed:number,txs:number},
77
- * manifestPda:string, url:string, cluster:string, rpc:string, arch:string, upgraded:boolean, deployedAt:string}>}
78
- */
79
- export async function deploy(o = {}) {
80
- const log = o.log ?? console.log;
81
- const outDir = resolveOutDir(o.outDir || '.');
82
- const zooDir = path.join(outDir, '.zoo');
83
- const manifest = readJson(path.join(zooDir, 'manifest.json'));
84
- const buildInfo = readJson(path.join(zooDir, 'build.json'), {});
85
- const staticPlan = readJson(path.join(zooDir, 'static-plan.json'), []);
86
- const crateDir = path.join(zooDir, 'crate');
87
- if ((buildInfo.target || manifest.target) === 'shared') return deployShared(o, { log, outDir, zooDir, manifest, buildInfo, staticPlan });
88
-
89
- const rpc = o.connection?.rpcEndpoint || rpcUrl(o.cluster);
90
- const cluster = clusterLabel(o.cluster);
91
- const connection = o.connection || connect(rpc);
92
- // The URL heuristic can miss a private mainnet RPC; the genesis hash cannot. Never weakens the guard.
93
- let mainnet = isMainnet(o.cluster, rpc);
94
- const genesis = await genesisCluster(connection);
95
- if (genesis === 'mainnet' && !mainnet) { mainnet = true; log(`note: ${rpc.replace(/\?.*$/, '')} serves mainnet (genesis hash); treating it as mainnet`); }
96
- const wallet = o.wallet || loadWallet({ keypair: o.keypair });
97
- const payer = wallet.keypair;
98
- log(`cluster ${cluster} (${rpc.replace(/\?.*$/, '')}) · payer ${payer.publicKey.toBase58()}${wallet.path ? ` (${wallet.path})` : ''}`);
99
-
100
- // 1. Which SBPF version does the cluster accept, and does the .so match?
101
- const arch = await detectSbpfArch(connection);
102
- let soPath = buildInfo.soPath && fs.existsSync(buildInfo.soPath) ? buildInfo.soPath : null;
103
- const needsRebuild = !soPath || buildInfo.arch !== arch;
104
- if (needsRebuild && !fs.existsSync(path.join(crateDir, 'Cargo.toml'))) throw new Error(`${crateDir}: no crate; run build first`);
105
-
106
- // 2. Which program? --program <id> → upgrade that one; else the keypair next to the build.
107
- let programId, programKeypair = null, upgrade = false;
108
- if (o.programId) {
109
- programId = new PublicKey(o.programId);
110
- const info = await getProgramInfo(connection, programId);
111
- if (!info.exists) {
112
- const kp = programKeypairFor(zooDir, { create: false });
113
- if (!kp || !kp.keypair.publicKey.equals(programId)) throw new Error(`program ${programId.toBase58()} does not exist on ${cluster} and its keypair is not at ${zooDir}/program-keypair.json`);
114
- programKeypair = kp.keypair;
115
- } else upgrade = true;
116
- } else {
117
- const kp = programKeypairFor(zooDir);
118
- programKeypair = kp.keypair;
119
- programId = kp.keypair.publicKey;
120
- if (kp.created) log(`new program keypair → ${kp.path} (keep it: redeploys upgrade this program in place)`);
121
- const info = await getProgramInfo(connection, programId);
122
- upgrade = info.exists;
123
- if (upgrade && info.authority && !info.authority.equals(payer.publicKey)) throw new Error(`program ${programId.toBase58()} exists but its upgrade authority is ${info.authority.toBase58()}, not the payer; pass --keypair for that wallet`);
124
- }
125
-
126
- // 3. Cost sheet + mainnet guard — before anything is built or spent. The
127
- // estimate uses the .so we have (a rebuild for another arch barely moves it).
128
- const manifestOut = { ...manifest, programId: programId.toBase58(), cluster, rpc: rpc.replace(/\?.*$/, ''), arch, deployedAt: new Date().toISOString() };
129
- const manifestBytes = Buffer.byteLength(JSON.stringify(manifestOut));
130
- const knownSoSize = soPath ? fs.statSync(soPath).size : (buildInfo.soSize ?? null);
131
- let est = await estimateCost({ staticFiles: staticPlan, soSize: knownSoSize, manifestBytes, connection, upgrade });
132
- log('');
133
- log(formatCostTable(est));
134
- log('');
135
- const balance = await connection.getBalance(payer.publicKey);
136
- log(`${upgrade ? 'upgrade' : 'deploy'} ${programId.toBase58()} (${knownSoSize == null ? 'not built yet' : knownSoSize.toLocaleString() + ' B, ' + (needsRebuild ? `rebuilding for ${arch}` : arch)}) + ${staticPlan.length} asset(s); payer balance ${(balance / LAMPORTS_PER_SOL).toFixed(4)} SOL`);
137
- if (mainnet && !o.yes) throw new Error(`refusing to touch mainnet without --yes (estimated ${est.totalSol.toFixed(4)} SOL locked as rent)`);
138
-
139
- // 4. Rebuild for the cluster's arch if needed, then check the balance against the real size.
140
- if (needsRebuild) {
141
- log(soPath ? `built for ${buildInfo.arch}, cluster wants ${arch}: rebuilding` : `no .so in ${zooDir}; building for ${arch}`);
142
- const r = await runCargoBuildSbf(crateDir, { arch, outDir: path.join(zooDir, 'deploy'), log, cargo: o.cargo });
143
- if (!r.ok) throw new Error(r.missing ? `cargo-build-sbf is not installed; install with: ${INSTALL_HINT}` : `cargo build-sbf failed: ${r.error}`);
144
- soPath = r.soPath;
145
- writeJson(path.join(zooDir, 'build.json'), { ...buildInfo, arch, soPath, soSize: fs.statSync(soPath).size, builtAt: new Date().toISOString() });
146
- }
147
- const so = fs.readFileSync(soPath);
148
- if (so.length !== knownSoSize) est = await estimateCost({ staticFiles: staticPlan, soSize: so.length, manifestBytes, connection, upgrade });
149
- if (balance < est.totalLamports) throw new Error(`insufficient balance: ${(balance / LAMPORTS_PER_SOL).toFixed(4)} SOL < ${est.totalSol.toFixed(4)} SOL needed${mainnet ? '' : ' (airdrop: solana airdrop 10 --url ' + rpc + ')'}`);
150
-
151
- // 5. Program.
152
- const signatures = {};
153
- let progress = 0;
154
- const onProgress = (d, n) => { if (d === n || d - progress >= Math.max(1, Math.floor(n / 10))) { progress = d; log(` program bytes ${d}/${n} chunks`); } };
155
- const t0 = Date.now();
156
- if (upgrade) {
157
- const r = await upgradeProgram(connection, { payer, programId, so, onProgress }).catch((e) => {
158
- if (/exceeds the program's max data len/.test(String(e?.message))) throw new Error(`${e.message}; to deploy a fresh program remove ${path.join(zooDir, 'program-keypair.json')} and run deploy again`);
159
- throw e;
160
- });
161
- signatures.program = r.signature;
162
- log(`upgraded ${programId.toBase58()} in ${((Date.now() - t0) / 1000).toFixed(1)}s (${r.signature})`);
163
- } else {
164
- const r = await deployProgram(connection, { payer, programKeypair, so, onProgress, headroom: o.headroom ? Number(o.headroom) : 1 });
165
- signatures.program = r.signature;
166
- log(`deployed ${programId.toBase58()} in ${((Date.now() - t0) / 1000).toFixed(1)}s (${r.signature}); max data len ${r.maxDataLen.toLocaleString()} B`);
167
- }
168
-
169
- // 6. Assets.
170
- const assets = { uploaded: 0, skipped: 0, failed: 0, txs: 0 };
171
- const failures = [];
172
- if (!o.skipAssets) {
173
- const t1 = Date.now();
174
- let done = 0;
175
- await pool(staticPlan, async (f) => {
176
- const label = `[${String(++done).padStart(String(staticPlan.length).length)}/${staticPlan.length}] ${f.path}`;
177
- if (!f.file || !fs.existsSync(f.file)) { assets.failed++; failures.push(`${f.path}: source file missing (${f.file})`); log(`${label} MISSING ${f.file}`); return; }
178
- const data = fs.readFileSync(f.file);
179
- try {
180
- const r = await putAsset(connection, { authority: payer, programId, path: f.path, contentType: f.contentType || 'application/octet-stream', data, force: o.force });
181
- if (r.skipped) { assets.skipped++; log(`${label} unchanged (${data.length.toLocaleString()} B)`); }
182
- else { assets.uploaded++; assets.txs += r.txs; log(`${label} ${data.length.toLocaleString()} B in ${r.txs} tx`); }
183
- } catch (e) {
184
- assets.failed++; failures.push(`${f.path}: ${e?.message || e}`); log(`${label} FAILED ${String(e?.message || e).split('\n')[0]}`);
185
- }
186
- }, { concurrency: o.concurrency || 4 });
187
- log(`assets: ${assets.uploaded} uploaded, ${assets.skipped} unchanged, ${assets.failed} failed in ${((Date.now() - t1) / 1000).toFixed(1)}s`);
188
- }
189
-
190
- // 7. Manifest (always rewritten: it carries deployedAt).
191
- const mr = await putAsset(connection, { authority: payer, programId, path: MANIFEST_PATH, contentType: MANIFEST_CONTENT_TYPE, data: Buffer.from(JSON.stringify(manifestOut)), force: true });
192
- const manifestPda = mr.pda.toBase58();
193
- log(`manifest → ${MANIFEST_PATH} (${manifestPda}, ${manifestBytes.toLocaleString()} B)`);
194
-
195
- const url = `http://127.0.0.1:${DEFAULT_PORT}/`;
196
- const result = {
197
- programId: programId.toBase58(), signature: signatures.program || null, signatures, assets, manifestPda, url,
198
- cluster, rpc, arch, upgraded: upgrade, deployedAt: manifestOut.deployedAt, failures,
199
- };
200
- writeJson(path.join(zooDir, 'deploy.json'), { ...result, payer: payer.publicKey.toBase58() });
201
- log('');
202
- log(`serve it: openzoo-transmute serve ${programId.toBase58()} --cluster ${cluster}${wallet.path ? ` --keypair ${wallet.path}` : ''}`);
203
- log(`then open ${url} (explorer at ${url}.zoo/)`);
204
- if (failures.length) throw Object.assign(new Error(`${failures.length} asset(s) failed to upload:\n ${failures.join('\n ')}`), { result });
205
- return result;
206
- }
207
-
208
- /** Rent one asset of `size` bytes would lock on this cluster, in SOL. */
209
- export async function assetRentSol(connection, size, contentType = '') {
210
- return (await assetRent(connection, size, contentType)) / LAMPORTS_PER_SOL;
211
- }
212
-
213
- // ---------------------------------------------------------------- shared runtime
214
-
215
- /** Load or create the site keypair: its public key is the site id (looks like a program id, costs nothing). */
216
- export function siteKeypairFor(zooDir, { create = true } = {}) {
217
- const p = path.join(zooDir, 'site-keypair.json');
218
- if (fs.existsSync(p)) return { keypair: Keypair.fromSecretKey(Uint8Array.from(readJson(p))), path: p, created: false };
219
- if (!create) return null;
220
- const kp = Keypair.generate();
221
- fs.writeFileSync(p, JSON.stringify([...kp.secretKey]), { mode: 0o600 });
222
- return { keypair: kp, path: p, created: true };
223
- }
224
-
225
- /**
226
- * Deploy to the shared runtime: no program, only accounts — the site account,
227
- * the bytecode module at /.zoo/code.bin, the static assets and the manifest,
228
- * all PDAs of the runtime program namespaced by the site id.
229
- */
230
- async function deployShared(o, { log, outDir, zooDir, manifest, buildInfo, staticPlan }) {
231
- const runtime = o.runtime || process.env.OPENZOO_VM_PROGRAM || manifest.runtime || buildInfo.runtime;
232
- if (!runtime) throw new Error('shared target needs the runtime program id: --runtime <id> (or OPENZOO_VM_PROGRAM)');
233
- const rpc = o.connection?.rpcEndpoint || rpcUrl(o.cluster);
234
- const cluster = clusterLabel(o.cluster);
235
- const connection = o.connection || connect(rpc);
236
- let mainnet = isMainnet(o.cluster, rpc);
237
- const genesis = await genesisCluster(connection);
238
- if (genesis === 'mainnet' && !mainnet) { mainnet = true; log(`note: ${rpc.replace(/\?.*$/, '')} serves mainnet (genesis hash); treating it as mainnet`); }
239
- const wallet = o.wallet || loadWallet({ keypair: o.keypair });
240
- const payer = wallet.keypair;
241
- const codePath = buildInfo.codePath && fs.existsSync(buildInfo.codePath) ? buildInfo.codePath : path.join(zooDir, 'code.bin');
242
- if (!fs.existsSync(codePath)) throw new Error(`no bytecode module at ${codePath}; run build --target shared first`);
243
- const code = fs.readFileSync(codePath);
244
- const siteKp = o.site ? null : siteKeypairFor(zooDir);
245
- const site = o.site ? new PublicKey(o.site) : siteKp.keypair.publicKey;
246
- const runtimeInfo = await getProgramInfo(connection, runtime);
247
- if (!runtimeInfo.exists) throw new Error(`runtime program ${runtime} is not deployed on ${cluster}`);
248
- log(`cluster ${cluster} (${rpc.replace(/\?.*$/, '')}) · payer ${payer.publicKey.toBase58()} · runtime ${new PublicKey(runtime).toBase58()} · site ${site.toBase58()}${siteKp?.created ? ' (new)' : ''}`);
249
-
250
- // Cost: site account + code + assets + manifest, all rent-exempt accounts.
251
- const manifestOut = { ...manifest, target: 'shared', runtime: new PublicKey(runtime).toBase58(), site: site.toBase58(), cluster, rpc: rpc.replace(/\?.*$/, ''), deployedAt: new Date().toISOString() };
252
- const manifestBytes = Buffer.byteLength(JSON.stringify(manifestOut));
253
- const items = [{ label: 'site account', bytes: 66 }, { label: `code module (${code.length.toLocaleString()} B)`, bytes: code.length + 20 }];
254
- for (const f of staticPlan) items.push({ label: f.path, bytes: (f.size ?? 0) + 7 + (f.contentType || '').length });
255
- items.push({ label: '/.zoo/manifest.json', bytes: manifestBytes + 40 });
256
- let total = 0;
257
- for (const it of items) { it.lamports = await connection.getMinimumBalanceForRentExemption(it.bytes); total += it.lamports; }
258
- const fees = 5000 * (3 + staticPlan.length * 3 + Math.ceil(code.length / 900) + 4);
259
- log(' item bytes SOL');
260
- for (const it of items) log(` ${it.label.padEnd(34)}${String(it.bytes).padStart(8)} ${(it.lamports / LAMPORTS_PER_SOL).toFixed(4).padStart(8)}`);
261
- log(` ${'rent total'.padEnd(34)}${''.padStart(8)} ${(total / LAMPORTS_PER_SOL).toFixed(4).padStart(8)}`);
262
- log(` ${'+ tx fees (approx.)'.padEnd(34)}${''.padStart(8)} ${(fees / LAMPORTS_PER_SOL).toFixed(4).padStart(8)}`);
263
- log(` ${'TOTAL'.padEnd(34)}${''.padStart(8)} ${((total + fees) / LAMPORTS_PER_SOL).toFixed(4).padStart(8)}`);
264
- if (mainnet && !o.yes) throw new Error('this is mainnet: pass --yes to spend the amount above');
265
- const balance = await connection.getBalance(payer.publicKey);
266
- if (balance < total + fees) throw new Error(`payer has ${(balance / LAMPORTS_PER_SOL).toFixed(4)} SOL, needs ~${((total + fees) / LAMPORTS_PER_SOL).toFixed(4)}`);
267
-
268
- const t0 = Date.now();
269
- const si = await initSite(connection, { authority: payer, runtime, site });
270
- log(`${si.created ? 'created' : 'reusing'} site account ${si.pda.toBase58()}`);
271
- const cr = await putAsset(connection, { authority: payer, programId: runtime, site, path: CODE_PATH, contentType: 'application/x-zoo-bytecode', data: code, force: o.force });
272
- log(`code → ${CODE_PATH} (${cr.pda.toBase58()}, ${code.length.toLocaleString()} B${cr.skipped ? ', unchanged' : ''})`);
273
- let uploaded = 0, skipped = 0, failed = 0;
274
- const failures = [];
275
- if (!o.skipAssets) {
276
- const files = staticPlan.filter((f) => f.file || f.path);
277
- let done = 0;
278
- await pool(files, async (f) => {
279
- try {
280
- const data = fs.readFileSync(f.file);
281
- const r = await putAsset(connection, { authority: payer, programId: runtime, site, path: f.path, contentType: f.contentType || 'application/octet-stream', data, force: o.force });
282
- if (r.skipped) skipped++; else uploaded++;
283
- log(`[${++done}/${files.length}] ${f.path} ${r.skipped ? 'unchanged' : `${data.length.toLocaleString()} B in ${r.txs} tx`}`);
284
- } catch (e) { failed++; failures.push({ path: f.path, error: String(e?.message || e) }); log(`[${++done}/${files.length}] ${f.path} FAILED: ${e?.message || e}`); }
285
- }, { concurrency: o.concurrency ? Number(o.concurrency) : 4 });
286
- log(`assets: ${uploaded} uploaded, ${skipped} unchanged, ${failed} failed`);
287
- }
288
- const mr = await putAsset(connection, { authority: payer, programId: runtime, site, path: MANIFEST_PATH, contentType: MANIFEST_CONTENT_TYPE, data: Buffer.from(JSON.stringify(manifestOut)), force: true });
289
- log(`manifest → ${MANIFEST_PATH} (${mr.pda.toBase58()}, ${manifestBytes.toLocaleString()} B) in ${((Date.now() - t0) / 1000).toFixed(1)}s`);
290
- const out = { target: 'shared', site: site.toBase58(), programId: site.toBase58(), runtime: new PublicKey(runtime).toBase58(), sitePda: si.pda.toBase58(), manifestPda: mr.pda.toBase58(), assets: { uploaded, skipped, failed }, cluster, rpc, deployedAt: manifestOut.deployedAt, failures, url: `${process.env.OPENZOO_HUB_URL || 'https://sites.openzoo.fun'}/s/${site.toBase58()}` };
291
- writeJson(path.join(zooDir, 'deploy.json'), out);
292
- log(`\nserve it: openzoo-transmute serve ${site.toBase58()} --runtime ${new PublicKey(runtime).toBase58()} --cluster ${cluster}`);
293
- log(`or open ${out.url}`);
294
- return out;
295
- }
296
-
297
- // ---------------------------------------------------------------- runtime program
298
-
299
- const HERE_DEPLOY = path.dirname(new URL(import.meta.url).pathname);
300
- export const PREBUILT_DIR = path.resolve(HERE_DEPLOY, '..', 'prebuilt');
301
- export const RUNTIME_CRATE = path.resolve(HERE_DEPLOY, '..', 'runtime', 'zoo-vm');
302
-
303
- /** The zoo-vm .so for an SBPF arch: prebuilt (v0 = mainnet) or built from runtime/zoo-vm. */
304
- export async function runtimeSoFor(arch, { log = console.log, so } = {}) {
305
- if (so) return { soPath: path.resolve(so), source: 'given' };
306
- const pre = path.join(PREBUILT_DIR, `zoo_vm.${arch}.so`);
307
- if (fs.existsSync(pre)) return { soPath: pre, source: 'prebuilt' };
308
- if (!fs.existsSync(path.join(RUNTIME_CRATE, 'Cargo.toml'))) throw new Error(`no prebuilt zoo-vm for ${arch} (${pre}) and no runtime/zoo-vm crate to build it from`);
309
- const r = await runCargoBuildSbf(RUNTIME_CRATE, { arch, outDir: path.join(RUNTIME_CRATE, 'target', 'deploy-' + arch), log });
310
- if (!r.ok) throw new Error(r.missing ? `no prebuilt zoo-vm for ${arch}; install the toolchain to build it: ${INSTALL_HINT}` : `zoo-vm build failed: ${r.error}`);
311
- return { soPath: r.soPath, source: 'built' };
312
- }
313
-
314
- /**
315
- * Deploy (or upgrade) the shared runtime program once per cluster. Sites then
316
- * cost only their accounts. Keypair kept at `programKeypair` (default
317
- * ./zoo-vm-keypair.json) so a redeploy upgrades in place.
318
- */
319
- export async function deployRuntime(o = {}) {
320
- const log = o.log ?? console.log;
321
- const rpc = rpcUrl(o.cluster);
322
- const cluster = clusterLabel(o.cluster);
323
- const connection = o.connection || connect(rpc);
324
- let mainnet = isMainnet(o.cluster, rpc);
325
- if ((await genesisCluster(connection)) === 'mainnet') mainnet = true;
326
- const wallet = o.wallet || loadWallet({ keypair: o.keypair });
327
- const payer = wallet.keypair;
328
- const arch = await detectSbpfArch(connection);
329
- const { soPath, source } = await runtimeSoFor(arch, { log, so: o.so });
330
- const so = fs.readFileSync(soPath);
331
- const kpPath = path.resolve(o.programKeypair || 'zoo-vm-keypair.json');
332
- let programKeypair;
333
- if (fs.existsSync(kpPath)) programKeypair = Keypair.fromSecretKey(Uint8Array.from(readJson(kpPath)));
334
- else { programKeypair = Keypair.generate(); fs.writeFileSync(kpPath, JSON.stringify([...programKeypair.secretKey]), { mode: 0o600 }); log(`new runtime keypair → ${kpPath} (keep it: redeploys upgrade in place)`); }
335
- const programId = programKeypair.publicKey;
336
- const info = await getProgramInfo(connection, programId);
337
- const upgrade = info.exists;
338
- const est = await estimateCost({ staticFiles: [], soSize: so.length, manifestBytes: 0, connection, upgrade });
339
- est.items = est.items.filter((i) => i.kind !== 'asset');
340
- est.lamports = est.items.filter((i) => !i.transient).reduce((n, i) => n + i.lamports, 0); est.sol = est.lamports / LAMPORTS_PER_SOL;
341
- est.totalLamports = est.lamports + est.transientLamports + est.feeLamports; est.totalSol = est.totalLamports / LAMPORTS_PER_SOL;
342
- log(`cluster ${cluster} (${rpc.replace(/\?.*$/, '')}) · payer ${payer.publicKey.toBase58()} · runtime ${programId.toBase58()} (${upgrade ? 'upgrade' : 'new'}) · ${path.basename(soPath)} ${so.length.toLocaleString()} B ${arch} (${source})`);
343
- log(formatCostTable(est));
344
- const bal = await connection.getBalance(payer.publicKey);
345
- if (bal < est.totalLamports) throw new Error(`payer has ${(bal / LAMPORTS_PER_SOL).toFixed(4)} SOL, needs ≈ ${est.totalSol.toFixed(4)} SOL`);
346
- if (mainnet && !o.yes) throw new Error(`refusing to spend ≈ ${est.totalSol.toFixed(4)} SOL on mainnet without --yes`);
347
- let done = 0; const total = Math.ceil(so.length / 900);
348
- const onProgress = (n) => { done += 1; if (done % 25 === 0 || done === total) log(` wrote ${Math.min(done, total)}/${total} chunks`); };
349
- const t0 = Date.now();
350
- const r = upgrade
351
- ? await upgradeProgram(connection, { payer, programId, so, onProgress })
352
- : await deployProgram(connection, { payer, programKeypair, so, onProgress, headroom: o.headroom ? Number(o.headroom) : 1 });
353
- log(`${upgrade ? 'upgraded' : 'deployed'} runtime ${programId.toBase58()} in ${((Date.now() - t0) / 1000).toFixed(1)}s (${r.signature})`);
354
- log(`\nsites now deploy against it: openzoo-transmute build <dir> --target shared && openzoo-transmute deploy <dir> --runtime ${programId.toBase58()} --cluster ${cluster}`);
355
- log(`hub: OPENZOO_VM_PROGRAM=${programId.toBase58()}`);
356
- return { programId: programId.toBase58(), signature: r.signature, upgrade, arch, soPath, cluster, rpc, keypairPath: kpPath };
357
- }
@@ -1,211 +0,0 @@
1
- // Eligibility rules: which Lambdas can be re-hosted on chain, and why not.
2
- //
3
- // A handler is eligible when everything it does can be expressed in the
4
- // zoo-host subset: pure computation over JS values, the request/response
5
- // bridge, `@vercel/kv`, the baked environment table and the cluster clock.
6
- // Anything that reaches outside the transaction (network, filesystem,
7
- // timers, randomness) or outside the subset (classes, generators, regex)
8
- // makes the whole function ineligible; the reason carries `file:line` so the
9
- // user can fix or exclude it.
10
- import path from 'node:path';
11
-
12
- export class Ineligible extends Error {
13
- constructor(reason, { file = null, line = null, node = null } = {}) {
14
- super(reason);
15
- this.name = 'Ineligible';
16
- this.reason = reason;
17
- this.file = file;
18
- this.line = line ?? node?.loc?.start?.line ?? null;
19
- }
20
- toJSON() {
21
- return { reason: this.reason, file: this.file, line: this.line };
22
- }
23
- /** "reason (file:line)" */
24
- describe() {
25
- const where = this.file ? ` (${this.file}${this.line ? ':' + this.line : ''})` : this.line ? ` (line ${this.line})` : '';
26
- return this.reason + where;
27
- }
28
- }
29
-
30
- /** Throw an Ineligible for an AST node. */
31
- export function ineligible(reason, node, file) {
32
- return new Ineligible(reason, { file, node });
33
- }
34
-
35
- // ---------------------------------------------------------------- imports
36
-
37
- /**
38
- * Module specifiers we know how to bind. Each entry maps an imported name to
39
- * a "special" the lowering understands, or `null` for names that are types /
40
- * inert at runtime (safe to ignore).
41
- */
42
- export const ALLOWED_IMPORTS = {
43
- '@vercel/kv': { kv: 'Kv', default: 'Kv', createClient: 'KvFactory', VercelKV: null },
44
- 'next/server': { NextResponse: 'NextResponse', NextRequest: null, userAgent: 'ineligible:userAgent() reads the request UA table, which is not available on chain', after: 'ineligible:after() schedules work past the response; there is no "after" on chain' },
45
- 'next': { NextApiRequest: null, NextApiResponse: null, NextApiHandler: null, PageConfig: null, GetServerSideProps: null },
46
- 'next/types': { '*': null },
47
- '@vercel/node': { VercelRequest: null, VercelResponse: null, VercelApiHandler: null },
48
- '@vercel/edge': { '*': 'ineligible:@vercel/edge helpers (geolocation/ipAddress/rewrite) read edge-network state that does not exist on chain' },
49
- '@vercel/functions': { '*': 'ineligible:@vercel/functions (waitUntil/geolocation/ipAddress) is not available on chain: nothing runs after the transaction' },
50
- 'next/headers': { '*': 'ineligible:next/headers (cookies()/headers()) is not supported yet; read request.headers / request.cookies instead' },
51
- 'next/navigation': { '*': 'ineligible:next/navigation is a rendering API, not a route API' },
52
- 'next/cache': { '*': 'ineligible:next/cache (revalidatePath/revalidateTag/unstable_cache) has no meaning on chain' },
53
- };
54
-
55
- /** Resolve one import. Returns [{local, special|null}] or throws Ineligible. */
56
- export function bindImport(source, specifiers, { file, line } = {}) {
57
- const where = { file, line };
58
- if (source.startsWith('.') || source.startsWith('/') || source.startsWith('@/') || source.startsWith('~/')) {
59
- throw new Ineligible(`import of local module \`${source}\`: local imports are not transmuted yet; inline the helper into the route file`, where);
60
- }
61
- if (source.startsWith('node:') || NODE_BUILTINS.has(source)) {
62
- throw new Ineligible(`import of Node built-in \`${source}\`: no filesystem, network, crypto or process access on chain`, where);
63
- }
64
- const table = ALLOWED_IMPORTS[source];
65
- if (!table) {
66
- throw new Ineligible(`import of \`${source}\`: only @vercel/kv, next/server, and the next / @vercel/node types can be used on chain`, where);
67
- }
68
- const out = [];
69
- for (const s of specifiers) {
70
- const imported = s.imported === undefined ? 'default' : s.imported;
71
- let special;
72
- if (imported === '*') {
73
- special = { t: 'Namespace', source };
74
- } else {
75
- const entry = imported in table ? table[imported] : table['*'];
76
- if (entry === undefined) {
77
- throw new Ineligible(`\`${imported}\` is not a known export of ${source} (supported: ${Object.keys(table).filter((k) => k !== '*').join(', ')})`, where);
78
- }
79
- if (typeof entry === 'string' && entry.startsWith('ineligible:')) {
80
- throw new Ineligible(entry.slice('ineligible:'.length), where);
81
- }
82
- special = entry ? { t: entry } : null;
83
- }
84
- out.push({ local: s.local, imported, special });
85
- }
86
- return out;
87
- }
88
-
89
- export const NODE_BUILTINS = new Set([
90
- 'fs', 'fs/promises', 'path', 'os', 'http', 'https', 'net', 'dns', 'crypto', 'child_process', 'stream', 'util', 'url', 'zlib',
91
- 'events', 'buffer', 'querystring', 'readline', 'worker_threads', 'cluster', 'tls', 'dgram', 'assert', 'module', 'process', 'timers',
92
- 'timers/promises', 'string_decoder', 'perf_hooks', 'async_hooks', 'vm', 'v8', 'inspector', 'tty', 'punycode', 'constants', 'sys',
93
- ]);
94
-
95
- // ---------------------------------------------------------------- globals
96
-
97
- /** Free identifiers that are meaningful on chain. Everything else is unknown. */
98
- export const KNOWN_GLOBALS = new Set([
99
- 'undefined', 'NaN', 'Infinity', 'Math', 'JSON', 'Object', 'Array', 'Number', 'String', 'Boolean', 'parseInt', 'parseFloat',
100
- 'isNaN', 'isFinite', 'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'decodeURI', 'Date', 'Error', 'TypeError',
101
- 'RangeError', 'SyntaxError', 'console', 'process', 'Response', 'URL', 'Promise', 'Request', 'Headers', 'globalThis',
102
- ]);
103
-
104
- /** Free identifiers with a specific reason for being unavailable. */
105
- export const BANNED_GLOBALS = {
106
- fetch: 'fetch(): network access is not available on chain (a Solana instruction cannot make HTTP requests)',
107
- require: 'require(): dynamic module loading is not available on chain',
108
- setTimeout: 'setTimeout: timers are not available on chain (instructions are synchronous)',
109
- setInterval: 'setInterval: timers are not available on chain',
110
- setImmediate: 'setImmediate: timers are not available on chain',
111
- queueMicrotask: 'queueMicrotask: no event loop on chain',
112
- Buffer: 'Buffer is not available on chain; work with strings and arrays',
113
- crypto: 'crypto / Web Crypto is not available on chain',
114
- TextEncoder: 'TextEncoder is not available on chain',
115
- TextDecoder: 'TextDecoder is not available on chain',
116
- XMLHttpRequest: 'XMLHttpRequest: network access is not available on chain',
117
- WebSocket: 'WebSocket: network access is not available on chain',
118
- window: '`window` does not exist in a Lambda',
119
- document: '`document` does not exist in a Lambda',
120
- eval: 'eval() is not supported',
121
- Function: 'the Function constructor is not supported',
122
- Map: 'Map is not supported on chain; use a plain object',
123
- Set: 'Set is not supported on chain; use an array',
124
- WeakMap: 'WeakMap is not supported on chain',
125
- WeakSet: 'WeakSet is not supported on chain',
126
- Symbol: 'Symbol is not supported on chain',
127
- Proxy: 'Proxy is not supported on chain',
128
- Reflect: 'Reflect is not supported on chain',
129
- RegExp: 'regular expressions are not supported on chain',
130
- Intl: 'Intl is not available on chain',
131
- BigInt: 'BigInt is not supported on chain (numbers are f64)',
132
- ArrayBuffer: 'typed arrays / ArrayBuffer are not supported on chain',
133
- Uint8Array: 'typed arrays are not supported on chain',
134
- Int32Array: 'typed arrays are not supported on chain',
135
- Float64Array: 'typed arrays are not supported on chain',
136
- DataView: 'DataView is not supported on chain',
137
- structuredClone: 'structuredClone is not available on chain; use JSON.parse(JSON.stringify(x))',
138
- FormData: 'FormData is not supported on chain; send JSON',
139
- Blob: 'Blob is not supported on chain',
140
- File: 'File is not supported on chain',
141
- ReadableStream: 'streaming responses are not supported on chain',
142
- WritableStream: 'streaming responses are not supported on chain',
143
- TransformStream: 'streaming responses are not supported on chain',
144
- AbortController: 'AbortController is not available on chain',
145
- performance: 'performance.now() is not available on chain; use Date.now()',
146
- navigator: '`navigator` does not exist in a Lambda',
147
- atob: 'base64 helpers are not available on chain yet',
148
- btoa: 'base64 helpers are not available on chain yet',
149
- WebAssembly: 'WebAssembly is not available on chain',
150
- URLSearchParams: 'constructing URLSearchParams is not supported yet; read request.nextUrl.searchParams / req.query',
151
- Headers: 'constructing Headers is not supported; pass a plain object as `headers` in the Response init',
152
- Request: 'constructing a Request is not supported on chain',
153
- };
154
-
155
- /** Method names the on-chain `Val::call` implements (union over receiver types). */
156
- export const RUNTIME_METHODS = new Set([
157
- // strings
158
- 'toUpperCase', 'toLowerCase', 'trim', 'trimStart', 'trimEnd', 'includes', 'startsWith', 'endsWith', 'indexOf', 'split', 'slice',
159
- 'substring', 'substr', 'charAt', 'charCodeAt', 'replace', 'replaceAll', 'repeat', 'padStart', 'padEnd', 'toString', 'valueOf',
160
- 'concat', 'at',
161
- // arrays
162
- 'push', 'pop', 'shift', 'unshift', 'join', 'splice', 'reverse', 'flat', 'map', 'filter', 'find', 'findIndex', 'some', 'every',
163
- 'forEach', 'reduce', 'flatMap', 'sort',
164
- // numbers
165
- 'toFixed',
166
- // objects
167
- 'hasOwnProperty',
168
- ]);
169
- export const CALLBACK_METHODS = new Set(['map', 'filter', 'find', 'findIndex', 'some', 'every', 'forEach', 'reduce', 'flatMap', 'sort']);
170
- export const MUTATING_METHODS = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort']);
171
-
172
- /** `Math.*` the runtime implements. */
173
- export const MATH_FUNCS = new Set(['floor', 'ceil', 'round', 'trunc', 'abs', 'sqrt', 'pow', 'sign', 'log', 'log2', 'log10', 'exp', 'sin', 'cos', 'tan', 'atan2', 'min', 'max', 'hypot']);
174
- export const MATH_CONSTS = { PI: Math.PI, E: Math.E, LN2: Math.LN2, LN10: Math.LN10, LOG2E: Math.LOG2E, LOG10E: Math.LOG10E, SQRT2: Math.SQRT2, SQRT1_2: Math.SQRT1_2 };
175
- export const GLOBAL_FUNCS = new Set(['Number', 'String', 'Boolean', 'isNaN', 'isFinite', 'parseFloat', 'parseInt', 'encodeURIComponent', 'encodeURI', 'decodeURIComponent', 'decodeURI']);
176
-
177
- /** `kv.<method>` → runtime call. Anything else is ineligible. */
178
- export const KV_METHODS = {
179
- get: 'kv_get', set: 'kv_set', incr: 'kv_incr', incrby: 'kv_incrby', decr: 'kv_decr', decrby: 'kv_decrby', del: 'kv_del', exists: 'kv_exists',
180
- };
181
-
182
- // ---------------------------------------------------------------- function metadata
183
-
184
- /** Reasons that come from the Lambda's config rather than its code. `null` when fine. */
185
- export function functionMetaReason(fn) {
186
- if (fn.middleware) return 'middleware runs before routing on the edge network; the gateway serves routes directly, so middleware is not transmuted';
187
- if (fn.supportsResponseStreaming) return 'streaming responses (supportsResponseStreaming) are not possible: an instruction answers once';
188
- if (fn.prerender) return 'ISR / prerender config: this function is a rendered page cache, not an API handler';
189
- if (fn.runtime && fn.runtime !== 'edge' && !/^nodejs\d+\.x$/.test(fn.runtime)) return `runtime \`${fn.runtime}\` is not JavaScript; only nodejs*.x and edge Lambdas are transmuted`;
190
- if (!fn.sourceFile) return 'no source file for this function';
191
- return null;
192
- }
193
-
194
- /** Non-blocking warnings for the whole deployment. */
195
- export function deploymentWarnings(deployment) {
196
- const out = [];
197
- for (const c of deployment.crons || []) out.push(`cron \`${c.schedule || '?'}\` → ${c.path || '?'}: crons are not supported on chain (nothing schedules an instruction); the route itself may still be served`);
198
- for (const fn of deployment.functions || []) {
199
- if (fn.middleware) out.push(`${fn.name}: middleware is skipped (see report.ineligible)`);
200
- if (fn.prerender) out.push(`${fn.name}: ISR/prerender config is ignored`);
201
- if (fn.maxDuration && fn.maxDuration > 60) out.push(`${fn.name}: maxDuration=${fn.maxDuration}s has no effect; an instruction is bounded by compute units, not seconds`);
202
- }
203
- for (const n of deployment.notes || []) out.push(n);
204
- return out;
205
- }
206
-
207
- /** Display path for a function source relative to the deployment root. */
208
- export function displayFile(fn, root) {
209
- if (!fn?.sourceFile) return fn?.name || '?';
210
- return root ? path.relative(root, fn.sourceFile) || fn.sourceFile : fn.sourceFile;
211
- }