|
| 1 | +## Runtime |
| 2 | + |
| 3 | +Default to Bun instead of Node.js. |
| 4 | + |
| 5 | +- `bun <file>` instead of `node` or `ts-node` |
| 6 | +- `bun test` instead of `jest` or `vitest` |
| 7 | +- `bun install` instead of `npm install` / `yarn install` / `pnpm install` |
| 8 | +- `bun run <script>` instead of `npm run` / `yarn run` / `pnpm run` |
| 9 | +- `bunx <package>` instead of `npx` |
| 10 | +- Bun automatically loads `.env` — don't use dotenv. |
| 11 | +- Prefer `Bun.file` over `node:fs` readFile/writeFile. |
| 12 | +- Use `node:` prefix for built-in modules without a Bun-native alternative (e.g. `node:async_hooks`). |
| 13 | + |
| 14 | +## Project structure |
| 15 | + |
| 16 | +``` |
| 17 | +src/ |
| 18 | + cli/ CLI entry point (commander.js) and commands |
| 19 | + config/ Schema (Zod v4), parser, validator, env interpolation |
| 20 | + api/ HTTP call building and response processing |
| 21 | + abi-encode.ts ABI encoding (0x01 + string[] name-value pairs) |
| 22 | + server.ts Bun.serve HTTP server (routes, CORS, rate limiting) |
| 23 | + pipeline.ts Request processing pipeline (auth → validate → cache → plugins → API call → encode → sign) |
| 24 | + auth.ts Client-facing request authentication (free / apiKey) |
| 25 | + cache.ts In-memory TTL response cache with periodic sweep |
| 26 | + sign.ts EIP-191 response signing and request ID derivation |
| 27 | + identity.ts DNS identity verification (ERC-7529) — public API |
| 28 | + endpoint.ts Endpoint resolution and specification-bound ID derivation |
| 29 | + plugins.ts Plugin loader, hook registry, budget tracking |
| 30 | + logger.ts AsyncLocalStorage context, text/json formats |
| 31 | + types.ts Shared Zod-inferred types |
| 32 | + guards.ts Type guard utilities |
| 33 | + version.ts Version from package.json or build-time define |
| 34 | +examples/ |
| 35 | + configs/ Example YAML configs (must always pass validation) |
| 36 | + plugins/ Example plugins (heartbeat, logger, slack-alerts, encrypted-channel) |
| 37 | +contracts/ |
| 38 | + src/ Vyper 0.4+ contracts (AirnodeVerifier, AirnodeDataFeed) |
| 39 | + test/ Foundry tests (unit, invariant, symbolic) |
| 40 | +book/ Docusaurus documentation site |
| 41 | +``` |
| 42 | + |
| 43 | +Key conventions: |
| 44 | + |
| 45 | +- No catch-all folders like `utils/` or `helpers/`. Place files directly in `src/` with clear names. Group by domain |
| 46 | + only when there are multiple related files (e.g. `src/config/`, `src/api/`). |
| 47 | +- Shared types inferred from Zod schemas live in `src/types.ts`. |
| 48 | +- Example configs in `examples/configs/` must always pass schema validation (tested). Update them when the schema |
| 49 | + changes. |
| 50 | +- Config format is YAML (parsed with `yaml` package). JSON also accepted. |
| 51 | +- In config YAML, the `settings` section goes immediately after `version`, before `apis`. |
| 52 | +- Runtime config is `config.yaml` + `.env` in the working directory (gitignored). |
| 53 | +- **Explicit over implicit**: config fields should be required with no defaults, unless a default is genuinely universal |
| 54 | + (e.g. `method: GET`). Only truly optional behavior (like `rateLimit`, `cors`) uses optional fields. When adding new |
| 55 | + schema fields, default to required. |
| 56 | + |
| 57 | +## Architecture |
| 58 | + |
| 59 | +### HTTP-first signed API server |
| 60 | + |
| 61 | +Airnode is an HTTP server (`Bun.serve`) that receives requests from clients, calls upstream APIs, signs the responses |
| 62 | +with the airnode's private key (EIP-191), and returns the signed data. Clients can then submit the signed responses |
| 63 | +on-chain themselves. There is no chain scanning, no coordinator cycle, no on-chain fulfillment — Airnode is a stateless |
| 64 | +HTTP service. |
| 65 | + |
| 66 | +Routes: |
| 67 | + |
| 68 | +- `POST /endpoints/{endpointId}` — call an endpoint with parameters in the request body |
| 69 | +- `GET /health` — health check with version and airnode address |
| 70 | + |
| 71 | +### Request processing pipeline |
| 72 | + |
| 73 | +The pipeline runs per-request in `src/pipeline.ts`: |
| 74 | + |
| 75 | +1. **Resolve endpoint** → look up endpoint by ID in the endpoint map |
| 76 | +2. **Plugin: onHttpRequest** → plugins can reject requests early |
| 77 | +3. **Authenticate** → verify client credentials (free access or API key via `X-Api-Key` header) |
| 78 | +4. **Validate parameters** → check that all required parameters are present |
| 79 | +5. **Check cache** → return cached response if TTL has not expired |
| 80 | +6. **Plugin: onBeforeApiCall** → plugins can modify parameters |
| 81 | +7. **Call API** → make upstream HTTP request via `src/api/call.ts` |
| 82 | +8. **Plugin: onAfterApiCall** → plugins can modify the response |
| 83 | +9. **Encode** → if endpoint has `encoding`, ABI-encode using type/path/times via `src/api/process.ts` |
| 84 | +10. **Plugin: onBeforeSign** → plugins can modify encoded data before signing |
| 85 | +11. **Sign** → EIP-191 sign `keccak256(requestId || keccak256(data))` via `src/sign.ts` |
| 86 | +12. **Cache** → store response if cache config is present |
| 87 | +13. **Plugin: onResponseSent** → observation hook for logging/monitoring |
| 88 | + |
| 89 | +### Config format |
| 90 | + |
| 91 | +Version `'1.0'`. Top-level sections: `version`, `server`, `settings`, `apis`. |
| 92 | + |
| 93 | +- `server` contains `port`, `host` (default `'0.0.0.0'`), `cors` (optional), `rateLimit` (optional). |
| 94 | +- `settings` contains `timeout` (default 10s), `proof` (`'none'` for Phase 1), `plugins`. |
| 95 | +- `apis[].url` is the upstream API base URL. Upstream credentials go in `apis[].headers`. |
| 96 | +- `apis[].auth` is client-facing: `{ type: 'free' }` or `{ type: 'apiKey', keys: [...] }`. |
| 97 | +- Endpoints use `encoding: { type, path, times? }` instead of `reservedParameters`. Encoding is optional — endpoints |
| 98 | + without it return raw JSON with a signature over the JSON hash. |
| 99 | +- Auth and cache config inherit from API level; endpoint-level overrides take precedence. |
| 100 | + |
| 101 | +### Plugin hooks |
| 102 | + |
| 103 | +Plugins register hooks that fire during request processing. Budgets reset per request. |
| 104 | + |
| 105 | +- `onHttpRequest` — can reject requests early (e.g. IP filtering, custom auth) |
| 106 | +- `onBeforeApiCall` — can modify request parameters before the upstream API call |
| 107 | +- `onAfterApiCall` — can modify the API response before encoding |
| 108 | +- `onBeforeSign` — can modify encoded data before signing |
| 109 | +- `onResponseSent` — observation only (logging, monitoring, heartbeats) |
| 110 | +- `onError` — observation only (error alerting) |
| 111 | + |
| 112 | +## Testing |
| 113 | + |
| 114 | +- `bun test` / `bun run test:unit` — TypeScript unit tests rooted in `src/`. Coverage thresholds: 95% lines, functions, |
| 115 | + and statements (configured in `bunfig.toml`). |
| 116 | +- `bun run test:contracts` — Foundry contract tests (`cd contracts && forge test`). Contracts are independent of the |
| 117 | + TypeScript node. |
| 118 | + |
| 119 | +Test conventions: |
| 120 | + |
| 121 | +- Each source file should have a co-located `.test.ts` file (e.g. `schema.ts` → `schema.test.ts`). |
| 122 | +- Tests must assert exact values, not just shapes. For hex/encoded data, hardcode the expected output and compare with |
| 123 | + `toBe()`. |
| 124 | + |
| 125 | +## Code style |
| 126 | + |
| 127 | +- Always order `scripts` in `package.json` alphabetically. |
| 128 | +- Functions must never exceed 3 levels of nesting, preferably 2 at most. Extract nested logic into named functions. |
| 129 | +- Always use early returns. Never use `else` blocks — invert the condition and return early. |
| 130 | +- Use single quotes. Backticks only when interpolating. |
| 131 | +- Wrap numeric values in `String()` in template literals: `` `Chain ${String(chain.id)}` ``. |
| 132 | +- All interface properties are `readonly`. Arrays use `readonly T[]` or `ReadonlyArray<T>`. Maps use `ReadonlyMap`. |
| 133 | +- No mutations. Use `map`, `filter`, `reduce`, `Object.fromEntries`, spread. When a mutation is necessary (loops, |
| 134 | + `Map.set`), annotate with `// eslint-disable-line functional/immutable-data` or `functional/no-loop-statements`. |
| 135 | +- No try/catch. Use `go()` from `@api3/promise-utils` for async, `goSync()` for sync. Always check `result.success` |
| 136 | + before accessing `result.data`. Early return on failure. |
| 137 | +- Don't use non-null assertions (`!`). Use narrowing or optional chaining. |
| 138 | +- Prefer readability over cleverness. Break complex expressions into named intermediate values. |
| 139 | +- Named exports at the bottom of files with separate `export type { ... }` blocks. |
| 140 | +- After finishing writing code, always run `bun run fmt` to format and fix lint issues. |
| 141 | +- Lint commands: `bun lint` (all), `bun lint:prettier`, `bun lint:eslint`, `bun lint:slither`. |
| 142 | +- ESLint uses `--cache` — don't use `bunx eslint .` directly. |
| 143 | +- Use multilevel section comments to separate logical sections: |
| 144 | + ```ts |
| 145 | + // ============================================================================= |
| 146 | + // Section name |
| 147 | + // ============================================================================= |
| 148 | + const foo = ... |
| 149 | + ``` |
| 150 | + 77 `=` signs at top-level (80 chars total with `// `). 75 when indented. |
| 151 | + |
| 152 | +## Contracts |
| 153 | + |
| 154 | +Vyper 0.4+ contracts in `contracts/src/`, tested with Foundry. EVM target is `prague` (Pectra). See |
| 155 | +`contracts/README.md` for full architecture docs. |
| 156 | + |
| 157 | +- Foundry skips `.vy` files — Vyper is compiled via FFI through `test/VyperDeploy.sol`. |
| 158 | +- Tests inherit from `VyperDeploy` and call `deployVyper("ContractName")` in `setUp()`. |
| 159 | +- snekmate imports (`from snekmate.utils import ...`) resolve via `-p lib/snekmate/src` passed to vyper CLI. |
| 160 | +- macOS has `python3` not `python` — snekmate's VyperDeployer won't work directly, use custom VyperDeploy. |
| 161 | +- Slither needs `--foundry-ignore-compile` after stripping Vyper build info files. |
| 162 | +- Single blank lines between sections (no double blanks). Follow the Vyper style guide. |
| 163 | + |
| 164 | +### Contract architecture |
| 165 | + |
| 166 | +Two contracts: |
| 167 | + |
| 168 | +| Contract | Path | Purpose | |
| 169 | +| -------------------- | --------- | ----------------------------------------------------- | |
| 170 | +| `AirnodeVerifier.vy` | Pull path | Verify signature, prevent replay, forward to callback | |
| 171 | +| `AirnodeDataFeed.vy` | Push path | Verify signature, store `(int224, uint32)` per beacon | |
| 172 | + |
| 173 | +Both use the same signature: `keccak256(encodePacked(endpointId, timestamp, data))` with EIP-191 personal sign. |
| 174 | +Permissionless — anyone can submit. No admin, no registry, no roles. |
| 175 | + |
| 176 | +### Signature format |
| 177 | + |
| 178 | +``` |
| 179 | +hash = keccak256(encodePacked(endpointId, timestamp, data)) |
| 180 | +signature = EIP-191 personal sign over hash |
| 181 | +``` |
| 182 | + |
| 183 | +The endpoint ID is a top-level field so future TLS proof verifiers can inspect it directly. |
| 184 | + |
| 185 | +### Beacon derivation |
| 186 | + |
| 187 | +``` |
| 188 | +beaconId = keccak256(encodePacked(airnode, endpointId)) |
| 189 | +``` |
| 190 | + |
| 191 | +Different airnodes serving the same endpoint produce different beacon IDs but the same endpoint ID. |
| 192 | + |
| 193 | +## Git |
| 194 | + |
| 195 | +Do not add `Co-authored-by` trailers referencing Claude in commit messages. |
| 196 | + |
| 197 | +## Design Context |
| 198 | + |
| 199 | +### Users |
| 200 | + |
| 201 | +API providers who want to serve data on-chain, smart contract developers integrating oracle feeds, and API3 DAO members |
| 202 | +managing infrastructure. They are technical, time-constrained, and value clarity over decoration. The primary interface |
| 203 | +is CLI + documentation — users arrive to get answers and leave. |
| 204 | + |
| 205 | +### Brand Personality |
| 206 | + |
| 207 | +Technical, Trustworthy, Minimal — confidence through precision and simplicity. Part of the API3 ecosystem (api3.org, |
| 208 | +market.api3.org). |
| 209 | + |
| 210 | +### Aesthetic Direction |
| 211 | + |
| 212 | +- **Visual tone**: Minimal and clean. Generous whitespace, content-first, no visual clutter. |
| 213 | +- **References**: api3.org and market.api3.org — the parent brand's visual language. |
| 214 | +- **Theme**: Dark mode primary, light mode optional. Dark backgrounds with high-contrast text. |
| 215 | +- **Colors**: Primary blue `#1843f5` (light) / `#7b9bff` (dark). Dark background `#0a0e2e` / surface `#111648`. CLI |
| 216 | + accent `#f3004b`. Accent yellow `#f3e37a` (from api3.org, available for highlights). |
| 217 | +- **Typography**: System sans-serif stack via Docusaurus/Infima. Code blocks are the primary content type. |
| 218 | + |
| 219 | +### Design Principles |
| 220 | + |
| 221 | +1. **Content over chrome** — every visual element must serve comprehension. No decorative flourishes. |
| 222 | +2. **Developer-native** — design for people who live in terminals and editors. Code examples > prose. |
| 223 | +3. **Quiet confidence** — trustworthiness comes from clarity and consistency, not from bold visuals. |
| 224 | +4. **Reduce to essentials** — if removing an element doesn't hurt understanding, remove it. |
| 225 | +5. **Dark-first** — dark mode is the default experience; optimize contrast and readability there first. |
| 226 | + |
| 227 | +## Documentation (book/) |
| 228 | + |
| 229 | +Docusaurus site in `book/`. Run with `bun run --cwd book start`. |
0 commit comments