feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
708
orig/pkg/oracle/price_oracle.go
Normal file
708
orig/pkg/oracle/price_oracle.go
Normal file
@@ -0,0 +1,708 @@
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/uniswap"
|
||||
)
|
||||
|
||||
// PriceOracle provides real-time price data for tokens
|
||||
type PriceOracle struct {
|
||||
client *ethclient.Client
|
||||
logger *logger.Logger
|
||||
priceCache map[string]*PriceData
|
||||
cacheMutex sync.RWMutex
|
||||
cacheExpiry time.Duration
|
||||
updateTicker *time.Ticker
|
||||
stopChan chan struct{}
|
||||
chainlinkFeeds map[common.Address]common.Address // token -> chainlink feed
|
||||
uniswapPools map[string]common.Address // token pair -> pool address
|
||||
}
|
||||
|
||||
// PriceData represents cached price information
|
||||
type PriceData struct {
|
||||
Price *big.Int // Price in wei (18 decimals)
|
||||
Timestamp time.Time
|
||||
Source string // "chainlink", "uniswap", "coingecko"
|
||||
Confidence float64 // 0.0 to 1.0
|
||||
}
|
||||
|
||||
// PriceRequest represents a price query
|
||||
type PriceRequest struct {
|
||||
TokenIn common.Address
|
||||
TokenOut common.Address
|
||||
AmountIn *big.Int
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// PriceResponse contains the price calculation result
|
||||
type PriceResponse struct {
|
||||
Price *big.Int
|
||||
AmountOut *big.Int
|
||||
SlippageBps *big.Int // basis points (1% = 100 bps)
|
||||
PriceImpact float64 // price impact as decimal (0.01 = 1%)
|
||||
Liquidity *big.Int // estimated pool liquidity
|
||||
Source string
|
||||
Timestamp time.Time
|
||||
Valid bool
|
||||
}
|
||||
|
||||
// NewPriceOracle creates a new price oracle instance
|
||||
func NewPriceOracle(client *ethclient.Client, logger *logger.Logger) *PriceOracle {
|
||||
oracle := &PriceOracle{
|
||||
client: client,
|
||||
logger: logger,
|
||||
priceCache: make(map[string]*PriceData),
|
||||
cacheExpiry: 5 * time.Minute, // 5-minute cache for arbitrage windows
|
||||
stopChan: make(chan struct{}),
|
||||
chainlinkFeeds: getChainlinkFeeds(),
|
||||
uniswapPools: getUniswapPools(),
|
||||
}
|
||||
|
||||
// Start background price updates
|
||||
oracle.updateTicker = time.NewTicker(15 * time.Second)
|
||||
go oracle.backgroundUpdater()
|
||||
|
||||
return oracle
|
||||
}
|
||||
|
||||
// GetPrice returns the current price for a token pair
|
||||
func (p *PriceOracle) GetPrice(ctx context.Context, req *PriceRequest) (*PriceResponse, error) {
|
||||
if req.TokenIn == req.TokenOut {
|
||||
return &PriceResponse{
|
||||
Price: big.NewInt(1e18), // 1:1 ratio
|
||||
AmountOut: new(big.Int).Set(req.AmountIn),
|
||||
Source: "identity",
|
||||
Timestamp: time.Now(),
|
||||
Valid: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Try multiple price sources in order of preference
|
||||
sources := []func(context.Context, *PriceRequest) (*PriceResponse, error){
|
||||
p.getChainlinkPrice,
|
||||
p.getUniswapV3Price,
|
||||
p.getUniswapV2Price,
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, getPrice := range sources {
|
||||
if response, err := getPrice(ctx, req); err == nil && response.Valid {
|
||||
p.cachePrice(req, response)
|
||||
return response, nil
|
||||
} else {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to cached price if available
|
||||
if cached := p.getCachedPrice(req); cached != nil {
|
||||
p.logger.Warn(fmt.Sprintf("Using cached price for %s/%s due to oracle failures",
|
||||
req.TokenIn.Hex(), req.TokenOut.Hex()))
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all price sources failed, last error: %w", lastErr)
|
||||
}
|
||||
|
||||
// getChainlinkPrice gets price from Chainlink price feeds
|
||||
func (p *PriceOracle) getChainlinkPrice(ctx context.Context, req *PriceRequest) (*PriceResponse, error) {
|
||||
feedAddr, exists := p.chainlinkFeeds[req.TokenIn]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no chainlink feed for token %s", req.TokenIn.Hex())
|
||||
}
|
||||
|
||||
// Chainlink ABI for latestRoundData()
|
||||
chainlinkABI := `[{"inputs":[],"name":"latestRoundData","outputs":[{"internalType":"uint80","name":"roundId","type":"uint80"},{"internalType":"int256","name":"answer","type":"int256"},{"internalType":"uint256","name":"startedAt","type":"uint256"},{"internalType":"uint256","name":"updatedAt","type":"uint256"},{"internalType":"uint80","name":"answeredInRound","type":"uint80"}],"stateMutability":"view","type":"function"}]`
|
||||
|
||||
contractABI, err := uniswap.ParseABI(chainlinkABI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse chainlink ABI: %w", err)
|
||||
}
|
||||
|
||||
// Call latestRoundData
|
||||
callData, err := contractABI.Pack("latestRoundData")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack chainlink call: %w", err)
|
||||
}
|
||||
|
||||
result, err := p.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &feedAddr,
|
||||
Data: callData,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("chainlink call failed: %w", err)
|
||||
}
|
||||
|
||||
// Unpack result
|
||||
unpacked, err := contractABI.Unpack("latestRoundData", result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack chainlink result: %w", err)
|
||||
}
|
||||
|
||||
if len(unpacked) < 5 {
|
||||
return nil, fmt.Errorf("invalid chainlink response length")
|
||||
}
|
||||
|
||||
answer, ok := unpacked[1].(*big.Int)
|
||||
if !ok || answer.Sign() <= 0 {
|
||||
return nil, fmt.Errorf("invalid chainlink price: %v", unpacked[1])
|
||||
}
|
||||
|
||||
updatedAt, ok := unpacked[3].(*big.Int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid chainlink timestamp: %v", unpacked[3])
|
||||
}
|
||||
|
||||
// Check if price is stale (older than 1 hour)
|
||||
if time.Since(time.Unix(updatedAt.Int64(), 0)) > time.Hour {
|
||||
return nil, fmt.Errorf("chainlink price is stale")
|
||||
}
|
||||
|
||||
// Convert amount using chainlink price
|
||||
// Chainlink prices are typically 8 decimals, convert to 18
|
||||
priceWei := new(big.Int).Mul(answer, big.NewInt(1e10))
|
||||
amountOut := new(big.Int).Mul(req.AmountIn, priceWei)
|
||||
amountOut.Div(amountOut, big.NewInt(1e18))
|
||||
|
||||
return &PriceResponse{
|
||||
Price: priceWei,
|
||||
AmountOut: amountOut,
|
||||
SlippageBps: big.NewInt(0), // Chainlink has no slippage
|
||||
Source: "chainlink",
|
||||
Timestamp: time.Unix(updatedAt.Int64(), 0),
|
||||
Valid: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getUniswapV3Price gets price from Uniswap V3 pools
|
||||
func (p *PriceOracle) getUniswapV3Price(ctx context.Context, req *PriceRequest) (*PriceResponse, error) {
|
||||
poolKey := fmt.Sprintf("%s-%s", req.TokenIn.Hex(), req.TokenOut.Hex())
|
||||
poolAddr, exists := p.uniswapPools[poolKey]
|
||||
if !exists {
|
||||
// Try reverse pair
|
||||
poolKey = fmt.Sprintf("%s-%s", req.TokenOut.Hex(), req.TokenIn.Hex())
|
||||
poolAddr, exists = p.uniswapPools[poolKey]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no uniswap v3 pool for pair %s/%s", req.TokenIn.Hex(), req.TokenOut.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
// Get pool state
|
||||
poolState, err := p.getPoolState(ctx, poolAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get pool state: %w", err)
|
||||
}
|
||||
|
||||
// Calculate price impact and slippage using Uniswap V3 pricing
|
||||
pricing := uniswap.NewUniswapV3Pricing(p.client)
|
||||
|
||||
// Get current price from pool (returns *big.Float)
|
||||
currentPrice := uniswap.SqrtPriceX96ToPrice(poolState.SqrtPriceX96)
|
||||
|
||||
// Calculate output amount using Uniswap V3 math
|
||||
amountOut, err := pricing.CalculateAmountOut(req.AmountIn, poolState.SqrtPriceX96, poolState.Liquidity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to calculate amount out: %w", err)
|
||||
}
|
||||
|
||||
// Convert price to big.Int for slippage calculation (multiply by 1e18 for precision)
|
||||
priceInt := new(big.Int)
|
||||
currentPrice.Mul(currentPrice, big.NewFloat(1e18))
|
||||
currentPrice.Int(priceInt)
|
||||
|
||||
// Calculate slippage in basis points
|
||||
slippageBps, err := p.calculateSlippage(req.AmountIn, amountOut, priceInt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to calculate slippage: %w", err)
|
||||
}
|
||||
|
||||
return &PriceResponse{
|
||||
Price: priceInt, // Use converted big.Int price
|
||||
AmountOut: amountOut,
|
||||
SlippageBps: slippageBps,
|
||||
Source: "uniswap_v3",
|
||||
Timestamp: time.Now(),
|
||||
Valid: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getUniswapV2Price gets price from Uniswap V2 style pools using constant product formula
|
||||
func (p *PriceOracle) getUniswapV2Price(ctx context.Context, req *PriceRequest) (*PriceResponse, error) {
|
||||
p.logger.Debug(fmt.Sprintf("Getting Uniswap V2 price for %s/%s", req.TokenIn.Hex(), req.TokenOut.Hex()))
|
||||
|
||||
// Find Uniswap V2 pool for this token pair
|
||||
poolAddr, err := p.findUniswapV2Pool(ctx, req.TokenIn, req.TokenOut)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find Uniswap V2 pool: %w", err)
|
||||
}
|
||||
|
||||
// Get pool reserves using getReserves() function
|
||||
reserves, err := p.getUniswapV2Reserves(ctx, poolAddr, req.TokenIn, req.TokenOut)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get pool reserves: %w", err)
|
||||
}
|
||||
|
||||
// Calculate output amount using constant product formula: x * y = k
|
||||
// amountOut = (amountIn * reserveOut) / (reserveIn + amountIn)
|
||||
amountInWithFee := new(big.Int).Mul(req.AmountIn, big.NewInt(997)) // 0.3% fee
|
||||
numerator := new(big.Int).Mul(amountInWithFee, reserves.ReserveOut)
|
||||
denominator := new(big.Int).Add(new(big.Int).Mul(reserves.ReserveIn, big.NewInt(1000)), amountInWithFee)
|
||||
|
||||
if denominator.Sign() == 0 {
|
||||
return nil, fmt.Errorf("division by zero in price calculation")
|
||||
}
|
||||
|
||||
amountOut := new(big.Int).Div(numerator, denominator)
|
||||
|
||||
// Calculate price impact
|
||||
priceImpact := p.calculateV2PriceImpact(req.AmountIn, reserves.ReserveIn, reserves.ReserveOut)
|
||||
|
||||
// Calculate slippage in basis points
|
||||
slippageBps := new(big.Int).Mul(new(big.Int).SetInt64(int64(priceImpact*10000)), big.NewInt(1))
|
||||
|
||||
p.logger.Debug(fmt.Sprintf("V2 price calculation: input=%s, output=%s, impact=%.4f%%",
|
||||
req.AmountIn.String(), amountOut.String(), priceImpact*100))
|
||||
|
||||
return &PriceResponse{
|
||||
AmountOut: amountOut,
|
||||
SlippageBps: slippageBps,
|
||||
PriceImpact: priceImpact,
|
||||
Source: "uniswap_v2",
|
||||
Timestamp: time.Now(),
|
||||
Valid: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// V2Reserves represents the reserves in a Uniswap V2 pool
|
||||
type V2Reserves struct {
|
||||
ReserveIn *big.Int
|
||||
ReserveOut *big.Int
|
||||
BlockTimestamp uint32
|
||||
}
|
||||
|
||||
// findUniswapV2Pool finds the Uniswap V2 pool address for a token pair
|
||||
func (p *PriceOracle) findUniswapV2Pool(ctx context.Context, token0, token1 common.Address) (common.Address, error) {
|
||||
// Uniswap V2 Factory address on Arbitrum
|
||||
factoryAddr := common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9")
|
||||
|
||||
// Sort tokens to match Uniswap V2 convention
|
||||
tokenA, tokenB := token0, token1
|
||||
if token0.Big().Cmp(token1.Big()) > 0 {
|
||||
tokenA, tokenB = token1, token0
|
||||
}
|
||||
|
||||
// Calculate pool address using CREATE2 formula
|
||||
// address = keccak256(abi.encodePacked(hex"ff", factory, salt, initCodeHash))[12:]
|
||||
// where salt = keccak256(abi.encodePacked(token0, token1))
|
||||
|
||||
// Create salt from sorted token addresses
|
||||
salt := crypto.Keccak256Hash(append(tokenA.Bytes(), tokenB.Bytes()...))
|
||||
|
||||
// Uniswap V2 init code hash
|
||||
initCodeHash := common.HexToHash("0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f")
|
||||
|
||||
// CREATE2 calculation
|
||||
create2Input := append([]byte{0xff}, factoryAddr.Bytes()...)
|
||||
create2Input = append(create2Input, salt.Bytes()...)
|
||||
create2Input = append(create2Input, initCodeHash.Bytes()...)
|
||||
|
||||
poolHash := crypto.Keccak256Hash(create2Input)
|
||||
poolAddr := common.BytesToAddress(poolHash[12:])
|
||||
|
||||
// Verify pool exists by checking if it has code
|
||||
code, err := p.client.CodeAt(ctx, poolAddr, nil)
|
||||
if err != nil || len(code) == 0 {
|
||||
return common.Address{}, fmt.Errorf("pool does not exist for pair %s/%s", token0.Hex(), token1.Hex())
|
||||
}
|
||||
|
||||
return poolAddr, nil
|
||||
}
|
||||
|
||||
// getUniswapV2Reserves gets the reserves from a Uniswap V2 pool
|
||||
func (p *PriceOracle) getUniswapV2Reserves(ctx context.Context, poolAddr common.Address, tokenIn, tokenOut common.Address) (*V2Reserves, error) {
|
||||
// Uniswap V2 Pair ABI for getReserves function
|
||||
pairABI := `[{"constant":true,"inputs":[],"name":"getReserves","outputs":[{"internalType":"uint112","name":"_reserve0","type":"uint112"},{"internalType":"uint112","name":"_reserve1","type":"uint112"},{"internalType":"uint32","name":"_blockTimestampLast","type":"uint32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"token1","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"}]`
|
||||
|
||||
contractABI, err := uniswap.ParseABI(pairABI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse pair ABI: %w", err)
|
||||
}
|
||||
|
||||
// Get getReserves data
|
||||
reservesData, err := contractABI.Pack("getReserves")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack getReserves call: %w", err)
|
||||
}
|
||||
|
||||
reservesResult, err := p.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &poolAddr,
|
||||
Data: reservesData,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getReserves call failed: %w", err)
|
||||
}
|
||||
|
||||
reservesUnpacked, err := contractABI.Unpack("getReserves", reservesResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack reserves: %w", err)
|
||||
}
|
||||
|
||||
reserve0 := reservesUnpacked[0].(*big.Int)
|
||||
reserve1 := reservesUnpacked[1].(*big.Int)
|
||||
blockTimestamp := reservesUnpacked[2].(uint32)
|
||||
|
||||
// Get token0 to determine reserve order
|
||||
token0Data, err := contractABI.Pack("token0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack token0 call: %w", err)
|
||||
}
|
||||
|
||||
token0Result, err := p.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &poolAddr,
|
||||
Data: token0Data,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token0 call failed: %w", err)
|
||||
}
|
||||
|
||||
token0Unpacked, err := contractABI.Unpack("token0", token0Result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack token0: %w", err)
|
||||
}
|
||||
|
||||
token0Addr := token0Unpacked[0].(common.Address)
|
||||
|
||||
// Determine which reserve corresponds to tokenIn and tokenOut
|
||||
var reserveIn, reserveOut *big.Int
|
||||
if tokenIn == token0Addr {
|
||||
reserveIn, reserveOut = reserve0, reserve1
|
||||
} else {
|
||||
reserveIn, reserveOut = reserve1, reserve0
|
||||
}
|
||||
|
||||
return &V2Reserves{
|
||||
ReserveIn: reserveIn,
|
||||
ReserveOut: reserveOut,
|
||||
BlockTimestamp: blockTimestamp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// calculateV2PriceImpact calculates the price impact for a Uniswap V2 trade
|
||||
func (p *PriceOracle) calculateV2PriceImpact(amountIn, reserveIn, reserveOut *big.Int) float64 {
|
||||
if reserveIn.Sign() == 0 || reserveOut.Sign() == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Price before = reserveOut / reserveIn
|
||||
priceBefore := new(big.Float).Quo(new(big.Float).SetInt(reserveOut), new(big.Float).SetInt(reserveIn))
|
||||
|
||||
// Calculate new reserves after trade
|
||||
amountInWithFee := new(big.Int).Mul(amountIn, big.NewInt(997))
|
||||
newReserveIn := new(big.Int).Add(reserveIn, new(big.Int).Div(amountInWithFee, big.NewInt(1000)))
|
||||
|
||||
numerator := new(big.Int).Mul(amountInWithFee, reserveOut)
|
||||
denominator := new(big.Int).Add(new(big.Int).Mul(reserveIn, big.NewInt(1000)), amountInWithFee)
|
||||
amountOut := new(big.Int).Div(numerator, denominator)
|
||||
|
||||
newReserveOut := new(big.Int).Sub(reserveOut, amountOut)
|
||||
|
||||
// Price after = newReserveOut / newReserveIn
|
||||
priceAfter := new(big.Float).Quo(new(big.Float).SetInt(newReserveOut), new(big.Float).SetInt(newReserveIn))
|
||||
|
||||
// Price impact = |priceAfter - priceBefore| / priceBefore
|
||||
priceDiff := new(big.Float).Sub(priceAfter, priceBefore)
|
||||
priceDiff.Abs(priceDiff)
|
||||
|
||||
impact := new(big.Float).Quo(priceDiff, priceBefore)
|
||||
impactFloat, _ := impact.Float64()
|
||||
|
||||
return impactFloat
|
||||
}
|
||||
|
||||
// PoolState represents the current state of a Uniswap V3 pool
|
||||
type PoolState struct {
|
||||
SqrtPriceX96 *big.Int
|
||||
Tick int32
|
||||
Liquidity *big.Int
|
||||
Token0 common.Address
|
||||
Token1 common.Address
|
||||
}
|
||||
|
||||
// getPoolState retrieves the current state of a Uniswap V3 pool
|
||||
func (p *PriceOracle) getPoolState(ctx context.Context, poolAddr common.Address) (*PoolState, error) {
|
||||
// Uniswap V3 Pool ABI (slot0 function)
|
||||
poolABI := `[{"inputs":[],"name":"slot0","outputs":[{"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"internalType":"int24","name":"tick","type":"int24"},{"internalType":"uint16","name":"observationIndex","type":"uint16"},{"internalType":"uint16","name":"observationCardinality","type":"uint16"},{"internalType":"uint16","name":"observationCardinalityNext","type":"uint16"},{"internalType":"uint8","name":"feeProtocol","type":"uint8"},{"internalType":"bool","name":"unlocked","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"liquidity","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token1","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]`
|
||||
|
||||
contractABI, err := uniswap.ParseABI(poolABI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse pool ABI: %w", err)
|
||||
}
|
||||
|
||||
// Get slot0 data
|
||||
slot0Data, err := contractABI.Pack("slot0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack slot0 call: %w", err)
|
||||
}
|
||||
|
||||
slot0Result, err := p.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &poolAddr,
|
||||
Data: slot0Data,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("slot0 call failed: %w", err)
|
||||
}
|
||||
|
||||
slot0Unpacked, err := contractABI.Unpack("slot0", slot0Result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack slot0: %w", err)
|
||||
}
|
||||
|
||||
// Get liquidity
|
||||
liquidityData, err := contractABI.Pack("liquidity")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack liquidity call: %w", err)
|
||||
}
|
||||
|
||||
liquidityResult, err := p.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &poolAddr,
|
||||
Data: liquidityData,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("liquidity call failed: %w", err)
|
||||
}
|
||||
|
||||
liquidityUnpacked, err := contractABI.Unpack("liquidity", liquidityResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack liquidity: %w", err)
|
||||
}
|
||||
|
||||
// Get token addresses
|
||||
token0Data, err := contractABI.Pack("token0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack token0 call: %w", err)
|
||||
}
|
||||
|
||||
token0Result, err := p.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &poolAddr,
|
||||
Data: token0Data,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token0 call failed: %w", err)
|
||||
}
|
||||
|
||||
token0Unpacked, err := contractABI.Unpack("token0", token0Result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack token0: %w", err)
|
||||
}
|
||||
|
||||
token1Data, err := contractABI.Pack("token1")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack token1 call: %w", err)
|
||||
}
|
||||
|
||||
token1Result, err := p.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &poolAddr,
|
||||
Data: token1Data,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token1 call failed: %w", err)
|
||||
}
|
||||
|
||||
token1Unpacked, err := contractABI.Unpack("token1", token1Result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack token1: %w", err)
|
||||
}
|
||||
|
||||
// Extract values with proper type conversion
|
||||
sqrtPriceX96, ok := slot0Unpacked[0].(*big.Int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid sqrtPriceX96 type: %T", slot0Unpacked[0])
|
||||
}
|
||||
|
||||
// Convert tick from interface to int32
|
||||
var tick int32
|
||||
switch v := slot0Unpacked[1].(type) {
|
||||
case *big.Int:
|
||||
tick = int32(v.Int64())
|
||||
case int32:
|
||||
tick = v
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid tick type: %T", slot0Unpacked[1])
|
||||
}
|
||||
|
||||
liquidity, ok := liquidityUnpacked[0].(*big.Int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid liquidity type: %T", liquidityUnpacked[0])
|
||||
}
|
||||
|
||||
token0, ok := token0Unpacked[0].(common.Address)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token0 type: %T", token0Unpacked[0])
|
||||
}
|
||||
|
||||
token1, ok := token1Unpacked[0].(common.Address)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token1 type: %T", token1Unpacked[0])
|
||||
}
|
||||
|
||||
return &PoolState{
|
||||
SqrtPriceX96: sqrtPriceX96,
|
||||
Tick: tick,
|
||||
Liquidity: liquidity,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// calculateSlippage calculates slippage in basis points
|
||||
func (p *PriceOracle) calculateSlippage(amountIn, amountOut, currentPrice *big.Int) (*big.Int, error) {
|
||||
if amountIn.Sign() == 0 || currentPrice.Sign() == 0 {
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
|
||||
// Expected amount out at current price
|
||||
expectedOut := new(big.Int).Mul(amountIn, currentPrice)
|
||||
expectedOut.Div(expectedOut, big.NewInt(1e18))
|
||||
|
||||
// Calculate slippage percentage
|
||||
if expectedOut.Sign() == 0 {
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
|
||||
// Slippage = (expectedOut - actualOut) / expectedOut * 10000 (basis points)
|
||||
diff := new(big.Int).Sub(expectedOut, amountOut)
|
||||
slippage := new(big.Int).Mul(diff, big.NewInt(10000))
|
||||
slippage.Div(slippage, expectedOut)
|
||||
|
||||
// Ensure non-negative slippage
|
||||
if slippage.Sign() < 0 {
|
||||
slippage.Neg(slippage)
|
||||
}
|
||||
|
||||
return slippage, nil
|
||||
}
|
||||
|
||||
// cachePrice stores a price in the cache
|
||||
func (p *PriceOracle) cachePrice(req *PriceRequest, response *PriceResponse) {
|
||||
key := fmt.Sprintf("%s-%s", req.TokenIn.Hex(), req.TokenOut.Hex())
|
||||
|
||||
p.cacheMutex.Lock()
|
||||
defer p.cacheMutex.Unlock()
|
||||
|
||||
p.priceCache[key] = &PriceData{
|
||||
Price: response.Price,
|
||||
Timestamp: response.Timestamp,
|
||||
Source: response.Source,
|
||||
Confidence: 1.0, // Full confidence for fresh data
|
||||
}
|
||||
}
|
||||
|
||||
// getCachedPrice retrieves a cached price if available and not expired
|
||||
func (p *PriceOracle) getCachedPrice(req *PriceRequest) *PriceResponse {
|
||||
key := fmt.Sprintf("%s-%s", req.TokenIn.Hex(), req.TokenOut.Hex())
|
||||
|
||||
p.cacheMutex.RLock()
|
||||
defer p.cacheMutex.RUnlock()
|
||||
|
||||
cached, exists := p.priceCache[key]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if cache is expired
|
||||
if time.Since(cached.Timestamp) > p.cacheExpiry {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate amount out using cached price
|
||||
amountOut := new(big.Int).Mul(req.AmountIn, cached.Price)
|
||||
amountOut.Div(amountOut, big.NewInt(1e18))
|
||||
|
||||
return &PriceResponse{
|
||||
Price: cached.Price,
|
||||
AmountOut: amountOut,
|
||||
Source: cached.Source + "_cached",
|
||||
Timestamp: cached.Timestamp,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
// backgroundUpdater runs periodic price updates
|
||||
func (p *PriceOracle) backgroundUpdater() {
|
||||
for {
|
||||
select {
|
||||
case <-p.updateTicker.C:
|
||||
p.updatePriceCache()
|
||||
case <-p.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updatePriceCache updates cached prices for major pairs
|
||||
func (p *PriceOracle) updatePriceCache() {
|
||||
// Update prices for major trading pairs
|
||||
majorPairs := []struct {
|
||||
tokenIn common.Address
|
||||
tokenOut common.Address
|
||||
}{
|
||||
// Add major pairs here based on your configuration
|
||||
}
|
||||
|
||||
for _, pair := range majorPairs {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
req := &PriceRequest{
|
||||
TokenIn: pair.tokenIn,
|
||||
TokenOut: pair.tokenOut,
|
||||
AmountIn: big.NewInt(1e18), // 1 token
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if response, err := p.GetPrice(ctx, req); err == nil {
|
||||
p.cachePrice(req, response)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the price oracle and cleanup resources
|
||||
func (p *PriceOracle) Stop() {
|
||||
if p.updateTicker != nil {
|
||||
p.updateTicker.Stop()
|
||||
}
|
||||
close(p.stopChan)
|
||||
}
|
||||
|
||||
// getChainlinkFeeds returns the mapping of tokens to Chainlink price feeds
|
||||
func getChainlinkFeeds() map[common.Address]common.Address {
|
||||
return map[common.Address]common.Address{
|
||||
// Add Chainlink feed addresses for major tokens
|
||||
// Example: WETH -> ETH/USD feed
|
||||
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"): common.HexToAddress("0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612"), // WETH/USD
|
||||
common.HexToAddress("0xA0b862F60edEf4452F25B4160F177db44DeB6Cf1"): common.HexToAddress("0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3"), // GNO/USD
|
||||
// Add more feeds as needed
|
||||
}
|
||||
}
|
||||
|
||||
// getUniswapPools returns the mapping of token pairs to Uniswap pool addresses
|
||||
func getUniswapPools() map[string]common.Address {
|
||||
return map[string]common.Address{
|
||||
// Add Uniswap V3 pool addresses for major pairs
|
||||
// Format: "token0-token1" -> pool address
|
||||
// Use lowercase hex addresses for consistency
|
||||
// Example: WETH-USDC pool
|
||||
"0x82af49447d8a07e3bd95bd0d56f35241523fbab1-0xa0b862f60edef4452f25b4160f177db44deb6cf1": common.HexToAddress("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"), // WETH-GNO
|
||||
// Add more pools as needed
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user