Thanks for helping build this. The repo is a pnpm/Turborepo monorepo with two areas:
apps/web + packages/ui (the front end) and apps/contracts (Solidity). Most
changes touch only one; the setup and checks below are split accordingly.
pnpm install
cp apps/web/.env.example apps/web/.env.local
pnpm devpnpm@11 and Node 22 (or 20.19+) — vite-node requires a narrower range than the
engines field. Vite does not read .env.example, so the copy is not optional: without
.env.local the app fail-fasts at startup and names the missing variable.
Everything in .env.example is a public Sepolia address. The only value you may want to
add is VITE_PRIVY_APP_ID; without it every read-only screen still works and the Connect
button explains why it is disabled.
Contract tooling (Hardhat, Foundry, Slither) lives under apps/contracts — see the
"Contracts" section of the root README for its own setup.
Front end (apps/web, packages/ui):
pnpm --filter web... typecheck
pnpm --filter web... lint
pnpm --filter web... build
pnpm --filter web exec vite-node scripts/check-abi.ts # no network
pnpm --filter web exec vite-node scripts/check-units.ts # no networkIf the change touches a contract call, also run the two that hit Sepolia:
pnpm --filter web exec vite-node scripts/verify-onchain.ts
pnpm --filter web exec vite-node scripts/verify-reverts.tsContracts (apps/contracts):
pnpm --filter rwa-contracts-evm build
pnpm --filter rwa-contracts-evm typecheck
pnpm --filter rwa-contracts-evm lint
FOUNDRY_FUZZ_SEED=1 pnpm --filter rwa-contracts-evm testCI runs the same sets, split into ci-web.yml and ci-contracts.yml, each only
triggered by changes under its own area. The on-chain front-end pair is a separate,
non-blocking job — a public RPC rate-limiting should not turn a PR red — so read its
output rather than assuming green.
typecheck, lint and build catch the ordinary things. The extra scripts exist
because this domain fails silently:
check-unitspins the preview arithmetic, rounding direction included. A reimplementation of the vault's fee maths gets the direction wrong without erroring.check-abiasserts the vendored error inventory, including that the merged ABI still carries an error the called contract's own ABI lacks. Without that property, nested reverts decode to nothing and every failure reads as "execution reverted".verify-onchaincompares live Sepolia against a reference snapshot and asserts the metrics batch arrives as a single multicall chunk — the single-block guarantee behindTVL = totalSupply × NAV.verify-revertsreproduces seven reverts byeth_calland asserts each maps to a human sentence. No wallet or gas required.
When one of these disagrees with the chain, that is a fact about the deployment, not a defect in the check. Record the diff and escalate — do not adjust the reference to match.
Read apps/web/src/lib/units.ts before touching anything that carries an amount. The
traps that have already cost time:
- wBOND is 18 decimals, the payment token is 6. The deposit vault charges its fee in token units before converting; the redemption vault charges in wBOND.
minAmountWadmeans different things on the two vaults — payment-token value on one, wBOND on the other. Same getter name, different store.Request.minOutis WAD for a deposit and payment-token units for a redemption, despite the argument being namedoutWad.DataFeed.getPrice()reverts when the price is unhealthy. Batches run withallowFailure: truefor that reason; removing it blanks the whole screen on a stale oracle.
Regenerate the ABIs from apps/contracts rather than editing
apps/web/src/lib/abi/ by hand:
pnpm --filter rwa-contracts-evm build
pnpm --filter web exec vite-node scripts/gen-abi.ts- Addresses live in
apps/web/src/lib/config.tsonly. A CI check greps for any other 0x literal underapps/web/src. - Every
useReadContractspassesbatchSize: 0, in the call parameters and never on the client — on the client it enables a different viem mechanism entirely. - UI primitives come from
packages/ui. Add them withpnpm --filter @workspace/ui exec shadcn add <name>; do not re-implement one locally. - Blocking vs warning. A reason the user cannot fix by typing (no wallet, wrong
network, paused) disables the form. A reason that depends on the amount (below minimum,
over balance) only warns — the input has to stay editable, because typing
24passes through2. - Comments explain why, not what. If a line needs a comment to say what it does, rename something instead.
Open an issue and get a nod first — don't spend real time on a new feature or a non-obvious fix before it's been discussed. This isn't process for its own sake: it's the difference between a PR that merges same-day and one that sits because the approach needed to change. Small fixes and obvious bugs don't need this — use judgment.
- Commits are informative. Not
feat: almost completedorfix: stuff. A commit message should say what changed and, where it's not obvious, why. - Never commit broken code. Each commit is a real, working snapshot. If you need to push work-in-progress to remote as a backup, push it to its own branch — don't land it on the branch others will build on.
- Split commits reasonably. Prefer several focused commits (
git add -pis useful here) over one large commit that bundles unrelated changes. - No dead code, no
old/, noarchive/. Git already keeps full history — commented-out code and "just in case" folders don't need to live in the tree. Delete it; it's recoverable from history if it's ever actually needed. - Merge only when the branch is actually done. Don't merge early just to hand off a
partial fix to someone else — use
git cherry-pick(or just ask) instead of merging an unfinished branch into a shared one. Opening a PR early for visibility or feedback is fine — mark it a draft so it's clear it isn't ready to merge yet; that's a different thing from merging unfinished work. - Refactor for a reason, not for taste. A drive-by rewrite of working code because it "reads better" your way creates a large diff with no behavior change and makes history harder to trace. Refactor when it's needed for the fix or feature at hand, or file it as its own separate PR with a concrete justification (perf, a bug it enables fixing, etc.) — don't bundle it into an unrelated change.
- Before solving something from scratch, check if it's already been solved. If the task is something the industry has already built many times over — wallet connection, KYC integration, a common DeFi primitive — look at how it's usually done first. Re-deriving a well-trodden pattern from zero is rarely the best use of the time.
Before requesting review, check your own change against the task/ticket it's solving and give it a self-review pass — most review cycles that go back and forth repeatedly are cases where this step got skipped. From there: automated checks (lint/typecheck/build/ test) run first, then a human review. Don't merge your own PR unless the process explicitly allows it.
If your change touches wallet connection, signing, token approvals, slippage, contract privileges, or anything that could move funds, see SECURITY.md before opening the PR.