Euler Lite is a single-service Nuxt 4 / Vue 3 / TypeScript frontend for the Euler DeFi
lending protocol (lend, borrow, earn, portfolio, rewards, swaps). There is no separate backend:
the Nitro server layer only serves the SPA shell and a set of /api/internal/* proxy routes.
All Ethereum interaction uses viem + the @eulerxyz/euler-v2-sdk; wallet connection uses
wagmi + Reown AppKit.
Deep-dive docs live in docs/ (see "Reference docs" below) and setup/scripts are in README.md
and package.json. This file captures the non-obvious, durable context that isn't already
obvious from those sources.
Euler Lite is the only service. Standard commands live in README.md ("Available Scripts") and
package.json.
- Use Node 24.22.1, pinned in
.nvmrc, for local and cloud work. Runnvm installandnvm usefrom the repository root before installing dependencies. Non-login shells must also select this version or include itsbindirectory inPATH. CI reads.nvmrc.
- The app reads runtime config from
.env(gitignored). A working dev.envis created during setup by copying.env.exampleand settingNUXT_PUBLIC_APP_URL=http://localhost:3000plus a public RPC:RPC_URL_1=https://ethereum-rpc.publicnode.com. If.envis missing, recreate it the same way — at least oneRPC_URL_<chainId>is required or the chain selector shows no chains..env.exampleselects Euler's published subgraph catalog viaSUBGRAPH_CATALOG_URL. Forks can select a custom catalog or direct endpoints; seedocs/fork-setup.md. NUXT_PUBLIC_APP_KIT_PROJECT_ID(Reown/WalletConnect) is left empty; browsing/read flows work without it, but live wallet connection needs a real project ID.- Enabled chains are derived from
RPC_URL_<chainId>vars at server startup, so restartnpm run devafter editing chain env vars (they are not hot-reloaded).
- Dev server:
npm run dev→ http://localhost:3000 (use a tmux session so it persists). - Vault/market data (Explore page, vault details) loads from the upstream V3 API + the configured
RPC; some individual price/vault upstream calls may log
502/timeout warnings without breaking the page. - Lint:
npm run lint(a few pre-existingno-explicit-anywarnings, 0 errors). - Typecheck:
npm run typecheck. - Tests:
npm run test:run(single pass;npm run testis watch mode). Test runs print lots ofpinowarn/error JSON lines from exercised code paths — this is expected; check the final vitest summary. See "Testing" below for the golden/parity suites and the CI build-before-test gotcha. - A
pre-commithook (simple-git-hooks + lint-staged) runseslint --fixon staged files.
- SPA, not SSR.
nuxt.config.tssetsssr: false. Nitro still runs to serve the HTML shell (with injected config scripts) and the/api/internal/*proxies. Details indocs/architecture.md. - No Pinia. State lives in module-scoped
refs inside composables (e.g.useVaults,useEulerAccount,useTxBatch,useWallets).app.vueeagerly instantiates the key composables at the root so their watchers survive navigation. TanStack Vue Query (plugins/01.query.ts) is used for some async caching, not as global app state. - Config flows env → shared builder → server plugin →
windowglobal → composable:RPC_URL_*,DEPRECATED_CHAINS,ONCHAIN_SDK_CHAINS,EVAULT_FETCH_CHUNK_CHAINS→readChainEnvConfig()inutils/chain-env.ts→server/plugins/chain-config.ts→window.__CHAIN_CONFIG__→useChainConfig(). On every HTML render the plugin layers the cachedEulerChains.jsonstatusandexplorerUrlover the env lists (utils/chain-status.ts):unlistedchains are dropped,stagingchains are dropped whenDOPPLER_ENVIRONMENT=prd,deprecatedchains joindeprecatedChainIds. With no cached manifest the env lists apply unchanged. Note:__CHAIN_CONFIG__carries only chain IDs + validation metadata (enabledChainIds,deprecatedChainIds,onchainSdkChainIds,eVaultFetchChunkChainIds,unsupportedChainIds,chainEnvIssues) and the registryexplorerUrls— theRPC_URL_*values themselves stay server-side. On the server,useChainConfig()calls the same builder directly.- AppKit / V3 / Swap / Pyth URLs + branding →
readAppEnvConfig()inutils/app-config.ts→server/plugins/app-config.ts(overlaysv3ImagesUrl) →window.__APP_CONFIG__→useEnvConfig(). Add new__APP_CONFIG__keys in the shared builder, not by re-parsing env in the plugin. On the server,useEnvConfig()calls the same builder directly. NUXT_PUBLIC_CONFIG_*feature flags/page toggles → NuxtruntimeConfig.public→useDeployConfig()
- Configured upstream secrets stay server-side.
RPC_URL_*, subgraph URLs (SUBGRAPH_URL_*), and the V3 URL are never shipped to the browser; the app's own reads go through same-origin proxies (/api/internal/rpc/{chainId},/api/internal/proxy/subgraph/{chainId},/api/internal/v3), andpythHermesUrlis exposed to the client only as the string'proxy'. This is not a claim that the browser makes zero third-party requests: per-chain viem transports use the network's public RPC (network.rpcUrls.default.http) as a fallback behind the proxy (plugins/00.wagmi.ts), and Reown AppKit + the connected wallet provider make their own outbound calls (AppKit Blockchain API, WalletConnect relay, etc.).
| Directory | Contents |
|---|---|
pages/ |
File-based routes. Key sections: /explore, /lend, /borrow, /earn, /portfolio, /curators/[slug], /position/[number]/* (supply/withdraw/repay/multiply/migrate), /batch. /ui is a dev-only component playground stripped from production builds. |
components/ |
base/, layout/, entities/ (feature components — scanned by Tailwind), ui/ (design-system kit — SCSS, excluded from Tailwind scanning), plus root Batch*.vue. Auto-imported flat (pathPrefix: false), so use LogoBrand, not BaseLogoBrand. |
composables/ |
Business logic (~97 files). Subdirs borrow/, repay/, cowswap/, guards/, position/, useKeyring/, useRpcClient/. Auto-import nuance: only top-level composables/*/index.ts are auto-imported; nested files must be imported explicitly (e.g. ~/composables/repay/useRepaySwapCore). |
utils/ |
Pure helpers. utils/vault/ is the largest (classification, APY, LTV, liquidation). |
entities/ |
App-specific domain types/constants (account.ts, chainRegistry.ts, constants.ts, menu.ts, oracle.ts, migration/cowswap helpers). On-chain types come from the SDK, not here. |
server/ |
Nitro layer: api/, middleware/, plugins/, utils/. |
plugins/ |
Client/Nuxt plugins (numbered for load order): 00.wagmi.ts, 00.chartjs.client.ts, 01.query.ts, theme.client.ts, node.ts (Buffer polyfill). |
middleware/ |
Route middleware (numbered): 01.network.global.ts (normalizes ?network=, applies legacy path rewrites), 02.spy-param.global.ts, ensure-vault.global.ts. |
services/ |
3 thin fetch wrappers over internal API routes (country.ts, screening.ts, vpn.ts). |
abis/ |
viem ABIs (vault.ts, evc.ts, erc20.ts, pyth.ts, keyring.ts, merkl.ts, ...). |
types/ |
Shared TS types + global Window augmentations (types/index.ts). |
assets/ |
styles/ (SCSS + theme vars in variables.scss), tokens/ (icon overrides), chains/, sprite/. |
tests/ |
Vitest suites + golden/, parity/, execution/ (see Testing). |
scripts/ |
Node tooling for parity/execution runs, Doppler sync, preview-SDK install. |
docs/ |
Developer docs (see below). |
- Internal proxies (
server/api/internal/) keep configured upstreams/secrets server-side and are the app's main outbound data path. Representative routes:v3/[...path](allowlisted V3 proxy),rpc/[chainId](JSON-RPC, allowlisted methods),token-list,vaults(pre-computed snapshot),euler-chains(deployment metadata used as the SDKdeploymentsUrl),curator-profiles/{entityId}+curator-networks/{entityId}(V3 entity profile and cross-chain product/Earn counts),proxy/subgraph/[chainId],proxy/merkl|fuul|incentra|turtle/*,proxy/aave+proxy/morpho(external-migration discovery),proxy/intrinsic-apy-overrides,pyth/updates,screen-address,tenderly/*,oracle-*,labels/*,tos. This list is not exhaustive and drifts between branches (e.g. the client-error log endpoint has been removed ondevelopmentto keep browser diagnostics local) — treat theserver/api/internal/directory +docs/architecture.mdas the authoritative inventory, especially when auditing CSP /connect-src. - Public routes (
server/api/public/):is-known,metadata(documented indocs/public-api.md). Note:internal/screen-addressis additionally consumed cross-origin by first-party*.euler.financeSPAs via a path-scoped CORS exception incors.ts— keep its contract backward-compatible. - Server middleware:
geo-gate.ts(451 for sanctioned countries via the country fromgetEdgeContext— trusted-header mapping perEDGE_PROVIDERpreset inutils/edge-presets.ts; setDEV_GEO_COUNTRYlocally since there's no edge header),cors.ts,security-headers.ts,body-limit.ts,ensure-vault.ts. Middleware and routes stay vendor-neutral: edge header names live only in the presets file, consumed throughserver/utils/edge.ts. - Server plugins (load order matters):
app-config.ts/chain-config.tsinject thewindowconfig;csp.ts(nonce-based CSP) must run after them;warm-cache.tswarms labels/token-list/ vault caches in the background. Caching internals are indocs/server-side-caching.md. - Logging: server uses
pinoJSON (server/utils/logger.ts); client/shared uses a console shim (utils/logger.ts). viem errors are sanitized viautils/viem-errors.ts.
Three distinct test layers — most day-to-day work only touches the first.
- Unit / integration (Vitest,
vitest.config.ts). 125+ files undertests/mirroring source dirs. Run withnpm run test:run. Nuxt/Nitro globals and Vue reactivity are stubbed intests/setup.ts;#appis aliased totests/stubs/nuxt-app.ts.- Gotcha:
tests/utils/logger-bundle.test.tsinspects the production client bundle at.output/public/_nuxtand auto-skips when no build exists (this is the "1 skipped" test in a plain run). CI runsnpm run buildbeforenpm run test:runso it executes. Build first if you need to exercise it locally.
- Gotcha:
- Golden canary (
tests/golden/). A version canary (not a correctness suite) that pins the byte-for-byte calldata a user would sign for one representative operation per SDK encoder family (plain vault op, borrow, repay, migration, swap, leverage). Each SDKexecutionService.plan*output is normalized to a canonical[{to, data, value, evcBatch}]tx list (normalize.ts) and asserted against a committed fixture undertests/golden/plans/*.json; its purpose is to catch an SDK version bump that silently changes calldata. It uses committed fixtures — no worktree and no separate vitest config — and runs as part ofnpm run test:run. Scripts:npm run test:golden(golden only),npm run test:golden:update(UPDATE_GOLDEN=1; regenerateplans/*.jsonafter a deliberate builder change, then review the diff), andnpm run test:golden:fetch-fixtures(re-fetch the swap-quote fixtures intests/golden/fixtures/*.jsonvia the SDKSwapService, plus the mainnet deployment addresses they were validated against infixtures/deployment.json; override the endpoint withGOLDEN_SWAP_API_URL). Outside update mode a missing fixture is a hard failure, so the suite can't mint its own baseline. Details intests/golden/README.md. - Parity / execution (Playwright,
scripts/*.mjs+tests/parity/,tests/execution/). Browser-driven diff and fork-based (Anvil) transaction recording. Driven by theparity:*andexecution:*npm scripts using JSON scenario files. The UI exposesdata-id/data-field/data-valueattributes specifically for these scrapers — don't remove them casually.
CI (.github/workflows/ci.yaml) runs three blocking jobs on every PR: lint, typecheck,
and test (which builds first, then test:run). Match these locally before pushing.
Start with docs/README.md (index) and docs/architecture.md. Notable topics:
development-guide.md, sdk-integration.md, transaction-building.md, server-side-caching.md,
pricing-system.md, pyth-oracle-handling.md, portfolio-logic.md,
vault-labels-and-verification.md, token-list.md, geo-blocking.md, tos-signing.md,
keyring-hooks.md, public-api.md, intrinsic-apy.md, reul-unlocks.md.
When touching transaction-building or position flows, verify against the Euler v2 protocol
invariants — do not reinvent them inline. The authoritative sources are docs/transaction-building.md
(EVC batching, sub-accounts, Permit2), docs/pricing-system.md / docs/pyth-oracle-handling.md
(oracle pricing), and the .claude/skills/review-business/SKILL.md skill, which links the
upstream euler-xyz/agent-skills (euler-vaults, euler-earn, euler-advanced, euler-irm-oracles)
plus https://docs.euler.finance. Key invariants those cover (checklist, not a substitute for the docs):
The reviewed execution is exhaustive internally, while the user-facing review remains intentionally handcrafted and non-one-to-one. Read User-facing review compatibility before changing review inputs or rendering. Pyth details remain invisible in review.
- Vault state-changing calls go through the EVC (
abis/evc.ts), not directly to the vault. - At most one controller (borrow) vault enabled per account;
enableController()must precede the borrow, andenableCollateral()must be set before collateral is counted. - Prices resolve through the EulerRouter / cross-adapter oracle config — never a raw price feed.
- Vaults/adapters are factory-deployed; treat non-factory addresses as untrusted.
- Flash loans must be repaid within the same batch;
pullDebtmoves debt to the caller. - EulerEarn respects target allocations, cap timelocks, and ascending PublicAllocator withdrawal
order (
docs/euler-earnskill).
- Vue style:
<script setup lang="ts">only (no Options API);defineProps/defineEmitsuse the TypeScript generic form. Rely on Nuxt auto-imports for Vue APIs and top-level composables. - TypeScript: avoid
any(ESLint warns). Type on-chain amounts asbigint. Note Nuxt setsstrict: false/noUncheckedIndexedAccess: falsein the generated tsconfig, so the compiler won't catch nullable-index access for you. - viem: normalize addresses with
getAddress()before comparing; useparseUnits/formatUnitswith the asset's real decimals (never hardcode 18). - Styling: Tailwind classes for
pages/,layout/,base/,entities/; thecomponents/ui/design kit uses SCSS and is intentionally outside Tailwind'scontentscan (tailwind.config.js). Colors/spacing derive from CSS vars inassets/styles/variables.scss. - Theme: default dark; an inline critical script reads
localStorage.themebefore paint. Chart.js colors are bridged from CSS vars viauseThemeColors. - Local SDK dev:
nuxt.config.tsdetects a symlinked@eulerxyz/euler-v2-sdkand excludes it from ViteoptimizeDeps. Keeppackage.json/package-lock.jsonpinned to the published SDK version unless a PR is intentionally bumping it (see README "Development with the SDK"). - PR review skills:
.claude/skills/{review-business,review-security,review-stack}/SKILL.mddocument the review criteria used for this repo — useful before submitting changes.