From 2f65971a2ed2fdf96c427d22670292b4a4a9acd5 Mon Sep 17 00:00:00 2001 From: Di Mei Date: Sat, 22 Feb 2025 14:22:00 -0500 Subject: [PATCH 1/2] support Edwards key --- cdp/cdp.py | 19 +++--- cdp/cdp_api_client.py | 95 +++++++++++++++----------- cdp/wallet.py | 41 +++++++++-- poetry.lock | 155 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 237 insertions(+), 73 deletions(-) diff --git a/cdp/cdp.py b/cdp/cdp.py index 84c2416..a20179d 100644 --- a/cdp/cdp.py +++ b/cdp/cdp.py @@ -35,10 +35,6 @@ class Cdp: def __new__(cls): """Create or return the singleton instance of the Cdp class. - This method overrides the default `__new__` behavior to implement the Singleton pattern. - It ensures that only one instance of the Cdp class exists throughout the application's lifecycle. - If an instance already exists, it returns the existing instance; otherwise, it creates a new one. - Returns: Cdp: The singleton instance of the Cdp class. @@ -68,7 +64,7 @@ def configure( debugging (bool): Whether debugging is enabled. Defaults to False. base_path (str): The base URL for the CDP API. Defaults to "https://api.cdp.coinbase.com/platform". max_network_retries (int): The maximum number of network retries. Defaults to 3. - source (Optional[str]): Specifies whether the sdk is being used directly or if it's an Agentkit extension. + source (Optional[str]): Specifies whether the SDK is being used directly or if it's an Agentkit extension. source_version (Optional[str]): The version of the source package. """ @@ -109,21 +105,24 @@ def configure_from_json( debugging (bool): Whether debugging is enabled. Defaults to False. base_path (str): The base URL for the CDP API. Defaults to "https://api.cdp.coinbase.com/platform". max_network_retries (int): The maximum number of network retries. Defaults to 3. - source (Optional[str]): Specifies whether the sdk is being used directly or if it's an Agentkit extension. + source (Optional[str]): Specifies whether the SDK is being used directly or if it's an Agentkit extension. source_version (Optional[str]): The version of the source package. Raises: - InvalidConfigurationError: If the JSON file is missing the 'api_key_name' or 'private_key'. + InvalidConfigurationError: If the JSON file is missing the API key identifier or the private key. """ with open(os.path.expanduser(file_path)) as file: data = json.load(file) - api_key_name = data.get("name") + # Accept either "name" or "id" for the API key identifier. + api_key_name = data.get("name") or data.get("id") private_key = data.get("privateKey") if not api_key_name: - raise InvalidConfigurationError("Invalid JSON format: Missing 'api_key_name'") + raise InvalidConfigurationError( + "Invalid JSON format: Missing API key identifier ('name' or 'id')" + ) if not private_key: - raise InvalidConfigurationError("Invalid JSON format: Missing 'private_key'") + raise InvalidConfigurationError("Invalid JSON format: Missing 'privateKey'") cls.configure( api_key_name, private_key, diff --git a/cdp/cdp_api_client.py b/cdp/cdp_api_client.py index 15a7d2f..20a8e21 100644 --- a/cdp/cdp_api_client.py +++ b/cdp/cdp_api_client.py @@ -1,10 +1,11 @@ +import base64 import random import time from urllib.parse import urlparse import jwt from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ec, ed25519 from urllib3.util import Retry from cdp import __version__ @@ -36,10 +37,16 @@ def __init__( Args: api_key (str): The API key for authentication. private_key (str): The private key for authentication. - host (str, optional): The base URL for the API. Defaults to "https://api.cdp.coinbase.com/platform". + For ECDSA keys, this should be a PEM-encoded string. + For Ed25519 keys, this should be a base64-encoded string representing + either the raw 32-byte seed or a 64-byte key (private+public), in which + case only the first 32 bytes are used. + host (str, optional): The base URL for the API. + Defaults to "https://api.cdp.coinbase.com/platform". debugging (bool): Whether debugging is enabled. - max_network_retries (int): The maximum number of network retries. Defaults to 3. - source (str): Specifies whether the sdk is being used directly or if it's an Agentkit extension. + max_network_retries (int): The maximum number of network retries. + Defaults to 3. + source (str): Specifies whether the SDK is being used directly or if it's an Agentkit extension. source_version (str): The version of the source package. """ @@ -73,7 +80,7 @@ def private_key(self) -> str: return self._private_key @property - def debugging(self) -> str: + def debugging(self) -> bool: """Whether debugging is enabled. Returns: @@ -96,18 +103,16 @@ def call_api( Args: method: Method to call. url: Path to method endpoint. - header_params: Header parameters to be - placed in the request header. + header_params: Header parameters to be placed in the request header. body: Request body. - post_params (dict): Request post form parameters, - for `application/x-www-form-urlencoded`, `multipart/form-data`. - _request_timeout: timeout setting for this request. + post_params (dict): Request post form parameters. + _request_timeout: Timeout setting for this request. Returns: RESTResponse """ - if self.debugging is True: + if self.debugging: print(f"CDP API REQUEST: {method} {url}") if header_params is None: @@ -132,7 +137,7 @@ def response_deserialize( ApiResponse[ApiResponseT] """ - if self.debugging is True: + if self.debugging: print(f"CDP API RESPONSE: Status: {response_data.status}, Data: {response_data.data}") try: @@ -141,23 +146,16 @@ def response_deserialize( raise ApiError.from_error(e) from None def _apply_headers(self, url: str, method: str, header_params: dict[str, str]) -> None: - """Apply authentication to the configuration. + """Apply authentication headers. Args: url (str): The URL to authenticate. method (str): The HTTP method to use. header_params (dict[str, str]): The header parameters. - Returns: - None - """ token = self._build_jwt(url, method) - - # Add the JWT token to the headers header_params["Authorization"] = f"Bearer {token}" - - # Add additional custom headers header_params["Content-Type"] = "application/json" header_params["Correlation-Context"] = self._get_correlation_data() @@ -169,20 +167,41 @@ def _build_jwt(self, url: str, method: str = "GET") -> str: method (str): The HTTP method to use. Returns: - str: The JWT for the given API endpoint URL. + str: The JWT. """ + private_key_obj = None + key_data = self.private_key.encode() + # First, try to load as a PEM-encoded key (typically for ECDSA keys). try: - private_key = serialization.load_pem_private_key( - self.private_key.encode(), password=None - ) - if not isinstance(private_key, ec.EllipticCurvePrivateKey): - raise InvalidAPIKeyFormatError("Invalid key type") - except Exception as e: - raise InvalidAPIKeyFormatError("Could not parse the private key") from e + private_key_obj = serialization.load_pem_private_key(key_data, password=None) + except Exception: + # If PEM loading fails, assume the key is provided as base64-encoded raw bytes. + try: + decoded_key = base64.b64decode(self.private_key) + # For Ed25519 keys, the raw private key should be 32 bytes. + # Sometimes a 64-byte key is provided (concatenated private and public parts). + if len(decoded_key) == 32: + private_key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key) + elif len(decoded_key) == 64: + private_key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key[:32]) + else: + raise InvalidAPIKeyFormatError( + "Ed25519 private key must be 32 or 64 bytes after base64 decoding" + ) + except Exception as e2: + raise InvalidAPIKeyFormatError("Could not parse the private key") from e2 + + # Determine signing algorithm based on the key type. + if isinstance(private_key_obj, ec.EllipticCurvePrivateKey): + alg = "ES256" + elif isinstance(private_key_obj, ed25519.Ed25519PrivateKey): + alg = "EdDSA" + else: + raise InvalidAPIKeyFormatError("Unsupported key type") header = { - "alg": "ES256", + "alg": alg, "kid": self.api_key, "typ": "JWT", "nonce": self._nonce(), @@ -195,18 +214,18 @@ def _build_jwt(self, url: str, method: str = "GET") -> str: "iss": "cdp", "aud": ["cdp_service"], "nbf": int(time.time()), - "exp": int(time.time()) + 60, # +1 minute + "exp": int(time.time()) + 60, # Token valid for 1 minute "uris": [uri], } try: - return jwt.encode(claims, private_key, algorithm="ES256", headers=header) + return jwt.encode(claims, private_key_obj, algorithm=alg, headers=header) except Exception as e: print(f"Error during JWT signing: {e!s}") raise InvalidAPIKeyFormatError("Could not sign the JWT") from e def _nonce(self) -> str: - """Generate a random nonce for the JWT. + """Generate a random nonce. Returns: str: The nonce. @@ -215,7 +234,7 @@ def _nonce(self) -> str: return "".join(random.choices("0123456789", k=16)) def _get_correlation_data(self) -> str: - """Return encoded correlation data including the SDK version, language, and source. + """Return correlation data including SDK version, language, and source. Returns: str: The correlation data. @@ -230,7 +249,7 @@ def _get_correlation_data(self) -> str: return ",".join(f"{key}={value}" for key, value in data.items()) def _get_retry_strategy(self, max_network_retries: int) -> Retry: - """Return the retry strategy for the CDP API Client. + """Return the retry strategy. Args: max_network_retries (int): The maximum number of network retries. @@ -240,8 +259,8 @@ def _get_retry_strategy(self, max_network_retries: int) -> Retry: """ return Retry( - total=max_network_retries, # Number of total retries - status_forcelist=[500, 502, 503, 504], # Retry on HTTP status code 500 - allowed_methods=["GET"], # Retry only on GET requests - backoff_factor=1, # Exponential backoff factor + total=max_network_retries, + status_forcelist=[500, 502, 503, 504], + allowed_methods=["GET"], + backoff_factor=1, ) diff --git a/cdp/wallet.py b/cdp/wallet.py index 5707197..cf781a3 100644 --- a/cdp/wallet.py +++ b/cdp/wallet.py @@ -688,17 +688,48 @@ def load_seed_from_file(self, file_path: str) -> None: def _encryption_key(self) -> bytes: """Generate an encryption key based on the private key. + For ECDSA keys (PEM encoded), an ECDH exchange is performed. + For Ed25519 keys (base64 encoded), the raw private key bytes are hashed. + Returns: bytes: The generated encryption key. """ - private_key = serialization.load_pem_private_key(Cdp.private_key.encode(), password=None) - - public_key = private_key.public_key() + import base64 - shared_secret = private_key.exchange(ec.ECDH(), public_key) + from cryptography.hazmat.primitives.asymmetric import ed25519 - return hashlib.sha256(shared_secret).digest() + # Attempt to load as a PEM-encoded key (for ECDSA) + try: + key_obj = serialization.load_pem_private_key(Cdp.private_key.encode(), password=None) + except Exception: + # If PEM loading fails, assume the key is provided as a base64-encoded Ed25519 key. + try: + decoded = base64.b64decode(Cdp.private_key) + if len(decoded) == 32: + key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded) + elif len(decoded) == 64: + key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded[:32]) + else: + raise ValueError("Invalid Ed25519 key length") + except Exception as e2: + raise ValueError("Could not parse the private key") from e2 + + # For ECDSA keys, perform an ECDH exchange with its own public key. + if isinstance(key_obj, ec.EllipticCurvePrivateKey): + public_key = key_obj.public_key() + shared_secret = key_obj.exchange(ec.ECDH(), public_key) + return hashlib.sha256(shared_secret).digest() + # For Ed25519 keys, derive the encryption key by hashing the raw private key bytes. + elif isinstance(key_obj, ed25519.Ed25519PrivateKey): + raw_bytes = key_obj.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), + ) + return hashlib.sha256(raw_bytes).digest() + else: + raise ValueError("Unsupported key type for encryption key derivation") def _existing_seeds(self, file_path: str) -> dict[str, Any]: """Load existing seeds from a file. diff --git a/poetry.lock b/poetry.lock index 8f9a8c4..2d89780 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -6,6 +6,7 @@ version = "2.4.4" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, @@ -17,6 +18,7 @@ version = "3.11.10" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "aiohttp-3.11.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cbad88a61fa743c5d283ad501b01c153820734118b65aee2bd7dbb735475ce0d"}, {file = "aiohttp-3.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80886dac673ceaef499de2f393fc80bb4481a129e6cb29e624a12e3296cc088f"}, @@ -107,7 +109,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" @@ -115,6 +117,7 @@ version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, @@ -129,6 +132,7 @@ version = "1.0.0" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, @@ -140,6 +144,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -151,6 +156,7 @@ version = "4.7.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, @@ -164,7 +170,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -173,6 +179,7 @@ version = "1.5.1" description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, @@ -184,6 +191,8 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.10\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -195,18 +204,19 @@ version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\""] [[package]] name = "babel" @@ -214,6 +224,7 @@ version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, @@ -228,6 +239,7 @@ version = "2.9.3" description = "Generation of mnemonics, seeds, private/public keys and addresses for different types of cryptocurrencies" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "bip_utils-2.9.3-py3-none-any.whl", hash = "sha256:ee26b8417a576c7f89b847da37316db01a5cece1994c1609d37fbeefb91ad45e"}, {file = "bip_utils-2.9.3.tar.gz", hash = "sha256:72a8c95484b57e92311b0b2a3d5195b0ce4395c19a0b157d4a289e8b1300f48a"}, @@ -262,6 +274,7 @@ version = "3.0.0" description = "efficient arrays of booleans -- C extension" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "bitarray-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ddbf71a97ad1d6252e6e93d2d703b624d0a5b77c153b12f9ea87d83e1250e0c"}, {file = "bitarray-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0e7f24a0b01e6e6a0191c50b06ca8edfdec1988d9d2b264d669d2487f4f4680"}, @@ -408,6 +421,7 @@ version = "24.1.2" description = "Composable complex class support for attrs and dataclasses." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cattrs-24.1.2-py3-none-any.whl", hash = "sha256:67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0"}, {file = "cattrs-24.1.2.tar.gz", hash = "sha256:8028cfe1ff5382df59dd36474a86e02d817b06eaf8af84555441bac915d2ef85"}, @@ -422,8 +436,8 @@ typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_ver bson = ["pymongo (>=4.4.0)"] cbor2 = ["cbor2 (>=5.4.6)"] msgpack = ["msgpack (>=1.0.5)"] -msgspec = ["msgspec (>=0.18.5)"] -orjson = ["orjson (>=3.9.2)"] +msgspec = ["msgspec (>=0.18.5) ; implementation_name == \"cpython\""] +orjson = ["orjson (>=3.9.2) ; implementation_name == \"cpython\""] pyyaml = ["pyyaml (>=6.0)"] tomlkit = ["tomlkit (>=0.11.8)"] ujson = ["ujson (>=5.7.0)"] @@ -434,6 +448,7 @@ version = "5.6.5" description = "CBOR (de)serializer with extensive tag support" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2"}, {file = "cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33"}, @@ -483,7 +498,7 @@ files = [ [package.extras] benchmarks = ["pytest-benchmark (==4.0.0)"] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions ; python_version < \"3.12\""] test = ["coverage (>=7)", "hypothesis", "pytest"] [[package]] @@ -492,6 +507,7 @@ version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, @@ -503,6 +519,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -582,6 +599,7 @@ version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["main", "dev"] files = [ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, @@ -696,6 +714,7 @@ version = "2.0.1" description = "Python bindings for C-KZG-4844" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "ckzg-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b7f9ba6d215f8981c5545f952aac84875bd564a63da02fb22a3d1321662ecdc0"}, {file = "ckzg-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8fdec3ff96399acba9baeef9e1b0b5258c08f73245780e6c69f7b73def5e8d0a"}, @@ -799,6 +818,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -813,6 +833,7 @@ version = "20.0.0" description = "Cross-platform Python CFFI bindings for libsecp256k1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "coincurve-20.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d559b22828638390118cae9372a1bb6f6594f5584c311deb1de6a83163a0919b"}, {file = "coincurve-20.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33d7f6ebd90fcc550f819f7f2cce2af525c342aac07f0ccda46ad8956ad9d99b"}, @@ -879,6 +900,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -890,6 +912,7 @@ version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, @@ -959,7 +982,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "crcmod" @@ -967,6 +990,7 @@ version = "1.7" description = "CRC Generator" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e"}, ] @@ -977,6 +1001,7 @@ version = "44.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main"] files = [ {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, @@ -1011,10 +1036,10 @@ files = [ cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] +pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] @@ -1026,6 +1051,8 @@ version = "1.0.0" description = "Cython implementation of Toolz: High performance functional utilities" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "implementation_name == \"cpython\"" files = [ {file = "cytoolz-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ecf5a887acb8f079ab1b81612b1c889bcbe6611aa7804fd2df46ed310aa5a345"}, {file = "cytoolz-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0ef30c1e091d4d59d14d8108a16d50bd227be5d52a47da891da5019ac2f8e4"}, @@ -1127,6 +1154,7 @@ version = "0.15" description = "On the fly conversion of Python docstrings to markdown" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "docstring-to-markdown-0.15.tar.gz", hash = "sha256:e146114d9c50c181b1d25505054a8d0f7a476837f0da2c19f07e06eaed52b73d"}, {file = "docstring_to_markdown-0.15-py3-none-any.whl", hash = "sha256:27afb3faedba81e34c33521c32bbd258d7fbb79eedf7d29bc4e81080e854aec0"}, @@ -1138,6 +1166,7 @@ version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, @@ -1149,6 +1178,7 @@ version = "0.19.0" description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" +groups = ["main"] files = [ {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, @@ -1167,6 +1197,7 @@ version = "1.4.1" description = "Ed25519 public-key signatures (BLAKE2b fork)" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "ed25519-blake2b-1.4.1.tar.gz", hash = "sha256:731e9f93cd1ac1a64649575f3519a99ffe0bb1e4cf7bf5f5f0be513a39df7363"}, ] @@ -1177,6 +1208,7 @@ version = "5.1.0" description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" optional = false python-versions = "<4,>=3.8" +groups = ["main"] files = [ {file = "eth_abi-5.1.0-py3-none-any.whl", hash = "sha256:84cac2626a7db8b7d9ebe62b0fdca676ab1014cc7f777189e3c0cd721a4c16d8"}, {file = "eth_abi-5.1.0.tar.gz", hash = "sha256:33ddd756206e90f7ddff1330cc8cac4aa411a824fe779314a0a52abea2c8fc14"}, @@ -1199,6 +1231,7 @@ version = "0.13.4" description = "eth-account: Sign Ethereum transactions and messages with local private keys" optional = false python-versions = "<4,>=3.8" +groups = ["main"] files = [ {file = "eth_account-0.13.4-py3-none-any.whl", hash = "sha256:a4c109e9bad3a278243fcc028b755fb72b43e25b1e6256b3f309a44f5f7d87c3"}, {file = "eth_account-0.13.4.tar.gz", hash = "sha256:2e1f2de240bef3d9f3d8013656135d2a79b6be6d4e7885bce9cace4334a4a376"}, @@ -1227,6 +1260,7 @@ version = "0.7.0" description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" optional = false python-versions = ">=3.8, <4" +groups = ["main"] files = [ {file = "eth-hash-0.7.0.tar.gz", hash = "sha256:bacdc705bfd85dadd055ecd35fd1b4f846b671add101427e089a4ca2e8db310a"}, {file = "eth_hash-0.7.0-py3-none-any.whl", hash = "sha256:b8d5a230a2b251f4a291e3164a23a14057c4a6de4b0aa4a16fa4dc9161b57e2f"}, @@ -1239,7 +1273,7 @@ pycryptodome = {version = ">=3.6.6,<4", optional = true, markers = "extra == \"p dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] docs = ["sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] pycryptodome = ["pycryptodome (>=3.6.6,<4)"] -pysha3 = ["pysha3 (>=1.0.0,<2.0.0)", "safe-pysha3 (>=1.0.0)"] +pysha3 = ["pysha3 (>=1.0.0,<2.0.0) ; python_version < \"3.9\"", "safe-pysha3 (>=1.0.0) ; python_version >= \"3.9\""] test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] @@ -1248,6 +1282,7 @@ version = "0.8.1" description = "eth-keyfile: A library for handling the encrypted keyfiles used to store ethereum private keys" optional = false python-versions = "<4,>=3.8" +groups = ["main"] files = [ {file = "eth_keyfile-0.8.1-py3-none-any.whl", hash = "sha256:65387378b82fe7e86d7cb9f8d98e6d639142661b2f6f490629da09fddbef6d64"}, {file = "eth_keyfile-0.8.1.tar.gz", hash = "sha256:9708bc31f386b52cca0969238ff35b1ac72bd7a7186f2a84b86110d3c973bec1"}, @@ -1269,6 +1304,7 @@ version = "0.6.0" description = "eth-keys: Common API for Ethereum key operations" optional = false python-versions = "<4,>=3.8" +groups = ["main"] files = [ {file = "eth_keys-0.6.0-py3-none-any.whl", hash = "sha256:b396fdfe048a5bba3ef3990739aec64901eb99901c03921caa774be668b1db6e"}, {file = "eth_keys-0.6.0.tar.gz", hash = "sha256:ba33230f851d02c894e83989185b21d76152c49b37e35b61b1d8a6d9f1d20430"}, @@ -1290,6 +1326,7 @@ version = "2.1.0" description = "eth-rlp: RLP definitions for common Ethereum objects in Python" optional = false python-versions = ">=3.8, <4" +groups = ["main"] files = [ {file = "eth-rlp-2.1.0.tar.gz", hash = "sha256:d5b408a8cd20ed496e8e66d0559560d29bc21cee482f893936a1f05d0dddc4a0"}, {file = "eth_rlp-2.1.0-py3-none-any.whl", hash = "sha256:6f476eb7e37d81feaba5d98aed887e467be92648778c44b19fe594aea209cde1"}, @@ -1312,6 +1349,7 @@ version = "5.0.1" description = "eth-typing: Common type annotations for ethereum python packages" optional = false python-versions = "<4,>=3.8" +groups = ["main"] files = [ {file = "eth_typing-5.0.1-py3-none-any.whl", hash = "sha256:f30d1af16aac598f216748a952eeb64fbcb6e73efa691d2de31148138afe96de"}, {file = "eth_typing-5.0.1.tar.gz", hash = "sha256:83debf88c9df286db43bb7374974681ebcc9f048fac81be2548dbc549a3203c0"}, @@ -1331,6 +1369,7 @@ version = "5.1.0" description = "eth-utils: Common utility functions for python code that interacts with Ethereum" optional = false python-versions = "<4,>=3.8" +groups = ["main"] files = [ {file = "eth_utils-5.1.0-py3-none-any.whl", hash = "sha256:a99f1f01b51206620904c5af47fac65abc143aebd0a76bdec860381c5a3230f8"}, {file = "eth_utils-5.1.0.tar.gz", hash = "sha256:84c6314b9cf1fcd526107464bbf487e3f87097a2e753360d5ed319f7d42e3f20"}, @@ -1353,6 +1392,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -1367,6 +1408,7 @@ version = "1.5.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, @@ -1468,6 +1510,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -1479,6 +1522,7 @@ version = "1.2.1" description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" optional = false python-versions = "<4,>=3.8" +groups = ["main"] files = [ {file = "hexbytes-1.2.1-py3-none-any.whl", hash = "sha256:e64890b203a31f4a23ef11470ecfcca565beaee9198df623047df322b757471a"}, {file = "hexbytes-1.2.1.tar.gz", hash = "sha256:515f00dddf31053db4d0d7636dd16061c1d896c3109b8e751005db4ca46bcca7"}, @@ -1495,6 +1539,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1509,6 +1554,7 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -1520,6 +1566,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -1531,6 +1578,7 @@ version = "0.19.2" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, @@ -1550,6 +1598,7 @@ version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, @@ -1567,6 +1616,7 @@ version = "2023.0.1" description = "Python implementation of the Language Server Protocol." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2"}, {file = "lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d"}, @@ -1582,6 +1632,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -1606,6 +1657,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1676,6 +1728,7 @@ version = "0.4.2" description = "Collection of plugins for markdown-it-py" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, @@ -1695,6 +1748,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -1706,6 +1760,7 @@ version = "6.1.0" description = "multidict implementation" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, @@ -1810,6 +1865,7 @@ version = "1.13.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, @@ -1863,6 +1919,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -1874,6 +1931,7 @@ version = "4.0.0" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d"}, {file = "myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531"}, @@ -1900,6 +1958,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1911,6 +1970,7 @@ version = "0.10.0" description = "(Soon to be) the fastest pure-Python PEG parser I could muster" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f"}, {file = "parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c"}, @@ -1925,6 +1985,7 @@ version = "0.8.4" description = "A Python Parser" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, @@ -1940,6 +2001,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1955,6 +2017,7 @@ version = "0.9.1" description = "A collection of helpful Python tools!" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "pockets-0.9.1-py2.py3-none-any.whl", hash = "sha256:68597934193c08a08eb2bf6a1d85593f627c22f9b065cc727a4f03f669d96d86"}, {file = "pockets-0.9.1.tar.gz", hash = "sha256:9320f1a3c6f7a9133fe3b571f283bcf3353cd70249025ae8d618e40e9f7e92b3"}, @@ -1969,6 +2032,7 @@ version = "0.2.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, @@ -2060,6 +2124,7 @@ version = "0.2.1" description = "Python bindings for schnorrkel RUST crate" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "py_sr25519_bindings-0.2.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10489c399768dc4ac91c90a6c8da60aeb77a48b21a81944244d41b0d4c4be2f"}, {file = "py_sr25519_bindings-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8358a7b3048765008a79733447dfdcafdce3f66859c98634055fee6868252e12"}, @@ -2174,6 +2239,7 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -2185,6 +2251,7 @@ version = "3.21.0" description = "Cryptographic library for Python" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] files = [ {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, @@ -2226,6 +2293,7 @@ version = "2.10.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, @@ -2238,7 +2306,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -2246,6 +2314,7 @@ version = "2.27.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, @@ -2358,6 +2427,7 @@ version = "1.3.1" description = "A pythonic generic language server (pronounced like 'pie glass')" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pygls-1.3.1-py3-none-any.whl", hash = "sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e"}, {file = "pygls-1.3.1.tar.gz", hash = "sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018"}, @@ -2376,6 +2446,7 @@ version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, @@ -2390,6 +2461,7 @@ version = "2.10.1" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, @@ -2407,6 +2479,7 @@ version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, @@ -2433,6 +2506,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -2455,6 +2529,7 @@ version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, @@ -2473,6 +2548,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2487,6 +2563,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -2501,6 +2578,7 @@ version = "1.1.2" description = "JSON RPC 2.0 server library" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "python-lsp-jsonrpc-1.1.2.tar.gz", hash = "sha256:4688e453eef55cd952bff762c705cedefa12055c0aec17a06f595bcc002cc912"}, {file = "python_lsp_jsonrpc-1.1.2-py3-none-any.whl", hash = "sha256:7339c2e9630ae98903fdaea1ace8c47fba0484983794d6aafd0bd8989be2b03c"}, @@ -2518,6 +2596,7 @@ version = "1.12.0" description = "Python Language Server for the Language Server Protocol" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "python_lsp_server-1.12.0-py3-none-any.whl", hash = "sha256:2e912c661881d85f67f2076e4e66268b695b62bf127e07e81f58b187d4bb6eda"}, {file = "python_lsp_server-1.12.0.tar.gz", hash = "sha256:b6a336f128da03bd9bac1e61c3acca6e84242b8b31055a1ccf49d83df9dc053b"}, @@ -2550,6 +2629,7 @@ version = "16.0.0" description = "Unicode normalization forms (NFC, NFKC, NFD, NFKD). A library independent of the Python core Unicode database." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "pyunormalize-16.0.0-py3-none-any.whl", hash = "sha256:c647d95e5d1e2ea9a2f448d1d95d8518348df24eab5c3fd32d2b5c3300a49152"}, {file = "pyunormalize-16.0.0.tar.gz", hash = "sha256:2e1dfbb4a118154ae26f70710426a52a364b926c9191f764601f5a8cb12761f7"}, @@ -2561,6 +2641,8 @@ version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, @@ -2588,6 +2670,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -2650,6 +2733,7 @@ version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, @@ -2753,6 +2837,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -2774,6 +2859,7 @@ version = "4.0.1" description = "rlp: A package for Recursive Length Prefix encoding and decoding" optional = false python-versions = "<4,>=3.8" +groups = ["main"] files = [ {file = "rlp-4.0.1-py3-none-any.whl", hash = "sha256:ff6846c3c27b97ee0492373aa074a7c3046aadd973320f4fffa7ac45564b0258"}, {file = "rlp-4.0.1.tar.gz", hash = "sha256:bcefb11013dfadf8902642337923bd0c786dc8a27cb4c21da6e154e52869ecb1"}, @@ -2794,6 +2880,7 @@ version = "0.7.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478"}, {file = "ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63"}, @@ -2821,6 +2908,7 @@ version = "0.0.58" description = "A Language Server Protocol implementation for Ruff." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff_lsp-0.0.58-py3-none-any.whl", hash = "sha256:d59f420ef56a58497f646fef0f5b87d6518e3d63e02044e36677cbdc1f9b7717"}, {file = "ruff_lsp-0.0.58.tar.gz", hash = "sha256:378db39955b32260473602b531dc6333d6686d1d8956673ef1c5203e08132032"}, @@ -2842,6 +2930,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -2853,6 +2942,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2864,6 +2954,7 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -2875,6 +2966,7 @@ version = "8.1.3" description = "Python documentation generator" optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, @@ -2910,6 +3002,7 @@ version = "2024.10.3" description = "Rebuild Sphinx documentation on changes, with hot reloading in the browser." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa"}, {file = "sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1"}, @@ -2932,6 +3025,7 @@ version = "2.5.0" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "sphinx_autodoc_typehints-2.5.0-py3-none-any.whl", hash = "sha256:53def4753239683835b19bfa8b68c021388bd48a096efcb02cdab508ece27363"}, {file = "sphinx_autodoc_typehints-2.5.0.tar.gz", hash = "sha256:259e1026b218d563d72743f417fcc25906a9614897fe37f91bd8d7d58f748c3b"}, @@ -2951,6 +3045,7 @@ version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -2967,6 +3062,7 @@ version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -2983,6 +3079,7 @@ version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -2999,6 +3096,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -3013,6 +3111,7 @@ version = "0.7" description = "Sphinx \"napoleon\" extension." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "sphinxcontrib-napoleon-0.7.tar.gz", hash = "sha256:407382beed396e9f2d7f3043fad6afda95719204a1e1a231ac865f40abcbfcf8"}, {file = "sphinxcontrib_napoleon-0.7-py2.py3-none-any.whl", hash = "sha256:711e41a3974bdf110a484aec4c1a556799eb0b3f3b897521a018ad7e2db13fef"}, @@ -3028,6 +3127,7 @@ version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -3044,6 +3144,7 @@ version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -3060,6 +3161,7 @@ version = "0.41.3" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, @@ -3077,6 +3179,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -3118,6 +3222,8 @@ version = "1.0.0" description = "List processing tools and functional utilities" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "implementation_name == \"cpython\" or implementation_name == \"pypy\"" files = [ {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, @@ -3129,6 +3235,7 @@ version = "2.32.0.20241016" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, @@ -3143,6 +3250,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -3154,6 +3262,7 @@ version = "5.10.0" description = "Ultra fast JSON encoder and decoder for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, @@ -3241,13 +3350,14 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3258,6 +3368,7 @@ version = "0.32.1" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"}, {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"}, @@ -3269,7 +3380,7 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "watchfiles" @@ -3277,6 +3388,7 @@ version = "1.0.0" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "watchfiles-1.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1d19df28f99d6a81730658fbeb3ade8565ff687f95acb59665f11502b441be5f"}, {file = "watchfiles-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28babb38cf2da8e170b706c4b84aa7e4528a6fa4f3ee55d7a0866456a1662041"}, @@ -3360,6 +3472,7 @@ version = "7.6.0" description = "web3: A Python library for interacting with Ethereum" optional = false python-versions = "<4,>=3.8" +groups = ["main"] files = [ {file = "web3-7.6.0-py3-none-any.whl", hash = "sha256:670dac222b2ec5ce72f4572d8e5d91afe79fcac03af9dabfc69da4fe9f6621df"}, {file = "web3-7.6.0.tar.gz", hash = "sha256:25df8acdcb78eb872c3299408b79e8b4fd091602de5e3d29cbd8459e8f75ff23"}, @@ -3393,6 +3506,7 @@ version = "13.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee"}, {file = "websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7"}, @@ -3488,6 +3602,7 @@ version = "1.18.3" description = "Yet another URL library" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, @@ -3579,6 +3694,6 @@ multidict = ">=4.0" propcache = ">=0.2.0" [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" content-hash = "93bda329adce112ff0b1bcb35f83acae6ccd44a29cde239a39545973ba807ecc" From 7387a348e8a8d1599c8b8940729736151604fd3d Mon Sep 17 00:00:00 2001 From: Di Mei Date: Sat, 22 Feb 2025 16:17:01 -0500 Subject: [PATCH 2/2] support Edwards key --- cdp/cdp.py | 10 +- cdp/cdp_api_client.py | 26 +-- cdp/wallet.py | 459 ++++-------------------------------------- 3 files changed, 49 insertions(+), 446 deletions(-) diff --git a/cdp/cdp.py b/cdp/cdp.py index a20179d..bf6e561 100644 --- a/cdp/cdp.py +++ b/cdp/cdp.py @@ -19,7 +19,6 @@ class Cdp: base_path (str): The base URL for the Platform API. max_network_retries (int): The maximum number of network retries. api_clients (Optional[ApiClients]): The Platform API clients instance. - """ _instance = None @@ -37,7 +36,6 @@ def __new__(cls): Returns: Cdp: The singleton instance of the Cdp class. - """ if cls._instance is None: cls._instance = super().__new__(cls) @@ -64,9 +62,8 @@ def configure( debugging (bool): Whether debugging is enabled. Defaults to False. base_path (str): The base URL for the CDP API. Defaults to "https://api.cdp.coinbase.com/platform". max_network_retries (int): The maximum number of network retries. Defaults to 3. - source (Optional[str]): Specifies whether the SDK is being used directly or if it's an Agentkit extension. + source (Optional[str]): Specifies whether the sdk is being used directly or if it's an Agentkit extension. source_version (Optional[str]): The version of the source package. - """ cls.api_key_name = api_key_name cls.private_key = private_key @@ -99,18 +96,19 @@ def configure_from_json( ) -> None: """Configure the CDP SDK from a JSON file. + This updated method now accepts either "name" or "id" as the API key identifier. + Args: file_path (str): The path to the JSON file. Defaults to "~/Downloads/cdp_api_key.json". use_server_signer (bool): Whether to use the server signer. Defaults to False. debugging (bool): Whether debugging is enabled. Defaults to False. base_path (str): The base URL for the CDP API. Defaults to "https://api.cdp.coinbase.com/platform". max_network_retries (int): The maximum number of network retries. Defaults to 3. - source (Optional[str]): Specifies whether the SDK is being used directly or if it's an Agentkit extension. + source (Optional[str]): Specifies whether the sdk is being used directly or if it's an Agentkit extension. source_version (Optional[str]): The version of the source package. Raises: InvalidConfigurationError: If the JSON file is missing the API key identifier or the private key. - """ with open(os.path.expanduser(file_path)) as file: data = json.load(file) diff --git a/cdp/cdp_api_client.py b/cdp/cdp_api_client.py index 20a8e21..b2c5bc6 100644 --- a/cdp/cdp_api_client.py +++ b/cdp/cdp_api_client.py @@ -41,14 +41,11 @@ def __init__( For Ed25519 keys, this should be a base64-encoded string representing either the raw 32-byte seed or a 64-byte key (private+public), in which case only the first 32 bytes are used. - host (str, optional): The base URL for the API. - Defaults to "https://api.cdp.coinbase.com/platform". + host (str, optional): The base URL for the API. Defaults to "https://api.cdp.coinbase.com/platform". debugging (bool): Whether debugging is enabled. - max_network_retries (int): The maximum number of network retries. - Defaults to 3. + max_network_retries (int): The maximum number of network retries. Defaults to 3. source (str): Specifies whether the SDK is being used directly or if it's an Agentkit extension. source_version (str): The version of the source package. - """ retry_strategy = self._get_retry_strategy(max_network_retries) configuration = Configuration(host=host, retries=retry_strategy) @@ -65,7 +62,6 @@ def api_key(self) -> str: Returns: str: The API key. - """ return self._api_key @@ -75,7 +71,6 @@ def private_key(self) -> str: Returns: str: The private key. - """ return self._private_key @@ -85,7 +80,6 @@ def debugging(self) -> bool: Returns: bool: Whether debugging is enabled. - """ return self._debugging @@ -110,7 +104,6 @@ def call_api( Returns: RESTResponse - """ if self.debugging: print(f"CDP API REQUEST: {method} {url}") @@ -135,7 +128,6 @@ def response_deserialize( Returns: ApiResponse[ApiResponseT] - """ if self.debugging: print(f"CDP API RESPONSE: Status: {response_data.status}, Data: {response_data.data}") @@ -152,7 +144,6 @@ def _apply_headers(self, url: str, method: str, header_params: dict[str, str]) - url (str): The URL to authenticate. method (str): The HTTP method to use. header_params (dict[str, str]): The header parameters. - """ token = self._build_jwt(url, method) header_params["Authorization"] = f"Bearer {token}" @@ -167,20 +158,18 @@ def _build_jwt(self, url: str, method: str = "GET") -> str: method (str): The HTTP method to use. Returns: - str: The JWT. - + str: The JWT for the given API endpoint URL. """ private_key_obj = None key_data = self.private_key.encode() - # First, try to load as a PEM-encoded key (typically for ECDSA keys). + # Business change: Support both ECDSA and Ed25519 keys. try: + # Try loading as a PEM-encoded key (typically for ECDSA keys). private_key_obj = serialization.load_pem_private_key(key_data, password=None) except Exception: - # If PEM loading fails, assume the key is provided as base64-encoded raw bytes. + # If PEM loading fails, assume the key is provided as base64-encoded raw bytes (Ed25519). try: decoded_key = base64.b64decode(self.private_key) - # For Ed25519 keys, the raw private key should be 32 bytes. - # Sometimes a 64-byte key is provided (concatenated private and public parts). if len(decoded_key) == 32: private_key_obj = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key) elif len(decoded_key) == 64: @@ -229,7 +218,6 @@ def _nonce(self) -> str: Returns: str: The nonce. - """ return "".join(random.choices("0123456789", k=16)) @@ -238,7 +226,6 @@ def _get_correlation_data(self) -> str: Returns: str: The correlation data. - """ data = { "sdk_version": __version__, @@ -256,7 +243,6 @@ def _get_retry_strategy(self, max_network_retries: int) -> Retry: Returns: Retry: The retry strategy. - """ return Retry( total=max_network_retries, diff --git a/cdp/wallet.py b/cdp/wallet.py index cf781a3..e36e20c 100644 --- a/cdp/wallet.py +++ b/cdp/wallet.py @@ -12,7 +12,8 @@ from bip_utils import Bip32Slip10Secp256k1, Bip39MnemonicValidator, Bip39SeedGenerator from Crypto.Cipher import AES from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ec # unchanged: ed25519 import added below +from cryptography.hazmat.primitives.asymmetric import ed25519 from eth_account import Account from cdp.address import Address @@ -53,7 +54,6 @@ def __init__(self, model: WalletModel, seed: str | None = None) -> None: Args: model (WalletModel): The WalletModel object representing the wallet. seed (Optional[str]): The seed for the wallet. Defaults to None. - """ self._model = model self._addresses: list[WalletAddress] | None = None @@ -66,7 +66,6 @@ def id(self) -> str: Returns: str: The ID of the wallet. - """ return self._model.id @@ -76,7 +75,6 @@ def network_id(self) -> str: Returns: str: The network ID of the wallet. - """ return self._model.network_id @@ -86,7 +84,6 @@ def server_signer_status(self) -> str: Returns: str: The server signer status of the wallet. - """ return self._model.server_signer_status @@ -96,7 +93,6 @@ def addresses(self) -> list[WalletAddress]: Returns: List[WalletAddress]: The addresses of the wallet. - """ if self._addresses is None: self._set_addresses() @@ -109,7 +105,6 @@ def can_sign(self) -> bool: Returns: bool: True if the wallet can sign, False otherwise. - """ return self._master is not None @@ -129,10 +124,6 @@ def create( Returns: Wallet: The created wallet object. - - Raises: - Exception: If there's an error creating the wallet. - """ return cls.create_with_seed( seed=None, @@ -159,10 +150,6 @@ def create_with_seed( Returns: Wallet: The created wallet object. - - Raises: - Exception: If there's an error creating the wallet. - """ create_wallet_request = CreateWalletRequest( wallet=CreateWalletRequestWallet( @@ -189,10 +176,6 @@ def _wait_for_signer(self, interval_seconds: float, timeout_seconds: float) -> " Returns: Wallet: The current wallet instance. - - Raises: - TimeoutError: If the wallet creation times out. - """ start_time = time.time() @@ -207,12 +190,7 @@ def _wait_for_signer(self, interval_seconds: float, timeout_seconds: float) -> " return self def reload(self) -> None: - """Reload the wallet model from the API. - - Returns: - None - - """ + """Reload the wallet model from the API.""" model = Cdp.api_clients.wallets.get_wallet(self.id) self._model = model return @@ -226,10 +204,6 @@ def fetch(cls, wallet_id: str) -> "Wallet": Returns: Wallet: The retrieved wallet object. - - Raises: - Exception: If there's an error retrieving the wallet. - """ model = Cdp.api_clients.wallets.get_wallet(wallet_id) @@ -241,10 +215,6 @@ def list(cls) -> Iterator["Wallet"]: Returns: Iterator[Wallet]: An iterator of wallet objects. - - Raises: - Exception: If there's an error listing the wallets. - """ while True: page = None @@ -263,23 +233,7 @@ def list(cls) -> Iterator["Wallet"]: def import_wallet( cls, data: WalletData | MnemonicSeedPhrase, network_id: str = "base-sepolia" ) -> "Wallet": - """Import a wallet from previously exported wallet data or a mnemonic seed phrase. - - Args: - data (Union[WalletData, MnemonicSeedPhrase]): Either: - - WalletData: The wallet data to import, containing wallet_id and seed - - MnemonicSeedPhrase: A valid BIP-39 mnemonic phrase object for importing external wallets - network_id (str): The network ID of the wallet. Defaults to "base-sepolia". - - Returns: - Wallet: The imported wallet. - - Raises: - ValueError: If data is not a WalletData or MnemonicSeedPhrase instance. - ValueError: If the mnemonic phrase is invalid. - Exception: If there's an error getting the wallet. - - """ + """Import a wallet from previously exported wallet data or a mnemonic seed phrase.""" if isinstance(data, MnemonicSeedPhrase): # Validate mnemonic phrase if not data.mnemonic_phrase: @@ -308,31 +262,11 @@ def import_wallet( @classmethod def import_data(cls, data: WalletData) -> "Wallet": - """Import a wallet from previously exported wallet data. - - Args: - data (WalletData): The wallet data to import. - - Returns: - Wallet: The imported wallet. - - Raises: - ValueError: If data is not a WalletData instance. - Exception: If there's an error getting the wallet. - - """ + """Import a wallet from previously exported wallet data.""" return cls.import_wallet(data) def create_address(self) -> "WalletAddress": - """Create a new address for the wallet. - - Returns: - WalletAddress: The created address object. - - Raises: - Exception: If there's an error creating the address. - - """ + """Create a new address for the wallet.""" if self._addresses is None: self._set_addresses() @@ -362,18 +296,7 @@ def create_address(self) -> "WalletAddress": return wallet_address def create_webhook(self, notification_uri: str) -> "Webhook": - """Create a new webhook for the wallet. - - Args: - notification_uri (str): The notification URI of the webhook. - - Returns: - Webhook: The created webhook object. It can be used to monitor activities happening in the wallet. When they occur, webhook will make a request to the specified URI. - - Raises: - Exception: If there's an error creating the webhook. - - """ + """Create a new webhook for the wallet.""" create_wallet_webhook_request = CreateWalletWebhookRequest( notification_uri=notification_uri ) @@ -384,51 +307,21 @@ def create_webhook(self, notification_uri: str) -> "Webhook": return Webhook(model) def faucet(self, asset_id: str | None = None) -> FaucetTransaction: - """Request faucet funds. - - Args: - asset_id (Optional[str]): The asset ID. Defaults to None. - - Returns: - FaucetTransaction: The faucet transaction object. - - Raises: - ValueError: If the default address does not exist. - - """ + """Request faucet funds.""" if self.default_address is None: raise ValueError("Default address does not exist") return self.default_address.faucet(asset_id) def balance(self, asset_id: str) -> Decimal: - """Get the balance of a specific asset for the default address. - - Args: - asset_id (str): The ID of the asset to check the balance for. - - Returns: - Any: The balance of the specified asset. - - Raises: - ValueError: If the default address does not exist. - - """ + """Get the balance of a specific asset for the default address.""" if self.default_address is None: raise ValueError("Default address does not exist") return self.default_address.balance(asset_id) def balances(self) -> BalanceMap: - """List balances of the address. - - Returns: - BalanceMap: The balances of the address, keyed by asset ID. Ether balances are denominated in ETH. - - Raises: - ValueError: If the default address does not exist. - - """ + """List balances of the address.""" if self.default_address is None: raise ValueError("Default address does not exist") @@ -442,22 +335,7 @@ def transfer( gasless: bool = False, skip_batching: bool = False, ) -> Transfer: - """Transfer funds from the wallet. - - Args: - amount (Union[Number, Decimal, str]): The amount of funds to transfer. - asset_id (str): The ID of the asset to transfer. - destination (Union[Address, 'Wallet', str]): The destination for the transfer. - gasless (bool): Whether the transfer should be gasless. Defaults to False. - skip_batching (bool): When True, the Transfer will be submitted immediately. Otherwise, the Transfer will be batched. Defaults to False. Note: requires gasless option to be set to True. - - Returns: - Any: The result of the transfer operation. - - Raises: - ValueError: If the default address does not exist. - - """ + """Transfer funds from the wallet.""" if self.default_address is None: raise ValueError("Default address does not exist") @@ -468,20 +346,7 @@ def transfer( return self.default_address.transfer(amount, asset_id, destination, gasless, skip_batching) def trade(self, amount: Number | Decimal | str, from_asset_id: str, to_asset_id: str) -> Trade: - """Trade funds from the wallet address. - - Args: - amount (Union[Number, Decimal, str]): The amount to trade. - from_asset_id (str): The asset ID to trade from. - to_asset_id (str): The asset ID to trade to. - - Returns: - Trade: The trade object. - - Raises: - ValueError: If the default address does not exist. - - """ + """Trade funds from the wallet address.""" if self.default_address is None: raise ValueError("Default address does not exist") @@ -496,23 +361,7 @@ def invoke_contract( amount: Number | Decimal | str | None = None, asset_id: str | None = None, ) -> ContractInvocation: - """Invoke a method on the specified contract address, with the given ABI and arguments. - - Args: - contract_address (str): The address of the contract to invoke. - method (str): The name of the method to call on the contract. - abi (Optional[list[dict]]): The ABI of the contract, if provided. - args (Optional[dict]): The arguments to pass to the method. - amount (Optional[Union[Number, Decimal, str]]): The amount to send with the invocation, if applicable. - asset_id (Optional[str]): The asset ID associated with the amount, if applicable. - - Returns: - ContractInvocation: The contract invocation object. - - Raises: - ValueError: If the default address does not exist. - - """ + """Invoke a method on the specified contract address.""" if self.default_address is None: raise ValueError("Default address does not exist") @@ -523,16 +372,7 @@ def invoke_contract( return invocation def sign_payload(self, unsigned_payload: str) -> PayloadSignature: - """Sign the given unsigned payload. - - Args: - unsigned_payload (str): The unsigned payload. - - Returns: - PayloadSignature: The payload signature object. - - - """ + """Sign the given unsigned payload.""" if self.default_address is None: raise ValueError("Default address does not exist") @@ -540,12 +380,7 @@ def sign_payload(self, unsigned_payload: str) -> PayloadSignature: @property def default_address(self) -> WalletAddress | None: - """Get the default address of the wallet. - - Returns: - Optional[WalletAddress]: The default address object, or None if not set. - - """ + """Get the default address of the wallet.""" return ( self._address(self._model.default_address.address_id) if self._model.default_address is not None @@ -553,33 +388,14 @@ def default_address(self) -> WalletAddress | None: ) def export_data(self) -> WalletData: - """Export the wallet's data. - - Returns: - WalletData: The wallet's data. - - Raises: - ValueError: If the wallet does not have a seed loaded. - - """ + """Export the wallet's data.""" if self._master is None or self._seed is None: raise ValueError("Wallet does not have seed loaded") return WalletData(self.id, self._seed, self.network_id) def save_seed(self, file_path: str, encrypt: bool | None = False) -> None: - """[Save the wallet seed to a file (deprecated). - - This method is deprecated, and will be removed in a future version. Use load_seed_from_file() instead. - - Args: - file_path (str): The path to the file where the seed will be saved. - encrypt (Optional[bool]): Whether to encrypt the seed before saving. Defaults to False. - - Raises: - ValueError: If the wallet does not have a seed loaded. - - """ + """(Deprecated) Save the wallet seed to a file.""" import warnings warnings.warn( @@ -590,16 +406,7 @@ def save_seed(self, file_path: str, encrypt: bool | None = False) -> None: self.save_seed_to_file(file_path, encrypt) def save_seed_to_file(self, file_path: str, encrypt: bool | None = False) -> None: - """Save the wallet seed to a file. - - Args: - file_path (str): The path to the file where the seed will be saved. - encrypt (Optional[bool]): Whether to encrypt the seed before saving. Defaults to False. - - Raises: - ValueError: If the wallet does not have a seed loaded. - - """ + """Save the wallet seed to a file.""" if self._master is None or self._seed is None: raise ValueError("Wallet does not have seed loaded") @@ -632,17 +439,7 @@ def save_seed_to_file(self, file_path: str, encrypt: bool | None = False) -> Non json.dump(existing_seeds, f, indent=4) def load_seed(self, file_path: str) -> None: - """Load the wallet seed from a file (deprecated). - - This method is deprecated, and will be removed in a future version. Use load_seed_from_file() instead. - - Args: - file_path (str): The path to the file containing the seed data. - - Raises: - ValueError: If the file does not contain seed data for this wallet or if decryption fails. - - """ + """(Deprecated) Load the wallet seed from a file.""" import warnings warnings.warn( @@ -653,15 +450,7 @@ def load_seed(self, file_path: str) -> None: self.load_seed_from_file(file_path) def load_seed_from_file(self, file_path: str) -> None: - """Load the wallet seed from a file. - - Args: - file_path (str): The path to the file containing the seed data. - - Raises: - ValueError: If the file does not contain seed data for this wallet or if decryption fails. - - """ + """Load the wallet seed from a file.""" existing_seeds = self._existing_seeds(file_path) if self.id not in existing_seeds: @@ -685,19 +474,18 @@ def load_seed_from_file(self, file_path: str) -> None: self._seed = seed self._master = self._set_master_node() + # --- Business Change: Updated _encryption_key to support Ed25519 keys --- def _encryption_key(self) -> bytes: """Generate an encryption key based on the private key. - + For ECDSA keys (PEM encoded), an ECDH exchange is performed. For Ed25519 keys (base64 encoded), the raw private key bytes are hashed. - + Returns: bytes: The generated encryption key. - """ - import base64 - - from cryptography.hazmat.primitives.asymmetric import ed25519 + import base64, hashlib + from cryptography.hazmat.primitives.asymmetric import ec, ed25519 # Attempt to load as a PEM-encoded key (for ECDSA) try: @@ -730,17 +518,10 @@ def _encryption_key(self) -> bytes: return hashlib.sha256(raw_bytes).digest() else: raise ValueError("Unsupported key type for encryption key derivation") + # --- End of Business Change --- def _existing_seeds(self, file_path: str) -> dict[str, Any]: - """Load existing seeds from a file. - - Args: - file_path (str): The path to the file containing seed data. - - Returns: - Dict[str, Any]: A dictionary of existing seeds. - - """ + """Load existing seeds from a file.""" seeds_in_file = {} if os.path.exists(file_path): @@ -750,12 +531,7 @@ def _existing_seeds(self, file_path: str) -> dict[str, Any]: return seeds_in_file def _set_addresses(self) -> None: - """Set the addresses of the wallet by fetching them from the API. - - Returns: - None - - """ + """Set the addresses of the wallet by fetching them from the API.""" addresses = Cdp.api_clients.addresses.list_addresses(self.id, limit=self.MAX_ADDRESSES) self._addresses = [ @@ -763,19 +539,7 @@ def _set_addresses(self) -> None: ] def _build_wallet_address(self, model: AddressModel, index: int | None = None) -> WalletAddress: - """Build a wallet address object. - - Args: - model (AddressModel): The address model. - index (Optional[int]): The index of the address. Defaults to None. - - Returns: - WalletAddress: The created address object. - - Raises: - ValueError: If the derived key does not match the wallet. - - """ + """Build a wallet address object.""" if not self.can_sign: return WalletAddress(model) @@ -788,27 +552,14 @@ def _build_wallet_address(self, model: AddressModel, index: int | None = None) - return WalletAddress(model, account) def _address(self, address_id: str) -> WalletAddress | None: - """Get an address by its ID. - - Args: - address_id (str): The ID of the address to retrieve. - - Returns: - Optional[WalletAddress]: The retrieved address object, or None if not found. - - """ + """Get an address by its ID.""" return next( (address for address in self.addresses if address.address_id == address_id), None, ) def _set_master_node(self) -> Bip32Slip10Secp256k1 | None: - """Set the master node for the wallet. - - Returns: - Optional[Bip32Slip10Secp256k1]: The master node, or None if no seed is available. - - """ + """Set the master node for the wallet.""" if self._seed is None: seed = os.urandom(64) self._seed = seed.hex() @@ -823,133 +574,47 @@ def _set_master_node(self) -> Bip32Slip10Secp256k1 | None: return Bip32Slip10Secp256k1.FromSeed(seed) def _validate_seed(self, seed: bytes) -> None: - """Validate the seed. - - Args: - seed (bytes): The seed to validate. - - Raises: - ValueError: If the seed length is invalid. - - """ + """Validate the seed.""" if len(seed) != 32 and len(seed) != 64: raise ValueError("Seed must be 32 or 64 bytes") def _derive_key(self, index: int) -> Bip32Slip10Secp256k1: - """Derive a key from the master node. - - Args: - index (int): The index to use for key derivation. - - Returns: - Bip32Slip10Secp256k1: The derived key. - - """ + """Derive a key from the master node.""" return self._master.DerivePath("m/44'/60'/0'/0" + f"/{index}") def _create_attestation(self, key: Bip32Slip10Secp256k1, public_key_hex: str) -> str: - """Create an attestation for the given private key in the format expected. - - Args: - key (Bip32Slip10Secp256k1): The private key. - public_key_hex (str): The public key in hexadecimal format. - - Returns: - str: The hexadecimal representation of the attestation. - - """ - payload = json.dumps( - {"wallet_id": self.id, "public_key": public_key_hex}, separators=(",", ":") - ) - - signature = coincurve.PrivateKey(key.PrivateKey().Raw().ToBytes()).sign_recoverable( - payload.encode() - ) - + """Create an attestation for the given private key.""" + payload = json.dumps({"wallet_id": self.id, "public_key": public_key_hex}, separators=(",", ":")) + signature = coincurve.PrivateKey(key.PrivateKey().Raw().ToBytes()).sign_recoverable(payload.encode()) r = signature[:32] s = signature[32:64] v = signature[64] + 27 + 4 - attestation = bytes([v]) + r + s - return attestation.hex() def __str__(self) -> str: - """Return a string representation of the Wallet object. - - Returns: - str: A string representation of the Wallet. - - """ + """Return a string representation of the Wallet object.""" return f"Wallet: (id: {self.id}, network_id: {self.network_id}, server_signer_status: {self.server_signer_status})" def __repr__(self) -> str: - """Return a string representation of the Wallet object. - - Returns: - str: A string representation of the Wallet. - - """ + """Return a string representation of the Wallet object.""" return str(self) def deploy_token( self, name: str, symbol: str, total_supply: Number | Decimal | str ) -> SmartContract: - """Deploy a token smart contract. - - Args: - name (str): The name of the token. - symbol (str): The symbol of the token. - total_supply (Union[Number, Decimal, str]): The total supply of the token. - - Returns: - SmartContract: The deployed smart contract. - - Raises: - ValueError: If the default address does not exist. - - """ if self.default_address is None: raise ValueError("Default address does not exist") - return self.default_address.deploy_token(name, symbol, str(total_supply)) def deploy_nft(self, name: str, symbol: str, base_uri: str) -> SmartContract: - """Deploy an NFT smart contract. - - Args: - name (str): The name of the NFT. - symbol (str): The symbol of the NFT. - base_uri (str): The base URI for the NFT. - - Returns: - SmartContract: The deployed smart contract. - - Raises: - ValueError: If the default address does not exist. - - """ if self.default_address is None: raise ValueError("Default address does not exist") - return self.default_address.deploy_nft(name, symbol, base_uri) def deploy_multi_token(self, uri: str) -> SmartContract: - """Deploy a multi-token smart contract. - - Args: - uri (str): The URI for the multi-token contract. - - Returns: - SmartContract: The deployed smart contract. - - Raises: - ValueError: If the default address does not exist. - - """ if self.default_address is None: raise ValueError("Default address does not exist") - return self.default_address.deploy_multi_token(uri) def deploy_contract( @@ -959,62 +624,16 @@ def deploy_contract( contract_name: str, constructor_args: dict, ) -> SmartContract: - """Deploy a custom contract. - - Args: - solidity_version (str): The version of the solidity compiler, must be 0.8.+, such as "0.8.28+commit.7893614a". See https://binaries.soliditylang.org/bin/list.json - solidity_input_json (str): The input json for the solidity compiler. See https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description for more details. - contract_name (str): The name of the contract class to be deployed. - constructor_args (dict): The arguments for the constructor. - - Returns: - SmartContract: The deployed smart contract. - - Raises: - ValueError: If the default address does not exist. - - """ if self.default_address is None: raise ValueError("Default address does not exist") - - return self.default_address.deploy_contract( - solidity_version, solidity_input_json, contract_name, constructor_args - ) + return self.default_address.deploy_contract(solidity_version, solidity_input_json, contract_name, constructor_args) def fund(self, amount: Number | Decimal | str, asset_id: str) -> FundOperation: - """Fund the wallet from your account on the Coinbase Platform. - - Args: - amount (Union[Number, Decimal, str]): The amount of the Asset to fund the wallet with. - asset_id (str): The ID of the Asset to fund with. For Ether, 'eth', 'gwei', and 'wei' are supported. - - Returns: - FundOperation: The created fund operation object. - - Raises: - ValueError: If the default address does not exist. - - """ if self.default_address is None: raise ValueError("Default address does not exist") - return self.default_address.fund(amount, asset_id) def quote_fund(self, amount: Number | Decimal | str, asset_id: str) -> FundQuote: - """Get a quote for funding the wallet from your Coinbase platform account. - - Args: - amount (Union[Number, Decimal, str]): The amount to fund. - asset_id (str): The ID of the Asset to fund with. For Ether, 'eth', 'gwei', and 'wei' are supported. - - Returns: - FundQuote: The fund quote object. - - Raises: - ValueError: If the default address does not exist. - - """ if self.default_address is None: raise ValueError("Default address does not exist") - return self.default_address.quote_fund(amount, asset_id)