Source code for erc7730.lint.v2.lint_validate_display_fields

"""
V2 linter that validates display fields against reference ABIs fetched from Etherscan.

In v2, ABI and EIP-712 schemas are NOT embedded in the descriptor. Instead:
  - For contract context: fetch ABI from Etherscan, validate display field paths match ABI params,
    and check selector exhaustiveness.
  - For EIP-712 context: no schema to validate against (no-op).
"""

from typing import final, override

from erc7730.common import client
from erc7730.common.abi import compute_signature, get_functions
from erc7730.common.output import OutputAdder
from erc7730.lint.v2 import ERC7730Linter
from erc7730.lint.v2.path_schemas import compute_format_schema_paths
from erc7730.model.paths import DataPath
from erc7730.model.paths.path_schemas import compute_abi_schema_paths
from erc7730.model.resolved.v2.context import ResolvedContractContext, ResolvedEIP712Context
from erc7730.model.resolved.v2.descriptor import ResolvedERC7730Descriptor


[docs] @final class ValidateDisplayFieldsLinter(ERC7730Linter): """ Validates display fields against reference ABIs fetched from Etherscan. For contract context: - Fetches ABI from Etherscan for each deployment - Validates that display field paths exist in the ABI - Validates that all ABI function params have display fields - Checks that all selectors in the ABI have corresponding display formats For EIP-712 context: - No schema available in v2 resolved model, so no validation is performed """
[docs] @override def lint(self, descriptor: ResolvedERC7730Descriptor, out: OutputAdder) -> None: match descriptor.context: case ResolvedEIP712Context(): pass # no schema to validate against in v2 case ResolvedContractContext(): self._validate_contract_display_fields(descriptor, out)
@classmethod def _validate_contract_display_fields(cls, descriptor: ResolvedERC7730Descriptor, out: OutputAdder) -> None: context = descriptor.context if not isinstance(context, ResolvedContractContext): return if (deployments := context.contract.deployments) is None: return # Try to fetch ABI from Etherscan for the first deployment that succeeds reference_abis = None explorer_url = None for deployment in deployments: try: if (abis := client.get_contract_abis(deployment.chainId, deployment.address)) is None: continue except Exception as e: out.warning( title="Could not fetch ABI", message=f"Fetching reference ABI for chain id {deployment.chainId} failed, display fields will " f"not be validated against ABI: {e}", ) continue reference_abis = get_functions(abis) try: explorer_url = client.get_contract_explorer_url(deployment.chainId, deployment.address) except NotImplementedError: explorer_url = f"<chain id {deployment.chainId} address {deployment.address}>" break if reference_abis is None: return if reference_abis.proxy: return out.info( title="Proxy contract", message=f"Contract {explorer_url} is likely to be a proxy, validation of display fields skipped", ) # Build ABI paths by selector abi_paths_by_selector: dict[str, set[DataPath]] = {} for selector, abi in reference_abis.functions.items(): abi_paths_by_selector[selector] = compute_abi_schema_paths(abi) # Validate display field paths against ABI paths for selector, fmt in descriptor.display.formats.items(): if selector not in abi_paths_by_selector: out.warning( title="Unknown selector", message=f"Selector {selector} in display formats not found in reference ABI (see {explorer_url}). " f"This could indicate a custom function or a stale descriptor.", ) continue format_paths = compute_format_schema_paths(fmt) abi_paths = abi_paths_by_selector[selector] # Check for display fields referencing non-existent ABI paths for path in format_paths.data_paths - abi_paths: out.error( title="Invalid display field", message=f"A display field is defined for `{path}`, but it does not exist in function " f"{selector} ABI (see {explorer_url}). Please check the field path is valid.", ) # Check for ABI paths without corresponding display fields for path in abi_paths - format_paths.data_paths: out.warning( title="Missing display field", message=f"No display field is defined for path `{path}` in function {selector} " f"(see {explorer_url}).", ) # Check selector exhaustiveness: all ABI functions should have display formats for selector, abi in reference_abis.functions.items(): if selector not in descriptor.display.formats: out.warning( title="Missing display format", message=f"Function {compute_signature(abi)} (selector: {selector}) exists in reference ABI " f"(see {explorer_url}) but has no display format defined in the descriptor.", )