Canister Studio
A TanStack Start app, rendered on the server, built on @ic-reactor/core,
@ic-reactor/react and @ic-reactor/vite-plugin with TanStack Query’s own
hooks. It gives every method of any canister on mainnet a form built from its
Candid, and calls it with every rule of ic-reactor 4 in view:
- Typed canisters. The ICP ledger, the ckBTC and ckETH ledgers and the
ckBTC minter come from three modules that the Vite plugin generates from
committed
.didfiles withcandid-core-cli gen. The wallet page calls them through the generated types, with no cast. - Any canister, read live. Open any mainnet canister id: a server function
reads its
candid:servicemetadata and module hash in one certifiedread_state, compiles the interface with@candid-core/cliin a worker thread with a deadline, and caches it by module hash. The browser rebuilds the actor from the compiled envelope withschemaFromContract. An interface read from the uncertified__get_candid_interface_tmp_hackquery is labelled so, and a certificate that fails to verify, or leaves the metadata unproven, is never followed by a fallback. - Automatic forms. A field per Candid type: amounts in token units read
with
parseUnits, principals checked as you type, blobs as hex, text or base64, and variants, options, vectors and records, withcreated_at_timeset to now for ICRC deduplication. The submit button names what is wrong, and the issues of aninvalid_argsrefusal land on their fields. - Server rendering. One client per request on the server and one per tab,
made by the router. Loaders prefetch mainnet reads and never throw, the
browser hydrates them without reading again, and a failed read is never
sent to the browser, which reads it itself and gets the full
ReactorError. A shared query link, a method and its arguments as hex Candid, renders its result in the server’s HTML. - Safe writes. Signed out, a write is disabled and says why. Signed in, a
confirm step shows the sender, the target, the method, the Candid text and
the sha256 of the exact argument bytes. A write is sent once and never
retried. A failure shows its
kind, its code and whether it may have executed, and after an unknown outcomeclient.resendOfoffers the same call again, deduplicated bycreated_at_time. - Reads. Certified reads under keys of their own, the live query key with
its caller segment, and the ckBTC minter’s
get_btc_address, an update, read once per caller through{ update: "idempotent" }. - The Safety lab. Ten scenarios that
createTestClient()runs in the browser against a mocked ICRC-1 ledger: an anonymous write refused before sending, arguments that do not encode, a lost reply and its deduplicated re-send, an HTTP 429 the client sends again by itself, canister rejects, a typedErr, a caller that changes, cache isolation and certified reads. Each shows its request log and the error’s fields. - The playground and the interface tools. Paste a
.didand compile it in the browser with the compiler’s lazily loaded WASM; for any canister, get its Candid, its TypeScript module, a projection to the methods you choose, and a check of a written.didagainst the live one.
The guides it shows at work: The client and SSR and hydration (a client per request, prefetch and hydration), Reads, Writes and Errors and mayHaveExecuted (every call), Values and units (the forms and the replies), Auth (Internet Identity), Testing (the Safety lab and the tests) and The Vite plugin (the typed modules).
The tests run the real client on createTestClient() from
@ic-reactor/core/testing, with each canister mocked at its mainnet id and
the interface loader given a fetcher of the test’s own, and never reach
mainnet. The app itself calls mainnet: sign-in is mainnet’s Internet
Identity, and a transfer you confirm moves real tokens.
StackBlitz installs the example from npm and starts its dev server
(npm run dev). The home page renders there on the server with its mainnet
reads, because its loader calls no server function. TanStack Start’s server
functions do not run there: they keep each request in Node’s
AsyncLocalStorage, which StackBlitz does not implement. The typed canisters
need none to load and to call. Any other canister opened from inside the app
is read and compiled in the browser instead, with the compiler’s lazily
loaded WASM, and so is the live side of a pasted .did check. A projection,
the TypeScript module of a canister read live and the check of a typed
canister’s committed .did run on the studio’s server only, and say so
there. Clone the repository to run the server path.