From 023ea15e4407e3be7510a7bed174daabc4875911 Mon Sep 17 00:00:00 2001 From: Felipe Alvarado Date: Tue, 19 Mar 2024 13:20:21 +0100 Subject: [PATCH 1/6] Add issue template and workflows --- .../add_safe_address_new_chain.yml | 121 +++++ .github/scripts/execute_address_issue.py | 425 ++++++++++++++++++ .github/scripts/validate_address_issue.py | 227 ++++++++++ .github/workflows/execute_address_issue.yml | 133 ++++++ .github/workflows/validate_address_issue.yml | 88 ++++ 5 files changed, 994 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/add_safe_address_new_chain.yml create mode 100644 .github/scripts/execute_address_issue.py create mode 100644 .github/scripts/validate_address_issue.py create mode 100644 .github/workflows/execute_address_issue.yml create mode 100644 .github/workflows/validate_address_issue.yml diff --git a/.github/ISSUE_TEMPLATE/add_safe_address_new_chain.yml b/.github/ISSUE_TEMPLATE/add_safe_address_new_chain.yml new file mode 100644 index 000000000..248922d29 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/add_safe_address_new_chain.yml @@ -0,0 +1,121 @@ +name: Add safe addresses for a new chain +description: Add addresses for Safe Contracts in a new chain +title: "[New chain]: {chain name}" +labels: ["add-new-address"] # Important: this label must be created in the repository. +projects: [] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this issue! In this way you can add support to a new chain. + - type: textarea + id: summary + attributes: + label: Summary + description: A brief summary. + placeholder: I would like to add {chain name} chain... + validations: + required: true + - type: input + id: chain_id + attributes: + label: Chain ID + description: Id of the chain to be supported. + placeholder: ex. 1 + validations: + required: true + - type: input + id: chain_ir_url + attributes: + label: Chain detail URL + description: chainlist.org url with chain id detail. + placeholder: ex. https://chainlist.org/chain/1 + validations: + required: true + - type: input + id: rpc_url + attributes: + label: RPC URL + description: RPC URL + placeholder: ex. https://eth.drpc.org + validations: + required: true + - type: input + id: blockscout_client_url + attributes: + label: Blockscout Client URL + description: Blockscout URL. + placeholder: ex. https://gnosis.blockscout.com/api/v1/graphql + - type: input + id: etherscan_client_url + attributes: + label: Etherscan Client URL + description: Etherscan URL. + placeholder: ex. https://etherscan.io + - type: input + id: etherscan_client_api_url + attributes: + label: Etherscan Client API URL + description: Etherscan URL. + placeholder: ex. https://api.etherscan.io + - type: dropdown + id: version + attributes: + label: Version + description: Safe contract version + multiple: false + options: + - "1.3.0" + - "1.3.0 L2" + - "1.4.1" + - "1.4.1 L2" + default: 0 + validations: + required: true + - type: markdown + attributes: + value: | + --- + # Master copies + - type: input + id: address_master_copy + attributes: + label: Address (Master copy) + description: Address safe contract + placeholder: ex. "0x69f4D1788e39c87893C980c06EdF4b7f686e2938" + - type: input + id: tx_hash_master_copy + attributes: + label: Deployment Tx hash (Master copy) + description: Contract deployment Tx hash. + placeholder: ex. 0x25b182f34baa23c122b081b249fb9da27f032e663e0a0ab3833be1c1c9266c3e + - type: input + id: block_explorer_url_master_copy + attributes: + label: Block explorer URL (Master copy) + description: Detail of contract address in the chain block explorer or deployment log. + placeholder: ex. https://etherscan.io/address/0x69f4d1788e39c87893c980c06edf4b7f686e2938 + - type: markdown + attributes: + value: | + --- + # Proxy factories + - type: input + id: address_proxy + attributes: + label: Address (Proxy factory) + description: Address safe contract + placeholder: ex. "0x69f4D1788e39c87893C980c06EdF4b7f686e2938" + - type: input + id: tx_hash_proxy + attributes: + label: Deployment Tx hash (Proxy factory) + description: Contract deployment Tx hash. + placeholder: ex. 0x25b182f34baa23c122b081b249fb9da27f032e663e0a0ab3833be1c1c9266c3e + - type: textarea + id: block_explorer_url_proxy + attributes: + label: Block explorer URL (Proxy factory) + description: Detail of contract address in the chain block explorer or deployment log. + placeholder: ex. https://etherscan.io/address/0x69f4d1788e39c87893c980c06edf4b7f686e2938 diff --git a/.github/scripts/execute_address_issue.py b/.github/scripts/execute_address_issue.py new file mode 100644 index 000000000..36859595f --- /dev/null +++ b/.github/scripts/execute_address_issue.py @@ -0,0 +1,425 @@ +import json +import os +import re +from typing import Optional + +import requests +from github import Github +from github.GithubException import GithubException +from github.Repository import Repository +from hexbytes import HexBytes +from web3 import Web3 + + +def get_chain_enum_name(chain_id: str) -> Optional[str]: + try: + url = f"https://raw.githubusercontent.com/ethereum-lists/chains/master/_data/chains/eip155-{chain_id}.json" + response = requests.get(url) + + if response.status_code == 200: + return response.json().get("name").upper().replace(" ", "_") + return None + except Exception as e: + print(f"Error getting chain name: {e}") + return None + + +def get_contract_block_from_tx_hash(rpc_url: str, tx_hash: str) -> Optional[str]: + try: + w3 = Web3(Web3.HTTPProvider(rpc_url)) + tx = w3.eth.get_transaction(HexBytes(tx_hash)) + return str(tx.blockNumber) + except Exception as e: + print(f"Error getting transaction: {e}") + return None + + +def get_github_repository(github_token: str, repository_name) -> Repository: + print(f"Getting repository {repository_name}") + github = Github(github_token) + return github.get_repo(repository_name) + + +def create_issue_branch(repo: Repository, chain_id: str, version: str) -> str: + branch_name = "add-new-chain-" + chain_id + "-" + version + "-addresses" + print(f"Creating branch {branch_name}") + try: + repo.create_git_ref( + ref=f"refs/heads/{branch_name}", sha=repo.get_branch("main").commit.sha + ) + except GithubException as e: + print(f"Unable to create pull request: {e}") + return branch_name + + +def create_pr( + repo: Repository, branch_name: str, chain_id: str, chain_enum_name: str +) -> None: + try: + repo.create_pull( + title=f"Add new chain {chain_enum_name} {chain_id} addresses", + body=f"Automatic PR to add new address to {chain_enum_name} {chain_id} chain", + head=branch_name, + base="main", + ) + except GithubException as e: + print(f"Unable to create pull request: {e}") + + +def upsert_chain_id( + repo: Repository, branch_name: str, chain_id: str, chain_enum_name: str +) -> None: + file_path = "gnosis/eth/ethereum_network.py" + file = repo.get_contents(file_path, ref=branch_name) + content = file.decoded_content.decode("utf-8") + + match = re.search( + r'class EthereumNetwork\(Enum\):(\s*\n\s*"""[^"]*"""\s*\n\s*)?(.+?)(\n\s*@.*)', + content, + re.MULTILINE | re.DOTALL, + ) + + if match: + enum_lines = str(match.group(2).strip()).split("\n") + + existing_entry = next( + (line for line in enum_lines if re.search(rf"\b{chain_id}\b", line)), None + ) + + if existing_entry: + print(f"Entry with ID '{chain_id}' already exists.") + else: + new_entry = f" {chain_enum_name} = {chain_id}" + enum_lines.append(new_entry) + enum_lines.sort(key=lambda x: int(x.split("=")[1].strip())) + + updated_content = ( + content[: match.start()] + + "class EthereumNetwork(Enum):" + + match.group(1) + + "\n".join(enum_lines) + + match.group(3) + ) + + repo.update_file( + file_path, + f"Add new chain {chain_id}", + updated_content, + file.sha, + branch_name, + ) + + print(f"Entry '{chain_enum_name} = {chain_id}' added successfully.") + else: + print("Error: EthereumNetwork class definition not found in the file.") + + +def upsert_explorer_client_url( + repo: Repository, + branch_name: str, + chain_enum_name: str, + client_url: str, + file_path: str, + config_enum_name: str, +) -> None: + file = repo.get_contents(file_path, ref=branch_name) + content = file.decoded_content.decode("utf-8") + + match = re.search( + config_enum_name + r" = \{\n(.+?)(\n\s*}.*)", content, re.MULTILINE | re.DOTALL + ) + + if match: + url_lines = str(match.group(1).strip()).split("\n") + + existing_entry = next( + ( + line + for line in url_lines + if re.search(f'EthereumNetwork.{chain_enum_name}: "{client_url}"', line) + ), + None, + ) + + if existing_entry: + print(f"Entry with URL '{client_url}' already exists.") + else: + new_entry = f' EthereumNetwork.{chain_enum_name}: "{client_url}"' + url_lines.append(new_entry) + + updated_content = ( + content[: match.start()] + + config_enum_name + + " = {\n " + + "\n".join(url_lines) + + match.group(2) + ) + + repo.update_file( + file_path, + f"Add new explorer client URL: {client_url}", + updated_content, + file.sha, + branch_name, + ) + + print( + f'Entry EthereumNetwork.{chain_enum_name}: "{client_url}" added successfully.' + ) + else: + print("Error: Class definition not found in the file.") + + +def upsert_contract_address_master_copy( + repo: Repository, + branch_name: str, + chain_enum_name: str, + address: str, + block_number: str, + version: str, +) -> None: + file_path = "gnosis/safe/addresses.py" + file = repo.get_contents(file_path, ref=branch_name) + content = file.decoded_content.decode("utf-8") + + print( + f"Updating Master Copy address chain {chain_enum_name} address '{address}' and block_number {block_number}" + ) + + match_network = re.search( + r"(MASTER_COPIES: Dict\[EthereumNetwork, List\[Tuple\[str, int, str]]] = \{.+EthereumNetwork\." + + re.escape(chain_enum_name) + + r": \[)(.+?)(].+PROXY_FACTORIES.+)", + content, + re.MULTILINE | re.DOTALL, + ) + + if match_network: + match_contract = re.search( + r"(\(\n?\s*\"" + + re.escape(address) + + r"\",\n?\s*" + + re.escape(block_number) + + r",\n?\s*\"" + + re.escape(version) + + r"\",?\n?\s*\))", + match_network.group(2), + re.MULTILINE | re.DOTALL, + ) + + if match_contract: + print( + f"Entry in chain {chain_enum_name} with address '{address}' and block_number {block_number}" + + f" and version {version} already exists." + ) + else: + new_entry = ( + f' ("{address}", {block_number}, "{version}"), #{version}\n ' + ) + updated_content = ( + content[: match_network.start()] + + match_network.group(1) + + match_network.group(2) + + new_entry + + match_network.group(3) + ) + repo.update_file( + file_path, + f"Add new master copy address {address}", + updated_content, + file.sha, + branch_name, + ) + else: + match = re.search( + r"(MASTER_COPIES: Dict\[EthereumNetwork, List\[Tuple\[str, int, str]]] = \{)(.+?)(}.+PROXY_FACTORIES.+)", + content, + re.MULTILINE | re.DOTALL, + ) + + if match: + new_entry = ( + f' EthereumNetwork.{chain_enum_name}: [\n ("{address}", {block_number}, ' + + f'"{version}") #{version} \n ],\n' + ) + updated_content = ( + content[: match.start()] + + match.group(1) + + match.group(2) + + new_entry + + match.group(3) + ) + repo.update_file( + file_path, + f"Add new master copy address {address}", + updated_content, + file.sha, + branch_name, + ) + else: + print("Error: MASTER_COPIES definition not found in the file.") + + +def upsert_contract_address_proxy_factory( + repo: Repository, + branch_name: str, + chain_enum_name: str, + address: str, + block_number: str, + version: str, +) -> None: + file_path = "gnosis/safe/addresses.py" + file = repo.get_contents(file_path, ref=branch_name) + content = file.decoded_content.decode("utf-8") + + print( + f"Updating Proxy Factory address chain {chain_enum_name} address '{address}' and block_number {block_number}" + ) + + match_network = re.search( + r"(PROXY_FACTORIES: Dict\[EthereumNetwork, List\[Tuple\[str, int]]] = \{.+EthereumNetwork\." + + re.escape(chain_enum_name) + + r": \[)(.+?)(].+)", + content, + re.MULTILINE | re.DOTALL, + ) + + if match_network: + match_contract = re.search( + r"(\(\n?\s*\"" + + re.escape(address) + + r"\",\n?\s*" + + re.escape(block_number) + + r"\",?\n?\s*\))", + match_network.group(2), + re.MULTILINE | re.DOTALL, + ) + + if match_contract: + print( + f"Entry in chain {chain_enum_name} with address '{address}' and block_number {block_number}" + + " already exists." + ) + else: + new_entry = f' ("{address}", {block_number}"), #{version}\n ' + updated_content = ( + content[: match_network.start()] + + match_network.group(1) + + match_network.group(2) + + new_entry + + match_network.group(3) + ) + repo.update_file( + file_path, + f"Add new proxy address {address}", + updated_content, + file.sha, + branch_name, + ) + else: + match = re.search( + r"(PROXY_FACTORIES: Dict\[EthereumNetwork, List\[Tuple\[str, int]]] = \{)(.+?)(}.+)", + content, + re.MULTILINE | re.DOTALL, + ) + + if match: + new_entry = ( + f' EthereumNetwork.{chain_enum_name}: [\n ("{address}", {block_number}' + + f") #{version} \n ],\n" + ) + updated_content = ( + content[: match.start()] + + match.group(1) + + match.group(2) + + new_entry + + match.group(3) + ) + repo.update_file( + file_path, + f"Add new proxy address {address}", + updated_content, + file.sha, + branch_name, + ) + else: + print("Error: PROXY_FACTORIES definition not found in the file.") + + +def execute_issue_changes() -> None: + github_token = os.environ.get("GITHUB_TOKEN") + repository_name = os.environ.get("GITHUB_REPOSITORY_NAME") + issue_body_info = json.loads(os.environ.get("ISSUE_BODY_INFO")) + chain_id = issue_body_info.get("chainId") + version = issue_body_info.get("version") + blockscout_client_url = issue_body_info.get("blockscoutClientUrl") + etherscan_client_url = issue_body_info.get("etherscanClientUrl") + etherscan_client_api_url = issue_body_info.get("etherscanClientApiUrl") + rpc_url = issue_body_info.get("rpcUrl") + address_master_copy = issue_body_info.get("addressMasterCopy") + tx_hash_master_copy = issue_body_info.get("txHashMasterCopy") + address_proxy = issue_body_info.get("addressProxy") + tx_hash_proxy = issue_body_info.get("txHashProxy") + + repo = get_github_repository(github_token, repository_name) + + chain_enum_name = get_chain_enum_name(chain_id) + if not chain_enum_name: + return + + branch_name = create_issue_branch(repo, chain_id, version) + + upsert_chain_id(repo, branch_name, chain_id, chain_enum_name) + + if blockscout_client_url: + print("Updating Blockscout client") + file_path = "gnosis/eth/clients/blockscout_client.py" + upsert_explorer_client_url( + repo, + branch_name, + chain_enum_name, + blockscout_client_url, + file_path, + "NETWORK_WITH_URL", + ) + + if etherscan_client_url: + print("Updating Ether Scan client") + file_path = "gnosis/eth/clients/etherscan_client.py" + upsert_explorer_client_url( + repo, + branch_name, + chain_enum_name, + etherscan_client_url, + file_path, + "NETWORK_WITH_URL", + ) + if etherscan_client_api_url: + print("Updating Ether Scan API client") + file_path = "gnosis/eth/clients/etherscan_client.py" + upsert_explorer_client_url( + repo, + branch_name, + chain_enum_name, + etherscan_client_api_url, + file_path, + "NETWORK_WITH_API_URL", + ) + + if rpc_url and address_master_copy and tx_hash_master_copy: + tx_block = get_contract_block_from_tx_hash(rpc_url, tx_hash_master_copy) + upsert_contract_address_master_copy( + repo, branch_name, chain_enum_name, address_master_copy, tx_block, version + ) + + if rpc_url and address_proxy and tx_hash_proxy: + tx_block = get_contract_block_from_tx_hash(rpc_url, tx_hash_proxy) + upsert_contract_address_proxy_factory( + repo, branch_name, chain_enum_name, address_proxy, tx_block, version + ) + + create_pr(repo, branch_name, chain_id, chain_enum_name) + + +if __name__ == "__main__": + execute_issue_changes() diff --git a/.github/scripts/validate_address_issue.py b/.github/scripts/validate_address_issue.py new file mode 100644 index 000000000..7004ce6cd --- /dev/null +++ b/.github/scripts/validate_address_issue.py @@ -0,0 +1,227 @@ +import json +import os +import uuid +from typing import Any, Dict, Optional + +import requests +import validators +from hexbytes import HexBytes +from web3 import Web3 + +from gnosis.eth.utils import mk_contract_address_2 + +ERRORS = [] + + +def get_chain_name(chain_id: str) -> Optional[str]: + try: + url = f"https://raw.githubusercontent.com/ethereum-lists/chains/master/_data/chains/eip155-{chain_id}.json" + response = requests.get(url) + + if response.status_code == 200: + return response.json().get("name") + return None + except Exception as e: + print(f"Error getting chain name: {e}") + return None + + +def get_chain_id_from_rpc_url(rpc_url: str) -> Optional[int]: + try: + response = requests.post( + rpc_url, + json={"jsonrpc": "2.0", "method": "eth_chainId", "params": [], "id": 1}, + ) + + if response.status_code == 200: + return int(response.json().get("result"), 16) + return None + except Exception as e: + print(f"Error validating RPC url: {e}") + return None + + +def get_contract_address_and_block_from_tx_hash( + rpc_url: str, tx_hash: str +) -> Optional[Dict[str, Any]]: + try: + w3 = Web3(Web3.HTTPProvider(rpc_url)) + tx = w3.eth.get_transaction(HexBytes(tx_hash)) + return { + "block": tx.blockNumber, + "address": mk_contract_address_2(tx.to, tx.input[:32], tx.input[32:]), + } + except Exception as e: + print(f"Error getting transaction: {e}") + return None + + +def validate_chain(chain_id: str) -> Optional[str]: + if not chain_id: + ERRORS.append("Chain ID is required.") + return + + chain_name = get_chain_name(chain_id) + if not chain_name: + ERRORS.append(f"Chain with chain ID: {chain_id} not found.") + return + + print(f"Chain name: {chain_name}") + return chain_name + + +def validate_rpc(rpc_url: str, chain_id: str) -> None: + if not rpc_url: + ERRORS.append("RPC URL is required.") + return + + if not chain_id: + ERRORS.append("Unable to validate RPC URL without chain ID.") + return + + rpc_chain_id = get_chain_id_from_rpc_url(rpc_url) + if not rpc_chain_id: + ERRORS.append(f"Unable to validate RPC URL {rpc_url}.") + return + + if rpc_chain_id != int(chain_id): + ERRORS.append( + f"Chain ID {chain_id} provided is different than chain id obtained from RPC URL {rpc_url} {rpc_chain_id}." + ) + return + + print(f"Chain ID obtained from rpc url: {rpc_chain_id}") + + +def validate_not_required_url(field_name: str, url: str) -> None: + if url: + if not validators.url(url): + ERRORS.append(f"{field_name} URL ({url}) provided is not valid.") + return + + print(f"Validating {field_name} URL -> {url}") + + print(f"Skipping {field_name} URL validation!") + + +def validate_version(version: str) -> None: + if version not in ["1.3.0", "1.3.0 L2", "1.4.1", "1.4.1 L2"]: + ERRORS.append(f"Version {version} is not valid.") + return + + print(f"Validating version: {version}!") + + +def validate_address_and_transactions( + type: str, address: str, tx_hash: str, rpc_url: str +) -> Optional[Dict[str, Any]]: + if not address and not tx_hash: + print("Skipping address and tx validation. Not data provided!") + return + + if not address: + ERRORS.append(f"{type} address is required.") + return + + if not tx_hash: + ERRORS.append(f"{type} tx_hash is required.") + return + + if not rpc_url: + ERRORS.append(f"Unable to validate {type} address and tx without RPC URL.") + return + + tx_info = get_contract_address_and_block_from_tx_hash(rpc_url, tx_hash) + if not tx_info: + ERRORS.append(f"Unable to obtain {type} Tx info {tx_hash}") + return + + if tx_info["address"] != address: + ERRORS.append( + f"{type} address obtained from Tx is diferent than provided {tx_info['address']}" + ) + return + + print(f"{type} Tx. info: {tx_info}") + return tx_info + + +def add_message_to_env(message: str) -> None: + with open(os.environ["GITHUB_OUTPUT"], "a") as fh: + delimiter = uuid.uuid1() + print(f"comment_message<<{delimiter}", file=fh) + print(message, file=fh) + print(delimiter, file=fh) + + +def validate_issue_inputs() -> None: + issue_body_info = json.loads(os.environ.get("ISSUE_BODY_INFO")) + chain_id = issue_body_info.get("chainId") + rpc_url = issue_body_info.get("rpcUrl") + blockscout_client_url = issue_body_info.get("blockscoutClientUrl") + etherscan_client_url = issue_body_info.get("etherscanClientUrl") + etherscan_client_api_url = issue_body_info.get("etherscanClientApiUrl") + version = issue_body_info.get("version") + address_master_copy = issue_body_info.get("addressMasterCopy") + tx_hash_master_copy = issue_body_info.get("txHashMasterCopy") + address_proxy = issue_body_info.get("addressProxy") + tx_hash_proxy = issue_body_info.get("txHashProxy") + + print("Inputs to validate:") + print(f"Chain ID: {chain_id}") + print(f"RPC URL: {rpc_url}") + print(f"Blockscout Client URL: {blockscout_client_url}") + print(f"Etherscan Client URL: {etherscan_client_url}") + print(f"Etherscan Client API URL: {etherscan_client_api_url}") + print(f"Version: {version}") + print(f"Address (Master copy): {address_master_copy}") + print(f"Deployment Tx hash (Master copy): {tx_hash_master_copy}") + print(f"Address (Proxy factory): {address_proxy}") + print(f"Deployment Tx hash (Proxy factory): {tx_hash_proxy}") + + print("Start validation:") + chain_name = validate_chain(chain_id) + validate_rpc(rpc_url, chain_id) + validate_not_required_url("BlockscoutClientUrl", blockscout_client_url) + validate_not_required_url("EtherscanClientUrl", etherscan_client_url) + validate_not_required_url("EtherscanClientApiUrl", etherscan_client_api_url) + validate_version(version) + tx_master_info = validate_address_and_transactions( + "Master copy", address_master_copy, tx_hash_master_copy, rpc_url + ) + tx_proxy_info = validate_address_and_transactions( + "Proxy factory", address_proxy, tx_hash_proxy, rpc_url + ) + + if len(ERRORS) > 0: + errors_comment = "\n".join(ERRORS) + add_message_to_env( + "Validation has failed with the following errors:" + + f"\n- {errors_comment}" + + "\n\n Validation failed!❌" + ) + return + chain_name_comment = chain_name if chain_name else "N/A" + tx_master_block_comment = tx_master_info.get("block") if tx_master_info else "N/A" + tx_proxy_block_comment = tx_proxy_info.get("block") if tx_proxy_info else "N/A" + add_message_to_env( + "All elements have been validated and are correct:" + + f"\n- Chain ID: {chain_id}" + + f"\n- Chain Name: {chain_name_comment}" + + f"\n- RPC URL: {rpc_url}" + + f"\n- Blockscout Client URL: {blockscout_client_url}" + + f"\n- Etherscan Client URL: {etherscan_client_url}" + + f"\n- Etherscan Client API URL: {etherscan_client_api_url}" + + f"\n- Version: {version}" + + f"\n- Address Master Copy: {address_master_copy}" + + f"\n- Tx Hash Master Copy: {tx_hash_master_copy}" + + f"\n- Tx Block Master Copy: {tx_master_block_comment}" + + f"\n- Address Proxy: {address_proxy}" + + f"\n- Tx Hash Proxy: {tx_hash_proxy}" + + f"\n- Tx Block Proxy: {tx_proxy_block_comment}" + + "\n\n Validation successful!✅" + ) + + +if __name__ == "__main__": + validate_issue_inputs() diff --git a/.github/workflows/execute_address_issue.yml b/.github/workflows/execute_address_issue.yml new file mode 100644 index 000000000..816e3ae75 --- /dev/null +++ b/.github/workflows/execute_address_issue.yml @@ -0,0 +1,133 @@ +name: Process Issue + +on: + issue_comment: + types: [created] + +jobs: + process-issue: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Check if comment is "/execute" by repo owner + id: check-comment + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.ORG_SECRET_NAME }} # Important: This secret with read permissions on the teams information must be configured in repository settings -> secrets -> actions + script: | + const comment = context.payload.comment.body; + const teamMembers = await github.rest.teams.listMembersInOrg({ + org: 'safe-global', + team_slug: 'core-api', + }); + const isMember = context.payload.sender.type === 'User' && teamMembers.data.some(member => member.login === context.payload.sender.login); + if (comment.trim() === '/execute' && isMember) { + console.log('The comment is "/execute" by a authorised member.'); + return true; + } + console.log('The comment is not "/execute" or not by a authorised member.'); + return false; + + - name: Add comment to issue with starting message + if: steps.check-comment.outputs.result == 'true' + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '🚀 Starting to apply the changes for the new address!' + }) + + - name: Add comment to issue with command failure + if: steps.check-comment.outputs.result == 'false' + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '❌ Wrong comment or invalid permissions!' + }) + + - name: Get issue inputs + if: steps.check-comment.outputs.result == 'true' + id: get-issue-inputs + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issueBody = context.payload.issue.body.replaceAll("_No response_", ""); + + const titles = { + summary: 'Summary', + chainId: 'Chain ID', + chainDetailUrl: 'Chain detail URL', + rpcUrl: 'RPC URL', + blockscoutClientUrl: 'Blockscout Client URL', + etherscanClientUrl: 'Etherscan Client URL', + etherscanClientApiUrl: 'Etherscan Client API URL', + version: 'Version', + addressMasterCopy: 'Address \\(Master copy\\)', + txHashMasterCopy: 'Deployment Tx hash \\(Master copy\\)', + blockExplorerUrlMasterCopy: 'Block explorer URL \\(Master copy\\)', + addressProxy: 'Address \\(Proxy factory\\)', + txHashProxy: 'Deployment Tx hash \\(Proxy factory\\)', + blockExplorerUrlProxy: 'Block explorer URL \\(Proxy factory\\)', + }; + + const buildPattern = title => new RegExp(`### ${title}(?:\\r\\n\\r\\n|\\n\\n|\\n|\\r\\n)(.+?)(?:\\r\\n\\r\\n|\\n\\n|\\n|\\r\\n)`); + + const extractInfo = (pattern, text) => { + const match = text.match(pattern); + return match ? match[1].trim() : null; + }; + + const extractedInfo = {}; + Object.keys(titles).forEach(key => { + const pattern = buildPattern(titles[key]); + extractedInfo[key] = extractInfo(pattern, issueBody); + }); + + console.log('Extracted Info:', extractedInfo); + + return extractedInfo; + + - name: Setup Python + if: steps.check-comment.outputs.result == 'true' + uses: actions/setup-python@v2 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + pip install PyGithub + pip install web3==6.15.1 + pip install hexbytes==0.3.1 + + - name: Process Issue and Create PR + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_BODY_INFO: ${{ steps.get-issue-inputs.outputs.result }} + GITHUB_REPOSITORY_NAME: 'safe-global/safe-eth-py' + run: | + python .github/scripts/execute_address_issue.py + + - name: Add comment to issue + if: steps.check-comment.outputs.result == 'true' + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '✅ Execution finished, please review the PR and merge it.' + }) diff --git a/.github/workflows/validate_address_issue.yml b/.github/workflows/validate_address_issue.yml new file mode 100644 index 000000000..4ab3db1ce --- /dev/null +++ b/.github/workflows/validate_address_issue.yml @@ -0,0 +1,88 @@ +name: "Validate the add address issue inputs" + +on: + issues: + types: [opened, edited] + +jobs: + validate-issue-inputs: + runs-on: ubuntu-latest + if: contains(github.event.issue.labels.*.name, 'add-new-address') + steps: + - name: Get issue inputs + id: get-issue-inputs + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issueBody = context.payload.issue.body.replaceAll("_No response_", ""); + + const titles = { + summary: 'Summary', + chainId: 'Chain ID', + chainDetailUrl: 'Chain detail URL', + rpcUrl: 'RPC URL', + blockscoutClientUrl: 'Blockscout Client URL', + etherscanClientUrl: 'Etherscan Client URL', + etherscanClientApiUrl: 'Etherscan Client API URL', + version: 'Version', + addressMasterCopy: 'Address \\(Master copy\\)', + txHashMasterCopy: 'Deployment Tx hash \\(Master copy\\)', + blockExplorerUrlMasterCopy: 'Block explorer URL \\(Master copy\\)', + addressProxy: 'Address \\(Proxy factory\\)', + txHashProxy: 'Deployment Tx hash \\(Proxy factory\\)', + blockExplorerUrlProxy: 'Block explorer URL \\(Proxy factory\\)', + }; + + const buildPattern = title => new RegExp(`### ${title}(?:\\r\\n\\r\\n|\\n\\n|\\n|\\r\\n)(.+?)(?:\\r\\n\\r\\n|\\n\\n|\\n|\\r\\n)`); + + const extractInfo = (pattern, text) => { + const match = text.match(pattern); + return match ? match[1].trim() : null; + }; + + const extractedInfo = {}; + Object.keys(titles).forEach(key => { + const pattern = buildPattern(titles[key]); + extractedInfo[key] = extractInfo(pattern, issueBody); + }); + + console.log('Extracted Info:', extractedInfo); + + return extractedInfo; + + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + pip install web3==6.15.1 + pip install safe-eth-py==6.0.0b18 + pip install validators==0.22.0 + pip install hexbytes==0.3.1 + + - name: Validate input data + id: validate-input-data + env: + ISSUE_BODY_INFO: ${{ steps.get-issue-inputs.outputs.result }} + run: | + python .github/scripts/validate_address_issue.py + + - name: Add comment to issue + uses: actions/github-script@v5 + env: + COMMENT_OUTPUT: ${{ steps.validate-input-data.outputs.comment_message }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: process.env.COMMENT_OUTPUT + }) From 8910c02af6cdf50dc4ede8a1911226a88c3d8d10 Mon Sep 17 00:00:00 2001 From: Felipe Alvarado Date: Tue, 19 Mar 2024 20:45:42 +0100 Subject: [PATCH 2/6] Apply PR suggestions --- .../create_pr_with_new_address.py} | 80 +++++++++++-------- .../validate_new_address_issue_input_data.py} | 60 ++++++++------ ...sue.yml => create_pr_with_new_address.yml} | 5 +- ...validate_new_address_issue_input_data.yml} | 4 +- 4 files changed, 86 insertions(+), 63 deletions(-) rename .github/scripts/{execute_address_issue.py => github_adding_addresses/create_pr_with_new_address.py} (87%) rename .github/scripts/{validate_address_issue.py => github_adding_addresses/validate_new_address_issue_input_data.py} (83%) rename .github/workflows/{execute_address_issue.yml => create_pr_with_new_address.yml} (97%) rename .github/workflows/{validate_address_issue.yml => validate_new_address_issue_input_data.yml} (95%) diff --git a/.github/scripts/execute_address_issue.py b/.github/scripts/github_adding_addresses/create_pr_with_new_address.py similarity index 87% rename from .github/scripts/execute_address_issue.py rename to .github/scripts/github_adding_addresses/create_pr_with_new_address.py index 36859595f..630b996d6 100644 --- a/.github/scripts/execute_address_issue.py +++ b/.github/scripts/github_adding_addresses/create_pr_with_new_address.py @@ -1,3 +1,8 @@ +""" +Creates a PR with the necessary changes to add the new addresses. +Verify and apply the necessary changes. +""" + import json import os import re @@ -8,10 +13,11 @@ from github.GithubException import GithubException from github.Repository import Repository from hexbytes import HexBytes -from web3 import Web3 + +from gnosis.eth import EthereumClient -def get_chain_enum_name(chain_id: str) -> Optional[str]: +def get_chain_enum_name(chain_id: int) -> Optional[str]: try: url = f"https://raw.githubusercontent.com/ethereum-lists/chains/master/_data/chains/eip155-{chain_id}.json" response = requests.get(url) @@ -19,29 +25,28 @@ def get_chain_enum_name(chain_id: str) -> Optional[str]: if response.status_code == 200: return response.json().get("name").upper().replace(" ", "_") return None - except Exception as e: + except IOError as e: print(f"Error getting chain name: {e}") return None -def get_contract_block_from_tx_hash(rpc_url: str, tx_hash: str) -> Optional[str]: - try: - w3 = Web3(Web3.HTTPProvider(rpc_url)) - tx = w3.eth.get_transaction(HexBytes(tx_hash)) - return str(tx.blockNumber) - except Exception as e: - print(f"Error getting transaction: {e}") - return None +def get_contract_block_from_tx_hash(rpc_url: str, tx_hash: str) -> Optional[int]: + ethereum_client = EthereumClient(rpc_url) + tx = ethereum_client.get_transaction(HexBytes(tx_hash)) + if not tx: + print(f"Transaction not found: {tx_hash}") + return + return tx.get("blockNumber") -def get_github_repository(github_token: str, repository_name) -> Repository: +def get_github_repository(github_token: str, repository_name: str) -> Repository: print(f"Getting repository {repository_name}") github = Github(github_token) return github.get_repo(repository_name) -def create_issue_branch(repo: Repository, chain_id: str, version: str) -> str: - branch_name = "add-new-chain-" + chain_id + "-" + version + "-addresses" +def create_issue_branch(repo: Repository, chain_id: int, version: str) -> str: + branch_name = f"add-new-chain-{chain_id}-{version}-addresses" print(f"Creating branch {branch_name}") try: repo.create_git_ref( @@ -53,7 +58,7 @@ def create_issue_branch(repo: Repository, chain_id: str, version: str) -> str: def create_pr( - repo: Repository, branch_name: str, chain_id: str, chain_enum_name: str + repo: Repository, branch_name: str, chain_id: int, chain_enum_name: str ) -> None: try: repo.create_pull( @@ -67,7 +72,7 @@ def create_pr( def upsert_chain_id( - repo: Repository, branch_name: str, chain_id: str, chain_enum_name: str + repo: Repository, branch_name: str, chain_id: int, chain_enum_name: str ) -> None: file_path = "gnosis/eth/ethereum_network.py" file = repo.get_contents(file_path, ref=branch_name) @@ -175,7 +180,7 @@ def upsert_contract_address_master_copy( branch_name: str, chain_enum_name: str, address: str, - block_number: str, + block_number: int, version: str, ) -> None: file_path = "gnosis/safe/addresses.py" @@ -199,7 +204,7 @@ def upsert_contract_address_master_copy( r"(\(\n?\s*\"" + re.escape(address) + r"\",\n?\s*" - + re.escape(block_number) + + re.escape(str(block_number)) + r",\n?\s*\"" + re.escape(version) + r"\",?\n?\s*\))", @@ -214,7 +219,7 @@ def upsert_contract_address_master_copy( ) else: new_entry = ( - f' ("{address}", {block_number}, "{version}"), #{version}\n ' + f' ("{address}", {block_number}, "{version}"), # v{version}\n ' ) updated_content = ( content[: match_network.start()] @@ -240,7 +245,7 @@ def upsert_contract_address_master_copy( if match: new_entry = ( f' EthereumNetwork.{chain_enum_name}: [\n ("{address}", {block_number}, ' - + f'"{version}") #{version} \n ],\n' + + f'"{version}"), # v{version} \n ],\n' ) updated_content = ( content[: match.start()] @@ -265,7 +270,7 @@ def upsert_contract_address_proxy_factory( branch_name: str, chain_enum_name: str, address: str, - block_number: str, + block_number: int, version: str, ) -> None: file_path = "gnosis/safe/addresses.py" @@ -289,8 +294,8 @@ def upsert_contract_address_proxy_factory( r"(\(\n?\s*\"" + re.escape(address) + r"\",\n?\s*" - + re.escape(block_number) - + r"\",?\n?\s*\))", + + re.escape(str(block_number)) + + r",?\n?\s*\))", match_network.group(2), re.MULTILINE | re.DOTALL, ) @@ -301,7 +306,7 @@ def upsert_contract_address_proxy_factory( + " already exists." ) else: - new_entry = f' ("{address}", {block_number}"), #{version}\n ' + new_entry = f' ("{address}", {block_number}), # v{version}\n ' updated_content = ( content[: match_network.start()] + match_network.group(1) @@ -326,7 +331,7 @@ def upsert_contract_address_proxy_factory( if match: new_entry = ( f' EthereumNetwork.{chain_enum_name}: [\n ("{address}", {block_number}' - + f") #{version} \n ],\n" + + f"), # v{version} \n ],\n" ) updated_content = ( content[: match.start()] @@ -350,7 +355,7 @@ def execute_issue_changes() -> None: github_token = os.environ.get("GITHUB_TOKEN") repository_name = os.environ.get("GITHUB_REPOSITORY_NAME") issue_body_info = json.loads(os.environ.get("ISSUE_BODY_INFO")) - chain_id = issue_body_info.get("chainId") + chain_id = int(issue_body_info.get("chainId")) version = issue_body_info.get("version") blockscout_client_url = issue_body_info.get("blockscoutClientUrl") etherscan_client_url = issue_body_info.get("etherscanClientUrl") @@ -384,7 +389,7 @@ def execute_issue_changes() -> None: ) if etherscan_client_url: - print("Updating Ether Scan client") + print("Updating Etherscan client") file_path = "gnosis/eth/clients/etherscan_client.py" upsert_explorer_client_url( repo, @@ -395,7 +400,7 @@ def execute_issue_changes() -> None: "NETWORK_WITH_URL", ) if etherscan_client_api_url: - print("Updating Ether Scan API client") + print("Updating Etherscan API client") file_path = "gnosis/eth/clients/etherscan_client.py" upsert_explorer_client_url( repo, @@ -408,15 +413,22 @@ def execute_issue_changes() -> None: if rpc_url and address_master_copy and tx_hash_master_copy: tx_block = get_contract_block_from_tx_hash(rpc_url, tx_hash_master_copy) - upsert_contract_address_master_copy( - repo, branch_name, chain_enum_name, address_master_copy, tx_block, version - ) + if tx_block: + upsert_contract_address_master_copy( + repo, + branch_name, + chain_enum_name, + address_master_copy, + tx_block, + version, + ) if rpc_url and address_proxy and tx_hash_proxy: tx_block = get_contract_block_from_tx_hash(rpc_url, tx_hash_proxy) - upsert_contract_address_proxy_factory( - repo, branch_name, chain_enum_name, address_proxy, tx_block, version - ) + if tx_block: + upsert_contract_address_proxy_factory( + repo, branch_name, chain_enum_name, address_proxy, tx_block, version + ) create_pr(repo, branch_name, chain_id, chain_enum_name) diff --git a/.github/scripts/validate_address_issue.py b/.github/scripts/github_adding_addresses/validate_new_address_issue_input_data.py similarity index 83% rename from .github/scripts/validate_address_issue.py rename to .github/scripts/github_adding_addresses/validate_new_address_issue_input_data.py index 7004ce6cd..e4971b693 100644 --- a/.github/scripts/validate_address_issue.py +++ b/.github/scripts/github_adding_addresses/validate_new_address_issue_input_data.py @@ -1,3 +1,9 @@ +""" +Validates the issue inputs to add new Safe contract addresses. +Composes message for a comment in the ticket with the result of the validation. If the result is negative, +composes a list of errors. +""" + import json import os import uuid @@ -5,15 +11,14 @@ import requests import validators -from hexbytes import HexBytes -from web3 import Web3 +from gnosis.eth import EthereumClient from gnosis.eth.utils import mk_contract_address_2 ERRORS = [] -def get_chain_name(chain_id: str) -> Optional[str]: +def get_chain_name(chain_id: int) -> Optional[str]: try: url = f"https://raw.githubusercontent.com/ethereum-lists/chains/master/_data/chains/eip155-{chain_id}.json" response = requests.get(url) @@ -21,7 +26,7 @@ def get_chain_name(chain_id: str) -> Optional[str]: if response.status_code == 200: return response.json().get("name") return None - except Exception as e: + except IOError as e: print(f"Error getting chain name: {e}") return None @@ -36,7 +41,7 @@ def get_chain_id_from_rpc_url(rpc_url: str) -> Optional[int]: if response.status_code == 200: return int(response.json().get("result"), 16) return None - except Exception as e: + except IOError as e: print(f"Error validating RPC url: {e}") return None @@ -44,33 +49,39 @@ def get_chain_id_from_rpc_url(rpc_url: str) -> Optional[int]: def get_contract_address_and_block_from_tx_hash( rpc_url: str, tx_hash: str ) -> Optional[Dict[str, Any]]: - try: - w3 = Web3(Web3.HTTPProvider(rpc_url)) - tx = w3.eth.get_transaction(HexBytes(tx_hash)) - return { - "block": tx.blockNumber, - "address": mk_contract_address_2(tx.to, tx.input[:32], tx.input[32:]), - } - except Exception as e: - print(f"Error getting transaction: {e}") - return None + ethereum_client = EthereumClient(rpc_url) + tx = ethereum_client.get_transaction(tx_hash) + if not tx: + print(f"Transaction not found: {tx_hash}") + return + return { + "block": tx.get("blockNumber"), + "address": mk_contract_address_2( + tx.get("to"), tx.get("input")[:32], tx.get("input")[32:] + ), + } -def validate_chain(chain_id: str) -> Optional[str]: - if not chain_id: +def validate_chain(chain_id_input: str) -> Optional[Dict[str, Any]]: + if not chain_id_input.isdigit(): ERRORS.append("Chain ID is required.") return - chain_name = get_chain_name(chain_id) + if not chain_id_input: + ERRORS.append("Chain ID is required.") + return + + chain_id = int(chain_id_input) + chain_name = (get_chain_name(chain_id), chain_id) if not chain_name: ERRORS.append(f"Chain with chain ID: {chain_id} not found.") return print(f"Chain name: {chain_name}") - return chain_name + return {"chain_id": chain_id, "chain_name": chain_name} -def validate_rpc(rpc_url: str, chain_id: str) -> None: +def validate_rpc(rpc_url: str, chain_id: int) -> None: if not rpc_url: ERRORS.append("RPC URL is required.") return @@ -100,6 +111,7 @@ def validate_not_required_url(field_name: str, url: str) -> None: return print(f"Validating {field_name} URL -> {url}") + return print(f"Skipping {field_name} URL validation!") @@ -156,7 +168,7 @@ def add_message_to_env(message: str) -> None: def validate_issue_inputs() -> None: issue_body_info = json.loads(os.environ.get("ISSUE_BODY_INFO")) - chain_id = issue_body_info.get("chainId") + chain_id_input = issue_body_info.get("chainId") rpc_url = issue_body_info.get("rpcUrl") blockscout_client_url = issue_body_info.get("blockscoutClientUrl") etherscan_client_url = issue_body_info.get("etherscanClientUrl") @@ -168,7 +180,7 @@ def validate_issue_inputs() -> None: tx_hash_proxy = issue_body_info.get("txHashProxy") print("Inputs to validate:") - print(f"Chain ID: {chain_id}") + print(f"Chain ID: {chain_id_input}") print(f"RPC URL: {rpc_url}") print(f"Blockscout Client URL: {blockscout_client_url}") print(f"Etherscan Client URL: {etherscan_client_url}") @@ -180,7 +192,9 @@ def validate_issue_inputs() -> None: print(f"Deployment Tx hash (Proxy factory): {tx_hash_proxy}") print("Start validation:") - chain_name = validate_chain(chain_id) + chain_info = validate_chain(chain_id_input) + chain_id = chain_info.get("chain_id") if chain_info else None + chain_name = chain_info.get("chain_name") if chain_info else None validate_rpc(rpc_url, chain_id) validate_not_required_url("BlockscoutClientUrl", blockscout_client_url) validate_not_required_url("EtherscanClientUrl", etherscan_client_url) diff --git a/.github/workflows/execute_address_issue.yml b/.github/workflows/create_pr_with_new_address.yml similarity index 97% rename from .github/workflows/execute_address_issue.yml rename to .github/workflows/create_pr_with_new_address.yml index 816e3ae75..614f5f86a 100644 --- a/.github/workflows/execute_address_issue.yml +++ b/.github/workflows/create_pr_with_new_address.yml @@ -108,8 +108,7 @@ jobs: - name: Install dependencies run: | pip install PyGithub - pip install web3==6.15.1 - pip install hexbytes==0.3.1 + pip install safe-eth-py==6.0.0b18 - name: Process Issue and Create PR env: @@ -117,7 +116,7 @@ jobs: ISSUE_BODY_INFO: ${{ steps.get-issue-inputs.outputs.result }} GITHUB_REPOSITORY_NAME: 'safe-global/safe-eth-py' run: | - python .github/scripts/execute_address_issue.py + python .github/scripts/github_adding_addresses/create_pr_with_new_address.py - name: Add comment to issue if: steps.check-comment.outputs.result == 'true' diff --git a/.github/workflows/validate_address_issue.yml b/.github/workflows/validate_new_address_issue_input_data.yml similarity index 95% rename from .github/workflows/validate_address_issue.yml rename to .github/workflows/validate_new_address_issue_input_data.yml index 4ab3db1ce..ceb79e2b3 100644 --- a/.github/workflows/validate_address_issue.yml +++ b/.github/workflows/validate_new_address_issue_input_data.yml @@ -61,17 +61,15 @@ jobs: - name: Install dependencies run: | - pip install web3==6.15.1 pip install safe-eth-py==6.0.0b18 pip install validators==0.22.0 - pip install hexbytes==0.3.1 - name: Validate input data id: validate-input-data env: ISSUE_BODY_INFO: ${{ steps.get-issue-inputs.outputs.result }} run: | - python .github/scripts/validate_address_issue.py + python .github/scripts/github_adding_addresses/validate_new_address_issue_input_data.py - name: Add comment to issue uses: actions/github-script@v5 From b5ca9076564d6f8b199942e24b572326d9cc8d73 Mon Sep 17 00:00:00 2001 From: Felipe Alvarado Date: Tue, 19 Mar 2024 20:52:49 +0100 Subject: [PATCH 3/6] Remove unnecessary HexBytes --- .../github_adding_addresses/create_pr_with_new_address.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/scripts/github_adding_addresses/create_pr_with_new_address.py b/.github/scripts/github_adding_addresses/create_pr_with_new_address.py index 630b996d6..7c65c3655 100644 --- a/.github/scripts/github_adding_addresses/create_pr_with_new_address.py +++ b/.github/scripts/github_adding_addresses/create_pr_with_new_address.py @@ -12,7 +12,6 @@ from github import Github from github.GithubException import GithubException from github.Repository import Repository -from hexbytes import HexBytes from gnosis.eth import EthereumClient @@ -32,7 +31,7 @@ def get_chain_enum_name(chain_id: int) -> Optional[str]: def get_contract_block_from_tx_hash(rpc_url: str, tx_hash: str) -> Optional[int]: ethereum_client = EthereumClient(rpc_url) - tx = ethereum_client.get_transaction(HexBytes(tx_hash)) + tx = ethereum_client.get_transaction(tx_hash) if not tx: print(f"Transaction not found: {tx_hash}") return From 123cb05e723cdb01c3f89b64c9ded634c0f31c3b Mon Sep 17 00:00:00 2001 From: Felipe Alvarado Date: Tue, 19 Mar 2024 21:02:53 +0100 Subject: [PATCH 4/6] Fix version options --- .github/ISSUE_TEMPLATE/add_safe_address_new_chain.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/add_safe_address_new_chain.yml b/.github/ISSUE_TEMPLATE/add_safe_address_new_chain.yml index 248922d29..0a5fb157c 100644 --- a/.github/ISSUE_TEMPLATE/add_safe_address_new_chain.yml +++ b/.github/ISSUE_TEMPLATE/add_safe_address_new_chain.yml @@ -67,9 +67,9 @@ body: multiple: false options: - "1.3.0" - - "1.3.0 L2" + - "1.3.0+L2" - "1.4.1" - - "1.4.1 L2" + - "1.4.1+L2" default: 0 validations: required: true From d9ea3743a7ac6cd2d91aed160ea24608187f0a7a Mon Sep 17 00:00:00 2001 From: Felipe Alvarado Date: Wed, 20 Mar 2024 10:30:41 +0100 Subject: [PATCH 5/6] Add convert name for chain and validate addresses --- .../create_pr_with_new_address.py | 17 ++- .../validate_new_address_issue_input_data.py | 110 ++++++++++++++---- 2 files changed, 100 insertions(+), 27 deletions(-) diff --git a/.github/scripts/github_adding_addresses/create_pr_with_new_address.py b/.github/scripts/github_adding_addresses/create_pr_with_new_address.py index 7c65c3655..dfce7bbb5 100644 --- a/.github/scripts/github_adding_addresses/create_pr_with_new_address.py +++ b/.github/scripts/github_adding_addresses/create_pr_with_new_address.py @@ -16,13 +16,22 @@ from gnosis.eth import EthereumClient +def convert_chain_name(name: str) -> str: + # Change every symbol that is not a word or digit for underscore + name_converted = re.sub(r"[^\w\d]+", r"_", name.upper().replace(")", "")) + # Add underscore at the beggining if start by digit + if name_converted[0].isdigit(): + name_converted = "_" + name_converted + return name_converted + + def get_chain_enum_name(chain_id: int) -> Optional[str]: try: url = f"https://raw.githubusercontent.com/ethereum-lists/chains/master/_data/chains/eip155-{chain_id}.json" response = requests.get(url) if response.status_code == 200: - return response.json().get("name").upper().replace(" ", "_") + return convert_chain_name(response.json().get("name")) return None except IOError as e: print(f"Error getting chain name: {e}") @@ -34,7 +43,7 @@ def get_contract_block_from_tx_hash(rpc_url: str, tx_hash: str) -> Optional[int] tx = ethereum_client.get_transaction(tx_hash) if not tx: print(f"Transaction not found: {tx_hash}") - return + return None return tx.get("blockNumber") @@ -113,7 +122,7 @@ def upsert_chain_id( branch_name, ) - print(f"Entry '{chain_enum_name} = {chain_id}' added successfully.") + print(f"Entry '{chain_enum_name} = {chain_id}' added successfully.") else: print("Error: EthereumNetwork class definition not found in the file.") @@ -369,7 +378,7 @@ def execute_issue_changes() -> None: chain_enum_name = get_chain_enum_name(chain_id) if not chain_enum_name: - return + return None branch_name = create_issue_branch(repo, chain_id, version) diff --git a/.github/scripts/github_adding_addresses/validate_new_address_issue_input_data.py b/.github/scripts/github_adding_addresses/validate_new_address_issue_input_data.py index e4971b693..7c1418448 100644 --- a/.github/scripts/github_adding_addresses/validate_new_address_issue_input_data.py +++ b/.github/scripts/github_adding_addresses/validate_new_address_issue_input_data.py @@ -6,6 +6,7 @@ import json import os +import re import uuid from typing import Any, Dict, Optional @@ -18,13 +19,22 @@ ERRORS = [] -def get_chain_name(chain_id: int) -> Optional[str]: +def convert_chain_name(name: str) -> str: + # Change every symbol that is not a word or digit for underscore + name_converted = re.sub(r"[^\w\d]+", r"_", name.upper().replace(")", "")) + # Add underscore at the beggining if start by digit + if name_converted[0].isdigit(): + name_converted = "_" + name_converted + return name_converted + + +def get_chain_enum_name(chain_id: int) -> Optional[str]: try: url = f"https://raw.githubusercontent.com/ethereum-lists/chains/master/_data/chains/eip155-{chain_id}.json" response = requests.get(url) if response.status_code == 200: - return response.json().get("name") + return convert_chain_name(response.json().get("name")) return None except IOError as e: print(f"Error getting chain name: {e}") @@ -53,7 +63,7 @@ def get_contract_address_and_block_from_tx_hash( tx = ethereum_client.get_transaction(tx_hash) if not tx: print(f"Transaction not found: {tx_hash}") - return + return None return { "block": tx.get("blockNumber"), "address": mk_contract_address_2( @@ -65,17 +75,17 @@ def get_contract_address_and_block_from_tx_hash( def validate_chain(chain_id_input: str) -> Optional[Dict[str, Any]]: if not chain_id_input.isdigit(): ERRORS.append("Chain ID is required.") - return + return None if not chain_id_input: ERRORS.append("Chain ID is required.") - return + return None chain_id = int(chain_id_input) - chain_name = (get_chain_name(chain_id), chain_id) + chain_name = get_chain_enum_name(chain_id) if not chain_name: ERRORS.append(f"Chain with chain ID: {chain_id} not found.") - return + return None print(f"Chain name: {chain_name}") return {"chain_id": chain_id, "chain_name": chain_name} @@ -84,22 +94,22 @@ def validate_chain(chain_id_input: str) -> Optional[Dict[str, Any]]: def validate_rpc(rpc_url: str, chain_id: int) -> None: if not rpc_url: ERRORS.append("RPC URL is required.") - return + return None if not chain_id: ERRORS.append("Unable to validate RPC URL without chain ID.") - return + return None rpc_chain_id = get_chain_id_from_rpc_url(rpc_url) if not rpc_chain_id: ERRORS.append(f"Unable to validate RPC URL {rpc_url}.") - return + return None if rpc_chain_id != int(chain_id): ERRORS.append( f"Chain ID {chain_id} provided is different than chain id obtained from RPC URL {rpc_url} {rpc_chain_id}." ) - return + return None print(f"Chain ID obtained from rpc url: {rpc_chain_id}") @@ -108,51 +118,103 @@ def validate_not_required_url(field_name: str, url: str) -> None: if url: if not validators.url(url): ERRORS.append(f"{field_name} URL ({url}) provided is not valid.") - return + return None print(f"Validating {field_name} URL -> {url}") - return + return None print(f"Skipping {field_name} URL validation!") def validate_version(version: str) -> None: - if version not in ["1.3.0", "1.3.0 L2", "1.4.1", "1.4.1 L2"]: + if version not in ["1.3.0", "1.3.0+L2", "1.4.1", "1.4.1+L2"]: ERRORS.append(f"Version {version} is not valid.") - return + return None print(f"Validating version: {version}!") +def validate_master_copy_address_by_version(address: str, version: str) -> None: + valid_versions_master_copy = { + "1.3.0": [ + "0x69f4D1788e39c87893C980c06EdF4b7f686e2938", + "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552", + ], + "1.3.0+L2": [ + "0x3E5c63644E683549055b9Be8653de26E0B4CD36E", + "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + ], + "1.4.1": ["0x41675C099F32341bf84BFc5382aF534df5C7461a"], + "1.4.1+L2": ["0x29fcB43b46531BcA003ddC8FCB67FFE91900C762"], + } + + if version not in valid_versions_master_copy.keys(): + ERRORS.append("Unable to validate Master copy address without valid version.") + return None + + if address not in valid_versions_master_copy[version]: + ERRORS.append( + f"Master copy address {address} is not valid for version {version}" + ) + return None + + print(f"Master copy address {address} is valid for version {version}") + + +def validate_proxy_address_by_version(address: str, version: str) -> None: + valid_versions_proxy = { + "1.3.0": [ + "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + ], + "1.3.0+L2": [ + "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2", + "0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC", + ], + "1.4.1": ["0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67"], + "1.4.1+L2": ["0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67"], + } + + if version not in valid_versions_proxy.keys(): + ERRORS.append("Unable to validate Proxy address without valid version.") + return None + + if address not in valid_versions_proxy[version]: + ERRORS.append(f"Proxy address {address} is not valid for version {version}") + return None + + print(f"Proxy address {address} is valid for version {version}") + + def validate_address_and_transactions( type: str, address: str, tx_hash: str, rpc_url: str ) -> Optional[Dict[str, Any]]: if not address and not tx_hash: print("Skipping address and tx validation. Not data provided!") - return + return None if not address: ERRORS.append(f"{type} address is required.") - return + return None if not tx_hash: ERRORS.append(f"{type} tx_hash is required.") - return + return None if not rpc_url: ERRORS.append(f"Unable to validate {type} address and tx without RPC URL.") - return + return None tx_info = get_contract_address_and_block_from_tx_hash(rpc_url, tx_hash) if not tx_info: ERRORS.append(f"Unable to obtain {type} Tx info {tx_hash}") - return + return None if tx_info["address"] != address: ERRORS.append( f"{type} address obtained from Tx is diferent than provided {tx_info['address']}" ) - return + return None print(f"{type} Tx. info: {tx_info}") return tx_info @@ -200,6 +262,8 @@ def validate_issue_inputs() -> None: validate_not_required_url("EtherscanClientUrl", etherscan_client_url) validate_not_required_url("EtherscanClientApiUrl", etherscan_client_api_url) validate_version(version) + validate_master_copy_address_by_version(address_master_copy, version) + validate_proxy_address_by_version(address_proxy, version) tx_master_info = validate_address_and_transactions( "Master copy", address_master_copy, tx_hash_master_copy, rpc_url ) @@ -208,13 +272,13 @@ def validate_issue_inputs() -> None: ) if len(ERRORS) > 0: - errors_comment = "\n".join(ERRORS) + errors_comment = "\n- ".join(ERRORS) add_message_to_env( "Validation has failed with the following errors:" + f"\n- {errors_comment}" + "\n\n Validation failed!❌" ) - return + return None chain_name_comment = chain_name if chain_name else "N/A" tx_master_block_comment = tx_master_info.get("block") if tx_master_info else "N/A" tx_proxy_block_comment = tx_proxy_info.get("block") if tx_proxy_info else "N/A" From 39e9c28ba194e2eb3a5c1778ec5bd511c2e0ef42 Mon Sep 17 00:00:00 2001 From: Felipe Alvarado Date: Fri, 22 Mar 2024 13:04:24 +0100 Subject: [PATCH 6/6] Update secret name --- .github/workflows/create_pr_with_new_address.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create_pr_with_new_address.yml b/.github/workflows/create_pr_with_new_address.yml index 614f5f86a..90a164bb7 100644 --- a/.github/workflows/create_pr_with_new_address.yml +++ b/.github/workflows/create_pr_with_new_address.yml @@ -15,7 +15,7 @@ jobs: id: check-comment uses: actions/github-script@v5 with: - github-token: ${{ secrets.ORG_SECRET_NAME }} # Important: This secret with read permissions on the teams information must be configured in repository settings -> secrets -> actions + github-token: ${{ secrets.TOKEN_GITHUB_READ_ORG_TEAMS }} # Important: This secret with read permissions on the teams information must be configured in repository settings -> secrets -> actions script: | const comment = context.payload.comment.body; const teamMembers = await github.rest.teams.listMembersInOrg({