Skip to content

Commit 5600d7b

Browse files
feat: transaction record query include duplicates (hiero-ledger#1671)
Signed-off-by: Siddhartha Ganguly <gangulysiddhartha22@gmail.com> Signed-off-by: gangulysiddhartha22-cmyk <gangulysiddhartha22@gmail.com>
1 parent 7c1b220 commit 5600d7b

7 files changed

Lines changed: 904 additions & 159 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
3232
- Format `tests/unit/custom_fee_test.py` with black for code style consistency. (#1525)
3333

3434
### Added
35+
- Added support for the `includeDuplicates` flag in `TransactionRecordQuery` and `duplicates` field in `TransactionRecord` (#1635)
3536
- Added logging in bot-gfi-assign-on-comment.js to prevent silent skips. (`#1668`)
3637
- Added `AssessedCustomFee` domain model to represent assessed custom fees. (`#1637`)
3738
- Add __repr__ method for ContractId class to improve debugging (#1714)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""
2+
Demonstrates behavior when submitting duplicate transactions and querying records.
3+
4+
Key points shown:
5+
- Submitting the same signed transaction multiple times usually fails with DUPLICATE_TRANSACTION (precheck)
6+
- No duplicate records are created for precheck-rejected submissions
7+
- TransactionRecordQuery with include_duplicates=True returns an empty duplicates list in normal cases
8+
- This is the expected behavior on mainnet/testnet — real duplicate records are rare
9+
(only occur with near-simultaneous consensus from multiple nodes)
10+
11+
Do NOT expect to see non-empty duplicates in this example — that's intentional.
12+
"""
13+
14+
import sys
15+
import time
16+
from hiero_sdk_python import Client
17+
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
18+
from hiero_sdk_python.crypto.private_key import PrivateKey
19+
from hiero_sdk_python.hbar import Hbar
20+
from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery
21+
from hiero_sdk_python.response_code import ResponseCode
22+
from hiero_sdk_python.exceptions import PrecheckError
23+
24+
25+
def main():
26+
try:
27+
_run()
28+
except Exception as e:
29+
print(f"Error: {e}", file=sys.stderr)
30+
sys.exit(1)
31+
32+
33+
def submit_duplicates(tx, client, count=3):
34+
"""Submit the same signed transaction multiple times — expect duplicates to fail precheck."""
35+
for i in range(1, count + 1):
36+
print(f"\nSubmitting attempt #{i} (same transaction bytes)...")
37+
try:
38+
receipt = tx.execute(client)
39+
status_name = ResponseCode(receipt.status).name
40+
print(f" → Unexpected success: {status_name}")
41+
print(" (This is rare — means the duplicate reached consensus before rejection)")
42+
except PrecheckError as e:
43+
if e.status == ResponseCode.DUPLICATE_TRANSACTION:
44+
print(" → DUPLICATE_TRANSACTION (expected — precheck rejection)")
45+
else:
46+
print(f" → Unexpected precheck error: {e.status.name}", file=sys.stderr)
47+
sys.exit(1)
48+
except Exception as e:
49+
print(f" → Other failure: {e}", file=sys.stderr)
50+
sys.exit(1)
51+
52+
53+
def print_record_info(record):
54+
"""Print summary of the main record and any duplicates (usually none)."""
55+
main_status = ResponseCode(record.receipt.status).name
56+
memo = record.transaction_memo or '(none)'
57+
58+
print("\nMain record:")
59+
print(f" Status : {main_status}")
60+
print(f" Memo : {memo}")
61+
print(f" Duplicates : {len(record.duplicates)}")
62+
63+
if record.duplicates:
64+
print("\nDuplicates (rare in normal operation):")
65+
for i, dup in enumerate(record.duplicates, 1):
66+
dup_status = ResponseCode(dup.receipt.status).name
67+
dup_memo = dup.transaction_memo or '(none)'
68+
print(f" #{i:2} | Status: {dup_status:18} | Memo: {dup_memo}")
69+
else:
70+
print(" (No duplicate records — this is normal when duplicates are rejected at precheck)")
71+
72+
73+
def _run():
74+
client = Client.from_env() # Expects OPERATOR_ID, OPERATOR_KEY, HEDERA_NETWORK in env
75+
76+
print("Creating a test transaction (AccountCreate)...")
77+
new_key = PrivateKey.generate_ed25519()
78+
79+
tx = (
80+
AccountCreateTransaction()
81+
.set_key_without_alias(new_key.public_key())
82+
.set_initial_balance(Hbar.from_tinybars(10_000_000))
83+
.set_transaction_memo("Duplicate demo — original")
84+
.freeze_with(client)
85+
.sign(client.operator_private_key)
86+
)
87+
88+
print("Submitting original transaction...")
89+
receipt = tx.execute(client)
90+
91+
if receipt.status != ResponseCode.SUCCESS:
92+
print(f"Original transaction failed: {ResponseCode(receipt.status).name}", file=sys.stderr)
93+
sys.exit(1)
94+
95+
tx_id = receipt.transaction_id
96+
print(f"Original Transaction ID: {tx_id}")
97+
98+
# Submit duplicates — almost always rejected at precheck
99+
submit_duplicates(tx, client, count=3)
100+
101+
print("\nWaiting briefly for record availability (mirror node propagation)...")
102+
time.sleep(5) # Usually enough on testnet; increase if needed
103+
104+
print("\nQuerying record with include_duplicates=True...")
105+
record = (
106+
TransactionRecordQuery()
107+
.set_transaction_id(tx_id)
108+
.set_include_duplicates(True)
109+
.execute(client)
110+
)
111+
112+
print_record_info(record)
113+
114+
print("\nConclusion:")
115+
print("• Duplicate submissions were rejected with DUPLICATE_TRANSACTION (precheck)")
116+
print("• No duplicate records were stored → duplicates list is empty")
117+
print("• This is the typical / expected outcome")
118+
print("• Real duplicate records only appear in rare race conditions (near-simultaneous consensus)")
119+
120+
121+
if __name__ == "__main__":
122+
main()

0 commit comments

Comments
 (0)