3. Redis Token Caching
Purpose
Before we can decode swap events, we need to know which tokens are in the pool. Uniswap V2 pools have token0
and token1
addresses that we need to fetch from the contract. To avoid expensive RPC calls on every swap, we cache these addresses in Redis.
Implementation
const token0Key = `univ2:${params.poolAddress}:token0`;
const token1Key = `univ2:${params.poolAddress}:token1`;
let tk0Addr = await redis.get(token0Key);
let tk1Addr = await redis.get(token1Key);
if (!tk0Addr || !tk1Addr) {
const poolContract = new univ2Abi.Contract(rpc, params.poolAddress);
tk0Addr = (await poolContract.token0()).toLowerCase();
tk1Addr = (await poolContract.token1()).toLowerCase();
await redis.set(token0Key, tk0Addr);
await redis.set(token1Key, tk1Addr);
}
Why This Matters
The Problem
Without caching, every swap event would require:
- RPC Call to get
token0
(~$0.0001-0.001 per call) - RPC Call to get
token1
(~$0.0001-0.001 per call) - For 1000 swaps: ~$0.20-2.00 in RPC costs
The Solution
With Redis caching:
- First event: Fetch from contract and cache (~$0.0002)
- All subsequent events: Read from Redis (~$0.000001)
- For 1000 swaps: ~$0.001 + negligible Redis costs
Cache Key Strategy
Naming Convention
const token0Key = `univ2:${params.poolAddress}:token0`;
const token1Key = `univ2:${params.poolAddress}:token1`;
univ2:
- Namespacing to avoid collisions${poolAddress}:
- Pool-specific data:token0
/:token1
- Clear field identification
Collision Prevention
Automatic Config Prefixing: All Redis keys are automatically prefixed with a hash of your adapter configuration, ensuring that different adapters or different configurations cannot collide with each other.
// Your actual Redis key structure:
// [configHash]:univ2:0x123...:token0
// [configHash]:univ3:0x456...:token0
Protocol Namespacing (Recommended):
While not mandatory due to automatic config prefixing, adding protocol prefixes like univ2:
is highly recommended for easier debugging and key organization.
// ✅ Recommended: Clear protocol namespacing
`univ2:${params.poolAddress}:token0`
`univ3:${params.poolAddress}:token0` // Different protocol
// ⚠️ Possible but less clear: Protocol prefix not required
`${params.poolAddress}:token0` // Still collision-safe due to config hash
- Debugging: Easier to identify which adapter/protocol owns the data
- Maintenance: Clear separation between different protocols
- Readability: Self-documenting keys in Redis
Address Normalization
Why Lowercase?
tk0Addr = (await poolContract.token0()).toLowerCase();
tk1Addr = (await poolContract.token1()).toLowerCase();
- Ethereum addresses are case-insensitive but case-significant for checksums
- Lowercase ensures consistent storage and comparison
- Prevents duplicate cache entries for the same address
// These are the same address but different strings:
'0x742d35Cc6634C0532925a3b8F6f5c6fF1c4a5F1' // Mixed case
'0x742d35cc6634c0532925a3b8f6f5c6ff1c4a5f1' // Lowercase
Error Handling
Network Failures
try {
const poolContract = new univ2Abi.Contract(rpc, params.poolAddress);
tk0Addr = (await poolContract.token0()).toLowerCase();
} catch (error) {
console.error(`Failed to fetch token0 for pool ${params.poolAddress}:`, error);
throw error; // Re-throw to fail the adapter
}
Best Practices
Cache Key Patterns
// Pool metadata
`univ2:${pool}:token0`
`univ2:${pool}:token1`
`univ2:${pool}:fee` // Could cache fee tier too
// User data (if needed)
`univ2:${pool}:${user}:balance`
// Global data
`univ2:factory` // Factory contract address
Monitoring Cache Hit Rates
// Log cache performance
if (tk0Addr && tk1Addr) {
console.log(`Cache hit for pool ${params.poolAddress}`);
} else {
console.log(`Cache miss for pool ${params.poolAddress}`);
}