package discovery import ( "context" "fmt" "math/big" "strings" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "coppertone.tech/fraktal/mev-bot/pkg/cache" "coppertone.tech/fraktal/mev-bot/pkg/types" ) // UniswapV3FactoryAddress is the Uniswap V3 factory on Arbitrum var UniswapV3FactoryAddress = common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984") // V3 fee tiers (in hundredths of a bip, i.e., 500 = 0.05%) var V3FeeTiers = []uint32{100, 500, 3000, 10000} // UniswapV3PoolDiscovery discovers and populates UniswapV3 pools type UniswapV3PoolDiscovery struct { client *ethclient.Client poolCache cache.PoolCache factoryAddr common.Address factoryABI abi.ABI poolABI abi.ABI } // NewUniswapV3PoolDiscovery creates a new V3 pool discovery instance func NewUniswapV3PoolDiscovery(client *ethclient.Client, poolCache cache.PoolCache) (*UniswapV3PoolDiscovery, error) { if client == nil { return nil, fmt.Errorf("client cannot be nil") } if poolCache == nil { return nil, fmt.Errorf("pool cache cannot be nil") } // Define minimal factory ABI for getPool function factoryABI, err := abi.JSON(strings.NewReader(`[{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint24","name":"","type":"uint24"}],"name":"getPool","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]`)) if err != nil { return nil, fmt.Errorf("failed to parse factory ABI: %w", err) } // Define minimal pool ABI poolABI, err := abi.JSON(strings.NewReader(`[ {"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"}, {"inputs":[],"name":"fee","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"}, {"inputs":[],"name":"tickSpacing","outputs":[{"internalType":"int24","name":"","type":"int24"}],"stateMutability":"view","type":"function"}, {"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"} ]`)) if err != nil { return nil, fmt.Errorf("failed to parse pool ABI: %w", err) } return &UniswapV3PoolDiscovery{ client: client, poolCache: poolCache, factoryAddr: UniswapV3FactoryAddress, factoryABI: factoryABI, poolABI: poolABI, }, nil } // DiscoverMajorPools discovers V3 pools for major token pairs across all fee tiers func (d *UniswapV3PoolDiscovery) DiscoverMajorPools(ctx context.Context) (int, error) { discovered := 0 for _, pair := range MajorTokenPairs { token0 := pair[0] token1 := pair[1] // Try all fee tiers for _, fee := range V3FeeTiers { poolAddr, err := d.getPoolAddress(ctx, token0, token1, fee) if err != nil { // Skip silently - pool may not exist for this fee tier continue } if poolAddr == (common.Address{}) { continue } poolInfo, err := d.fetchPoolInfo(ctx, poolAddr) if err != nil { // Pool exists but failed to fetch info - skip continue } // Skip pools with zero liquidity if poolInfo.Liquidity == nil || poolInfo.Liquidity.Cmp(big.NewInt(0)) == 0 { continue } if err := d.poolCache.Add(ctx, poolInfo); err != nil { // Already exists - not an error continue } discovered++ } } return discovered, nil } // getPoolAddress queries the V3 factory for a pool address func (d *UniswapV3PoolDiscovery) getPoolAddress(ctx context.Context, token0, token1 common.Address, fee uint32) (common.Address, error) { // ABI expects *big.Int for uint24 feeBig := big.NewInt(int64(fee)) data, err := d.factoryABI.Pack("getPool", token0, token1, feeBig) if err != nil { return common.Address{}, err } msg := ethereum.CallMsg{ To: &d.factoryAddr, Data: data, } result, err := d.client.CallContract(ctx, msg, nil) if err != nil { return common.Address{}, err } var poolAddr common.Address err = d.factoryABI.UnpackIntoInterface(&poolAddr, "getPool", result) if err != nil { return common.Address{}, err } return poolAddr, nil } // fetchPoolInfo fetches detailed information about a V3 pool func (d *UniswapV3PoolDiscovery) fetchPoolInfo(ctx context.Context, poolAddr common.Address) (*types.PoolInfo, error) { // Fetch token0 token0Data, _ := d.poolABI.Pack("token0") token0Result, err := d.client.CallContract(ctx, ethereum.CallMsg{To: &poolAddr, Data: token0Data}, nil) if err != nil { return nil, err } var token0 common.Address d.poolABI.UnpackIntoInterface(&token0, "token0", token0Result) // Fetch token1 token1Data, _ := d.poolABI.Pack("token1") token1Result, err := d.client.CallContract(ctx, ethereum.CallMsg{To: &poolAddr, Data: token1Data}, nil) if err != nil { return nil, err } var token1 common.Address d.poolABI.UnpackIntoInterface(&token1, "token1", token1Result) // Fetch fee feeData, _ := d.poolABI.Pack("fee") feeResult, err := d.client.CallContract(ctx, ethereum.CallMsg{To: &poolAddr, Data: feeData}, nil) if err != nil { return nil, err } var fee struct { Fee uint32 } d.poolABI.UnpackIntoInterface(&fee, "fee", feeResult) // Fetch tickSpacing tsData, _ := d.poolABI.Pack("tickSpacing") tsResult, err := d.client.CallContract(ctx, ethereum.CallMsg{To: &poolAddr, Data: tsData}, nil) if err != nil { return nil, err } var tickSpacing struct { Spacing int32 } d.poolABI.UnpackIntoInterface(&tickSpacing, "tickSpacing", tsResult) // Fetch slot0 (sqrtPriceX96, tick) slot0Data, _ := d.poolABI.Pack("slot0") slot0Result, err := d.client.CallContract(ctx, ethereum.CallMsg{To: &poolAddr, Data: slot0Data}, nil) if err != nil { return nil, err } var slot0 struct { SqrtPriceX96 *big.Int Tick int32 ObservationIndex uint16 ObservationCardinality uint16 ObservationCardinalityNext uint16 FeeProtocol uint8 Unlocked bool } d.poolABI.UnpackIntoInterface(&slot0, "slot0", slot0Result) // Fetch liquidity liqData, _ := d.poolABI.Pack("liquidity") liqResult, err := d.client.CallContract(ctx, ethereum.CallMsg{To: &poolAddr, Data: liqData}, nil) if err != nil { return nil, err } var liq struct { Liquidity *big.Int } d.poolABI.UnpackIntoInterface(&liq, "liquidity", liqResult) // Get decimals token0Decimals := getTokenDecimals(token0) token1Decimals := getTokenDecimals(token1) tick := slot0.Tick poolInfo := &types.PoolInfo{ Address: poolAddr, Protocol: types.ProtocolUniswapV3, Token0: token0, Token1: token1, Token0Decimals: token0Decimals, Token1Decimals: token1Decimals, Fee: fee.Fee, TickSpacing: tickSpacing.Spacing, SqrtPriceX96: slot0.SqrtPriceX96, Tick: &tick, Liquidity: liq.Liquidity, } return poolInfo, nil }