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
package/bin/openzoo.js CHANGED
@@ -179,22 +179,6 @@ usage:
179
179
  npx openzoo contexts --forget <hash|all> drop manifest entries
180
180
  npx openzoo balance wallet balance on every rail — Solana (USDC/TOKEN/SOL),
181
181
  Base (USDC/ETH), Robinhood Chain (USDG/memecoins/ETH)
182
- npx openzoo build [dir] transmute a Vercel-shaped app (Next.js pages/api,
183
- app/**/route, Vite + api/*) for Solana. Default
184
- --target shared: bytecode for the shared zoo-vm
185
- runtime, no Rust toolchain, a site is data accounts
186
- (~0.03-0.1 SOL). --target program: own Pinocchio
187
- program per site (--arch v0|v3, ~1 SOL+)
188
- npx openzoo deploy [dir] deploy the site to Solana accounts (--cluster mainnet,
189
- --runtime <zoo-vm id> or OPENZOO_VM_PROGRAM, --yes to
190
- accept the rent estimate; burner wallet)
191
- npx openzoo serve [site] local gateway/explorer for a deployed site (--runtime,
192
- --cluster, --port 4402); /api/* reads are free
193
- simulations, writes are signed by the burner
194
- npx openzoo runtime deploy deploy the shared zoo-vm runtime once per cluster
195
- npx openzoo inspect [dir] print the app in Vercel terms + eligibility report
196
- npx openzoo hub hosted explorer for every transmuted site on the
197
- cluster (what sites.openzoo.fun runs)
198
182
  npx openzoo address print both funding addresses (Solana + EVM)
199
183
  npx openzoo help this text
200
184
 
@@ -433,27 +417,6 @@ async function main() {
433
417
  if (bal <= 0) console.log('buy some with: npx openzoo topup 10');
434
418
  break;
435
419
  }
436
- case 'build':
437
- case 'deploy':
438
- case 'serve':
439
- case 'inspect':
440
- case 'hub':
441
- case 'runtime':
442
- case 'transmute': {
443
- // VERCEL-SHAPED APP -> SOLANA MAINNET. `openzoo build|deploy|serve` hand
444
- // the argv to openzoo-transmute (a sibling npm package): it reads the
445
- // app the way `vercel build` would (pages/api, app/**/route, api/*,
446
- // .vercel/output), transmutes each Lambda into a Pinocchio Rust program
447
- // route, stores the static build in asset accounts, deploys with the
448
- // burner wallet at ~/.openzoo/wallet.json, and `serve` runs the local
449
- // gateway/explorer that maps http://localhost:4402/... onto the program.
450
- const argv = process.argv.slice(2).map((a, i) => (i === 0 && a === 'transmute' ? 'help' : a));
451
- // The transmuter ships inside this package (transmute/): lib/cli.js, the
452
- // compiler, the gateway + hub and the on-chain runtime crate
453
- // (transmute/runtime/zoo-host) all travel with npm — no second install.
454
- await (await import('../transmute/lib/cli.js')).run(argv);
455
- break;
456
- }
457
420
  case 'address':
458
421
  (await import('../lib/info.js')).printAddress();
459
422
  break;
package/lib/config.js CHANGED
@@ -80,6 +80,40 @@ export const EVM_FUNDING_ASSETS = {
80
80
  ],
81
81
  };
82
82
 
83
+ /**
84
+ * RPC endpoint BY CHAIN ID — the only lookup a 402 row can be resolved with.
85
+ *
86
+ * A row names its chain as `eip155:<id>` and nothing else. The rail NAMES
87
+ * above are a coarser bucket: railOf() collapses every chain that is not Base
88
+ * or Robinhood into the single name "evm", so a lookup keyed by rail name
89
+ * cannot tell Polygon from Arbitrum and answers with whatever the fallback
90
+ * branch happens to be.
91
+ *
92
+ * MEASURED 2026-09-09: the payment preflight did exactly that — for any
93
+ * non-Base EVM row it read the balance over the ROBINHOOD RPC, asked a Polygon
94
+ * USDC address that is not a contract there, threw, was swallowed by the
95
+ * preflight's catch, and signed an authorization for a chain the wallet held
96
+ * nothing on. The facilitator then reverted it and the caller was told
97
+ * "execution reverted" while 76 USDC sat on Base. Keyed by chain id, that
98
+ * cannot happen: either we can read the chain, or we say we cannot.
99
+ */
100
+ export const EVM_RPC_BY_CHAIN = {
101
+ 8453: () => config.baseRpcUrl,
102
+ 4663: () => config.rhRpcUrl,
103
+ 137: () => process.env.OPENZOO_POLYGON_RPC || 'https://polygon-rpc.com',
104
+ 42161: () => process.env.OPENZOO_ARBITRUM_RPC || 'https://arb1.arbitrum.io/rpc',
105
+ 10: () => process.env.OPENZOO_OPTIMISM_RPC || 'https://mainnet.optimism.io',
106
+ 480: () => process.env.OPENZOO_WORLD_RPC || 'https://worldchain-mainnet.gateway.tenderly.co',
107
+ };
108
+
109
+ /** RPC for a row's `eip155:<id>` network, or null when we cannot read that chain. */
110
+ export function evmRpcForNetwork(network) {
111
+ const m = /^eip155:(\d+)$/.exec(String(network || ''));
112
+ if (!m) return null;
113
+ const f = EVM_RPC_BY_CHAIN[Number(m[1])];
114
+ return f ? f() : null;
115
+ }
116
+
83
117
  /** RPC endpoint for an EVM rail name. */
84
118
  export function evmRpcFor(rail) {
85
119
  // A rail with no RPC here reports NO balance at all — silently. Every entry
package/lib/pay.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Connection } from '@solana/web3.js';
2
- import { config, fundingLine, evmRpcFor } from './config.js';
2
+ import { config, fundingLine, evmRpcFor, evmRpcForNetwork } from './config.js';
3
3
  import { loadOrCreateWallet } from './wallet.js';
4
4
  import {
5
5
  parse402, orderAccepts, railOf, buildPaymentOnline, tokenBalance,
@@ -123,6 +123,30 @@ const RAIL_MEMO_MS = Number(process.env.OPENZOO_RAIL_MEMO_MS || 15_000);
123
123
  const underfundedUntil = new Map(); // asset -> epoch ms after which to re-try it
124
124
  let lastGoodAsset = null;
125
125
 
126
+ /**
127
+ * One line to stderr about which rail was chosen or skipped. Quiet by default
128
+ * would defeat the point — this is the answer to "why did my deposit not get
129
+ * used", and it is only printed when a rail was actually passed over, so a
130
+ * normal single-rail call stays silent. OPENZOO_QUIET=1 turns it off.
131
+ */
132
+ function railNote(msg) {
133
+ if (process.env.OPENZOO_QUIET === '1') return;
134
+ try { process.stderr.write(`openzoo: ${msg}\n`); } catch { /* stderr closed */ }
135
+ }
136
+
137
+ /** "Base (eip155:8453)" — the name a human knows plus the id a log needs. */
138
+ function railLabel(accept) {
139
+ const n = chainName(accept?.network);
140
+ return n === accept?.network ? String(n) : `${n} (${accept.network})`;
141
+ }
142
+
143
+ /** A chain a human recognises, for funding instructions. */
144
+ const CHAIN_NAMES = { 1: 'Ethereum', 10: 'Optimism', 137: 'Polygon', 480: 'World Chain', 4663: 'Robinhood Chain', 8453: 'Base', 42161: 'Arbitrum' };
145
+ export function chainName(network) {
146
+ const m = /^eip155:(\d+)$/.exec(String(network || ''));
147
+ return (m && CHAIN_NAMES[Number(m[1])]) || network;
148
+ }
149
+
126
150
  const memoKey = (a) => `${a?.network || ''}|${a?.asset || ''}`;
127
151
 
128
152
  /** lastGood first, known-underfunded last (never dropped — only deprioritised). */
@@ -197,8 +221,12 @@ export class PayClient {
197
221
  const b = await tokenBalance(this.connection, this.keypair.publicKey, accept.asset);
198
222
  return BigInt(b.raw || 0);
199
223
  }
224
+ // By chain id, for the reason spelled out in buildPaymentFor: the rail name
225
+ // cannot tell one non-Base EVM chain from another.
226
+ const rpcUrl = evmRpcForNetwork(accept.network) ?? evmRpcFor(rail);
227
+ if (!rpcUrl) return 0n;
200
228
  const raw = await evmTokenBalance({
201
- rpcUrl: evmRpcFor(rail), token: accept.asset, owner: this.evmAddress,
229
+ rpcUrl, token: accept.asset, owner: this.evmAddress,
202
230
  });
203
231
  return BigInt(raw || 0);
204
232
  }
@@ -253,8 +281,26 @@ export class PayClient {
253
281
  // Preflight: check the settlement-asset balance so an unfundable payment
254
282
  // fails HERE with funding instructions instead of at the facilitator.
255
283
  // Advisory only — an unreachable RPC never blocks a payment attempt.
256
- const rpcUrl = rail === 'base' ? config.baseRpcUrl : config.rhRpcUrl;
284
+ //
285
+ // THE RPC IS CHOSEN BY THE ROW'S CHAIN ID, NEVER BY ITS RAIL NAME.
286
+ // railOf() calls every chain that is not Base or Robinhood "evm", so the
287
+ // old `rail === 'base' ? baseRpc : rhRpc` sent a POLYGON row's balance
288
+ // read to Robinhood Chain, where that token address is not a contract.
289
+ // The read threw, the catch below swallowed it, and this signed an
290
+ // authorization for a chain holding nothing — the facilitator reverted
291
+ // it and the caller was told "execution reverted" with a funded Base
292
+ // wallet one row away. MEASURED 2026-09-09.
293
+ const rpcUrl = evmRpcForNetwork(accept.network);
257
294
  const owner = this.evmAddress;
295
+ // A CHAIN WE CANNOT READ IS NOT A CHAIN WE SIGN FOR. Skipping the row
296
+ // costs a caller nothing — the walk moves to the next one and reports
297
+ // honestly if none is payable — while signing it blind spends the call
298
+ // on a revert and teaches the caller nothing about why.
299
+ if (owner && !rpcUrl) {
300
+ throw new UnderfundedError(accept, null, owner, {
301
+ line: `No RPC configured for ${accept.network}, so this wallet's balance there cannot be checked — skipping that rail rather than signing blind. Set OPENZOO_POLYGON_RPC (or the matching chain's RPC) to use it.`,
302
+ });
303
+ }
258
304
  if (owner) {
259
305
  const bal = await evmTokenBalance({ rpcUrl, token: accept.asset, owner }).catch(() => null);
260
306
  if (bal !== null && bal < BigInt(accept.maxAmountRequired)) {
@@ -263,7 +309,7 @@ export class PayClient {
263
309
  // Paxos USDG, which implements EIP-3009 directly, so the only
264
310
  // question left on every EVM rail is whether the wallet holds enough.
265
311
  const sym = accept?.extra?.symbol || 'the quoted asset';
266
- const where = rail === 'robinhood' ? 'Robinhood Chain' : rail === 'base' ? 'Base' : accept.network;
312
+ const where = rail === 'robinhood' ? 'Robinhood Chain' : rail === 'base' ? 'Base' : chainName(accept.network);
267
313
  throw new UnderfundedError(accept, null, owner, {
268
314
  line: await withOnrampLink(
269
315
  `Send a few cents of ${sym} on ${where} to ${owner}.`,
@@ -340,6 +386,7 @@ export class PayClient {
340
386
  }
341
387
  try {
342
388
  payment = await this.buildPaymentFor(cand, onStage, quote);
389
+ if (fundErrs.length) railNote(`paying on ${railLabel(cand)}`);
343
390
  accept = cand;
344
391
  lastGoodAsset = memoKey(cand);
345
392
  underfundedUntil.delete(memoKey(cand));
@@ -352,6 +399,15 @@ export class PayClient {
352
399
  // Remember it so the next call does not re-pay the discovery cost.
353
400
  underfundedUntil.set(memoKey(cand), Date.now() + RAIL_MEMO_MS);
354
401
  fundErrs.push({ sym: cand?.extra?.symbol || cand?.asset, err: e });
402
+ // SAY WHICH RAIL WAS PASSED OVER AND WHY.
403
+ //
404
+ // A caller whose wallet is funded on one chain and empty on five
405
+ // others sees only the LAST row's failure, and the chain that failed
406
+ // is not the chain they funded. Reading that as "my deposit is not
407
+ // seen" is the correct reading of what they were shown — the walk was
408
+ // silent about the five it skipped. One line per skip, on stderr,
409
+ // makes the choice inspectable without a flag or a debug build.
410
+ railNote(`skipping ${railLabel(cand)} — ${e.message.replace(/^openzoo wallet underfunded: /, '')}`);
355
411
  }
356
412
  }
357
413
  if (!payment) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.51.4",
4
- "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental. `openzoo build|deploy|serve|hub|runtime` transmute a Vercel-shaped app (Next.js / Vite + /api) into Solana accounts on the shared zoo-vm Pinocchio runtime (~0.05 SOL a site) or a dedicated program, mainnet.",
3
+ "version": "0.51.5",
4
+ "description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -10,8 +10,7 @@
10
10
  "files": [
11
11
  "bin",
12
12
  "lib",
13
- "README.md",
14
- "transmute"
13
+ "README.md"
15
14
  ],
16
15
  "engines": {
17
16
  "node": ">=18"
@@ -26,9 +25,7 @@
26
25
  "dugite": "^3.2.3",
27
26
  "selfsigned": "^5.5.0",
28
27
  "viem": "^2.21.0",
29
- "zod": "^3.24.0",
30
- "acorn": "^8.14.0",
31
- "acorn-walk": "^8.3.4"
28
+ "zod": "^3.24.0"
32
29
  },
33
30
  "repository": {
34
31
  "type": "git",
@@ -1,385 +0,0 @@
1
- # openzoo-transmute
2
-
3
- Transmute a Vercel-shaped app — Next.js `pages/api`, app-router `route.ts`, or
4
- a Vite repo with `api/*.js` — into **(a)** a Rust [Pinocchio](https://github.com/anza-xyz/pinocchio)
5
- Solana program that hosts the `/api/*` Lambdas as instruction routes and
6
- **(b)** the static build stored in program-derived accounts; deploy it to
7
- Solana; serve it through a local gateway that speaks the same routes Vercel
8
- would.
9
-
10
- ```
11
- vercel build ─► .vercel/output/{config.json, static/, functions/*.func}
12
- │
13
- openzoo build ─► .zoo-out/.zoo/{crate/ (Rust), manifest.json, deploy/<site>.so, static-plan.json}
14
- openzoo deploy ─► program id + one asset account per file + /.zoo/manifest.json
15
- openzoo serve ─► http://127.0.0.1:4402/ (GET = free simulation, POST = signed transaction)
16
- ```
17
-
18
- The whole reverse-engineering — what `vercel build` emits, what the Lambda
19
- receives from `@vercel/node-bridge`, what Fluid Compute changes, the term-by-term
20
- mapping, the limits, the cost model and the security notes — is in
21
- [`docs/VERCEL_TO_SOLANA.md`](docs/VERCEL_TO_SOLANA.md). The patch that mounts
22
- these commands in `npx openzoo` is [`docs/OPENZOO_CLI_PATCH.md`](docs/OPENZOO_CLI_PATCH.md).
23
-
24
- ## How it works
25
-
26
- * **The Lambda contract becomes instruction data.** Vercel's bridge hands a
27
- function `{method, path, headers, body}` and expects `{statusCode, headers,
28
- body}`. `lib/wire.js` and `runtime/zoo-host/src/wire.rs` are that pair as
29
- bytes: `[tag][route][method][path][query][headers][body]` in, `[status][headers][body]`
30
- out via `set_return_data` plus `sol_log_data` chunks.
31
- * **Each handler becomes a Rust route.** The compiler (`lib/compile/`) lowers
32
- the JS subset it accepts onto `zoo_host::{Ctx, Val}` — a JS-semantics dynamic
33
- value with `req.query`/`req.body`/`res.json`/`Response.json`/`kv.*`/`process.env`
34
- implemented in `runtime/zoo-host/src/{ctx,val,json,kv}.rs`. Anything outside
35
- the subset (network, fs, timers, regex, classes…) is reported with `file:line`
36
- and the rest of the app still builds.
37
- * **`static/` becomes asset PDAs.** One rent-exempt account per file, written in
38
- 900-byte chunks, writes gated by the program's upgrade authority
39
- (`runtime/zoo-host/src/assets.rs`). `@vercel/kv` becomes KV PDAs the program
40
- discovers by dry run.
41
- * **Reads are free, writes are transactions.** `GET/HEAD/OPTIONS` run as
42
- `simulateTransaction` (no signature, no fee); everything else is a signed
43
- transaction paid by the gateway wallet. Without a wallet the gateway answers
44
- `402` — the seam where openzoo's x402 flow plugs in.
45
-
46
- ## Install
47
-
48
- ```sh
49
- # from this repository — the package is not on npm yet (`npm view openzoo-transmute` → 404)
50
- cd openzoo-transmute && npm install --no-audit --no-fund
51
- node bin/openzoo-transmute.js help # or: npm link && openzoo-transmute help
52
-
53
- # once published: npx openzoo-transmute help
54
- # through the openzoo CLI once docs/OPENZOO_CLI_PATCH.md is applied:
55
- npx openzoo build | deploy | serve | inspect
56
- ```
57
-
58
- Until it is published, read `npx openzoo-transmute` in the examples below as
59
- `node <repo>/openzoo-transmute/bin/openzoo-transmute.js` (or the `npm link`ed
60
- `openzoo-transmute`).
61
-
62
- Requirements: Node ≥ 18, and for `build`/`deploy` the Solana toolchain
63
- (`cargo build-sbf`): `sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"`.
64
- `build --skip-cargo` works without it (you get the crate and the cost sheet, no `.so`).
65
-
66
- ## Quickstart (shared runtime — the default)
67
-
68
- A site does not need its own program. The `zoo-vm` runtime
69
- (`runtime/zoo-vm`, prebuilt at `prebuilt/zoo_vm.v0.so`) is deployed once per
70
- cluster; every site is then just data accounts under it — a 66-byte site
71
- account, its bytecode module (`/.zoo/code.bin`, ~1–2 KB for a handful of
72
- routes), its static files and its manifest. Rent for a small site is
73
- **≈0.03–0.1 SOL** instead of ≈1 SOL+ for a dedicated program.
74
-
75
- ```sh
76
- cd my-next-app # or a Vite repo with api/*.js, or a .vercel/output
77
- npx openzoo-transmute inspect # the app in Vercel terms + eligibility report
78
- npx openzoo-transmute build # → .zoo-out/.zoo/{code.bin,manifest.json,…}; no cargo needed
79
- npx openzoo-transmute deploy --cluster mainnet --runtime <zoo-vm id> --yes
80
- npx openzoo-transmute serve # site id + runtime read from .zoo/deploy.json
81
- open http://127.0.0.1:4402/ # explorer at /.zoo/, manifest at /.zoo/manifest.json
82
- ```
83
-
84
- The site id is the public key of `.zoo/site-keypair.json` (looks like a
85
- program id, costs nothing; keep it — redeploys update the same site). The hub
86
- serves it at `https://sites.openzoo.fun/s/<siteId>` once its
87
- `OPENZOO_VM_PROGRAM` points at the same runtime.
88
-
89
- Deploying the runtime itself (once, ≈2.2 SOL on mainnet for the 313 KB .so,
90
- upgradeable by the deployer's key):
91
-
92
- ```sh
93
- npx openzoo-transmute runtime deploy --cluster mainnet --yes # keypair → ./zoo-vm-keypair.json
94
- export OPENZOO_VM_PROGRAM=<printed id>
95
- ```
96
-
97
- `--target program` keeps the original path: one Pinocchio program per site,
98
- compiled from the generated crate with `cargo build-sbf`.
99
-
100
- ## Quickstart (dedicated program)
101
-
102
- ```sh
103
- npx openzoo-transmute build --target program # → .zoo-out/.zoo/{crate,manifest.json,deploy/*.so}
104
- npx openzoo-transmute deploy --cluster mainnet --yes
105
- npx openzoo-transmute serve <programId> --cluster mainnet
106
- ```
107
-
108
- `inspect` prints one line per function with its route, style, runtime,
109
- `maxDuration`, `memory` and methods, marks each `✓`/`✗` and says why:
110
-
111
- ```
112
- functions (2):
113
- ✓ /api/hello pages nodejs20.x 10s 1024MB any
114
- pages/api/hello.js env=GREETING
115
- ✓ /api/counter app nodejs20.x 10s 1024MB POST
116
- app/api/counter/route.js env=GREETING
117
- ```
118
-
119
- `deploy` prints the rent sheet before spending anything and refuses mainnet
120
- without `--yes` (this one was measured on a local validator for the two-route
121
- sample in [The generated crate](#the-generated-crate), `.so` built with `--arch v3`):
122
-
123
- ```
124
- item bytes SOL
125
- program account 36 0.0011
126
- program data (2×.so = 416,672 B) 416,717 2.9012
127
- /app.js 52 0.0013
128
- /index.html 44 0.0012
129
- /.zoo/manifest.json (manifest) 939 0.0074
130
- rent total 2.9123
131
- + tx fees (approx.) 0.0012
132
- TOTAL 2.9135
133
- (rent per http://127.0.0.1:8899)
134
- ```
135
-
136
- Then `serve` maps HTTP onto the program:
137
-
138
- ```
139
- GET / → 200 text/html (asset PDA, etag, 304 on If-None-Match)
140
- GET /api/hello?name=zoo&n=21 → 200 {"hello":"zoo","n":42,"greeting":"hi"} x-zoo-simulated: true, x-zoo-cu: 11067
141
- POST /api/counter → 200 {"hits":1} x-zoo-signature: <tx>, x-zoo-simulated: false, x-zoo-cu: 10634
142
- POST /api/counter → 402 payment required (gateway started without a keypair)
143
- ```
144
-
145
- ## CLI reference
146
-
147
- | command | what it does |
148
- |---|---|
149
- | `build [dir] [--target shared\|program] [--runtime <id>] [--out .zoo-out] [--name <crate>] [--arch v0\|v3] [--cluster <c>] [--skip-cargo] [--json]` | `--target shared` (default): transmute to a `ZOOB` bytecode module at `<out>/.zoo/code.bin` for the shared runtime, no cargo, site-sized cost sheet. `--target program`: read the app (`lib/vercel.js`), transmute (`lib/compile/`), write `<out>/.zoo/{crate/, manifest.json, report.json, static-plan.json, build.json}`, run `cargo build-sbf --arch <arch>` → `.zoo/deploy/<name>.so`, print the cost sheet (`--cluster` only chooses the RPC whose rent parameters price it, default `localnet`; unreachable → the 6.96 SOL/MB rule). Default arch is `v0` (mainnet); `deploy` re-detects the cluster's SBPF version and rebuilds if needed. Exit code 2 when nothing was eligible |
150
- | `deploy [dir\|outDir] [--cluster mainnet\|devnet\|testnet\|localnet\|<url>] [--keypair <path>] [--yes] [--runtime <id>] [--program <id>] [--concurrency 4] [--skip-assets] [--force] [--json]` | shared build: create the site account under `--runtime` (or `OPENZOO_VM_PROGRAM`), upload `code.bin`, assets and manifest as PDAs namespaced by the site id (keypair at `.zoo/site-keypair.json`). Program build: deploy or upgrade the program (keypair kept at `.zoo/program-keypair.json`, so redeploys upgrade in place), upload every static file (unchanged ones skipped by comparing bytes on chain), write `/.zoo/manifest.json`, record `.zoo/deploy.json` |
151
- | `serve [siteId\|programId] [--runtime <id>] [--cluster <c>] [--port 4402] [--host 127.0.0.1] [--keypair <path>\|--no-keypair] [--quiet]` | the local gateway; with `--runtime` the positional is a site id on the shared runtime. It defaults to the `.zoo/deploy.json` under the current directory (or its `.zoo-out/`). Reads are simulated, writes are signed by the wallet; without a wallet (or with `--no-keypair`) writes answer 402. Without `--cluster`/`OPENZOO_CLUSTER` this command assumes `localnet`, unlike the others |
152
- | `inspect [dir] [--json]` | the Vercel model (functions, static files, routes, crons) plus the eligibility report |
153
- | `status <siteId\|programId> [--runtime <id>] [--cluster <c>] [--json]` | program account, authority, `maxDataLen`, deploy slot, and the on-chain manifest; with `--runtime`, the site account and its manifest |
154
- | `runtime deploy [--cluster <c>] [--keypair <path>] [--yes] [--program-keypair zoo-vm-keypair.json] [--so <path>]` | deploy (or upgrade in place) the shared `zoo-vm` runtime: `prebuilt/zoo_vm.<arch>.so`, or built from `runtime/zoo-vm` when no prebuilt matches the cluster's SBPF version |
155
- | `runtime status <id> [--cluster <c>]` | the runtime program's account |
156
- | `help`, `--version` | |
157
-
158
- Environment: `OPENZOO_VM_PROGRAM` (shared runtime id; default for `--runtime`), `OPENZOO_TARGET` (`shared`/`program`), `OPENZOO_CLUSTER` (default `mainnet` for `deploy`/`status`, `localnet` for `serve`), `OPENZOO_RPC` (mainnet RPC
159
- URL), `OPENZOO_KEYPAIR` / `OPENZOO_WALLET` (signer path), `OPENZOO_DEBUG=1`
160
- (stack traces). Signer discovery order (`lib/wallet.js`): `--keypair` →
161
- `OPENZOO_KEYPAIR` → `OPENZOO_WALLET` or `~/.openzoo/wallet.json` (the openzoo
162
- burner, `{solana:[64 bytes]}` or a bare `solana-keygen` array) → `~/.config/solana/id.json`.
163
-
164
- Gateway responses carry `x-zoo-program`, `x-zoo-route`, `x-zoo-simulated`,
165
- `x-zoo-cu`, `x-zoo-signature` (writes) and `x-zoo-asset` (static); route params
166
- reach the handler as `x-zoo-param-<name>` headers. `413` above 900 body bytes,
167
- `405` when an app-router file does not export the method, `502` with the last
168
- program log lines when the instruction fails, `404` JSON (or the app's
169
- `handle: error` route) otherwise.
170
-
171
- ## The generated crate
172
-
173
- `build` writes a crate that depends on the runtime by path. For this two-route
174
- sample —
175
-
176
- ```js
177
- // pages/api/hello.js
178
- export default function handler(req, res) {
179
- res.status(200).json({ hello: req.query.name || 'world', n: Number(req.query.n) * 2, greeting: process.env.GREETING })
180
- }
181
- // app/api/counter/route.js
182
- import { kv } from '@vercel/kv'
183
- export async function POST() { const n = await kv.incr('hits'); return Response.json({ hits: n }) }
184
- ```
185
-
186
- — with `GREETING=hi` in `.env.production`, the compiler (`lib/compile/index.js`,
187
- `transmute()`) emits exactly this; only the ~240-line prelude of helper
188
- functions between the entrypoint and the routes is elided:
189
-
190
- ```toml
191
- # .zoo-out/.zoo/crate/Cargo.toml (crate name = the app directory, underscored)
192
- [package]
193
- name = "my_site"
194
- version = "0.1.0"
195
- edition = "2021"
196
- description = "Generated by openzoo-transmute: Vercel Lambdas re-hosted as a Solana program"
197
- publish = false
198
-
199
- [lib]
200
- crate-type = ["cdylib", "lib"]
201
-
202
- [dependencies]
203
- pinocchio = "0.11.2"
204
- libm = "0.2"
205
- zoo-host = { path = "<openzoo-transmute>/runtime/zoo-host" }
206
-
207
- [profile.release]
208
- overflow-checks = false
209
- lto = "fat"
210
- codegen-units = 1
211
- opt-level = 3
212
- ```
213
-
214
- ```rust
215
- // .zoo-out/.zoo/crate/src/lib.rs
216
- //! Generated by openzoo-transmute — do not edit.
217
- //! nextjs (nextjs) — 2 route(s), 2 static file(s)
218
- //! route 0: /api/hello ← api/hello
219
- //! route 1: /api/counter [POST] ← api/counter
220
- #![no_std]
221
- #![allow(unused_mut, unused_variables, unused_assignments, dead_code, unused_parens, unreachable_code, unused_braces, unused_labels, unused_imports, non_snake_case, non_upper_case_globals, unused_unsafe, clippy::all)]
222
- extern crate alloc;
223
- use alloc::{format, string::String, vec::Vec};
224
- use pinocchio::{AccountView, Address, ProgramResult};
225
- use zoo_host::{Ctx, Route, Val};
226
- use zoo_host::val as zv;
227
- use zoo_host::json as zjson;
228
-
229
- pinocchio::program_entrypoint!(process_instruction);
230
- pinocchio::default_allocator!();
231
- pinocchio::nostd_panic_handler!();
232
-
233
- /// The Lambda environment table, baked at build time (public on chain).
234
- const ENV: &[(&str, &str)] = &[("GREETING", "hi")];
235
- /// Route table: instruction byte 1 indexes it.
236
- const ROUTES: &[Route] = &[route_0, route_1];
237
-
238
- pub fn process_instruction(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) -> ProgramResult {
239
- zoo_host::dispatch(program_id, accounts, data, ROUTES, ENV)
240
- }
241
-
242
- // … prelude: __zoo_query (req.query + route params), __zoo_resp / __zoo_send (Response objects),
243
- // __zoo_cookies, __zoo_num, __zoo_set_headers, … …
244
-
245
- // ---- route 0: api/hello (pages/api/hello.js)
246
- const PARAMS_0: &[(&str, bool)] = &[];
247
- fn route_0(cx: &mut Ctx) -> Result<(), Val> {
248
- let _ = { let __a1 = Val::Num(200.0); cx.res_status(&__a1); Val::Undef };
249
- let _ = { let __a2 = { let mut __o3 = Val::obj(); __o3.set_str("hello", { let __x4 = (__zoo_query(cx, PARAMS_0)).get_str("name"); if !__x4.truthy() { Val::str("world") } else { __x4 } }); __o3.set_str("n", ({ let __a: Vec<Val> = alloc::vec![(__zoo_query(cx, PARAMS_0)).get_str("n")]; zv::global_call("Number", &__a)? }).mul(&(Val::Num(2.0)))); __o3.set_str("greeting", { let __a5 = Val::str("GREETING"); cx.env(&__a5) }); __o3 }; cx.res_json(&__a2); Val::Undef };
250
- Ok(())
251
- }
252
-
253
- // ---- route 1: api/counter (app/api/counter/route.js)
254
- const PARAMS_1: &[(&str, bool)] = &[];
255
- fn route_1(cx: &mut Ctx) -> Result<(), Val> {
256
- let __m = cx.req_method();
257
- match __m.as_str() {
258
- Some("POST") => route_1_post(cx),
259
- Some("OPTIONS") => { cx.res_status(&Val::Num(204.0)); cx.res_header(&Val::str("allow"), &Val::str("POST, OPTIONS")); cx.res_end(&Val::Undef); Ok(()) }
260
- _ => { cx.res_status(&Val::Num(405.0)); cx.res_header(&Val::str("allow"), &Val::str("POST, OPTIONS")); cx.res_end(&Val::Undef); Ok(()) }
261
- }
262
- }
263
- fn route_1_post(cx: &mut Ctx) -> Result<(), Val> {
264
- let mut v_n: Val = { let __a6 = Val::str("hits"); let __a7 = Val::Num(1.0); cx.kv_incrby(&__a6, &__a7)? };
265
- { let __rv = { let __b = { let mut __o8 = Val::obj(); __o8.set_str("hits", v_n.clone()); __o8 }; let __i = Val::Undef; __zoo_resp("json", __b, __i) }; __zoo_send(cx, &__rv)?; }
266
- return Ok(());
267
- Ok(())
268
- }
269
- ```
270
-
271
- `Route = fn(&mut Ctx) -> Result<(), Val>`; `Err(v)` is a thrown JS value and
272
- answers 500 like a crashed Lambda; returning without responding answers 504 like
273
- a Lambda that never ended its response. The `?` after `kv_incrby` is the KV
274
- account-discovery path: a missing account is logged as `ZOOK` and the gateway
275
- retries with it. Built with `--arch v3` this crate is a 208,336-byte `.so`
276
- (a 416,717-byte program data account, 2.90 SOL of rent); on a local validator
277
- `GET /api/hello?name=zoo&n=21` costs 11,067 CU and `POST /api/counter`
278
- 10,634 CU — nowhere near the 400 k limit.
279
-
280
- ## Tests
281
-
282
- ```sh
283
- cd openzoo-transmute
284
- npm install --no-audit --no-fund
285
- npm test # node --test test/*.test.js
286
- ```
287
-
288
- The unit tests need no chain. The end-to-end test in `test/gateway.test.js`
289
- deploys a pre-built site program and drives the gateway against a **local
290
- validator**; it skips itself with a reason when either is missing:
291
-
292
- ```sh
293
- # 1. toolchain (once)
294
- sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
295
- export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"
296
-
297
- # 2. a validator with a funded wallet (keep it running in another shell)
298
- solana-test-validator --reset
299
- solana airdrop 100 --url http://127.0.0.1:8899 # ~/.config/solana/id.json
300
-
301
- # 3. a site program with route 0 = /api/hello (pages style) and route 1 =
302
- # /api/counter (POST, kv key "hits") — e.g. the two-route sample below
303
- # (test validator = SBPF v3; mainnet = v0):
304
- openzoo-transmute build <sample> --arch v3 # → <sample>/.zoo-out/.zoo/deploy/<name>.so
305
- # then point the SAMPLE_SO constant in test/gateway.test.js at that .so
306
- # (it is a fixed path today; no sample crate ships with the package yet)
307
-
308
- # 4. run
309
- OPENZOO_TEST_RPC=http://127.0.0.1:8899 npm test
310
- ```
311
-
312
- `test/gateway.test.js` looks for the `.so` at `SAMPLE_SO` and a validator at
313
- `OPENZOO_TEST_RPC` (default `http://127.0.0.1:8899`); `test/build.test.js`
314
- probes `localnet` for real rent numbers and falls back to the 6.96 SOL/MB rule.
315
- The runtime crate also builds on the host (`cd runtime/zoo-host && cargo build`;
316
- its `Cargo.toml` pulls the sha2/curve25519 fallbacks for non-SBF targets), and
317
- for the chain: `cargo build-sbf --arch v3` (test validator) or `--arch v0` (mainnet).
318
-
319
- ## Repository layout
320
-
321
- ```
322
- bin/openzoo-transmute.js the executable (→ lib/cli.js run())
323
- lib/cli.js build · deploy · serve · inspect · status
324
- lib/vercel.js the Vercel deployment model (Build Output API v3 reader / synthesizer)
325
- lib/eligibility.js what can run on chain, with reasons
326
- lib/compile/ JS → Rust: parse.js (acorn + TS stripping) → ir.js (lowering) → rust.js (printer, crate skeleton, prelude)
327
- lib/build.js crate + manifest + cargo build-sbf + cost sheet
328
- lib/deploy.js deploy / upgrade + assets + manifest
329
- lib/gateway.js the local HTTP front (routes, static, 402/413 seams, explorer)
330
- lib/solana.js loader (deploy/upgrade in pure JS), assets, KV, invoke + discovery
331
- lib/wire.js the bridge contract as bytes, PDAs
332
- lib/wallet.js signer discovery, cluster URLs
333
- runtime/zoo-host/ the no_std pinocchio runtime the generated crate links
334
- docs/VERCEL_TO_SOLANA.md the reverse-engineering document
335
- docs/OPENZOO_CLI_PATCH.md mounting build/deploy/serve in npx openzoo
336
- ```
337
-
338
- ## Limits at a glance
339
-
340
- | | |
341
- |---|---|
342
- | request body | 900 B (one transaction is 1232 B) |
343
- | response | ≤ 1024 B in return data, ≈ 7 KB through log chunks |
344
- | KV value | 10 000 B JSON per key |
345
- | compute | 400 k CU requested, 1.4 M max |
346
- | clock | seconds |
347
- | rent | 6.96 SOL per MB of static + program bytes, 2 × the `.so` reserved |
348
- | not supported | network, fs, timers, randomness, regex, classes, streaming, `waitUntil`, ISR, crons, middleware, local imports |
349
-
350
- ## The hosted explorer (`hub`)
351
-
352
- `serve` fronts one program on localhost. `hub` fronts every program on a
353
- cluster from one public host, read-only:
354
-
355
- ```
356
- openzoo-transmute hub --cluster mainnet --port 8080 # or: fly deploy (Dockerfile + fly.toml here)
357
- https://<host>/s/<programId> pins that site (cookie) and opens its /
358
- https://<host>/s/<programId>/api/x one page of it, absolute
359
- https://<host>/.hub paste a program id; recently served sites
360
- ```
361
-
362
- Reads are free simulations, exactly as in `serve`. Writes answer 402 on a
363
- public host (a shared signer would be drained); use `serve` with your wallet
364
- locally, or the x402-paid lane once the openzoo proxy fronts it. The site's
365
- own root-relative links keep working because the pinned program rides a
366
- cookie, so a transmuted app needs no base-path changes.
367
-
368
- ## Roadmap
369
-
370
- * **Browser fork / `sol://` scheme** — a browser that resolves
371
- `sol://<programId>/path` straight from the RPC (assets from PDAs, `/api/*`
372
- through `simulateTransaction`), so a site needs no gateway and no domain.
373
- * **x402-paid writes through the openzoo proxy** — the gateway already answers
374
- `402` with an `x402` stub for mutating requests when it has no signer; next is
375
- letting the openzoo proxy pay per request from the burner wallet so a public
376
- gateway never holds keys.
377
- * **Chunked request bodies** — bodies > 900 B staged into a request account over
378
- several transactions (the same mechanism assets already use), then referenced
379
- by the invoke.
380
- * **Response accounts** — responses > ~7 KB written into a per-request account
381
- and read back by the gateway, instead of the log buffer.
382
- * Compiler coverage: local imports, `RegExp`, base64 (`atob`/`btoa`), `cookies()`,
383
- `URLSearchParams`, more of `@vercel/kv` (`hget`, lists, `expire`).
384
-
385
- License: MIT.
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- // `openzoo-transmute build|deploy|serve|inspect|status|help` — see lib/cli.js.
3
- import { run } from '../lib/cli.js';
4
-
5
- const code = await run(process.argv.slice(2));
6
- // `serve` returns 0 while its server keeps the loop alive; only bail on failure.
7
- if (code) process.exit(code);