From 67c4e884cb9210352d0f010e3f322bcfc7096051 Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Tue, 10 Sep 2024 16:44:22 +0300 Subject: [PATCH 01/20] Organize Project + add Web3 client --- ProposalTools/Checks/check.py | 4 +- ProposalTools/Checks/diff.py | 6 +- ProposalTools/Checks/feed_price.py | 10 +- ProposalTools/Checks/global_variables.py | 6 +- ProposalTools/GIT/git_manager.py | 2 +- ProposalTools/{API => apis}/__init__.py | 0 ProposalTools/{API => apis}/chainlink_api.py | 4 +- .../contract_source_code_api.py | 33 ++++- .../contract_code}/source_code.py | 2 +- ProposalTools/apis/web3_client.py | 78 +++++++++++ ProposalTools/check_proposal.py | 10 +- ProposalTools/checks/__init__.py | 5 + ProposalTools/checks/check.py | 39 ++++++ ProposalTools/checks/diff.py | 123 ++++++++++++++++++ ProposalTools/checks/feed_price.py | 67 ++++++++++ ProposalTools/checks/global_variables.py | 104 +++++++++++++++ ProposalTools/git/__init__.py | 0 ProposalTools/git/git_manager.py | 65 +++++++++ ProposalTools/utils/__init__.py | 0 ProposalTools/utils/chain_enum.py | 18 +++ ProposalTools/utils/pretty_printer.py | 14 ++ ProposalTools/utils/singleton.py | 6 + 22 files changed, 570 insertions(+), 26 deletions(-) rename ProposalTools/{API => apis}/__init__.py (100%) rename ProposalTools/{API => apis}/chainlink_api.py (97%) rename ProposalTools/{API => apis/contract_code}/contract_source_code_api.py (79%) rename ProposalTools/{Utils => apis/contract_code}/source_code.py (98%) create mode 100644 ProposalTools/apis/web3_client.py create mode 100644 ProposalTools/checks/__init__.py create mode 100644 ProposalTools/checks/check.py create mode 100644 ProposalTools/checks/diff.py create mode 100644 ProposalTools/checks/feed_price.py create mode 100644 ProposalTools/checks/global_variables.py create mode 100644 ProposalTools/git/__init__.py create mode 100644 ProposalTools/git/git_manager.py create mode 100644 ProposalTools/utils/__init__.py create mode 100644 ProposalTools/utils/chain_enum.py create mode 100644 ProposalTools/utils/pretty_printer.py create mode 100644 ProposalTools/utils/singleton.py diff --git a/ProposalTools/Checks/check.py b/ProposalTools/Checks/check.py index d80c3c6..b296a63 100644 --- a/ProposalTools/Checks/check.py +++ b/ProposalTools/Checks/check.py @@ -4,8 +4,8 @@ from pathlib import Path import ProposalTools.config as config -from ProposalTools.Utils.source_code import SourceCode -from ProposalTools.Utils.chain_enum import Chain +from ProposalTools.apis.contract_code.source_code import SourceCode +from ProposalTools.utils.chain_enum import Chain class Check(ABC): diff --git a/ProposalTools/Checks/diff.py b/ProposalTools/Checks/diff.py index e249276..f50b430 100644 --- a/ProposalTools/Checks/diff.py +++ b/ProposalTools/Checks/diff.py @@ -3,9 +3,9 @@ from typing import Optional from dataclasses import dataclass -from ProposalTools.Utils.source_code import SourceCode -from ProposalTools.Checks.check import Check -import ProposalTools.Utils.pretty_printer as pp +from ProposalTools.apis.contract_code.source_code import SourceCode +from ProposalTools.checks.check import Check +import ProposalTools.utils.pretty_printer as pp @dataclass diff --git a/ProposalTools/Checks/feed_price.py b/ProposalTools/Checks/feed_price.py index 437259b..b519433 100644 --- a/ProposalTools/Checks/feed_price.py +++ b/ProposalTools/Checks/feed_price.py @@ -1,11 +1,11 @@ from pathlib import Path import re -from ProposalTools.API.chainlink_api import ChainLinkAPI -from ProposalTools.Utils.chain_enum import Chain -from ProposalTools.Checks.check import Check -from ProposalTools.Utils.source_code import SourceCode -import ProposalTools.Utils.pretty_printer as pp +from ProposalTools.apis.chainlink_api import ChainLinkAPI +from ProposalTools.utils.chain_enum import Chain +from ProposalTools.checks.check import Check +from ProposalTools.apis.contract_code.source_code import SourceCode +import ProposalTools.utils.pretty_printer as pp class FeedPriceCheck(Check): diff --git a/ProposalTools/Checks/global_variables.py b/ProposalTools/Checks/global_variables.py index 86a0e10..9e093fe 100644 --- a/ProposalTools/Checks/global_variables.py +++ b/ProposalTools/Checks/global_variables.py @@ -3,9 +3,9 @@ from solidity_parser.parser import Node -from ProposalTools.Checks.check import Check -from ProposalTools.Utils.source_code import SourceCode -import ProposalTools.Utils.pretty_printer as pp +from ProposalTools.checks.check import Check +from ProposalTools.apis.contract_code.source_code import SourceCode +import ProposalTools.utils.pretty_printer as pp class GlobalVariableCheck(Check): diff --git a/ProposalTools/GIT/git_manager.py b/ProposalTools/GIT/git_manager.py index 5746553..4060048 100644 --- a/ProposalTools/GIT/git_manager.py +++ b/ProposalTools/GIT/git_manager.py @@ -3,7 +3,7 @@ from git import Repo import ProposalTools.config as config -import ProposalTools.Utils.pretty_printer as pp +import ProposalTools.utils.pretty_printer as pp class GitManager: diff --git a/ProposalTools/API/__init__.py b/ProposalTools/apis/__init__.py similarity index 100% rename from ProposalTools/API/__init__.py rename to ProposalTools/apis/__init__.py diff --git a/ProposalTools/API/chainlink_api.py b/ProposalTools/apis/chainlink_api.py similarity index 97% rename from ProposalTools/API/chainlink_api.py rename to ProposalTools/apis/chainlink_api.py index 7a5da2e..5f86a48 100644 --- a/ProposalTools/API/chainlink_api.py +++ b/ProposalTools/apis/chainlink_api.py @@ -2,8 +2,8 @@ from pydantic import BaseModel from typing import Optional -from ProposalTools.Utils.chain_enum import Chain -from ProposalTools.Utils.singleton import Singleton +from ProposalTools.utils.chain_enum import Chain +from ProposalTools.utils.singleton import Singleton class Docs(BaseModel): assetClass: Optional[str] = None diff --git a/ProposalTools/API/contract_source_code_api.py b/ProposalTools/apis/contract_code/contract_source_code_api.py similarity index 79% rename from ProposalTools/API/contract_source_code_api.py rename to ProposalTools/apis/contract_code/contract_source_code_api.py index 5b867b7..06e651c 100644 --- a/ProposalTools/API/contract_source_code_api.py +++ b/ProposalTools/apis/contract_code/contract_source_code_api.py @@ -4,8 +4,8 @@ import os import json -from ProposalTools.Utils.chain_enum import Chain -from ProposalTools.Utils.source_code import SourceCode +from ProposalTools.utils.chain_enum import Chain +from ProposalTools.apis.contract_code.source_code import SourceCode @dataclass @@ -67,7 +67,7 @@ def __init__(self, chain: Chain) -> None: if not self.api_key: raise ValueError(f"{chain}SCAN_API_KEY environment variable is not set.") - self.base_url = f"{api_info.base_url}?module=contract&action=getsourcecode&apikey={self.api_key}" + self.base_url = f"{api_info.base_url}?module=contract&apikey={self.api_key}" def get_source_code(self, proposal_address: str) -> List[SourceCode]: """ @@ -82,7 +82,7 @@ def get_source_code(self, proposal_address: str) -> List[SourceCode]: Raises: ValueError: If there's an error fetching the source code. """ - url = f"{self.base_url}&address={proposal_address}" + url = f"{self.base_url}&action=getsourcecode&address={proposal_address}" response = requests.get(url) response.raise_for_status() data = response.json() @@ -103,3 +103,28 @@ def get_source_code(self, proposal_address: str) -> List[SourceCode]: for source_name, source_code in sources.items() ] return source_codes + + def get_contract_abi(self, contract_address: str) -> list[dict]: + """ + Fetch the ABI of a smart contract from the blockchain explorer API. + + Args: + contract_address (str): The address of the smart contract. + + Returns: + list[dict]: The contract ABI as a list of dictionaries. + + Raises: + ValueError: If there's an error fetching the ABI. + """ + url = f"{self.base_url}?&action=getabi&address={contract_address}" + response = requests.get(url) + response.raise_for_status() + data = response.json() + + if data['status'] != '1': + raise ValueError(f"Error fetching contract ABI: {data.get('message', 'Unknown error')}\n{data.get('result')}") + + abi = json.loads(data['result']) + return abi + diff --git a/ProposalTools/Utils/source_code.py b/ProposalTools/apis/contract_code/source_code.py similarity index 98% rename from ProposalTools/Utils/source_code.py rename to ProposalTools/apis/contract_code/source_code.py index 4d46491..031bcf7 100644 --- a/ProposalTools/Utils/source_code.py +++ b/ProposalTools/apis/contract_code/source_code.py @@ -1,7 +1,7 @@ from solidity_parser import parser from dataclasses import dataclass -import ProposalTools.Utils.pretty_printer as pp +import ProposalTools.utils.pretty_printer as pp @dataclass diff --git a/ProposalTools/apis/web3_client.py b/ProposalTools/apis/web3_client.py new file mode 100644 index 0000000..c5ad49d --- /dev/null +++ b/ProposalTools/apis/web3_client.py @@ -0,0 +1,78 @@ +import os +from typing import Any, Optional + +from web3 import Web3 +from web3.exceptions import ContractLogicError + +from ProposalTools.apis.contract_code.contract_source_code_api import ContractSourceCodeAPI, Chain + + +class Web3API: + """ + Web3API is a client for interacting with Ethereum smart contracts through the Infura API. + + This class manages connection to the Ethereum blockchain using Web3 and Infura, and provides + functionality for interacting with deployed smart contracts. + + Attributes: + w3_client (Web3): The Web3 client connected to an Ethereum provider. + """ + + def __init__(self, api_key: Optional[str] = None): + """ + Initializes the Web3API client with the Infura API. + + Args: + api_key (Optional[str]): The Infura API key to connect to the Ethereum blockchain. + If not provided, it will be fetched from the environment variable `INFURA_API_KEY`. + + Raises: + ValueError: If the Infura API key is not set or invalid. + ConnectionError: If the connection to the Infura API fails. + """ + self.api_key = api_key or os.getenv("INFURA_API_KEY") + if self.api_key is None: + raise ValueError("Infura API key is required. Set it in the 'INFURA_API_KEY' environment variable or pass it as an argument.") + + infura_url = f"https://mainnet.infura.io/v3/{self.api_key}" + self.w3_client = Web3(Web3.HTTPProvider(infura_url)) + + # Verify connection to Ethereum node + if not self.w3_client.isConnected(): + raise ConnectionError("Failed to connect to Infura API. Check your network connection and API key.") + + # ETHScan API to retrieve the abi + self.contract_code_api = ContractSourceCodeAPI(Chain.ETH) + + def call_contract_function(self, contract_address: str, function_name: str, *args: Any) -> Any: + """ + Calls a specified function on an Ethereum smart contract. + + Args: + contract_address (str): The Ethereum address of the smart contract. + function_name (str): The name of the contract function to call. + *args (Any): Any arguments that need to be passed to the contract function. + + Returns: + Any: The result of the contract function call. + + Raises: + ValueError: If the contract ABI or function name is invalid. + ContractLogicError: If there's an error calling the contract function (e.g., invalid function name or input). + """ + contract_abi = self.contract_code_api.get_contract_abi(contract_address) + + try: + contract = self.w3_client.eth.contract(address=contract_address, abi=contract_abi) + except Exception as e: + raise ValueError(f"Error initializing contract: {str(e)}") + + try: + contract_function = getattr(contract.functions, function_name) + except AttributeError: + raise ValueError(f"Function '{function_name}' not found in contract.") + + try: + return contract_function(*args).call() + except ContractLogicError as e: + raise ContractLogicError(f"Error executing contract function '{function_name}': {str(e)}") diff --git a/ProposalTools/check_proposal.py b/ProposalTools/check_proposal.py index 2f09cd6..32dc38b 100644 --- a/ProposalTools/check_proposal.py +++ b/ProposalTools/check_proposal.py @@ -2,11 +2,11 @@ import json from typing import Any, Optional -from ProposalTools.Utils.chain_enum import Chain -import ProposalTools.Utils.pretty_printer as pp -from ProposalTools.GIT.git_manager import GitManager -from ProposalTools.API.contract_source_code_api import ContractSourceCodeAPI -import ProposalTools.Checks as Checks +from ProposalTools.utils.chain_enum import Chain +import ProposalTools.utils.pretty_printer as pp +from ProposalTools.git.git_manager import GitManager +from ProposalTools.apis.contract_code.contract_source_code_api import ContractSourceCodeAPI +import ProposalTools.checks as Checks def parse_args() -> tuple[Optional[str], Optional[str], Optional[str], Optional[str]]: diff --git a/ProposalTools/checks/__init__.py b/ProposalTools/checks/__init__.py new file mode 100644 index 0000000..e168da8 --- /dev/null +++ b/ProposalTools/checks/__init__.py @@ -0,0 +1,5 @@ +from .diff import DiffCheck +from .global_variables import GlobalVariableCheck +from .feed_price import FeedPriceCheck + +all = ["DiffCheck", "GlobalVariableCheck", "FeedPriceCheck"] \ No newline at end of file diff --git a/ProposalTools/checks/check.py b/ProposalTools/checks/check.py new file mode 100644 index 0000000..b296a63 --- /dev/null +++ b/ProposalTools/checks/check.py @@ -0,0 +1,39 @@ +from abc import ABC +from datetime import datetime +import json +from pathlib import Path + +import ProposalTools.config as config +from ProposalTools.apis.contract_code.source_code import SourceCode +from ProposalTools.utils.chain_enum import Chain + + +class Check(ABC): + def __init__(self, customer: str, chain: Chain, proposal_address: str, source_codes: list[SourceCode]): + self.customer = customer + self.chain = chain + self.proposal_address = proposal_address + self.source_codes = source_codes + self.customer_folder = config.MAIN_PATH / customer + self.check_folder = self.customer_folder / "checks" / chain / proposal_address / f"{self.__class__.__name__}_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + self.check_folder.mkdir(parents=True, exist_ok=True) + + + def _write_to_file(self, path: str | Path, data: dict | str) -> None: + """ + Writes data to a specified file, creating the file and its parent directories if they do not exist. + + Args: + path (str | Path): The relative path to the file where the data will be written. + data (Any): The data to be written to the file. This can be a dictionary for JSON files or a string for text files. + """ + full_file_path = self.check_folder / path + + # Ensure the directory exists; if not, create it + if not full_file_path.exists(): + full_file_path.parent.mkdir(parents=True, exist_ok=True) + full_file_path.touch() + + with open(full_file_path, "a") as f: + json.dump(data, f, indent=4) if isinstance(data, dict) or isinstance(data, list) else f.write(data) + f.write("\n") \ No newline at end of file diff --git a/ProposalTools/checks/diff.py b/ProposalTools/checks/diff.py new file mode 100644 index 0000000..f50b430 --- /dev/null +++ b/ProposalTools/checks/diff.py @@ -0,0 +1,123 @@ +import difflib +from pathlib import Path +from typing import Optional +from dataclasses import dataclass + +from ProposalTools.apis.contract_code.source_code import SourceCode +from ProposalTools.checks.check import Check +import ProposalTools.utils.pretty_printer as pp + + +@dataclass +class Compared: + """ + A dataclass representing the result of comparing a local file with a proposal file. + + Attributes: + local_file (str): The path to the local file. + proposal_file (str): The name of the file from the proposal. + diff (str): The path to the file containing the diff result. + """ + local_file: str + proposal_file: str + diff: str + + +class DiffCheck(Check): + """ + A class that performs a diff check between local and remote (proposal) source codes. + + This class compares source files from a local repository with those from a remote proposal, + identifying differences and generating patch files. + """ + + def __find_most_common_path(self, source_path: Path, repo: Path) -> Optional[Path]: + """ + Find the most common file path between a source path and a repository. + + This method attempts to locate the corresponding local file for a given source path from the proposal. + + Args: + source_path (Path): The source file path from the remote repository. + repo (Path): The local repository path. + + Returns: + Optional[Path]: The most common file path if found, otherwise None. + """ + for i in range(len(source_path.parts)): + current_source_path = Path(*source_path.parts[i:]) + local_files = list(repo.rglob(str(current_source_path))) + if len(local_files) == 1: + return local_files[0] + return None + + def find_diffs(self) -> list[SourceCode]: + """ + Find and save differences between local and remote source codes. + + This method compares the contents of local files with those from the proposal, generating patch files + for any differences found. + + Returns: + list[Compared]: list of missing files. + """ + missing_files = [] + files_with_diffs = [] + + target_repo = self.customer_folder / "modules" + + for source_code in self.source_codes: + local_file = self.__find_most_common_path(Path(source_code.file_name), target_repo) + if not local_file: + missing_files.append(source_code) + continue + + local_content = local_file.read_text().splitlines() + remote_content = source_code.file_content + + diff = difflib.unified_diff(local_content, remote_content, fromfile=str(local_file), tofile=source_code.file_name) + diff_text = '\n'.join(diff) + + if diff_text: + diff_file = f"{local_file.stem}.patch" + files_with_diffs.append( + Compared( + str(local_file), + source_code.file_name, + str(self.check_folder / diff_file) + ) + ) + self._write_to_file(diff_file, diff_text) + + self.__print_diffs_results(missing_files, files_with_diffs) + return missing_files + + def __print_diffs_results(self, missing_files: list[SourceCode], files_with_diffs: list[Compared]): + """ + Print the results of the diff check. + + This method outputs a summary of the comparison, including the number of files compared, + the number of missing files, and the number of files with differences. + + Args: + missing_files (list[SourceCode]): A list of SourceCode objects representing missing files. + files_with_diffs (list[Compared]): A list of Compared objects representing files with differences. + """ + total_number_of_files = len(self.source_codes) + number_of_missing_files = len(missing_files) + number_of_files_with_diffs = len(files_with_diffs) + + msg = f"Compared {total_number_of_files - number_of_missing_files}/{total_number_of_files} files for proposal {self.proposal_address}" + if number_of_missing_files == 0: + pp.pretty_print(msg, pp.Colors.SUCCESS) + else: + pp.pretty_print(msg, pp.Colors.WARNING) + for source_code in missing_files: + pp.pretty_print(f"Missing file: {source_code.file_name} in local repo", pp.Colors.WARNING) + + if number_of_files_with_diffs == 0: + pp.pretty_print("No differences found.", pp.Colors.SUCCESS) + else: + pp.pretty_print(f"Found differences in {number_of_files_with_diffs} files", pp.Colors.FAILURE) + for compared_pair in files_with_diffs: + pp.pretty_print(f"Local: {compared_pair.local_file}\nProposal: {compared_pair.proposal_file}\nDiff: {compared_pair.diff}", pp.Colors.FAILURE) diff --git a/ProposalTools/checks/feed_price.py b/ProposalTools/checks/feed_price.py new file mode 100644 index 0000000..b519433 --- /dev/null +++ b/ProposalTools/checks/feed_price.py @@ -0,0 +1,67 @@ +from pathlib import Path +import re + +from ProposalTools.apis.chainlink_api import ChainLinkAPI +from ProposalTools.utils.chain_enum import Chain +from ProposalTools.checks.check import Check +from ProposalTools.apis.contract_code.source_code import SourceCode +import ProposalTools.utils.pretty_printer as pp + + +class FeedPriceCheck(Check): + """ + Verifies the price feed addresses in the provided source code against official Chainlink data. + + This class is responsible for verifying that the price feed addresses found in the source code + match the addresses provided by the Chainlink API. It also categorizes these addresses into + verified and violated based on whether they are found in the official source. + """ + + def __init__(self, customer: str, chain: Chain, proposal_address: str, source_codes: list[SourceCode]) -> None: + """ + Initializes the VerifyFeedPrice object with customer information, proposal address, + and source codes to be checked. + + Args: + customer (str): The name of the customer for whom the verification is being performed. + chain (Chain): The blockchain network to verify the price feeds against. + proposal_address (str): The address of the proposal being verified. + source_codes (list[SourceCode]): A list of source code objects containing the Solidity contracts to be checked. + """ + super().__init__(customer, chain, proposal_address, source_codes) + self.api = ChainLinkAPI() + self.address_pattern = r'0x[a-fA-F0-9]{40}' + + # Retrieve price feeds from Chainlink API and map them by contract address + price_feeds = self.api.get_price_feeds_info(self.chain) + self.price_feeds_dict = {feed.contractAddress: feed for feed in price_feeds} + self.price_feeds_dict.update({feed.proxyAddress: feed for feed in price_feeds if feed.proxyAddress}) + + + def verify_feed_price(self) -> None: + """ + Verifies the price feeds in the source code against the Chainlink API for the specified chain. + + This method retrieves price feeds from the Chainlink API, compares them against the addresses + found in the source code, and then categorizes them into verified or violated based on the comparison. + """ + # Iterate through each source code file to find and verify address variables + for source_code in self.source_codes: + verified_sources_path = f"{Path(source_code.file_name).stem.removesuffix('.sol')}/verified_sources.json" + verified_variables = [] + + contract_text = '\n'.join(source_code.file_content) + addresses = re.findall(self.address_pattern, contract_text) + for address in addresses: + if address in self.price_feeds_dict: + feed = self.price_feeds_dict[address] + pp.pretty_print( + f"Found {address} on Chainlink\nname:{feed.name} Decimals:{feed.decimals}", + pp.Colors.SUCCESS + ) + verified_variables.append(feed.dict()) + + if verified_variables: + self._write_to_file(verified_sources_path, verified_variables) + else: + pp.pretty_print(f"No address related to chain link found in {Path(source_code.file_name).stem}", pp.Colors.INFO) \ No newline at end of file diff --git a/ProposalTools/checks/global_variables.py b/ProposalTools/checks/global_variables.py new file mode 100644 index 0000000..9e093fe --- /dev/null +++ b/ProposalTools/checks/global_variables.py @@ -0,0 +1,104 @@ +import re +from pathlib import Path +from solidity_parser.parser import Node + + +from ProposalTools.checks.check import Check +from ProposalTools.apis.contract_code.source_code import SourceCode +import ProposalTools.utils.pretty_printer as pp + + +class GlobalVariableCheck(Check): + """ + A class that performs a check on global variables within Solidity contracts. + + This class checks if global variables in source codes files are either constant or immutable. + """ + + def check_global_variables(self) -> None: + """ + Checks global variables in the source code to ensure they are either constant or immutable. + + This method parses the Solidity source code and checks for variables that do not meet the constant + or immutable criteria. + """ + source_code_to_violated_variables = {} + for source_code in self.source_codes: + violated_variables = self.__check_const(source_code) + violated_variables = self.__check_immutable(violated_variables, source_code.file_content) + + if violated_variables: + source_code_to_violated_variables[source_code.file_name] = violated_variables + + self.__process_results(source_code_to_violated_variables) + + def __check_const(self, source_code: SourceCode) -> list[Node]: + """ + Checks a source code for variables that are not declared as constant. + + Args: + source_code (SourceCode): The Solidity source code obj. + + Returns: + list[Node]: A list of AST nodes representing variables that are not constant. + """ + state_variables = source_code.get_state_variables() + if state_variables: + return [ + v for v in state_variables.values() + if not v.get("isDeclaredConst", False) + ] + return [] + + def __check_immutable(self, variables: list[Node], source_code: list[str]) -> list[Node]: + """ + Checks a list of variables to ensure they are declared as immutable in the source code. + + This method searches the source code to verify that the variables are declared with the 'immutable' keyword. + + Args: + variables (list[Node]): A list of AST nodes representing variables. + source_code (list[str]): The Solidity source code lines. + + Returns: + list[Node]: A list of AST nodes representing variables that are not immutable. + """ + violated_variables = [] + + for variable in variables: + variable_name = variable.get('name') + var_type = variable.get('typeName').get('name', variable.get("namePath", "")) + pattern = rf".*{var_type}.*{variable_name}.*" + + found = False + for line in source_code: + if re.search(pattern, line): + found = True + if "immutable" not in line: + violated_variables.append(variable) + break + + if not found: + violated_variables.append(variable) + + return violated_variables + + def __process_results(self, source_code_to_violated_variables: dict[str, list[Node]]): + """ + Processes the results of the global variable checks and prints them to the console. + + This method logs the results of the global variable check, including any violations found. + + Args: + source_code_to_violated_variables (dict[str, list[Node]]): A dictionary mapping file names + to lists of violated variables. + """ + if not source_code_to_violated_variables: + pp.pretty_print("All global variables are constant or immutable.", pp.Colors.SUCCESS) + else: + pp.pretty_print("Global variable checks failed:", pp.Colors.FAILURE) + for file_name, violated_variables in source_code_to_violated_variables.items(): + pp.pretty_print(f"File {file_name} contains variables that are not constant or immutable" + ,pp.Colors.FAILURE) + self._write_to_file(Path(file_name).stem.removesuffix(".sol"), violated_variables) + \ No newline at end of file diff --git a/ProposalTools/git/__init__.py b/ProposalTools/git/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ProposalTools/git/git_manager.py b/ProposalTools/git/git_manager.py new file mode 100644 index 0000000..4060048 --- /dev/null +++ b/ProposalTools/git/git_manager.py @@ -0,0 +1,65 @@ +import json +from pathlib import Path +from git import Repo + +import ProposalTools.config as config +import ProposalTools.utils.pretty_printer as pp + + +class GitManager: + """ + A class to manage Git repositories for a specific customer. + + Attributes: + customer (str): The name or identifier of the customer. + repos (dict): A dictionary mapping repository names to their URLs. + """ + + def __init__(self, customer: str) -> None: + """ + Initialize the GitManager with the given customer name and load the repository URLs. + + Args: + customer (str): The name or identifier of the customer. + """ + self.customer = customer + + self.customer_path = config.MAIN_PATH / self.customer / "modules" + self.customer_path.mkdir(parents=True, exist_ok=True) + + self.repos = self._load_repos() + + def _load_repos(self) -> dict: + """ + Load repository URLs from the JSON file for the given customer. + + Returns: + dict: A dictionary mapping repository names to their URLs. + """ + with open(config.REPOS_PATH) as f: + repos_data = json.load(f) + + # Normalize the customer name to handle case differences + normalized_customer = self.customer.lower() + repos = next((repos for key, repos in repos_data.items() if key.lower() == normalized_customer), []) + + return {Path(r).stem: r for r in repos} + + def clone_or_update(self) -> None: + """ + Clone the repositories for the customer. + + If the repository already exists locally, it will update the repository and its submodules. + Otherwise, it will clone the repository and initialize submodules. + """ + for repo_name, repo_url in self.repos.items(): + repo_path: Path = self.customer_path / repo_name + if repo_path.exists(): + pp.pretty_print(f"Repository {repo_name} already exists at {repo_path}. Updating repo and submodules.", pp.Colors.INFO) + repo = Repo(repo_path) + repo.git.pull() + repo.git.submodule('update', '--init', '--recursive') + else: + pp.pretty_print(f"Cloning {repo_name} from URL: {repo_url} to {repo_path}...", pp.Colors.INFO) + Repo.clone_from(repo_url, repo_path, multi_options=["--recurse-submodules"]) + diff --git a/ProposalTools/utils/__init__.py b/ProposalTools/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ProposalTools/utils/chain_enum.py b/ProposalTools/utils/chain_enum.py new file mode 100644 index 0000000..cc626c4 --- /dev/null +++ b/ProposalTools/utils/chain_enum.py @@ -0,0 +1,18 @@ +from enum import StrEnum + + +class Chain(StrEnum): + """ + Enumeration for supported blockchain networks. + """ + ETH = 'ETH' + ARB = 'ARB' + AVAX = 'AVAX' + BASE = 'BASE' + BSC = 'BSC' + GNO = 'GNO' + MET = 'MET' + OPT = 'OPT' + POLY = 'POLY' + SCR = 'SCR' + ZK = 'ZK' \ No newline at end of file diff --git a/ProposalTools/utils/pretty_printer.py b/ProposalTools/utils/pretty_printer.py new file mode 100644 index 0000000..16d3d4e --- /dev/null +++ b/ProposalTools/utils/pretty_printer.py @@ -0,0 +1,14 @@ +from enum import StrEnum + +class Colors(StrEnum): + SUCCESS = '\033[92m' + FAILURE = '\033[91m' + WARNING = '\033[93m' + INFO = '' + RESET = '\033[0m' + +def pretty_print(message: str, status: Colors): + separator_line = status + '-' * 80 + Colors.RESET + print(separator_line) + print(status + message + Colors.RESET) + print(separator_line) \ No newline at end of file diff --git a/ProposalTools/utils/singleton.py b/ProposalTools/utils/singleton.py new file mode 100644 index 0000000..8a09d95 --- /dev/null +++ b/ProposalTools/utils/singleton.py @@ -0,0 +1,6 @@ +class Singleton(type): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] \ No newline at end of file From 8bdbc0342c58ea183494a9f2495215191e7ec942 Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Tue, 10 Sep 2024 16:52:09 +0300 Subject: [PATCH 02/20] Organize Project + add Web3 client --- ProposalTools/checks/__init__.py | 3 ++- ProposalTools/checks/check.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ProposalTools/checks/__init__.py b/ProposalTools/checks/__init__.py index e168da8..3c7fdee 100644 --- a/ProposalTools/checks/__init__.py +++ b/ProposalTools/checks/__init__.py @@ -1,5 +1,6 @@ from .diff import DiffCheck from .global_variables import GlobalVariableCheck from .feed_price import FeedPriceCheck +from .new_listing import NewListingCheck -all = ["DiffCheck", "GlobalVariableCheck", "FeedPriceCheck"] \ No newline at end of file +all = ["DiffCheck", "GlobalVariableCheck", "FeedPriceCheck", "NewListingCheck"] \ No newline at end of file diff --git a/ProposalTools/checks/check.py b/ProposalTools/checks/check.py index b296a63..ddd7de6 100644 --- a/ProposalTools/checks/check.py +++ b/ProposalTools/checks/check.py @@ -19,7 +19,7 @@ def __init__(self, customer: str, chain: Chain, proposal_address: str, source_co self.check_folder.mkdir(parents=True, exist_ok=True) - def _write_to_file(self, path: str | Path, data: dict | str) -> None: + def _write_to_file(self, path: str | Path, data: dict | str | list) -> None: """ Writes data to a specified file, creating the file and its parent directories if they do not exist. From f186f4fa7e2f3ff03deea9f9e9d7448058e9515f Mon Sep 17 00:00:00 2001 From: Niv vaknin <122722245+nivcertora@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:53:29 +0300 Subject: [PATCH 03/20] Delete ProposalTools/Checks directory --- ProposalTools/Checks/__init__.py | 6 - ProposalTools/Checks/check.py | 39 ---- ProposalTools/Checks/diff.py | 123 ------------- ProposalTools/Checks/feed_price.py | 67 ------- ProposalTools/Checks/global_variables.py | 104 ----------- ProposalTools/Checks/new_listing.py | 215 ----------------------- 6 files changed, 554 deletions(-) delete mode 100644 ProposalTools/Checks/__init__.py delete mode 100644 ProposalTools/Checks/check.py delete mode 100644 ProposalTools/Checks/diff.py delete mode 100644 ProposalTools/Checks/feed_price.py delete mode 100644 ProposalTools/Checks/global_variables.py delete mode 100644 ProposalTools/Checks/new_listing.py diff --git a/ProposalTools/Checks/__init__.py b/ProposalTools/Checks/__init__.py deleted file mode 100644 index 3c7fdee..0000000 --- a/ProposalTools/Checks/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .diff import DiffCheck -from .global_variables import GlobalVariableCheck -from .feed_price import FeedPriceCheck -from .new_listing import NewListingCheck - -all = ["DiffCheck", "GlobalVariableCheck", "FeedPriceCheck", "NewListingCheck"] \ No newline at end of file diff --git a/ProposalTools/Checks/check.py b/ProposalTools/Checks/check.py deleted file mode 100644 index ddd7de6..0000000 --- a/ProposalTools/Checks/check.py +++ /dev/null @@ -1,39 +0,0 @@ -from abc import ABC -from datetime import datetime -import json -from pathlib import Path - -import ProposalTools.config as config -from ProposalTools.apis.contract_code.source_code import SourceCode -from ProposalTools.utils.chain_enum import Chain - - -class Check(ABC): - def __init__(self, customer: str, chain: Chain, proposal_address: str, source_codes: list[SourceCode]): - self.customer = customer - self.chain = chain - self.proposal_address = proposal_address - self.source_codes = source_codes - self.customer_folder = config.MAIN_PATH / customer - self.check_folder = self.customer_folder / "checks" / chain / proposal_address / f"{self.__class__.__name__}_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - self.check_folder.mkdir(parents=True, exist_ok=True) - - - def _write_to_file(self, path: str | Path, data: dict | str | list) -> None: - """ - Writes data to a specified file, creating the file and its parent directories if they do not exist. - - Args: - path (str | Path): The relative path to the file where the data will be written. - data (Any): The data to be written to the file. This can be a dictionary for JSON files or a string for text files. - """ - full_file_path = self.check_folder / path - - # Ensure the directory exists; if not, create it - if not full_file_path.exists(): - full_file_path.parent.mkdir(parents=True, exist_ok=True) - full_file_path.touch() - - with open(full_file_path, "a") as f: - json.dump(data, f, indent=4) if isinstance(data, dict) or isinstance(data, list) else f.write(data) - f.write("\n") \ No newline at end of file diff --git a/ProposalTools/Checks/diff.py b/ProposalTools/Checks/diff.py deleted file mode 100644 index f50b430..0000000 --- a/ProposalTools/Checks/diff.py +++ /dev/null @@ -1,123 +0,0 @@ -import difflib -from pathlib import Path -from typing import Optional -from dataclasses import dataclass - -from ProposalTools.apis.contract_code.source_code import SourceCode -from ProposalTools.checks.check import Check -import ProposalTools.utils.pretty_printer as pp - - -@dataclass -class Compared: - """ - A dataclass representing the result of comparing a local file with a proposal file. - - Attributes: - local_file (str): The path to the local file. - proposal_file (str): The name of the file from the proposal. - diff (str): The path to the file containing the diff result. - """ - local_file: str - proposal_file: str - diff: str - - -class DiffCheck(Check): - """ - A class that performs a diff check between local and remote (proposal) source codes. - - This class compares source files from a local repository with those from a remote proposal, - identifying differences and generating patch files. - """ - - def __find_most_common_path(self, source_path: Path, repo: Path) -> Optional[Path]: - """ - Find the most common file path between a source path and a repository. - - This method attempts to locate the corresponding local file for a given source path from the proposal. - - Args: - source_path (Path): The source file path from the remote repository. - repo (Path): The local repository path. - - Returns: - Optional[Path]: The most common file path if found, otherwise None. - """ - for i in range(len(source_path.parts)): - current_source_path = Path(*source_path.parts[i:]) - local_files = list(repo.rglob(str(current_source_path))) - if len(local_files) == 1: - return local_files[0] - return None - - def find_diffs(self) -> list[SourceCode]: - """ - Find and save differences between local and remote source codes. - - This method compares the contents of local files with those from the proposal, generating patch files - for any differences found. - - Returns: - list[Compared]: list of missing files. - """ - missing_files = [] - files_with_diffs = [] - - target_repo = self.customer_folder / "modules" - - for source_code in self.source_codes: - local_file = self.__find_most_common_path(Path(source_code.file_name), target_repo) - if not local_file: - missing_files.append(source_code) - continue - - local_content = local_file.read_text().splitlines() - remote_content = source_code.file_content - - diff = difflib.unified_diff(local_content, remote_content, fromfile=str(local_file), tofile=source_code.file_name) - diff_text = '\n'.join(diff) - - if diff_text: - diff_file = f"{local_file.stem}.patch" - files_with_diffs.append( - Compared( - str(local_file), - source_code.file_name, - str(self.check_folder / diff_file) - ) - ) - self._write_to_file(diff_file, diff_text) - - self.__print_diffs_results(missing_files, files_with_diffs) - return missing_files - - def __print_diffs_results(self, missing_files: list[SourceCode], files_with_diffs: list[Compared]): - """ - Print the results of the diff check. - - This method outputs a summary of the comparison, including the number of files compared, - the number of missing files, and the number of files with differences. - - Args: - missing_files (list[SourceCode]): A list of SourceCode objects representing missing files. - files_with_diffs (list[Compared]): A list of Compared objects representing files with differences. - """ - total_number_of_files = len(self.source_codes) - number_of_missing_files = len(missing_files) - number_of_files_with_diffs = len(files_with_diffs) - - msg = f"Compared {total_number_of_files - number_of_missing_files}/{total_number_of_files} files for proposal {self.proposal_address}" - if number_of_missing_files == 0: - pp.pretty_print(msg, pp.Colors.SUCCESS) - else: - pp.pretty_print(msg, pp.Colors.WARNING) - for source_code in missing_files: - pp.pretty_print(f"Missing file: {source_code.file_name} in local repo", pp.Colors.WARNING) - - if number_of_files_with_diffs == 0: - pp.pretty_print("No differences found.", pp.Colors.SUCCESS) - else: - pp.pretty_print(f"Found differences in {number_of_files_with_diffs} files", pp.Colors.FAILURE) - for compared_pair in files_with_diffs: - pp.pretty_print(f"Local: {compared_pair.local_file}\nProposal: {compared_pair.proposal_file}\nDiff: {compared_pair.diff}", pp.Colors.FAILURE) diff --git a/ProposalTools/Checks/feed_price.py b/ProposalTools/Checks/feed_price.py deleted file mode 100644 index b519433..0000000 --- a/ProposalTools/Checks/feed_price.py +++ /dev/null @@ -1,67 +0,0 @@ -from pathlib import Path -import re - -from ProposalTools.apis.chainlink_api import ChainLinkAPI -from ProposalTools.utils.chain_enum import Chain -from ProposalTools.checks.check import Check -from ProposalTools.apis.contract_code.source_code import SourceCode -import ProposalTools.utils.pretty_printer as pp - - -class FeedPriceCheck(Check): - """ - Verifies the price feed addresses in the provided source code against official Chainlink data. - - This class is responsible for verifying that the price feed addresses found in the source code - match the addresses provided by the Chainlink API. It also categorizes these addresses into - verified and violated based on whether they are found in the official source. - """ - - def __init__(self, customer: str, chain: Chain, proposal_address: str, source_codes: list[SourceCode]) -> None: - """ - Initializes the VerifyFeedPrice object with customer information, proposal address, - and source codes to be checked. - - Args: - customer (str): The name of the customer for whom the verification is being performed. - chain (Chain): The blockchain network to verify the price feeds against. - proposal_address (str): The address of the proposal being verified. - source_codes (list[SourceCode]): A list of source code objects containing the Solidity contracts to be checked. - """ - super().__init__(customer, chain, proposal_address, source_codes) - self.api = ChainLinkAPI() - self.address_pattern = r'0x[a-fA-F0-9]{40}' - - # Retrieve price feeds from Chainlink API and map them by contract address - price_feeds = self.api.get_price_feeds_info(self.chain) - self.price_feeds_dict = {feed.contractAddress: feed for feed in price_feeds} - self.price_feeds_dict.update({feed.proxyAddress: feed for feed in price_feeds if feed.proxyAddress}) - - - def verify_feed_price(self) -> None: - """ - Verifies the price feeds in the source code against the Chainlink API for the specified chain. - - This method retrieves price feeds from the Chainlink API, compares them against the addresses - found in the source code, and then categorizes them into verified or violated based on the comparison. - """ - # Iterate through each source code file to find and verify address variables - for source_code in self.source_codes: - verified_sources_path = f"{Path(source_code.file_name).stem.removesuffix('.sol')}/verified_sources.json" - verified_variables = [] - - contract_text = '\n'.join(source_code.file_content) - addresses = re.findall(self.address_pattern, contract_text) - for address in addresses: - if address in self.price_feeds_dict: - feed = self.price_feeds_dict[address] - pp.pretty_print( - f"Found {address} on Chainlink\nname:{feed.name} Decimals:{feed.decimals}", - pp.Colors.SUCCESS - ) - verified_variables.append(feed.dict()) - - if verified_variables: - self._write_to_file(verified_sources_path, verified_variables) - else: - pp.pretty_print(f"No address related to chain link found in {Path(source_code.file_name).stem}", pp.Colors.INFO) \ No newline at end of file diff --git a/ProposalTools/Checks/global_variables.py b/ProposalTools/Checks/global_variables.py deleted file mode 100644 index 9e093fe..0000000 --- a/ProposalTools/Checks/global_variables.py +++ /dev/null @@ -1,104 +0,0 @@ -import re -from pathlib import Path -from solidity_parser.parser import Node - - -from ProposalTools.checks.check import Check -from ProposalTools.apis.contract_code.source_code import SourceCode -import ProposalTools.utils.pretty_printer as pp - - -class GlobalVariableCheck(Check): - """ - A class that performs a check on global variables within Solidity contracts. - - This class checks if global variables in source codes files are either constant or immutable. - """ - - def check_global_variables(self) -> None: - """ - Checks global variables in the source code to ensure they are either constant or immutable. - - This method parses the Solidity source code and checks for variables that do not meet the constant - or immutable criteria. - """ - source_code_to_violated_variables = {} - for source_code in self.source_codes: - violated_variables = self.__check_const(source_code) - violated_variables = self.__check_immutable(violated_variables, source_code.file_content) - - if violated_variables: - source_code_to_violated_variables[source_code.file_name] = violated_variables - - self.__process_results(source_code_to_violated_variables) - - def __check_const(self, source_code: SourceCode) -> list[Node]: - """ - Checks a source code for variables that are not declared as constant. - - Args: - source_code (SourceCode): The Solidity source code obj. - - Returns: - list[Node]: A list of AST nodes representing variables that are not constant. - """ - state_variables = source_code.get_state_variables() - if state_variables: - return [ - v for v in state_variables.values() - if not v.get("isDeclaredConst", False) - ] - return [] - - def __check_immutable(self, variables: list[Node], source_code: list[str]) -> list[Node]: - """ - Checks a list of variables to ensure they are declared as immutable in the source code. - - This method searches the source code to verify that the variables are declared with the 'immutable' keyword. - - Args: - variables (list[Node]): A list of AST nodes representing variables. - source_code (list[str]): The Solidity source code lines. - - Returns: - list[Node]: A list of AST nodes representing variables that are not immutable. - """ - violated_variables = [] - - for variable in variables: - variable_name = variable.get('name') - var_type = variable.get('typeName').get('name', variable.get("namePath", "")) - pattern = rf".*{var_type}.*{variable_name}.*" - - found = False - for line in source_code: - if re.search(pattern, line): - found = True - if "immutable" not in line: - violated_variables.append(variable) - break - - if not found: - violated_variables.append(variable) - - return violated_variables - - def __process_results(self, source_code_to_violated_variables: dict[str, list[Node]]): - """ - Processes the results of the global variable checks and prints them to the console. - - This method logs the results of the global variable check, including any violations found. - - Args: - source_code_to_violated_variables (dict[str, list[Node]]): A dictionary mapping file names - to lists of violated variables. - """ - if not source_code_to_violated_variables: - pp.pretty_print("All global variables are constant or immutable.", pp.Colors.SUCCESS) - else: - pp.pretty_print("Global variable checks failed:", pp.Colors.FAILURE) - for file_name, violated_variables in source_code_to_violated_variables.items(): - pp.pretty_print(f"File {file_name} contains variables that are not constant or immutable" - ,pp.Colors.FAILURE) - self._write_to_file(Path(file_name).stem.removesuffix(".sol"), violated_variables) - \ No newline at end of file diff --git a/ProposalTools/Checks/new_listing.py b/ProposalTools/Checks/new_listing.py deleted file mode 100644 index 7477cd2..0000000 --- a/ProposalTools/Checks/new_listing.py +++ /dev/null @@ -1,215 +0,0 @@ -from solidity_parser.parser import Node -from pydantic import BaseModel - -from ProposalTools.Checks.check import Check -import ProposalTools.Utils.pretty_printer as pp - - -class ListingDetails(BaseModel): - asset: str - assetSymbol: str = None - priceFeedAddress: str = None - - -class FunctionCallDetails(BaseModel): - pool: str - asset: str - asset_seed: str - - -class NewListingCheck(Check): - def new_listing_check(self) -> None: - """ - Checks if the proposal address is a new listing on the blockchain. - - This method retrieves functions from the source codes and checks if there are any new listings. - If new listings are detected, it handles them accordingly. Otherwise, it prints a message indicating - no new listings were found. - """ - functions = self._get_functions_from_source_codes() - - if "newListings" in functions or "newListingsCustom" in functions: - self._handle_new_listings(functions) - else: - pp.pretty_print(f"No new listings detected for {self.proposal_address}", pp.Colors.INFO) - - def _get_functions_from_source_codes(self) -> dict: - """ - Retrieves functions from the source codes. - - This method iterates over the source codes and collects all functions defined in them. - - Returns: - dict: A dictionary where keys are function names and values are function nodes. - """ - functions = {} - for source_code in self.source_codes: - functions.update(source_code.get_functions() if source_code.get_functions() else {}) - return functions - - def _handle_new_listings(self, functions: dict) -> None: - """ - Handles new listings detected in the functions. - - This method extracts listings from the function node and checks for approval and supply calls - related to the listings. It prints messages indicating the status of these calls. - - Args: - functions (dict): A dictionary of functions retrieved from the source codes. - """ - pp.pretty_print(f"New listing detected for {self.proposal_address}", pp.Colors.WARNING) - listings = self.__extract_listings_from_function( - functions.get("newListings", functions.get("newListingsCustom"))._node - ) - if listings: - pp.pretty_print(f"Found {len(listings)} new listings", pp.Colors.SUCCESS) - else: - pp.pretty_print(f"Failed to extract listings from function", pp.Colors.FAILURE) - - approval_calls, supply_calls = self.__extract_approval_and_supply_calls( - functions.get("_postExecute")._node - ) - approval_calls = {call.asset: call for call in approval_calls} - supply_calls = {call.asset: call for call in supply_calls} - - for listing in listings: - self._check_listing_calls(listing, approval_calls, supply_calls) - - def _check_listing_calls(self, listing: ListingDetails, approval_calls: dict, supply_calls: dict) -> None: - """ - Checks the approval and supply calls for a given listing. - - This method verifies if there are approval and supply calls for the given listing and prints - messages indicating the status of these calls. - - Args: - listing (ListingDetails): The details of the listing to check. - approval_calls (dict): A dictionary of approval calls. - supply_calls (dict): A dictionary of supply calls. - """ - pp.pretty_print(f"Listing: {listing}", pp.Colors.WARNING) - if listing.asset not in approval_calls: - pp.pretty_print(f"Missing approval call for {listing.asset}", pp.Colors.FAILURE) - self._write_to_file("missing_approval_calls.json", listing.dict()) - else: - pp.pretty_print(f"Found approval call for {listing.asset}", pp.Colors.SUCCESS) - self._write_to_file("found_approval_calls.json", listing.dict()) - if listing.asset not in supply_calls: - pp.pretty_print(f"Missing supply call for {listing.asset}", pp.Colors.FAILURE) - self._write_to_file("missing_supply_calls.json", listing.dict()) - else: - pp.pretty_print(f"Found supply call for {listing.asset}", pp.Colors.SUCCESS) - self._write_to_file("found_supply_calls.json", listing.dict()) - - def __extract_listings_from_function(self, function_node: Node) -> list[ListingDetails]: - """ - Extracts new listings information from the function node. - - This method simplifies the extraction of new listings by checking the function node for - variable declarations related to listings and extracting the relevant details. - - Args: - function_node (Node): The function node to extract listings from. - - Returns: - list[ListingDetails]: A list of ListingDetails objects representing the new listings. - """ - if function_node.get('type') != 'FunctionDefinition': - return [] - - new_listings = [] - for statement in function_node.get('body', {}).get('statements', []): - if statement.get('type') == 'VariableDeclarationStatement': - for var in statement.get('variables', []): - if var.get('typeName', {}).get('baseTypeName', {}).get('namePath') == 'IAaveV3ConfigEngine.Listing': - new_listings.extend(self._extract_listings_from_statements(function_node)) - return new_listings - - def _extract_listings_from_statements(self, function_node: Node) -> list[ListingDetails]: - """ - Extracts listings from the statements in the function node. - - This method iterates over the statements in the function node and extracts listing details - from the relevant expressions. - - Args: - function_node (Node): The function node to extract listings from. - - Returns: - list[ListingDetails]: A list of ListingDetails objects representing the new listings. - """ - new_listings = [] - for expr_stmt in function_node.get('body', {}).get('statements', []): - if expr_stmt.get('type') == 'ExpressionStatement': - expr = expr_stmt.get('expression', {}) - if expr.get('type') == 'BinaryOperation' and expr.get('operator') == '=': - left = expr.get('left', {}) - if left.get('type') == 'IndexAccess' and left.get('base', {}).get('name') == 'listings': - listing_details = self.__extract_listing_details(expr.get('right', {}).get('arguments', [])) - if listing_details: - new_listings.append(listing_details) - return new_listings - - def __extract_listing_details(self, arguments: list[Node]) -> ListingDetails: - """ - Extracts listing details from function arguments. - - This method extracts the asset, asset symbol, and price feed address from the function arguments - and returns a ListingDetails object. - - Args: - arguments (list[Node]): The list of function arguments to extract details from. - - Returns: - ListingDetails: An object containing the extracted listing details. - """ - listing_info = {} - if arguments: - listing_info['asset'] = arguments[0].get('name') - listing_info['assetSymbol'] = arguments[1].get('value') if len(arguments) > 1 else None - listing_info['priceFeedAddress'] = arguments[2].get('number') if len(arguments) > 2 else None - return ListingDetails(**listing_info) - - def __extract_approval_and_supply_calls(self, function_node: Node) -> tuple[list[FunctionCallDetails], list[FunctionCallDetails]]: - """ - Extracts approval and supply calls from the function node. - - This method iterates over the statements in the function node and extracts details of approval - and supply calls. - - Args: - function_node (Node): The function node to extract calls from. - - Returns: - tuple[list[FunctionCallDetails], list[FunctionCallDetails]]: Two lists containing the details - of approval and supply calls respectively. - """ - approval_calls, supply_calls = [], [] - for statement in function_node.get('body', {}).get('statements', []): - if statement.get('type') == 'ExpressionStatement': - expression = statement.get('expression', {}) - if expression.get('type') == 'FunctionCall': - function_name = expression.get('expression', {}).get('name', "") - if "approve" in function_name or "forceApprove" in function_name: - approval_calls.append(self._extract_function_call_details(expression)) - elif 'supply' in function_name: - supply_calls.append(self._extract_function_call_details(expression)) - return approval_calls, supply_calls - - def _extract_function_call_details(self, expression: dict) -> FunctionCallDetails: - """ - Extracts details of a function call. - - This method extracts the pool, asset, and asset seed from the function call expression - and returns a FunctionCallDetails object. - - Args: - expression (dict): The function call expression to extract details from. - - Returns: - FunctionCallDetails: An object containing the extracted function call details. - """ - pool = expression['arguments'][0]['expression']['name'] - asset = expression['arguments'][1]['name'] - asset_seed = expression['arguments'][2]['name'] - return FunctionCallDetails(pool=pool, asset=asset, asset_seed=asset_seed) \ No newline at end of file From 606dd9225bfd0c9b34d975ce4a8ecbd8ab179757 Mon Sep 17 00:00:00 2001 From: Niv vaknin <122722245+nivcertora@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:53:38 +0300 Subject: [PATCH 04/20] Delete ProposalTools/GIT directory --- ProposalTools/GIT/__init__.py | 0 ProposalTools/GIT/git_manager.py | 65 -------------------------------- 2 files changed, 65 deletions(-) delete mode 100644 ProposalTools/GIT/__init__.py delete mode 100644 ProposalTools/GIT/git_manager.py diff --git a/ProposalTools/GIT/__init__.py b/ProposalTools/GIT/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ProposalTools/GIT/git_manager.py b/ProposalTools/GIT/git_manager.py deleted file mode 100644 index 4060048..0000000 --- a/ProposalTools/GIT/git_manager.py +++ /dev/null @@ -1,65 +0,0 @@ -import json -from pathlib import Path -from git import Repo - -import ProposalTools.config as config -import ProposalTools.utils.pretty_printer as pp - - -class GitManager: - """ - A class to manage Git repositories for a specific customer. - - Attributes: - customer (str): The name or identifier of the customer. - repos (dict): A dictionary mapping repository names to their URLs. - """ - - def __init__(self, customer: str) -> None: - """ - Initialize the GitManager with the given customer name and load the repository URLs. - - Args: - customer (str): The name or identifier of the customer. - """ - self.customer = customer - - self.customer_path = config.MAIN_PATH / self.customer / "modules" - self.customer_path.mkdir(parents=True, exist_ok=True) - - self.repos = self._load_repos() - - def _load_repos(self) -> dict: - """ - Load repository URLs from the JSON file for the given customer. - - Returns: - dict: A dictionary mapping repository names to their URLs. - """ - with open(config.REPOS_PATH) as f: - repos_data = json.load(f) - - # Normalize the customer name to handle case differences - normalized_customer = self.customer.lower() - repos = next((repos for key, repos in repos_data.items() if key.lower() == normalized_customer), []) - - return {Path(r).stem: r for r in repos} - - def clone_or_update(self) -> None: - """ - Clone the repositories for the customer. - - If the repository already exists locally, it will update the repository and its submodules. - Otherwise, it will clone the repository and initialize submodules. - """ - for repo_name, repo_url in self.repos.items(): - repo_path: Path = self.customer_path / repo_name - if repo_path.exists(): - pp.pretty_print(f"Repository {repo_name} already exists at {repo_path}. Updating repo and submodules.", pp.Colors.INFO) - repo = Repo(repo_path) - repo.git.pull() - repo.git.submodule('update', '--init', '--recursive') - else: - pp.pretty_print(f"Cloning {repo_name} from URL: {repo_url} to {repo_path}...", pp.Colors.INFO) - Repo.clone_from(repo_url, repo_path, multi_options=["--recurse-submodules"]) - From 27dafa59b8a23f0a0a0c44752696e0d78ad61955 Mon Sep 17 00:00:00 2001 From: Niv vaknin <122722245+nivcertora@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:53:46 +0300 Subject: [PATCH 05/20] Delete ProposalTools/Utils directory --- ProposalTools/Utils/__init__.py | 0 ProposalTools/Utils/chain_enum.py | 18 ------------------ ProposalTools/Utils/pretty_printer.py | 14 -------------- ProposalTools/Utils/singleton.py | 6 ------ 4 files changed, 38 deletions(-) delete mode 100644 ProposalTools/Utils/__init__.py delete mode 100644 ProposalTools/Utils/chain_enum.py delete mode 100644 ProposalTools/Utils/pretty_printer.py delete mode 100644 ProposalTools/Utils/singleton.py diff --git a/ProposalTools/Utils/__init__.py b/ProposalTools/Utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ProposalTools/Utils/chain_enum.py b/ProposalTools/Utils/chain_enum.py deleted file mode 100644 index cc626c4..0000000 --- a/ProposalTools/Utils/chain_enum.py +++ /dev/null @@ -1,18 +0,0 @@ -from enum import StrEnum - - -class Chain(StrEnum): - """ - Enumeration for supported blockchain networks. - """ - ETH = 'ETH' - ARB = 'ARB' - AVAX = 'AVAX' - BASE = 'BASE' - BSC = 'BSC' - GNO = 'GNO' - MET = 'MET' - OPT = 'OPT' - POLY = 'POLY' - SCR = 'SCR' - ZK = 'ZK' \ No newline at end of file diff --git a/ProposalTools/Utils/pretty_printer.py b/ProposalTools/Utils/pretty_printer.py deleted file mode 100644 index 16d3d4e..0000000 --- a/ProposalTools/Utils/pretty_printer.py +++ /dev/null @@ -1,14 +0,0 @@ -from enum import StrEnum - -class Colors(StrEnum): - SUCCESS = '\033[92m' - FAILURE = '\033[91m' - WARNING = '\033[93m' - INFO = '' - RESET = '\033[0m' - -def pretty_print(message: str, status: Colors): - separator_line = status + '-' * 80 + Colors.RESET - print(separator_line) - print(status + message + Colors.RESET) - print(separator_line) \ No newline at end of file diff --git a/ProposalTools/Utils/singleton.py b/ProposalTools/Utils/singleton.py deleted file mode 100644 index 8a09d95..0000000 --- a/ProposalTools/Utils/singleton.py +++ /dev/null @@ -1,6 +0,0 @@ -class Singleton(type): - _instances = {} - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] \ No newline at end of file From 160ce12d22cc2d4aa22a8a558449320294414957 Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Tue, 10 Sep 2024 16:57:34 +0300 Subject: [PATCH 06/20] Organize Project + add Web3 client --- ProposalTools/checks/new_listing.py | 193 ++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 ProposalTools/checks/new_listing.py diff --git a/ProposalTools/checks/new_listing.py b/ProposalTools/checks/new_listing.py new file mode 100644 index 0000000..6675464 --- /dev/null +++ b/ProposalTools/checks/new_listing.py @@ -0,0 +1,193 @@ +from solidity_parser.parser import Node +from pydantic import BaseModel + +from ProposalTools.checks.check import Check +import ProposalTools.utils.pretty_printer as pp + + +class ListingDetails(BaseModel): + asset: str + assetSymbol: str = None + priceFeedAddress: str = None + + +class FunctionCallDetails(BaseModel): + pool: str + asset: str + asset_seed: str + + +class NewListingCheck(Check): + def new_listing_check(self) -> None: + """ + Checks if the proposal address is a new listing on the blockchain. + This method retrieves functions from the source codes and checks if there are any new listings. + If new listings are detected, it handles them accordingly. Otherwise, it prints a message indicating + no new listings were found. + """ + functions = self._get_functions_from_source_codes() + + if "newListings" in functions or "newListingsCustom" in functions: + self._handle_new_listings(functions) + else: + pp.pretty_print(f"No new listings detected for {self.proposal_address}", pp.Colors.INFO) + + def _get_functions_from_source_codes(self) -> dict: + """ + Retrieves functions from the source codes. + This method iterates over the source codes and collects all functions defined in them. + Returns: + dict: A dictionary where keys are function names and values are function nodes. + """ + functions = {} + for source_code in self.source_codes: + functions.update(source_code.get_functions() if source_code.get_functions() else {}) + return functions + + def _handle_new_listings(self, functions: dict) -> None: + """ + Handles new listings detected in the functions. + This method extracts listings from the function node and checks for approval and supply calls + related to the listings. It prints messages indicating the status of these calls. + Args: + functions (dict): A dictionary of functions retrieved from the source codes. + """ + pp.pretty_print(f"New listing detected for {self.proposal_address}", pp.Colors.WARNING) + listings = self.__extract_listings_from_function( + functions.get("newListings", functions.get("newListingsCustom"))._node + ) + if listings: + pp.pretty_print(f"Found {len(listings)} new listings", pp.Colors.SUCCESS) + else: + pp.pretty_print(f"Failed to extract listings from function", pp.Colors.FAILURE) + + approval_calls, supply_calls = self.__extract_approval_and_supply_calls( + functions.get("_postExecute")._node + ) + approval_calls = {call.asset: call for call in approval_calls} + supply_calls = {call.asset: call for call in supply_calls} + + for listing in listings: + self._check_listing_calls(listing, approval_calls, supply_calls) + + def _check_listing_calls(self, listing: ListingDetails, approval_calls: dict, supply_calls: dict) -> None: + """ + Checks the approval and supply calls for a given listing. + This method verifies if there are approval and supply calls for the given listing and prints + messages indicating the status of these calls. + Args: + listing (ListingDetails): The details of the listing to check. + approval_calls (dict): A dictionary of approval calls. + supply_calls (dict): A dictionary of supply calls. + """ + pp.pretty_print(f"Listing: {listing}", pp.Colors.WARNING) + if listing.asset not in approval_calls: + pp.pretty_print(f"Missing approval call for {listing.asset}", pp.Colors.FAILURE) + self._write_to_file("missing_approval_calls.json", listing.dict()) + else: + pp.pretty_print(f"Found approval call for {listing.asset}", pp.Colors.SUCCESS) + self._write_to_file("found_approval_calls.json", listing.dict()) + if listing.asset not in supply_calls: + pp.pretty_print(f"Missing supply call for {listing.asset}", pp.Colors.FAILURE) + self._write_to_file("missing_supply_calls.json", listing.dict()) + else: + pp.pretty_print(f"Found supply call for {listing.asset}", pp.Colors.SUCCESS) + self._write_to_file("found_supply_calls.json", listing.dict()) + + def __extract_listings_from_function(self, function_node: Node) -> list[ListingDetails]: + """ + Extracts new listings information from the function node. + This method simplifies the extraction of new listings by checking the function node for + variable declarations related to listings and extracting the relevant details. + Args: + function_node (Node): The function node to extract listings from. + Returns: + list[ListingDetails]: A list of ListingDetails objects representing the new listings. + """ + if function_node.get('type') != 'FunctionDefinition': + return [] + + new_listings = [] + for statement in function_node.get('body', {}).get('statements', []): + if statement.get('type') == 'VariableDeclarationStatement': + for var in statement.get('variables', []): + if var.get('typeName', {}).get('baseTypeName', {}).get('namePath') == 'IAaveV3ConfigEngine.Listing': + new_listings.extend(self._extract_listings_from_statements(function_node)) + return new_listings + + def _extract_listings_from_statements(self, function_node: Node) -> list[ListingDetails]: + """ + Extracts listings from the statements in the function node. + This method iterates over the statements in the function node and extracts listing details + from the relevant expressions. + Args: + function_node (Node): The function node to extract listings from. + Returns: + list[ListingDetails]: A list of ListingDetails objects representing the new listings. + """ + new_listings = [] + for expr_stmt in function_node.get('body', {}).get('statements', []): + if expr_stmt.get('type') == 'ExpressionStatement': + expr = expr_stmt.get('expression', {}) + if expr.get('type') == 'BinaryOperation' and expr.get('operator') == '=': + left = expr.get('left', {}) + if left.get('type') == 'IndexAccess' and left.get('base', {}).get('name') == 'listings': + listing_details = self.__extract_listing_details(expr.get('right', {}).get('arguments', [])) + if listing_details: + new_listings.append(listing_details) + return new_listings + + def __extract_listing_details(self, arguments: list[Node]) -> ListingDetails: + """ + Extracts listing details from function arguments. + This method extracts the asset, asset symbol, and price feed address from the function arguments + and returns a ListingDetails object. + Args: + arguments (list[Node]): The list of function arguments to extract details from. + Returns: + ListingDetails: An object containing the extracted listing details. + """ + listing_info = {} + if arguments: + listing_info['asset'] = arguments[0].get('name') + listing_info['assetSymbol'] = arguments[1].get('value') if len(arguments) > 1 else None + listing_info['priceFeedAddress'] = arguments[2].get('number') if len(arguments) > 2 else None + return ListingDetails(**listing_info) + + def __extract_approval_and_supply_calls(self, function_node: Node) -> tuple[list[FunctionCallDetails], list[FunctionCallDetails]]: + """ + Extracts approval and supply calls from the function node. + This method iterates over the statements in the function node and extracts details of approval + and supply calls. + Args: + function_node (Node): The function node to extract calls from. + Returns: + tuple[list[FunctionCallDetails], list[FunctionCallDetails]]: Two lists containing the details + of approval and supply calls respectively. + """ + approval_calls, supply_calls = [], [] + for statement in function_node.get('body', {}).get('statements', []): + if statement.get('type') == 'ExpressionStatement': + expression = statement.get('expression', {}) + if expression.get('type') == 'FunctionCall': + function_name = expression.get('expression', {}).get('name', "") + if "approve" in function_name or "forceApprove" in function_name: + approval_calls.append(self._extract_function_call_details(expression)) + elif 'supply' in function_name: + supply_calls.append(self._extract_function_call_details(expression)) + return approval_calls, supply_calls + + def _extract_function_call_details(self, expression: dict) -> FunctionCallDetails: + """ + Extracts details of a function call. + This method extracts the pool, asset, and asset seed from the function call expression + and returns a FunctionCallDetails object. + Args: + expression (dict): The function call expression to extract details from. + Returns: + FunctionCallDetails: An object containing the extracted function call details. + """ + pool = expression['arguments'][0]['expression']['name'] + asset = expression['arguments'][1]['name'] + asset_seed = expression['arguments'][2]['name'] + return FunctionCallDetails(pool=pool, asset=asset, asset_seed=asset_seed) From cefef34a620e66a31f42e51775067bba89252b73 Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Tue, 10 Sep 2024 17:01:41 +0300 Subject: [PATCH 07/20] Organize Project + add Web3 client --- ProposalTools/apis/contract_code/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ProposalTools/apis/contract_code/__init__.py diff --git a/ProposalTools/apis/contract_code/__init__.py b/ProposalTools/apis/contract_code/__init__.py new file mode 100644 index 0000000..e69de29 From 57f97a55592d1fc80481748e8182bd19d028aeb5 Mon Sep 17 00:00:00 2001 From: nivcertora Date: Tue, 10 Sep 2024 14:05:12 +0000 Subject: [PATCH 08/20] Auto change version. --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 483a268..9baeb8b 100644 --- a/version +++ b/version @@ -1 +1 @@ -20240828.104045.704958 +20240910.140512.451893 From 69887342d1570f8dfef768f2f2cd13db62dff263 Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Wed, 11 Sep 2024 10:23:57 +0300 Subject: [PATCH 09/20] Emphasis that the client is only ETH mainnet --- .github/workflows/ci.yml | 1 + .gitignore | 2 +- ProposalTools/apis/web3_client.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08c2023..fe3a879 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: GNOSCAN_API_KEY: ${{ secrets.GNOSCAN_API_KEY }} SCRSCAN_API_KEY: ${{ secrets.SCRSCAN_API_KEY }} BSCSCAN_API_KEY: ${{ secrets.BSCSCAN_API_KEY }} + ETH_MAINNET_API: ${{ secrets.ETH_MAINNET_API }} PRP_TOOL_PATH: "." permissions: diff --git a/.gitignore b/.gitignore index 261b40e..56f6713 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ **.ipynb **__pycache__ -**/Tests +**tests **/.vscode \ No newline at end of file diff --git a/ProposalTools/apis/web3_client.py b/ProposalTools/apis/web3_client.py index c5ad49d..810652f 100644 --- a/ProposalTools/apis/web3_client.py +++ b/ProposalTools/apis/web3_client.py @@ -30,7 +30,7 @@ def __init__(self, api_key: Optional[str] = None): ValueError: If the Infura API key is not set or invalid. ConnectionError: If the connection to the Infura API fails. """ - self.api_key = api_key or os.getenv("INFURA_API_KEY") + self.api_key = api_key or os.getenv("ETH_MAINNET_API") if self.api_key is None: raise ValueError("Infura API key is required. Set it in the 'INFURA_API_KEY' environment variable or pass it as an argument.") From 2c157c5293856fdb0a92b1020a13e4153ad1d276 Mon Sep 17 00:00:00 2001 From: nivcertora Date: Wed, 11 Sep 2024 07:28:07 +0000 Subject: [PATCH 10/20] Auto change version. --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 9baeb8b..976a98a 100644 --- a/version +++ b/version @@ -1 +1 @@ -20240910.140512.451893 +20240911.072807.616785 From 554cbc056f534a4340aff9befecccefde5a5299a Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Wed, 11 Sep 2024 14:59:14 +0300 Subject: [PATCH 11/20] Add support for call function api for soldity smart contract --- .../__init__.py | 0 .../chains_api.py} | 114 ++++++++++++++++-- .../source_code.py | 0 ProposalTools/apis/web3_client.py | 78 ------------ ProposalTools/check_proposal.py | 4 +- ProposalTools/checks/check.py | 2 +- ProposalTools/checks/diff.py | 2 +- ProposalTools/checks/feed_price.py | 2 +- ProposalTools/checks/global_variables.py | 2 +- requirements.txt | 4 +- 10 files changed, 116 insertions(+), 92 deletions(-) rename ProposalTools/apis/{contract_code => block_explorers}/__init__.py (100%) rename ProposalTools/apis/{contract_code/contract_source_code_api.py => block_explorers/chains_api.py} (53%) rename ProposalTools/apis/{contract_code => block_explorers}/source_code.py (100%) delete mode 100644 ProposalTools/apis/web3_client.py diff --git a/ProposalTools/apis/contract_code/__init__.py b/ProposalTools/apis/block_explorers/__init__.py similarity index 100% rename from ProposalTools/apis/contract_code/__init__.py rename to ProposalTools/apis/block_explorers/__init__.py diff --git a/ProposalTools/apis/contract_code/contract_source_code_api.py b/ProposalTools/apis/block_explorers/chains_api.py similarity index 53% rename from ProposalTools/apis/contract_code/contract_source_code_api.py rename to ProposalTools/apis/block_explorers/chains_api.py index 06e651c..af6016d 100644 --- a/ProposalTools/apis/contract_code/contract_source_code_api.py +++ b/ProposalTools/apis/block_explorers/chains_api.py @@ -1,11 +1,14 @@ from dataclasses import dataclass import requests -from typing import List, Callable +from typing import Callable, Any import os import json +from eth_abi import encode +from eth_utils import keccak + from ProposalTools.utils.chain_enum import Chain -from ProposalTools.apis.contract_code.source_code import SourceCode +from ProposalTools.apis.block_explorers.source_code import SourceCode @dataclass @@ -16,7 +19,7 @@ class APIinfo: base_url: str api_key: Callable[[], str] -class ContractSourceCodeAPI(): +class ChainAPI(): """ Manages interactions with blockchain explorer APIs to fetch smart contract source codes. @@ -67,9 +70,9 @@ def __init__(self, chain: Chain) -> None: if not self.api_key: raise ValueError(f"{chain}SCAN_API_KEY environment variable is not set.") - self.base_url = f"{api_info.base_url}?module=contract&apikey={self.api_key}" + self.base_url = f"{api_info.base_url}?apikey={self.api_key}" - def get_source_code(self, proposal_address: str) -> List[SourceCode]: + def get_source_code(self, proposal_address: str) -> list[SourceCode]: """ Fetch the source code of a smart contract from the blockchain explorer API. @@ -82,7 +85,7 @@ def get_source_code(self, proposal_address: str) -> List[SourceCode]: Raises: ValueError: If there's an error fetching the source code. """ - url = f"{self.base_url}&action=getsourcecode&address={proposal_address}" + url = f"{self.base_url}&module=contract&action=getsourcecode&address={proposal_address}" response = requests.get(url) response.raise_for_status() data = response.json() @@ -117,7 +120,7 @@ def get_contract_abi(self, contract_address: str) -> list[dict]: Raises: ValueError: If there's an error fetching the ABI. """ - url = f"{self.base_url}?&action=getabi&address={contract_address}" + url = f"{self.base_url}&module=contract&action=getabi&address={contract_address}" response = requests.get(url) response.raise_for_status() data = response.json() @@ -128,3 +131,100 @@ def get_contract_abi(self, contract_address: str) -> list[dict]: abi = json.loads(data['result']) return abi + def call_contract_function(self, contract_address: str, function_name: str, arguments: list | None = None) -> Any: + """ + Encodes the ABI and calls a smart contract function using the blockchain explorer's eth_call proxy API. + + Args: + contract_address (str): The address of the smart contract. + function_name (str): The name of the function to call (e.g., "balanceOf"). + arguments (list[Any]): The arguments to be passed to the contract function. + + Returns: + Any: The result of the contract function call, cleaned if it's an address. + + Raises: + ValueError: If there's an error making the API call. + """ + # Step 1: Get the contract ABI + abi = self.get_contract_abi(contract_address) + + # Step 2: Get the function ABI and method ID + function_abi = self._get_function_abi(function_name, abi) + method_id = self._get_method_id(function_abi) + + # Step 3: Encode the function arguments + if arguments is None: + arguments = [] + encoded_args = self._encode_arguments(function_abi, arguments) + + # Step 4: Form the data payload for the eth_call + data = method_id + encoded_args + + # Step 5: Prepare the request URL + url = f"{self.base_url}&module=proxy&action=eth_call&to={contract_address}&data={data}&tag=latest" + + # Step 6: Make the request to the blockchain explorer + response = requests.get(url) + response.raise_for_status() + + # Step 7: Process the response + result = response.json().get('result') + if not result: + raise ValueError(f"Error calling contract function: {response.json()}") + + # Step 8: Detect if the return type is an address and clean it if necessary + if function_abi.get('outputs') and function_abi['outputs'][0]['type'] == 'address': + # Clean the address by removing the leading zeros + result = "0x" + result[-40:] # Keep only the last 20 bytes (40 hex chars) + + return result + + def _get_function_abi(self, function_name: str, abi: list[dict]) -> dict: + """ + Retrieves the ABI for the specified function from the contract's ABI. + + Args: + function_name (str): The name of the function to retrieve. + abi (list[dict]): The contract ABI. + + Returns: + dict: The ABI of the function. + + Raises: + ValueError: If the function is not found in the ABI. + """ + for item in abi: + if item['type'] == 'function' and item['name'] == function_name: + return item + raise ValueError(f"Function {function_name} not found in contract ABI.") + + + def _get_method_id(self, function_abi: dict) -> str: + """ + Generates the method ID (the first 4 bytes of the keccak-256 hash of the function signature). + + Args: + function_abi (dict): The ABI of the function. + + Returns: + str: The 4-byte method ID in hex format (without '0x'). + """ + function_signature = f"{function_abi['name']}({','.join([input['type'] for input in function_abi['inputs']])})" + return keccak(text=function_signature).hex()[:10] # First 4 bytes = first 8 hex chars + + + def _encode_arguments(self, function_abi: dict, arguments: list) -> str: + """ + Encodes the arguments using the ABI format. + + Args: + function_abi (dict): The ABI of the function. + arguments (list[Any]): The arguments to be encoded. + + Returns: + str: Encoded arguments as a hex string (without '0x'). + """ + argument_types = [input['type'] for input in function_abi['inputs']] + encoded_args = encode(argument_types, arguments) + return encoded_args.hex() diff --git a/ProposalTools/apis/contract_code/source_code.py b/ProposalTools/apis/block_explorers/source_code.py similarity index 100% rename from ProposalTools/apis/contract_code/source_code.py rename to ProposalTools/apis/block_explorers/source_code.py diff --git a/ProposalTools/apis/web3_client.py b/ProposalTools/apis/web3_client.py deleted file mode 100644 index 810652f..0000000 --- a/ProposalTools/apis/web3_client.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -from typing import Any, Optional - -from web3 import Web3 -from web3.exceptions import ContractLogicError - -from ProposalTools.apis.contract_code.contract_source_code_api import ContractSourceCodeAPI, Chain - - -class Web3API: - """ - Web3API is a client for interacting with Ethereum smart contracts through the Infura API. - - This class manages connection to the Ethereum blockchain using Web3 and Infura, and provides - functionality for interacting with deployed smart contracts. - - Attributes: - w3_client (Web3): The Web3 client connected to an Ethereum provider. - """ - - def __init__(self, api_key: Optional[str] = None): - """ - Initializes the Web3API client with the Infura API. - - Args: - api_key (Optional[str]): The Infura API key to connect to the Ethereum blockchain. - If not provided, it will be fetched from the environment variable `INFURA_API_KEY`. - - Raises: - ValueError: If the Infura API key is not set or invalid. - ConnectionError: If the connection to the Infura API fails. - """ - self.api_key = api_key or os.getenv("ETH_MAINNET_API") - if self.api_key is None: - raise ValueError("Infura API key is required. Set it in the 'INFURA_API_KEY' environment variable or pass it as an argument.") - - infura_url = f"https://mainnet.infura.io/v3/{self.api_key}" - self.w3_client = Web3(Web3.HTTPProvider(infura_url)) - - # Verify connection to Ethereum node - if not self.w3_client.isConnected(): - raise ConnectionError("Failed to connect to Infura API. Check your network connection and API key.") - - # ETHScan API to retrieve the abi - self.contract_code_api = ContractSourceCodeAPI(Chain.ETH) - - def call_contract_function(self, contract_address: str, function_name: str, *args: Any) -> Any: - """ - Calls a specified function on an Ethereum smart contract. - - Args: - contract_address (str): The Ethereum address of the smart contract. - function_name (str): The name of the contract function to call. - *args (Any): Any arguments that need to be passed to the contract function. - - Returns: - Any: The result of the contract function call. - - Raises: - ValueError: If the contract ABI or function name is invalid. - ContractLogicError: If there's an error calling the contract function (e.g., invalid function name or input). - """ - contract_abi = self.contract_code_api.get_contract_abi(contract_address) - - try: - contract = self.w3_client.eth.contract(address=contract_address, abi=contract_abi) - except Exception as e: - raise ValueError(f"Error initializing contract: {str(e)}") - - try: - contract_function = getattr(contract.functions, function_name) - except AttributeError: - raise ValueError(f"Function '{function_name}' not found in contract.") - - try: - return contract_function(*args).call() - except ContractLogicError as e: - raise ContractLogicError(f"Error executing contract function '{function_name}': {str(e)}") diff --git a/ProposalTools/check_proposal.py b/ProposalTools/check_proposal.py index cd0db07..240f37e 100644 --- a/ProposalTools/check_proposal.py +++ b/ProposalTools/check_proposal.py @@ -5,7 +5,7 @@ from ProposalTools.utils.chain_enum import Chain import ProposalTools.utils.pretty_printer as pp from ProposalTools.git.git_manager import GitManager -from ProposalTools.apis.contract_code.contract_source_code_api import ContractSourceCodeAPI +from ProposalTools.apis.block_explorers.chains_api import ChainAPI import ProposalTools.checks as Checks @@ -57,7 +57,7 @@ def proposals_check(customer: str, chain_name: str, proposal_addresses: list[str proposal_addresses (list[str]): List of proposal addresses. """ chain = Chain[chain_name.upper()] - api = ContractSourceCodeAPI(chain) + api = ChainAPI(chain) pp.pretty_print(f"Processing customer {customer}, for chain: {chain}", pp.Colors.INFO) for proposal_address in proposal_addresses: diff --git a/ProposalTools/checks/check.py b/ProposalTools/checks/check.py index ddd7de6..15852f5 100644 --- a/ProposalTools/checks/check.py +++ b/ProposalTools/checks/check.py @@ -4,7 +4,7 @@ from pathlib import Path import ProposalTools.config as config -from ProposalTools.apis.contract_code.source_code import SourceCode +from ProposalTools.apis.block_explorers.source_code import SourceCode from ProposalTools.utils.chain_enum import Chain diff --git a/ProposalTools/checks/diff.py b/ProposalTools/checks/diff.py index f50b430..14087af 100644 --- a/ProposalTools/checks/diff.py +++ b/ProposalTools/checks/diff.py @@ -3,7 +3,7 @@ from typing import Optional from dataclasses import dataclass -from ProposalTools.apis.contract_code.source_code import SourceCode +from ProposalTools.apis.block_explorers.source_code import SourceCode from ProposalTools.checks.check import Check import ProposalTools.utils.pretty_printer as pp diff --git a/ProposalTools/checks/feed_price.py b/ProposalTools/checks/feed_price.py index b519433..cdb199f 100644 --- a/ProposalTools/checks/feed_price.py +++ b/ProposalTools/checks/feed_price.py @@ -4,7 +4,7 @@ from ProposalTools.apis.chainlink_api import ChainLinkAPI from ProposalTools.utils.chain_enum import Chain from ProposalTools.checks.check import Check -from ProposalTools.apis.contract_code.source_code import SourceCode +from ProposalTools.apis.block_explorers.source_code import SourceCode import ProposalTools.utils.pretty_printer as pp diff --git a/ProposalTools/checks/global_variables.py b/ProposalTools/checks/global_variables.py index 9e093fe..39631df 100644 --- a/ProposalTools/checks/global_variables.py +++ b/ProposalTools/checks/global_variables.py @@ -4,7 +4,7 @@ from ProposalTools.checks.check import Check -from ProposalTools.apis.contract_code.source_code import SourceCode +from ProposalTools.apis.block_explorers.source_code import SourceCode import ProposalTools.utils.pretty_printer as pp diff --git a/requirements.txt b/requirements.txt index 05e1c91..2e545e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ requests GitPython solidity-parser -pydantic \ No newline at end of file +pydantic +eth-abi +eth-utils From c8fb59c9d6ee46d262d7edda45fab3421d7e4aab Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Wed, 11 Sep 2024 15:02:45 +0300 Subject: [PATCH 12/20] Remove not needed api --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe3a879..08c2023 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,6 @@ jobs: GNOSCAN_API_KEY: ${{ secrets.GNOSCAN_API_KEY }} SCRSCAN_API_KEY: ${{ secrets.SCRSCAN_API_KEY }} BSCSCAN_API_KEY: ${{ secrets.BSCSCAN_API_KEY }} - ETH_MAINNET_API: ${{ secrets.ETH_MAINNET_API }} PRP_TOOL_PATH: "." permissions: From a431f57ac5d554c3ac4b3439b5d70916643021ba Mon Sep 17 00:00:00 2001 From: nivcertora Date: Wed, 11 Sep 2024 12:06:34 +0000 Subject: [PATCH 13/20] Auto change version. --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 976a98a..7d14663 100644 --- a/version +++ b/version @@ -1 +1 @@ -20240911.072807.616785 +20240911.120634.632897 From b3bb71657f5edd457bba6949504bd18706aa5b59 Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Wed, 11 Sep 2024 15:07:31 +0300 Subject: [PATCH 14/20] Clear the docs --- .../apis/block_explorers/chains_api.py | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/ProposalTools/apis/block_explorers/chains_api.py b/ProposalTools/apis/block_explorers/chains_api.py index af6016d..e3f2340 100644 --- a/ProposalTools/apis/block_explorers/chains_api.py +++ b/ProposalTools/apis/block_explorers/chains_api.py @@ -14,18 +14,28 @@ @dataclass class APIinfo: """ - Data class for storing API base URL and API key retrieval function. + Data class for storing API base URL and a function to retrieve the API key. + + Attributes: + base_url (str): The base URL of the blockchain explorer API. + api_key (Callable[[], str]): A function to retrieve the API key for the explorer. """ base_url: str api_key: Callable[[], str] -class ChainAPI(): + +class ChainAPI: """ - Manages interactions with blockchain explorer APIs to fetch smart contract source codes. + A class to interact with blockchain explorer APIs for fetching contract ABIs, + source code, and calling smart contract functions using the 'eth_call' proxy. Attributes: chain_mapping (dict): Maps Chain enum to APIinfo containing base URL and API key function. + base_url (str): The full base URL for making requests to the selected chain. + api_key (str): The API key required to access the blockchain explorer API. """ + + # Maps Chain enums to their corresponding API base URL and API key retrieval function. chain_mapping = { Chain.ETH: APIinfo(base_url="https://api.etherscan.io/api", api_key=lambda: os.getenv('ETHSCAN_API_KEY')), @@ -49,21 +59,20 @@ class ChainAPI(): api_key=lambda: os.getenv('SCRSCAN_API_KEY')), Chain.ZK: APIinfo(base_url="https://api-era.zksync.network/api", api_key=lambda: os.getenv('ZKSCAN_API_KEY')) - } def __init__(self, chain: Chain) -> None: """ - Initialize APIManager with a specific blockchain chain. + Initializes the ChainAPI with the appropriate blockchain network's base URL and API key. Args: - chain (Chain): The blockchain network to use (from the Chain enum). + chain (Chain): The blockchain network to interact with (from the Chain enum). Raises: - ValueError: If the chain is not supported or the API key is not set. + ValueError: If the selected chain is unsupported or the API key is not set. """ if chain not in self.chain_mapping: - raise ValueError(f"Unsupported chain: {chain}. Try one of the following: {', '.join([c.name for c in self.chain_mapping.keys()])}") + raise ValueError(f"Unsupported chain: {chain}. Available chains: {', '.join([c.name for c in self.chain_mapping.keys()])}") api_info = self.chain_mapping[chain] self.api_key = api_info.api_key() @@ -74,16 +83,16 @@ def __init__(self, chain: Chain) -> None: def get_source_code(self, proposal_address: str) -> list[SourceCode]: """ - Fetch the source code of a smart contract from the blockchain explorer API. + Fetches the source code of a smart contract from the blockchain explorer API. Args: - proposal_address (str): The address of the smart contract. + proposal_address (str): The address of the smart contract to retrieve the source code. Returns: - List[SourceCode]: A list of SourceCode objects containing file names and contents. + list[SourceCode]: A list of SourceCode objects containing the file names and source code contents. Raises: - ValueError: If there's an error fetching the source code. + ValueError: If the API request fails or the source code could not be retrieved. """ url = f"{self.base_url}&module=contract&action=getsourcecode&address={proposal_address}" response = requests.get(url) @@ -92,12 +101,12 @@ def get_source_code(self, proposal_address: str) -> list[SourceCode]: if data['status'] != '1': raise ValueError(f"Error fetching source code: {data.get('message', 'Unknown error')}\n{data.get('result')}") - + result = data['result'][0]["SourceCode"] try: json_data = json.loads(result) except json.JSONDecodeError: - # Handle non-JSON formatted response + # Handle non-JSON formatted responses json_data = json.loads(result.removeprefix("{").removesuffix("}")) sources = json_data.get("sources", {proposal_address: {"content": result}}) @@ -109,7 +118,7 @@ def get_source_code(self, proposal_address: str) -> list[SourceCode]: def get_contract_abi(self, contract_address: str) -> list[dict]: """ - Fetch the ABI of a smart contract from the blockchain explorer API. + Fetches the ABI of a smart contract from the blockchain explorer API. Args: contract_address (str): The address of the smart contract. @@ -118,7 +127,7 @@ def get_contract_abi(self, contract_address: str) -> list[dict]: list[dict]: The contract ABI as a list of dictionaries. Raises: - ValueError: If there's an error fetching the ABI. + ValueError: If the API request fails or the ABI could not be retrieved. """ url = f"{self.base_url}&module=contract&action=getabi&address={contract_address}" response = requests.get(url) @@ -128,64 +137,60 @@ def get_contract_abi(self, contract_address: str) -> list[dict]: if data['status'] != '1': raise ValueError(f"Error fetching contract ABI: {data.get('message', 'Unknown error')}\n{data.get('result')}") - abi = json.loads(data['result']) - return abi + return json.loads(data['result']) - def call_contract_function(self, contract_address: str, function_name: str, arguments: list | None = None) -> Any: + def call_contract_function(self, contract_address: str, function_name: str, arguments: list[Any] | None = None) -> Any: """ Encodes the ABI and calls a smart contract function using the blockchain explorer's eth_call proxy API. Args: contract_address (str): The address of the smart contract. function_name (str): The name of the function to call (e.g., "balanceOf"). - arguments (list[Any]): The arguments to be passed to the contract function. + arguments (list[Any]): The arguments to pass to the contract function (if any). Returns: - Any: The result of the contract function call, cleaned if it's an address. + Any: The result of the contract function call, with cleaned output if the return type is an address. Raises: - ValueError: If there's an error making the API call. + ValueError: If the API request fails or there is an error with the function call. """ - # Step 1: Get the contract ABI + # Step 1: Fetch the contract ABI abi = self.get_contract_abi(contract_address) - # Step 2: Get the function ABI and method ID + # Step 2: Retrieve the function ABI and compute the method ID function_abi = self._get_function_abi(function_name, abi) method_id = self._get_method_id(function_abi) - # Step 3: Encode the function arguments + # Step 3: Encode the arguments if arguments is None: arguments = [] encoded_args = self._encode_arguments(function_abi, arguments) - # Step 4: Form the data payload for the eth_call + # Step 4: Prepare the data payload for the eth_call data = method_id + encoded_args - # Step 5: Prepare the request URL + # Step 5: Make the request to the blockchain explorer eth_call endpoint url = f"{self.base_url}&module=proxy&action=eth_call&to={contract_address}&data={data}&tag=latest" - - # Step 6: Make the request to the blockchain explorer response = requests.get(url) response.raise_for_status() - # Step 7: Process the response + # Step 6: Handle the response result = response.json().get('result') if not result: raise ValueError(f"Error calling contract function: {response.json()}") - # Step 8: Detect if the return type is an address and clean it if necessary + # Step 7: Clean the result if the return type is an address if function_abi.get('outputs') and function_abi['outputs'][0]['type'] == 'address': - # Clean the address by removing the leading zeros - result = "0x" + result[-40:] # Keep only the last 20 bytes (40 hex chars) + result = "0x" + result[-40:] # Keep only the last 20 bytes (40 hex chars) of the address return result def _get_function_abi(self, function_name: str, abi: list[dict]) -> dict: """ - Retrieves the ABI for the specified function from the contract's ABI. + Retrieves the ABI of a specific function from the contract ABI. Args: - function_name (str): The name of the function to retrieve. + function_name (str): The name of the function. abi (list[dict]): The contract ABI. Returns: @@ -199,31 +204,29 @@ def _get_function_abi(self, function_name: str, abi: list[dict]) -> dict: return item raise ValueError(f"Function {function_name} not found in contract ABI.") - def _get_method_id(self, function_abi: dict) -> str: """ - Generates the method ID (the first 4 bytes of the keccak-256 hash of the function signature). + Generates the method ID from the function signature (first 4 bytes of the keccak-256 hash). Args: function_abi (dict): The ABI of the function. Returns: - str: The 4-byte method ID in hex format (without '0x'). + str: The 4-byte method ID as a hex string. """ function_signature = f"{function_abi['name']}({','.join([input['type'] for input in function_abi['inputs']])})" - return keccak(text=function_signature).hex()[:10] # First 4 bytes = first 8 hex chars - + return keccak(text=function_signature).hex()[:10] # First 4 bytes = first 8 hex characters - def _encode_arguments(self, function_abi: dict, arguments: list) -> str: + def _encode_arguments(self, function_abi: dict, arguments: list[Any]) -> str: """ - Encodes the arguments using the ABI format. + Encodes function arguments in ABI format. Args: function_abi (dict): The ABI of the function. - arguments (list[Any]): The arguments to be encoded. + arguments (list[Any]): The function arguments to encode. Returns: - str: Encoded arguments as a hex string (without '0x'). + str: The ABI-encoded arguments as a hex string. """ argument_types = [input['type'] for input in function_abi['inputs']] encoded_args = encode(argument_types, arguments) From 631ba2252deb6f2764a98896d91ec91428da09e4 Mon Sep 17 00:00:00 2001 From: nivcertora Date: Wed, 11 Sep 2024 12:12:02 +0000 Subject: [PATCH 15/20] Auto change version. --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 7d14663..1234713 100644 --- a/version +++ b/version @@ -1 +1 @@ -20240911.120634.632897 +20240911.121202.211171 From 6033c760021bd37391abc0006d6bb96d90707c77 Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Thu, 12 Sep 2024 14:21:36 +0300 Subject: [PATCH 16/20] Add API integration tests --- .github/workflows/ci.yml | 2 +- .gitignore | 1 - ProposalTools/tests/__init__.py | 0 ProposalTools/tests/blockchain_api_test.py | 33 ++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 ProposalTools/tests/__init__.py create mode 100644 ProposalTools/tests/blockchain_api_test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08c2023..de9d1a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - name: Execute Regression Tests run: | - ls -l + pytest ~/tests --maxfail=1 --disable-warnings --tb=short CheckProposal --config ProposalTools/execution.json diff --git a/.gitignore b/.gitignore index 56f6713..e58d9a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ **.ipynb **__pycache__ -**tests **/.vscode \ No newline at end of file diff --git a/ProposalTools/tests/__init__.py b/ProposalTools/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ProposalTools/tests/blockchain_api_test.py b/ProposalTools/tests/blockchain_api_test.py new file mode 100644 index 0000000..f350e84 --- /dev/null +++ b/ProposalTools/tests/blockchain_api_test.py @@ -0,0 +1,33 @@ +import pytest +from ProposalTools.utils.chain_enum import Chain +from ProposalTools.apis.block_explorers.chains_api import ChainAPI + +@pytest.mark.parametrize( + "chain, contract_address, function_name, expected_result", + [ + (Chain.SCR, "0x32f924C0e0F1Abf5D1ff35B05eBc5E844dEdD2A9", "BASE_TO_USD_AGGREGATOR", "0x6bF14CB0A831078629D993FDeBcB182b21A8774C"), + (Chain.ZK, "0x162C97F6B4FA5a915A44D430bb7AE0eE716b3b87", "ASSET_TO_USD_AGGREGATOR", "0x1824D297C6d6D311A204495277B63e943C2D376E") + ] +) +def test_chain_api_integration(chain, contract_address, function_name, expected_result): + """ + End-to-end integration test for the ChainAPI class: + 1. Check if source code fetch works without crashing. + 2. Fetch the ABI of the contract. + 3. Call the smart contract function and verify the result. + """ + + # Step 1: Create a ChainAPI instance for the specified chain + api = ChainAPI(chain) + + # Step 2: Fetch the source code of the contract and verify it doesn't crash + source_code = api.get_source_code(contract_address) + assert source_code is not None, f"Source code retrieval failed for contract: {contract_address}" + + # Step 3: Fetch the ABI of the contract + abi = api.get_contract_abi(contract_address) + assert abi is not None and len(abi) > 0, f"ABI retrieval failed for contract: {contract_address}" + + # Step 4: Call the contract function and verify the result + result = api.call_contract_function(contract_address, function_name) + assert result.lower() == expected_result.lower(), f"Function call returned incorrect result: {result}. Expected: {expected_result}" diff --git a/requirements.txt b/requirements.txt index 2e545e2..703f74a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ solidity-parser pydantic eth-abi eth-utils +pytest From 61664208c1e3fd74db5a33a375c7e68f76c7b7fd Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Thu, 12 Sep 2024 14:26:23 +0300 Subject: [PATCH 17/20] Fix pytest tests path --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de9d1a0..00beb14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,8 @@ jobs: - name: Execute Regression Tests run: | - pytest ~/tests --maxfail=1 --disable-warnings --tb=short + ls -l + pytest ProposalTools/tests --maxfail=1 --disable-warnings --tb=short CheckProposal --config ProposalTools/execution.json From 3c034dc39a01a2ec19c9b2491d602088a8ae787a Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Thu, 12 Sep 2024 14:34:02 +0300 Subject: [PATCH 18/20] Update requirements --- .github/workflows/ci.yml | 1 - requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00beb14..4d9f7e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,6 @@ jobs: - name: Execute Regression Tests run: | - ls -l pytest ProposalTools/tests --maxfail=1 --disable-warnings --tb=short CheckProposal --config ProposalTools/execution.json diff --git a/requirements.txt b/requirements.txt index 703f74a..e0e968d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pydantic eth-abi eth-utils pytest +eth-hash[pycryptodome] From 2a32fb13b6596c6797574b5116d47f342e1c9ca9 Mon Sep 17 00:00:00 2001 From: niv vaknin Date: Thu, 12 Sep 2024 14:38:01 +0300 Subject: [PATCH 19/20] Add ZKSCAN API key --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d9f7e0..7eda8b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: GNOSCAN_API_KEY: ${{ secrets.GNOSCAN_API_KEY }} SCRSCAN_API_KEY: ${{ secrets.SCRSCAN_API_KEY }} BSCSCAN_API_KEY: ${{ secrets.BSCSCAN_API_KEY }} + ZKSCAN_API_KEY: ${{ secrets.ZKSCAN_API_KEY }} PRP_TOOL_PATH: "." permissions: From f036ece8abdf2933f3653ab6aa6f7ec3537bdd2f Mon Sep 17 00:00:00 2001 From: nivcertora Date: Thu, 12 Sep 2024 11:41:45 +0000 Subject: [PATCH 20/20] Auto change version. --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 1234713..e8d11c6 100644 --- a/version +++ b/version @@ -1 +1 @@ -20240911.121202.211171 +20240912.114145.494795