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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@ jobs:
virtualenvs-create: true
virtualenvs-in-project: true

- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: ./.venv
key: venv-${{ runner.os }}-${{ matrix.python }}-${{ hashFiles('poetry.lock') }}

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --with dev
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CDP Python SDK Changelog

## [0.18.0] - 2025-02-12

### Added

- Add `TransactionReceipt` and `TransactionLog` to contract invocation response.


## [0.17.0] - 2025-02-11

### Added
Expand Down
2 changes: 1 addition & 1 deletion cdp/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.16.0"
__version__ = "0.18.0"
2 changes: 2 additions & 0 deletions cdp/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@
from cdp.client.models.trade_list import TradeList
from cdp.client.models.transaction import Transaction
from cdp.client.models.transaction_content import TransactionContent
from cdp.client.models.transaction_log import TransactionLog
from cdp.client.models.transaction_receipt import TransactionReceipt
from cdp.client.models.transaction_type import TransactionType
from cdp.client.models.transfer import Transfer
from cdp.client.models.transfer_list import TransferList
Expand Down
2 changes: 2 additions & 0 deletions cdp/client/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@
from cdp.client.models.trade_list import TradeList
from cdp.client.models.transaction import Transaction
from cdp.client.models.transaction_content import TransactionContent
from cdp.client.models.transaction_log import TransactionLog
from cdp.client.models.transaction_receipt import TransactionReceipt
from cdp.client.models.transaction_type import TransactionType
from cdp.client.models.transfer import Transfer
from cdp.client.models.transfer_list import TransferList
Expand Down
10 changes: 8 additions & 2 deletions cdp/client/models/ethereum_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from cdp.client.models.ethereum_token_transfer import EthereumTokenTransfer
from cdp.client.models.ethereum_transaction_access_list import EthereumTransactionAccessList
from cdp.client.models.ethereum_transaction_flattened_trace import EthereumTransactionFlattenedTrace
from cdp.client.models.transaction_receipt import TransactionReceipt
from typing import Optional, Set
from typing_extensions import Self

Expand All @@ -49,7 +50,8 @@ class EthereumTransaction(BaseModel):
block_timestamp: Optional[datetime] = Field(default=None, description="The timestamp of the block in which the event was emitted")
mint: Optional[StrictStr] = Field(default=None, description="This is for handling optimism rollup specific EIP-2718 transaction type field.")
rlp_encoded_tx: Optional[StrictStr] = Field(default=None, description="RLP encoded transaction as a hex string (prefixed with 0x) for native compatibility with popular eth clients such as etherjs, viem etc.")
__properties: ClassVar[List[str]] = ["from", "gas", "gas_price", "hash", "input", "nonce", "to", "index", "value", "type", "max_fee_per_gas", "max_priority_fee_per_gas", "priority_fee_per_gas", "transaction_access_list", "token_transfers", "flattened_traces", "block_timestamp", "mint", "rlp_encoded_tx"]
receipt: Optional[TransactionReceipt] = None
__properties: ClassVar[List[str]] = ["from", "gas", "gas_price", "hash", "input", "nonce", "to", "index", "value", "type", "max_fee_per_gas", "max_priority_fee_per_gas", "priority_fee_per_gas", "transaction_access_list", "token_transfers", "flattened_traces", "block_timestamp", "mint", "rlp_encoded_tx", "receipt"]

model_config = ConfigDict(
populate_by_name=True,
Expand Down Expand Up @@ -107,6 +109,9 @@ def to_dict(self) -> Dict[str, Any]:
if _item_flattened_traces:
_items.append(_item_flattened_traces.to_dict())
_dict['flattened_traces'] = _items
# override the default output from pydantic by calling `to_dict()` of receipt
if self.receipt:
_dict['receipt'] = self.receipt.to_dict()
return _dict

@classmethod
Expand Down Expand Up @@ -137,7 +142,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"flattened_traces": [EthereumTransactionFlattenedTrace.from_dict(_item) for _item in obj["flattened_traces"]] if obj.get("flattened_traces") is not None else None,
"block_timestamp": obj.get("block_timestamp"),
"mint": obj.get("mint"),
"rlp_encoded_tx": obj.get("rlp_encoded_tx")
"rlp_encoded_tx": obj.get("rlp_encoded_tx"),
"receipt": TransactionReceipt.from_dict(obj["receipt"]) if obj.get("receipt") is not None else None
})
return _obj

Expand Down
10 changes: 7 additions & 3 deletions cdp/client/models/ethereum_validator_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import json

from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr
from typing import Any, ClassVar, Dict, List
from typing import Any, ClassVar, Dict, List, Optional
from cdp.client.models.balance import Balance
from typing import Optional, Set
from typing_extensions import Self
Expand All @@ -36,7 +36,9 @@ class EthereumValidatorMetadata(BaseModel):
withdrawable_epoch: StrictStr = Field(description="The epoch at which the validator can withdraw.", alias="withdrawableEpoch")
balance: Balance
effective_balance: Balance
__properties: ClassVar[List[str]] = ["index", "public_key", "withdrawal_address", "slashed", "activationEpoch", "exitEpoch", "withdrawableEpoch", "balance", "effective_balance"]
fee_recipient_address: StrictStr = Field(description="The address for execution layer rewards (MEV & tx fees). If using a reward splitter plan, this is a smart contract address that splits rewards based on defined commissions and send a portion to the forwarded_fee_recipient_address. ")
forwarded_fee_recipient_address: Optional[StrictStr] = Field(default=None, description="If using a reward splitter plan, this address receives a defined percentage of the total execution layer rewards. ")
__properties: ClassVar[List[str]] = ["index", "public_key", "withdrawal_address", "slashed", "activationEpoch", "exitEpoch", "withdrawableEpoch", "balance", "effective_balance", "fee_recipient_address", "forwarded_fee_recipient_address"]

model_config = ConfigDict(
populate_by_name=True,
Expand Down Expand Up @@ -103,7 +105,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"exitEpoch": obj.get("exitEpoch"),
"withdrawableEpoch": obj.get("withdrawableEpoch"),
"balance": Balance.from_dict(obj["balance"]) if obj.get("balance") is not None else None,
"effective_balance": Balance.from_dict(obj["effective_balance"]) if obj.get("effective_balance") is not None else None
"effective_balance": Balance.from_dict(obj["effective_balance"]) if obj.get("effective_balance") is not None else None,
"fee_recipient_address": obj.get("fee_recipient_address"),
"forwarded_fee_recipient_address": obj.get("forwarded_fee_recipient_address")
})
return _obj

Expand Down
91 changes: 91 additions & 0 deletions cdp/client/models/transaction_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# coding: utf-8

"""
Coinbase Platform API

This is the OpenAPI 3.0 specification for the Coinbase Platform APIs, used in conjunction with the Coinbase Platform SDKs.

The version of the OpenAPI document: 0.0.1-alpha
Generated by OpenAPI Generator (https://openapi-generator.tech)

Do not edit the class manually.
""" # noqa: E501


from __future__ import annotations
import pprint
import re # noqa: F401
import json

from pydantic import BaseModel, ConfigDict, Field, StrictStr
from typing import Any, ClassVar, Dict, List
from typing import Optional, Set
from typing_extensions import Self

class TransactionLog(BaseModel):
"""
A log emitted from an onchain transaction.
""" # noqa: E501
address: StrictStr = Field(description="An onchain address of a contract.")
topics: List[StrictStr]
data: StrictStr = Field(description="The data included in this log.")
__properties: ClassVar[List[str]] = ["address", "topics", "data"]

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 TransactionLog 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 TransactionLog from a dict"""
if obj is None:
return None

if not isinstance(obj, dict):
return cls.model_validate(obj)

_obj = cls.model_validate({
"address": obj.get("address"),
"topics": obj.get("topics"),
"data": obj.get("data")
})
return _obj


101 changes: 101 additions & 0 deletions cdp/client/models/transaction_receipt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# 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, StrictInt, StrictStr
from typing import Any, ClassVar, Dict, List
from cdp.client.models.transaction_log import TransactionLog
from typing import Optional, Set
from typing_extensions import Self

class TransactionReceipt(BaseModel):
"""
The receipt of an onchain transaction's execution.
""" # noqa: E501
status: StrictInt = Field(description="The status of a transaction is 1 if successful or 0 if it was reverted.")
logs: List[TransactionLog]
gas_used: StrictStr = Field(description="The amount of gas actually used by this transaction.")
effective_gas_price: StrictStr = Field(description="The effective gas price the transaction was charged at.")
__properties: ClassVar[List[str]] = ["status", "logs", "gas_used", "effective_gas_price"]

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 TransactionReceipt 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,
)
# override the default output from pydantic by calling `to_dict()` of each item in logs (list)
_items = []
if self.logs:
for _item_logs in self.logs:
if _item_logs:
_items.append(_item_logs.to_dict())
_dict['logs'] = _items
return _dict

@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of TransactionReceipt from a dict"""
if obj is None:
return None

if not isinstance(obj, dict):
return cls.model_validate(obj)

_obj = cls.model_validate({
"status": obj.get("status"),
"logs": [TransactionLog.from_dict(_item) for _item in obj["logs"]] if obj.get("logs") is not None else None,
"gas_used": obj.get("gas_used"),
"effective_gas_price": obj.get("effective_gas_price")
})
return _obj


9 changes: 4 additions & 5 deletions cdp/client/models/user_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,13 @@ class UserOperation(BaseModel):
calls: List[Call] = Field(description="The list of calls to make from the smart wallet.")
unsigned_payload: StrictStr = Field(description="The hex-encoded hash that must be signed by the user.")
signature: Optional[StrictStr] = Field(default=None, description="The hex-encoded signature of the user operation.")
status: Optional[StrictStr] = Field(default=None, description="The status of the user operation.")
__properties: ClassVar[List[str]] = ["id", "network_id", "calls", "unsigned_payload", "signature", "status"]
transaction_hash: Optional[StrictStr] = Field(default=None, description="The hash of the transaction that was broadcast.")
status: StrictStr = Field(description="The status of the user operation.")
__properties: ClassVar[List[str]] = ["id", "network_id", "calls", "unsigned_payload", "signature", "transaction_hash", "status"]

@field_validator('status')
def status_validate_enum(cls, value):
"""Validates the enum"""
if value is None:
return value

if value not in set(['pending', 'signed', 'broadcast', 'complete', 'failed']):
raise ValueError("must be one of enum values ('pending', 'signed', 'broadcast', 'complete', 'failed')")
return value
Expand Down Expand Up @@ -108,6 +106,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"calls": [Call.from_dict(_item) for _item in obj["calls"]] if obj.get("calls") is not None else None,
"unsigned_payload": obj.get("unsigned_payload"),
"signature": obj.get("signature"),
"transaction_hash": obj.get("transaction_hash"),
"status": obj.get("status")
})
return _obj
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

project = 'CDP SDK'
author = 'Coinbase Developer Platform'
release = '0.17.0'
release = '0.18.0'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "cdp-sdk"
version = "0.17.0"
version = "0.18.0"
description = "CDP Python SDK"
authors = ["John Peterson <[email protected]>"]
license = "LICENSE.md"
Expand Down
30 changes: 30 additions & 0 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,36 @@ def test_historical_balances(imported_wallet):
assert balances
assert all(balance.amount > 0 for balance in balances)

@pytest.mark.e2e
def test_invoke_contract_with_transaction_receipt(imported_wallet):
"""Test invoke contract with transaction receipt."""
destination_wallet = Wallet.create()

faucet_transaction = imported_wallet.faucet("usdc")
faucet_transaction.wait()

# Transfer 0.000001 USDC to the destination address.
invocation = imported_wallet.invoke_contract(
contract_address="0x036CbD53842c5426634e7929541eC2318f3dCF7e",
method="transfer",
args={"to": destination_wallet.default_address.address_id, "value": "1"}
)

invocation.wait()

transaction_content = invocation.transaction.content.actual_instance
transaction_receipt = transaction_content.receipt

assert transaction_receipt.status == 1

transaction_logs = transaction_receipt.logs
assert len(transaction_logs) == 1

transaction_log = transaction_logs[0]
assert transaction_log.address == "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
assert transaction_log.topics[0] == "Transfer"
assert transaction_log.topics[1] == f"from: {imported_wallet.default_address.address_id}"
assert transaction_log.topics[2] == f"to: {destination_wallet.default_address.address_id}"

@pytest.mark.skip(reason="Gasless transfers have unpredictable latency")
def test_gasless_transfer(imported_wallet):
Expand Down