From 2ee0c2ced2949d8849681865c93905ab8369a770 Mon Sep 17 00:00:00 2001 From: jianlunz-cb Date: Wed, 18 Dec 2024 10:29:30 -0800 Subject: [PATCH 1/6] feat: Support Register/List/Update Smart Contract (#65) --- cdp/client/__init__.py | 4 +- cdp/client/api/addresses_api.py | 15 + cdp/client/api/assets_api.py | 2 + cdp/client/api/balance_history_api.py | 2 + cdp/client/api/contract_events_api.py | 2 + cdp/client/api/contract_invocations_api.py | 6 + cdp/client/api/external_addresses_api.py | 8 + cdp/client/api/fund_api.py | 6 + cdp/client/api/mpc_wallet_stake_api.py | 4 + cdp/client/api/networks_api.py | 2 + cdp/client/api/onchain_identity_api.py | 2 + cdp/client/api/reputation_api.py | 278 +------------ cdp/client/api/server_signers_api.py | 8 + cdp/client/api/smart_contracts_api.py | 388 ++++++++++++++++-- cdp/client/api/stake_api.py | 14 + cdp/client/api/trades_api.py | 6 + cdp/client/api/transaction_history_api.py | 2 + cdp/client/api/transfers_api.py | 6 + cdp/client/api/wallets_api.py | 9 + cdp/client/api/webhooks_api.py | 10 + cdp/client/configuration.py | 38 ++ cdp/client/models/__init__.py | 4 +- cdp/client/models/address_reputation.py | 6 +- .../models/register_smart_contract_request.py | 90 ++++ cdp/client/models/smart_contract.py | 20 +- cdp/client/models/transfer.py | 16 +- .../models/update_smart_contract_request.py | 90 ++++ cdp/smart_contract.py | 146 ++++++- cdp/wallet_address.py | 4 +- tests/factories/smart_contract_factory.py | 23 +- tests/test_smart_contract.py | 125 ++++++ tests/test_wallet_address.py | 12 +- 32 files changed, 996 insertions(+), 352 deletions(-) create mode 100644 cdp/client/models/register_smart_contract_request.py create mode 100644 cdp/client/models/update_smart_contract_request.py diff --git a/cdp/client/__init__.py b/cdp/client/__init__.py index 216b8c1..68ff0a9 100644 --- a/cdp/client/__init__.py +++ b/cdp/client/__init__.py @@ -50,14 +50,12 @@ from cdp.client.exceptions import ApiException # import models into sdk package -from cdp.client.models.abi import ABI from cdp.client.models.address import Address from cdp.client.models.address_balance_list import AddressBalanceList from cdp.client.models.address_historical_balance_list import AddressHistoricalBalanceList from cdp.client.models.address_list import AddressList from cdp.client.models.address_reputation import AddressReputation from cdp.client.models.address_reputation_metadata import AddressReputationMetadata -from cdp.client.models.address_risk import AddressRisk from cdp.client.models.address_transaction_list import AddressTransactionList from cdp.client.models.asset import Asset from cdp.client.models.balance import Balance @@ -116,6 +114,7 @@ from cdp.client.models.payload_signature import PayloadSignature from cdp.client.models.payload_signature_list import PayloadSignatureList from cdp.client.models.read_contract_request import ReadContractRequest +from cdp.client.models.register_smart_contract_request import RegisterSmartContractRequest from cdp.client.models.seed_creation_event import SeedCreationEvent from cdp.client.models.seed_creation_event_result import SeedCreationEventResult from cdp.client.models.server_signer import ServerSigner @@ -150,6 +149,7 @@ from cdp.client.models.transaction_type import TransactionType from cdp.client.models.transfer import Transfer from cdp.client.models.transfer_list import TransferList +from cdp.client.models.update_smart_contract_request import UpdateSmartContractRequest from cdp.client.models.update_webhook_request import UpdateWebhookRequest from cdp.client.models.user import User from cdp.client.models.validator import Validator diff --git a/cdp/client/api/addresses_api.py b/cdp/client/api/addresses_api.py index 4ec0209..fb0dd2e 100644 --- a/cdp/client/api/addresses_api.py +++ b/cdp/client/api/addresses_api.py @@ -315,6 +315,7 @@ def _create_address_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -618,6 +619,7 @@ def _create_payload_signature_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -893,6 +895,8 @@ def _get_address_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1183,6 +1187,8 @@ def _get_address_balance_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1473,6 +1479,8 @@ def _get_payload_signature_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1765,6 +1773,8 @@ def _list_address_balances_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -2059,6 +2069,8 @@ def _list_addresses_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -2368,6 +2380,8 @@ def _list_payload_signatures_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -2663,6 +2677,7 @@ def _request_faucet_funds_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/assets_api.py b/cdp/client/api/assets_api.py index 8d82e45..4729160 100644 --- a/cdp/client/api/assets_api.py +++ b/cdp/client/api/assets_api.py @@ -293,6 +293,8 @@ def _get_asset_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/balance_history_api.py b/cdp/client/api/balance_history_api.py index 9bd47f5..3e9abf4 100644 --- a/cdp/client/api/balance_history_api.py +++ b/cdp/client/api/balance_history_api.py @@ -343,6 +343,8 @@ def _list_address_historical_balance_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/contract_events_api.py b/cdp/client/api/contract_events_api.py index c425a7b..c8cc88c 100644 --- a/cdp/client/api/contract_events_api.py +++ b/cdp/client/api/contract_events_api.py @@ -396,6 +396,8 @@ def _list_contract_events_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/contract_invocations_api.py b/cdp/client/api/contract_invocations_api.py index 8082d7b..083b34f 100644 --- a/cdp/client/api/contract_invocations_api.py +++ b/cdp/client/api/contract_invocations_api.py @@ -340,6 +340,7 @@ def _broadcast_contract_invocation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -643,6 +644,7 @@ def _create_contract_invocation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -933,6 +935,8 @@ def _get_contract_invocation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1242,6 +1246,8 @@ def _list_contract_invocations_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/external_addresses_api.py b/cdp/client/api/external_addresses_api.py index 6fd201f..857820f 100644 --- a/cdp/client/api/external_addresses_api.py +++ b/cdp/client/api/external_addresses_api.py @@ -311,6 +311,8 @@ def _get_external_address_balance_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -601,6 +603,8 @@ def _get_faucet_transaction_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -893,6 +897,8 @@ def _list_external_address_balances_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1202,6 +1208,8 @@ def _request_external_faucet_funds_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/fund_api.py b/cdp/client/api/fund_api.py index 9dd56c1..e4ed67e 100644 --- a/cdp/client/api/fund_api.py +++ b/cdp/client/api/fund_api.py @@ -326,6 +326,7 @@ def _create_fund_operation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -629,6 +630,7 @@ def _create_fund_quote_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -919,6 +921,8 @@ def _get_fund_operation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1228,6 +1232,8 @@ def _list_fund_operations_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/mpc_wallet_stake_api.py b/cdp/client/api/mpc_wallet_stake_api.py index 1d309e2..544e5d3 100644 --- a/cdp/client/api/mpc_wallet_stake_api.py +++ b/cdp/client/api/mpc_wallet_stake_api.py @@ -338,6 +338,7 @@ def _broadcast_staking_operation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -641,6 +642,7 @@ def _create_staking_operation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -931,6 +933,8 @@ def _get_staking_operation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/networks_api.py b/cdp/client/api/networks_api.py index de7dd00..0c60658 100644 --- a/cdp/client/api/networks_api.py +++ b/cdp/client/api/networks_api.py @@ -278,6 +278,8 @@ def _get_network_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/onchain_identity_api.py b/cdp/client/api/onchain_identity_api.py index e09fdf8..8ab19b1 100644 --- a/cdp/client/api/onchain_identity_api.py +++ b/cdp/client/api/onchain_identity_api.py @@ -346,6 +346,8 @@ def _resolve_identity_by_address_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/reputation_api.py b/cdp/client/api/reputation_api.py index 5d77525..7755e6c 100644 --- a/cdp/client/api/reputation_api.py +++ b/cdp/client/api/reputation_api.py @@ -19,7 +19,6 @@ from pydantic import Field, StrictStr from typing_extensions import Annotated from cdp.client.models.address_reputation import AddressReputation -from cdp.client.models.address_risk import AddressRisk from cdp.client.api_client import ApiClient, RequestSerialized from cdp.client.api_response import ApiResponse @@ -294,6 +293,8 @@ def _get_address_reputation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -312,278 +313,3 @@ def _get_address_reputation_serialize( ) - - - @validate_call - def get_address_risk( - self, - network_id: Annotated[StrictStr, Field(description="The ID of the blockchain network.")], - address_id: Annotated[StrictStr, Field(description="The ID of the address to fetch the risk for.")], - _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, - ) -> AddressRisk: - """Get the risk of an address - - Get the risk of an address - - :param network_id: The ID of the blockchain network. (required) - :type network_id: str - :param address_id: The ID of the address to fetch the risk for. (required) - :type address_id: str - :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._get_address_risk_serialize( - network_id=network_id, - address_id=address_id, - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - '200': "AddressRisk", - } - 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 get_address_risk_with_http_info( - self, - network_id: Annotated[StrictStr, Field(description="The ID of the blockchain network.")], - address_id: Annotated[StrictStr, Field(description="The ID of the address to fetch the risk for.")], - _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[AddressRisk]: - """Get the risk of an address - - Get the risk of an address - - :param network_id: The ID of the blockchain network. (required) - :type network_id: str - :param address_id: The ID of the address to fetch the risk for. (required) - :type address_id: str - :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._get_address_risk_serialize( - network_id=network_id, - address_id=address_id, - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - '200': "AddressRisk", - } - 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 get_address_risk_without_preload_content( - self, - network_id: Annotated[StrictStr, Field(description="The ID of the blockchain network.")], - address_id: Annotated[StrictStr, Field(description="The ID of the address to fetch the risk for.")], - _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: - """Get the risk of an address - - Get the risk of an address - - :param network_id: The ID of the blockchain network. (required) - :type network_id: str - :param address_id: The ID of the address to fetch the risk for. (required) - :type address_id: str - :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._get_address_risk_serialize( - network_id=network_id, - address_id=address_id, - _request_auth=_request_auth, - _content_type=_content_type, - _headers=_headers, - _host_index=_host_index - ) - - _response_types_map: Dict[str, Optional[str]] = { - '200': "AddressRisk", - } - response_data = self.api_client.call_api( - *_param, - _request_timeout=_request_timeout - ) - return response_data.response - - - def _get_address_risk_serialize( - self, - network_id, - address_id, - _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 - if network_id is not None: - _path_params['network_id'] = network_id - if address_id is not None: - _path_params['address_id'] = address_id - # process the query parameters - # process the header parameters - # process the form parameters - # process the body parameter - - - # set the HTTP header `Accept` - if 'Accept' not in _header_params: - _header_params['Accept'] = self.api_client.select_header_accept( - [ - 'application/json' - ] - ) - - - # authentication setting - _auth_settings: List[str] = [ - ] - - return self.api_client.param_serialize( - method='GET', - resource_path='/v1/networks/{network_id}/addresses/{address_id}/risk', - 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 - ) - - diff --git a/cdp/client/api/server_signers_api.py b/cdp/client/api/server_signers_api.py index 84e3e82..1d4fb4d 100644 --- a/cdp/client/api/server_signers_api.py +++ b/cdp/client/api/server_signers_api.py @@ -297,6 +297,7 @@ def _create_server_signer_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -557,6 +558,8 @@ def _get_server_signer_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -854,6 +857,7 @@ def _list_server_signer_events_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -1133,6 +1137,8 @@ def _list_server_signers_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1421,6 +1427,7 @@ def _submit_server_signer_seed_event_result_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -1709,6 +1716,7 @@ def _submit_server_signer_signature_event_result_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/smart_contracts_api.py b/cdp/client/api/smart_contracts_api.py index ab3a59e..ac7096e 100644 --- a/cdp/client/api/smart_contracts_api.py +++ b/cdp/client/api/smart_contracts_api.py @@ -19,13 +19,14 @@ from pydantic import Field, StrictStr from typing import Optional from typing_extensions import Annotated -from cdp.client.models.abi import ABI 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 +from cdp.client.models.register_smart_contract_request import RegisterSmartContractRequest from cdp.client.models.smart_contract import SmartContract from cdp.client.models.smart_contract_list import SmartContractList from cdp.client.models.solidity_value import SolidityValue +from cdp.client.models.update_smart_contract_request import UpdateSmartContractRequest from cdp.client.api_client import ApiClient, RequestSerialized from cdp.client.api_response import ApiResponse @@ -328,6 +329,7 @@ def _create_smart_contract_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -646,6 +648,7 @@ def _deploy_smart_contract_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -936,6 +939,8 @@ def _get_smart_contract_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1198,6 +1203,8 @@ def _list_smart_contracts_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1501,6 +1508,8 @@ def _read_contract_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1524,9 +1533,9 @@ def _read_contract_serialize( @validate_call def register_smart_contract( self, - contract_address: Annotated[StrictStr, Field(description="EVM address of the smart contract (42 characters, including '0x', in lowercase)")], network_id: Annotated[StrictStr, Field(description="The ID of the network to fetch.")], - abi: ABI, + contract_address: Annotated[StrictStr, Field(description="EVM address of the smart contract (42 characters, including '0x', in lowercase)")], + register_smart_contract_request: Optional[RegisterSmartContractRequest] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -1539,17 +1548,17 @@ def register_smart_contract( _content_type: Optional[StrictStr] = None, _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, - ) -> None: + ) -> SmartContract: """Register a smart contract Register a smart contract - :param contract_address: EVM address of the smart contract (42 characters, including '0x', in lowercase) (required) - :type contract_address: str :param network_id: The ID of the network to fetch. (required) :type network_id: str - :param abi: (required) - :type abi: ABI + :param contract_address: EVM address of the smart contract (42 characters, including '0x', in lowercase) (required) + :type contract_address: str + :param register_smart_contract_request: + :type register_smart_contract_request: RegisterSmartContractRequest :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 @@ -1573,9 +1582,9 @@ def register_smart_contract( """ # noqa: E501 _param = self._register_smart_contract_serialize( - contract_address=contract_address, network_id=network_id, - abi=abi, + contract_address=contract_address, + register_smart_contract_request=register_smart_contract_request, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -1583,7 +1592,7 @@ def register_smart_contract( ) _response_types_map: Dict[str, Optional[str]] = { - '200': None, + '200': "SmartContract", } response_data = self.api_client.call_api( *_param, @@ -1599,9 +1608,9 @@ def register_smart_contract( @validate_call def register_smart_contract_with_http_info( self, - contract_address: Annotated[StrictStr, Field(description="EVM address of the smart contract (42 characters, including '0x', in lowercase)")], network_id: Annotated[StrictStr, Field(description="The ID of the network to fetch.")], - abi: ABI, + contract_address: Annotated[StrictStr, Field(description="EVM address of the smart contract (42 characters, including '0x', in lowercase)")], + register_smart_contract_request: Optional[RegisterSmartContractRequest] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -1614,17 +1623,17 @@ def register_smart_contract_with_http_info( _content_type: Optional[StrictStr] = None, _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, - ) -> ApiResponse[None]: + ) -> ApiResponse[SmartContract]: """Register a smart contract Register a smart contract - :param contract_address: EVM address of the smart contract (42 characters, including '0x', in lowercase) (required) - :type contract_address: str :param network_id: The ID of the network to fetch. (required) :type network_id: str - :param abi: (required) - :type abi: ABI + :param contract_address: EVM address of the smart contract (42 characters, including '0x', in lowercase) (required) + :type contract_address: str + :param register_smart_contract_request: + :type register_smart_contract_request: RegisterSmartContractRequest :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 @@ -1648,9 +1657,9 @@ def register_smart_contract_with_http_info( """ # noqa: E501 _param = self._register_smart_contract_serialize( - contract_address=contract_address, network_id=network_id, - abi=abi, + contract_address=contract_address, + register_smart_contract_request=register_smart_contract_request, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -1658,7 +1667,7 @@ def register_smart_contract_with_http_info( ) _response_types_map: Dict[str, Optional[str]] = { - '200': None, + '200': "SmartContract", } response_data = self.api_client.call_api( *_param, @@ -1674,9 +1683,9 @@ def register_smart_contract_with_http_info( @validate_call def register_smart_contract_without_preload_content( self, - contract_address: Annotated[StrictStr, Field(description="EVM address of the smart contract (42 characters, including '0x', in lowercase)")], network_id: Annotated[StrictStr, Field(description="The ID of the network to fetch.")], - abi: ABI, + contract_address: Annotated[StrictStr, Field(description="EVM address of the smart contract (42 characters, including '0x', in lowercase)")], + register_smart_contract_request: Optional[RegisterSmartContractRequest] = None, _request_timeout: Union[ None, Annotated[StrictFloat, Field(gt=0)], @@ -1694,12 +1703,12 @@ def register_smart_contract_without_preload_content( Register a smart contract - :param contract_address: EVM address of the smart contract (42 characters, including '0x', in lowercase) (required) - :type contract_address: str :param network_id: The ID of the network to fetch. (required) :type network_id: str - :param abi: (required) - :type abi: ABI + :param contract_address: EVM address of the smart contract (42 characters, including '0x', in lowercase) (required) + :type contract_address: str + :param register_smart_contract_request: + :type register_smart_contract_request: RegisterSmartContractRequest :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 @@ -1723,9 +1732,9 @@ def register_smart_contract_without_preload_content( """ # noqa: E501 _param = self._register_smart_contract_serialize( - contract_address=contract_address, network_id=network_id, - abi=abi, + contract_address=contract_address, + register_smart_contract_request=register_smart_contract_request, _request_auth=_request_auth, _content_type=_content_type, _headers=_headers, @@ -1733,7 +1742,7 @@ def register_smart_contract_without_preload_content( ) _response_types_map: Dict[str, Optional[str]] = { - '200': None, + '200': "SmartContract", } response_data = self.api_client.call_api( *_param, @@ -1744,9 +1753,9 @@ def register_smart_contract_without_preload_content( def _register_smart_contract_serialize( self, - contract_address, network_id, - abi, + contract_address, + register_smart_contract_request, _request_auth, _content_type, _headers, @@ -1768,16 +1777,16 @@ def _register_smart_contract_serialize( _body_params: Optional[bytes] = None # process the path parameters - if contract_address is not None: - _path_params['contract_address'] = contract_address if network_id is not None: _path_params['network_id'] = network_id + if contract_address is not None: + _path_params['contract_address'] = contract_address # process the query parameters # process the header parameters # process the form parameters # process the body parameter - if abi is not None: - _body_params = abi + if register_smart_contract_request is not None: + _body_params = register_smart_contract_request # set the HTTP header `Accept` @@ -1804,6 +1813,8 @@ def _register_smart_contract_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1822,3 +1833,308 @@ def _register_smart_contract_serialize( ) + + + @validate_call + def update_smart_contract( + self, + network_id: Annotated[StrictStr, Field(description="The ID of the network to fetch.")], + contract_address: Annotated[StrictStr, Field(description="EVM address of the smart contract (42 characters, including '0x', in lowercase)")], + update_smart_contract_request: Optional[UpdateSmartContractRequest] = None, + _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, + ) -> SmartContract: + """Update a smart contract + + Update a smart contract + + :param network_id: The ID of the network to fetch. (required) + :type network_id: str + :param contract_address: EVM address of the smart contract (42 characters, including '0x', in lowercase) (required) + :type contract_address: str + :param update_smart_contract_request: + :type update_smart_contract_request: UpdateSmartContractRequest + :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._update_smart_contract_serialize( + network_id=network_id, + contract_address=contract_address, + update_smart_contract_request=update_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': "SmartContract", + } + 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 update_smart_contract_with_http_info( + self, + network_id: Annotated[StrictStr, Field(description="The ID of the network to fetch.")], + contract_address: Annotated[StrictStr, Field(description="EVM address of the smart contract (42 characters, including '0x', in lowercase)")], + update_smart_contract_request: Optional[UpdateSmartContractRequest] = None, + _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[SmartContract]: + """Update a smart contract + + Update a smart contract + + :param network_id: The ID of the network to fetch. (required) + :type network_id: str + :param contract_address: EVM address of the smart contract (42 characters, including '0x', in lowercase) (required) + :type contract_address: str + :param update_smart_contract_request: + :type update_smart_contract_request: UpdateSmartContractRequest + :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._update_smart_contract_serialize( + network_id=network_id, + contract_address=contract_address, + update_smart_contract_request=update_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': "SmartContract", + } + 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 update_smart_contract_without_preload_content( + self, + network_id: Annotated[StrictStr, Field(description="The ID of the network to fetch.")], + contract_address: Annotated[StrictStr, Field(description="EVM address of the smart contract (42 characters, including '0x', in lowercase)")], + update_smart_contract_request: Optional[UpdateSmartContractRequest] = None, + _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: + """Update a smart contract + + Update a smart contract + + :param network_id: The ID of the network to fetch. (required) + :type network_id: str + :param contract_address: EVM address of the smart contract (42 characters, including '0x', in lowercase) (required) + :type contract_address: str + :param update_smart_contract_request: + :type update_smart_contract_request: UpdateSmartContractRequest + :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._update_smart_contract_serialize( + network_id=network_id, + contract_address=contract_address, + update_smart_contract_request=update_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': "SmartContract", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _update_smart_contract_serialize( + self, + network_id, + contract_address, + update_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 + if network_id is not None: + _path_params['network_id'] = network_id + if contract_address is not None: + _path_params['contract_address'] = contract_address + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if update_smart_contract_request is not None: + _body_params = update_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', + 'session' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/v1/networks/{network_id}/smart_contracts/{contract_address}', + 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 + ) + + diff --git a/cdp/client/api/stake_api.py b/cdp/client/api/stake_api.py index f2790f3..e2b4059 100644 --- a/cdp/client/api/stake_api.py +++ b/cdp/client/api/stake_api.py @@ -302,6 +302,8 @@ def _build_staking_operation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -680,6 +682,8 @@ def _fetch_historical_staking_balances_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -987,6 +991,8 @@ def _fetch_staking_rewards_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1277,6 +1283,8 @@ def _get_external_staking_operation_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1550,6 +1558,8 @@ def _get_staking_context_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1840,6 +1850,8 @@ def _get_validator_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -2166,6 +2178,8 @@ def _list_validators_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/trades_api.py b/cdp/client/api/trades_api.py index 1e010cb..7a69a45 100644 --- a/cdp/client/api/trades_api.py +++ b/cdp/client/api/trades_api.py @@ -340,6 +340,7 @@ def _broadcast_trade_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -643,6 +644,7 @@ def _create_trade_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -933,6 +935,8 @@ def _get_trade_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1242,6 +1246,8 @@ def _list_trades_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/transaction_history_api.py b/cdp/client/api/transaction_history_api.py index 02b6ea7..6833c9f 100644 --- a/cdp/client/api/transaction_history_api.py +++ b/cdp/client/api/transaction_history_api.py @@ -328,6 +328,8 @@ def _list_address_transactions_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/transfers_api.py b/cdp/client/api/transfers_api.py index a7be92c..c4775dd 100644 --- a/cdp/client/api/transfers_api.py +++ b/cdp/client/api/transfers_api.py @@ -340,6 +340,7 @@ def _broadcast_transfer_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -643,6 +644,7 @@ def _create_transfer_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -933,6 +935,8 @@ def _get_transfer_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1242,6 +1246,8 @@ def _list_transfers_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/wallets_api.py b/cdp/client/api/wallets_api.py index 66eaa59..0a54674 100644 --- a/cdp/client/api/wallets_api.py +++ b/cdp/client/api/wallets_api.py @@ -296,6 +296,7 @@ def _create_wallet_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey' ] return self.api_client.param_serialize( @@ -556,6 +557,8 @@ def _get_wallet_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -831,6 +834,8 @@ def _get_wallet_balance_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1091,6 +1096,8 @@ def _list_wallet_balances_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1370,6 +1377,8 @@ def _list_wallets_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/api/webhooks_api.py b/cdp/client/api/webhooks_api.py index ba2844a..fcfbbf9 100644 --- a/cdp/client/api/webhooks_api.py +++ b/cdp/client/api/webhooks_api.py @@ -311,6 +311,8 @@ def _create_wallet_webhook_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -584,6 +586,8 @@ def _create_webhook_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -844,6 +848,8 @@ def _delete_webhook_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1123,6 +1129,8 @@ def _list_webhooks_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( @@ -1411,6 +1419,8 @@ def _update_webhook_serialize( # authentication setting _auth_settings: List[str] = [ + 'apiKey', + 'session' ] return self.api_client.param_serialize( diff --git a/cdp/client/configuration.py b/cdp/client/configuration.py index e995c53..8f7de86 100644 --- a/cdp/client/configuration.py +++ b/cdp/client/configuration.py @@ -59,6 +59,26 @@ class Configuration: in PEM format. :param retries: Number of retries for API requests. + :Example: + + API Key Authentication Example. + Given the following security scheme in the OpenAPI specification: + components: + securitySchemes: + cookieAuth: # name for the security scheme + type: apiKey + in: cookie + name: JSESSIONID # cookie name + + You can programmatically set the cookie: + +conf = cdp.client.Configuration( + api_key={'cookieAuth': 'abc123'} + api_key_prefix={'cookieAuth': 'JSESSIONID'} +) + + The following cookie will be added to the HTTP request: + Cookie: JSESSIONID abc123 """ _default = None @@ -373,6 +393,24 @@ def auth_settings(self): :return: The Auth Settings information dict. """ auth = {} + if 'apiKey' in self.api_key: + auth['apiKey'] = { + 'type': 'api_key', + 'in': 'header', + 'key': 'Jwt', + 'value': self.get_api_key_with_prefix( + 'apiKey', + ), + } + if 'session' in self.api_key: + auth['session'] = { + 'type': 'api_key', + 'in': 'header', + 'key': 'Jwt', + 'value': self.get_api_key_with_prefix( + 'session', + ), + } return auth def to_debug_report(self): diff --git a/cdp/client/models/__init__.py b/cdp/client/models/__init__.py index d10d6ed..9191c6a 100644 --- a/cdp/client/models/__init__.py +++ b/cdp/client/models/__init__.py @@ -14,14 +14,12 @@ # import models into model package -from cdp.client.models.abi import ABI from cdp.client.models.address import Address from cdp.client.models.address_balance_list import AddressBalanceList from cdp.client.models.address_historical_balance_list import AddressHistoricalBalanceList from cdp.client.models.address_list import AddressList from cdp.client.models.address_reputation import AddressReputation from cdp.client.models.address_reputation_metadata import AddressReputationMetadata -from cdp.client.models.address_risk import AddressRisk from cdp.client.models.address_transaction_list import AddressTransactionList from cdp.client.models.asset import Asset from cdp.client.models.balance import Balance @@ -80,6 +78,7 @@ from cdp.client.models.payload_signature import PayloadSignature from cdp.client.models.payload_signature_list import PayloadSignatureList from cdp.client.models.read_contract_request import ReadContractRequest +from cdp.client.models.register_smart_contract_request import RegisterSmartContractRequest from cdp.client.models.seed_creation_event import SeedCreationEvent from cdp.client.models.seed_creation_event_result import SeedCreationEventResult from cdp.client.models.server_signer import ServerSigner @@ -114,6 +113,7 @@ from cdp.client.models.transaction_type import TransactionType from cdp.client.models.transfer import Transfer from cdp.client.models.transfer_list import TransferList +from cdp.client.models.update_smart_contract_request import UpdateSmartContractRequest from cdp.client.models.update_webhook_request import UpdateWebhookRequest from cdp.client.models.user import User from cdp.client.models.validator import Validator diff --git a/cdp/client/models/address_reputation.py b/cdp/client/models/address_reputation.py index bbe381c..3c69e1e 100644 --- a/cdp/client/models/address_reputation.py +++ b/cdp/client/models/address_reputation.py @@ -27,9 +27,9 @@ class AddressReputation(BaseModel): """ The reputation score with metadata of a blockchain address. """ # noqa: E501 - reputation_score: StrictInt = Field(description="The reputation score of a wallet address which lie between 0 to 100.") + score: StrictInt = Field(description="The score of a wallet address, ranging from -100 to 100. A negative score indicates a bad reputation, while a positive score indicates a good reputation.") metadata: AddressReputationMetadata - __properties: ClassVar[List[str]] = ["reputation_score", "metadata"] + __properties: ClassVar[List[str]] = ["score", "metadata"] model_config = ConfigDict( populate_by_name=True, @@ -85,7 +85,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ - "reputation_score": obj.get("reputation_score"), + "score": obj.get("score"), "metadata": AddressReputationMetadata.from_dict(obj["metadata"]) if obj.get("metadata") is not None else None }) return _obj diff --git a/cdp/client/models/register_smart_contract_request.py b/cdp/client/models/register_smart_contract_request.py new file mode 100644 index 0000000..0095bd4 --- /dev/null +++ b/cdp/client/models/register_smart_contract_request.py @@ -0,0 +1,90 @@ +# 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_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self + +class RegisterSmartContractRequest(BaseModel): + """ + Smart Contract data to be registered + """ # noqa: E501 + abi: StrictStr = Field(description="ABI of the smart contract") + contract_name: Optional[Annotated[str, Field(strict=True, max_length=100)]] = Field(default=None, description="Name of the smart contract") + __properties: ClassVar[List[str]] = ["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 RegisterSmartContractRequest 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 RegisterSmartContractRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "abi": obj.get("abi"), + "contract_name": obj.get("contract_name") + }) + return _obj + + diff --git a/cdp/client/models/smart_contract.py b/cdp/client/models/smart_contract.py index 1d1a54f..3156064 100644 --- a/cdp/client/models/smart_contract.py +++ b/cdp/client/models/smart_contract.py @@ -17,8 +17,8 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from pydantic import BaseModel, ConfigDict, Field, StrictBool, 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 cdp.client.models.transaction import Transaction @@ -29,17 +29,18 @@ class SmartContract(BaseModel): """ Represents a smart contract on the blockchain """ # noqa: E501 - smart_contract_id: StrictStr = Field(description="The unique identifier of the smart contract") + smart_contract_id: StrictStr = Field(description="The unique identifier of the smart contract.") network_id: StrictStr = Field(description="The name of the blockchain network") - wallet_id: StrictStr = Field(description="The ID of the wallet that deployed the smart contract") + wallet_id: Optional[StrictStr] = Field(default=None, description="The ID of the wallet that deployed the smart contract. If this smart contract was deployed externally, this will be omitted.") contract_address: StrictStr = Field(description="The EVM address of the smart contract") contract_name: StrictStr = Field(description="The name of the smart contract") - deployer_address: StrictStr = Field(description="The EVM address of the account that deployed the smart contract") + deployer_address: Optional[StrictStr] = Field(default=None, description="The EVM address of the account that deployed the smart contract. If this smart contract was deployed externally, this will be omitted.") type: SmartContractType - options: SmartContractOptions + options: Optional[SmartContractOptions] = None abi: StrictStr = Field(description="The JSON-encoded ABI of the contract") - transaction: Transaction - __properties: ClassVar[List[str]] = ["smart_contract_id", "network_id", "wallet_id", "contract_address", "contract_name", "deployer_address", "type", "options", "abi", "transaction"] + 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"] model_config = ConfigDict( populate_by_name=True, @@ -107,7 +108,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "type": obj.get("type"), "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 + "transaction": Transaction.from_dict(obj["transaction"]) if obj.get("transaction") is not None else None, + "is_external": obj.get("is_external") }) return _obj diff --git a/cdp/client/models/transfer.py b/cdp/client/models/transfer.py index 2b6fb1a..3e4a58e 100644 --- a/cdp/client/models/transfer.py +++ b/cdp/client/models/transfer.py @@ -17,7 +17,7 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr from typing import Any, ClassVar, Dict, List, Optional from cdp.client.models.asset import Asset from cdp.client.models.sponsored_send import SponsoredSend @@ -34,7 +34,7 @@ class Transfer(BaseModel): address_id: StrictStr = Field(description="The onchain address of the sender") destination: StrictStr = Field(description="The onchain address of the recipient") amount: StrictStr = Field(description="The amount in the atomic units of the asset") - asset_id: StrictStr = Field(description="The ID of the asset being transferred") + asset_id: StrictStr = Field(description="The ID of the asset being transferred. Use `asset.asset_id` instead.") asset: Asset transfer_id: StrictStr = Field(description="The ID of the transfer") transaction: Optional[Transaction] = None @@ -42,20 +42,10 @@ class Transfer(BaseModel): unsigned_payload: Optional[StrictStr] = Field(default=None, description="The unsigned payload of the transfer. This is the payload that needs to be signed by the sender.") signed_payload: Optional[StrictStr] = Field(default=None, description="The signed payload of the transfer. This is the payload that has been signed by the sender.") transaction_hash: Optional[StrictStr] = Field(default=None, description="The hash of the transfer transaction") - status: Optional[StrictStr] = Field(default=None, description="The status of the transfer") + status: Optional[StrictStr] = None gasless: StrictBool = Field(description="Whether the transfer uses sponsored gas") __properties: ClassVar[List[str]] = ["network_id", "wallet_id", "address_id", "destination", "amount", "asset_id", "asset", "transfer_id", "transaction", "sponsored_send", "unsigned_payload", "signed_payload", "transaction_hash", "status", "gasless"] - @field_validator('status') - def status_validate_enum(cls, value): - """Validates the enum""" - if value is None: - return value - - if value not in set(['pending', 'broadcast', 'complete', 'failed']): - raise ValueError("must be one of enum values ('pending', 'broadcast', 'complete', 'failed')") - return value - model_config = ConfigDict( populate_by_name=True, validate_assignment=True, diff --git a/cdp/client/models/update_smart_contract_request.py b/cdp/client/models/update_smart_contract_request.py new file mode 100644 index 0000000..0337fb8 --- /dev/null +++ b/cdp/client/models/update_smart_contract_request.py @@ -0,0 +1,90 @@ +# 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_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self + +class UpdateSmartContractRequest(BaseModel): + """ + Smart Contract data to be updated + """ # noqa: E501 + abi: Optional[StrictStr] = Field(default=None, description="ABI of the smart contract") + contract_name: Optional[Annotated[str, Field(strict=True, max_length=100)]] = Field(default=None, description="Name of the smart contract") + __properties: ClassVar[List[str]] = ["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 UpdateSmartContractRequest 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 UpdateSmartContractRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "abi": obj.get("abi"), + "contract_name": obj.get("contract_name") + }) + return _obj + + diff --git a/cdp/smart_contract.py b/cdp/smart_contract.py index a1b919c..bd5689c 100644 --- a/cdp/smart_contract.py +++ b/cdp/smart_contract.py @@ -1,20 +1,24 @@ import json import time +from collections.abc import Iterator from enum import Enum from typing import Any from eth_account.signers.local import LocalAccount from cdp.cdp import Cdp +from cdp.client import SmartContractList from cdp.client.models.create_smart_contract_request import CreateSmartContractRequest from cdp.client.models.deploy_smart_contract_request import DeploySmartContractRequest from cdp.client.models.multi_token_contract_options import MultiTokenContractOptions from cdp.client.models.nft_contract_options import NFTContractOptions from cdp.client.models.read_contract_request import ReadContractRequest +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.token_contract_options import TokenContractOptions +from cdp.client.models.update_smart_contract_request import UpdateSmartContractRequest from cdp.transaction import Transaction @@ -27,6 +31,7 @@ class Type(Enum): ERC20 = "erc20" ERC721 = "erc721" ERC1155 = "erc1155" + CUSTOM = "custom" def __str__(self) -> str: """Return a string representation of the Type.""" @@ -112,7 +117,7 @@ def network_id(self) -> str: return self._model.network_id @property - def wallet_id(self) -> str: + def wallet_id(self) -> str | None: """Get the wallet ID that deployed the smart contract. Returns: @@ -132,7 +137,17 @@ def contract_address(self) -> str: return self._model.contract_address @property - def deployer_address(self) -> str: + def contract_name(self) -> str: + """Get the contract address of the smart contract. + + Returns: + The contract address. + + """ + return self._model.contract_name + + @property + def deployer_address(self) -> str | None: """Get the deployer address of the smart contract. Returns: @@ -141,6 +156,16 @@ def deployer_address(self) -> str: """ return self._model.deployer_address + @property + def is_external(self) -> bool: + """Get the contract address of the smart contract. + + Returns: + The contract address. + + """ + return self._model.is_external + @property def type(self) -> Type: """Get the type of the smart contract. @@ -155,7 +180,9 @@ def type(self) -> Type: return self.Type(self._model.type) @property - def options(self) -> TokenContractOptions | NFTContractOptions | MultiTokenContractOptions: + def options( + self, + ) -> TokenContractOptions | NFTContractOptions | MultiTokenContractOptions | None: """Get the options of the smart contract. Returns: @@ -165,6 +192,9 @@ def options(self) -> TokenContractOptions | NFTContractOptions | MultiTokenContr ValueError: If the smart contract type is unknown or if options are not set. """ + if self.is_external: + raise ValueError("SmartContract options cannot be returned for external SmartContract") + if self._model.options is None or self._model.options.actual_instance is None: raise ValueError("Smart contract options are not set") @@ -196,6 +226,8 @@ def transaction(self) -> Transaction | None: Transaction: The transaction. """ + if self.is_external: + return None if self._transaction is None and self._model.transaction is not None: self._update_transaction(self._model) return self._transaction @@ -213,6 +245,8 @@ def sign(self, key: LocalAccount) -> "SmartContract": ValueError: If the key is not a LocalAccount. """ + if self.is_external: + raise ValueError("Cannot sign an external SmartContract") if not isinstance(key, LocalAccount): raise ValueError("key must be a LocalAccount") @@ -229,6 +263,9 @@ def broadcast(self) -> "SmartContract": ValueError: If the smart contract deployment is not signed. """ + if self.is_external: + raise ValueError("Cannot broadcast an external SmartContract") + if not self.transaction.signed: raise ValueError("Cannot broadcast unsigned SmartContract deployment") @@ -252,6 +289,8 @@ def reload(self) -> "SmartContract": The updated SmartContract object. """ + if self.is_external: + raise ValueError("Cannot reload an external SmartContract") model = Cdp.api_clients.smart_contracts.get_smart_contract( wallet_id=self.wallet_id, address_id=self.deployer_address, @@ -275,6 +314,8 @@ def wait(self, interval_seconds: float = 0.2, timeout_seconds: float = 10) -> "S TimeoutError: If the smart contract deployment times out. """ + if self.is_external: + raise ValueError("Cannot wait for an external SmartContract") start_time = time.time() while self.transaction is not None and not self.transaction.terminal_state: self.reload() @@ -373,6 +414,105 @@ def read( ) return cls._convert_solidity_value(model) + @classmethod + def update( + cls, + contract_address: str, + network_id: str, + contract_name: str | None = None, + abi: list[dict] | None = None, + ) -> "SmartContract": + """Update an existing SmartContract. + + Args: + network_id: The ID of the network. + contract_name: The name of the smart contract. + contract_address: The address of the smart contract. + abi: The ABI of the smart contract. + + Returns: + The updated smart contract. + + """ + abi_json = None + + if abi: + abi_json = json.dumps(abi, separators=(",", ":")) + + update_smart_contract_request = UpdateSmartContractRequest( + abi=abi_json, + contract_name=contract_name, + ) + + model = Cdp.api_clients.smart_contracts.update_smart_contract( + contract_address=contract_address, + network_id=network_id, + update_smart_contract_request=update_smart_contract_request, + ) + + return cls(model) + + @classmethod + def register( + cls, + contract_address: str, + network_id: str, + abi: list[dict], + contract_name: str | None = None, + ) -> "SmartContract": + """Register a new SmartContract. + + Args: + network_id: The ID of the network. + contract_name: The name of the smart contract. + contract_address: The address of the smart contract. + abi: The ABI of the smart contract. + + Returns: + The registered smart contract. + + """ + abi_json = None + + if abi: + abi_json = json.dumps(abi, separators=(",", ":")) + + register_smart_contract_request = RegisterSmartContractRequest( + abi=abi_json, + contract_name=contract_name, + ) + + model = Cdp.api_clients.smart_contracts.register_smart_contract( + contract_address=contract_address, + network_id=network_id, + register_smart_contract_request=register_smart_contract_request, + ) + + return cls(model) + + @classmethod + def list(cls) -> Iterator["SmartContract"]: + """List smart contracts. + + Returns: + Iterator[SmartContract]: An iterator of smart contract objects. + + """ + while True: + page = None + + response: SmartContractList = Cdp.api_clients.smart_contracts.list_smart_contracts( + page=page + ) + + for smart_contract_model in response.data: + yield cls(smart_contract_model) + + if not response.has_more: + break + + page = response.next_page + @classmethod def _convert_solidity_value(cls, solidity_value: SolidityValue) -> Any: type_ = solidity_value.type diff --git a/cdp/wallet_address.py b/cdp/wallet_address.py index fe9a0c6..14865e4 100644 --- a/cdp/wallet_address.py +++ b/cdp/wallet_address.py @@ -201,7 +201,9 @@ def invoke_contract( normalized_amount = Decimal(amount) if amount else Decimal("0") if normalized_amount > 0.0 and not asset_id: - raise ValueError("Asset ID is required for contract invocation if an amount is provided") + raise ValueError( + "Asset ID is required for contract invocation if an amount is provided" + ) if amount and asset_id: self._ensure_sufficient_balance(normalized_amount, asset_id) diff --git a/tests/factories/smart_contract_factory.py b/tests/factories/smart_contract_factory.py index efa2bf6..77541ef 100644 --- a/tests/factories/smart_contract_factory.py +++ b/tests/factories/smart_contract_factory.py @@ -23,8 +23,29 @@ def _create_smart_contract_model(status="complete"): deployer_address="0xdeployeraddress", type="erc20", options=smart_contract_options, - abi='{"abi": "data"}', + abi='{"abi":"data"}', transaction=transaction_model_factory(status), + is_external=False, + ) + + return _create_smart_contract_model + + +@pytest.fixture +def external_smart_contract_factory(transaction_model_factory): + """Create and return a factory for creating SmartContractModel fixtures.""" + + def _create_smart_contract_model(status="complete"): + return SmartContract( + SmartContractModel( + smart_contract_id="test-contract-id", + network_id="base-sepolia", + contract_address="0xcontractaddress", + contract_name="TestContract", + type="custom", + abi='{"abi":"data"}', + is_external=True, + ) ) return _create_smart_contract_model diff --git a/tests/test_smart_contract.py b/tests/test_smart_contract.py index f07d5d4..cf2a03a 100644 --- a/tests/test_smart_contract.py +++ b/tests/test_smart_contract.py @@ -1,8 +1,11 @@ +import json from unittest.mock import ANY, Mock, call, patch import pytest +from cdp.client.models.register_smart_contract_request import RegisterSmartContractRequest from cdp.client.models.solidity_value import SolidityValue +from cdp.client.models.update_smart_contract_request import UpdateSmartContractRequest from cdp.smart_contract import SmartContract @@ -35,6 +38,24 @@ def test_smart_contract_properties(smart_contract_factory): assert smart_contract.transaction.transaction_hash == "0xtransactionhash" +def test_external_smart_contract_properties(external_smart_contract_factory): + """Test the properties of a SmartContract object.""" + smart_contract = external_smart_contract_factory() + assert smart_contract.smart_contract_id == "test-contract-id" + assert smart_contract.network_id == "base-sepolia" + assert smart_contract.contract_address == "0xcontractaddress" + assert smart_contract.type.value == SmartContract.Type.CUSTOM.value + assert smart_contract.abi == {"abi": "data"} + + assert not smart_contract.wallet_id + assert not smart_contract.deployer_address + assert not smart_contract.transaction + with pytest.raises( + ValueError, match="SmartContract options cannot be returned for external SmartContract" + ): + _ = smart_contract.options + + @patch("cdp.Cdp.api_clients") def test_create_smart_contract(mock_api_clients, smart_contract_factory): """Test the creation of a SmartContract object.""" @@ -84,6 +105,13 @@ def test_broadcast_unsigned_smart_contract(smart_contract_factory): smart_contract.broadcast() +def test_broadcast_external_smart_contract(external_smart_contract_factory): + """Test the broadcasting of an external SmartContract object.""" + smart_contract = external_smart_contract_factory() + with pytest.raises(ValueError, match="Cannot broadcast an external SmartContract"): + smart_contract.broadcast() + + @patch("cdp.Cdp.api_clients") def test_reload_smart_contract(mock_api_clients, smart_contract_factory): """Test the reloading of a SmartContract object.""" @@ -103,6 +131,13 @@ def test_reload_smart_contract(mock_api_clients, smart_contract_factory): assert smart_contract.transaction.status.value == "complete" +def test_reload_external_smart_contract(external_smart_contract_factory): + """Test the reloading of an external SmartContract object.""" + smart_contract = external_smart_contract_factory() + with pytest.raises(ValueError, match="Cannot reload an external SmartContract"): + smart_contract.reload() + + @patch("cdp.Cdp.api_clients") @patch("cdp.smart_contract.time.sleep") @patch("cdp.smart_contract.time.time") @@ -129,6 +164,13 @@ def test_wait_for_smart_contract(mock_time, mock_sleep, mock_api_clients, smart_ assert mock_time.call_count == 3 +def test_wait_external_smart_contract(external_smart_contract_factory): + """Test the waiting of an external SmartContract object.""" + smart_contract = external_smart_contract_factory() + with pytest.raises(ValueError, match="Cannot wait for an external SmartContract"): + smart_contract.wait() + + @patch("cdp.Cdp.api_clients") @patch("cdp.smart_contract.time.sleep") @patch("cdp.smart_contract.time.time") @@ -157,6 +199,13 @@ def test_sign_smart_contract_invalid_key(smart_contract_factory): smart_contract.sign("invalid_key") +def test_sign_external_smart_contract(external_smart_contract_factory): + """Test the signing of an external SmartContract object.""" + smart_contract = external_smart_contract_factory() + with pytest.raises(ValueError, match="Cannot sign an external SmartContract"): + smart_contract.sign("key") + + def test_smart_contract_str_representation(smart_contract_factory): """Test the string representation of a SmartContract object.""" smart_contract = smart_contract_factory() @@ -1626,3 +1675,79 @@ def test_read_pure_int8_without_abi(mock_api_clients): contract_address="0x1234567890123456789012345678901234567890", read_contract_request=ANY, ) + + +@patch("cdp.Cdp.api_clients") +def test_register_smart_contract(mock_api_clients, smart_contract_factory): + """Test the registration of a SmartContract object.""" + mock_register_contract = Mock() + expected_smart_contract = smart_contract_factory()._model + mock_register_contract.return_value = expected_smart_contract + mock_api_clients.smart_contracts.register_smart_contract = mock_register_contract + + contract_address = expected_smart_contract.contract_address + network_id = expected_smart_contract.network_id + contract_name = expected_smart_contract.contract_name + abi_json = json.loads(expected_smart_contract.abi) + abi = expected_smart_contract.abi + + smart_contract = SmartContract.register( + abi=abi_json, + contract_name=contract_name, + contract_address=contract_address, + network_id=network_id, + ) + + assert isinstance(smart_contract, SmartContract) + mock_register_contract.assert_called_once_with( + contract_address=contract_address, + network_id=network_id, + register_smart_contract_request=RegisterSmartContractRequest( + abi=abi, contract_name=contract_name + ), + ) + + _validate_smart_contract(smart_contract, expected_smart_contract) + + +@patch("cdp.Cdp.api_clients") +def test_update_smart_contract(mock_api_clients, smart_contract_factory, all_read_types_abi): + """Test the update of a SmartContract object.""" + mock_updated_contract = Mock() + mock_api_clients.smart_contracts.update_smart_contract = mock_updated_contract + contract_name = "test-contract-2" + abi = '{"abi":"data2"}' + abi_json = json.loads(abi) + + expected_smart_contract = smart_contract_factory()._model + expected_smart_contract.contract_name = contract_name + expected_smart_contract.abi = abi + mock_updated_contract.return_value = expected_smart_contract + contract_address = expected_smart_contract.contract_address + network_id = expected_smart_contract.network_id + + smart_contract = SmartContract.update( + abi=abi_json, + contract_name=contract_name, + contract_address=contract_address, + network_id=network_id, + ) + + assert isinstance(smart_contract, SmartContract) + mock_updated_contract.assert_called_once_with( + contract_address=contract_address, + network_id=network_id, + update_smart_contract_request=UpdateSmartContractRequest( + abi=abi, contract_name=contract_name + ), + ) + + _validate_smart_contract(smart_contract, expected_smart_contract) + + +def _validate_smart_contract(returned_smart_contract, expected_smart_contract): + assert returned_smart_contract.network_id == expected_smart_contract.network_id + assert returned_smart_contract.contract_address == expected_smart_contract.contract_address + assert returned_smart_contract.contract_name == expected_smart_contract.contract_name + assert returned_smart_contract.abi == json.loads(expected_smart_contract.abi) + assert returned_smart_contract.is_external == expected_smart_contract.is_external diff --git a/tests/test_wallet_address.py b/tests/test_wallet_address.py index 1f8376d..9b3e75c 100644 --- a/tests/test_wallet_address.py +++ b/tests/test_wallet_address.py @@ -480,7 +480,9 @@ def test_invoke_contract_with_invalid_input( """Test the invoke_contract method raises an error with invalid input.""" wallet_address_with_key = wallet_address_factory(key=True) - with pytest.raises(Exception, match="Asset ID is required for contract invocation if an amount is provided"): + with pytest.raises( + Exception, match="Asset ID is required for contract invocation if an amount is provided" + ): wallet_address_with_key.invoke_contract( contract_address="0xcontractaddress", method="testMethod", @@ -490,13 +492,11 @@ def test_invoke_contract_with_invalid_input( ) -@ patch("cdp.wallet_address.ContractInvocation") -@ patch("cdp.Cdp.api_clients") -@ patch("cdp.Cdp.use_server_signer", False) +@patch("cdp.wallet_address.ContractInvocation") +@patch("cdp.Cdp.api_clients") +@patch("cdp.Cdp.use_server_signer", False) def test_invoke_contract_api_error( mock_api_clients, mock_contract_invocation, wallet_address_factory, balance_model_factory - - ): """Test the invoke_contract method raises an error when the create API call fails.""" wallet_address_with_key = wallet_address_factory(key=True) From 7f4780dccdb8a60e70a6ca9e6d55fa28fa097eb0 Mon Sep 17 00:00:00 2001 From: Ryan Gilbert Date: Wed, 18 Dec 2024 16:41:27 -0500 Subject: [PATCH 2/6] chore: add network_id to WalletData (#66) --- CHANGELOG.md | 2 ++ README.md | 2 +- cdp/wallet.py | 3 ++- cdp/wallet_data.py | 27 +++++++++++++++++++++++---- tests/test_wallet.py | 19 +++++++++++++++++++ 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3e9c91..bac05be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add `network_id` to `WalletData` so that it is saved with the seed data and surfaced via the export function + ### [0.12.1] - 2024-12-10 ### Added diff --git a/README.md b/README.md index 00883c6..3514725 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ list(address.trades()) The SDK creates wallets with [Developer-Managed (1-1)](https://docs.cdp.coinbase.com/mpc-wallet/docs/wallets#developer-managed-wallets) keys by default, which means you are responsible for securely storing the keys required to re-instantiate wallets. The below code walks you through how to export a wallet and store it in a secure location. ```python -# Export the data required to re-instantiate the wallet. The data contains the seed and the ID of the wallet. +# Export the data required to re-instantiate the wallet. The data contains the seed, the ID of the wallet, and the network ID. data = wallet.export_data() ``` diff --git a/cdp/wallet.py b/cdp/wallet.py index 46958e0..9405770 100644 --- a/cdp/wallet.py +++ b/cdp/wallet.py @@ -492,7 +492,7 @@ def export_data(self) -> WalletData: if self._master is None or self._seed is None: raise ValueError("Wallet does not have seed loaded") - return WalletData(self.id, self._seed) + 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. @@ -530,6 +530,7 @@ def save_seed(self, file_path: str, encrypt: bool | None = False) -> None: "encrypted": encrypt, "auth_tag": auth_tag, "iv": iv, + "network_id": self.network_id, } with open(file_path, "w") as f: diff --git a/cdp/wallet_data.py b/cdp/wallet_data.py index dc4816e..65649da 100644 --- a/cdp/wallet_data.py +++ b/cdp/wallet_data.py @@ -1,16 +1,18 @@ class WalletData: """A class representing wallet data required to recreate a wallet.""" - def __init__(self, wallet_id: str, seed: str) -> None: + def __init__(self, wallet_id: str, seed: str, network_id: str | None = None) -> None: """Initialize the WalletData class. Args: wallet_id (str): The ID of the wallet. seed (str): The seed of the wallet. + network_id (str | None): The network ID of the wallet. Defaults to None. """ self._wallet_id = wallet_id self._seed = seed + self._network_id = network_id @property def wallet_id(self) -> str: @@ -32,6 +34,16 @@ def seed(self) -> str: """ return self._seed + @property + def network_id(self) -> str | None: + """Get the network ID of the wallet. + + Returns: + str: The network ID of the wallet. + + """ + return self._network_id + def to_dict(self) -> dict[str, str]: """Convert the wallet data to a dictionary. @@ -39,7 +51,10 @@ def to_dict(self) -> dict[str, str]: dict[str, str]: The dictionary representation of the wallet data. """ - return {"wallet_id": self.wallet_id, "seed": self.seed} + result = {"wallet_id": self.wallet_id, "seed": self.seed} + if self._network_id is not None: + result["network_id"] = self.network_id + return result @classmethod def from_dict(cls, data: dict[str, str]) -> "WalletData": @@ -52,7 +67,11 @@ def from_dict(cls, data: dict[str, str]) -> "WalletData": WalletData: The wallet data. """ - return cls(data["wallet_id"], data["seed"]) + return cls( + data["wallet_id"], + data["seed"], + data.get("network_id") + ) def __str__(self) -> str: """Return a string representation of the WalletData object. @@ -61,7 +80,7 @@ def __str__(self) -> str: str: A string representation of the wallet data. """ - return f"WalletData: (wallet_id: {self.wallet_id}, seed: {self.seed})" + return f"WalletData: (wallet_id: {self.wallet_id}, seed: {self.seed}, network_id: {self.network_id})" def __repr__(self) -> str: """Return a string representation of the WalletData object. diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 751d56b..6ed56af 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -658,3 +658,22 @@ def test_wallet_quote_fund_no_default_address(wallet_factory): with pytest.raises(ValueError, match="Default address does not exist"): wallet.quote_fund(amount="1.0", asset_id="eth") + +@patch("cdp.Cdp.use_server_signer", False) +@patch("cdp.wallet.os") +@patch("cdp.wallet.Bip32Slip10Secp256k1") +def test_wallet_export_data(mock_bip32, mock_os, wallet_factory, master_key_factory): + """Test Wallet export_data method.""" + seed = b"\x00" * 64 + mock_urandom = Mock(return_value=seed) + mock_os.urandom = mock_urandom + mock_from_seed = Mock(return_value=master_key_factory(seed)) + mock_bip32.FromSeed = mock_from_seed + + wallet = wallet_factory() + + exported = wallet.export_data() + + assert exported.wallet_id == wallet.id + assert exported.seed == seed.hex() + assert exported.network_id == wallet.network_id From 75d7bc46bb9b03372b02f369b83bf9a52fa84b96 Mon Sep 17 00:00:00 2001 From: jianlunz-cb Date: Wed, 18 Dec 2024 14:25:06 -0800 Subject: [PATCH 3/6] feat: Change update api to be an instance method (#67) --- CHANGELOG.md | 5 +++++ cdp/smart_contract.py | 14 +++++--------- tests/test_smart_contract.py | 5 ++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bac05be..b9905b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Added + +- Add support for registering, updating, and listing smart contracts that are +deployed external to CDP. + - Add `network_id` to `WalletData` so that it is saved with the seed data and surfaced via the export function ### [0.12.1] - 2024-12-10 diff --git a/cdp/smart_contract.py b/cdp/smart_contract.py index bd5689c..0b43462 100644 --- a/cdp/smart_contract.py +++ b/cdp/smart_contract.py @@ -414,20 +414,15 @@ def read( ) return cls._convert_solidity_value(model) - @classmethod def update( - cls, - contract_address: str, - network_id: str, + self, contract_name: str | None = None, abi: list[dict] | None = None, ) -> "SmartContract": """Update an existing SmartContract. Args: - network_id: The ID of the network. contract_name: The name of the smart contract. - contract_address: The address of the smart contract. abi: The ABI of the smart contract. Returns: @@ -445,12 +440,13 @@ def update( ) model = Cdp.api_clients.smart_contracts.update_smart_contract( - contract_address=contract_address, - network_id=network_id, + contract_address=self.contract_address, + network_id=self.network_id, update_smart_contract_request=update_smart_contract_request, ) - return cls(model) + self._model = model + return self @classmethod def register( diff --git a/tests/test_smart_contract.py b/tests/test_smart_contract.py index cf2a03a..2f2ea90 100644 --- a/tests/test_smart_contract.py +++ b/tests/test_smart_contract.py @@ -1719,6 +1719,7 @@ def test_update_smart_contract(mock_api_clients, smart_contract_factory, all_rea abi = '{"abi":"data2"}' abi_json = json.loads(abi) + existing_smart_contract = smart_contract_factory() expected_smart_contract = smart_contract_factory()._model expected_smart_contract.contract_name = contract_name expected_smart_contract.abi = abi @@ -1726,11 +1727,9 @@ def test_update_smart_contract(mock_api_clients, smart_contract_factory, all_rea contract_address = expected_smart_contract.contract_address network_id = expected_smart_contract.network_id - smart_contract = SmartContract.update( + smart_contract = existing_smart_contract.update( abi=abi_json, contract_name=contract_name, - contract_address=contract_address, - network_id=network_id, ) assert isinstance(smart_contract, SmartContract) From 02811fdba1972995ac4dbb55a1f829cc67e38858 Mon Sep 17 00:00:00 2001 From: arpitsrivastava-cb <137556158+arpitsrivastava-cb@users.noreply.github.com> Date: Thu, 19 Dec 2024 22:57:38 +0530 Subject: [PATCH 4/6] feat: Add reputation score (#64) * feat: Add reputation score * feat: lint issue * feat: Onboard reputation score for an address * feat: Add entry in changelog * fix: lint issue --- CHANGELOG.md | 2 + cdp/address.py | 18 +++++++ cdp/address_reputation.py | 38 ++++++++++++++ cdp/api_clients.py | 17 +++++++ tests/factories/address_reputation_factory.py | 50 +++++++++++++++++++ tests/test_address.py | 23 +++++++++ tests/test_address_reputation.py | 48 ++++++++++++++++++ 7 files changed, 196 insertions(+) create mode 100644 cdp/address_reputation.py create mode 100644 tests/factories/address_reputation_factory.py create mode 100644 tests/test_address_reputation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b9905b0..ab92b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # CDP Python SDK Changelog ## Unreleased +- Add support for fetching address reputation + - Add `reputation` method to `Address` to fetch the reputation of the address. ### Added diff --git a/cdp/address.py b/cdp/address.py index abe7002..c3e848f 100644 --- a/cdp/address.py +++ b/cdp/address.py @@ -1,6 +1,7 @@ from collections.abc import Iterator from decimal import Decimal +from cdp.address_reputation import AddressReputation from cdp.asset import Asset from cdp.balance import Balance from cdp.balance_map import BalanceMap @@ -23,6 +24,7 @@ def __init__(self, network_id: str, address_id: str) -> None: """ self._network_id = network_id self._id = address_id + self._reputation: AddressReputation | None = None @property def address_id(self) -> str: @@ -133,6 +135,22 @@ def transactions(self) -> Iterator[Transaction]: """ return Transaction.list(network_id=self.network_id, address_id=self.address_id) + def reputation(self) -> AddressReputation: + """Get the reputation of the address. + + Returns: + AddressReputation: The reputation of the address. + + """ + if self._reputation is not None: + return self._reputation + + response = Cdp.api_clients.reputation.get_address_reputation( + network_id=self.network_id, address_id=self.address_id + ) + self._reputation = AddressReputation(response) + return self._reputation + def __str__(self) -> str: """Return a string representation of the Address.""" return f"Address: (address_id: {self.address_id}, network_id: {self.network_id})" diff --git a/cdp/address_reputation.py b/cdp/address_reputation.py new file mode 100644 index 0000000..48d8a63 --- /dev/null +++ b/cdp/address_reputation.py @@ -0,0 +1,38 @@ +from cdp.client import AddressReputationMetadata +from cdp.client.models.address_reputation import AddressReputation as AddressReputationModel + + +class AddressReputation: + """A representation of the reputation of a blockchain address.""" + + def __init__(self, model: AddressReputationModel) -> None: + """Initialize the AddressReputation class.""" + if not model: + raise ValueError("model is required") + + self._score = model.score + self._metadata = model.metadata + + @property + def metadata(self) -> AddressReputationMetadata: + """Return the metadata of the address.""" + return self._metadata + + @property + def score(self) -> int: + """Return the score of the address.""" + return self._score + + @property + def risky(self) -> bool: + """Return whether the address is risky.""" + return self.score < 0 + + def __str__(self) -> str: + """Return a string representation of the AddressReputation.""" + metadata = ", ".join(f"{key}={getattr(self.metadata, key)}" for key in vars(self.metadata)) + return f"Address Reputation: (score={self.score}, metadata=({metadata}))" + + def __repr__(self) -> str: + """Return a string representation of the AddressReputation.""" + return str(self) diff --git a/cdp/api_clients.py b/cdp/api_clients.py index e60e17b..8b37942 100644 --- a/cdp/api_clients.py +++ b/cdp/api_clients.py @@ -1,4 +1,5 @@ from cdp.cdp_api_client import CdpApiClient +from cdp.client import ReputationApi from cdp.client.api.addresses_api import AddressesApi from cdp.client.api.assets_api import AssetsApi from cdp.client.api.balance_history_api import BalanceHistoryApi @@ -55,6 +56,7 @@ def __init__(self, cdp_client: CdpApiClient) -> None: self._balance_history: BalanceHistoryApi | None = None self._transaction_history: TransactionHistoryApi | None = None self._fund: FundApi | None = None + self._reputation: ReputationApi | None = None @property def wallets(self) -> WalletsApi: @@ -250,3 +252,18 @@ def fund(self) -> FundApi: if self._fund is None: self._fund = FundApi(api_client=self._cdp_client) return self._fund + + @property + def reputation(self) -> ReputationApi: + """Get the ReputationApi client instance. + + Returns: + ReputationApi: The ReputationApi client instance. + + Note: + This property lazily initializes the ReputationApi client on first access. + + """ + if self._reputation is None: + self._reputation = ReputationApi(api_client=self._cdp_client) + return self._reputation diff --git a/tests/factories/address_reputation_factory.py b/tests/factories/address_reputation_factory.py new file mode 100644 index 0000000..d06ecc1 --- /dev/null +++ b/tests/factories/address_reputation_factory.py @@ -0,0 +1,50 @@ +import pytest + +from cdp.address_reputation import AddressReputation +from cdp.client.models.address_reputation import AddressReputation as AddressReputationModel +from cdp.client.models.address_reputation_metadata import AddressReputationMetadata + + +@pytest.fixture +def address_reputation_model_factory(): + """Create and return a factory for creating AddressReputation fixtures.""" + + def _create_address_reputation_model( + score=1, + total_transactions=2, + unique_days_active=3, + longest_active_streak=4, + current_active_streak=5, + activity_period_days=6, + token_swaps_performed=7, + bridge_transactions_performed=8, + lend_borrow_stake_transactions=9, + ens_contract_interactions=10, + smart_contract_deployments=10, + ): + metadata = AddressReputationMetadata( + total_transactions=total_transactions, + unique_days_active=unique_days_active, + longest_active_streak=longest_active_streak, + current_active_streak=current_active_streak, + activity_period_days=activity_period_days, + token_swaps_performed=token_swaps_performed, + bridge_transactions_performed=bridge_transactions_performed, + lend_borrow_stake_transactions=lend_borrow_stake_transactions, + ens_contract_interactions=ens_contract_interactions, + smart_contract_deployments=smart_contract_deployments, + ) + return AddressReputationModel(score=score, metadata=metadata) + + return _create_address_reputation_model + + +@pytest.fixture +def address_reputation_factory(address_reputation_model_factory): + """Create and return a factory for creating AddressReputation fixtures.""" + + def _create_address_reputation(score=10): + reputation_model = address_reputation_model_factory(score=score) + return AddressReputation(reputation_model) + + return _create_address_reputation diff --git a/tests/test_address.py b/tests/test_address.py index 4c255cd..a247728 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -4,6 +4,7 @@ import pytest from cdp.address import Address +from cdp.address_reputation import AddressReputation from cdp.balance_map import BalanceMap from cdp.client.exceptions import ApiException from cdp.errors import ApiError @@ -240,6 +241,28 @@ def test_address_transactions_error(mock_api_clients, address_factory): next(transactions) +@patch("cdp.Cdp.api_clients") +def test_address_reputation(mock_api_clients, address_factory, address_reputation_factory): + """Test the reputation property of an Address.""" + address = address_factory() + address_reputation_data = address_reputation_factory(score=-10) + + mock_address_reputation = Mock() + mock_address_reputation.return_value = address_reputation_data + mock_api_clients.reputation.get_address_reputation = mock_address_reputation + + reputation = address.reputation() + + assert isinstance(reputation, AddressReputation) + + assert reputation.metadata.activity_period_days == 6 + assert reputation.risky is True + + mock_address_reputation.assert_called_once_with( + network_id=address.network_id, address_id=address.address_id + ) + + def test_address_str_representation(address_factory): """Test the str representation of an Address.""" address = address_factory() diff --git a/tests/test_address_reputation.py b/tests/test_address_reputation.py new file mode 100644 index 0000000..c57049a --- /dev/null +++ b/tests/test_address_reputation.py @@ -0,0 +1,48 @@ +from cdp.address_reputation import AddressReputation + + +def test_address_reputation_initialization(address_reputation_factory): + """Test address reputation initialization.""" + address_reputation = address_reputation_factory() + assert isinstance(address_reputation, AddressReputation) + + +def test_address_reputation_score(address_reputation_factory): + """Test address reputation score.""" + address_reputation = address_reputation_factory() + + assert address_reputation.score == 10 + + +def test_address_reputation_metadata(address_reputation_factory): + """Test address reputation metadata.""" + address_reputation = address_reputation_factory() + + assert address_reputation.metadata.total_transactions == 2 + assert address_reputation.metadata.unique_days_active == 3 + + +def test_address_reputation_risky(address_reputation_factory): + """Test address reputation risky.""" + address_reputation = address_reputation_factory(score=-5) + assert address_reputation.risky is True + + address_reputation = address_reputation_factory(score=0) + assert address_reputation.risky is False + + address_reputation = address_reputation_factory(score=5) + assert address_reputation.risky is False + + +def test_address_reputation_str(address_reputation_factory): + """Test address reputation str.""" + address_reputation = address_reputation_factory(score=10) + expected_str = "Address Reputation: (score=10, metadata=(total_transactions=2, unique_days_active=3, longest_active_streak=4, current_active_streak=5, activity_period_days=6, token_swaps_performed=7, bridge_transactions_performed=8, lend_borrow_stake_transactions=9, ens_contract_interactions=10, smart_contract_deployments=10))" + assert str(address_reputation) == expected_str + + +def test_address_reputation_repr(address_reputation_factory): + """Test address reputation repr.""" + address_reputation = address_reputation_factory(score=10) + expected_repr = "Address Reputation: (score=10, metadata=(total_transactions=2, unique_days_active=3, longest_active_streak=4, current_active_streak=5, activity_period_days=6, token_swaps_performed=7, bridge_transactions_performed=8, lend_borrow_stake_transactions=9, ens_contract_interactions=10, smart_contract_deployments=10))" + assert repr(address_reputation) == expected_repr From f1e9fbf8d2ea4a26ca4d6069d86c7fd6dd3156ee Mon Sep 17 00:00:00 2001 From: Derek Date: Thu, 19 Dec 2024 14:59:09 -0800 Subject: [PATCH 5/6] feat(PSDK-670): Support external wallet imports, wallet imports from CDP Python SDK (#68) * First draft of external wallet import logic * Minor wording update to recent changelog * Lint fixes * More lint fixes * Unit tests first draft * Unit test update * Unit test update, lint fix, import-related method name changes (see code review comments) --- CHANGELOG.md | 4 ++ cdp/__init__.py | 26 +++---- cdp/mnemonic_seed_phrase.py | 15 ++++ cdp/wallet.py | 137 ++++++++++++++++++++++++++++++++---- cdp/wallet_data.py | 76 +++++++++++++++++--- pyproject.toml | 5 ++ tests/test_wallet.py | 79 +++++++++++++++++++++ 7 files changed, 305 insertions(+), 37 deletions(-) create mode 100644 cdp/mnemonic_seed_phrase.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ab92b77..6c1d14a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ deployed external to CDP. - Add `network_id` to `WalletData` so that it is saved with the seed data and surfaced via the export function +- Add ability to import external wallets into CDP via a BIP-39 mnemonic phrase, as a 1-of-1 wallet +- Add ability to import WalletData files exported by the NodeJS CDP SDK +- Deprecate `Wallet.load_seed` method in favor of `Wallet.load_seed_from_file` +- Deprecate `Wallet.save_seed` method in favor of `Wallet.save_seed_to_file` ### [0.12.1] - 2024-12-10 diff --git a/cdp/__init__.py b/cdp/__init__.py index 1473e30..d0bb8a9 100644 --- a/cdp/__init__.py +++ b/cdp/__init__.py @@ -7,6 +7,7 @@ from cdp.contract_invocation import ContractInvocation from cdp.faucet_transaction import FaucetTransaction from cdp.hash_utils import hash_message, hash_typed_data_message +from cdp.mnemonic_seed_phrase import MnemonicSeedPhrase from cdp.payload_signature import PayloadSignature from cdp.smart_contract import SmartContract from cdp.sponsored_send import SponsoredSend @@ -19,24 +20,25 @@ from cdp.webhook import Webhook __all__ = [ - "__version__", - "Cdp", - "ContractInvocation", - "Wallet", - "WalletAddress", - "WalletData", - "Webhook", - "Asset", - "Transfer", "Address", - "Transaction", + "Asset", "Balance", "BalanceMap", + "Cdp", + "ContractInvocation", "FaucetTransaction", - "Trade", - "SponsoredSend", + "MnemonicSeedPhrase", "PayloadSignature", "SmartContract", + "SponsoredSend", + "Trade", + "Transaction", + "Transfer", + "Wallet", + "WalletAddress", + "WalletData", + "Webhook", + "__version__", "hash_message", "hash_typed_data_message", ] diff --git a/cdp/mnemonic_seed_phrase.py b/cdp/mnemonic_seed_phrase.py new file mode 100644 index 0000000..f48abab --- /dev/null +++ b/cdp/mnemonic_seed_phrase.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + + +@dataclass +class MnemonicSeedPhrase: + """Class representing a BIP-39mnemonic seed phrase. + + Used to import external wallets into CDP as 1-of-1 wallets. + + Args: + mnemonic_phrase (str): A valid BIP-39 mnemonic phrase (12, 15, 18, 21, or 24 words). + + """ + + mnemonic_phrase: str diff --git a/cdp/wallet.py b/cdp/wallet.py index 9405770..baf5f66 100644 --- a/cdp/wallet.py +++ b/cdp/wallet.py @@ -9,7 +9,7 @@ from typing import Any, Union import coincurve -from bip_utils import Bip32Slip10Secp256k1 +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 @@ -31,6 +31,7 @@ from cdp.faucet_transaction import FaucetTransaction from cdp.fund_operation import FundOperation from cdp.fund_quote import FundQuote +from cdp.mnemonic_seed_phrase import MnemonicSeedPhrase from cdp.payload_signature import PayloadSignature from cdp.smart_contract import SmartContract from cdp.trade import Trade @@ -118,7 +119,7 @@ def create( interval_seconds: float = 0.2, timeout_seconds: float = 20, ) -> "Wallet": - """Create a new wallet. + """Create a new wallet with a random seed. Args: network_id (str): The network ID of the wallet. Defaults to "base-sepolia". @@ -131,6 +132,36 @@ def create( Raises: Exception: If there's an error creating the wallet. + """ + return cls.create_with_seed( + seed=None, + network_id=network_id, + interval_seconds=interval_seconds, + timeout_seconds=timeout_seconds, + ) + + @classmethod + def create_with_seed( + cls, + seed: str | None = None, + network_id: str = "base-sepolia", + interval_seconds: float = 0.2, + timeout_seconds: float = 20, + ) -> "Wallet": + """Create a new wallet with the given seed. + + Args: + seed (str): The seed to use for the wallet. If None, a random seed will be generated. + network_id (str): The network ID of the wallet. Defaults to "base-sepolia". + interval_seconds (float): The interval between checks in seconds. Defaults to 0.2. + timeout_seconds (float): The maximum time to wait for the server signer to be active. Defaults to 20. + + Returns: + Wallet: The created wallet object. + + Raises: + Exception: If there's an error creating the wallet. + """ create_wallet_request = CreateWalletRequest( wallet=CreateWalletRequestWallet( @@ -139,7 +170,7 @@ def create( ) model = Cdp.api_clients.wallets.create_wallet(create_wallet_request) - wallet = cls(model) + wallet = cls(model, seed) if Cdp.use_server_signer: wallet._wait_for_signer(interval_seconds, timeout_seconds) @@ -228,29 +259,65 @@ def list(cls) -> Iterator["Wallet"]: page = response.next_page @classmethod - def import_data(cls, data: WalletData) -> "Wallet": - """Import a wallet from previously exported wallet data. + def import_wallet(cls, data: WalletData | MnemonicSeedPhrase) -> "Wallet": + """Import a wallet from previously exported wallet data or a mnemonic seed phrase. Args: - data (WalletData): The wallet data to import. + 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 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. """ - if not isinstance(data, WalletData): - raise ValueError("Data must be a WalletData instance") + if isinstance(data, MnemonicSeedPhrase): + # Validate mnemonic phrase + if not data.mnemonic_phrase: + raise ValueError("BIP-39 mnemonic seed phrase must be provided") - model = Cdp.api_clients.wallets.get_wallet(data.wallet_id) + # Validate the mnemonic using bip_utils + if not Bip39MnemonicValidator().IsValid(data.mnemonic_phrase): + raise ValueError("Invalid BIP-39 mnemonic seed phrase") - wallet = cls(model, data.seed) + # Convert mnemonic to seed + seed_bytes = Bip39SeedGenerator(data.mnemonic_phrase).Generate() + seed = seed_bytes.hex() - wallet._set_addresses() + # Create wallet using the provided seed + wallet = cls.create_with_seed(seed=seed) + wallet._set_addresses() + return wallet - return wallet + elif isinstance(data, WalletData): + model = Cdp.api_clients.wallets.get_wallet(data.wallet_id) + wallet = cls(model, data.seed) + wallet._set_addresses() + return wallet + + raise ValueError("Data must be a WalletData or MnemonicSeedPhrase instance") + + @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. + + """ + return cls.import_wallet(data) def create_address(self) -> "WalletAddress": """Create a new address for the wallet. @@ -495,6 +562,28 @@ def export_data(self) -> WalletData: 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. + + """ + import warnings + + warnings.warn( + "save_seed() is deprecated and will be removed in a future version. Use save_seed_to_file() instead.", + DeprecationWarning, + stacklevel=2, + ) + 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: @@ -537,6 +626,26 @@ def save_seed(self, file_path: str, encrypt: bool | None = False) -> None: 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. + + """ + import warnings + warnings.warn( + "load_seed() is deprecated and will be removed in a future version. Use load_seed_from_file() instead.", + DeprecationWarning, + stacklevel=2, + ) + 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: @@ -685,8 +794,8 @@ def _validate_seed(self, seed: bytes) -> None: ValueError: If the seed length is invalid. """ - if len(seed) != 64: - raise ValueError("Invalid seed length") + 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. diff --git a/cdp/wallet_data.py b/cdp/wallet_data.py index 65649da..4a4eaf7 100644 --- a/cdp/wallet_data.py +++ b/cdp/wallet_data.py @@ -24,6 +24,16 @@ def wallet_id(self) -> str: """ return self._wallet_id + @property + def walletId(self) -> str | None: + """Get the ID of the wallet (camelCase alias). + + Returns: + str | None: The ID of the wallet. + + """ + return self._wallet_id + @property def seed(self) -> str: """Get the seed of the wallet. @@ -39,22 +49,41 @@ def network_id(self) -> str | None: """Get the network ID of the wallet. Returns: - str: The network ID of the wallet. + str | None: The network ID of the wallet. + + """ + return self._network_id + + @property + def networkId(self) -> str | None: + """Get the network ID of the wallet (camelCase alias). + + Returns: + str | None: The network ID of the wallet. """ return self._network_id - def to_dict(self) -> dict[str, str]: + def to_dict(self, camel_case: bool = False) -> dict[str, str]: """Convert the wallet data to a dictionary. + Args: + camel_case (bool): Whether to use camelCase keys. Defaults to False. + Returns: dict[str, str]: The dictionary representation of the wallet data. """ - result = {"wallet_id": self.wallet_id, "seed": self.seed} - if self._network_id is not None: - result["network_id"] = self.network_id - return result + if camel_case: + result = {"walletId": self.walletId, "seed": self.seed} + if self._network_id is not None: + result["networkId"] = self.networkId + return result + else: + result = {"wallet_id": self.wallet_id, "seed": self.seed} + if self._network_id is not None: + result["network_id"] = self.network_id + return result @classmethod def from_dict(cls, data: dict[str, str]) -> "WalletData": @@ -62,16 +91,41 @@ def from_dict(cls, data: dict[str, str]) -> "WalletData": Args: data (dict[str, str]): The data to create the WalletData object from. + Must contain exactly one of ('wallet_id' or 'walletId'), and a seed. + May optionally contain exactly one of ('network_id' or 'networkId'). Returns: WalletData: The wallet data. + Raises: + ValueError: + - If both 'wallet_id' and 'walletId' are present, or if neither is present. + - If both 'network_id' and 'networkId' are present, or if neither is present. + """ - return cls( - data["wallet_id"], - data["seed"], - data.get("network_id") - ) + has_snake_case_wallet = "wallet_id" in data + has_camel_case_wallet = "walletId" in data + + if has_snake_case_wallet and has_camel_case_wallet: + raise ValueError("Data cannot contain both 'wallet_id' and 'walletId' keys") + + wallet_id = data.get("wallet_id") if has_snake_case_wallet else data.get("walletId") + if wallet_id is None: + raise ValueError("Data must contain either 'wallet_id' or 'walletId'") + + seed = data.get("seed") + if seed is None: + raise ValueError("Data must contain 'seed'") + + has_snake_case_network = "network_id" in data + has_camel_case_network = "networkId" in data + + if has_snake_case_network and has_camel_case_network: + raise ValueError("Data cannot contain both 'network_id' and 'networkId' keys") + + network_id = data.get("network_id") if has_snake_case_network else data.get("networkId") + + return cls(wallet_id, seed, network_id) def __str__(self) -> str: """Return a string representation of the WalletData object. diff --git a/pyproject.toml b/pyproject.toml index 0867de4..70dabd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,11 @@ exclude = ["./build/**", "./dist/**", "./docs/**", "./cdp/client/**"] select = ["E", "F", "I", "N", "W", "D", "UP", "B", "C4", "SIM", "RUF"] ignore = ["D213", "D203", "D100", "D104", "D107", "E501"] +[tool.ruff.lint.pep8-naming] +# Allow camelCase property names in WalletData import for cross-compatibility with Node.JS SDK +classmethod-decorators = ["classmethod"] +ignore-names = ["networkId", "walletId"] + [tool.ruff.format] quote-style = "double" indent-style = "space" diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 6ed56af..0e06d65 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -659,6 +659,7 @@ def test_wallet_quote_fund_no_default_address(wallet_factory): with pytest.raises(ValueError, match="Default address does not exist"): wallet.quote_fund(amount="1.0", asset_id="eth") + @patch("cdp.Cdp.use_server_signer", False) @patch("cdp.wallet.os") @patch("cdp.wallet.Bip32Slip10Secp256k1") @@ -677,3 +678,81 @@ def test_wallet_export_data(mock_bip32, mock_os, wallet_factory, master_key_fact assert exported.wallet_id == wallet.id assert exported.seed == seed.hex() assert exported.network_id == wallet.network_id + + +@patch("cdp.Cdp.use_server_signer", False) +@patch("cdp.Cdp.api_clients") +@patch("cdp.wallet.Account") +def test_wallet_import_from_mnemonic_seed_phrase( + mock_account, + mock_api_clients, + wallet_factory, + address_model_factory, +): + """Test importing a wallet from a mnemonic seed phrase.""" + # Valid 24-word mnemonic and expected address + valid_mnemonic = "crouch cereal notice one canyon kiss tape employ ghost column vanish despair eight razor laptop keen rally gaze riot regret assault jacket risk curve" + expected_address = "0x43A0477E658C6e05136e81C576CF02daCEa067bB" + public_key = "0x037e6cbdd1d949f60f41d5db7ffa9b3ddce0b77eab35ef7affd3f64cbfd9e33a91" + + # Create mock address model + mock_address = address_model_factory( + address_id=expected_address, + public_key=public_key, + wallet_id="new-wallet-id", + network_id="base-sepolia", + index=0, + ) + + # Create mock wallet model with the address model + mock_wallet = wallet_factory( + id="new-wallet-id", network_id="base-sepolia", default_address=mock_address + )._model + + # Add debug assertions + assert mock_wallet.default_address is not None + assert mock_wallet.default_address.address_id == expected_address + + # Mock Account.from_key to return an account with our expected address + mock_account_instance = Mock(spec=Account) + mock_account_instance.address = expected_address + mock_account.from_key = Mock(return_value=mock_account_instance) + + # Mock both API calls to return the same wallet model + mock_api_clients.wallets.create_wallet = Mock(return_value=mock_wallet) + mock_api_clients.wallets.get_wallet = Mock(return_value=mock_wallet) + mock_api_clients.addresses.create_address = Mock(return_value=mock_address) + + # Mock list_addresses call + mock_address_list = Mock() + mock_address_list.data = [mock_address] + mock_api_clients.addresses.list_addresses = Mock(return_value=mock_address_list) + + # Import wallet using mnemonic + from cdp.mnemonic_seed_phrase import MnemonicSeedPhrase + + wallet = Wallet.import_wallet(MnemonicSeedPhrase(valid_mnemonic)) + + # Verify the wallet was created successfully + assert isinstance(wallet, Wallet) + + # Verify the default address matches expected address + assert wallet.default_address is not None + assert wallet.default_address.address_id == expected_address + assert wallet.default_address._model.public_key == public_key + + +def test_wallet_import_from_mnemonic_empty_phrase(): + """Test importing a wallet with an empty mnemonic phrase.""" + from cdp.mnemonic_seed_phrase import MnemonicSeedPhrase + + with pytest.raises(ValueError, match="BIP-39 mnemonic seed phrase must be provided"): + Wallet.import_wallet(MnemonicSeedPhrase("")) + + +def test_wallet_import_from_mnemonic_invalid_phrase(): + """Test importing a wallet with an invalid mnemonic phrase.""" + from cdp.mnemonic_seed_phrase import MnemonicSeedPhrase + + with pytest.raises(ValueError, match="Invalid BIP-39 mnemonic seed phrase"): + Wallet.import_wallet(MnemonicSeedPhrase("invalid mnemonic phrase")) From a6cbe886abcf9e63d65cea556340b4c4845dc360 Mon Sep 17 00:00:00 2001 From: Derek Date: Thu, 19 Dec 2024 15:40:13 -0800 Subject: [PATCH 6/6] chore: Version bump to v0.13.0, changelog (#70) * Release 0.13.0 * Changelog formatting improvements --- CHANGELOG.md | 16 ++++++++-------- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c1d14a..5e91552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,34 +1,34 @@ # CDP Python SDK Changelog -## Unreleased -- Add support for fetching address reputation - - Add `reputation` method to `Address` to fetch the reputation of the address. +## [0.13.0] - 2024-12-19 ### Added - +- Add support for fetching address reputation + - Add `reputation` method to `Address` to fetch the reputation of the address. - Add support for registering, updating, and listing smart contracts that are deployed external to CDP. - - Add `network_id` to `WalletData` so that it is saved with the seed data and surfaced via the export function - Add ability to import external wallets into CDP via a BIP-39 mnemonic phrase, as a 1-of-1 wallet - Add ability to import WalletData files exported by the NodeJS CDP SDK + +### Deprecated - Deprecate `Wallet.load_seed` method in favor of `Wallet.load_seed_from_file` - Deprecate `Wallet.save_seed` method in favor of `Wallet.save_seed_to_file` -### [0.12.1] - 2024-12-10 +## [0.12.1] - 2024-12-10 ### Added - Wallet address contract invocation input validation for payable contracts. -### [0.12.0] - 2024-12-06 +## [0.12.0] - 2024-12-06 ### Added - Use Poetry as the dependency manager - Relax dependency version constraints -### [0.11.0] - 2024-11-27 +## [0.11.0] - 2024-11-27 ### Added diff --git a/pyproject.toml b/pyproject.toml index 70dabd2..602a614 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cdp-sdk" -version = "0.12.1" +version = "0.13.0" description = "CDP Python SDK" authors = ["John Peterson "] license = "LICENSE.md"