Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions .github/ISSUE_TEMPLATE/bug.md

This file was deleted.

22 changes: 0 additions & 22 deletions .github/ISSUE_TEMPLATE/feature.md

This file was deleted.

48 changes: 0 additions & 48 deletions .github/ISSUE_TEMPLATE/work-item.md

This file was deleted.

16 changes: 0 additions & 16 deletions .github/PULL_REQUEST_TEMPLATE.md

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/commitlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ jobs:
run: pip install commitizen

- name: Check commit history
run: cz check --rev-range $(git rev-list --all --reverse | head -1)..HEAD
run: cz check --rev $(git rev-list --all --reverse | head -1)
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest] # eventually add `windows-latest`
python-version: [3.9, "3.10", "3.11", "3.12"]
python-version: [3.9, "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand Down
11 changes: 6 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-yaml

Expand All @@ -10,24 +10,25 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 24.4.2
rev: 24.10.0
hooks:
- id: black
name: black

- repo: https://github.com/pycqa/flake8
rev: 7.0.0
rev: 7.1.1
hooks:
- id: flake8
additional_dependencies: [flake8-breakpoint, flake8-print, flake8-pydantic, flake8-type-checking]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies: [types-setuptools, pydantic]

- repo: https://github.com/executablebooks/mdformat
rev: 0.7.17
rev: 0.7.19
hooks:
- id: mdformat
additional_dependencies: [mdformat-gfm, mdformat-frontmatter, mdformat-pyproject]
Expand Down
22 changes: 19 additions & 3 deletions ape_quicknode/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
from ape import plugins
from .provider import QuickNode
from .constants import QUICKNODE_NETWORKS


@plugins.register(plugins.ProviderPlugin)
def providers():
from .constants import QUICKNODE_NETWORKS
from .provider import QuickNode

for ecosystem_name in QUICKNODE_NETWORKS:
for network_name in QUICKNODE_NETWORKS[ecosystem_name]:
yield ecosystem_name, network_name, QuickNode
yield ecosystem_name, network_name, QuickNode


def __getattr__(name: str):
if name == "NETWORKS":
from .constants import QUICKNODE_NETWORKS

return QUICKNODE_NETWORKS

if name == "QuickNode":
from .provider import QuickNode

return QuickNode

raise AttributeError(name)
2 changes: 1 addition & 1 deletion ape_quicknode/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@
"mainnet": "https://{subdomain}.avalanche-mainnet.quiknode.pro/{auth_token}/",
"fuji": "https://{subdomain}.avalanche-fuji.quiknode.pro/{auth_token}/",
},
}
}
5 changes: 4 additions & 1 deletion ape_quicknode/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from ape.exceptions import ApeException


class QuickNodeProviderError(ApeException):
"""
Raised when there's an error with the QuickNode provider.
"""


class QuickNodeFeatureNotAvailable(QuickNodeProviderError):
"""
Raised when a requested feature is not available in the current QuickNode plan.
"""


class MissingAuthTokenError(QuickNodeProviderError):
def __init__(self, missing_vars):
super().__init__(f"Missing environment variables: {', '.join(missing_vars)}")
super().__init__(f"Missing environment variables: {', '.join(missing_vars)}")
54 changes: 36 additions & 18 deletions ape_quicknode/provider.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,58 @@
import os
from collections.abc import Iterable
from typing import Any, Optional
from pydantic import BaseModel, Field
from typing import TYPE_CHECKING, Any, Optional

from ape.api import ReceiptAPI, TraceAPI, TransactionAPI, UpstreamProvider
from ape.exceptions import (
APINotImplementedError,
ContractLogicError,
ProviderError,
VirtualMachineError,
)
from ape.types import BlockID
from ape_ethereum.provider import Web3Provider
from ape_ethereum.transactions import AccessList
from eth_typing import HexStr
from pydantic import BaseModel
from requests import HTTPError
from web3 import HTTPProvider, Web3
from web3.exceptions import ContractLogicError as Web3ContractLogicError
from web3.gas_strategies.rpc import rpc_gas_price_strategy
from web3.middleware import geth_poa_middleware

try:
from web3.middleware import ExtraDataToPOAMiddleware # type: ignore
except ImportError:
from web3.middleware import geth_poa_middleware as ExtraDataToPOAMiddleware # type: ignore

from web3.types import RPCEndpoint

from .constants import QUICKNODE_NETWORKS
from .exceptions import QuickNodeFeatureNotAvailable, QuickNodeProviderError, MissingAuthTokenError
from .exceptions import MissingAuthTokenError, QuickNodeFeatureNotAvailable, QuickNodeProviderError
from .trace import QuickNodeTransactionTrace

if TYPE_CHECKING:
from collections.abc import Iterable

from ape.types import BlockID
from ape_ethereum.transactions import AccessList

DEFAULT_ENVIRONMENT_VARIABLE_NAMES = ("QUICKNODE_SUBDOMAIN", "QUICKNODE_AUTH_TOKEN")

NETWORKS_SUPPORTING_WEBSOCKETS = ("ethereum", "arbitrum", "base", "optimism", "polygon")

# Flashbots will try to publish private transactions for 25 blocks.
PRIVATE_TX_BLOCK_WAIT = 25


class QuickNode(Web3Provider, UpstreamProvider, BaseModel):
name: str = Field(default="QuickNode")
name: str = "QuickNode"

def __init__(self, network: Any, name: str = "QuickNode", **data):
super().__init__(network=network, name=name, **data)
self._web3 = None
self.network_uris = {}

@property
def provider_name(self) -> str:
return self.name

network_uris: dict[tuple, str] = {}

@property
Expand All @@ -58,7 +71,10 @@ def uri(self):
if not subdomain or not auth_token:
raise MissingAuthTokenError(DEFAULT_ENVIRONMENT_VARIABLE_NAMES)

if ecosystem_name not in QUICKNODE_NETWORKS or network_name not in QUICKNODE_NETWORKS[ecosystem_name]:
if (
ecosystem_name not in QUICKNODE_NETWORKS
or network_name not in QUICKNODE_NETWORKS[ecosystem_name]
):
raise ProviderError(f"Unsupported network: {ecosystem_name} - {network_name}")

uri_template = QUICKNODE_NETWORKS[ecosystem_name][network_name]
Expand Down Expand Up @@ -88,7 +104,7 @@ def connect(self):
self._web3 = Web3(HTTPProvider(self.uri))
try:
if self.network.ecosystem.name in ["optimism", "base", "polygon"]:
self._web3.middleware_onion.inject(geth_poa_middleware, layer=0)
self._web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)

self._web3.eth.set_gas_price_strategy(rpc_gas_price_strategy)
except Exception as err:
Expand All @@ -104,8 +120,10 @@ def _get_prestate_trace(self, transaction_hash: str) -> dict:

def get_transaction_trace(self, transaction_hash: str, **kwargs) -> TraceAPI:
if not transaction_hash.startswith("0x"):
raise QuickNodeProviderError("Transaction hash must be a hexadecimal string starting with '0x'")

raise QuickNodeProviderError(
"Transaction hash must be a hexadecimal string starting with '0x'"
)

return QuickNodeTransactionTrace(transaction_hash=transaction_hash, provider=self, **kwargs)

def get_virtual_machine_error(self, exception: Exception, **kwargs) -> VirtualMachineError:
Expand Down Expand Up @@ -138,14 +156,14 @@ def get_virtual_machine_error(self, exception: Exception, **kwargs) -> VirtualMa
return VirtualMachineError(message=message, txn=txn)

def create_access_list(
self, transaction: TransactionAPI, block_id: Optional[BlockID] = None
) -> list[AccessList]:
self, transaction: TransactionAPI, block_id: Optional["BlockID"] = None
) -> list["AccessList"]:
if self.network.ecosystem.name == "polygon-zkevm":
raise APINotImplementedError()

return super().create_access_list(transaction, block_id=block_id)

def make_request(self, rpc: str, parameters: Optional[Iterable] = None) -> Any:
def make_request(self, rpc: str, parameters: Optional["Iterable"] = None) -> Any:
parameters = parameters or []
try:
result = self.web3.provider.make_request(RPCEndpoint(rpc), parameters)
Expand Down Expand Up @@ -192,4 +210,4 @@ def get_receipt(
)
return super().get_receipt(
txn_hash, required_confirmations=required_confirmations, timeout=timeout, **kwargs
)
)
4 changes: 3 additions & 1 deletion ape_quicknode/trace.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from functools import cached_property
from typing import Any, Optional

from ape_ethereum.trace import TraceApproach, TransactionTrace
from hexbytes import HexBytes


class QuickNodeTransactionTrace(TransactionTrace):
call_trace_approach: TraceApproach = TraceApproach.PARITY

Expand Down Expand Up @@ -31,4 +33,4 @@ def _top_level_call(self) -> dict:
self.transaction_hash,
{"tracer": "callTracer", "tracerConfig": {"onlyTopLevelCall": True}},
],
)
)
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=51.1.1", "wheel", "setuptools_scm[toml]>=5.0"]
requires = ["setuptools>=75.6.0", "wheel", "setuptools_scm[toml]>=5.0"]

[tool.mypy]
exclude = "build/"
Expand All @@ -15,7 +15,7 @@ write_to = "ape_quicknode/version.py"

[tool.black]
line-length = 100
target-version = ['py39', 'py310', 'py311', 'py312']
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
include = '\.pyi?$'

[tool.pytest.ini_options]
Expand Down
Loading