Skip to content

Commit

Permalink
feat: implement resolution of $ref / field definitions (#65)
Browse files Browse the repository at this point in the history
* feat: implement resolution of $ref / field definitions

* add throwing converter

* implement Definition resolution

* merge

* update submodule

* fixes

* fixes

* tests

* tests / fixes

* linter
  • Loading branch information
jnicoulaud-ledger authored Oct 14, 2024
1 parent f4b4eb2 commit e373917
Show file tree
Hide file tree
Showing 107 changed files with 4,329 additions and 541 deletions.
14 changes: 0 additions & 14 deletions convert_all.sh

This file was deleted.

70 changes: 15 additions & 55 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 8 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies = [
"rich",
"typer",
"lark>=1.2.2",
"pydantic-string-url>=1.0.2",
]

[project.urls]
Expand Down Expand Up @@ -65,14 +66,12 @@ dev = [
"pytest>=8.3.2",
"pytest-cov>=5.0.0",
"pytest-xdist[psutil]>=3.6.1",
"pytest-timeout>=2.3.1",
"pytest-sugar>=1.0.0",
"pytest-benchmark>=4.0.0",
"pytest-depends>=1.0.1",
"pytest-unordered>=0.6.1",
"pytest-raises>=0.11",
"pytest-datadir-ng>=1.1.1",
"sphinx>=7.3.7,<8.1.0",
"prettydiff[terminal]>=0.1.0",
"sphinx>=7.3.7,<8.1.0", # pinned because sphinxcontrib-mermaid is not compatible
"sphinxcontrib-apidoc>=0.5.0",
"sphinxcontrib-typer>=0.5.0",
"sphinxcontrib-mermaid>=0.9.2",
Expand All @@ -83,9 +82,7 @@ dev = [
"autodoc-pydantic>=2.2.0",
"myst-parser[linkify]>=4.0.0",
"ruff>=0.6.3",
"ruff-lsp>=0.0.55",
"prettydiff[terminal]>=0.1.0",
"prettydiff",
"ruff-lsp>=0.0.55"
]

[tool.pdm.version]
Expand All @@ -94,20 +91,20 @@ write_to = "erc7730/VERSION"

[tool.pytest.ini_options]
testpaths = ["tests"]
python_classes = "!Test"
addopts = [
"--import-mode=importlib",
"-p",
"no:web3",
"-Werror",
"-n=0",
"-n=auto",
"--dist=loadscope",
"--basetemp=tmp",
"--junitxml=tests/.tests.xml",
"--cov=src",
"--cov-report=xml:tests/.coverage.xml",
"--cov-report=term",
"--cov-branch",
"--benchmark-disable",
"--cov-branch"
]

[tool.coverage.paths]
Expand Down Expand Up @@ -151,4 +148,4 @@ exclude_dirs = ["tests"]

[tool.codespell]
ignore-words = ".codespellignore"
skip = "pdm.lock"
skip = "pdm.lock,*.ambr"
106 changes: 106 additions & 0 deletions src/erc7730/common/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import os
from dataclasses import dataclass
from io import UnsupportedOperation
from typing import Any

import requests
from pydantic import RootModel
from pydantic_string_url import FileUrl, HttpUrl

from erc7730.common.pydantic import _BaseModel
from erc7730.model.abi import ABI


@dataclass
class ScanSite:
host: str
api_key: str


SCAN_SITES = {
1: ScanSite(host="api.etherscan.io", api_key="ETHERSCAN_API_KEY"),
56: ScanSite(host="api.bscscan.com", api_key="BSCSCAN_API_KEY"),
137: ScanSite(host="api.polygonscan.io", api_key="POLYGONSCAN_API_KEY"),
1101: ScanSite(host="api-zkevm.polygonscan.com", api_key="POLYGONSKEVMSCAN_API_KEY"),
42161: ScanSite(host="api.arbiscan.io", api_key="ARBISCAN_API_KEY"),
8453: ScanSite(host="api.basescan.io", api_key="BASESCAN_API_KEY"),
10: ScanSite(host="api-optimistic.etherscan.io", api_key="OPTIMISMSCAN_API_KEY"),
25: ScanSite(host="api.cronoscan.com", api_key="CRONOSCAN_API_KEY"),
250: ScanSite(host="api.ftmscan.com", api_key="FANTOMSCAN_API_KEY"),
284: ScanSite(host="api-moonbeam.moonscan.io", api_key="MOONSCAN_API_KEY"),
199: ScanSite(host="api.bttcscan.com", api_key="BTTCSCAN_API_KEY"),
59144: ScanSite(host="api.lineascan.build", api_key="LINEASCAN_API_KEY"),
534352: ScanSite(host="api.scrollscan.com", api_key="SCROLLSCAN_API_KEY"),
421614: ScanSite(host="api-sepolia.arbiscan.io", api_key="ARBISCAN_SEPOLIA_API_KEY"),
84532: ScanSite(host="api-sepolia.basescan.org", api_key="BASESCAN_SEPOLIA_API_KEY"),
11155111: ScanSite(host="api-sepolia.etherscan.io", api_key="ETHERSCAN_SEPOLIA_API_KEY"),
11155420: ScanSite(host="api-sepolia-optimistic.etherscan.io", api_key="OPTIMISMSCAN_SEPOLIA_API_KEY"),
534351: ScanSite(host="api-sepolia.scrollscan.com", api_key="SCROLLSCAN_SEPOLIA_API_KEY"),
}


def get_contract_abis(chain_id: int, contract_address: str) -> list[ABI] | None:
"""
Get contract ABIs from an etherscan-like site.
:param chain_id: EIP-155 chain ID
:param contract_address: EVM contract address
:return: deserialized list of ABIs
:raises ValueError: if chain id not supported, API key not setup, or unexpected response
"""
if (site := SCAN_SITES.get(chain_id)) is None:
raise UnsupportedOperation(
f"Chain ID {chain_id} is not supported, please report this to authors of " f"python-erc7730 library"
)
return get(
url=HttpUrl(f"https://{site.host}/api?module=contract&action=getabi&address={contract_address}"),
model=RootModel[list[ABI]],
).root


def get(url: FileUrl | HttpUrl, model: type[_BaseModel]) -> _BaseModel:
"""
Fetch data from a file or an HTTP URL and deserialize it.
This method implements some automated adaptations to handle user provided URLs:
- adaptation to "raw.githubusercontent.com" for GitHub URLs
- injection of API key parameters for etherscan-like sites
- unwrapping of "result" field for etherscan-like sites
:param url: URL to get data from
:param model: Pydantic model to deserialize the data
:return: deserialized response
:raises ValueError: if URL type is not supported, API key not setup, or unexpected response
"""
# TODO add disk cache support
if isinstance(url, HttpUrl):
response = requests.get(_adapt_http_url(url), timeout=10)
response.raise_for_status()
data = _adapt_http_response(url, response.json())
if isinstance(data, str):
return model.model_validate_json(data)
return model.model_validate(data)
if isinstance(url, FileUrl):
# TODO add support for file:// URLs
raise NotImplementedError("file:// URL support is not implemented")
raise ValueError(f"Unsupported URL type: {type(url)}")


def _adapt_http_url(url: HttpUrl) -> HttpUrl:
if url.startswith("https://github.com"):
return HttpUrl(url.replace("https://github.com/", "https://raw.githubusercontent.com/").replace("/blob/", "/"))

for scan_site in SCAN_SITES.values():
if url.startswith(f"https://{scan_site.host}"):
if (api_key := os.environ.get(scan_site.api_key)) is None:
raise ValueError(f"{scan_site.api_key} environment variable is required")
return HttpUrl(f"{url}&apikey={api_key}")

return url


def _adapt_http_response(url: HttpUrl, response: Any) -> Any:
for scan_site in SCAN_SITES.values():
if url.startswith(f"https://{scan_site.host}") and (result := response.get("result")) is not None:
return result
return response
31 changes: 0 additions & 31 deletions src/erc7730/common/client/etherscan.py

This file was deleted.

16 changes: 16 additions & 0 deletions src/erc7730/common/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing_extensions import TypeVar

_T = TypeVar("_T")


def first_not_none(*args: _T | None) -> _T | None:
"""
Return the first argument that is not None.
:param args: sequence of optional values
:return: first non-None value, or None if there are none
"""
for arg in args:
if arg is not None:
return arg
return None
Loading

0 comments on commit e373917

Please sign in to comment.