Skip to content

Commit 33b8730

Browse files
feat: Expose all missing protobuf fields in TransactionRecord (hiero-ledger#2008)
Signed-off-by: Siddhartha Ganguly <gangulysiddhartha22@gmail.com> Co-authored-by: Manish Dait <90558243+manishdait@users.noreply.github.com>
1 parent 12aefcf commit 33b8730

9 files changed

Lines changed: 1210 additions & 129 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Changelog
1+
# Changelog
22

33
All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](https://semver.org).
@@ -7,7 +7,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
77
## [Unreleased]
88

99
### Src
10-
-
10+
- Exposed all missing `TransactionRecord` protobuf fields `consensusTimestamp`, `scheduleRef`, `assessed_custom_fees`, `automatic_token_associations`, `parent_consensus_timestamp`, `alias`, `ethereum_hash`, `paid_staking_rewards`, `evm_address`, `contractCreateResult` with proper `None` handling, PRNG oneof handling with unset values return `None` instead of default values 0 / b"" (#1636)
1111

1212
### Tests
1313
- Refactor `mock_server` setup for network level TLS handling and added thread safety
@@ -38,6 +38,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
3838
- Fix the TransactionGetReceiptQuery to raise ReceiptStatusError for the non-retryable and non success receipt status
3939
- Refactor `AccountInfo` to use the existing `StakingInfo` wrapper class instead of flattened staking fields. Access is now via `info.staking_info.staked_account_id`, `info.staking_info.staked_node_id`, and `info.staking_info.decline_reward`. The old flat accessors (`info.staked_account_id`, `info.staked_node_id`, `info.decline_staking_reward`) are still available as deprecated properties and will emit a `DeprecationWarning`. (#1366)
4040
- Added abstract `Key` supper class to handle various proto Keys.
41+
42+
4143
### Examples
4244

4345
### Tests
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""
2+
Simple example: Exploring TransactionRecord fields.
3+
4+
This example creates a mock TransactionRecord with sample data and prints all fields
5+
in a readable format.
6+
7+
No network or client needed — just run the file!
8+
9+
Run:
10+
python examples/transaction/transaction_record.py
11+
"""
12+
13+
from collections import defaultdict
14+
15+
from hiero_sdk_python.account.account_id import AccountId
16+
from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult
17+
from hiero_sdk_python.contract.contract_id import ContractId
18+
from hiero_sdk_python.hapi.services import transaction_receipt_pb2
19+
from hiero_sdk_python.response_code import ResponseCode
20+
from hiero_sdk_python.schedule.schedule_id import ScheduleId
21+
from hiero_sdk_python.timestamp import Timestamp
22+
from hiero_sdk_python.tokens.assessed_custom_fee import AssessedCustomFee
23+
from hiero_sdk_python.tokens.token_association import TokenAssociation
24+
from hiero_sdk_python.tokens.token_id import TokenId
25+
from hiero_sdk_python.transaction.transaction_id import TransactionId
26+
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
27+
from hiero_sdk_python.transaction.transaction_record import TransactionRecord
28+
29+
30+
def create_mock_record():
31+
"""Create a mock TransactionRecord with sample values for all fields."""
32+
# Basic setup
33+
tx_id = TransactionId.from_string("0.0.1234@1698765432.000000000")
34+
35+
receipt_proto = transaction_receipt_pb2.TransactionReceipt()
36+
receipt_proto.status = ResponseCode.SUCCESS.value
37+
38+
receipt = TransactionReceipt(
39+
receipt_proto=receipt_proto,
40+
transaction_id=tx_id
41+
)
42+
43+
ts = Timestamp(seconds=1698765432, nanos=123456789)
44+
sched = ScheduleId(0, 0, 9999)
45+
46+
record = TransactionRecord(
47+
transaction_id=tx_id,
48+
transaction_hash=b'\x01\x02\x03\x04' * 12,
49+
transaction_memo="Hello from example!",
50+
transaction_fee=50000,
51+
receipt=receipt,
52+
token_transfers=defaultdict(lambda: defaultdict(int)),
53+
nft_transfers=defaultdict(list),
54+
transfers=defaultdict(int),
55+
new_pending_airdrops=[],
56+
prng_number=42,
57+
prng_bytes=None, # mutually exclusive with prng_number
58+
duplicates=[],
59+
children=[],
60+
consensus_timestamp=ts,
61+
schedule_ref=sched,
62+
assessed_custom_fees=[
63+
AssessedCustomFee(
64+
amount=1000000,
65+
fee_collector_account_id=AccountId(shard=0, realm=0, num=98),
66+
effective_payer_account_ids=[AccountId(shard=0, realm=0, num=100)]
67+
)
68+
],
69+
automatic_token_associations=[
70+
TokenAssociation(
71+
token_id=TokenId(shard=0, realm=0, num=5678),
72+
account_id=AccountId(shard=0, realm=0, num=1234)
73+
)
74+
],
75+
parent_consensus_timestamp=ts,
76+
alias=b'\x12\x34\x56\x78\x9a\xbc',
77+
ethereum_hash=b'\xab' * 32,
78+
paid_staking_rewards=[
79+
(AccountId(shard=0, realm=0, num=456), 500000),
80+
(AccountId(shard=0, realm=0, num=789), 250000)
81+
],
82+
evm_address=b'\xef' * 20,
83+
contract_create_result=ContractFunctionResult(
84+
contract_id=ContractId(shard=0, realm=0, contract=1000),
85+
contract_call_result=b"Contract created successfully!"
86+
),
87+
)
88+
89+
record.transfers[AccountId(shard=0, realm=0, num=100)] = -10000
90+
record.transfers[AccountId(shard=0, realm=0, num=200)] = 10000
91+
92+
return record
93+
94+
def _print_basic_fields(record):
95+
print("Basic:")
96+
print(f" Transaction ID: {record.transaction_id}")
97+
print(f" Memo: {record.transaction_memo}")
98+
print(f" Fee: {record.transaction_fee} tinybars")
99+
print(f" Hash: {record.transaction_hash.hex() if record.transaction_hash else 'None'}")
100+
if record.receipt:
101+
try:
102+
status = ResponseCode(record.receipt.status).name
103+
except ValueError:
104+
status = str(record.receipt.status)
105+
else:
106+
status = "None"
107+
print(f" Receipt Status: {status}")
108+
print(f" PRNG Number: {record.prng_number}")
109+
print(f" PRNG Bytes (hex): {record.prng_bytes.hex() if record.prng_bytes else 'None'}")
110+
111+
112+
def _print_transfer_fields(record):
113+
print(f" HBAR Transfers: {dict(record.transfers) if record.transfers else 'None'}")
114+
print(f" Token Transfers: {dict(record.token_transfers) if record.token_transfers else 'None'}")
115+
print(f" NFT Transfers: { {k: len(v) for k, v in record.nft_transfers.items()} if record.nft_transfers else 'None'}")
116+
print(f" Pending Airdrops: {len(record.new_pending_airdrops)}")
117+
118+
119+
def _print_new_fields(record):
120+
print(f" Consensus Timestamp: {record.consensus_timestamp}")
121+
print(f" Parent Consensus Timestamp: {record.parent_consensus_timestamp}")
122+
print(f" Schedule Ref: {record.schedule_ref}")
123+
print(f" Assessed Custom Fees ({len(record.assessed_custom_fees)}):")
124+
for fee in record.assessed_custom_fees:
125+
token = fee.token_id if fee.token_id else "HBAR"
126+
payers = ", ".join(str(p) for p in fee.effective_payer_account_ids) if fee.effective_payer_account_ids else "N/A"
127+
print(f" - {fee.amount} {token} → Collector: {fee.fee_collector_account_id}, Payers: {payers}")
128+
print(f" Automatic Token Associations ({len(record.automatic_token_associations)}):")
129+
for assoc in record.automatic_token_associations:
130+
print(f" - Token {assoc.token_id} → Account {assoc.account_id}")
131+
print(f" Alias (hex): {record.alias.hex() if record.alias else 'None'}")
132+
print(f" Ethereum Hash (hex): {record.ethereum_hash.hex() if record.ethereum_hash else 'None'}")
133+
print(f" Paid Staking Rewards ({len(record.paid_staking_rewards)}):")
134+
for account, amount in record.paid_staking_rewards:
135+
print(f" - {account}: {amount} tinybars")
136+
print(f" EVM Address (hex): {record.evm_address.hex() if record.evm_address else 'None'}")
137+
if record.contract_create_result:
138+
print(f" Contract Create Result: {record.contract_create_result.contract_id}")
139+
print(f" Result bytes (first 32): {record.contract_create_result.contract_call_result[:32].hex() if record.contract_create_result.contract_call_result else 'None'}...")
140+
else:
141+
print(" Contract Create Result: None")
142+
143+
144+
def print_all_fields(record):
145+
"""Print all fields of TransactionRecord in a simple, readable way."""
146+
print("=== TransactionRecord Example ===")
147+
_print_basic_fields(record)
148+
_print_transfer_fields(record)
149+
_print_new_fields(record)
150+
151+
def main():
152+
"""Run the TransactionRecord example."""
153+
print("Creating mock TransactionRecord...\n")
154+
record = create_mock_record()
155+
print_all_fields(record)
156+
157+
158+
if __name__ == "__main__":
159+
main()
160+

src/hiero_sdk_python/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from .tokens.token_pause_transaction import TokenPauseTransaction
5353
from .tokens.token_airdrop_claim import TokenClaimAirdropTransaction
5454
from .tokens.assessed_custom_fee import AssessedCustomFee
55+
from .tokens.token_association import TokenAssociation
5556

5657
# Transaction
5758
from .transaction.transaction import Transaction
@@ -193,6 +194,7 @@
193194
"TokenBurnTransaction",
194195
"TokenGrantKycTransaction",
195196
"TokenRelationship",
197+
"TokenAssociation",
196198
"TokenUpdateTransaction",
197199
"TokenAirdropTransaction",
198200
"TokenCancelAirdropTransaction",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Dataclass for automatic token associations in Hedera transaction records."""
2+
from __future__ import annotations
3+
4+
from dataclasses import dataclass
5+
6+
from hiero_sdk_python.account.account_id import AccountId
7+
from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenAssociation as TokenAssociationProto
8+
from hiero_sdk_python.tokens.token_id import TokenId
9+
10+
11+
@dataclass(frozen=True)
12+
class TokenAssociation:
13+
"""Represents an automatic token association between a token and an account.
14+
15+
This class appears in `TransactionRecord.automatic_token_associations` (repeated field)
16+
when the network creates associations automatically (e.g., during token transfers
17+
or airdrops to unassociated accounts).
18+
19+
These associations are informational only and cannot be used to create new associations
20+
on the ledger — use TokenAssociateTransaction for that.
21+
"""
22+
23+
token_id: TokenId | None = None
24+
"""The ID of the token that was automatically associated."""
25+
26+
account_id: AccountId | None = None
27+
"""The ID of the account that received the automatic association."""
28+
29+
@classmethod
30+
def _from_proto(cls, proto: TokenAssociationProto) -> TokenAssociation:
31+
"""Create a TokenAssociation instance from the protobuf message."""
32+
return cls(
33+
token_id=(
34+
TokenId._from_proto(proto.token_id)
35+
if proto.HasField("token_id")
36+
else None
37+
),
38+
account_id=(
39+
AccountId._from_proto(proto.account_id)
40+
if proto.HasField("account_id")
41+
else None
42+
),
43+
)
44+
45+
def _to_proto(self) -> TokenAssociationProto:
46+
"""Convert this TokenAssociation instance back to a protobuf message."""
47+
proto = TokenAssociationProto()
48+
49+
if self.token_id is not None:
50+
proto.token_id.CopyFrom(self.token_id._to_proto())
51+
52+
if self.account_id is not None:
53+
proto.account_id.CopyFrom(self.account_id._to_proto())
54+
55+
return proto
56+
57+
def to_bytes(self) -> bytes:
58+
"""Serialize this TokenAssociation to raw protobuf bytes."""
59+
return self._to_proto().SerializeToString()
60+
61+
@classmethod
62+
def from_bytes(cls, data: bytes) -> TokenAssociation:
63+
"""Deserialize a TokenAssociation from raw protobuf bytes."""
64+
proto = TokenAssociationProto()
65+
proto.ParseFromString(data)
66+
return cls._from_proto(proto)
67+
68+
def __repr__(self) -> str:
69+
"""Returns an unambiguous string representation for debugging."""
70+
return f"TokenAssociation(token_id={self.token_id!r}, account_id={self.account_id!r})"
71+
72+
def __str__(self) -> str:
73+
"""Returns a human-readable string representation."""
74+
return self.__repr__()
75+

0 commit comments

Comments
 (0)