Skip to content

Commit 809ddaf

Browse files
committed
Airnode v2: HTTP-first signed API server
A complete rewrite of Airnode as an HTTP server that signs API responses. The airnode never touches the chain — clients receive signed data and submit it on-chain themselves. Two delivery paths: - Pull: POST /endpoints/{endpointId} → call API → sign → respond - Push: background loop → call API → sign → store → GET /beacons Two Vyper contracts: - AirnodeVerifier: verify signature, prevent replay, forward callback - AirnodeDataFeed: verify signature, store (int224, uint32) per beacon Signature format: keccak256(encodePacked(endpointId, timestamp, data)) with EIP-191 personal sign. Endpoint IDs are specification-bound hashes of the full API spec (URL, path, method, params, encoding).
0 parents  commit 809ddaf

3,667 files changed

Lines changed: 742931 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-docs.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Deploy Docs
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths: [book/**]
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
pages: write
12+
id-token: write
13+
14+
concurrency:
15+
group: pages
16+
cancel-in-progress: false
17+
18+
jobs:
19+
build:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- uses: oven-sh/setup-bun@v2
24+
- run: bun install --cwd book
25+
- run: bun run --cwd book build
26+
- uses: actions/upload-pages-artifact@v3
27+
with:
28+
path: book/build
29+
30+
deploy:
31+
needs: build
32+
runs-on: ubuntu-latest
33+
environment:
34+
name: github-pages
35+
url: ${{ steps.deployment.outputs.page_url }}
36+
steps:
37+
- id: deployment
38+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# dependencies (bun install)
2+
node_modules
3+
4+
# output
5+
out
6+
dist
7+
*.tgz
8+
9+
# temporary files
10+
tmp/
11+
12+
# code coverage
13+
coverage
14+
*.lcov
15+
16+
# logs
17+
logs
18+
_.log
19+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20+
21+
# runtime config (contains secrets)
22+
/config/
23+
/config.yaml
24+
25+
# dotenv environment variable files
26+
.env
27+
.env.development.local
28+
.env.test.local
29+
.env.production.local
30+
.env.local
31+
32+
# caches
33+
.eslintcache
34+
.cache
35+
*.tsbuildinfo
36+
contracts/cache/
37+
contracts/broadcast/
38+
39+
# docusaurus
40+
book/build/
41+
book/.docusaurus/
42+
43+
# IntelliJ based IDEs
44+
.idea
45+
46+
# Finder (MacOS) folder config
47+
.DS_Store

.gitmodules

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[submodule "contracts/lib/snekmate"]
2+
path = contracts/lib/snekmate
3+
url = https://github.com/pcaversaccio/snekmate
4+
ignore = dirty
5+
[submodule "contracts/lib/account-abstraction"]
6+
path = contracts/lib/account-abstraction
7+
url = https://github.com/eth-infinitism/account-abstraction
8+
ignore = dirty

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
contracts/lib

.prettierrc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"bracketSpacing": true,
3+
"printWidth": 120,
4+
"singleQuote": true,
5+
"tabWidth": 2,
6+
"trailingComma": "es5",
7+
"useTabs": false,
8+
"plugins": ["prettier-plugin-solidity"],
9+
"overrides": [
10+
{
11+
"files": "*.md",
12+
"options": {
13+
"parser": "markdown",
14+
"proseWrap": "always"
15+
}
16+
}
17+
]
18+
}

CLAUDE.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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

Comments
 (0)