Skip to content

Commit

Permalink
Move to data models + fixed tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nivcertora committed Jan 2, 2025
1 parent cc93ba4 commit 5b2f399
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 85 deletions.
101 changes: 82 additions & 19 deletions Quorum/apis/governance/aave_governance.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
"""
Expand All @@ -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']]
86 changes: 50 additions & 36 deletions Quorum/auto_report/aave_tags.py
Original file line number Diff line number Diff line change
@@ -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
50 changes: 20 additions & 30 deletions Quorum/tests/expected/test_auto_report/v3-132.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 5b2f399

Please sign in to comment.