|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +DefiLlama Bridges Server — aggregates cross-chain bridge transaction data from 100+ protocols. Fetches on-chain events via adapters, stores in PostgreSQL, aggregates into hourly/daily volumes, and exposes a REST API via Fastify. |
| 8 | + |
| 9 | +## Commands |
| 10 | + |
| 11 | +- **Type check:** `npm run ts` (runs `tsc --noEmit`) |
| 12 | +- **Build:** `npm run build` (Vite → `dist/`, CJS output) |
| 13 | +- **Dev server:** `npm run dev` (requires `.env` file; tsx watch on port 3000) |
| 14 | +- **Production server:** `npm run start` (runs `dist/index.js`) |
| 15 | +- **Cron worker:** `npm run start:cron` (runs `dist/startCron.js`) |
| 16 | +- **Test adapter:** `npm run test <bridgeName> [numBlocks]` — runs adapter against recent blocks |
| 17 | +- **Test historical:** `npm run test-txs <startTs> <endTs> <adapter>` — backfill test |
| 18 | +- **Backfill adapter:** `npm run adapter <startTs> <endTs> <bridgeName> [chain]` (requires `.env`) |
| 19 | +- **Aggregate:** `npm run aggregate <startTs> <endTs> <bridgeName>` (requires `.env`) |
| 20 | +- **Formatting:** Prettier with `printWidth: 120`, `tabWidth: 2` |
| 21 | + |
| 22 | +## Architecture |
| 23 | + |
| 24 | +### Data Pipeline Overview |
| 25 | + |
| 26 | +The system follows a three-stage pipeline: **Fetch → Store → Aggregate**. |
| 27 | + |
| 28 | +1. **Fetch**: Adapters pull raw bridge transaction events from on-chain logs or APIs |
| 29 | +2. **Store**: Raw transactions are inserted into `bridges.transactions` with block timestamps |
| 30 | +3. **Aggregate**: Raw txs are priced via DefiLlama's price API (`coins.llama.fi`), then rolled up into hourly and daily volume tables |
| 31 | + |
| 32 | +### Adapter System (`src/adapters/`) |
| 33 | + |
| 34 | +Each bridge protocol has a directory under `src/adapters/` exporting a default `BridgeAdapter`: |
| 35 | +```ts |
| 36 | +{ [chainName: string]: (fromBlock: number, toBlock: number) => Promise<EventData[]> } |
| 37 | +``` |
| 38 | + |
| 39 | +Two main patterns: |
| 40 | +1. **EVM event log pattern** (most adapters) — define `ContractEventParams`/`PartialContractEventParams` arrays, pass to `getTxDataFromEVMEventLogs()` from `src/helpers/processTransactions.ts`. Each params object specifies a `target` contract address, `topic` (event signature), `abi`, and key mappings (`logKeys`, `argKeys`, `txKeys`, `fixedEventData`) that map event fields to `EventData` fields. See `src/adapters/celer/index.ts` for a representative example. |
| 41 | +2. **API-based pattern** — query bridge indexer APIs directly, construct `EventData` objects manually (e.g., `src/adapters/across/`) |
| 42 | + |
| 43 | +Some adapters use `AsyncBridgeAdapter` (with `isAsync: true` and a `build()` method) when they need async initialization before returning the chain function map. |
| 44 | + |
| 45 | +Adapters are registered in `src/adapters/index.ts` keyed by `bridgeDbName`. |
| 46 | + |
| 47 | +### Adapter Execution Flow (`src/utils/adapter.ts`) |
| 48 | + |
| 49 | +**`runAdapterToCurrentBlock(bridgeNetwork)`** — the main entry point for running a single adapter: |
| 50 | + |
| 51 | +1. Resolves the adapter (handling async adapters via `build()`) |
| 52 | +2. Ensures `bridges.config` entries exist for each chain the adapter supports |
| 53 | +3. For each chain in parallel (staggered by 200ms): |
| 54 | + - Looks up the `bridgeID` from the config table |
| 55 | + - Calls `getBlocksForRunningAdapter()` to determine the block range: |
| 56 | + - Gets the latest on-chain block number |
| 57 | + - Finds the last recorded block from DB (`lastRecordedBlocks` query) or Redis progress cache |
| 58 | + - Redis progress (`adapter_progress:{name}:{chain}`, 7-day TTL) tracks the last successfully processed block, allowing resumption after crashes |
| 59 | + - `startBlock = lastRecordedEndBlock + 1` |
| 60 | + - Processes blocks in chunks of `maxBlocksToQueryByChain[chain]` (defined in `src/utils/constants.ts`, ~1.5-2 hours of blocks per chain) |
| 61 | + - Each chunk calls `runAdapterHistorical()` |
| 62 | + |
| 63 | +**`runAdapterHistorical(startBlock, endBlock, ...)`** — processes a block range: |
| 64 | + |
| 65 | +1. Checks Redis progress to skip already-processed ranges |
| 66 | +2. Fetches event logs by calling the adapter's chain function (with `async-retry`, 4 retries) |
| 67 | +3. Estimates timestamps by sampling 10 blocks across the range from the provider (Solana uses `getBlockTime` per tx instead) |
| 68 | +4. Filters out tx groups with 100+ events per txHash |
| 69 | +5. Inserts each event into `bridges.transactions` within a SQL transaction (with retries) |
| 70 | +6. Updates Redis progress on success |
| 71 | +7. On failure after 3 attempts, logs error to DB and sends Discord notification |
| 72 | + |
| 73 | +**Skipped bridges**: Certain high-volume bridges (`bridgesToSkip` in `adapter.ts`: wormhole, layerzero, hyperlane, mayan, relay, cashmere, teleswap, intersoon, ccip) are excluded from the general `runAllAdapters` job and run via dedicated handlers with separate timeouts in the cron schedule. |
| 74 | + |
| 75 | +### `getTxDataFromEVMEventLogs()` (`src/helpers/processTransactions.ts`) |
| 76 | + |
| 77 | +The core EVM adapter helper. For each `ContractEventParams` in the array: |
| 78 | + |
| 79 | +1. If `isTransfer: true`, auto-generates ERC20 Transfer event params targeting the contract address |
| 80 | +2. Calls `@defillama/sdk` `getLogs()` to fetch raw event logs for the block range |
| 81 | +3. Processes each log with concurrency 20 via `PromisePool`: |
| 82 | + - Extracts values via `logKeys` (from raw log fields), `argKeys` (from parsed event args), `txKeys` (from transaction data) |
| 83 | + - Applies filters: `includeArg`/`excludeArg`, `includeTxData`, `functionSignatureFilter`, `custom` filter functions |
| 84 | + - Optionally extracts token from receipt (`getTokenFromReceipt`) or input data (`inputDataExtraction`) |
| 85 | + - Applies `fixedEventData` overrides and `mapTokens` token address remapping |
| 86 | +4. Returns accumulated `EventData[]` after applying address-level `excludeFrom`/`excludeTo`/`excludeToken` filters |
| 87 | + |
| 88 | +### Cron System (`src/server/cron.ts`) |
| 89 | + |
| 90 | +The cron worker (`npm run start:cron`) is **not** a traditional cron scheduler — it runs all jobs once with staggered `setTimeout` delays and then self-terminates after 54 minutes (designed to be restarted by PM2 or an external orchestrator). |
| 91 | + |
| 92 | +**Timeline of a single cron cycle:** |
| 93 | +- **+5 min**: `aggregateLayerZero` (last 2 days), `aggregateAll` (last 36 hours), `aggregateHourly`, `aggregateDaily`, `runAllAdapters` |
| 94 | +- **+25 min**: Individual heavy adapters — `runWormhole`, `runMayan`, `runLayerZero`, `runHyperlane`, `runInterSoon`, `runRelay`, `runCashmere`, `runTeleswap`, `runCCIP`, `runSnowbridge` |
| 95 | +- **+54 min**: Process exits (prints getLogs usage summary, closes DB connections) |
| 96 | + |
| 97 | +Each job runs with `withTimeout()` — a `Promise.race` against a timeout (5-40 min depending on job). Set `NO_CRON=1` to disable. |
| 98 | + |
| 99 | +**`runAllAdapters` job** (`src/server/jobs/runAllAdapters.ts`): |
| 100 | +1. Queries max `tx_block` per `bridge_id` from `bridges.transactions` to get last recorded blocks |
| 101 | +2. Shuffles all bridge networks randomly (load distribution) |
| 102 | +3. Processes up to 10 adapters concurrently via `PromisePool` |
| 103 | +4. Skips bridges in `bridgesToSkip` (they have dedicated handlers) |
| 104 | + |
| 105 | +### Aggregation Pipeline (`src/utils/aggregate.ts`) |
| 106 | + |
| 107 | +**`aggregateData(timestamp, bridgeDbName, chain, hourly, largeTxThreshold)`**: |
| 108 | + |
| 109 | +1. Determines the time window (previous hour or previous day relative to `timestamp`) |
| 110 | +2. Queries all raw transactions from `bridges.transactions` for that bridge/chain/time window |
| 111 | +3. Collects unique token addresses and fetches batch prices from `coins.llama.fi` via `getLlamaPrices()` |
| 112 | +4. For each transaction: |
| 113 | + - Converts raw token amount to USD using price + decimals (respects `transformTokens` and `transformTokenDecimals` mappings) |
| 114 | + - If `is_usd_volume` flag is set, uses amount directly as USD value |
| 115 | + - Accumulates per-token and per-address deposit/withdrawal totals |
| 116 | + - Flags transactions above `largeTxThreshold` for separate storage |
| 117 | + - Skips values over $10B as likely errors |
| 118 | +5. Inserts aggregated row into `bridges.hourly_aggregated` or `bridges.daily_aggregated` |
| 119 | +6. Inserts qualifying rows into `bridges.large_transactions` |
| 120 | + |
| 121 | +**`aggregateHourlyVolume` / `aggregateDailyVolume`** (`src/server/jobs/`): Secondary aggregation that copies data from `hourly_aggregated` into simplified `hourly_volume`/`daily_volume` tables joined with chain info from `bridges.config`. |
| 122 | + |
| 123 | +### Bridge Network Registry (`src/data/bridgeNetworkData.ts`) |
| 124 | + |
| 125 | +Each bridge is registered as a `BridgeNetwork` with a unique `id`, `bridgeDbName`, `chains[]`, `largeTxThreshold`, and optional `chainMapping`/`destinationChain`. Lookup helpers in `src/data/importBridgeNetwork.ts`. |
| 126 | + |
| 127 | +### EventData (`src/utils/types.ts`) |
| 128 | + |
| 129 | +Core return type from all adapters: `blockNumber`, `txHash`, `from`, `to`, `token`, `amount` (ethers BigNumber), `isDeposit`, plus optional `chain`, `chainOverride`, `isUSDVolume`, `txsCountedAs`. |
| 130 | + |
| 131 | +### Key Helpers (`src/helpers/`) |
| 132 | + |
| 133 | +- `processTransactions.ts` — `getTxDataFromEVMEventLogs()`: core EVM log fetching/parsing |
| 134 | +- `eventParams.ts` — `constructTransferParams()` for ERC20 Transfer events |
| 135 | +- `bridgeAdapter.type.ts` — `BridgeAdapter`, `AsyncBridgeAdapter`, `ContractEventParams` types |
| 136 | +- `tokenMappings.ts` — token address transforms (`transformTokens`) and decimal overrides (`transformTokenDecimals`); also `chainMappings` for chain name aliasing |
| 137 | +- Chain-specific helpers: `solana.ts`, `sui.ts`, `tron.ts`, `stellar.ts` |
| 138 | + |
| 139 | +### Database (`sql/data.sql`) |
| 140 | + |
| 141 | +PostgreSQL under `bridges` schema. Key tables: |
| 142 | +- `bridges.transactions` — raw tx records (unique on bridge_id, chain, tx_hash, token, tx_from, tx_to) |
| 143 | +- `bridges.hourly_aggregated` / `bridges.daily_aggregated` — pre-aggregated volumes with token_total[] and address_total[] arrays |
| 144 | +- `bridges.hourly_volume` / `bridges.daily_volume` — simplified volume tables (joined with chain from config) |
| 145 | +- `bridges.large_transactions` — transactions above `largeTxThreshold` |
| 146 | +- `bridges.config` — maps (bridge_name, chain) → UUID |
| 147 | + |
| 148 | +DB access via `src/utils/db.js` using the `postgres` npm package. Two connection pools: `sql` (max 10) for writes, `querySql` (max 6) for reads. |
| 149 | + |
| 150 | +### Server (`src/server/`) |
| 151 | + |
| 152 | +- `index.ts` — Fastify server with Redis caching (70-min TTL, 10-min warming interval) |
| 153 | +- `cron.ts` — Delay-based job scheduler (see Cron System above) |
| 154 | +- Handlers in `src/handlers/` use `wrap()` from `src/utils/wrap.ts` for Lambda/Fastify compatibility |
| 155 | + |
| 156 | +### Utilities (`src/utils/`) |
| 157 | + |
| 158 | +- `adapter.ts` — adapter runner: `runAdapterToCurrentBlock()`, `runAdapterHistorical()`, `runAllAdaptersToCurrentBlock()`; defines `bridgesToSkip` |
| 159 | +- `aggregate.ts` — fetches prices from `coins.llama.fi`, computes USD volumes, inserts aggregated rows |
| 160 | +- `blocks.ts` — `getLatestBlockNumber()`, `getBlockByTimestamp()` (multi-chain: EVM, Solana, Sui, Stellar, Tron, IBC) |
| 161 | +- `prices.ts` — DefiLlama price API integration |
| 162 | +- `cache.ts` — Redis wrapper with getLogs tracking per adapter:chain |
| 163 | +- `constants.ts` — `maxBlocksToQueryByChain` per-chain block query limits (~1.5-2 hours of blocks) |
| 164 | + |
| 165 | +## Adding a New Adapter |
| 166 | + |
| 167 | +1. Create `src/adapters/<bridge-name>/index.ts` exporting a default `BridgeAdapter` |
| 168 | +2. Add a `BridgeNetwork` entry in `src/data/bridgeNetworkData.ts` with a new unique `id` |
| 169 | +3. Import and register in `src/adapters/index.ts` |
| 170 | +4. Test with: `npm run test <bridge-name> <numBlocks>` |
| 171 | + |
| 172 | +## Environment Variables |
| 173 | + |
| 174 | +Required: `DB_URL` (or `PSQL_USERNAME` + `PSQL_PW` + `PSQL_URL`). Optional: `REDIS_URL`, `PORT` (default 3000), `DISCORD_WEBHOOK`, `ALLIUM_API_KEY`, per-chain RPC vars (e.g. `ETHEREUM_RPC`). |
| 175 | + |
| 176 | +## Deployment |
| 177 | + |
| 178 | +Push to `master` triggers CI: Node 18, `npm ci`, `npm run ts`, then `sls deploy --stage prod` (AWS Lambda via Serverless Framework). |
0 commit comments