This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
- Type check:
npm run ts(runstsc --noEmit) - Build:
npm run build(Vite →dist/, CJS output) - Dev server:
npm run dev(requires.envfile; tsx watch on port 3000) - Production server:
npm run start(runsdist/index.js) - Cron worker:
npm run start:cron(runsdist/startCron.js) - Test adapter:
npm run test <bridgeName> [numBlocks]— runs adapter against recent blocks - Test historical:
npm run test-txs <startTs> <endTs> <adapter>— backfill test - Backfill adapter:
npm run adapter <startTs> <endTs> <bridgeName> [chain](requires.env) - Aggregate:
npm run aggregate <startTs> <endTs> <bridgeName>(requires.env) - Formatting: Prettier with
printWidth: 120,tabWidth: 2
The system follows a three-stage pipeline: Fetch → Store → Aggregate.
- Fetch: Adapters pull raw bridge transaction events from on-chain logs or APIs
- Store: Raw transactions are inserted into
bridges.transactionswith block timestamps - Aggregate: Raw txs are priced via DefiLlama's price API (
coins.llama.fi), then rolled up into hourly and daily volume tables
Each bridge protocol has a directory under src/adapters/ exporting a default BridgeAdapter:
{ [chainName: string]: (fromBlock: number, toBlock: number) => Promise<EventData[]> }Two main patterns:
- EVM event log pattern (most adapters) — define
ContractEventParams/PartialContractEventParamsarrays, pass togetTxDataFromEVMEventLogs()fromsrc/helpers/processTransactions.ts. Each params object specifies atargetcontract address,topic(event signature),abi, and key mappings (logKeys,argKeys,txKeys,fixedEventData) that map event fields toEventDatafields. Seesrc/adapters/celer/index.tsfor a representative example. - API-based pattern — query bridge indexer APIs directly, construct
EventDataobjects manually (e.g.,src/adapters/across/)
Some adapters use AsyncBridgeAdapter (with isAsync: true and a build() method) when they need async initialization before returning the chain function map.
Adapters are registered in src/adapters/index.ts keyed by bridgeDbName.
runAdapterToCurrentBlock(bridgeNetwork) — the main entry point for running a single adapter:
- Resolves the adapter (handling async adapters via
build()) - Ensures
bridges.configentries exist for each chain the adapter supports - For each chain in parallel (staggered by 200ms):
- Looks up the
bridgeIDfrom the config table - Calls
getBlocksForRunningAdapter()to determine the block range:- Gets the latest on-chain block number
- Finds the last recorded block from DB (
lastRecordedBlocksquery) or Redis progress cache - Redis progress (
adapter_progress:{name}:{chain}, 7-day TTL) tracks the last successfully processed block, allowing resumption after crashes startBlock = lastRecordedEndBlock + 1
- Processes blocks in chunks of
maxBlocksToQueryByChain[chain](defined insrc/utils/constants.ts, ~1.5-2 hours of blocks per chain) - Each chunk calls
runAdapterHistorical()
- Looks up the
runAdapterHistorical(startBlock, endBlock, ...) — processes a block range:
- Checks Redis progress to skip already-processed ranges
- Fetches event logs by calling the adapter's chain function (with
async-retry, 4 retries) - Estimates timestamps by sampling 10 blocks across the range from the provider (Solana uses
getBlockTimeper tx instead) - Filters out tx groups with 100+ events per txHash
- Inserts each event into
bridges.transactionswithin a SQL transaction (with retries) - Updates Redis progress on success
- On failure after 3 attempts, logs error to DB and sends Discord notification
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.
The core EVM adapter helper. For each ContractEventParams in the array:
- If
isTransfer: true, auto-generates ERC20 Transfer event params targeting the contract address - Calls
@defillama/sdkgetLogs()to fetch raw event logs for the block range - Processes each log with concurrency 20 via
PromisePool:- Extracts values via
logKeys(from raw log fields),argKeys(from parsed event args),txKeys(from transaction data) - Applies filters:
includeArg/excludeArg,includeTxData,functionSignatureFilter,customfilter functions - Optionally extracts token from receipt (
getTokenFromReceipt) or input data (inputDataExtraction) - Applies
fixedEventDataoverrides andmapTokenstoken address remapping
- Extracts values via
- Returns accumulated
EventData[]after applying address-levelexcludeFrom/excludeTo/excludeTokenfilters
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).
Timeline of a single cron cycle:
- +5 min:
aggregateLayerZero(last 2 days),aggregateAll(last 36 hours),aggregateHourly,aggregateDaily,runAllAdapters - +25 min: Individual heavy adapters —
runWormhole,runMayan,runLayerZero,runHyperlane,runInterSoon,runRelay,runCashmere,runTeleswap,runCCIP,runSnowbridge - +54 min: Process exits (prints getLogs usage summary, closes DB connections)
Each job runs with withTimeout() — a Promise.race against a timeout (5-40 min depending on job). Set NO_CRON=1 to disable.
runAllAdapters job (src/server/jobs/runAllAdapters.ts):
- Queries max
tx_blockperbridge_idfrombridges.transactionsto get last recorded blocks - Shuffles all bridge networks randomly (load distribution)
- Processes up to 10 adapters concurrently via
PromisePool - Skips bridges in
bridgesToSkip(they have dedicated handlers)
aggregateData(timestamp, bridgeDbName, chain, hourly, largeTxThreshold):
- Determines the time window (previous hour or previous day relative to
timestamp) - Queries all raw transactions from
bridges.transactionsfor that bridge/chain/time window - Collects unique token addresses and fetches batch prices from
coins.llama.fiviagetLlamaPrices() - For each transaction:
- Converts raw token amount to USD using price + decimals (respects
transformTokensandtransformTokenDecimalsmappings) - If
is_usd_volumeflag is set, uses amount directly as USD value - Accumulates per-token and per-address deposit/withdrawal totals
- Flags transactions above
largeTxThresholdfor separate storage - Skips values over $10B as likely errors
- Converts raw token amount to USD using price + decimals (respects
- Inserts aggregated row into
bridges.hourly_aggregatedorbridges.daily_aggregated - Inserts qualifying rows into
bridges.large_transactions
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.
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.
Core return type from all adapters: blockNumber, txHash, from, to, token, amount (ethers BigNumber), isDeposit, plus optional chain, chainOverride, isUSDVolume, txsCountedAs.
processTransactions.ts—getTxDataFromEVMEventLogs(): core EVM log fetching/parsingeventParams.ts—constructTransferParams()for ERC20 Transfer eventsbridgeAdapter.type.ts—BridgeAdapter,AsyncBridgeAdapter,ContractEventParamstypestokenMappings.ts— token address transforms (transformTokens) and decimal overrides (transformTokenDecimals); alsochainMappingsfor chain name aliasing- Chain-specific helpers:
solana.ts,sui.ts,tron.ts,stellar.ts
PostgreSQL under bridges schema. Key tables:
bridges.transactions— raw tx records (unique on bridge_id, chain, tx_hash, token, tx_from, tx_to)bridges.hourly_aggregated/bridges.daily_aggregated— pre-aggregated volumes with token_total[] and address_total[] arraysbridges.hourly_volume/bridges.daily_volume— simplified volume tables (joined with chain from config)bridges.large_transactions— transactions abovelargeTxThresholdbridges.config— maps (bridge_name, chain) → UUID
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.
index.ts— Fastify server with Redis caching (70-min TTL, 10-min warming interval)cron.ts— Delay-based job scheduler (see Cron System above)- Handlers in
src/handlers/usewrap()fromsrc/utils/wrap.tsfor Lambda/Fastify compatibility
adapter.ts— adapter runner:runAdapterToCurrentBlock(),runAdapterHistorical(),runAllAdaptersToCurrentBlock(); definesbridgesToSkipaggregate.ts— fetches prices fromcoins.llama.fi, computes USD volumes, inserts aggregated rowsblocks.ts—getLatestBlockNumber(),getBlockByTimestamp()(multi-chain: EVM, Solana, Sui, Stellar, Tron, IBC)prices.ts— DefiLlama price API integrationcache.ts— Redis wrapper with getLogs tracking per adapter:chainconstants.ts—maxBlocksToQueryByChainper-chain block query limits (~1.5-2 hours of blocks)
- Create
src/adapters/<bridge-name>/index.tsexporting a defaultBridgeAdapter - Add a
BridgeNetworkentry insrc/data/bridgeNetworkData.tswith a new uniqueid - Import and register in
src/adapters/index.ts - Test with:
npm run test <bridge-name> <numBlocks>
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).
Push to master triggers CI: Node 18, npm ci, npm run ts, then sls deploy --stage prod (AWS Lambda via Serverless Framework).