|
| 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