Skip to content

Commit 0e78b77

Browse files
feat: added the support for the include dups for tx receipt query (#1166) (#1186)
Signed-off-by: HusseinYasser <[email protected]>
1 parent 97c7b5f commit 0e78b77

File tree

6 files changed

+258
-15
lines changed

6 files changed

+258
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
6464
- Added `coding_token_transactions.md` for a high level overview training on how token transactions are created in the python sdk.
6565
- Added prompt for codeRabbit on how to review /examples ([#1180](https://github.com/hiero-ledger/hiero-sdk-python/issues/1180))
6666
- Add Linked Issue Enforcer to automatically close PRs without linked issues `.github/workflows/bot-linked-issue-enforcer.yml`.
67+
- Added support for include duplicates in get transaction receipt query (#1166)
6768

6869
### Changed
6970
- Refactored `account_create_transaction_evm_alias.py` to improve readability by splitting the monolithic function into smaller helper functions. [#1017](https://github.com/hiero-ledger/hiero-sdk-python/issues/1017)

examples/query/transaction_get_receipt_query.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,38 @@ def create_account(client, operator_key):
6262
sys.exit(1)
6363

6464

65-
def _print_receipt_with_children(queried_receipt):
65+
def _print_receipt_children(queried_receipt):
6666
"""Pretty-print receipt status and any child receipts."""
67-
print(f"✅ Queried transaction status: {ResponseCode(queried_receipt.status).name}")
6867

6968
children = queried_receipt.children
70-
print(f"Child receipts count: {len(children)}")
7169

7270
if not children:
7371
print("No child receipts returned (this can be normal depending on transaction type).")
7472
return
7573

74+
print(f"Child receipts count: {len(children)}")
75+
7676
print("Child receipts:")
7777
for idx, child in enumerate(children, start=1):
7878
print(f" {idx}. status={ResponseCode(child.status).name}")
7979

8080

81+
def _print_receipt_duplicates(queried_receipt):
82+
"""Pretty-print receipt status and any duplicate receipts."""
83+
84+
duplicates = queried_receipt.duplicates
85+
86+
if not duplicates:
87+
print("No duplicate receipts returned (this can be normal depending on transaction type).")
88+
return
89+
90+
print(f"Duplicate receipts count: {len(duplicates)}")
91+
92+
print("Duplicate receipts:")
93+
for idx, duplicate in enumerate(duplicates, start=1):
94+
print(f" {idx}. status={ResponseCode(duplicate.status).name}")
95+
96+
8197
def query_receipt():
8298
"""
8399
A full example that include account creation, Hbar transfer, and receipt querying.
@@ -109,14 +125,20 @@ def query_receipt():
109125

110126
# Query Transaction Receipt
111127
print("\nSTEP 3: Querying transaction receipt (include child receipts)...")
112-
receipt_query = TransactionGetReceiptQuery().set_transaction_id(transaction_id).set_include_children(True)
128+
receipt_query = (
129+
TransactionGetReceiptQuery()
130+
.set_transaction_id(transaction_id)
131+
.set_include_children(True)
132+
.set_include_duplicates(True)
133+
)
113134
queried_receipt = receipt_query.execute(client)
114135
print(
115136
f"✅ Success! Queried transaction status: {ResponseCode(queried_receipt.status).name}"
116137
)
117138

118139

119-
_print_receipt_with_children(queried_receipt)
140+
_print_receipt_children(queried_receipt)
141+
_print_receipt_duplicates(queried_receipt)
120142

121143
if __name__ == "__main__":
122144
query_receipt()

src/hiero_sdk_python/query/transaction_get_receipt_query.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import traceback
2-
from typing import Optional, Union
2+
from typing import List, Optional, Union
33

44
from hiero_sdk_python.channels import _Channel
55
from hiero_sdk_python.client.client import Client
66
from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError
77
from hiero_sdk_python.executable import _ExecutionState, _Method
8-
from hiero_sdk_python.hapi.services import query_header_pb2, query_pb2, response_pb2, transaction_get_receipt_pb2
8+
from hiero_sdk_python.hapi.services import query_header_pb2, query_pb2, response_pb2, transaction_get_receipt_pb2, transaction_receipt_pb2
99
from hiero_sdk_python.query.query import Query
1010
from hiero_sdk_python.response_code import ResponseCode
1111
from hiero_sdk_python.transaction.transaction_id import TransactionId
@@ -28,17 +28,21 @@ def __init__(
2828
self,
2929
transaction_id: Optional[TransactionId] = None,
3030
include_children: bool = False,
31+
include_duplicates: bool = False,
3132
) -> None:
3233
"""
3334
Initializes a new instance of the TransactionGetReceiptQuery class.
3435
3536
Args:
3637
transaction_id (TransactionId, optional): The ID of the transaction.
38+
include_children (bool): Whether to include child transaction receipts.
39+
include_duplicates (bool): Whether to include duplicate transaction receipts.
3740
"""
3841
super().__init__()
3942
self.transaction_id: Optional[TransactionId] = transaction_id
4043
self._frozen: bool = False
4144
self.include_children = include_children
45+
self.include_duplicates = include_duplicates
4246

4347
def _require_not_frozen(self) -> None:
4448
"""
@@ -86,6 +90,25 @@ def set_include_children(
8690
self.include_children = include_children
8791
return self
8892

93+
def set_include_duplicates(
94+
self, include_duplicates: bool
95+
) -> "TransactionGetReceiptQuery":
96+
"""
97+
Sets include_duplicates for which to retrieve the duplicate transaction receipts.
98+
99+
Args:
100+
include_duplicates: bool.
101+
102+
Returns:
103+
TransactionGetReceiptQuery: The current instance for method chaining.
104+
105+
Raises:
106+
ValueError: If the query is frozen and cannot be modified.
107+
"""
108+
self._require_not_frozen()
109+
self.include_duplicates = include_duplicates
110+
return self
111+
89112
def freeze(self) -> "TransactionGetReceiptQuery":
90113
"""
91114
Marks the query as frozen, preventing further modification.
@@ -125,6 +148,7 @@ def _make_request(self) -> query_pb2.Query:
125148
transaction_get_receipt.transactionID.CopyFrom(self.transaction_id._to_proto())
126149

127150
transaction_get_receipt.include_child_receipts = self.include_children
151+
transaction_get_receipt.includeDuplicates = self.include_duplicates
128152

129153
query = query_pb2.Query()
130154
if not hasattr(query, "transactionGetReceipt"):
@@ -223,6 +247,22 @@ def _map_status_error(self, response: response_pb2.Response) -> Union[PrecheckEr
223247
TransactionReceipt._from_proto(response.transactionGetReceipt.receipt, self.transaction_id),
224248
)
225249

250+
def _map_receipt_list(self, receipts: List[transaction_receipt_pb2.TransactionReceipt]) -> List["TransactionReceipt"]:
251+
"""
252+
Maps a list of protobuf transaction receipts to TransactionReceipt objects.
253+
254+
Args:
255+
receipts: A list of protobuf TransactionReceipt objects
256+
257+
Returns:
258+
A list of TransactionReceipt objects
259+
"""
260+
return [
261+
TransactionReceipt._from_proto(receipt_proto, self.transaction_id)
262+
for receipt_proto in receipts
263+
]
264+
265+
226266
def execute(self, client: Client) -> TransactionReceipt:
227267
"""
228268
Executes the transaction receipt query.
@@ -248,13 +288,18 @@ def execute(self, client: Client) -> TransactionReceipt:
248288
parent = TransactionReceipt._from_proto(response.transactionGetReceipt.receipt, self.transaction_id)
249289

250290
if self.include_children:
251-
children = []
252-
253-
for child_proto in response.transactionGetReceipt.child_transaction_receipts:
254-
child_receipt = TransactionReceipt._from_proto(child_proto, self.transaction_id)
255-
children.append(child_receipt)
291+
children = self._map_receipt_list(
292+
response.transactionGetReceipt.child_transaction_receipts
293+
)
256294

257295
parent._set_children(children)
296+
297+
if self.include_duplicates:
298+
duplicates = self._map_receipt_list(
299+
response.transactionGetReceipt.duplicateTransactionReceipts
300+
)
301+
302+
parent._set_duplicates(duplicates)
258303

259304
return parent
260305

src/hiero_sdk_python/transaction/transaction_receipt.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(
3939
receipt_proto: transaction_receipt_pb2.TransactionReceipt,
4040
transaction_id: Optional[TransactionId] = None,
4141
children: Optional[list["TransactionReceipt"]] = None,
42+
duplicates: Optional[list["TransactionReceipt"]] = None,
4243
) -> None:
4344
"""
4445
Initializes the TransactionReceipt with the provided protobuf receipt.
@@ -51,6 +52,7 @@ def __init__(
5152
self.status: Optional[response_code_pb2.ResponseCodeEnum] = receipt_proto.status
5253
self._receipt_proto: transaction_receipt_pb2.TransactionReceipt = receipt_proto
5354
self._children: list["TransactionReceipt"] = children or []
55+
self._duplicates: list["TransactionReceipt"] = duplicates or []
5456

5557
@property
5658
def token_id(self) -> Optional[TokenId]:
@@ -226,6 +228,25 @@ def _set_children(self, children: list["TransactionReceipt"]) -> None:
226228
"""
227229
self._children = children
228230

231+
@property
232+
def duplicates(self) -> list["TransactionReceipt"]:
233+
"""
234+
Returns the duplicate transaction receipts associated with this receipt.
235+
236+
Returns:
237+
list[TransactionReceipt]: Duplicate receipts (empty if not requested or none exist).
238+
"""
239+
return self._duplicates
240+
241+
def _set_duplicates(self, duplicates: list["TransactionReceipt"]) -> None:
242+
"""
243+
Internal setter for duplicate receipts (used by receipt queries).
244+
245+
Args:
246+
duplicates (list[TransactionReceipt]): Duplicate receipts.
247+
"""
248+
self._duplicates = duplicates
249+
229250
def _to_proto(self):
230251
"""
231252
Returns the underlying protobuf transaction receipt.

tests/integration/transaction_get_receipt_query_children_e2e_test.py renamed to tests/integration/transaction_get_receipt_query_e2e_test.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
"""
2-
E2E integration tests for TransactionGetReceiptQuery include_child_receipts support.
2+
E2E integration tests for TransactionGetReceiptQuery support.
33
44
These tests validate the full SDK flow against a real Hedera network:
55
- submit a real transaction
66
- query its receipt
77
- verify children behavior with and without include_children flag
8+
- verify duplicate transactions returned with include_duplicates flag
89
910
NOTE:
1011
The contract used in these tests (StatefulContract) does NOT deterministically
1112
produce child receipts, so we only assert API correctness and stability,
1213
not children count > 0.
1314
"""
1415

16+
from hiero_sdk_python.transaction.transaction_id import TransactionId
1517
import pytest
18+
import threading
1619

1720
from hiero_sdk_python.hbar import Hbar
1821
from hiero_sdk_python.query.transaction_get_receipt_query import TransactionGetReceiptQuery
@@ -47,7 +50,7 @@ def _extract_tx_id(tx, receipt):
4750
)
4851

4952

50-
def _submit_simple_transfer(env):
53+
def _submit_simple_transfer(env, node_ids=None, tx_id=None):
5154
"""
5255
Submit a simple transfer transaction and return its TransactionId.
5356
"""
@@ -58,7 +61,10 @@ def _submit_simple_transfer(env):
5861
.add_hbar_transfer(env.operator_id, Hbar(-0.01).to_tinybars())
5962
.add_hbar_transfer(receiver.id, Hbar(0.01).to_tinybars())
6063
)
61-
64+
if tx_id is not None:
65+
tx.set_transaction_id(tx_id)
66+
if node_ids is not None:
67+
tx.set_node_account_ids(node_ids)
6268
receipt = tx.execute(env.client)
6369
assert receipt.status == ResponseCode.SUCCESS
6470

@@ -182,3 +188,34 @@ def test_get_receipt_query_children_with_contract_execute_e2e(env):
182188
assert child.status is not None
183189

184190
assert isinstance(queried.children, list)
191+
192+
@pytest.mark.integration
193+
@pytest.mark.xfail(reason="Flaky test due to network conditions causing no duplicates to be created. Python Virtual Threads compete too quickly.")
194+
def test_get_receipt_query_include_duplicates_execute_e2e(env):
195+
"""
196+
E2E:
197+
Execute a real contract transaction and query its receipt with include_duplicates enabled.
198+
199+
We assert:
200+
- no crash
201+
- receipt.duplicates exists and is a list
202+
"""
203+
tx_id = TransactionId.generate(env.operator_id)
204+
nodes = env.client.network.nodes
205+
nodes = [nodes[0]._account_id, nodes[1]._account_id]
206+
tx1 = threading.Thread(target=_submit_simple_transfer, args=(env, [nodes[0]], tx_id))
207+
tx2 = threading.Thread(target=_submit_simple_transfer, args=(env, [nodes[1]], tx_id))
208+
tx1.start()
209+
tx2.start()
210+
tx1.join()
211+
tx2.join()
212+
queried = (
213+
TransactionGetReceiptQuery()
214+
.set_transaction_id(tx_id)
215+
.set_include_duplicates(True)
216+
.execute(env.client)
217+
)
218+
219+
assert queried.status == ResponseCode.SUCCESS
220+
assert isinstance(queried.duplicates, list)
221+
assert len(queried.duplicates) == 1

0 commit comments

Comments
 (0)