Skip to content
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 4337 bundler API client #776

Merged
merged 7 commits into from
Feb 20, 2024
Merged
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
21 changes: 2 additions & 19 deletions gnosis/cowsap/cow_swap_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from functools import cached_property
from typing import Any, Dict, List, Optional, TypedDict, Union, cast

import requests
from eth_account import Account
from eth_account.messages import encode_defunct
from eth_typing import AnyAddress, ChecksumAddress, HexStr
Expand All @@ -11,6 +10,7 @@
from gnosis.eth.eip712 import eip712_encode_hash

from ..eth.constants import NULL_ADDRESS
from ..util.http import prepare_http_session
from .order import Order, OrderKind


Expand Down Expand Up @@ -64,26 +64,9 @@ def __init__(self, ethereum_network: EthereumNetwork, request_timeout: int = 10)
self.network
]
self.base_url = self.API_BASE_URLS[self.network]
self.http_session = self._prepare_http_session()
self.http_session = prepare_http_session(10, 100)
self.request_timeout = request_timeout

def _prepare_http_session(self) -> requests.Session:
"""
Prepare http session with custom pooling. See:
https://urllib3.readthedocs.io/en/stable/advanced-usage.html
https://docs.python-requests.org/en/v1.2.3/api/#requests.adapters.HTTPAdapter
https://web3py.readthedocs.io/en/stable/providers.html#httpprovider
"""
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=100,
pool_block=False,
)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session

@cached_property
def weth_address(self) -> ChecksumAddress:
"""
Expand Down
6 changes: 6 additions & 0 deletions gnosis/eth/account_abstraction/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Account abstraction utils
"""
# flake8: noqa F401
from .bundler_client import BundlerClient
from .user_operation import UserOperation
74 changes: 74 additions & 0 deletions gnosis/eth/account_abstraction/bundler_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import logging
from typing import Any, Dict, List, Optional

from eth_typing import ChecksumAddress, HexStr

from gnosis.util.http import prepare_http_session

from .user_operation import UserOperation

logger = logging.getLogger(__name__)


class BundlerClient:
"""
Account Abstraction client for EIP4337 bundlers
"""

def __init__(
self,
url: str,
retry_count: int = 1,
):
self.url = url
self.retry_count = retry_count
self.http_session = prepare_http_session(1, 100, retry_count=retry_count)
moisses89 marked this conversation as resolved.
Show resolved Hide resolved

def _do_request(self, payload: Dict[Any, Any]) -> Optional[Dict[str, Any]]:
response = self.http_session.post(self.url, json=payload)
if not response.ok:
raise ConnectionError(
f"Error connecting to bundler {self.url} : {response.status_code} {response.content}"
)

response_json = response.json()
result = response_json.get("result")
if not result and "error" in response_json:
logger.warning(
"Bundler returned error for payload %s : %s",
payload,
response_json["error"],
)
return result

def get_user_operation_by_hash(
moisses89 marked this conversation as resolved.
Show resolved Hide resolved
self, user_operation_hash: HexStr
) -> Optional[UserOperation]:
payload = {
"jsonrpc": "2.0",
"method": "eth_getUserOperationByHash",
"params": [user_operation_hash],
"id": 1,
}
result = self._do_request(payload)
return UserOperation(user_operation_hash, result) if result else None

def get_user_operation_receipt(
self, user_operation_hash: HexStr
) -> Optional[Dict[str, Any]]:
payload = {
"jsonrpc": "2.0",
"method": "eth_getUserOperationReceipt",
"params": [user_operation_hash],
"id": 1,
}
return self._do_request(payload)

def supported_entry_points(self) -> List[ChecksumAddress]:
moisses89 marked this conversation as resolved.
Show resolved Hide resolved
payload = {
"jsonrpc": "2.0",
"method": "eth_supportedEntryPoints",
"params": [],
"id": 1,
}
return self._do_request(payload)
110 changes: 110 additions & 0 deletions gnosis/eth/account_abstraction/user_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import dataclasses
from typing import Any, Dict, Union

from eth_abi import encode as abi_encode
from eth_typing import ChecksumAddress, HexStr
from hexbytes import HexBytes

from gnosis.eth.utils import fast_keccak


@dataclasses.dataclass
class UserOperation:
"""
EIP4337 UserOperation for Entrypoint v0.6

https://github.com/eth-infinitism/account-abstraction/blob/v0.6.0
"""

sender: ChecksumAddress
nonce: int
init_code: bytes
call_data: bytes
call_gas_limit: int
verification_gas_limit: int
pre_verification_gas: int
max_fee_per_gas: int
max_priority_fee_per_gas: int
paymaster_and_data: bytes
signature: bytes
entry_point: ChecksumAddress
transaction_hash: bytes
block_hash: bytes
block_number: int
user_operation_hash: bytes

def __init__(
moisses89 marked this conversation as resolved.
Show resolved Hide resolved
self,
user_operation_hash: Union[HexStr, bytes],
user_operation_response: Dict[str, Any],
):
self.sender = ChecksumAddress(
user_operation_response["userOperation"]["sender"]
)
self.nonce = int(user_operation_response["userOperation"]["nonce"], 16)
self.init_code = HexBytes(user_operation_response["userOperation"]["initCode"])
self.call_data = HexBytes(user_operation_response["userOperation"]["callData"])
self.call_gas_limit = int(
user_operation_response["userOperation"]["callGasLimit"], 16
)
self.verification_gas_limit = int(
user_operation_response["userOperation"]["verificationGasLimit"], 16
)
self.pre_verification_gas = int(
user_operation_response["userOperation"]["preVerificationGas"], 16
)
self.max_fee_per_gas = int(
user_operation_response["userOperation"]["maxFeePerGas"], 16
)
self.max_priority_fee_per_gas = int(
user_operation_response["userOperation"]["maxPriorityFeePerGas"], 16
)
self.paymaster_and_data = HexBytes(
user_operation_response["userOperation"]["paymasterAndData"]
)
self.signature = HexBytes(user_operation_response["userOperation"]["signature"])
self.entry_point = ChecksumAddress(user_operation_response["entryPoint"])
self.transaction_hash = HexBytes(user_operation_response["transactionHash"])
self.block_hash = HexBytes(user_operation_response["blockHash"])
self.block_number = int(user_operation_response["blockNumber"], 16)
self.user_operation_hash = HexBytes(user_operation_hash)

def __str__(self):
return f"User Operation sender={self.sender} nonce={self.nonce} hash={self.user_operation_hash.hex()}"

def calculate_user_operation_hash(self, chain_id: int) -> bytes:
hash_init_code = fast_keccak(self.init_code)
hash_call_data = fast_keccak(self.call_data)
hash_paymaster_and_data = fast_keccak(self.paymaster_and_data)
user_operation_encoded = abi_encode(
[
"address",
"uint256",
"bytes32",
"bytes32",
"uint256",
"uint256",
"uint256",
"uint256",
"uint256",
"bytes32",
],
[
self.sender,
self.nonce,
hash_init_code,
hash_call_data,
self.call_gas_limit,
self.verification_gas_limit,
self.pre_verification_gas,
self.max_fee_per_gas,
self.max_priority_fee_per_gas,
hash_paymaster_and_data,
],
)
return fast_keccak(
abi_encode(
["bytes32", "address", "uint256"],
[fast_keccak(user_operation_encoded), self.entry_point, chain_id],
)
)
22 changes: 2 additions & 20 deletions gnosis/eth/clients/etherscan_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from typing import Any, Dict, Optional
from urllib.parse import urljoin

import requests

from ...util.http import prepare_http_session
from .. import EthereumNetwork
from .contract_metadata import ContractMetadata

Expand Down Expand Up @@ -118,27 +117,10 @@ def __init__(
raise EtherscanClientConfigurationProblem(
f"Network {network.name} - {network.value} not supported"
)
self.http_session = self._prepare_http_session()
self.http_session = prepare_http_session(10, 100)
self.http_session.headers = self.HTTP_HEADERS
self.request_timeout = request_timeout

def _prepare_http_session(self) -> requests.Session:
"""
Prepare http session with custom pooling. See:
https://urllib3.readthedocs.io/en/stable/advanced-usage.html
https://docs.python-requests.org/en/v1.2.3/api/#requests.adapters.HTTPAdapter
https://web3py.readthedocs.io/en/stable/providers.html#httpprovider
"""
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=100,
pool_block=False,
)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session

def build_url(self, path: str):
url = urljoin(self.base_api_url, path)
if self.api_key:
Expand Down
22 changes: 2 additions & 20 deletions gnosis/eth/clients/sourcify_client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from typing import Any, Dict, List, Optional
from urllib.parse import urljoin

import requests

from gnosis.util import cache

from ...util.http import prepare_http_session
from .. import EthereumNetwork
from ..utils import fast_is_checksum_address
from .contract_metadata import ContractMetadata
Expand Down Expand Up @@ -40,31 +39,14 @@ def __init__(
self.network = network
self.base_url_api = base_url_api
self.base_url_repo = base_url_repo
self.http_session = self._prepare_http_session()
self.http_session = prepare_http_session(10, 100)
self.request_timeout = request_timeout

if not self.is_chain_supported(network.value):
raise SourcifyClientConfigurationProblem(
f"Network {network.name} - {network.value} not supported"
)

def _prepare_http_session(self) -> requests.Session:
"""
Prepare http session with custom pooling. See:
https://urllib3.readthedocs.io/en/stable/advanced-usage.html
https://docs.python-requests.org/en/v1.2.3/api/#requests.adapters.HTTPAdapter
https://web3py.readthedocs.io/en/stable/providers.html#httpprovider
"""
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=100,
pool_block=False,
)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session

def _get_abi_from_metadata(self, metadata: Dict[str, Any]) -> List[Dict[str, Any]]:
return metadata["output"]["abi"]

Expand Down
31 changes: 2 additions & 29 deletions gnosis/eth/ethereum_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
)

import eth_abi
import requests
from eth_abi.exceptions import DecodingError
from eth_account import Account
from eth_account.signers.local import LocalAccount
Expand Down Expand Up @@ -60,6 +59,7 @@
)
from gnosis.util import cache, chunks

from ..util.http import prepare_http_session
from .constants import (
ERC20_721_TRANSFER_TOPIC,
GAS_CALL_DATA_BYTE,
Expand Down Expand Up @@ -1189,7 +1189,7 @@ def __init__(
:param use_caching_middleware: Use web3 simple cache middleware: https://web3py.readthedocs.io/en/stable/middleware.html#web3.middleware.construct_simple_cache_middleware
:param batch_request_max_size: Max size for JSON RPC Batch requests. Some providers have a limitation on 500
"""
self.http_session = self._prepare_http_session(retry_count)
self.http_session = prepare_http_session(1, 100, retry_count=retry_count)
self.ethereum_node_url: str = ethereum_node_url
self.timeout = provider_timeout
self.slow_timeout = slow_provider_timeout
Expand Down Expand Up @@ -1237,33 +1237,6 @@ def __init__(
def __str__(self):
return f"EthereumClient for url={self.ethereum_node_url}"

def _prepare_http_session(self, retry_count: int) -> requests.Session:
"""
Prepare http session with custom pooling. See:
https://urllib3.readthedocs.io/en/stable/advanced-usage.html
https://docs.python-requests.org/en/v1.2.3/api/#requests.adapters.HTTPAdapter
https://web3py.readthedocs.io/en/stable/providers.html#httpprovider
"""
session = requests.Session()
retry_conf = (
requests.adapters.Retry(
total=retry_count,
backoff_factor=0.3,
)
if retry_count
else 0
)

adapter = requests.adapters.HTTPAdapter(
pool_connections=1, # Doing all the connections to the same url
pool_maxsize=100, # Number of concurrent connections
max_retries=retry_conf, # Nodes are not very responsive some times
pool_block=False,
)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session

def raw_batch_request(
self, payload: List[Dict[str, Any]], batch_size: Optional[int] = None
) -> Iterable[Optional[Dict[str, Any]]]:
Expand Down
Empty file.
Loading
Loading