Layer 1 (drive-registry) dApp. A custom Photos.sol contract creates and owns a drive per user via
the drive-registry precompile (0x…0902), grants the user a Writer role, and anchors the album-tree root
CID on-chain. Design: DESIGN.md.
Status: M7 — contract + headless flow through
createLibrary, albums, blobs, and the client-computed metadata-root anchor (setRoot), plus a fully interactive UI (React 19 + Vite + Tailwind + PAPI, port 5178). State A creates a library (pick a provider → negotiate →createLibrary, M5); State B browses and manages it: create albums, upload photos with client-generated thumbnails, render a thumbnail grid, and open a photo full-res in a lightbox, recomputing +setRoot-anchoring the metadata root after each mutation (M6). The lightbox now has an in-browser crop/rotate editor that re-PUTs the edited photo to the same path (copy-on-write), regenerates its thumbnail, and re-anchors the root (M7). Next: tests + polish (M8).
contracts/ Photos.sol + vendored IDriveRegistry.sol; build.sh → src/contract/Photos.json
scripts/lib/ TS helpers — PAPI deploy/call, /negotiate, libraryOf read + setRoot (photos.ts),
provider /fs ops (fs-client.ts), byte-exact metadata/data root (merkle.ts)
scripts/ deploy-contract.ts, photos-flow.ts (run via tsx)
src/contract/ photos-abi.ts (vendored ABI the UI uses); Photos.json (bin, generated by build:contract)
src/ React UI — state/ (chain·network·wallet·library·album), lib/ (chain-client,
photos-contract read + write/setRoot, fs-client, merkle, thumbnail),
components/ (CreateLibraryPanel, AlbumBar, UploadButton, PhotoGrid, Lightbox),
pages/Library.tsx (State A create vs State B browse)
Needs solc + resolc on PATH (see examples/contracts/README.md), and a running chain + provider.
cd user-interfaces && pnpm install
# terminals 1 & 2, from repo root:
just start-chain
just start-provider
just demo # register + accept the provider once
# build, deploy, run the headless flow (one namespaced entrypoint):
just photos build # compile Photos.sol → src/contract/Photos.json
just photos deploy # deploy; prints the contract H160
just photos flow # deploy → createLibrary → album + photo → setRoot → verifyjust photos help lists the actions. Each action also has a raw pnpm equivalent, e.g.
pnpm --filter @web3-storage/photos build:contract.
pnpm --filter @web3-storage/photos dev # serves http://127.0.0.1:5178 (or use the run-local-uis skill)The UI needs the deployed contract address: it's read from localStorage['photos.contract'], then
VITE_PHOTOS_CONTRACT, then NetworkConfig.photosContract. Locally, run just photos deploy (or
just photos flow) and paste the printed 0x… into the in-app field (saved to localStorage).
It reads libraryOf unsigned for state detection. State A ("no library") runs the create flow
(map account → negotiate terms → createLibrary{value}). State B ("drive #N") browses the
library: the album bar lists/creates folders, Upload generates a downscaled JPEG thumbnail per
photo (canvas, longest edge ~320px) and PUTs the full photo plus the thumbnail, the grid renders
from thumbnails (kilobytes per cell), and clicking a photo opens it full-resolution in a lightbox.
Every mutation recomputes the drive's metadata Merkle root client-side (@web3-storage/core's merkle.ts) and
anchors it on-chain via setRoot, so the on-chain anchor shown in the header updates live. /fs
requests are unsigned (dev providers run /fs auth disabled; signed requests are M8).
photos-flow asserts the contract owns the drive, the user holds a Writer role on the underlying bucket,
and libraryOf(user) reports the new drive. It then drives the provider's /fs API (mkdir an album,
PUT a multi-MB photo + a thumbnail), computes the drive's metadata Merkle root client-side
(merkle.ts, a byte-exact port of crates/providers/storage/src/index/fs.rs), anchors it via setRoot, and
verifies the locally recomputed root equals both the on-chain anchor and the provider's index_root
(plus a tamper check) — proving the control plane and the integrity anchor before any UI exists.