diff --git a/bal_addresses/__init__.py b/bal_addresses/__init__.py index 6e2b7ed5..cae3a8b6 100644 --- a/bal_addresses/__init__.py +++ b/bal_addresses/__init__.py @@ -12,5 +12,3 @@ ChecksumError, UnexpectedListLengthError, ) -from .subgraph import Subgraph -from .pools_gauges import BalPoolsGauges diff --git a/bal_addresses/pools_gauges.py b/bal_addresses/pools_gauges.py deleted file mode 100644 index ceb4a69b..00000000 --- a/bal_addresses/pools_gauges.py +++ /dev/null @@ -1,215 +0,0 @@ -from typing import Dict -import json -import requests -from .utils import to_checksum_address - -from bal_addresses.subgraph import Subgraph -from bal_addresses.errors import NoResultError - -GITHUB_RAW_OUTPUTS = ( - "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/main/outputs" -) -GITHUB_RAW_CONFIG = ( - "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/main/config" -) - - -class BalPoolsGauges: - def __init__(self, chain, use_cached_core_pools=True): - self.chain = chain - self.subgraph = Subgraph(self.chain) - if use_cached_core_pools: - self.core_pools = ( - requests.get(f"{GITHUB_RAW_OUTPUTS}/core_pools.json") - .json() - .get(chain, {}) - ) - else: - self.core_pools = self.build_core_pools() - - def is_pool_exempt_from_yield_fee(self, pool_id: str) -> bool: - data = self.subgraph.fetch_graphql_data( - "core", "yield_fee_exempt", {"poolId": pool_id} - ) - for pool in data["poolTokens"]: - address = pool["poolId"]["address"] - if pool["id"].split("-")[-1] == address: - continue - if pool["isExemptFromYieldProtocolFee"] == True: - return True - - def get_bpt_balances(self, pool_id: str, block: int) -> Dict[str, int]: - variables = {"poolId": pool_id, "block": int(block)} - data = self.subgraph.fetch_graphql_data( - "core", "get_user_pool_balances", variables - ) - results = {} - if "pool" in data and data["pool"]: - for share in data["pool"]["shares"]: - user_address = to_checksum_address(share["userAddress"]["id"]) - results[user_address] = float(share["balance"]) - return results - - def get_gauge_deposit_shares( - self, gauge_address: str, block: int - ) -> Dict[str, int]: - gauge_address = to_checksum_address(gauge_address) - variables = {"gaugeAddress": gauge_address, "block": int(block)} - data = self.subgraph.fetch_graphql_data( - self.subgraph.BALANCER_GAUGES_SHARES_QUERY, variables - ) - results = {} - if "data" in data and "gaugeShares" in data["data"]: - for share in data["data"]["gaugeShares"]: - user_address = to_checksum_address(share["user"]["id"]) - results[user_address] = float(share["balance"]) - return results - - def is_core_pool(self, pool_id: str) -> bool: - """ - check if a pool is a core pool using a fresh query to the subgraph - - params: - pool_id: this is the long version of a pool id, so contract address + suffix - - returns: - True if the pool is a core pool - """ - return pool_id in self.core_pools - - def query_preferential_gauges(self, skip=0, step_size=100) -> list: - """ - TODO: add docstring - """ - variables = {"skip": skip, "step_size": step_size} - data = self.subgraph.fetch_graphql_data("gauges", "pref_gauges", variables) - try: - result = data["liquidityGauges"] - except KeyError: - result = [] - if len(result) > 0: - # didnt reach end of results yet, collect next page - result += self.query_preferential_gauges(skip + step_size, step_size) - return result - - def query_root_gauges(self, skip=0, step_size=100) -> list: - variables = {"skip": skip, "step_size": step_size} - data = self.subgraph.fetch_graphql_data("gauges", "root_gauges", variables) - try: - result = data["rootGauges"] - except KeyError: - result = [] - if len(result) > 0: - # didnt reach end of results yet, collect next page - result += self.query_root_gauges(skip + step_size, step_size) - return result - - def get_last_join_exit(self, pool_id: int) -> int: - """ - Returns a timestamp of the last join/exit for a given pool id - """ - data = self.subgraph.fetch_graphql_data( - "core", "last_join_exit", {"poolId": pool_id} - ) - try: - return data["joinExits"][0]["timestamp"] - except: - raise NoResultError( - f"empty or malformed results looking for last join/exit on pool {self.chain}:{pool_id}" - ) - - def get_liquid_pools_with_protocol_yield_fee(self) -> dict: - """ - query the official balancer subgraph and retrieve pools that - meet all three of the following conditions: - - have at least one underlying asset that is yield bearing - - have a liquidity greater than $250k - - provide the protocol with a fee on the yield; by either: - - having a yield fee > 0 - - being a meta stable pool with swap fee > 0 (these old style pools dont have - the yield fee field yet) - - being a gyro pool (take yield fee by default in case of a rate provider) - - returns: - dictionary of the format {pool_id: symbol} - """ - filtered_pools = {} - data = self.subgraph.fetch_graphql_data( - "core", "liquid_pools_protocol_yield_fee" - ) - try: - for pool in data["pools"]: - filtered_pools[pool["id"]] = pool["symbol"] - except KeyError: - # no results for this chain - pass - return filtered_pools - - def has_alive_preferential_gauge(self, pool_id: str) -> bool: - """ - check if a pool has an alive preferential gauge using a fresh query to the subgraph - - params: - - pool_id: id of the pool - - returns: - - True if the pool has a preferential gauge which is not killed - """ - variables = {"pool_id": pool_id} - data = self.subgraph.fetch_graphql_data( - "gauges", "alive_preferential_gauge", variables - ) - try: - result = data["pools"] - except KeyError: - result = [] - if len(result) == 0: - print(f"Pool {pool_id} on {self.chain} has no preferential gauge") - return False - for gauge in result: - if gauge["preferentialGauge"]["isKilled"] == False: - return True - print(f"Pool {pool_id} on {self.chain} has no alive preferential gauge") - - def build_core_pools(self): - """ - build the core pools dictionary by taking pools from `get_pools_with_rate_provider` and: - - check if the pool has an alive preferential gauge - - add pools from whitelist - - remove pools from blacklist - - returns: - dictionary of the format {pool_id: symbol} - """ - core_pools = self.get_liquid_pools_with_protocol_yield_fee() - - # make sure the pools have an alive preferential gauge - for pool_id in core_pools.copy(): - if not self.has_alive_preferential_gauge(pool_id): - del core_pools[pool_id] - - # add pools from whitelist - whitelist = requests.get(f"{GITHUB_RAW_CONFIG}/core_pools_whitelist.json") - whitelist.raise_for_status() - whitelist = whitelist.json() - try: - for pool, symbol in whitelist[self.chain].items(): - if pool not in core_pools: - core_pools[pool] = symbol - except KeyError: - # no results for this chain - pass - - # remove pools from blacklist - blacklist = requests.get(f"{GITHUB_RAW_CONFIG}/core_pools_blacklist.json") - blacklist.raise_for_status() - blacklist = blacklist.json() - try: - for pool in blacklist[self.chain]: - if pool in core_pools: - del core_pools[pool] - except KeyError: - # no results for this chain - pass - - return core_pools diff --git a/bal_addresses/requirements.txt b/bal_addresses/requirements.txt index f126efed..a939e689 100644 --- a/bal_addresses/requirements.txt +++ b/bal_addresses/requirements.txt @@ -1,4 +1,5 @@ pathlib>=1.0 +git+https://github.com/BalancerMaxis/bal_tools@v0.0.1 requests pandas web3 diff --git a/bal_addresses/subgraph.py b/bal_addresses/subgraph.py deleted file mode 100644 index 01e558a6..00000000 --- a/bal_addresses/subgraph.py +++ /dev/null @@ -1,91 +0,0 @@ -from urllib.request import urlopen -import os -from gql import Client, gql -from gql.transport.requests import RequestsHTTPTransport - -from bal_addresses import AddrBook - - -graphql_base_path = f"{os.path.dirname(os.path.abspath(__file__))}/graphql" - - -class Subgraph: - def __init__(self, chain: str): - if chain not in AddrBook.chain_ids_by_name.keys(): - raise ValueError(f"Invalid chain: {chain}") - self.chain = chain - - def get_subgraph_url(self, subgraph="core") -> str: - """ - perform some soup magic to determine the latest subgraph url used in the official frontend - - params: - - subgraph: "core", "gauges" , "blocks" or "aura" - - returns: - - https url of the subgraph - """ - chain = "gnosis-chain" if self.chain == "gnosis" else self.chain - - if subgraph == "core": - magic_word = "subgraph:" - elif subgraph == "gauges": - magic_word = "gauge:" - elif subgraph == "blocks": - magic_word = "blocks:" - elif subgraph == "aura": - if chain == "zkevm": - return "https://subgraph.satsuma-prod.com/ab0804deff79/1xhub-ltd/aura-finance-zkevm/api" - elif chain in ["avalanche"]: # list of chains without an aura subgraph - return None - else: - return ( - f"https://graph.aura.finance/subgraphs/name/aura/aura-{chain}-v2-1" - ) - - # get subgraph url from production frontend - frontend_file = f"https://raw.githubusercontent.com/balancer/frontend-v2/develop/src/lib/config/{chain}/index.ts" - found_magic_word = False - with urlopen(frontend_file) as f: - for line in f: - if found_magic_word: - - url = line.decode("utf-8").strip().strip(" ,'") - return url - if magic_word + " " in str(line): - # url is on same line - return line.decode("utf-8").split(magic_word)[1].strip().strip(",'") - if magic_word in str(line): - # url is on next line, return it on the next iteration - found_magic_word = True - - def fetch_graphql_data(self, subgraph: str, query: str, params: dict = None): - """ - query a subgraph using a locally saved query - - params: - - query: the name of the query (file) to be executed - - params: optional parameters to be passed to the query - - returns: - - result of the query - """ - # build the client - url = self.get_subgraph_url(subgraph) - transport = RequestsHTTPTransport( - url=url, - ) - client = Client(transport=transport, fetch_schema_from_transport=True) - - # retrieve the query from its file and execute it - with open(f"{graphql_base_path}/{subgraph}/{query}.gql") as f: - gql_query = gql(f.read()) - result = client.execute(gql_query, variable_values=params) - - return result - - def get_first_block_after_utc_timestamp(self, timestamp: int) -> int: - data = self.fetch_graphql_data( - "blocks", "first_block_after_ts", {"timestamp": int(timestamp)} - ) - return int(data["blocks"][0]["number"]) diff --git a/gen_core_pools.py b/gen_core_pools.py index 41539faf..9755f5a6 100644 --- a/gen_core_pools.py +++ b/gen_core_pools.py @@ -1,5 +1,5 @@ import json -from bal_addresses.pools_gauges import BalPoolsGauges +from bal_tools import BalPoolsGauges def main(): diff --git a/gen_pools_and_gauges.py b/gen_pools_and_gauges.py index 87c985f7..90e41f8a 100644 --- a/gen_pools_and_gauges.py +++ b/gen_pools_and_gauges.py @@ -3,8 +3,8 @@ import pandas as pd import requests -from bal_addresses.pools_gauges import BalPoolsGauges -from bal_addresses.subgraph import Subgraph +from bal_tools import BalPoolsGauges +from bal_tools import Subgraph NO_GAUGE_SUBGRAPH = ["bsc", "kovan", "fantom", "rinkeby"] diff --git a/gen_subgraph_urls.py b/gen_subgraph_urls.py index f60754b2..a917cff0 100644 --- a/gen_subgraph_urls.py +++ b/gen_subgraph_urls.py @@ -1,8 +1,7 @@ import json import requests - -from bal_addresses.subgraph import Subgraph +from bal_tools.subgraph import Subgraph def main(): diff --git a/generate_current_permissions.py b/generate_current_permissions.py index 0cb81b5f..35fddf61 100644 --- a/generate_current_permissions.py +++ b/generate_current_permissions.py @@ -8,25 +8,28 @@ ALCHEMY_KEY = os.getenv("ALCHEMY_KEY") w3_by_chain = { - "base": Web3( - Web3.HTTPProvider(f"https://base-mainnet.g.alchemy.com/v2/{ALCHEMY_KEY}") - ), "gnosis": Web3(Web3.HTTPProvider(f"https://rpc.gnosischain.com")), "zkevm": Web3(Web3.HTTPProvider(f"https://zkevm-rpc.com")), "avalanche": Web3(Web3.HTTPProvider(f"https://api.avax.network/ext/bc/C/rpc")), - "fantom": Web3(Web3.HTTPProvider("https://rpc.fantom.network")), ### Less reliable RPCs first to fail fast :) - "mainnet": Web3(Web3.HTTPProvider(f"https://mainnet.infura.io/v3/{INFURA_KEY}")), + "mainnet": Web3( + Web3.HTTPProvider(f"https://eth-mainnet.g.alchemy.com/v2/{ALCHEMY_KEY}") + ), + "base": Web3( + Web3.HTTPProvider(f"https://base-mainnet.g.alchemy.com/v2/{ALCHEMY_KEY}") + ), "arbitrum": Web3( - Web3.HTTPProvider(f"https://arbitrum-mainnet.infura.io/v3/{INFURA_KEY}") + Web3.HTTPProvider(f"https://arb-mainnet.g.alchemy.com/v2/{ALCHEMY_KEY}") ), "optimism": Web3( - Web3.HTTPProvider(f"https://optimism-mainnet.infura.io/v3/{INFURA_KEY}") + Web3.HTTPProvider(f"https://opt-mainnet.g.alchemy.com/v2/{ALCHEMY_KEY}") ), "polygon": Web3( - Web3.HTTPProvider(f"https://polygon-mainnet.infura.io/v3/{INFURA_KEY}") + Web3.HTTPProvider(f"https://polygon-mainnet.g.alchemy.com/v2/{ALCHEMY_KEY}") + ), + "sepolia": Web3( + Web3.HTTPProvider(f"https://eth-sepolia.g.alchemy.com/v2/{ALCHEMY_KEY}") ), - "sepolia": Web3(Web3.HTTPProvider(f"https://sepolia.infura.io/v3/{INFURA_KEY}")), } diff --git a/outputs/permissions/active/fantom.json b/outputs/permissions/active/fantom.json index a3c43e56..0967ef42 100644 --- a/outputs/permissions/active/fantom.json +++ b/outputs/permissions/active/fantom.json @@ -1,59 +1 @@ -{ - "0x613346615cf0b67eb4dddb25974f638ea9054ffcba6253356eab0c0d6b2beee5": [ - "0x419F7925b8C9e409B6Ee8792242556fa210A7A09", - "0x43A6C3Cd50776297Baa34d92217E1181BAF9C9B4", - "0xe273ED010295c69b652d93De4390234042065258", - "0x7701711545830ec044a5907f8292950e24B6a011", - "0x0faA25293A36241C214F3760C6FF443e1b731981" - ], - "0x1f49de385af1f787ffdbaa879dd9f69a291a44d65e3209396c0faeaf35165e42": [ - "0x419F7925b8C9e409B6Ee8792242556fa210A7A09", - "0x43A6C3Cd50776297Baa34d92217E1181BAF9C9B4", - "0xe273ED010295c69b652d93De4390234042065258", - "0x7701711545830ec044a5907f8292950e24B6a011", - "0x0faA25293A36241C214F3760C6FF443e1b731981" - ], - "0xe4f58b20b91d2aa26b17bf7b6d206053ef49c007c105db60c6c5b63ba61eb159": [ - "0x419F7925b8C9e409B6Ee8792242556fa210A7A09", - "0x43A6C3Cd50776297Baa34d92217E1181BAF9C9B4", - "0xe273ED010295c69b652d93De4390234042065258", - "0x7701711545830ec044a5907f8292950e24B6a011", - "0x0faA25293A36241C214F3760C6FF443e1b731981" - ], - "0xb9fe74424b9ef83062b3b19b2d4afff4e31d0ecee07c4d44f830f84db1d68415": [ - "0x419F7925b8C9e409B6Ee8792242556fa210A7A09", - "0x43A6C3Cd50776297Baa34d92217E1181BAF9C9B4", - "0xe273ED010295c69b652d93De4390234042065258", - "0x7701711545830ec044a5907f8292950e24B6a011", - "0x0faA25293A36241C214F3760C6FF443e1b731981" - ], - "0xf41701137fd710c221d68f7103fd6a64c3914383f695636a59d588226646c9df": [ - "0x9d0327954009C59eD70Dc98b7726e911879d4D92" - ], - "0x5bc378820edc8261ac3898ec88e8bee0d5f16fb9f6f71435a59f1a8010f8b677": [ - "0x9d0327954009C59eD70Dc98b7726e911879d4D92" - ], - "0xcd0cc5c1132a8d963f443e6d4eace05e0fc29532dae743d2e03a7391a9b25ec1": [ - "0x419F7925b8C9e409B6Ee8792242556fa210A7A09", - "0x43A6C3Cd50776297Baa34d92217E1181BAF9C9B4", - "0x7701711545830ec044a5907f8292950e24B6a011", - "0x0faA25293A36241C214F3760C6FF443e1b731981" - ], - "0x4f471780cf1bf482a126b1bde17518ff8d63bb1b35ec0e7ac2c594d1e62abc25": [ - "0x419F7925b8C9e409B6Ee8792242556fa210A7A09", - "0x43A6C3Cd50776297Baa34d92217E1181BAF9C9B4", - "0xe273ED010295c69b652d93De4390234042065258", - "0x7701711545830ec044a5907f8292950e24B6a011", - "0x0faA25293A36241C214F3760C6FF443e1b731981" - ], - "0xdf9dc03a6ab1cf43cd556a9e3f5f91e7e6fe495dd7ae950689bf294fdbf699ed": [ - "0x9d0327954009C59eD70Dc98b7726e911879d4D92" - ], - "0xaa71aa88cd3944c17523ff0a64c6e533d8066ea755c4539194cb741182cb9ecc": [ - "0x9d0327954009C59eD70Dc98b7726e911879d4D92" - ], - "0x91a484dd2c010cdae861f168371dad11bcb6432aaf363ba69dc6d2abf56c7c20": [ - "0x9d0327954009C59eD70Dc98b7726e911879d4D92", - "0xA09BC385421f18D5d5072924f9d3709bB2B76281" - ] -} \ No newline at end of file +{} diff --git a/setup.py b/setup.py index fb97a9d6..8ed48cba 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -VERSION = "0.9.1" +VERSION = "0.9.3" DESCRIPTION = "Balancer Maxi Addressbook" LONG_DESCRIPTION = "Balancer Maxi Addressbook and Balancer Permissions helper" @@ -14,7 +14,7 @@ long_description=LONG_DESCRIPTION, packages=find_packages(), include_package_data=True, # Automatically include non-Python files - package_data={"": ["graphql/**/*.gql", "abis/*.json"]}, + package_data={"": ["abis/*.json"]}, url="https://github.com/BalancerMaxis/bal_addresses", install_requires=[ "setuptools>=42", @@ -23,6 +23,7 @@ "web3", "gql[requests]", "requests", + "bal_tools @ git+https://github.com/BalancerMaxis/bal_tools@v0.0.1", ], keywords=["python", "first package"], classifiers=[ diff --git a/tests/conftest.py b/tests/conftest.py index a8875365..dc295969 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,23 +1,9 @@ import pytest from bal_addresses import AddrBook -from bal_addresses.pools_gauges import BalPoolsGauges -from bal_addresses.subgraph import Subgraph @pytest.fixture(scope="module", params=list(AddrBook.chains["CHAIN_IDS_BY_NAME"])) def chain(request): chain = request.param return chain - - -@pytest.fixture(scope="module") -def bal_pools_gauges(chain): - if chain == "fantom": - pytest.skip("Skipping Fantom, no pools/gauges") - return BalPoolsGauges(chain) - - -@pytest.fixture(scope="module") -def subgraph(chain): - return Subgraph(chain) diff --git a/tests/test_pools_gauges.py b/tests/test_pools_gauges.py deleted file mode 100644 index ae517e54..00000000 --- a/tests/test_pools_gauges.py +++ /dev/null @@ -1,91 +0,0 @@ -import pytest - -EXAMPLE_PREFERENTIAL_GAUGES = { - "mainnet": "0x93d199263632a4ef4bb438f1feb99e57b4b5f0bd0000000000000000000005c2", # wsteTH-WETH -} -EXAMPLE_CORE_POOLS = { - "mainnet": "0x93d199263632a4ef4bb438f1feb99e57b4b5f0bd0000000000000000000005c2", # wsteTH-WETH - "polygon": "0xcd78a20c597e367a4e478a2411ceb790604d7c8f000000000000000000000c22", # maticX-WMATIC-BPT - "arbitrum": "0x0c8972437a38b389ec83d1e666b69b8a4fcf8bfd00000000000000000000049e", # wstETH/rETH/sfrxETH - "base": "0xc771c1a5905420daec317b154eb13e4198ba97d0000000000000000000000023", # "rETH-WETH-BPT" -} -EXAMPLE_YIELD_FEE_EXEMPT_TRUE = { - "gnosis": "0xdd439304a77f54b1f7854751ac1169b279591ef7000000000000000000000064" -} - - -def test_has_alive_preferential_gauge(bal_pools_gauges): - """ - confirm example alive preferential gauge can be found - """ - try: - example = EXAMPLE_PREFERENTIAL_GAUGES[bal_pools_gauges.chain] - except KeyError: - pytest.skip(f"Skipping {bal_pools_gauges.chain}, no example preferential gauge") - - assert bal_pools_gauges.has_alive_preferential_gauge(example) - - -def test_core_pools_dict(bal_pools_gauges): - """ - confirm we get a dict back with entries - """ - core_pools = bal_pools_gauges.core_pools - assert isinstance(core_pools, dict) - - -def test_core_pools_attr(bal_pools_gauges): - """ - confirm example core pool is in the dict - """ - core_pools = bal_pools_gauges.core_pools - - try: - example = EXAMPLE_CORE_POOLS[bal_pools_gauges.chain] - except KeyError: - pytest.skip(f"Skipping {bal_pools_gauges.chain}, no example core pools") - - assert example in core_pools - - -def test_is_core_pool(bal_pools_gauges): - """ - confirm example core pool is present - """ - try: - example = EXAMPLE_CORE_POOLS[bal_pools_gauges.chain] - except KeyError: - pytest.skip(f"Skipping {bal_pools_gauges.chain}, no example core pools") - - assert bal_pools_gauges.is_core_pool(example) - - -def test_is_core_pool_false(bal_pools_gauges): - """ - confirm spoofed core pool is not present - """ - with pytest.raises(AssertionError): - assert bal_pools_gauges.is_core_pool( - "0x0000000000000000000000000000000000000000000000000000000000000000" - ) - - -def test_is_pool_exempt_from_yield_fee(bal_pools_gauges): - """ - confirm example pool is exempt from yield fee - """ - try: - example = EXAMPLE_YIELD_FEE_EXEMPT_TRUE[bal_pools_gauges.chain] - except KeyError: - pytest.skip( - f"Skipping {bal_pools_gauges.chain}, no example yield fee exempt pool" - ) - - assert bal_pools_gauges.is_pool_exempt_from_yield_fee(example) - - -def test_build_core_pools(bal_pools_gauges): - """ - confirm core_pools can be built and is a dict - """ - assert isinstance(bal_pools_gauges.build_core_pools(), dict) diff --git a/tests/test_subgraph.py b/tests/test_subgraph.py deleted file mode 100644 index a3add29d..00000000 --- a/tests/test_subgraph.py +++ /dev/null @@ -1,24 +0,0 @@ -import pytest - -from bal_addresses.subgraph import Subgraph - - -def test_get_first_block_after_utc_timestamp(chain, subgraph): - """ - confirm we get the correct block number back - """ - if chain == "mainnet": - block = subgraph.get_first_block_after_utc_timestamp(1708607101) - assert isinstance(block, int) - assert block == 19283331 - else: - pytest.skip(f"Skipping {chain}") - - -def test_invalid_chain(): - """ - we should get a raise when passing an invalid chain - """ - - with pytest.raises(ValueError): - Subgraph("invalid_chain")