diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08c2023..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: @@ -36,7 +37,7 @@ 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/.gitignore b/.gitignore index 261b40e..e58d9a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ **.ipynb **__pycache__ -**/Tests **/.vscode \ No newline at end of file diff --git a/ProposalTools/API/contract_source_code_api.py b/ProposalTools/API/contract_source_code_api.py deleted file mode 100644 index 5b867b7..0000000 --- a/ProposalTools/API/contract_source_code_api.py +++ /dev/null @@ -1,105 +0,0 @@ -from dataclasses import dataclass -import requests -from typing import List, Callable -import os -import json - -from ProposalTools.Utils.chain_enum import Chain -from ProposalTools.Utils.source_code import SourceCode - - -@dataclass -class APIinfo: - """ - Data class for storing API base URL and API key retrieval function. - """ - base_url: str - api_key: Callable[[], str] - -class ContractSourceCodeAPI(): - """ - Manages interactions with blockchain explorer APIs to fetch smart contract source codes. - - Attributes: - chain_mapping (dict): Maps Chain enum to APIinfo containing base URL and API key function. - """ - chain_mapping = { - Chain.ETH: APIinfo(base_url="https://api.etherscan.io/api", - api_key=lambda: os.getenv('ETHSCAN_API_KEY')), - Chain.ARB: APIinfo(base_url="https://api.arbiscan.io/api", - api_key=lambda: os.getenv('ARBSCAN_API_KEY')), - Chain.AVAX: APIinfo(base_url="https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan/api", - api_key=lambda: os.getenv('AVAXSCAN_API_KEY', "FREE")), - Chain.BASE: APIinfo(base_url="https://api.basescan.org/api", - api_key=lambda: os.getenv('BASESCAN_API_KEY')), - Chain.BSC: APIinfo(base_url="https://api.bscscan.com/api", - api_key=lambda: os.getenv('BSCSCAN_API_KEY')), - Chain.GNO: APIinfo(base_url="https://api.gnosisscan.io/api", - api_key=lambda: os.getenv('GNOSCAN_API_KEY')), - Chain.MET: APIinfo(base_url="https://api.routescan.io/v2/network/mainnet/evm/1088/etherscan/api", - api_key=lambda: os.getenv('METSCAN_API_KEY', "FREE")), - Chain.OPT: APIinfo(base_url="https://api-optimistic.etherscan.io/api", - api_key=lambda: os.getenv('OPTSCAN_API_KEY')), - Chain.POLY: APIinfo(base_url="https://api.polygonscan.com/api", - api_key=lambda: os.getenv('POLYSCAN_API_KEY')), - Chain.SCR: APIinfo(base_url="https://api.scrollscan.com/api", - 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. - - Args: - chain (Chain): The blockchain network to use (from the Chain enum). - - Raises: - ValueError: If the chain is not supported 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()])}") - - api_info = self.chain_mapping[chain] - self.api_key = api_info.api_key() - 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}" - - def get_source_code(self, proposal_address: str) -> List[SourceCode]: - """ - Fetch the source code of a smart contract from the blockchain explorer API. - - Args: - proposal_address (str): The address of the smart contract. - - Returns: - List[SourceCode]: A list of SourceCode objects containing file names and contents. - - Raises: - ValueError: If there's an error fetching the source code. - """ - url = f"{self.base_url}&address={proposal_address}" - response = requests.get(url) - response.raise_for_status() - data = response.json() - - 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 - json_data = json.loads(result.removeprefix("{").removesuffix("}")) - - sources = json_data.get("sources", {proposal_address: {"content": result}}) - source_codes = [ - SourceCode(file_name=source_name, file_content=source_code["content"].splitlines()) - for source_name, source_code in sources.items() - ] - return source_codes 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/GIT/__init__.py b/ProposalTools/apis/block_explorers/__init__.py similarity index 100% rename from ProposalTools/GIT/__init__.py rename to ProposalTools/apis/block_explorers/__init__.py diff --git a/ProposalTools/apis/block_explorers/chains_api.py b/ProposalTools/apis/block_explorers/chains_api.py new file mode 100644 index 0000000..e3f2340 --- /dev/null +++ b/ProposalTools/apis/block_explorers/chains_api.py @@ -0,0 +1,233 @@ +from dataclasses import dataclass +import requests +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.block_explorers.source_code import SourceCode + + +@dataclass +class APIinfo: + """ + 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: + """ + 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')), + Chain.ARB: APIinfo(base_url="https://api.arbiscan.io/api", + api_key=lambda: os.getenv('ARBSCAN_API_KEY')), + Chain.AVAX: APIinfo(base_url="https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan/api", + api_key=lambda: os.getenv('AVAXSCAN_API_KEY', "FREE")), + Chain.BASE: APIinfo(base_url="https://api.basescan.org/api", + api_key=lambda: os.getenv('BASESCAN_API_KEY')), + Chain.BSC: APIinfo(base_url="https://api.bscscan.com/api", + api_key=lambda: os.getenv('BSCSCAN_API_KEY')), + Chain.GNO: APIinfo(base_url="https://api.gnosisscan.io/api", + api_key=lambda: os.getenv('GNOSCAN_API_KEY')), + Chain.MET: APIinfo(base_url="https://api.routescan.io/v2/network/mainnet/evm/1088/etherscan/api", + api_key=lambda: os.getenv('METSCAN_API_KEY', "FREE")), + Chain.OPT: APIinfo(base_url="https://api-optimistic.etherscan.io/api", + api_key=lambda: os.getenv('OPTSCAN_API_KEY')), + Chain.POLY: APIinfo(base_url="https://api.polygonscan.com/api", + api_key=lambda: os.getenv('POLYSCAN_API_KEY')), + Chain.SCR: APIinfo(base_url="https://api.scrollscan.com/api", + 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: + """ + Initializes the ChainAPI with the appropriate blockchain network's base URL and API key. + + Args: + chain (Chain): The blockchain network to interact with (from the Chain enum). + + Raises: + 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}. 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() + 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}?apikey={self.api_key}" + + def get_source_code(self, proposal_address: str) -> list[SourceCode]: + """ + Fetches the source code of a smart contract from the blockchain explorer API. + + Args: + proposal_address (str): The address of the smart contract to retrieve the source code. + + Returns: + list[SourceCode]: A list of SourceCode objects containing the file names and source code contents. + + Raises: + 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) + response.raise_for_status() + data = response.json() + + 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 responses + json_data = json.loads(result.removeprefix("{").removesuffix("}")) + + sources = json_data.get("sources", {proposal_address: {"content": result}}) + source_codes = [ + SourceCode(file_name=source_name, file_content=source_code["content"].splitlines()) + for source_name, source_code in sources.items() + ] + return source_codes + + def get_contract_abi(self, contract_address: str) -> list[dict]: + """ + Fetches 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 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) + 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')}") + + return json.loads(data['result']) + + 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 pass to the contract function (if any). + + Returns: + Any: The result of the contract function call, with cleaned output if the return type is an address. + + Raises: + ValueError: If the API request fails or there is an error with the function call. + """ + # Step 1: Fetch the contract ABI + abi = self.get_contract_abi(contract_address) + + # 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 arguments + if arguments is None: + arguments = [] + encoded_args = self._encode_arguments(function_abi, arguments) + + # Step 4: Prepare the data payload for the eth_call + data = method_id + encoded_args + + # 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" + response = requests.get(url) + response.raise_for_status() + + # Step 6: Handle the response + result = response.json().get('result') + if not result: + raise ValueError(f"Error calling contract function: {response.json()}") + + # Step 7: Clean the result if the return type is an address + if function_abi.get('outputs') and function_abi['outputs'][0]['type'] == 'address': + 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 of a specific function from the contract ABI. + + Args: + function_name (str): The name of the function. + 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 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 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 characters + + def _encode_arguments(self, function_abi: dict, arguments: list[Any]) -> str: + """ + Encodes function arguments in ABI format. + + Args: + function_abi (dict): The ABI of the function. + arguments (list[Any]): The function arguments to encode. + + Returns: + 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) + return encoded_args.hex() diff --git a/ProposalTools/Utils/source_code.py b/ProposalTools/apis/block_explorers/source_code.py similarity index 98% rename from ProposalTools/Utils/source_code.py rename to ProposalTools/apis/block_explorers/source_code.py index b631e49..593d44b 100644 --- a/ProposalTools/Utils/source_code.py +++ b/ProposalTools/apis/block_explorers/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/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/check_proposal.py b/ProposalTools/check_proposal.py index 1e9670a..240f37e 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.block_explorers.chains_api import ChainAPI +import ProposalTools.checks as Checks def parse_args() -> tuple[Optional[str], Optional[str], Optional[str], Optional[str]]: @@ -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/__init__.py b/ProposalTools/checks/__init__.py similarity index 100% rename from ProposalTools/Checks/__init__.py rename to ProposalTools/checks/__init__.py diff --git a/ProposalTools/Checks/check.py b/ProposalTools/checks/check.py similarity index 92% rename from ProposalTools/Checks/check.py rename to ProposalTools/checks/check.py index 924d0cd..15852f5 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.block_explorers.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 similarity index 96% rename from ProposalTools/Checks/diff.py rename to ProposalTools/checks/diff.py index e249276..14087af 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.block_explorers.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 similarity index 92% rename from ProposalTools/Checks/feed_price.py rename to ProposalTools/checks/feed_price.py index 437259b..cdb199f 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.block_explorers.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 similarity index 96% rename from ProposalTools/Checks/global_variables.py rename to ProposalTools/checks/global_variables.py index 86a0e10..39631df 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.block_explorers.source_code import SourceCode +import ProposalTools.utils.pretty_printer as pp class GlobalVariableCheck(Check): diff --git a/ProposalTools/Checks/new_listing.py b/ProposalTools/checks/new_listing.py similarity index 98% rename from ProposalTools/Checks/new_listing.py rename to ProposalTools/checks/new_listing.py index 7477cd2..6675464 100644 --- a/ProposalTools/Checks/new_listing.py +++ b/ProposalTools/checks/new_listing.py @@ -1,8 +1,8 @@ from solidity_parser.parser import Node from pydantic import BaseModel -from ProposalTools.Checks.check import Check -import ProposalTools.Utils.pretty_printer as pp +from ProposalTools.checks.check import Check +import ProposalTools.utils.pretty_printer as pp class ListingDetails(BaseModel): @@ -21,7 +21,6 @@ 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. @@ -36,9 +35,7 @@ def new_listing_check(self) -> None: 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. """ @@ -50,10 +47,8 @@ def _get_functions_from_source_codes(self) -> dict: 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. """ @@ -78,10 +73,8 @@ def _handle_new_listings(self, functions: dict) -> None: 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. @@ -104,13 +97,10 @@ def _check_listing_calls(self, listing: ListingDetails, approval_calls: dict, su 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. """ @@ -128,13 +118,10 @@ def __extract_listings_from_function(self, function_node: Node) -> list[ListingD 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. """ @@ -153,13 +140,10 @@ def _extract_listings_from_statements(self, function_node: Node) -> list[Listing 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. """ @@ -173,13 +157,10 @@ def __extract_listing_details(self, arguments: list[Node]) -> ListingDetails: 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. @@ -199,17 +180,14 @@ def __extract_approval_and_supply_calls(self, function_node: Node) -> tuple[list 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 + return FunctionCallDetails(pool=pool, asset=asset, asset_seed=asset_seed) diff --git a/ProposalTools/Utils/__init__.py b/ProposalTools/git/__init__.py similarity index 100% rename from ProposalTools/Utils/__init__.py rename to ProposalTools/git/__init__.py diff --git a/ProposalTools/GIT/git_manager.py b/ProposalTools/git/git_manager.py similarity index 98% rename from ProposalTools/GIT/git_manager.py rename to 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/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/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 similarity index 100% rename from ProposalTools/Utils/chain_enum.py rename to ProposalTools/utils/chain_enum.py diff --git a/ProposalTools/Utils/pretty_printer.py b/ProposalTools/utils/pretty_printer.py similarity index 100% rename from ProposalTools/Utils/pretty_printer.py rename to ProposalTools/utils/pretty_printer.py diff --git a/ProposalTools/Utils/singleton.py b/ProposalTools/utils/singleton.py similarity index 100% rename from ProposalTools/Utils/singleton.py rename to ProposalTools/utils/singleton.py diff --git a/requirements.txt b/requirements.txt index 05e1c91..e0e968d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,8 @@ requests GitPython solidity-parser -pydantic \ No newline at end of file +pydantic +eth-abi +eth-utils +pytest +eth-hash[pycryptodome] diff --git a/version b/version index 483a268..e8d11c6 100644 --- a/version +++ b/version @@ -1 +1 @@ -20240828.104045.704958 +20240912.114145.494795