Skip to content

Commit 4c282ee

Browse files
authored
feat: Release 0.18.0 with transaction receipt on contract invocations (#103)
* chore: fixed casing error for assetids * new e2e test + backend updates (#101) * new e2e test + backend updates * add changes from openapi spec for transaction receipt * remove cache dependencies install for unit tests * feat: update changelog & bump version (#104) * update changelog * bumped up version
1 parent 55045b7 commit 4c282ee

13 files changed

+255
-20
lines changed

.github/workflows/unit_tests.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,6 @@ jobs:
2424
virtualenvs-create: true
2525
virtualenvs-in-project: true
2626

27-
- name: Load cached venv
28-
id: cached-poetry-dependencies
29-
uses: actions/cache@v3
30-
with:
31-
path: ./.venv
32-
key: venv-${{ runner.os }}-${{ matrix.python }}-${{ hashFiles('poetry.lock') }}
33-
3427
- name: Install dependencies
3528
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
3629
run: poetry install --with dev

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# CDP Python SDK Changelog
22

3+
## [0.18.0] - 2025-02-12
4+
5+
### Added
6+
7+
- Add `TransactionReceipt` and `TransactionLog` to contract invocation response.
8+
9+
310
## [0.17.0] - 2025-02-11
411

512
### Added

cdp/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.16.0"
1+
__version__ = "0.18.0"

cdp/client/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@
159159
from cdp.client.models.trade_list import TradeList
160160
from cdp.client.models.transaction import Transaction
161161
from cdp.client.models.transaction_content import TransactionContent
162+
from cdp.client.models.transaction_log import TransactionLog
163+
from cdp.client.models.transaction_receipt import TransactionReceipt
162164
from cdp.client.models.transaction_type import TransactionType
163165
from cdp.client.models.transfer import Transfer
164166
from cdp.client.models.transfer_list import TransferList

cdp/client/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122
from cdp.client.models.trade_list import TradeList
123123
from cdp.client.models.transaction import Transaction
124124
from cdp.client.models.transaction_content import TransactionContent
125+
from cdp.client.models.transaction_log import TransactionLog
126+
from cdp.client.models.transaction_receipt import TransactionReceipt
125127
from cdp.client.models.transaction_type import TransactionType
126128
from cdp.client.models.transfer import Transfer
127129
from cdp.client.models.transfer_list import TransferList

cdp/client/models/ethereum_transaction.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from cdp.client.models.ethereum_token_transfer import EthereumTokenTransfer
2424
from cdp.client.models.ethereum_transaction_access_list import EthereumTransactionAccessList
2525
from cdp.client.models.ethereum_transaction_flattened_trace import EthereumTransactionFlattenedTrace
26+
from cdp.client.models.transaction_receipt import TransactionReceipt
2627
from typing import Optional, Set
2728
from typing_extensions import Self
2829

@@ -49,7 +50,8 @@ class EthereumTransaction(BaseModel):
4950
block_timestamp: Optional[datetime] = Field(default=None, description="The timestamp of the block in which the event was emitted")
5051
mint: Optional[StrictStr] = Field(default=None, description="This is for handling optimism rollup specific EIP-2718 transaction type field.")
5152
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.")
52-
__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"]
53+
receipt: Optional[TransactionReceipt] = None
54+
__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"]
5355

5456
model_config = ConfigDict(
5557
populate_by_name=True,
@@ -107,6 +109,9 @@ def to_dict(self) -> Dict[str, Any]:
107109
if _item_flattened_traces:
108110
_items.append(_item_flattened_traces.to_dict())
109111
_dict['flattened_traces'] = _items
112+
# override the default output from pydantic by calling `to_dict()` of receipt
113+
if self.receipt:
114+
_dict['receipt'] = self.receipt.to_dict()
110115
return _dict
111116

112117
@classmethod
@@ -137,7 +142,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
137142
"flattened_traces": [EthereumTransactionFlattenedTrace.from_dict(_item) for _item in obj["flattened_traces"]] if obj.get("flattened_traces") is not None else None,
138143
"block_timestamp": obj.get("block_timestamp"),
139144
"mint": obj.get("mint"),
140-
"rlp_encoded_tx": obj.get("rlp_encoded_tx")
145+
"rlp_encoded_tx": obj.get("rlp_encoded_tx"),
146+
"receipt": TransactionReceipt.from_dict(obj["receipt"]) if obj.get("receipt") is not None else None
141147
})
142148
return _obj
143149

cdp/client/models/ethereum_validator_metadata.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import json
1919

2020
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr
21-
from typing import Any, ClassVar, Dict, List
21+
from typing import Any, ClassVar, Dict, List, Optional
2222
from cdp.client.models.balance import Balance
2323
from typing import Optional, Set
2424
from typing_extensions import Self
@@ -36,7 +36,9 @@ class EthereumValidatorMetadata(BaseModel):
3636
withdrawable_epoch: StrictStr = Field(description="The epoch at which the validator can withdraw.", alias="withdrawableEpoch")
3737
balance: Balance
3838
effective_balance: Balance
39-
__properties: ClassVar[List[str]] = ["index", "public_key", "withdrawal_address", "slashed", "activationEpoch", "exitEpoch", "withdrawableEpoch", "balance", "effective_balance"]
39+
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. ")
40+
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. ")
41+
__properties: ClassVar[List[str]] = ["index", "public_key", "withdrawal_address", "slashed", "activationEpoch", "exitEpoch", "withdrawableEpoch", "balance", "effective_balance", "fee_recipient_address", "forwarded_fee_recipient_address"]
4042

4143
model_config = ConfigDict(
4244
populate_by_name=True,
@@ -103,7 +105,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
103105
"exitEpoch": obj.get("exitEpoch"),
104106
"withdrawableEpoch": obj.get("withdrawableEpoch"),
105107
"balance": Balance.from_dict(obj["balance"]) if obj.get("balance") is not None else None,
106-
"effective_balance": Balance.from_dict(obj["effective_balance"]) if obj.get("effective_balance") is not None else None
108+
"effective_balance": Balance.from_dict(obj["effective_balance"]) if obj.get("effective_balance") is not None else None,
109+
"fee_recipient_address": obj.get("fee_recipient_address"),
110+
"forwarded_fee_recipient_address": obj.get("forwarded_fee_recipient_address")
107111
})
108112
return _obj
109113

cdp/client/models/transaction_log.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# coding: utf-8
2+
3+
"""
4+
Coinbase Platform API
5+
6+
This is the OpenAPI 3.0 specification for the Coinbase Platform APIs, used in conjunction with the Coinbase Platform SDKs.
7+
8+
The version of the OpenAPI document: 0.0.1-alpha
9+
Generated by OpenAPI Generator (https://openapi-generator.tech)
10+
11+
Do not edit the class manually.
12+
""" # noqa: E501
13+
14+
15+
from __future__ import annotations
16+
import pprint
17+
import re # noqa: F401
18+
import json
19+
20+
from pydantic import BaseModel, ConfigDict, Field, StrictStr
21+
from typing import Any, ClassVar, Dict, List
22+
from typing import Optional, Set
23+
from typing_extensions import Self
24+
25+
class TransactionLog(BaseModel):
26+
"""
27+
A log emitted from an onchain transaction.
28+
""" # noqa: E501
29+
address: StrictStr = Field(description="An onchain address of a contract.")
30+
topics: List[StrictStr]
31+
data: StrictStr = Field(description="The data included in this log.")
32+
__properties: ClassVar[List[str]] = ["address", "topics", "data"]
33+
34+
model_config = ConfigDict(
35+
populate_by_name=True,
36+
validate_assignment=True,
37+
protected_namespaces=(),
38+
)
39+
40+
41+
def to_str(self) -> str:
42+
"""Returns the string representation of the model using alias"""
43+
return pprint.pformat(self.model_dump(by_alias=True))
44+
45+
def to_json(self) -> str:
46+
"""Returns the JSON representation of the model using alias"""
47+
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
48+
return json.dumps(self.to_dict())
49+
50+
@classmethod
51+
def from_json(cls, json_str: str) -> Optional[Self]:
52+
"""Create an instance of TransactionLog from a JSON string"""
53+
return cls.from_dict(json.loads(json_str))
54+
55+
def to_dict(self) -> Dict[str, Any]:
56+
"""Return the dictionary representation of the model using alias.
57+
58+
This has the following differences from calling pydantic's
59+
`self.model_dump(by_alias=True)`:
60+
61+
* `None` is only added to the output dict for nullable fields that
62+
were set at model initialization. Other fields with value `None`
63+
are ignored.
64+
"""
65+
excluded_fields: Set[str] = set([
66+
])
67+
68+
_dict = self.model_dump(
69+
by_alias=True,
70+
exclude=excluded_fields,
71+
exclude_none=True,
72+
)
73+
return _dict
74+
75+
@classmethod
76+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
77+
"""Create an instance of TransactionLog from a dict"""
78+
if obj is None:
79+
return None
80+
81+
if not isinstance(obj, dict):
82+
return cls.model_validate(obj)
83+
84+
_obj = cls.model_validate({
85+
"address": obj.get("address"),
86+
"topics": obj.get("topics"),
87+
"data": obj.get("data")
88+
})
89+
return _obj
90+
91+
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# coding: utf-8
2+
3+
"""
4+
Coinbase Platform API
5+
6+
This is the OpenAPI 3.0 specification for the Coinbase Platform APIs, used in conjunction with the Coinbase Platform SDKs.
7+
8+
The version of the OpenAPI document: 0.0.1-alpha
9+
Generated by OpenAPI Generator (https://openapi-generator.tech)
10+
11+
Do not edit the class manually.
12+
""" # noqa: E501
13+
14+
15+
from __future__ import annotations
16+
import pprint
17+
import re # noqa: F401
18+
import json
19+
20+
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
21+
from typing import Any, ClassVar, Dict, List
22+
from cdp.client.models.transaction_log import TransactionLog
23+
from typing import Optional, Set
24+
from typing_extensions import Self
25+
26+
class TransactionReceipt(BaseModel):
27+
"""
28+
The receipt of an onchain transaction's execution.
29+
""" # noqa: E501
30+
status: StrictInt = Field(description="The status of a transaction is 1 if successful or 0 if it was reverted.")
31+
logs: List[TransactionLog]
32+
gas_used: StrictStr = Field(description="The amount of gas actually used by this transaction.")
33+
effective_gas_price: StrictStr = Field(description="The effective gas price the transaction was charged at.")
34+
__properties: ClassVar[List[str]] = ["status", "logs", "gas_used", "effective_gas_price"]
35+
36+
model_config = ConfigDict(
37+
populate_by_name=True,
38+
validate_assignment=True,
39+
protected_namespaces=(),
40+
)
41+
42+
43+
def to_str(self) -> str:
44+
"""Returns the string representation of the model using alias"""
45+
return pprint.pformat(self.model_dump(by_alias=True))
46+
47+
def to_json(self) -> str:
48+
"""Returns the JSON representation of the model using alias"""
49+
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
50+
return json.dumps(self.to_dict())
51+
52+
@classmethod
53+
def from_json(cls, json_str: str) -> Optional[Self]:
54+
"""Create an instance of TransactionReceipt from a JSON string"""
55+
return cls.from_dict(json.loads(json_str))
56+
57+
def to_dict(self) -> Dict[str, Any]:
58+
"""Return the dictionary representation of the model using alias.
59+
60+
This has the following differences from calling pydantic's
61+
`self.model_dump(by_alias=True)`:
62+
63+
* `None` is only added to the output dict for nullable fields that
64+
were set at model initialization. Other fields with value `None`
65+
are ignored.
66+
"""
67+
excluded_fields: Set[str] = set([
68+
])
69+
70+
_dict = self.model_dump(
71+
by_alias=True,
72+
exclude=excluded_fields,
73+
exclude_none=True,
74+
)
75+
# override the default output from pydantic by calling `to_dict()` of each item in logs (list)
76+
_items = []
77+
if self.logs:
78+
for _item_logs in self.logs:
79+
if _item_logs:
80+
_items.append(_item_logs.to_dict())
81+
_dict['logs'] = _items
82+
return _dict
83+
84+
@classmethod
85+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
86+
"""Create an instance of TransactionReceipt from a dict"""
87+
if obj is None:
88+
return None
89+
90+
if not isinstance(obj, dict):
91+
return cls.model_validate(obj)
92+
93+
_obj = cls.model_validate({
94+
"status": obj.get("status"),
95+
"logs": [TransactionLog.from_dict(_item) for _item in obj["logs"]] if obj.get("logs") is not None else None,
96+
"gas_used": obj.get("gas_used"),
97+
"effective_gas_price": obj.get("effective_gas_price")
98+
})
99+
return _obj
100+
101+

cdp/client/models/user_operation.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,13 @@ class UserOperation(BaseModel):
3232
calls: List[Call] = Field(description="The list of calls to make from the smart wallet.")
3333
unsigned_payload: StrictStr = Field(description="The hex-encoded hash that must be signed by the user.")
3434
signature: Optional[StrictStr] = Field(default=None, description="The hex-encoded signature of the user operation.")
35-
status: Optional[StrictStr] = Field(default=None, description="The status of the user operation.")
36-
__properties: ClassVar[List[str]] = ["id", "network_id", "calls", "unsigned_payload", "signature", "status"]
35+
transaction_hash: Optional[StrictStr] = Field(default=None, description="The hash of the transaction that was broadcast.")
36+
status: StrictStr = Field(description="The status of the user operation.")
37+
__properties: ClassVar[List[str]] = ["id", "network_id", "calls", "unsigned_payload", "signature", "transaction_hash", "status"]
3738

3839
@field_validator('status')
3940
def status_validate_enum(cls, value):
4041
"""Validates the enum"""
41-
if value is None:
42-
return value
43-
4442
if value not in set(['pending', 'signed', 'broadcast', 'complete', 'failed']):
4543
raise ValueError("must be one of enum values ('pending', 'signed', 'broadcast', 'complete', 'failed')")
4644
return value
@@ -108,6 +106,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
108106
"calls": [Call.from_dict(_item) for _item in obj["calls"]] if obj.get("calls") is not None else None,
109107
"unsigned_payload": obj.get("unsigned_payload"),
110108
"signature": obj.get("signature"),
109+
"transaction_hash": obj.get("transaction_hash"),
111110
"status": obj.get("status")
112111
})
113112
return _obj

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
project = 'CDP SDK'
1616
author = 'Coinbase Developer Platform'
17-
release = '0.17.0'
17+
release = '0.18.0'
1818

1919
# -- General configuration ---------------------------------------------------
2020
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "cdp-sdk"
3-
version = "0.17.0"
3+
version = "0.18.0"
44
description = "CDP Python SDK"
55
authors = ["John Peterson <[email protected]>"]
66
license = "LICENSE.md"

tests/test_e2e.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,36 @@ def test_historical_balances(imported_wallet):
171171
assert balances
172172
assert all(balance.amount > 0 for balance in balances)
173173

174+
@pytest.mark.e2e
175+
def test_invoke_contract_with_transaction_receipt(imported_wallet):
176+
"""Test invoke contract with transaction receipt."""
177+
destination_wallet = Wallet.create()
178+
179+
faucet_transaction = imported_wallet.faucet("usdc")
180+
faucet_transaction.wait()
181+
182+
# Transfer 0.000001 USDC to the destination address.
183+
invocation = imported_wallet.invoke_contract(
184+
contract_address="0x036CbD53842c5426634e7929541eC2318f3dCF7e",
185+
method="transfer",
186+
args={"to": destination_wallet.default_address.address_id, "value": "1"}
187+
)
188+
189+
invocation.wait()
190+
191+
transaction_content = invocation.transaction.content.actual_instance
192+
transaction_receipt = transaction_content.receipt
193+
194+
assert transaction_receipt.status == 1
195+
196+
transaction_logs = transaction_receipt.logs
197+
assert len(transaction_logs) == 1
198+
199+
transaction_log = transaction_logs[0]
200+
assert transaction_log.address == "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
201+
assert transaction_log.topics[0] == "Transfer"
202+
assert transaction_log.topics[1] == f"from: {imported_wallet.default_address.address_id}"
203+
assert transaction_log.topics[2] == f"to: {destination_wallet.default_address.address_id}"
174204

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

0 commit comments

Comments
 (0)