uniswap_viewer.viewer_async
viewer_async.py - Asynchronous interface for accessing Uniswap V3 pool data.
This module provides an async viewer for interacting with Uniswap V3 pools using asynchronous Web3 providers (e.g., Web3 AsyncMiddleware). It allows retrieving token prices, tick data, and computing tick-related values.
Main components:
Viewer: an async class for accessing Uniswap V3 pools.stream_new_blocks: async generator for tracking new block numbers.get_decimals: async utility to fetch token decimals from an ERC-20 token.
1""" 2viewer_async.py - Asynchronous interface for accessing Uniswap V3 pool data. 3 4This module provides an async viewer for interacting with Uniswap V3 pools 5using asynchronous Web3 providers (e.g., Web3 AsyncMiddleware). It allows 6retrieving token prices, tick data, and computing tick-related values. 7 8Main components: 9- `Viewer`: an async class for accessing Uniswap V3 pools. 10- `stream_new_blocks`: async generator for tracking new block numbers. 11- `get_decimals`: async utility to fetch token decimals from an ERC-20 token. 12""" 13 14import math 15import asyncio 16import weakref 17from typing import Optional, Dict, Any 18from collections.abc import Generator 19 20from web3 import AsyncWeb3 21 22from .utils import get_abi, TICKS_KEYS, prepare_token_address 23 24 25class Viewer: 26 """ 27 Asynchronous Uniswap V3 pool viewer. 28 29 This class allows querying price and tick information from a Uniswap V3 pool 30 in an async context. 31 """ 32 33 def __init__(self, w3: AsyncWeb3, token0: str, token1: str, 34 fee: int = 3000): 35 """ 36 Initializes the Viewer instance. 37 38 Args: 39 w3: Async-enabled Web3 instance. 40 token0 (str): First token address (must be > token1 bitwise). 41 token1 (str): Second token address. 42 fee (int, optional): Pool fee (bps). One of 100, 500, 3000, 10000. 43 Defaults to 3000 (0.3%). 44 45 Raises: 46 AssertionError: If invalid fee or token ordering. 47 """ 48 # Prepare arguments 49 token0 = prepare_token_address(token0) 50 token1 = prepare_token_address(token1) 51 52 # Check arguments 53 assert fee in (100, 500, 3000, 10000), \ 54 "Allowed fee must be in (100, 500, 3000, 10000) " \ 55 "that corresponds to 0.01%, 0.05%, 0.3%, 1%." 56 57 assert int(token0, 16) > int(token1, 16), \ 58 "token0 must go after token1 in bit representation." 59 60 # Attributes 61 self._w3 = weakref.ref(w3) 62 self._token0 = self._w3().to_checksum_address(token0) 63 self._token1 = self._w3().to_checksum_address(token1) 64 self._fee = fee 65 self._decimals0 = None 66 self._decimals1 = None 67 self._pool_address = None 68 self._pool_contract = None 69 70 async def init(self): 71 """ 72 Initializes decimals and resolves the pool address and contract. 73 74 This method must be awaited before calling other methods that 75 depend on pool context (e.g., price, tick queries). 76 77 Raises: 78 AssertionError: If pool is not found on-chain. 79 """ 80 self._decimals0 = await get_decimals(self._w3(), self._token0) 81 self._decimals1 = await get_decimals(self._w3(), self._token1) 82 self._pool_address = await self._get_pool_address() 83 self._pool_contract = self._w3().eth.contract( 84 address=self._pool_address, abi=get_abi('pool-abi') 85 ) 86 87 async def get_price(self, block_num: Optional[int] = None) -> float: 88 """ 89 Asynchronously fetches the pool price as a float. 90 91 Args: 92 block_num (int, optional): Specific block number, or latest if None. 93 94 Returns: 95 float: Price of token0 in terms of token1. 96 """ 97 self._check_initialized() 98 slot0 = await self._pool_contract.functions.slot0().call( 99 block_identifier=block_num 100 ) 101 sqrtPriceX96 = slot0[0] 102 price = (sqrtPriceX96 / (2**96)) ** 2 103 price *= 10 ** (self._decimals1 - self._decimals0) 104 return price 105 106 async def get_tick_data(self, tick: int, 107 block_num: Optional[int] = None) -> Dict[str, Any]: 108 """ 109 Retrieves detailed tick information from the pool. 110 111 Args: 112 tick (int): The tick index. 113 block_num (int, optional): Block to query from. Defaults to latest. 114 115 Returns: 116 dict: Tick data fields mapped by standard TICKS_KEYS. 117 """ 118 self._check_initialized() 119 self._check_available_tick(tick) 120 tick_data = await self._pool_contract.functions.ticks(tick).call( 121 block_identifier=block_num 122 ) 123 return dict(zip(TICKS_KEYS, tick_data)) 124 125 def calc_tick(self, price: float, normalize: bool = False) -> int: 126 """ 127 Calculates the tick index corresponding to a given price. 128 129 Args: 130 price (float): Price of token0 in token1 units. 131 normalize (bool): Apply `tick_slot` in the end to get the tick slot. 132 Defaults to False. 133 134 Returns: 135 int: Closest tick index. 136 """ 137 price *= 10 ** (self._decimals0 - self._decimals1) 138 tick = math.floor(math.log(price) / math.log(1.0001)) 139 if normalize: 140 tick = self.tick_slot(tick) 141 return tick 142 143 def tick_spacing(self) -> int: 144 """ 145 Returns the tick spacing associated with the pool's fee tier. 146 147 Returns: 148 int: Tick spacing (1, 10, 60, or 200). 149 """ 150 return { 151 100: 1, 152 500: 10, 153 3000: 60, 154 10000: 200, 155 }[self._fee] 156 157 def tick_slot(self, tick: int) -> int: 158 """ 159 Calculates the tick slot for a given tick index. 160 161 Args: 162 tick (int): Tick index. 163 164 Returns: 165 int: Lower-bound of the spacing interval containing the tick. 166 """ 167 return tick - tick % self.tick_spacing() 168 169 async def _get_pool_address(self): 170 factory_contract = self._w3().eth.contract( 171 address='0x1F98431c8aD98523631AE4a59f267346ea31F984', 172 abi=get_abi('factory-abi'), 173 ) 174 pool_address = await factory_contract.functions \ 175 .getPool(self._token1, self._token0, self._fee).call() 176 assert pool_address != "0x0000000000000000000000000000000000000000", \ 177 "Could not find pool for the tokens." 178 return pool_address 179 180 def _check_initialized(self): 181 assert self._pool_contract is not None, \ 182 "Instance is not initialized, " \ 183 "run `await instance.init()` right after creation." 184 185 def _check_available_tick(self, tick): 186 spacing = self.tick_spacing() 187 assert tick % spacing == 0, \ 188 f"Tick must be multiple of {spacing} due to fee = {self._fee}, " \ 189 f"{tick} given. Please, use `tick = viewer.tick_slot(tick)` or " \ 190 f"`tick = viewer.calc_tick(price, normalize=True)` to normalize." 191 192 193async def stream_new_blocks(w3: AsyncWeb3, timeout: int = 1) -> Generator[int]: 194 """ 195 Asynchronously yields new block numbers as they appear on-chain. 196 197 Polls the chain periodically and yields any new blocks that have appeared 198 since the last poll. 199 200 Args: 201 w3: Async Web3 instance. 202 timeout (int): Poll interval in seconds. Defaults to 1. 203 204 Yields: 205 int: New block numbers in sequence. 206 """ 207 block_num_prev = None 208 while True: 209 block_num = await w3.eth.block_number 210 if block_num_prev is None: 211 yield block_num 212 else: 213 for num in range(block_num_prev + 1, block_num + 1): 214 yield num 215 block_num_prev = block_num 216 await asyncio.sleep(timeout) 217 218 219async def get_decimals(w3: AsyncWeb3, token: str) -> int: 220 """ 221 Asynchronously fetches the number of decimals of an ERC-20 token. 222 223 Args: 224 w3: Async Web3 instance. 225 token (str): Ethereum address of the ERC-20 token. 226 227 Returns: 228 int: Number of decimals. 229 """ 230 token_contract = w3.eth.contract(address=token, abi=get_abi('erc20-abi')) 231 decimals = await token_contract.functions.decimals().call() 232 return decimals
26class Viewer: 27 """ 28 Asynchronous Uniswap V3 pool viewer. 29 30 This class allows querying price and tick information from a Uniswap V3 pool 31 in an async context. 32 """ 33 34 def __init__(self, w3: AsyncWeb3, token0: str, token1: str, 35 fee: int = 3000): 36 """ 37 Initializes the Viewer instance. 38 39 Args: 40 w3: Async-enabled Web3 instance. 41 token0 (str): First token address (must be > token1 bitwise). 42 token1 (str): Second token address. 43 fee (int, optional): Pool fee (bps). One of 100, 500, 3000, 10000. 44 Defaults to 3000 (0.3%). 45 46 Raises: 47 AssertionError: If invalid fee or token ordering. 48 """ 49 # Prepare arguments 50 token0 = prepare_token_address(token0) 51 token1 = prepare_token_address(token1) 52 53 # Check arguments 54 assert fee in (100, 500, 3000, 10000), \ 55 "Allowed fee must be in (100, 500, 3000, 10000) " \ 56 "that corresponds to 0.01%, 0.05%, 0.3%, 1%." 57 58 assert int(token0, 16) > int(token1, 16), \ 59 "token0 must go after token1 in bit representation." 60 61 # Attributes 62 self._w3 = weakref.ref(w3) 63 self._token0 = self._w3().to_checksum_address(token0) 64 self._token1 = self._w3().to_checksum_address(token1) 65 self._fee = fee 66 self._decimals0 = None 67 self._decimals1 = None 68 self._pool_address = None 69 self._pool_contract = None 70 71 async def init(self): 72 """ 73 Initializes decimals and resolves the pool address and contract. 74 75 This method must be awaited before calling other methods that 76 depend on pool context (e.g., price, tick queries). 77 78 Raises: 79 AssertionError: If pool is not found on-chain. 80 """ 81 self._decimals0 = await get_decimals(self._w3(), self._token0) 82 self._decimals1 = await get_decimals(self._w3(), self._token1) 83 self._pool_address = await self._get_pool_address() 84 self._pool_contract = self._w3().eth.contract( 85 address=self._pool_address, abi=get_abi('pool-abi') 86 ) 87 88 async def get_price(self, block_num: Optional[int] = None) -> float: 89 """ 90 Asynchronously fetches the pool price as a float. 91 92 Args: 93 block_num (int, optional): Specific block number, or latest if None. 94 95 Returns: 96 float: Price of token0 in terms of token1. 97 """ 98 self._check_initialized() 99 slot0 = await self._pool_contract.functions.slot0().call( 100 block_identifier=block_num 101 ) 102 sqrtPriceX96 = slot0[0] 103 price = (sqrtPriceX96 / (2**96)) ** 2 104 price *= 10 ** (self._decimals1 - self._decimals0) 105 return price 106 107 async def get_tick_data(self, tick: int, 108 block_num: Optional[int] = None) -> Dict[str, Any]: 109 """ 110 Retrieves detailed tick information from the pool. 111 112 Args: 113 tick (int): The tick index. 114 block_num (int, optional): Block to query from. Defaults to latest. 115 116 Returns: 117 dict: Tick data fields mapped by standard TICKS_KEYS. 118 """ 119 self._check_initialized() 120 self._check_available_tick(tick) 121 tick_data = await self._pool_contract.functions.ticks(tick).call( 122 block_identifier=block_num 123 ) 124 return dict(zip(TICKS_KEYS, tick_data)) 125 126 def calc_tick(self, price: float, normalize: bool = False) -> int: 127 """ 128 Calculates the tick index corresponding to a given price. 129 130 Args: 131 price (float): Price of token0 in token1 units. 132 normalize (bool): Apply `tick_slot` in the end to get the tick slot. 133 Defaults to False. 134 135 Returns: 136 int: Closest tick index. 137 """ 138 price *= 10 ** (self._decimals0 - self._decimals1) 139 tick = math.floor(math.log(price) / math.log(1.0001)) 140 if normalize: 141 tick = self.tick_slot(tick) 142 return tick 143 144 def tick_spacing(self) -> int: 145 """ 146 Returns the tick spacing associated with the pool's fee tier. 147 148 Returns: 149 int: Tick spacing (1, 10, 60, or 200). 150 """ 151 return { 152 100: 1, 153 500: 10, 154 3000: 60, 155 10000: 200, 156 }[self._fee] 157 158 def tick_slot(self, tick: int) -> int: 159 """ 160 Calculates the tick slot for a given tick index. 161 162 Args: 163 tick (int): Tick index. 164 165 Returns: 166 int: Lower-bound of the spacing interval containing the tick. 167 """ 168 return tick - tick % self.tick_spacing() 169 170 async def _get_pool_address(self): 171 factory_contract = self._w3().eth.contract( 172 address='0x1F98431c8aD98523631AE4a59f267346ea31F984', 173 abi=get_abi('factory-abi'), 174 ) 175 pool_address = await factory_contract.functions \ 176 .getPool(self._token1, self._token0, self._fee).call() 177 assert pool_address != "0x0000000000000000000000000000000000000000", \ 178 "Could not find pool for the tokens." 179 return pool_address 180 181 def _check_initialized(self): 182 assert self._pool_contract is not None, \ 183 "Instance is not initialized, " \ 184 "run `await instance.init()` right after creation." 185 186 def _check_available_tick(self, tick): 187 spacing = self.tick_spacing() 188 assert tick % spacing == 0, \ 189 f"Tick must be multiple of {spacing} due to fee = {self._fee}, " \ 190 f"{tick} given. Please, use `tick = viewer.tick_slot(tick)` or " \ 191 f"`tick = viewer.calc_tick(price, normalize=True)` to normalize."
Asynchronous Uniswap V3 pool viewer.
This class allows querying price and tick information from a Uniswap V3 pool in an async context.
34 def __init__(self, w3: AsyncWeb3, token0: str, token1: str, 35 fee: int = 3000): 36 """ 37 Initializes the Viewer instance. 38 39 Args: 40 w3: Async-enabled Web3 instance. 41 token0 (str): First token address (must be > token1 bitwise). 42 token1 (str): Second token address. 43 fee (int, optional): Pool fee (bps). One of 100, 500, 3000, 10000. 44 Defaults to 3000 (0.3%). 45 46 Raises: 47 AssertionError: If invalid fee or token ordering. 48 """ 49 # Prepare arguments 50 token0 = prepare_token_address(token0) 51 token1 = prepare_token_address(token1) 52 53 # Check arguments 54 assert fee in (100, 500, 3000, 10000), \ 55 "Allowed fee must be in (100, 500, 3000, 10000) " \ 56 "that corresponds to 0.01%, 0.05%, 0.3%, 1%." 57 58 assert int(token0, 16) > int(token1, 16), \ 59 "token0 must go after token1 in bit representation." 60 61 # Attributes 62 self._w3 = weakref.ref(w3) 63 self._token0 = self._w3().to_checksum_address(token0) 64 self._token1 = self._w3().to_checksum_address(token1) 65 self._fee = fee 66 self._decimals0 = None 67 self._decimals1 = None 68 self._pool_address = None 69 self._pool_contract = None
Initializes the Viewer instance.
Args: w3: Async-enabled Web3 instance. token0 (str): First token address (must be > token1 bitwise). token1 (str): Second token address. fee (int, optional): Pool fee (bps). One of 100, 500, 3000, 10000. Defaults to 3000 (0.3%).
Raises: AssertionError: If invalid fee or token ordering.
71 async def init(self): 72 """ 73 Initializes decimals and resolves the pool address and contract. 74 75 This method must be awaited before calling other methods that 76 depend on pool context (e.g., price, tick queries). 77 78 Raises: 79 AssertionError: If pool is not found on-chain. 80 """ 81 self._decimals0 = await get_decimals(self._w3(), self._token0) 82 self._decimals1 = await get_decimals(self._w3(), self._token1) 83 self._pool_address = await self._get_pool_address() 84 self._pool_contract = self._w3().eth.contract( 85 address=self._pool_address, abi=get_abi('pool-abi') 86 )
Initializes decimals and resolves the pool address and contract.
This method must be awaited before calling other methods that depend on pool context (e.g., price, tick queries).
Raises: AssertionError: If pool is not found on-chain.
88 async def get_price(self, block_num: Optional[int] = None) -> float: 89 """ 90 Asynchronously fetches the pool price as a float. 91 92 Args: 93 block_num (int, optional): Specific block number, or latest if None. 94 95 Returns: 96 float: Price of token0 in terms of token1. 97 """ 98 self._check_initialized() 99 slot0 = await self._pool_contract.functions.slot0().call( 100 block_identifier=block_num 101 ) 102 sqrtPriceX96 = slot0[0] 103 price = (sqrtPriceX96 / (2**96)) ** 2 104 price *= 10 ** (self._decimals1 - self._decimals0) 105 return price
Asynchronously fetches the pool price as a float.
Args: block_num (int, optional): Specific block number, or latest if None.
Returns: float: Price of token0 in terms of token1.
107 async def get_tick_data(self, tick: int, 108 block_num: Optional[int] = None) -> Dict[str, Any]: 109 """ 110 Retrieves detailed tick information from the pool. 111 112 Args: 113 tick (int): The tick index. 114 block_num (int, optional): Block to query from. Defaults to latest. 115 116 Returns: 117 dict: Tick data fields mapped by standard TICKS_KEYS. 118 """ 119 self._check_initialized() 120 self._check_available_tick(tick) 121 tick_data = await self._pool_contract.functions.ticks(tick).call( 122 block_identifier=block_num 123 ) 124 return dict(zip(TICKS_KEYS, tick_data))
Retrieves detailed tick information from the pool.
Args: tick (int): The tick index. block_num (int, optional): Block to query from. Defaults to latest.
Returns: dict: Tick data fields mapped by standard TICKS_KEYS.
126 def calc_tick(self, price: float, normalize: bool = False) -> int: 127 """ 128 Calculates the tick index corresponding to a given price. 129 130 Args: 131 price (float): Price of token0 in token1 units. 132 normalize (bool): Apply `tick_slot` in the end to get the tick slot. 133 Defaults to False. 134 135 Returns: 136 int: Closest tick index. 137 """ 138 price *= 10 ** (self._decimals0 - self._decimals1) 139 tick = math.floor(math.log(price) / math.log(1.0001)) 140 if normalize: 141 tick = self.tick_slot(tick) 142 return tick
Calculates the tick index corresponding to a given price.
Args:
price (float): Price of token0 in token1 units.
normalize (bool): Apply tick_slot in the end to get the tick slot.
Defaults to False.
Returns: int: Closest tick index.
144 def tick_spacing(self) -> int: 145 """ 146 Returns the tick spacing associated with the pool's fee tier. 147 148 Returns: 149 int: Tick spacing (1, 10, 60, or 200). 150 """ 151 return { 152 100: 1, 153 500: 10, 154 3000: 60, 155 10000: 200, 156 }[self._fee]
Returns the tick spacing associated with the pool's fee tier.
Returns: int: Tick spacing (1, 10, 60, or 200).
158 def tick_slot(self, tick: int) -> int: 159 """ 160 Calculates the tick slot for a given tick index. 161 162 Args: 163 tick (int): Tick index. 164 165 Returns: 166 int: Lower-bound of the spacing interval containing the tick. 167 """ 168 return tick - tick % self.tick_spacing()
Calculates the tick slot for a given tick index.
Args: tick (int): Tick index.
Returns: int: Lower-bound of the spacing interval containing the tick.
194async def stream_new_blocks(w3: AsyncWeb3, timeout: int = 1) -> Generator[int]: 195 """ 196 Asynchronously yields new block numbers as they appear on-chain. 197 198 Polls the chain periodically and yields any new blocks that have appeared 199 since the last poll. 200 201 Args: 202 w3: Async Web3 instance. 203 timeout (int): Poll interval in seconds. Defaults to 1. 204 205 Yields: 206 int: New block numbers in sequence. 207 """ 208 block_num_prev = None 209 while True: 210 block_num = await w3.eth.block_number 211 if block_num_prev is None: 212 yield block_num 213 else: 214 for num in range(block_num_prev + 1, block_num + 1): 215 yield num 216 block_num_prev = block_num 217 await asyncio.sleep(timeout)
Asynchronously yields new block numbers as they appear on-chain.
Polls the chain periodically and yields any new blocks that have appeared since the last poll.
Args: w3: Async Web3 instance. timeout (int): Poll interval in seconds. Defaults to 1.
Yields: int: New block numbers in sequence.
220async def get_decimals(w3: AsyncWeb3, token: str) -> int: 221 """ 222 Asynchronously fetches the number of decimals of an ERC-20 token. 223 224 Args: 225 w3: Async Web3 instance. 226 token (str): Ethereum address of the ERC-20 token. 227 228 Returns: 229 int: Number of decimals. 230 """ 231 token_contract = w3.eth.contract(address=token, abi=get_abi('erc20-abi')) 232 decimals = await token_contract.functions.decimals().call() 233 return decimals
Asynchronously fetches the number of decimals of an ERC-20 token.
Args: w3: Async Web3 instance. token (str): Ethereum address of the ERC-20 token.
Returns: int: Number of decimals.