This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Metadata Portal is a self-hosted website that publishes the latest runtime metadata for Substrate-based chains as scannable QR codes for the Parity Signer (air-gapped) signing device. It has two parts that share one repo:
cli/—metadata-cli, a Rust binary with subcommands that maintain the QR-code corpus checked intopublic/qr/and thepublic/data.jsonconsumed by the frontend.src/— A Create-React-App + Tailwind frontend deployed to GitHub Pages (gh-pagesbranch). It is a static site that fetchesdata.jsonandportals.jsonat runtime.
The two halves communicate via files under public/: the CLI writes QR images and data.json; the frontend reads them.
Note: rust/ at the repo root contains only stale target/ directories — the live Rust code is cli/. The top-level Cargo.toml is a workspace with one member: cli.
Frontend (run from repo root):
yarn— install depsyarn start— dev server (requirespublic/data.json; either run the CLI first orcp public/test-file.json public/data.json)yarn build— production buildyarn lint— eslint oversrc/**/*.{ts,tsx}yarn prettier— formatsrc/**/*.{ts,tsx}yarn test— Jest via react-scripts
CLI (Rust, run from repo root via the Makefile so DYLD_FALLBACK_LIBRARY_PATH is set on macOS — direct cargo invocations may fail to link against libclang.dylib):
make updater—cargo run --release update(defaults to--source node; usecargo run --release -- update --source githubfor runtime WASM from GitHub releases)make collector— regeneratespublic/data.jsonfrom QRs inpublic/qr/plus live RPC specsmake signer— interactive: scans a signature QR via webcam (OpenCV) and converts unsigned QRs to signed onesmake verifier— validates all signed QRs against the configured public keymake cleaner— removes obsolete QRs not referenced bydata.jsonmake tests—cargo test --release- Single test:
cargo test --release -p metadata-cli <test_name>(e.g.,test_collector) - Lint:
cargo fmt --all -- --checkandcargo clippy -- -D warnings(CI requires both clean)
Local frontend bootstrap (per README.md): make updater && make collector && yarn start.
Entry point: cli/src/main.rs dispatches clap subcommands from opts.rs to module-per-command handlers:
updater/— adds new unsigned QRs when a chain has a metadata version not yet inpublic/qr/. Two sources:update_from_node(RPC viaqr_reader_pc/generate_message) andupdate_from_github(downloads runtime WASM from GitHub releases configured per-chain). Both callgenerate::generate_metadata_qrand embed provenance viasource.rs::save_source_info(zTXt PNG chunk).signer/— operator workflow: lists unsigned QRs, opens each in a browser, reads a signature QR via OpenCV webcam, then usesgenerate_message::full_runto produce a signed QR, preserving the source zTXt chunk.verifier/— checks every signed QR's signature againstverifier.public_keyfromconfig.toml.collector/export.rs— the source of truth forpublic/data.json: walksqr_dir, picks the right metadata QR per chain (preferring signed; keeping versions ≥ live; one symlink<chain>_metadata_latest.apngper chain pointing at the live version), and serializes anExportDatamap keyed bychain.portal_id().cleaner/— readsdata.jsonviafile::files_to_keepand removes everything else fromqr_dir.deployment_checker/— exits with code 12 when on-disk specs differ from the deployeddata.jsonso theupdate.ymlworkflow can detect "needs redeploy" without needing a non-zero failure semantics.
Cross-cutting:
common/path.rs—QrFileNameparses/produces filenames of the form[unsigned_]<chain>_<metadata_NNN|specs>.<apng|png>. Theunsigned_prefix is the only signal of signed-vs-unsigned.chainhere is theportal_id(relay-prefixed for parachains, e.g.polkadot-statemint).config.rs—AppConfig::loadcanonicalizesdata_file/public_dir/qr_diragainst the config's parent dir so the CLI works from anywhere.Chain.portal_id()is<relay>-<name>for parachains, plainnamefor relay chains;formatted_title()is what the UI shows.fetch.rs—RpcFetcheraccepts therpc_endpointfield as either a string or a list (string_or_vecdeserializer inconfig.rs); falls back through endpoints on failure.source.rs—Source::{Wasm, Rpc}is JSON-serialized into the PNGSourcezTXt chunk so the frontend can show provenance.
src/index.tsx mounts <App> inside BrowserRouter. src/components/App.tsx fetches data.json (chains) and portals.json (links to other portal instances), then drives a single-network detail view (Network) with a sidebar (NetworkSelect/PortalSelect). The current chain syncs to the URL hash. src/scheme.ts defines the TS shape mirroring cli/src/export.rs::ExportData.
Edit config.toml. A new entry needs name, rpc_endpoint, optional title, color, relay_chain, token_unit/token_decimals, and [chains.github_release] (with genesis_hash) if you also want GitHub-release-based updates. After editing, run make updater && make signer && make collector to populate QRs and refresh data.json.
.github/workflows/update.yml runs hourly: it runs update --source node then update --source github, commits to a long-lived sign-me-YYYY-MM-DD branch (creates a draft PR if none exists), and notifies Matrix. A release manager checks out that branch, runs make signer locally with a Parity Signer device, pushes the signed QRs, and the PR is merged. deploy.yml then runs make verifier && make collector and pushes the build to gh-pages. verify.yml runs cargo run --release -- verify on PRs that touch public/qr/** or config.toml.
- macOS: OpenCV via
brew install opencv. TheMakefileexportsDYLD_FALLBACK_LIBRARY_PATHto the Xcode toolchain solibclang.dylibresolves. Prefermake <target>over rawcargofor CLI work locally. - Linux:
libopencv-dev clang libclang-dev(Ubuntu). - The CLI uses Unix
symlinkincollector/export.rs(latest-metadata pointer) — Windows is not supported.