diff --git a/Quorum/apis/governance/aave_governance.py b/Quorum/apis/governance/aave_governance.py index 81c133b..7efdd47 100644 --- a/Quorum/apis/governance/aave_governance.py +++ b/Quorum/apis/governance/aave_governance.py @@ -1,33 +1,91 @@ import requests -from dataclasses import dataclass +from typing import Optional, List +from pydantic import BaseModel, Field +import json5 as json +# ============================== +# Constants / Endpoints +# ============================== BASE_BGD_CACHE_REPO = 'https://raw.githubusercontent.com/bgd-labs/v3-governance-cache/refs/heads/main/cache' PROPOSALS_URL = f'{BASE_BGD_CACHE_REPO}/1/0x9AEE0B04504CeF83A65AC3f0e838D0593BCb2BC7/proposals' BASE_SEATBELT_REPO = 'https://github.com/bgd-labs/seatbelt-gov-v3/blob/main/reports' SEATBELT_PAYLOADS_URL = f'{BASE_SEATBELT_REPO}/payloads' -@dataclass -class ChainInfo: + +# ============================== +# Chain Info Model +# ============================== +class ChainInfo(BaseModel): name: str block_explorer_link: str +# ============================== +# Data Models for BGD JSON +# ============================== +class IPFSData(BaseModel): + title: Optional[str] = None + discussions: Optional[str] = None + + +class PayloadData(BaseModel): + chain: str + payloads_controller: str = Field(alias='payloadsController') + payload_id: int = Field(alias='payloadId') + + class Config: + allow_population_by_alias = True + + +class ProposalData(BaseModel): + payloads: list[PayloadData] = Field(default_factory=list) + votingPortal: Optional[str] = None + ipfsHash: Optional[str] = None + + +class EventArgs(BaseModel): + creator: Optional[str] = None + accessLevel: Optional[int] = None + ipfsHash: Optional[str] = None + + +class EventData(BaseModel): + transactionHash: Optional[str] = None + args: EventArgs = Field(default_factory=EventArgs) + + +class BGDProposalData(BaseModel): + """ + Represents the entire JSON structure returned by the BGD cache + for a given proposal. + """ + ipfs: Optional[IPFSData] = None + proposal: Optional[ProposalData] = None + events: List[EventData] = Field(default_factory=list) + + +# ============================== +# Mapping for Chains +# ============================== AAVE_CHAIN_MAPPING = { - '1': ChainInfo('Ethereum', 'https://etherscan.io/address'), - '137': ChainInfo('Polygon', 'https://polygonscan.com/address'), - '43114': ChainInfo('Avalanche', 'https://snowtrace.io/address'), - '8453': ChainInfo('Base', 'https://basescan.org/address'), - '42161': ChainInfo('Arbitrum One', 'https://arbiscan.io/address'), - '1088': ChainInfo('Metis', 'https://explorer.metis.io/address'), - '10': ChainInfo('OP Mainnet', 'https://optimistic.etherscan.io/address'), - '56': ChainInfo('BNB Smart Chain', 'https://bscscan.com/address'), - '100': ChainInfo('Gnosis', 'https://gnosisscan.io/address'), - '534352': ChainInfo('Scroll', 'https://scrollscan.com/address'), - '324': ChainInfo('zkSync Era', 'https://era.zksync.network/address'), - '59144': ChainInfo('Linea', 'https://lineascan.build/'), + '1': ChainInfo(name='Ethereum', block_explorer_link='https://etherscan.io/address'), + '137': ChainInfo(name='Polygon', block_explorer_link='https://polygonscan.com/address'), + '43114': ChainInfo(name='Avalanche', block_explorer_link='https://snowtrace.io/address'), + '8453': ChainInfo(name='Base', block_explorer_link='https://basescan.org/address'), + '42161': ChainInfo(name='Arbitrum One', block_explorer_link='https://arbiscan.io/address'), + '1088': ChainInfo(name='Metis', block_explorer_link='https://explorer.metis.io/address'), + '10': ChainInfo(name='OP Mainnet', block_explorer_link='https://optimistic.etherscan.io/address'), + '56': ChainInfo(name='BNB Smart Chain',block_explorer_link='https://bscscan.com/address'), + '100': ChainInfo(name='Gnosis', block_explorer_link='https://gnosisscan.io/address'), + '534352':ChainInfo(name='Scroll', block_explorer_link='https://scrollscan.com/address'), + '324': ChainInfo(name='zkSync Era', block_explorer_link='https://era.zksync.network/address'), + '59144': ChainInfo(name='Linea', block_explorer_link='https://lineascan.build/') } +# ============================== +# AaveGovernanceAPI +# ============================== class AaveGovernanceAPI: """ A utility class to interact with the BGD governance cache and retrieve @@ -37,16 +95,20 @@ class AaveGovernanceAPI: def __init__(self) -> None: self.session = requests.Session() - def get_proposal_data(self, proposal_id: int) -> dict: + def get_proposal_data(self, proposal_id: int) -> BGDProposalData: """ - Fetches the proposal data from the BGD governance cache. + Fetches the proposal data from the BGD governance cache and + returns a pydantic-validated object. """ proposal_data_link = f'{PROPOSALS_URL}/{proposal_id}.json' resp = self.session.get(proposal_data_link) resp.raise_for_status() - return resp.json() - def get_payload_addresses(self, chain_id: str, controller: str, payload_id: int) -> list[str]: + raw_json = resp.json() + # Parse into our data model + return BGDProposalData(**raw_json) + + def get_payload_addresses(self, chain_id: str, controller: str, payload_id: int) -> List[str]: """ Fetches and returns the addresses from a given chain/payload. """ @@ -55,4 +117,5 @@ def get_payload_addresses(self, chain_id: str, controller: str, payload_id: int) resp.raise_for_status() payload_data = resp.json() + # We only need the 'target' field from each action return [a['target'] for a in payload_data['payload']['actions']] diff --git a/Quorum/auto_report/aave_tags.py b/Quorum/auto_report/aave_tags.py index 79d8cb7..83664e8 100644 --- a/Quorum/auto_report/aave_tags.py +++ b/Quorum/auto_report/aave_tags.py @@ -1,74 +1,88 @@ import json5 as json +from typing import Any, Dict +# Import the data models and API from Quorum.apis.governance.aave_governance import ( AaveGovernanceAPI, AAVE_CHAIN_MAPPING, BASE_SEATBELT_REPO, SEATBELT_PAYLOADS_URL, + BGDProposalData, + IPFSData, + ProposalData, + EventData, ) -def get_aave_tags(proposal_id: int) -> dict: + +def get_aave_tags(proposal_id: int) -> Dict[str, Any]: """ Utility function that orchestrates calls to AaveGovernanceAPI and compiles the final dictionary of tags for a given proposal. + + Returns: + A dictionary that can be directly rendered by your Jinja2 template. """ api = AaveGovernanceAPI() - proposal_data = api.get_proposal_data(proposal_id) + bgd_data: BGDProposalData = api.get_proposal_data(proposal_id) - ipfs: dict = proposal_data.get('ipfs', {}) - proposal: dict = proposal_data.get('proposal', {}) - create_event: dict = proposal_data.get('events', [{}])[0] # The create event is always the first. + # Safely unwrap fields (some might be None). + ipfs_data: IPFSData = bgd_data.ipfs or IPFSData() + proposal_data: ProposalData = bgd_data.proposal or ProposalData() + create_event: EventData = bgd_data.events[0] if bgd_data.events else EventData() - tags = {} + # Construct an empty dictionary for the Jinja2 context + tags: Dict[str, Any] = {} + + # Basic info tags['proposal_id'] = str(proposal_id) - tags['proposal_title'] = ipfs.get('title', 'N/A') + tags['proposal_title'] = ipfs_data.title if ipfs_data.title else 'N/A' tags['voting_link'] = f'https://vote.onaave.com/proposal/?proposalId={proposal_id}' - tags['gov_forum_link'] = ipfs.get('discussions', 'N/A') - - # Prepare lists for multi-chain payload references - tags['chain'], tags['payload_link'], tags['payload_seatbelt_link'] = [], [], [] + tags['gov_forum_link'] = ipfs_data.discussions if ipfs_data.discussions else 'N/A' - for p in proposal.get('payloads', []): - if not all(k in p for k in ['chain', 'payloadsController', 'payloadId']): - # Skip incomplete payload definitions - continue + # Multi-chain references + tags['chain'] = [] + tags['payload_link'] = [] + tags['payload_seatbelt_link'] = [] - chain_id = p['chain'] - controller = p['payloadsController'] - pid = p['payloadId'] + # Go through each payload in the proposal + for p in proposal_data.payloads: + # For each payload, retrieve the addresses from the API + addresses = api.get_payload_addresses( + chain_id = p.chain, + controller = p.payloads_controller, + payload_id = p.payload_id + ) - # Extract addresses from the payload - addresses = api.get_payload_addresses(chain_id, controller, pid) - - # For each address, add chain name, block explorer link, seatbelt link, etc. + # For each address, build up the chain/payload references for i, address in enumerate(addresses, 1): - chain_info = AAVE_CHAIN_MAPPING.get(chain_id) + chain_info = AAVE_CHAIN_MAPPING.get(p.chain) if not chain_info: # If chain info is missing, skip continue chain_display = chain_info.name + (f' {i}' if i != 1 else '') tags['chain'].append(chain_display) - + block_explorer_link = f'{chain_info.block_explorer_link}/{address}' tags['payload_link'].append(block_explorer_link) - - seatbelt_link = f'{SEATBELT_PAYLOADS_URL}/{chain_id}/{controller}/{pid}.md' + + seatbelt_link = f'{SEATBELT_PAYLOADS_URL}/{p.chain}/{p.payloads_controller}/{p.payload_id}.md' tags['payload_seatbelt_link'].append(seatbelt_link) - tags['transaction_hash'] = create_event.get('transactionHash', 'N/A') - tags['transaction_link'] = f'https://etherscan.io/tx/{tags["transaction_hash"]}' + # Transaction info + transaction_hash = create_event.transactionHash or 'N/A' + tags['transaction_hash'] = transaction_hash + tags['transaction_link'] = f'https://etherscan.io/tx/{transaction_hash}' - args: dict = create_event.get('args', {}) - tags['creator'] = args.get('creator', 'N/A') - tags['access_level'] = str(args.get('accessLevel', 'N/A')) - tags['ipfs_hash'] = args.get('ipfsHash', 'N/A') + # Creator + event args + args = create_event.args + tags['creator'] = args.creator if args.creator else 'N/A' + tags['access_level'] = str(args.accessLevel) if args.accessLevel is not None else 'N/A' + tags['ipfs_hash'] = args.ipfsHash if args.ipfsHash else 'N/A' - tags['createProposal_parameters_data'] = json.dumps( - {k: proposal.get(k, 'N/A') for k in ['payloads', 'votingPortal', 'ipfsHash']}, - indent=4 - ) + tags['createProposal_parameters_data'] = json.dumps(proposal_data.model_dump(), indent=4) + # seatbelt link for entire proposal tags['seatbelt_link'] = f'{BASE_SEATBELT_REPO}/proposals/{proposal_id}.md' return tags diff --git a/Quorum/tests/expected/test_auto_report/v3-132.md b/Quorum/tests/expected/test_auto_report/v3-132.md index 9cff55d..d5f95c9 100644 --- a/Quorum/tests/expected/test_auto_report/v3-132.md +++ b/Quorum/tests/expected/test_auto_report/v3-132.md @@ -56,63 +56,53 @@ Transaction: [0x423b2b381444d3a8a347536eaf643da3c7bc5e764ff4881863e012305d9542ba payloads: [ { chain: "1", - accessLevel: 1, - payloadsController: "0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5", - payloadId: 146, + payloads_controller: "0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5", + payload_id: 146, }, { chain: "137", - accessLevel: 1, - payloadsController: "0x401B5D0294E23637c18fcc38b1Bca814CDa2637C", - payloadId: 71, + payloads_controller: "0x401B5D0294E23637c18fcc38b1Bca814CDa2637C", + payload_id: 71, }, { chain: "43114", - accessLevel: 1, - payloadsController: "0x1140CB7CAfAcC745771C2Ea31e7B5C653c5d0B80", - payloadId: 42, + payloads_controller: "0x1140CB7CAfAcC745771C2Ea31e7B5C653c5d0B80", + payload_id: 42, }, { chain: "10", - accessLevel: 1, - payloadsController: "0x0E1a3Af1f9cC76A62eD31eDedca291E63632e7c4", - payloadId: 38, + payloads_controller: "0x0E1a3Af1f9cC76A62eD31eDedca291E63632e7c4", + payload_id: 38, }, { chain: "42161", - accessLevel: 1, - payloadsController: "0x89644CA1bB8064760312AE4F03ea41b05dA3637C", - payloadId: 39, + payloads_controller: "0x89644CA1bB8064760312AE4F03ea41b05dA3637C", + payload_id: 39, }, { chain: "1088", - accessLevel: 1, - payloadsController: "0x2233F8A66A728FBa6E1dC95570B25360D07D5524", - payloadId: 19, + payloads_controller: "0x2233F8A66A728FBa6E1dC95570B25360D07D5524", + payload_id: 19, }, { chain: "8453", - accessLevel: 1, - payloadsController: "0x2DC219E716793fb4b21548C0f009Ba3Af753ab01", - payloadId: 25, + payloads_controller: "0x2DC219E716793fb4b21548C0f009Ba3Af753ab01", + payload_id: 25, }, { chain: "100", - accessLevel: 1, - payloadsController: "0x9A1F491B86D09fC1484b5fab10041B189B60756b", - payloadId: 23, + payloads_controller: "0x9A1F491B86D09fC1484b5fab10041B189B60756b", + payload_id: 23, }, { chain: "534352", - accessLevel: 1, - payloadsController: "0x6b6B41c0f8C223715f712BE83ceC3c37bbfDC3fE", - payloadId: 15, + payloads_controller: "0x6b6B41c0f8C223715f712BE83ceC3c37bbfDC3fE", + payload_id: 15, }, { chain: "56", - accessLevel: 1, - payloadsController: "0xE5EF2Dd06755A97e975f7E282f828224F2C3e627", - payloadId: 17, + payloads_controller: "0xE5EF2Dd06755A97e975f7E282f828224F2C3e627", + payload_id: 17, }, ], votingPortal: "0x9b24C168d6A76b5459B1d47071a54962a4df36c3",