diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f1c6b8..b208b01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # CDP Python SDK Changelog -## Unreleased +## [0.15.0] - 2025-01-17 + +### Added + +- Add `deploy_contract` method to `WalletAddress` and `Wallet` to deploy an arbitrary contract. ## [0.14.1] - 2025-01-17 diff --git a/cdp/__version__.py b/cdp/__version__.py index f075dd3..9da2f8f 100644 --- a/cdp/__version__.py +++ b/cdp/__version__.py @@ -1 +1 @@ -__version__ = "0.14.1" +__version__ = "0.15.0" diff --git a/cdp/client/__init__.py b/cdp/client/__init__.py index bf587b4..706cc91 100644 --- a/cdp/client/__init__.py +++ b/cdp/client/__init__.py @@ -65,6 +65,8 @@ from cdp.client.models.broadcast_trade_request import BroadcastTradeRequest from cdp.client.models.broadcast_transfer_request import BroadcastTransferRequest from cdp.client.models.build_staking_operation_request import BuildStakingOperationRequest +from cdp.client.models.compile_smart_contract_request import CompileSmartContractRequest +from cdp.client.models.compiled_smart_contract import CompiledSmartContract from cdp.client.models.contract_event import ContractEvent from cdp.client.models.contract_event_list import ContractEventList from cdp.client.models.contract_invocation import ContractInvocation diff --git a/cdp/client/api/smart_contracts_api.py b/cdp/client/api/smart_contracts_api.py index ac7096e..7259310 100644 --- a/cdp/client/api/smart_contracts_api.py +++ b/cdp/client/api/smart_contracts_api.py @@ -19,6 +19,8 @@ from pydantic import Field, StrictStr from typing import Optional from typing_extensions import Annotated +from cdp.client.models.compile_smart_contract_request import CompileSmartContractRequest +from cdp.client.models.compiled_smart_contract import CompiledSmartContract from cdp.client.models.create_smart_contract_request import CreateSmartContractRequest from cdp.client.models.deploy_smart_contract_request import DeploySmartContractRequest from cdp.client.models.read_contract_request import ReadContractRequest @@ -46,6 +48,280 @@ def __init__(self, api_client=None) -> None: self.api_client = api_client + @validate_call + def compile_smart_contract( + self, + compile_smart_contract_request: CompileSmartContractRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> CompiledSmartContract: + """Compile a smart contract + + Compile a smart contract + + :param compile_smart_contract_request: (required) + :type compile_smart_contract_request: CompileSmartContractRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._compile_smart_contract_serialize( + compile_smart_contract_request=compile_smart_contract_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CompiledSmartContract", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def compile_smart_contract_with_http_info( + self, + compile_smart_contract_request: CompileSmartContractRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[CompiledSmartContract]: + """Compile a smart contract + + Compile a smart contract + + :param compile_smart_contract_request: (required) + :type compile_smart_contract_request: CompileSmartContractRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._compile_smart_contract_serialize( + compile_smart_contract_request=compile_smart_contract_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CompiledSmartContract", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def compile_smart_contract_without_preload_content( + self, + compile_smart_contract_request: CompileSmartContractRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Compile a smart contract + + Compile a smart contract + + :param compile_smart_contract_request: (required) + :type compile_smart_contract_request: CompileSmartContractRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._compile_smart_contract_serialize( + compile_smart_contract_request=compile_smart_contract_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "CompiledSmartContract", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _compile_smart_contract_serialize( + self, + compile_smart_contract_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if compile_smart_contract_request is not None: + _body_params = compile_smart_contract_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'apiKey' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/v1/smart_contracts/compile', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + @validate_call def create_smart_contract( self, diff --git a/cdp/client/models/__init__.py b/cdp/client/models/__init__.py index 64cf743..b66d34c 100644 --- a/cdp/client/models/__init__.py +++ b/cdp/client/models/__init__.py @@ -29,6 +29,8 @@ from cdp.client.models.broadcast_trade_request import BroadcastTradeRequest from cdp.client.models.broadcast_transfer_request import BroadcastTransferRequest from cdp.client.models.build_staking_operation_request import BuildStakingOperationRequest +from cdp.client.models.compile_smart_contract_request import CompileSmartContractRequest +from cdp.client.models.compiled_smart_contract import CompiledSmartContract from cdp.client.models.contract_event import ContractEvent from cdp.client.models.contract_event_list import ContractEventList from cdp.client.models.contract_invocation import ContractInvocation diff --git a/cdp/client/models/compile_smart_contract_request.py b/cdp/client/models/compile_smart_contract_request.py new file mode 100644 index 0000000..2968647 --- /dev/null +++ b/cdp/client/models/compile_smart_contract_request.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +""" + Coinbase Platform API + + This is the OpenAPI 3.0 specification for the Coinbase Platform APIs, used in conjunction with the Coinbase Platform SDKs. + + The version of the OpenAPI document: 0.0.1-alpha + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class CompileSmartContractRequest(BaseModel): + """ + CompileSmartContractRequest + """ # noqa: E501 + solidity_input_json: StrictStr = Field(description="The JSON input containing the Solidity code, dependencies, and compiler settings.") + contract_name: StrictStr = Field(description="The name of the contract to compile.") + solidity_compiler_version: StrictStr = Field(description="The version of the Solidity compiler to use.") + __properties: ClassVar[List[str]] = ["solidity_input_json", "contract_name", "solidity_compiler_version"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CompileSmartContractRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CompileSmartContractRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "solidity_input_json": obj.get("solidity_input_json"), + "contract_name": obj.get("contract_name"), + "solidity_compiler_version": obj.get("solidity_compiler_version") + }) + return _obj + + diff --git a/cdp/client/models/compiled_smart_contract.py b/cdp/client/models/compiled_smart_contract.py new file mode 100644 index 0000000..6b1f512 --- /dev/null +++ b/cdp/client/models/compiled_smart_contract.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +""" + Coinbase Platform API + + This is the OpenAPI 3.0 specification for the Coinbase Platform APIs, used in conjunction with the Coinbase Platform SDKs. + + The version of the OpenAPI document: 0.0.1-alpha + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class CompiledSmartContract(BaseModel): + """ + Represents a compiled smart contract that can be deployed onchain + """ # noqa: E501 + compiled_smart_contract_id: Optional[StrictStr] = Field(default=None, description="The unique identifier of the compiled smart contract.") + solidity_input_json: Optional[StrictStr] = Field(default=None, description="The JSON-encoded input for the Solidity compiler") + contract_creation_bytecode: Optional[StrictStr] = Field(default=None, description="The contract creation bytecode which will be used with constructor arguments to deploy the contract") + abi: Optional[StrictStr] = Field(default=None, description="The JSON-encoded ABI of the contract") + contract_name: Optional[StrictStr] = Field(default=None, description="The name of the smart contract to deploy") + __properties: ClassVar[List[str]] = ["compiled_smart_contract_id", "solidity_input_json", "contract_creation_bytecode", "abi", "contract_name"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CompiledSmartContract from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CompiledSmartContract from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "compiled_smart_contract_id": obj.get("compiled_smart_contract_id"), + "solidity_input_json": obj.get("solidity_input_json"), + "contract_creation_bytecode": obj.get("contract_creation_bytecode"), + "abi": obj.get("abi"), + "contract_name": obj.get("contract_name") + }) + return _obj + + diff --git a/cdp/client/models/create_smart_contract_request.py b/cdp/client/models/create_smart_contract_request.py index 6bb98e4..dafc25d 100644 --- a/cdp/client/models/create_smart_contract_request.py +++ b/cdp/client/models/create_smart_contract_request.py @@ -17,8 +17,8 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict -from typing import Any, ClassVar, Dict, List +from pydantic import BaseModel, ConfigDict, Field, StrictStr +from typing import Any, ClassVar, Dict, List, Optional from cdp.client.models.smart_contract_options import SmartContractOptions from cdp.client.models.smart_contract_type import SmartContractType from typing import Optional, Set @@ -30,7 +30,8 @@ class CreateSmartContractRequest(BaseModel): """ # noqa: E501 type: SmartContractType options: SmartContractOptions - __properties: ClassVar[List[str]] = ["type", "options"] + compiled_smart_contract_id: Optional[StrictStr] = Field(default=None, description="The optional UUID of the compiled smart contract to deploy. This field is only required when SmartContractType is set to custom.") + __properties: ClassVar[List[str]] = ["type", "options", "compiled_smart_contract_id"] model_config = ConfigDict( populate_by_name=True, @@ -87,7 +88,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "type": obj.get("type"), - "options": SmartContractOptions.from_dict(obj["options"]) if obj.get("options") is not None else None + "options": SmartContractOptions.from_dict(obj["options"]) if obj.get("options") is not None else None, + "compiled_smart_contract_id": obj.get("compiled_smart_contract_id") }) return _obj diff --git a/cdp/client/models/network_identifier.py b/cdp/client/models/network_identifier.py index 7d3a547..709679a 100644 --- a/cdp/client/models/network_identifier.py +++ b/cdp/client/models/network_identifier.py @@ -34,6 +34,8 @@ class NetworkIdentifier(str, Enum): SOLANA_MINUS_DEVNET = 'solana-devnet' SOLANA_MINUS_MAINNET = 'solana-mainnet' ARBITRUM_MINUS_MAINNET = 'arbitrum-mainnet' + ARBITRUM_MINUS_SEPOLIA = 'arbitrum-sepolia' + BITCOIN_MINUS_MAINNET = 'bitcoin-mainnet' @classmethod def from_json(cls, json_str: str) -> Self: diff --git a/cdp/client/models/smart_contract.py b/cdp/client/models/smart_contract.py index 3156064..1e14c66 100644 --- a/cdp/client/models/smart_contract.py +++ b/cdp/client/models/smart_contract.py @@ -40,7 +40,8 @@ class SmartContract(BaseModel): abi: StrictStr = Field(description="The JSON-encoded ABI of the contract") transaction: Optional[Transaction] = None is_external: StrictBool = Field(description="Whether the smart contract was deployed externally. If true, the deployer_address and transaction will be omitted.") - __properties: ClassVar[List[str]] = ["smart_contract_id", "network_id", "wallet_id", "contract_address", "contract_name", "deployer_address", "type", "options", "abi", "transaction", "is_external"] + compiled_smart_contract_id: Optional[StrictStr] = Field(default=None, description="The ID of the compiled smart contract that was used to deploy this contract") + __properties: ClassVar[List[str]] = ["smart_contract_id", "network_id", "wallet_id", "contract_address", "contract_name", "deployer_address", "type", "options", "abi", "transaction", "is_external", "compiled_smart_contract_id"] model_config = ConfigDict( populate_by_name=True, @@ -109,7 +110,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "options": SmartContractOptions.from_dict(obj["options"]) if obj.get("options") is not None else None, "abi": obj.get("abi"), "transaction": Transaction.from_dict(obj["transaction"]) if obj.get("transaction") is not None else None, - "is_external": obj.get("is_external") + "is_external": obj.get("is_external"), + "compiled_smart_contract_id": obj.get("compiled_smart_contract_id") }) return _obj diff --git a/cdp/client/models/smart_contract_options.py b/cdp/client/models/smart_contract_options.py index caff948..afc3620 100644 --- a/cdp/client/models/smart_contract_options.py +++ b/cdp/client/models/smart_contract_options.py @@ -24,7 +24,7 @@ from typing import Union, List, Set, Optional, Dict from typing_extensions import Literal, Self -SMARTCONTRACTOPTIONS_ONE_OF_SCHEMAS = ["MultiTokenContractOptions", "NFTContractOptions", "TokenContractOptions"] +SMARTCONTRACTOPTIONS_ONE_OF_SCHEMAS = ["MultiTokenContractOptions", "NFTContractOptions", "TokenContractOptions", "str"] class SmartContractOptions(BaseModel): """ @@ -36,8 +36,10 @@ class SmartContractOptions(BaseModel): oneof_schema_2_validator: Optional[NFTContractOptions] = None # data type: MultiTokenContractOptions oneof_schema_3_validator: Optional[MultiTokenContractOptions] = None - actual_instance: Optional[Union[MultiTokenContractOptions, NFTContractOptions, TokenContractOptions]] = None - one_of_schemas: Set[str] = { "MultiTokenContractOptions", "NFTContractOptions", "TokenContractOptions" } + # data type: str + oneof_schema_4_validator: Optional[StrictStr] = Field(default=None, description="The JSON-encoded arguments to pass to the constructor method for a custom contract. The keys should be the constructor parameter names and the values should be the argument values.") + actual_instance: Optional[Union[MultiTokenContractOptions, NFTContractOptions, TokenContractOptions, str]] = None + one_of_schemas: Set[str] = { "MultiTokenContractOptions", "NFTContractOptions", "TokenContractOptions", "str" } model_config = ConfigDict( validate_assignment=True, @@ -75,12 +77,18 @@ def actual_instance_must_validate_oneof(cls, v): error_messages.append(f"Error! Input type `{type(v)}` is not `MultiTokenContractOptions`") else: match += 1 + # validate data type: str + try: + instance.oneof_schema_4_validator = v + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) if match > 1: # more than 1 match - raise ValueError("Multiple matches found when setting `actual_instance` in SmartContractOptions with oneOf schemas: MultiTokenContractOptions, NFTContractOptions, TokenContractOptions. Details: " + ", ".join(error_messages)) + raise ValueError("Multiple matches found when setting `actual_instance` in SmartContractOptions with oneOf schemas: MultiTokenContractOptions, NFTContractOptions, TokenContractOptions, str. Details: " + ", ".join(error_messages)) elif match == 0: # no match - raise ValueError("No match found when setting `actual_instance` in SmartContractOptions with oneOf schemas: MultiTokenContractOptions, NFTContractOptions, TokenContractOptions. Details: " + ", ".join(error_messages)) + raise ValueError("No match found when setting `actual_instance` in SmartContractOptions with oneOf schemas: MultiTokenContractOptions, NFTContractOptions, TokenContractOptions, str. Details: " + ", ".join(error_messages)) else: return v @@ -113,13 +121,22 @@ def from_json(cls, json_str: str) -> Self: match += 1 except (ValidationError, ValueError) as e: error_messages.append(str(e)) + # deserialize data into str + try: + # validation + instance.oneof_schema_4_validator = json.loads(json_str) + # assign value to actual_instance + instance.actual_instance = instance.oneof_schema_4_validator + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) if match > 1: # more than 1 match - raise ValueError("Multiple matches found when deserializing the JSON string into SmartContractOptions with oneOf schemas: MultiTokenContractOptions, NFTContractOptions, TokenContractOptions. Details: " + ", ".join(error_messages)) + raise ValueError("Multiple matches found when deserializing the JSON string into SmartContractOptions with oneOf schemas: MultiTokenContractOptions, NFTContractOptions, TokenContractOptions, str. Details: " + ", ".join(error_messages)) elif match == 0: # no match - raise ValueError("No match found when deserializing the JSON string into SmartContractOptions with oneOf schemas: MultiTokenContractOptions, NFTContractOptions, TokenContractOptions. Details: " + ", ".join(error_messages)) + raise ValueError("No match found when deserializing the JSON string into SmartContractOptions with oneOf schemas: MultiTokenContractOptions, NFTContractOptions, TokenContractOptions, str. Details: " + ", ".join(error_messages)) else: return instance @@ -133,7 +150,7 @@ def to_json(self) -> str: else: return json.dumps(self.actual_instance) - def to_dict(self) -> Optional[Union[Dict[str, Any], MultiTokenContractOptions, NFTContractOptions, TokenContractOptions]]: + def to_dict(self) -> Optional[Union[Dict[str, Any], MultiTokenContractOptions, NFTContractOptions, TokenContractOptions, str]]: """Returns the dict representation of the actual instance""" if self.actual_instance is None: return None diff --git a/cdp/smart_contract.py b/cdp/smart_contract.py index 0b43462..e9be183 100644 --- a/cdp/smart_contract.py +++ b/cdp/smart_contract.py @@ -182,7 +182,7 @@ def type(self) -> Type: @property def options( self, - ) -> TokenContractOptions | NFTContractOptions | MultiTokenContractOptions | None: + ) -> TokenContractOptions | NFTContractOptions | MultiTokenContractOptions | str | None: """Get the options of the smart contract. Returns: @@ -205,6 +205,8 @@ def options( return self.NFTContractOptions(**options_dict) elif self.type == self.Type.ERC1155: return self.MultiTokenContractOptions(**options_dict) + elif self.type == self.Type.CUSTOM: + return json.dumps(options_dict, separators=(",", ":")) else: raise ValueError(f"Unknown smart contract type: {self.type}") @@ -333,7 +335,8 @@ def create( wallet_id: str, address_id: str, type: Type, - options: TokenContractOptions | NFTContractOptions | MultiTokenContractOptions, + options: TokenContractOptions | NFTContractOptions | MultiTokenContractOptions | str, + compiled_smart_contract_id: str | None = None, ) -> "SmartContract": """Create a new SmartContract object. @@ -342,6 +345,7 @@ def create( address_id: The ID of the address that will deploy the smart contract. type: The type of the smart contract (ERC20, ERC721, or ERC1155). options: The options of the smart contract. + compiled_smart_contract_id: The ID of the compiled smart contract. This must be set for custom contracts. Returns: The created smart contract. @@ -356,6 +360,8 @@ def create( openapi_options = NFTContractOptions(**options) elif isinstance(options, cls.MultiTokenContractOptions): openapi_options = MultiTokenContractOptions(**options) + elif isinstance(options, str): + openapi_options = options else: raise ValueError(f"Unsupported options type: {type(options)}") @@ -364,6 +370,7 @@ def create( create_smart_contract_request = CreateSmartContractRequest( type=type.value, options=smart_contract_options, + compiled_smart_contract_id=compiled_smart_contract_id, ) model = Cdp.api_clients.smart_contracts.create_smart_contract( diff --git a/cdp/wallet.py b/cdp/wallet.py index e2219a8..da17b71 100644 --- a/cdp/wallet.py +++ b/cdp/wallet.py @@ -642,6 +642,7 @@ def load_seed(self, file_path: str) -> None: """ import warnings + warnings.warn( "load_seed() is deprecated and will be removed in a future version. Use load_seed_from_file() instead.", DeprecationWarning, @@ -918,6 +919,35 @@ def deploy_multi_token(self, uri: str) -> SmartContract: return self.default_address.deploy_multi_token(uri) + def deploy_contract( + self, + solidity_version: str, + solidity_input_json: str, + 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 + ) + def fund(self, amount: Number | Decimal | str, asset_id: str) -> FundOperation: """Fund the wallet from your account on the Coinbase Platform. diff --git a/cdp/wallet_address.py b/cdp/wallet_address.py index d43d1b5..3a3d3cb 100644 --- a/cdp/wallet_address.py +++ b/cdp/wallet_address.py @@ -1,3 +1,4 @@ +import json from collections.abc import Iterator from decimal import Decimal from numbers import Number @@ -9,6 +10,7 @@ from cdp.address import Address from cdp.cdp import Cdp from cdp.client.models.address import Address as AddressModel +from cdp.client.models.compile_smart_contract_request import CompileSmartContractRequest from cdp.contract_invocation import ContractInvocation from cdp.errors import InsufficientFundsError from cdp.fund_operation import FundOperation @@ -338,6 +340,50 @@ def deploy_multi_token(self, uri: str) -> SmartContract: return smart_contract + def deploy_contract( + self, + solidity_version: str, + solidity_input_json: str, + contract_name: str, + constructor_args: dict, + ) -> SmartContract: + """Deploy an arbitrary 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. + + """ + compile_smart_contract_request = CompileSmartContractRequest( + solidity_compiler_version=solidity_version, + solidity_input_json=solidity_input_json, + contract_name=contract_name, + ) + compiled_contract = Cdp.api_clients.smart_contracts.compile_smart_contract( + compile_smart_contract_request=compile_smart_contract_request, + ) + + smart_contract = SmartContract.create( + wallet_id=self.wallet_id, + address_id=self.address_id, + type=SmartContract.Type.CUSTOM, + options=json.dumps(constructor_args or {}, separators=(",", ":")), + compiled_smart_contract_id=compiled_contract.compiled_smart_contract_id, + ) + + if Cdp.use_server_signer: + return smart_contract + + smart_contract.sign(self.key) + smart_contract.broadcast() + + return smart_contract + def transfers(self) -> Iterator[Transfer]: """List transfers for this wallet address. diff --git a/docs/cdp.client.models.rst b/docs/cdp.client.models.rst index a02b0b0..8756b3e 100644 --- a/docs/cdp.client.models.rst +++ b/docs/cdp.client.models.rst @@ -140,6 +140,22 @@ cdp.client.models.build\_staking\_operation\_request module :undoc-members: :show-inheritance: +cdp.client.models.compile\_smart\_contract\_request module +---------------------------------------------------------- + +.. automodule:: cdp.client.models.compile_smart_contract_request + :members: + :undoc-members: + :show-inheritance: + +cdp.client.models.compiled\_smart\_contract module +-------------------------------------------------- + +.. automodule:: cdp.client.models.compiled_smart_contract + :members: + :undoc-members: + :show-inheritance: + cdp.client.models.contract\_event module ---------------------------------------- diff --git a/docs/conf.py b/docs/conf.py index 35d4993..98b4802 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ project = 'CDP SDK' author = 'Coinbase Developer Platform' -release = '0.14.1' +release = '0.15.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/pyproject.toml b/pyproject.toml index 9c22904..05d0d4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cdp-sdk" -version = "0.14.1" +version = "0.15.0" description = "CDP Python SDK" authors = ["John Peterson "] license = "LICENSE.md" diff --git a/tests/factories/compiled_smart_contract_factory.py b/tests/factories/compiled_smart_contract_factory.py new file mode 100644 index 0000000..d37c755 --- /dev/null +++ b/tests/factories/compiled_smart_contract_factory.py @@ -0,0 +1,21 @@ +import pytest + +from cdp.client.models.compiled_smart_contract import ( + CompiledSmartContract as CompiledSmartContractModel, +) + + +@pytest.fixture +def compiled_smart_contract_model_factory(): + """Create and return a factory for creating CompiledSmartContractModel fixtures.""" + + def _create_compiled_smart_contract_model(): + return CompiledSmartContractModel( + compiled_smart_contract_id="test-compiled-smart-contract-id", + contract_name="TestContract", + abi='{"abi":"data"}', + solidity_version="0.8.28+commit.7893614a", + solidity_input_json='{"abi":"data"}', + ) + + return _create_compiled_smart_contract_model diff --git a/tests/test_e2e.py b/tests/test_e2e.py index d1b97e4..a010960 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -43,7 +43,7 @@ def test_wallet_data(wallet_data): "wallet_id": wallet_data["wallet_id"], "network_id": wallet_data["network_id"], "seed": wallet_data["seed"], - "default_address_id": wallet_data["default_address_id"] + "default_address_id": wallet_data["default_address_id"], } for key, value in expected.items(): @@ -73,7 +73,7 @@ def test_wallet_transfer(imported_wallet): try: imported_wallet.faucet().wait() except FaucetLimitReachedError: - print("Faucet limit reached, continuing...") + print("Faucet limit reached, continuing...") destination_wallet = Wallet.create() @@ -81,9 +81,7 @@ def test_wallet_transfer(imported_wallet): initial_dest_balance = Decimal(str(destination_wallet.balances().get("eth", 0))) transfer = imported_wallet.transfer( - amount=Decimal("0.000000001"), - asset_id="eth", - destination=destination_wallet + amount=Decimal("0.000000001"), asset_id="eth", destination=destination_wallet ) transfer.wait() @@ -105,14 +103,12 @@ def test_transaction_history(imported_wallet): try: imported_wallet.faucet().wait() except FaucetLimitReachedError: - print("Faucet limit reached, continuing...") + print("Faucet limit reached, continuing...") destination_wallet = Wallet.create() transfer = imported_wallet.transfer( - amount=Decimal("0.0001"), - asset_id="eth", - destination=destination_wallet + amount=Decimal("0.0001"), asset_id="eth", destination=destination_wallet ).wait() time.sleep(10) @@ -148,7 +144,7 @@ def test_wallet_export(imported_wallet): "encrypted": False, "auth_tag": "", "iv": "", - "network_id": exported_wallet.network_id + "network_id": exported_wallet.network_id, } os.unlink("test_seed.json") diff --git a/tests/test_smart_contract.py b/tests/test_smart_contract.py index 2f2ea90..96230cb 100644 --- a/tests/test_smart_contract.py +++ b/tests/test_smart_contract.py @@ -3,7 +3,10 @@ import pytest +from cdp.client.models.create_smart_contract_request import CreateSmartContractRequest from cdp.client.models.register_smart_contract_request import RegisterSmartContractRequest +from cdp.client.models.smart_contract import SmartContract as SmartContractModel +from cdp.client.models.smart_contract_options import SmartContractOptions from cdp.client.models.solidity_value import SolidityValue from cdp.client.models.update_smart_contract_request import UpdateSmartContractRequest from cdp.smart_contract import SmartContract @@ -80,6 +83,45 @@ def test_create_smart_contract(mock_api_clients, smart_contract_factory): ) +@patch("cdp.Cdp.api_clients") +def test_create_custom_smart_contract(mock_api_clients, transaction_model_factory): + """Test the creation of a custom SmartContract object.""" + mock_create_contract = Mock() + mock_create_contract.return_value = SmartContractModel( + smart_contract_id="test-contract-id", + network_id="base-sepolia", + wallet_id="test-wallet-id", + contract_address="0xcontractaddress", + contract_name="TestContract", + deployer_address="0xdeployeraddress", + type="custom", + options={}, + abi='{"abi":"data"}', + transaction=transaction_model_factory(status="complete"), + is_external=False, + ) + mock_api_clients.smart_contracts.create_smart_contract = mock_create_contract + + smart_contract = SmartContract.create( + wallet_id="test-wallet-id", + address_id="0xaddressid", + type=SmartContract.Type.CUSTOM, + options="{}", + compiled_smart_contract_id="test-compiled-smart-contract-id", + ) + + assert isinstance(smart_contract, SmartContract) + mock_create_contract.assert_called_once_with( + wallet_id="test-wallet-id", + address_id="0xaddressid", + create_smart_contract_request=CreateSmartContractRequest( + type=SmartContract.Type.CUSTOM, + options=SmartContractOptions(actual_instance="{}"), + compiled_smart_contract_id="test-compiled-smart-contract-id", + ), + ) + + @patch("cdp.Cdp.api_clients") def test_broadcast_smart_contract(mock_api_clients, smart_contract_factory): """Test the broadcasting of a SmartContract object.""" diff --git a/tests/test_transfer.py b/tests/test_transfer.py index b08be72..9fca7f6 100644 --- a/tests/test_transfer.py +++ b/tests/test_transfer.py @@ -85,9 +85,12 @@ def test_create_transfer(mock_asset, mock_api_clients, transfer_factory, asset_f assert create_transfer_request.network_id == "base-sepolia" assert create_transfer_request.gasless == gasless + @patch("cdp.Cdp.api_clients") @patch("cdp.transfer.Asset") -def test_create_transfer_with_skip_batching(mock_asset, mock_api_clients, transfer_factory, asset_factory): +def test_create_transfer_with_skip_batching( + mock_asset, mock_api_clients, transfer_factory, asset_factory +): """Test the creation of a Transfer object.""" mock_fetch = Mock() mock_fetch.return_value = asset_factory() @@ -127,6 +130,7 @@ def test_create_transfer_with_skip_batching(mock_asset, mock_api_clients, transf assert create_transfer_request.gasless assert create_transfer_request.skip_batching + def test_create_transfer_invalid_skip_batching(): """Test the creation of a Transfer object with skip_batching and no gasless.""" with pytest.raises(ValueError, match="skip_batching requires gasless to be True"): @@ -141,6 +145,7 @@ def test_create_transfer_invalid_skip_batching(): skip_batching=True, ) + @patch("cdp.Cdp.api_clients") def test_list_transfers(mock_api_clients, transfer_factory): """Test the listing of transfers.""" diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 333a197..4b4d452 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -460,6 +460,32 @@ def test_wallet_deploy_multi_token(wallet_factory): ) +@patch("cdp.Cdp.use_server_signer", True) +def test_wallet_deploy_contract(wallet_factory): + """Test the deploy_contract method of a Wallet.""" + wallet = wallet_factory() + mock_default_address = Mock(spec=WalletAddress) + mock_smart_contract = Mock(spec=SmartContract) + mock_default_address.deploy_contract.return_value = mock_smart_contract + + with patch.object( + Wallet, "default_address", new_callable=PropertyMock + ) as mock_default_address_prop: + mock_default_address_prop.return_value = mock_default_address + + smart_contract = wallet.deploy_contract( + solidity_version="0.8.28+commit.7893614a", + solidity_input_json="{}", + contract_name="TestContract", + constructor_args={"arg1": "value1"}, + ) + + assert isinstance(smart_contract, SmartContract) + mock_default_address.deploy_contract.assert_called_once_with( + "0.8.28+commit.7893614a", "{}", "TestContract", {"arg1": "value1"} + ) + + @patch("cdp.Cdp.use_server_signer", True) def test_wallet_deploy_token_no_default_address(wallet_factory): """Test the deploy_token method of a Wallet with no default address.""" @@ -502,6 +528,25 @@ def test_wallet_deploy_multi_token_no_default_address(wallet_factory): wallet.deploy_multi_token(uri="https://example.com/multi-token/{id}.json") +@patch("cdp.Cdp.use_server_signer", True) +def test_wallet_deploy_contract_no_default_address(wallet_factory): + """Test the deploy_contract method of a Wallet with no default address.""" + wallet = wallet_factory() + + with patch.object( + Wallet, "default_address", new_callable=PropertyMock + ) as mock_default_address_prop: + mock_default_address_prop.return_value = None + + with pytest.raises(ValueError, match="Default address does not exist"): + wallet.deploy_contract( + solidity_version="0.8.28+commit.7893614a", + solidity_input_json="{}", + contract_name="TestContract", + constructor_args={"arg1": "value1"}, + ) + + @patch("cdp.Cdp.use_server_signer", True) def test_wallet_deploy_token_with_server_signer(wallet_factory): """Test the deploy_token method of a Wallet with server-signer.""" @@ -565,6 +610,32 @@ def test_wallet_deploy_multi_token_with_server_signer(wallet_factory): ) +@patch("cdp.Cdp.use_server_signer", True) +def test_wallet_deploy_contract_with_server_signer(wallet_factory): + """Test the deploy_contract method of a Wallet with server-signer.""" + wallet = wallet_factory() + mock_default_address = Mock(spec=WalletAddress) + mock_smart_contract = Mock(spec=SmartContract) + mock_default_address.deploy_contract.return_value = mock_smart_contract + + with patch.object( + Wallet, "default_address", new_callable=PropertyMock + ) as mock_default_address_prop: + mock_default_address_prop.return_value = mock_default_address + + smart_contract = wallet.deploy_contract( + solidity_version="0.8.28+commit.7893614a", + solidity_input_json="{}", + contract_name="TestContract", + constructor_args={"arg1": "value1"}, + ) + + assert isinstance(smart_contract, SmartContract) + mock_default_address.deploy_contract.assert_called_once_with( + "0.8.28+commit.7893614a", "{}", "TestContract", {"arg1": "value1"} + ) + + @patch("cdp.Cdp.api_clients") def test_create_webhook(mock_api_clients, wallet_factory, webhook_factory): """Test Wallet create_webhook method.""" diff --git a/tests/test_wallet_address.py b/tests/test_wallet_address.py index 0dcaec0..416fe78 100644 --- a/tests/test_wallet_address.py +++ b/tests/test_wallet_address.py @@ -8,6 +8,7 @@ from eth_utils import to_bytes from web3 import Web3 +from cdp.client.models.compile_smart_contract_request import CompileSmartContractRequest from cdp.contract_invocation import ContractInvocation from cdp.errors import InsufficientFundsError from cdp.fund_operation import FundOperation @@ -868,6 +869,57 @@ def test_wallet_address_deploy_multi_token(mock_smart_contract, wallet_address_f mock_smart_contract_instance.broadcast.assert_called_once() +# Note: The decorators are applied from bottom to top, but parameters are passed from top to bottom +# So we need to reverse the order of the patches to match the parameter order +@patch("cdp.wallet_address.SmartContract") +@patch("cdp.Cdp.api_clients") +@patch("cdp.Cdp.use_server_signer", False) +def test_wallet_address_deploy_contract( + mock_cdp_api_clients, + mock_smart_contract, + wallet_address_factory, + compiled_smart_contract_model_factory, +): + """Test the deploy_contract method of a WalletAddress.""" + wallet_address = wallet_address_factory(key=True) + + mock_smart_contracts = Mock() + mock_cdp_api_clients.smart_contracts = mock_smart_contracts + compiled_contract = compiled_smart_contract_model_factory() + mock_smart_contracts.compile_smart_contract.return_value = compiled_contract + + mock_smart_contract_instance = Mock(spec=SmartContract) + mock_smart_contract.create.return_value = mock_smart_contract_instance + + smart_contract = wallet_address.deploy_contract( + solidity_version="0.8.28+commit.7893614a", + solidity_input_json='{"abi":"data"}', + contract_name="TestContract", + constructor_args={}, + ) + + mock_smart_contracts.compile_smart_contract.assert_called_once_with( + compile_smart_contract_request=CompileSmartContractRequest( + solidity_compiler_version="0.8.28+commit.7893614a", + solidity_input_json='{"abi":"data"}', + contract_name="TestContract", + ), + ) + + mock_smart_contract.create.assert_called_once_with( + wallet_id=wallet_address.wallet_id, + address_id=wallet_address.address_id, + type=mock_smart_contract.Type.CUSTOM, + options="{}", + compiled_smart_contract_id=compiled_contract.compiled_smart_contract_id, + ) + + assert isinstance(smart_contract, Mock) + assert smart_contract == mock_smart_contract_instance + mock_smart_contract_instance.sign.assert_called_once_with(wallet_address.key) + mock_smart_contract_instance.broadcast.assert_called_once() + + @patch("cdp.wallet_address.SmartContract") @patch("cdp.Cdp.use_server_signer", True) def test_wallet_address_deploy_token_with_server_signer( @@ -953,6 +1005,47 @@ def test_wallet_address_deploy_multi_token_with_server_signer( mock_smart_contract_instance.broadcast.assert_not_called() +@patch("cdp.wallet_address.SmartContract") +@patch("cdp.Cdp.api_clients") +@patch("cdp.Cdp.use_server_signer", True) +def test_deploy_contract_with_server_signer( + mock_cdp_api_clients, + mock_smart_contract, + wallet_address_factory, + compiled_smart_contract_model_factory, +): + """Test the deploy_contract method of a WalletAddress with server signer.""" + wallet_address = wallet_address_factory(key=True) + + mock_smart_contracts = Mock() + mock_cdp_api_clients.smart_contracts = mock_smart_contracts + compiled_contract = compiled_smart_contract_model_factory() + mock_smart_contracts.compile_smart_contract.return_value = compiled_contract + + mock_smart_contract_instance = Mock(spec=SmartContract) + mock_smart_contract.create.return_value = mock_smart_contract_instance + + smart_contract = wallet_address.deploy_contract( + solidity_version="0.8.28+commit.7893614a", + solidity_input_json='{"abi":"data"}', + contract_name="TestContract", + constructor_args={}, + ) + + mock_smart_contracts.compile_smart_contract.assert_called_once_with( + compile_smart_contract_request=CompileSmartContractRequest( + solidity_compiler_version="0.8.28+commit.7893614a", + solidity_input_json='{"abi":"data"}', + contract_name="TestContract", + ), + ) + + assert isinstance(smart_contract, Mock) + assert smart_contract == mock_smart_contract_instance + mock_smart_contract_instance.sign.assert_not_called() + mock_smart_contract_instance.broadcast.assert_not_called() + + @patch("cdp.wallet_address.SmartContract") def test_deploy_token_api_error(mock_smart_contract, wallet_address_factory): """Test the deploy_token method raises an error when the create API call fails.""" @@ -1047,6 +1140,77 @@ def test_deploy_multi_token_broadcast_api_error(mock_smart_contract, wallet_addr mock_smart_contract_instance.broadcast.assert_called_once() +@patch("cdp.wallet_address.SmartContract") +@patch("cdp.Cdp.api_clients") +def test_deploy_contract_api_error( + mock_cdp_api_clients, mock_smart_contract, wallet_address_factory +): + """Test the deploy_contract method raises an error when the API call fails.""" + wallet_address_with_key = wallet_address_factory(key=True) + + mock_smart_contracts = Mock() + mock_cdp_api_clients.smart_contracts = mock_smart_contracts + mock_smart_contract.create.side_effect = Exception("API Error") + + with pytest.raises(Exception, match="API Error"): + wallet_address_with_key.deploy_contract( + solidity_version="0.8.28+commit.7893614a", + solidity_input_json='{"abi":"data"}', + contract_name="TestContract", + constructor_args={}, + ) + + mock_smart_contracts.compile_smart_contract.assert_called_once_with( + compile_smart_contract_request=CompileSmartContractRequest( + solidity_compiler_version="0.8.28+commit.7893614a", + solidity_input_json='{"abi":"data"}', + contract_name="TestContract", + ), + ) + mock_smart_contract.create.assert_called_once() + + +@patch("cdp.wallet_address.SmartContract") +@patch("cdp.Cdp.api_clients") +def test_deploy_contract_broadcast_api_error( + mock_cdp_api_clients, + mock_smart_contract, + wallet_address_factory, + compiled_smart_contract_model_factory, +): + """Test the deploy_contract method raises an error when the API call fails.""" + wallet_address_with_key = wallet_address_factory(key=True) + + mock_smart_contracts = Mock() + mock_cdp_api_clients.smart_contracts = mock_smart_contracts + compiled_contract = compiled_smart_contract_model_factory() + mock_smart_contracts.compile_smart_contract.return_value = compiled_contract + + mock_smart_contract_instance = Mock(spec=SmartContract) + mock_smart_contract.create.return_value = mock_smart_contract_instance + mock_smart_contract_instance.broadcast.side_effect = Exception("API Error") + + with pytest.raises(Exception, match="API Error"): + wallet_address_with_key.deploy_contract( + solidity_version="0.8.28+commit.7893614a", + solidity_input_json='{"abi":"data"}', + contract_name="TestContract", + constructor_args={}, + ) + + mock_smart_contracts.compile_smart_contract.assert_called_once_with( + compile_smart_contract_request=CompileSmartContractRequest( + solidity_compiler_version="0.8.28+commit.7893614a", + solidity_input_json='{"abi":"data"}', + contract_name="TestContract", + ), + ) + + mock_smart_contract.create.assert_called_once() + mock_smart_contract_instance.sign.assert_called_once_with(wallet_address_with_key.key) + mock_smart_contract_instance.broadcast.assert_called_once() + + @patch("cdp.Cdp.api_clients") def test_ensure_sufficient_balance_sufficient_full_amount( mock_api_clients, wallet_address_factory, balance_model_factory