Local development
This page sets up a machine to work on COLLAB: the tools to install, a local GenesisL1-like chain with the demo DAOs, the development server with the production's clean URLs, private sandboxes for tests, and the test wallets, with the pitfalls people meet. It is for developers; nothing here touches GenesisL1 or needs Node.js, which COLLAB does not use anywhere.
What to install
| Tool | Version | Used for |
|---|---|---|
Foundry: forge, cast, anvil | a current release (foundryup, see https://book.getfoundry.sh/) | building and testing the contracts, the local chain, cast reads |
| solc | 0.8.28, pinned in foundry.toml | forge downloads it, or use --offline --use /path/to/solc-0.8.28 |
| Python | 3.10 or later; 3.11.4 or later to build a release (Building a release) | every tool in tools/, server/ and deploy/ |
| Pillow | 10.1 or later (python3-pil) | the prerender's Open Graph images, tools/make_assets.py |
| keccak-256 for Python | pycryptodome 3.20 or later (python3-pycryptodome), or OpenSSL 3.2 or later | the prerender and tools/seo/ |
| Playwright for Python, with Chromium | the end-to-end tests and screenshots |
curl -L https://foundry.paradigm.xyz | bash && foundryup # Foundry, from book.getfoundry.sh
sudo apt install python3 python3-pil python3-pycryptodome # or: python3 -m pip install -r tools/seo/requirements.txt
python3 -m pip install playwright && python3 -m playwright install chromiumThe Solidity libraries ship in lib/ (OpenZeppelin Contracts and Contracts Upgradeable 5.1.0, Solady 0.1.26, forge-std 1.9.4), trimmed in the release archive to what COLLAB compiles, and remappings.txt is the only remapping source. The tools look for Foundry's binaries on PATH, or in FOUNDRY_BIN (a folder holding anvil, cast and forge) when it is set; the end-to-end tests also look in ~/.foundry/bin, where foundryup installs them.
Build the contracts
forge build --skip test --skip script # the contracts alone
forge build test/Governance.t.sol test/Profile.t.sol # then a few test files at a time
forge testA first forge build of everything compiles with via-IR in one solc run, which can need more than 5 GB of memory. Building the contracts first and then a few test files per run keeps it well under 2 GB, and produces the same artifacts; tools/build_release.py does exactly that, four files per run (FORGE_BATCH). On a machine without network access, add --offline --use /path/to/solc-0.8.28 (the release build reads them from FORGE_FLAGS). Testing covers the test suites.
The local chain
tools/local_chain.py starts anvil as a GenesisL1-like chain and deploys everything on it:
python3 tools/local_chain.py up --seed # chain 29 on 127.0.0.1:8545, demo DAOs, web/config.local.js
python3 tools/local_chain.py warp 3600 # move the chain clock forward an hour, and mine a block
python3 tools/local_chain.py up --port 8546 # another port; --solc PATH (or SOLC_PATH) for forge's --useup runs anvil with chain id 29, 12 accounts of 1,000,000 L1 each, a block gas limit of 1,000,000,000 (GenesisL1's), a code size limit of 49,152 bytes and the paris hardfork. It then deploys stand-ins for the GenesisL1 contracts COLLAB integrates with: a wL1-compatible WETH9 (MockWL1 from test/mocks/Mocks.sol, compiled by forge create) and, from the committed bytecode in externals/bin/, two CIPNFT contracts and the GL1F and GL1F Crypto collections. It writes their lists to deployments/integrations.local.json, deploys COLLAB with script/Deploy.s.sol (EXT_WL1, EXT_INTEGRATIONS, and DEPLOY_OUT=deployments/local.json, so a local deployment never overwrites deployments/29.json), and writes web/config.local.js with tools/write_web_config.py. With --seed it runs script/SeedDemo.s.sol in four phases, with the clock moved forward between them, so the demo has proposals in every state. The anvil's process id and log are in deployments/anvil.pid and deployments/anvil.log; stop it by that id when you are done.
The demo data (five DAOs with slugs, logos, descriptions, proposals, tokens, pools, a sealed listing and models) is listed in The web app. The upstream contracts in externals/ are pinned copies of the live GenesisL1 ones, compiled with their own settings (solc 0.8.20); rebuild them only when they change, with SOLC=/path/to/solc-0.8.20 python3 tools/build_externals.py.
Private sandboxes
A sandbox is a private anvil loaded with a snapshot of the seeded chain, plus its own dev server. Several people or test runs can work at once without moving each other's clock or spending each other's balances, and every sandbox starts from the same state, so addresses, DAOs and proposals are identical everywhere.
python3 tools/dev_sandbox.py snapshot # once, from the seeded chain on 8545
python3 tools/dev_sandbox.py up mine --rpc-port 8601 --web-port 8701 # your copy
python3 tools/dev_sandbox.py warp mine 3600 # mine: block 130 at 1793581064
python3 tools/dev_sandbox.py reset mine # back to the snapshot
python3 tools/dev_sandbox.py status # every sandbox, running or stopped
python3 tools/dev_sandbox.py down minesnapshot takes --from URL (default http://127.0.0.1:8545) and writes build/dev/seed-state.hex with seed-state.meta.json (block and time). up starts anvil with the same flags as the local chain, loads the snapshot in one anvil_loadState request, sets the clock just past the snapshot's time, then starts tools/serve_local.py against it, and records the ports and process ids in build/dev/<name>.json (logs beside it). The end-to-end runner uses a sandbox of its own, e2e on ports 8690 and 8790 unless you pass others.
A seeded 2.2 chain is about 2.1 MB of hex, more than anvil's default request limit of 2 MiB, so up starts anvil with --no-request-size-limit; if you load a snapshot into an anvil of your own, pass the flag too. A failed up stops the anvil it started.
The development server
python3 tools/serve_local.py --port 8080 # web/ with the clean paths, against config.local.js's RPC
python3 tools/serve_local.py --port 8080 --rpc http://127.0.0.1:8601
python3 tools/serve_local.py --no-rewrites # a static host without rewrites (404.html and fallback.js)
python3 tools/serve_local.py --build-only # a static copy in build/local-web/It serves web/ directly, so edits show on reload, with the routes of production: /dao/<slug or address>[/…], /votes, /tokens[/<address>], /admin[/…], /swap, /pools, /locks, the docs pages (/docs/<slug> and its .md, once python3 tools/build_docs.py --write has written them) and the other entries, and /deploy answers 301 to /admin/deploy. Unknown paths get 404.html with status 404. It generates /config.js from web/config.local.js with the sandbox's RPC, no explorer and a local price endpoint (/dev/price, a fixed demo price in CoinGecko's format), so nothing leaves the machine, and rewrites each page's connect-src to allow the sandbox and any http://127.0.0.1:* endpoint, to try a custom RPC in Settings. Every response carries the security headers of production and Cache-Control: no-store.
| Option | Effect |
|---|---|
--undeployed | config.js says deployed: false with no addresses: the site before the operator deploys |
--site FILE | a site.json, published in config.js as site (alpha strip, announcement, featured and hidden DAOs, links) |
--data DIR | serves DIR at /data/ (.png, .json and .txt only) and makes this server the configured site (siteUrl is its own origin, so onSite() is true and pages read /data/) |
--prerender DIR | serves DIR before web/ for every path except /docs/… (/x/y tries DIR/x/y/index.html, then DIR/x/y), as nginx serves the prerender's output; also makes it the configured site |
With or without --data, /data/price.json answers a fresh demo price when there is no file, and any other missing /data/ file (and a missing /og/network.json) answers 204, as the server does for a cache file not written yet, so dev and test runs log no 404. To see the prerendered pages and the cache on your machine:
python3 tools/prerender.py --rpc http://127.0.0.1:8601 --deployment deployments/local.json \
--site http://127.0.0.1:8080 --web web --out /tmp/pre --data /tmp/data --lock /tmp/pre.lock
python3 tools/serve_local.py --port 8080 --rpc http://127.0.0.1:8601 --prerender /tmp/pre --data /tmp/dataTest wallets
The local chain's accounts are anvil's development accounts #0 to #11 (the well-known mnemonic test test test test test test test test test test test junk), each with 1,000,000 L1; the demo's USDC address is in deployments/demo.json. Their keys are public knowledge: use them on local chains only, and never send real funds to them.
- In a browser wallet. Add a network with RPC
http://127.0.0.1:8545(or your sandbox's port) and chain id 29, and import one of the development keys; account #0's is0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80. A wallet that already knows chain 29 as GenesisL1 may need the local RPC as a separate network. - In tests.
tools/e2e/harness.pyinjects an EIP-1193 and EIP-6963 test wallet ("Test Wallet", rdnsdev.collab.testwallet) that forwards to the sandbox, where anvil's accounts are unlocked, and marks the cookie notice as seen (cookie_notice=Trueshows it).Browser(base, rpc, account)opens a context;b.go(path)loads a page,b.nav(path)follows an in-app link without a load,b.shot(name)saves a screenshot tobuild/screenshots/, andb.errorscollects console errors and warnings.
python3 tools/e2e/harness.py /daos 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 # one page, a screenshot, its consoleRun on its own, the harness opens http://127.0.0.1:8080 with the wallet on the chain at http://127.0.0.1:8545, so start serve_local.py --port 8080 against that chain first; inside a sandbox, use Browser(base, rpc, account) from Python.
Common pitfalls
- Memory on the first build. One via-IR solc run over every source can need more than 5 GB. Build in batches as above, before the first
local_chain.py uptoo: itsforge createandforge scriptcompile whatever is not built yet. - No snapshot.
dev_sandbox.py upbeforesnapshotstops with "no snapshot: rundev_sandbox.py snapshotagainst a seeded chain first". Take the snapshot from a seeded chain, afterlocal_chain.py up --seed. - The request limit. A snapshot loaded into an anvil without
--no-request-size-limitfails, because it is over 2 MiB. Sandboxes started by an olderdev_sandbox.pymust go down and up;resetreloads the state into the same anvil, with its old flags. - No config.local.js.
serve_local.pybuilds/config.jsfromweb/config.local.js, whichlocal_chain.py upwrites. Without it the server cannot start. - Chain id 29 on two networks. The local chain uses GenesisL1's chain id on purpose, so the app behaves as on mainnet. Keep
deployments/local.jsonandweb/config.local.jsfor local work; neither is ever part of a release. - Time. anvil's clock moves only with blocks. Use
warpto end a voting period or a timelock; the app reads time from the chain (chainNow()), not from your computer. - Ports. Pick your own ports when others run sandboxes on the same machine, and check
dev_sandbox.py statusfirst. Stop what you start, by its process id. - Generated blocks. After changing a page's imports, run
python3 tools/shell_html.py --write; after changingdescifi.html, runpython3 tools/prerender.py --static-pages; after editing the docs,python3 tools/build_docs.py --write, and after adding or removing a docs page,python3 tools/prerender.py --static-sitemap(web/sitemap.xml and robots.txt).python3 tools/lint_web.pyfails on stale shells, copies and docs pages, and on a sitemap that lists other docs pages. - The browser for tests.
tools/e2e/harness.pylaunches Chromium from a fixed path,/opt/pw-browsers/chromium-1194/chrome-linux/chrome; on another machine, changeexecutable_pathin_chromium()to your Playwright Chromium. - Sealed-data tests. The
sealedscenarios drive the reference CIPNFT dapp as the other side of each trade: pointCIPNFT_REFat a checkout of the CIPNFT site. - Clean URLs need a domain root. Every path in the pages is root-absolute, so the site cannot be served from a subfolder.
Related
- Developer overview: the repository and where to start.
- The web app: the rules every page and view follows.
- Testing: forge, the end-to-end suites and the server tests.
- Reading COLLAB from the chain:
castagainst your sandbox. - Data formats: config.js, site.json and the cache files.