-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add proposal id option #63
Merged
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
cc93ba4
First step- seprate api logic from report crafting
nivcertora 5b2f399
Move to data models + fixed tests
nivcertora 59e6ddf
Extract all info for execution
nivcertora d82e269
Organize
nivcertora f54e723
Reverse mapping
nivcertora a57b84b
Add test for new entry points
nivcertora 25d497f
Merge branch 'main' into niv/CERT-7885-Add-Proposal-id-option
nivcertora f6273dd
Merge branch 'main' of github.com:Certora/Quorum into niv/CERT-7885-A…
nivcertora 55c53db
Merge branch 'niv/CERT-7885-Add-Proposal-id-option' of github.com:Cer…
nivcertora bea0486
Make README understandable
nivcertora 51203f3
Address comments
nivcertora c96e420
Merge branch 'main' into niv/CERT-7885-Add-Proposal-id-option
nivcertora 739e281
Intoduce command pattern
nivcertora cb40c8c
fix parse + CI
nivcertora 9d8e472
Merge branch 'main' of github.com:Certora/Quorum into niv/CERT-7885-A…
nivcertora 8288e48
.
nivcertora 72c247e
Fix test
nivcertora 8f09eca
Include Quroum package at install
nivcertora d57485b
Fix ci
nivcertora a51df2c
Try now
nivcertora 7691e48
Address Review comments
nivcertora 4097d3d
fix
nivcertora 20a4b3c
Fix typing
nivcertora 218e6df
Update README file
nivcertora b644502
.
nivcertora 9415e04
.
nivcertora 69891e6
Fix CI
nivcertora 851b574
.
nivcertora 551dc41
Improve description
nivcertora File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import requests | ||
|
||
from Quorum.utils.chain_enum import Chain | ||
from Quorum.apis.governance.data_models import BGDProposalData, PayloadAddresses | ||
|
||
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' | ||
|
||
CHAIN_ID_TO_CHAIN = { | ||
'1': Chain.ETH, | ||
'42161': Chain.ARB, | ||
'43114': Chain.AVAX, | ||
'8453': Chain.BASE, | ||
'56': Chain.BSC, | ||
'100': Chain.GNO, | ||
'10': Chain.OPT, | ||
'137': Chain.POLY, | ||
'534352': Chain.SCROLL, | ||
'324': Chain.ZK, | ||
'59144': Chain.LINEA, | ||
} | ||
|
||
class AaveGovernanceAPI: | ||
""" | ||
A utility class to interact with the BGD governance cache and retrieve | ||
relevant information about Aave proposals and payload addresses. | ||
""" | ||
|
||
def __init__(self) -> None: | ||
self.session = requests.Session() | ||
|
||
def get_proposal_data(self, proposal_id: int) -> BGDProposalData: | ||
""" | ||
Fetches and returns the data for a given proposal. | ||
|
||
Args: | ||
proposal_id: The ID of the proposal to fetch. | ||
|
||
Returns: | ||
A BGDProposalData object. | ||
""" | ||
proposal_data_link = f'{PROPOSALS_URL}/{proposal_id}.json' | ||
resp = self.session.get(proposal_data_link) | ||
resp.raise_for_status() | ||
|
||
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]: | ||
""" | ||
Retrieves a list of payload addresses for a given payload ID, chain, and controller. | ||
|
||
Args: | ||
chain_id: The chain ID for the proposal. | ||
controller: The controller for the proposal. | ||
payload_id: The ID of the payload to fetch. | ||
|
||
Returns: | ||
A list of addresses that are part of the payload. | ||
""" | ||
url = f'{BASE_BGD_CACHE_REPO}/{chain_id}/{controller}/payloads/{payload_id}.json' | ||
resp = self.session.get(url) | ||
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']] | ||
|
||
def get_all_payloads_addresses(self, proposal_id: int) -> list[PayloadAddresses]: | ||
""" | ||
Retrieves all payload addresses for a given proposal. | ||
|
||
Args: | ||
proposal_id: The ID of the proposal to fetch. | ||
|
||
Returns: | ||
A list of PayloadAddresses objects, each containing a chain ID and a list of addresses. | ||
""" | ||
data = self.get_proposal_data(proposal_id) | ||
results = [] | ||
for p in data.proposal.payloads: | ||
addresses = self.get_payload_addresses(p.chain, p.payloads_controller, p.payload_id) | ||
results.append(PayloadAddresses(chain=CHAIN_ID_TO_CHAIN[p.chain], addresses=addresses)) | ||
return results |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
from typing import List, Optional | ||
from pydantic import BaseModel, Field | ||
|
||
from Quorum.utils.chain_enum import Chain | ||
|
||
|
||
class IPFSData(BaseModel): | ||
title: str = 'N/A' | ||
discussions: str = 'N/A' | ||
|
||
|
||
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) | ||
voting_portal: Optional[str] = Field(alias='votingPortal') | ||
ipfs_hash: Optional[str] = Field(alias='ipfsHash') | ||
access_level: Optional[str | int] = Field(alias='accessLevel') | ||
|
||
class Config: | ||
allow_population_by_alias = True | ||
|
||
|
||
class EventArgs(BaseModel): | ||
creator: str = 'N/A' | ||
access_level: Optional[str | int] = Field(alias='accessLevel', default='N/A') | ||
ipfs_hash: str = Field(alias='ipfsHash', default='N/A') | ||
|
||
class Config: | ||
allow_population_by_alias = True | ||
|
||
|
||
class EventData(BaseModel): | ||
transaction_hash: str = Field(alias='transactionHash') | ||
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) | ||
|
||
|
||
class PayloadAddresses(BaseModel): | ||
chain: Chain | ||
addresses: List[str] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,105 @@ | ||
import requests | ||
from dataclasses import dataclass | ||
import json5 as json | ||
from pydantic import BaseModel | ||
from typing import Any, Dict | ||
|
||
from Quorum.apis.governance.aave_governance import AaveGovernanceAPI | ||
from Quorum.apis.governance.data_models import BGDProposalData, IPFSData, ProposalData, EventData | ||
|
||
|
||
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: | ||
class ChainInfo(BaseModel): | ||
name: str | ||
block_explorer_link: str | ||
|
||
|
||
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/') | ||
} | ||
|
||
|
||
def __extract_payload_addresses(session: requests.Session, chain_id: str, controller: str, payload_id: int) -> list[str]: | ||
resp = session.get(f'{BASE_BGD_CACHE_REPO}/{chain_id}/{controller}/payloads/{payload_id}.json') | ||
resp.raise_for_status() | ||
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() | ||
bgd_data: BGDProposalData = api.get_proposal_data(proposal_id) | ||
|
||
# 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() | ||
|
||
# Construct an empty dictionary for the Jinja2 context | ||
tags: Dict[str, Any] = {} | ||
|
||
# Basic info | ||
tags['proposal_id'] = str(proposal_id) | ||
tags['proposal_title'] = ipfs_data.title | ||
tags['voting_link'] = f'https://vote.onaave.com/proposal/?proposalId={proposal_id}' | ||
tags['gov_forum_link'] = ipfs_data.discussions | ||
|
||
# Multi-chain references | ||
tags['chain'] = [] | ||
tags['payload_link'] = [] | ||
tags['payload_seatbelt_link'] = [] | ||
|
||
# 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 | ||
) | ||
|
||
# For each address, build up the chain/payload references | ||
for i, address in enumerate(addresses, 1): | ||
chain_info = AAVE_CHAIN_MAPPING.get(p.chain) | ||
if not chain_info: | ||
# If chain info is missing, skip | ||
continue | ||
|
||
payload_data = resp.json() | ||
chain_display = chain_info.name + (f' {i}' if i != 1 else '') | ||
tags['chain'].append(chain_display) | ||
|
||
return [a['target'] for a in payload_data['payload']['actions']] | ||
block_explorer_link = f'{chain_info.block_explorer_link}/{address}' | ||
tags['payload_link'].append(block_explorer_link) | ||
|
||
seatbelt_link = f'{SEATBELT_PAYLOADS_URL}/{p.chain}/{p.payloads_controller}/{p.payload_id}.md' | ||
tags['payload_seatbelt_link'].append(seatbelt_link) | ||
|
||
def get_aave_tags(proposal_id: int) -> dict: | ||
with requests.Session() as session: | ||
proposal_data_link = f'{PROPOSALS_URL}/{proposal_id}.json' | ||
resp = session.get(proposal_data_link) | ||
resp.raise_for_status() | ||
# Transaction info | ||
transaction_hash = create_event.transaction_hash | ||
tags['transaction_hash'] = transaction_hash | ||
tags['transaction_link'] = f'https://etherscan.io/tx/{transaction_hash}' | ||
|
||
proposal_data: dict = resp.json() | ||
# Creator + event args | ||
args = create_event.args | ||
tags['creator'] = args.creator | ||
tags['access_level'] = args.access_level | ||
tags['ipfs_hash'] = args.ipfs_hash | ||
|
||
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. | ||
tags['createProposal_parameters_data'] = json.dumps(proposal_data.model_dump(), indent=4) | ||
|
||
tags = {} | ||
tags['proposal_id'] = str(proposal_id) | ||
tags['proposal_title'] = ipfs.get('title', 'N/A') | ||
tags['voting_link'] = f'https://vote.onaave.com/proposal/?proposalId={proposal_id}' | ||
tags['gov_forum_link'] = ipfs.get('discussions', 'N/A') | ||
# seatbelt link for entire proposal | ||
tags['seatbelt_link'] = f'{BASE_SEATBELT_REPO}/proposals/{proposal_id}.md' | ||
|
||
tags['chain'], tags['payload_link'], tags['payload_seatbelt_link'] = [], [], [] | ||
for p in proposal.get('payloads', []): | ||
# These are necessary fields in the payload data to construct the payload fields. | ||
if not all(k in p for k in ['chain', 'payloadsController', 'payloadId']): | ||
continue | ||
addresses = __extract_payload_addresses(session, p['chain'], p['payloadsController'], p['payloadId']) | ||
for i, address in enumerate(addresses, 1): | ||
tags['chain'].append(AAVE_CHAIN_MAPPING[p['chain']].name + (f' {i}' if i != 1 else '')) | ||
tags['payload_link'].append(f'{AAVE_CHAIN_MAPPING[p["chain"]].block_explorer_link}/{address}') | ||
tags['payload_seatbelt_link'].append( | ||
f'{SEATBELT_PAYLOADS_URL}/{p["chain"]}/{p["payloadsController"]}/{p["payloadId"]}.md' | ||
) | ||
|
||
tags['transaction_hash'] = create_event.get('transactionHash', 'N/A') | ||
tags['transaction_link'] = f'https://etherscan.io/tx/{tags["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') | ||
|
||
tags['createProposal_parameters_data'] = json.dumps({k: proposal.get(k, 'N/A') for k | ||
in ['payloads', 'votingPortal', 'ipfsHash']}, indent=4) | ||
|
||
tags['seatbelt_link'] = f'{BASE_SEATBELT_REPO}/proposals/{proposal_id}.md' | ||
|
||
return tags | ||
return tags |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I never seen this syntax. Is it checking if the first expression is None and if so assigns the second expression?
Did you check this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly. But must make sure bgd_data is not None.
Basically this replace the if/else one liner
(proposal_data: ProposalData = bgd_data.proposal if bgd_data.proposal is not None else ProposalData())
both expressions are equal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's cool
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nivcertora You haven't addressed this yet