# AUTOGENERATED! DO NOT EDIT! File to edit: ../src/helpers.ipynb.

# %% auto 0
__all__ = ['ADDRESS_ZERO', 'MAX_UINT256', 'normalize_address', 'chunk', 'amount_to_k_string', 'format_currency',
           'format_percentage', 'amount_to_m_string', 'float_to_uint256', 'get_future_timestamp', 'apply_slippage',
           'Pair', 'find_all_paths', 'Timer', 'time_it', 'atime_it']

# %% ../src/helpers.ipynb 2
from web3 import Web3, constants
from typing import List, Tuple, Optional, Callable
from decimal import Decimal
from datetime import datetime, timedelta
from dataclasses import dataclass
import networkx as nx
import math, time, asyncio
from contextlib import contextmanager, asynccontextmanager

# %% ../src/helpers.ipynb 3
def normalize_address(address: str) -> str: return Web3.to_checksum_address(address.lower())

ADDRESS_ZERO = constants.ADDRESS_ZERO
MAX_UINT256 = Web3.to_int(hexstr='0x' + 'f' * 64)

def chunk(list_to_chunk: List, n: int):
    for i in range(0, len(list_to_chunk), n):
        yield list_to_chunk[i : i + n]


def amount_to_k_string(amount: float) -> str:
    """Turns 2000 to "2K" """
    return f"{round(amount/1000, 2)}K"


def format_currency(value: float, symbol: str = "$", prefix: bool = True) -> str:
    v = "{:0,.2f}".format(value)
    return f"{symbol}{v}" if prefix else f"{v} {symbol}"


def format_percentage(value: float) -> str:
    return "{:0,.2f} %".format(value)


def amount_to_m_string(amount: float) -> str:
    """Turns 2000000 to "2M" """
    return f"{round(amount/1000000, 2)}M"

# %% ../src/helpers.ipynb 4
def float_to_uint256(amount: float, decimals: int = 18) -> int:
    """Convert float to uint256 with decimal scaling"""
    # Convert float to Decimal for precision
    amount_decimal = Decimal(str(amount))
    # Scale by decimals
    scaled_amount = amount_decimal * Decimal(10 ** decimals)
    # Convert to integer
    return int(scaled_amount)

# %% ../src/helpers.ipynb 6
def get_future_timestamp(deadline_minutes: float) -> int:
    """Convert minutes from now to future unix timestamp"""
    future_time = datetime.now() + timedelta(minutes=deadline_minutes)
    return int(future_time.timestamp())

# %% ../src/helpers.ipynb 8
def apply_slippage(amount: int, slippage: float) -> int:
    if slippage < 0 or slippage > 1: raise ValueError("Slippage must be between 0 and 1")
    return int(math.ceil(amount * (1 - slippage)))

# %% ../src/helpers.ipynb 12
# Claude 3.7 sonnet made this

@dataclass
class Pair: token0: str; token1: str; pool: str

def find_all_paths(pairs: List[Pair], start_token: str, end_token: str, cutoff=3) -> List[List[Tuple]]:
    # MultiGraph required to support parallel edges
    # same tokens can be present in different pools, hence parallel edges
    # specific pool identifier is stored inside edge attribute
    G, complete_paths = nx.MultiGraph(), []
    for pair in pairs: G.add_edge(pair.token0, pair.token1, pool=pair.pool)
    node_paths =  [p for p in nx.all_simple_paths(G, source=start_token, target=end_token, cutoff=cutoff)]
    for path in node_paths:
        edge_path = []
        # For each consecutive pair of nodes in the path
        for i in range(len(path) - 1):
            current = path[i]
            next_node = path[i + 1]
            
            # Get all edges between these nodes
            edges = G.get_edge_data(current, next_node)
            
            # There might be multiple edges (pools) between these nodes
            # Add all possible edges to create different complete paths
            current_paths = [] if not edge_path else edge_path.copy()
            new_edge_paths = []
            
            # If this is the first segment, initialize with empty path
            if not current_paths:
                current_paths = [[]]
                
            # For each possible edge between current and next_node
            for edge_key, edge_attrs in edges.items():
                pool = edge_attrs['pool']
                for current_path in current_paths:
                    # Create a new path that includes this edge
                    new_path = current_path + [(current, next_node, pool)]
                    new_edge_paths.append(new_path)
            
            edge_path = new_edge_paths
        
        # Add all possible edge paths to the complete paths
        complete_paths.extend(edge_path)
    
    # seen is a list of strings that look like this ["pool1-pool2-pool3", ...]
    uniques, seen = [], []

    for path in complete_paths:
        p = '-'.join(map(lambda x: x[2], path))
        if p not in seen:
            uniques.append(path)
            seen.append(p)

    # remove duplicates
    return uniques


# %% ../src/helpers.ipynb 15
# Claude 4 sonnet made this

class Timer:
    """Simple timer utility for measuring execution time"""
    
    def __init__(self, name: str = "Operation", precision: int = 4, callback: Optional[Callable] = None):
        self.name = name
        self.precision = precision
        self.callback = callback
        self.start_time: Optional[float] = None
        self.end_time: Optional[float] = None
        self.elapsed: Optional[float] = None
    
    def __enter__(self):
        self.start_time = time.perf_counter()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end_time = time.perf_counter()
        self.elapsed = self.end_time - self.start_time
        result = f"{self.name} took {self.elapsed:.{self.precision}f} seconds"
        
        if self.callback:
            self.callback(self.elapsed, result)
        else:
            print(result)
    
    async def __aenter__(self):
        self.start_time = time.perf_counter()
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        self.end_time = time.perf_counter()
        self.elapsed = self.end_time - self.start_time
        result = f"{self.name} took {self.elapsed:.{self.precision}f} seconds"
        
        if self.callback:
            if asyncio.iscoroutinefunction(self.callback):
                await self.callback(self.elapsed, result)
            else:
                self.callback(self.elapsed, result)
        else:
            print(result)

@contextmanager
def time_it(name: str = "Operation", precision: int = 4, callback: Optional[Callable] = None):
    """Context manager for timing synchronous code execution"""
    timer = Timer(name, precision, callback)
    with timer:
        yield timer

@asynccontextmanager
async def atime_it(name: str = "Operation", precision: int = 4, callback: Optional[Callable] = None):
    """Async context manager for timing asynchronous code execution"""
    timer = Timer(name, precision, callback)
    async with timer:
        yield timer
