diff --git a/CHANGELOG.md b/CHANGELOG.md index f95d59a3..599da393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.8.0 +Added extra Munch based explorable dotmaps + permissions +Restructured function returns to be more consistent. # 0.6.0 Use new balancer deployments repo. # 0.4.0 diff --git a/Makefile b/Makefile index 4dd849d0..088944a0 100644 --- a/Makefile +++ b/Makefile @@ -4,4 +4,4 @@ init: pip install -r bal_addresses/requirements.txt pip install -r bal_addresses/requirements-dev.txt ci: - pytest + pytest diff --git a/bal_addresses/__init__.py b/bal_addresses/__init__.py index 07cc4ddf..ddf2fe87 100644 --- a/bal_addresses/__init__.py +++ b/bal_addresses/__init__.py @@ -1,2 +1,3 @@ -from .addresses import AddrBook -from .permissions import BalPermissions \ No newline at end of file +from .addresses import AddrBook, GITHUB_DEPLOYMENTS_RAW, GITHUB_DEPLOYMENTS_NICE, GITHUB_RAW_OUTPUTS, GITHUB_RAW_EXTRAS +from .permissions import BalPermissions +from .errors import MultipleMatchesError, NoResultError diff --git a/bal_addresses/addresses.py b/bal_addresses/addresses.py index cadeda9e..97900f86 100644 --- a/bal_addresses/addresses.py +++ b/bal_addresses/addresses.py @@ -1,4 +1,5 @@ import json +from .errors import MultipleMatchesError, NoResultError from typing import Dict from typing import Optional @@ -20,28 +21,24 @@ GITHUB_RAW_OUTPUTS = ( "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/main/outputs" ) +GITHUB_RAW_EXTRAS = ( + "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/main/extras" +) ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -class MultipleMatchesError(Exception): - pass - -class NoResultError(Exception): - pass class AddrBook: + fullbook = requests.get(f"{GITHUB_RAW_OUTPUTS}/addressbook.json").json() - chains = Munch(requests.get( + chains = Munch.fromDict(requests.get( "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/main/extras/chains.json" ).json()) - CHAIN_IDS_BY_NAME = chains["CHAIN_IDS_BY_NAME"] - fx_description_by_name = DotMap(requests.get( + fx_description_by_name = Munch.fromDict(requests.get( "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/main/extras/func_desc_by_name.json" ).json()) - GITHUB_MONOREPO_RAW = ( - "https://raw.githubusercontent.com/balancer-labs/balancer-v2-monorepo/master" - ) + chain_ids_by_name = chains.CHAIN_IDS_BY_NAME def __init__(self, chain, jsonfile=False): self.jsonfile = jsonfile @@ -56,16 +53,18 @@ def __init__(self, chain, jsonfile=False): dactive = deployments["active"][chain] except Exception: dactive = {} - self.deployments_only = DotMap(dactive | dold) + self.deployments_only = Munch.fromDict(dactive | dold) try: self.flatbook = requests.get(f"{GITHUB_RAW_OUTPUTS}/{chain}.json").json() - self.reversebook = DotMap( - requests.get(f"{GITHUB_RAW_OUTPUTS}/{chain}_reverse.json").json()) + self.reversebook = requests.get(f"{GITHUB_RAW_OUTPUTS}/{chain}_reverse.json").json() except Exception: self.flatbook = {"zero/zero": ZERO_ADDRESS} self.reversebook = {ZERO_ADDRESS: "zero/zero"} self._deployments = None + self._extras = None + self._multisigs = None + @property def deployments(self) -> Optional[Munch]: @@ -76,9 +75,30 @@ def deployments(self) -> Optional[Munch]: return self._deployments else: self.populate_deployments() - return self._deployments + @property + def extras(self) -> Optional[Munch]: + """ + Get the extras for all chains in a form of a Munch object + """ + if self._extras is not None: + return self._extras + else: + self.populate_extras() + return self._extras + + @property + def multisigs(self) -> Optional[Munch]: + """ + Get the multisigs for all chains in a form of a Munch object + """ + if self._multisigs is not None: + return self._multisigs + else: + self.populate_multisigs() + return self._extras + def populate_deployments(self) -> None: chain_deployments = requests.get( f"{GITHUB_DEPLOYMENTS_RAW}/addresses/{self.chain}.json" @@ -104,31 +124,58 @@ def _process_deployment(self, deployment: Dict) -> Dict: processed_deployment[deployment_identifier] = v return processed_deployment + + def populate_extras(self) -> None: + chain_extras = requests.get( + f"{GITHUB_RAW_EXTRAS}/{self.chain}.json" + ) + if chain_extras.ok: + self._extras = Munch.fromDict(chain_extras.json()) + + + def populate_multisigs(self) -> None: + msigs = requests.get( + f"{GITHUB_RAW_EXTRAS}/multisigs.json" + ).json() + if msigs.get(self.chain): + self._multisigs = Munch.fromDict(msigs[self.chain]) + else: + print(f"Warning: No multisigs for chain {self.chain}, multisigs must be added in extras/multisig.json") + self._multisigs = Munch + + + def search_unique(self, substr): results = [s for s in self.flatbook.keys() if substr in s] if len(results) > 1: raise MultipleMatchesError(f"{substr} Multiple matches found: {results}") if len(results) < 1: raise NoResultError(f"{substr}") - return results[0] + return Munch.fromDict({ + "path": results[0], + "address": self.flatbook[results[0]] + }) def search_unique_deployment(self, substr): results = [s for s in self.deployments_only.keys() if substr in s] if len(results) > 1: - raise self.MultipleMatchesError(f"{substr} Multiple matches found: {results}") + raise MultipleMatchesError(f"{substr} Multiple matches found: {results}") if len(results) < 1: - raise self.NoResultError(f"{substr}") - return results[0] + raise NoResultError(f"{substr}") + return Munch.fromDict({ + "deployment": results[0], + "addresses_by_contract": self.deployments_only[results[0]] + }) def search_many_deployments(self, substr): search = [s for s in self.deployments_only.keys() if substr in s] - results = {key: self.deployments_only[key] for key in search if key in self.flatbook} - return results + return search def search_many(self, substr): - search = [s for s in self.flatbook.keys() if substr in s] - results = {key: self.flatbook[key] for key in search if key in self.flatbook} - return results + output = [] + results = {path: address for path, address in self.flatbook.items() if substr in path} + outputs = [Munch.fromDict({"path": path, "address": address}) for path, address in results.items()] + return outputs def latest_contract(self, contract_name): deployments = [] @@ -138,7 +185,12 @@ def latest_contract(self, contract_name): if len(deployments) == 0: raise NoResultError(contract_name) deployments.sort(reverse=True) - return self.deployments_only[deployments[0]][contract_name] + address = self.deployments_only[deployments[0]][contract_name] + return Munch.fromDict({ + "path": self.reversebook[address], + "address": address + }) + @staticmethod def checksum_address_dict(addresses): @@ -161,8 +213,7 @@ def build_dotmap(self): fullbook = json.load(f) else: fullbook = self.fullbook - return ( - DotMap(fullbook["active"].get(self.chain, {}) | fullbook["old"].get(self.chain, {}))) + return (fullbook["active"].get(self.chain, {}) | fullbook["old"].get(self.chain, {})) # Checksum one more time for good measure def flatten_dict(self, d, parent_key='', sep='/'): @@ -177,7 +228,7 @@ def flatten_dict(self, d, parent_key='', sep='/'): def generate_flatbook(self): print(f"Generating Addressbook for {self.chain}") - ab = dict(self.dotmap) + ab = dict(self.merge_deployments()) return self.flatten_dict(ab) diff --git a/bal_addresses/errors.py b/bal_addresses/errors.py new file mode 100644 index 00000000..c7c354de --- /dev/null +++ b/bal_addresses/errors.py @@ -0,0 +1,6 @@ + +class MultipleMatchesError(Exception): + pass + +class NoResultError(Exception): + pass \ No newline at end of file diff --git a/bal_addresses/gen_addresses.py b/bal_addresses/gen_addresses.py index 2b843b27..d9fc76a8 100644 --- a/bal_addresses/gen_addresses.py +++ b/bal_addresses/gen_addresses.py @@ -8,7 +8,7 @@ def reverse_dict(d): inv_map = {v: k for k, v in d.items()} return inv_map -def write_addressbooks(chainlist=AddrBook.CHAIN_IDS_BY_NAME.keys()): +def write_addressbooks(chainlist=AddrBook.chain_ids_by_name.keys()): for chain in chainlist: print(f"Writing addressbooks for {chain}") flatbook = AddrBook(chain, jsonfile="outputs/addressbook.json").generate_flatbook() @@ -18,7 +18,7 @@ def write_addressbooks(chainlist=AddrBook.CHAIN_IDS_BY_NAME.keys()): json.dump(reverse_dict(flatbook), f, indent=3) def main(): - chains = AddrBook.CHAIN_IDS_BY_NAME.keys() + chains = AddrBook.chain_ids_by_name.keys() print(f"Generating new addressbook jsons for {chains}") write_addressbooks(chains) diff --git a/bal_addresses/generate_current_permissions.py b/bal_addresses/generate_current_permissions.py index a0e97039..0c2cf7ac 100644 --- a/bal_addresses/generate_current_permissions.py +++ b/bal_addresses/generate_current_permissions.py @@ -2,7 +2,7 @@ import json import pandas as pd import os -from addresses import AddrBook +from addresses import AddrBook, GITHUB_DEPLOYMENTS_RAW from permissions import BalPermissions from web3 import Web3 import datetime @@ -29,7 +29,7 @@ def build_chain_permissions_list(chain_name): r = a.flatbook results = {} address_names = a.reversebook - action_ids_list = f"{BalPermissions.GITHUB_DEPLOYMENTS_RAW}/action-ids/{chain_name}/action-ids.json" + action_ids_list = f"{GITHUB_DEPLOYMENTS_RAW}/action-ids/{chain_name}/action-ids.json" w3 = w3_by_chain[chain_name] authorizer = w3.eth.contract(address=r["20210418-authorizer/Authorizer"], abi=json.load(open("bal_addresses/abis/Authorizer.json"))) try: diff --git a/bal_addresses/permissions.py b/bal_addresses/permissions.py index 9dbbbbc9..4914eb63 100644 --- a/bal_addresses/permissions.py +++ b/bal_addresses/permissions.py @@ -1,40 +1,17 @@ - -import json -from web3 import Web3 +from .errors import NoResultError, MultipleMatchesError import requests from dotmap import DotMap -from addresses import AddrBook +from bal_addresses import AddrBook, GITHUB_DEPLOYMENTS_RAW, GITHUB_RAW_OUTPUTS from collections import defaultdict from munch import Munch -### Errors -class MultipleMatchesError(Exception): - pass - - -class NoResultError(Exception): - pass - ### Main class class BalPermissions: - GITHUB_DEPLOYMENTS_RAW = "https://raw.githubusercontent.com/balancer/balancer-deployments/master" - ## TODO switch back to main branch - #GITHUB_RAW_OUTPUTS = "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/main/outputs" - GITHUB_RAW_OUTPUTS = "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/generate_permissions_jsons/outputs" - - - ### Errors - class MultipleMatchesError(Exception): - pass - - class NoResultError(Exception): - pass - def __init__(self, chain): self.chain = chain - self.active_permissions_by_action_id = requests.get(f"{self.GITHUB_RAW_OUTPUTS}/permissions/active/{chain}.json").json() - self.action_ids_by_contract_by_deployment = requests.get(f"{self.GITHUB_DEPLOYMENTS_RAW}/action-ids/{chain}/action-ids.json").json() + self.active_permissions_by_action_id = requests.get(f"{GITHUB_RAW_OUTPUTS}/permissions/active/{chain}.json").json() + self.action_ids_by_contract_by_deployment = requests.get(f"{GITHUB_DEPLOYMENTS_RAW}/action-ids/{chain}/action-ids.json").json() # Define self.paths_by_action_id = defaultdict(set) @@ -63,7 +40,7 @@ def search_many_paths_by_unique_deployment(self, deployment_substr, fx_substr) - a = AddrBook(self.chain) results = [] deployment = a.search_unique_deployment(deployment_substr) - deployment_fxs = self.search_path(deployment) + deployment_fxs = self.search_path(deployment.deployment) search = [s for s in deployment_fxs if fx_substr in s] for r in search: result = DotMap({ @@ -76,9 +53,9 @@ def search_many_paths_by_unique_deployment(self, deployment_substr, fx_substr) - def search_unique_path_by_unique_deployment(self, deployment_substr, fx_substr) -> dict[str, str]: results = self.search_many_paths_by_unique_deployment(deployment_substr, fx_substr) if len(results) > 1: - raise self.MultipleMatchesError(f"{fx_substr} Multiple matches found: {results}") + raise MultipleMatchesError(f"{fx_substr} Multiple matches found: {results}") if len(results) < 1: - raise self.NoResultError(f"{fx_substr}") + raise NoResultError(f"{fx_substr}") return results[0] def needs_authorizer(self, contract, deployment) -> bool: @@ -88,14 +65,14 @@ def allowed_addresses(self, action_id) -> list[str]: try: return self.active_permissions_by_action_id[action_id] except KeyError: - raise self.NoResultError(f"{action_id} has no authorized callers") + raise NoResultError(f"{action_id} has no authorized callers") def allowed_caller_names(self, action_id) -> list[str]: a = AddrBook(self.chain) try: addresslist = self.active_permissions_by_action_id[action_id] except KeyError: - raise self.NoResultError(f"{action_id} has no authorized callers") + raise NoResultError(f"{action_id} has no authorized callers") names = [a.flatbook.get(item, 'undef') for item in addresslist] return names diff --git a/setup.py b/setup.py index 9a35c425..59adc094 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -VERSION = '0.7.0' +VERSION = '0.8.0' DESCRIPTION = 'Balancer Maxi Addressbook' LONG_DESCRIPTION = 'Balancer Maxi Addressbook and Balancer Permissions helper' diff --git a/tests/test_addresses.py b/tests/test_addresses.py index a97d6a97..e41d65f7 100644 --- a/tests/test_addresses.py +++ b/tests/test_addresses.py @@ -38,6 +38,28 @@ def test_deployments_populated(): } } ) + responses.add( + responses.GET, + "https://raw.githubusercontent.com/BalancerMaxis" + "/bal_addresses/main/extras/mainnet.json", + json={ + "zero": { + "zero": "0x0000000000000000000000000000000000000000" + }, + "balancer": {} + + } + ) + responses.add( + responses.GET, + "https://raw.githubusercontent.com/BalancerMaxis" + "/bal_addresses/main/extras/multisigs.json", + json={ + "mainnet": { + "dao": "0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f", + } + } + ) a = AddrBook("mainnet") a.populate_deployments() @@ -50,7 +72,14 @@ def test_deployments_populated(): # Make sure that when we try to access a non-existing attribute, we get an error with pytest.raises(AttributeError): assert a.deployments.vault.non_existing_attribute - + a.populate_extras() + assert a.extras.zero.zero == "0x0000000000000000000000000000000000000000" + # Make sure that when we try to access a non-existing attribute, we get an error + with pytest.raises(AttributeError): + assert a.extras.balancer.non_existing_attribute + a.populate_multisigs() + print(a.multisigs) + assert a.multisigs.dao == "0x10A19e7eE7d7F8a52822f6817de8ea18204F2e4f" @responses.activate def test_deployments_invalid_format(): @@ -76,10 +105,36 @@ def test_deployments_invalid_format(): } } ) + responses.add( + responses.GET, + "https://raw.githubusercontent.com/BalancerMaxis" + "/bal_addresses/main/extras/mainnet.json", + json={ + "vault": { + "contracts": {'name': 'Vault'}, + "status": "ACTIVE" + } + } + ) + responses.add( + responses.GET, + "https://raw.githubusercontent.com/BalancerMaxis" + "/bal_addresses/main/extras/multisigs.json", + json={ + "mainnet": { + "contracts": {'name': 'Vault'}, + "status": "ACTIVE" + } + } + ) a = AddrBook("mainnet") a.populate_deployments() assert a.deployments.vault.contracts.name == "Vault" + a.populate_extras() + assert a.extras.vault.status == "ACTIVE" + a.populate_multisigs() + assert a.multisigs.status == "ACTIVE" @responses.activate @@ -99,7 +154,27 @@ def test_deployments_not_populated(): json={}, status=404 ) + responses.add( + responses.GET, + "https://raw.githubusercontent.com/BalancerMaxis" + "/bal_addresses/main/extras/mainnet.json", + json={}, + status=404 + ) + responses.add( + responses.GET, + "https://raw.githubusercontent.com/BalancerMaxis" + "/bal_addresses/main/extras/multisigs.json", + json={}, + status=404 + ) a = AddrBook("mainnet") assert a.deployments is None with pytest.raises(AttributeError): assert a.deployments.vault.non_existing_attribute + assert a.extras is None + with pytest.raises(AttributeError): + assert a.extras.non_existing_attribute + assert a.multisigs is None + with pytest.raises(AttributeError): + assert a.multisigs.non_existing_attribute \ No newline at end of file