Pricing Utilities - LP Token & Swap Volume Calculations
Overview
These utilities handle the complex task of pricing LP tokens and calculating USD values for swaps. They integrate real-time blockchain data with CoinGecko price feeds to provide accurate valuations.
Core Imports & Price Caching
import Big from 'big.js';
import { PoolConfig, PoolState } from '../model';
import { DataHandlerContext, BlockData } from '@subsquid/evm-processor';
import { Store } from '@subsquid/typeorm-store';
import { updatePoolStateFromOnChain } from './pool';
import { fetchHistoricalUsd, pricePosition } from '@absinthe/common';
/** in-memory, process-wide price cache (key = "<id>-<hourBucket>") */
const hourlyPriceCache = new Map<string, number>();
- Big.js: High-precision decimal arithmetic for financial calculations
- Price Cache: In-memory cache to avoid redundant CoinGecko API calls
- Pool Utilities: Integration with on-chain state updates
๐ฐ Price Caching System
getHourlyPrice()
export async function getHourlyPrice(
coingeckoId: string,
timestampMs: number,
coingeckoApiKey: string,
isMocked: boolean = false,
): Promise<number> {
if (isMocked) return 0;
if (!coingeckoId) throw new Error('coingeckoId required');
// Round timestamp to start of day for cache efficiency
const dayBucket = new Date(timestampMs).setHours(0, 0, 0, 0);
const k = `${coingeckoId}-${dayBucket}`;
// Return cached price if available
if (hourlyPriceCache.has(k)) return hourlyPriceCache.get(k)!;
// Fetch from CoinGecko and cache result
const price = await fetchHistoricalUsd(coingeckoId, timestampMs, coingeckoApiKey);
hourlyPriceCache.set(k, price);
return price;
}
- Day-level bucketing: Groups all requests within same day
- Memory efficiency: Reduces API calls by ~24x (hours per day)
- Process-wide cache: Shared across all pools and events
- Cache key format:
"ethereum-1640995200000"
(token-daystart)
// Multiple calls in same day return cached result
const ethPrice1 = await getHourlyPrice('ethereum', 1640995200000, apiKey); // API call
const ethPrice2 = await getHourlyPrice('ethereum', 1640998800000, apiKey); // Cache hit
๐ Swap Volume Pricing
computePricedSwapVolume()
export async function computePricedSwapVolume(
tokenAmount: bigint,
coingeckoId: string,
decimals: number,
atMs: number,
coingeckoApiKey: string,
isMocked: boolean = false,
): Promise<number> {
if (isMocked) return 0;
// Get cached token price
const price = await getHourlyPrice(coingeckoId, atMs, coingeckoApiKey);
// Convert raw amount to USD value
return pricePosition(price, tokenAmount, decimals);
}
- Swap valuation: Calculate USD value of tokens traded in swaps
- Decimal handling: Converts raw blockchain amounts to human-readable values
- Historical pricing: Uses price at time of transaction, not current price
// Swap: 1.5 WETH at $3000/ETH on Jan 1, 2022
const usdValue = await computePricedSwapVolume(
1500000000000000000n, // 1.5 ETH in wei
'ethereum', // CoinGecko ID
18, // ETH decimals
1640995200000, // Jan 1, 2022 timestamp
apiKey,
);
// Returns: 4500 (1.5 * $3000)
๐ LP Token Price Calculation
computeLpTokenPrice()
The most complex function - calculates the USD value of a single LP token based on underlying reserves.
export async function computeLpTokenPrice(
ctx: DataHandlerContext<Store>,
block: BlockData,
poolConfig: PoolConfig,
poolState: PoolState,
coingeckoApiKey: string,
timestampMs?: number,
): Promise<{
price: number; // USD value of 1 LP token
token0Price: number; // USD price of token0
token1Price: number; // USD price of token1
token0Value: number; // Total USD value of token0 reserves
token1Value: number; // Total USD value of token1 reserves
totalPoolValue: number; // Total USD value of all liquidity
totalSupplyBig: Big.Big; // Total LP tokens (decimal adjusted)
reserve0: bigint; // Raw token0 reserves
reserve1: bigint; // Raw token1 reserves
}> {
// Validation: Ensure all required config exists
if (!poolConfig) throw new Error('No poolConfig provided');
if (!poolState) throw new Error('No poolState provided');
if (!poolConfig.token0) throw new Error(`poolConfig.token0 missing in ${poolConfig.id}`);
if (!poolConfig.token1) throw new Error(`poolConfig.token1 missing in ${poolConfig.id}`);
if (!poolConfig.lpToken) throw new Error(`poolConfig.lpToken missing in ${poolConfig.id}`);
if (!poolConfig.token0.coingeckoId || !poolConfig.token1.coingeckoId) {
throw new Error('No coingecko id found for token0 or token1');
}
// Step 1: Update pool state with fresh blockchain data
poolState = await updatePoolStateFromOnChain(ctx, block, poolConfig.lpToken.address, poolConfig, poolState);
// Step 2: Handle edge case - zero total supply
if (poolState.totalSupply === 0n) {
console.warn(`Pool ${poolConfig.id} has zero total supply, returning price 0`);
return {
price: 0,
token0Price: 0,
token1Price: 0,
token0Value: 0,
token1Value: 0,
totalPoolValue: 0,
totalSupplyBig: new Big(0),
reserve0: BigInt(0),
reserve1: BigInt(0),
};
}
// Step 3: Fetch token prices in parallel for efficiency
const timestamp = timestampMs ?? Number(poolState.lastTsMs);
const [token0Price, token1Price] = await Promise.all([
getHourlyPrice(poolConfig.token0.coingeckoId, timestamp, coingeckoApiKey),
getHourlyPrice(poolConfig.token1.coingeckoId, timestamp, coingeckoApiKey),
]);
// Step 4: Handle edge case - zero token prices
if (token0Price === 0 || token1Price === 0) {
console.warn(`Token prices are 0 for pool ${poolConfig.id}, returning price 0`);
return {
price: 0,
token0Price: 0,
token1Price: 0,
token0Value: 0,
token1Value: 0,
totalPoolValue: 0,
totalSupplyBig: new Big(0),
reserve0: BigInt(0),
reserve1: BigInt(0),
};
}
// Step 5: Calculate USD value of each token's reserves
const token0Value = pricePosition(token0Price, poolState.reserve0, poolConfig.token0.decimals);
const token1Value = pricePosition(token1Price, poolState.reserve1, poolConfig.token1.decimals);
const totalPoolValue = token0Value + token1Value;
// Step 6: Convert total supply to decimal-adjusted value
const totalSupplyBig = new Big(poolState.totalSupply.toString()).div(new Big(10).pow(poolConfig.lpToken.decimals));
// Step 7: Additional safety check after decimal conversion
if (totalSupplyBig.eq(0)) {
console.warn(`Pool ${poolConfig.id} has zero supply after conversion, returning price 0`);
return {
price: 0,
token0Price: 0,
token1Price: 0,
token0Value: 0,
token1Value: 0,
totalPoolValue: 0,
totalSupplyBig: new Big(0),
reserve0: BigInt(0),
reserve1: BigInt(0),
};
}
// Step 8: Calculate final LP token price
const price = new Big(totalPoolValue).div(totalSupplyBig).toNumber();
return {
price,
token0Price,
token1Price,
token0Value,
token1Value,
totalPoolValue,
totalSupplyBig,
reserve0: poolState.reserve0,
reserve1: poolState.reserve1,
};
}
Example Calculation
// USDC/WETH Pool Example:
// Reserve0: 1,000,000 USDC (6 decimals)
// Reserve1: 500 WETH (18 decimals)
// USDC Price: $1.00
// WETH Price: $2,000
// Total LP Supply: 22,360 tokens (18 decimals)
const token0Value = 1000000 * 1.00 = $1,000,000 // USDC value
const token1Value = 500 * 2000 = $1,000,000 // WETH value
const totalPoolValue = $2,000,000 // Total liquidity
const lpTokenPrice = $2,000,000 รท 22,360 = $89.44 // Per LP token
Error Handling & Edge Cases
- Zero supply pools: Returns price 0 to avoid division by zero
- Missing CoinGecko IDs: Throws error early to fail fast
- Zero token prices: Returns price 0 (could be new/unlisted tokens)
- State synchronization: Always fetches fresh reserves before calculation
Performance Optimizations
- Parallel price fetching: Uses
Promise.all()
for token0 and token1 prices - Price caching: Leverages
getHourlyPrice()
caching system - Fresh state: Updates pool state only when needed for calculations
- Big.js precision: Avoids floating-point errors in financial calculations