feat: implement Transaction.from_bytes() deserialization for all transaction types#2438
feat: implement Transaction.from_bytes() deserialization for all transaction types#2438Mounil2005 wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes Transaction.from_bytes() round-trip behavior by adding _from_protobuf deserialization support across many transaction subclasses and by correcting/improving the transaction-type dispatch map used to select the appropriate concrete transaction class.
Changes:
- Adds
_from_protobufimplementations across account, token, consensus, file, schedule, contract, system, node, and PRNG transaction types to properly hydrate transaction-specific fields from protobuf. - Reworks the transaction type dispatch table into a module-level
_TRANSACTION_TYPE_MAP, fixing several keys/module paths and adding missing entries. - Improves protobuf field handling in a few places (e.g., memo defaults, composite-key handling via
Key.from_proto_key, and topic fee parsing helpers).
Reviewed changes
Copilot reviewed 48 out of 48 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/hiero_sdk_python/transaction/transaction.py | Moves/expands transaction type dispatch into a module-level map and adjusts base-body memo handling. |
| src/hiero_sdk_python/tokens/token_wipe_transaction.py | Implements _from_protobuf for TokenWipeTransaction field hydration. |
| src/hiero_sdk_python/tokens/token_update_transaction.py | Implements _from_protobuf for TokenUpdateTransaction (including keys/memo/metadata). |
| src/hiero_sdk_python/tokens/token_update_nfts_transaction.py | Implements _from_protobuf for TokenUpdateNftsTransaction. |
| src/hiero_sdk_python/tokens/token_unpause_transaction.py | Implements _from_protobuf for TokenUnpauseTransaction (snake_case proto field). |
| src/hiero_sdk_python/tokens/token_unfreeze_transaction.py | Implements _from_protobuf for TokenUnfreezeTransaction. |
| src/hiero_sdk_python/tokens/token_revoke_kyc_transaction.py | Implements _from_protobuf for TokenRevokeKycTransaction. |
| src/hiero_sdk_python/tokens/token_reject_transaction.py | Implements _from_protobuf for TokenRejectTransaction (fungible + NFT rejection parsing). |
| src/hiero_sdk_python/tokens/token_pause_transaction.py | Implements _from_protobuf for TokenPauseTransaction (snake_case proto field). |
| src/hiero_sdk_python/tokens/token_mint_transaction.py | Implements _from_protobuf for TokenMintTransaction (amount + metadata). |
| src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py | Implements _from_protobuf for TokenGrantKycTransaction. |
| src/hiero_sdk_python/tokens/token_freeze_transaction.py | Implements _from_protobuf for TokenFreezeTransaction. |
| src/hiero_sdk_python/tokens/token_fee_schedule_update_transaction.py | Implements _from_protobuf for TokenFeeScheduleUpdateTransaction. |
| src/hiero_sdk_python/tokens/token_dissociate_transaction.py | Implements _from_protobuf for TokenDissociateTransaction. |
| src/hiero_sdk_python/tokens/token_delete_transaction.py | Implements _from_protobuf for TokenDeleteTransaction. |
| src/hiero_sdk_python/tokens/token_create_transaction.py | Implements _from_protobuf for TokenCreateTransaction, hydrating token params and keys. |
| src/hiero_sdk_python/tokens/token_burn_transaction.py | Implements _from_protobuf for TokenBurnTransaction (amount + serials). |
| src/hiero_sdk_python/tokens/token_associate_transaction.py | Implements _from_protobuf for TokenAssociateTransaction. |
| src/hiero_sdk_python/tokens/token_airdrop_transaction.py | Adds airdrop transfer-list validation and implements _from_protobuf for transfers/NFT transfers. |
| src/hiero_sdk_python/tokens/token_airdrop_transaction_cancel.py | Implements _from_protobuf for TokenCancelAirdropTransaction pending-airdrop IDs. |
| src/hiero_sdk_python/tokens/token_airdrop_claim.py | Implements _from_protobuf for TokenClaimAirdropTransaction pending-airdrop IDs + validation. |
| src/hiero_sdk_python/tokens/custom_fixed_fee.py | Tightens fixed-fee proto validation and adds a topic-fee proto helper constructor. |
| src/hiero_sdk_python/system/freeze_transaction.py | Implements _from_protobuf for FreezeTransaction fields. |
| src/hiero_sdk_python/schedule/schedule_sign_transaction.py | Implements _from_protobuf for ScheduleSignTransaction. |
| src/hiero_sdk_python/schedule/schedule_delete_transaction.py | Implements _from_protobuf for ScheduleDeleteTransaction. |
| src/hiero_sdk_python/schedule/schedule_create_transaction.py | Implements _from_protobuf for ScheduleCreateTransaction (payer/admin/schedulable body fields). |
| src/hiero_sdk_python/prng_transaction.py | Implements _from_protobuf for PrngTransaction. |
| src/hiero_sdk_python/nodes/node_update_transaction.py | Switches admin key parsing to Key.from_proto_key for composite-key support. |
| src/hiero_sdk_python/nodes/node_delete_transaction.py | Implements _from_protobuf for NodeDeleteTransaction. |
| src/hiero_sdk_python/nodes/node_create_transaction.py | Switches admin key parsing to Key.from_proto_key for composite-key support. |
| src/hiero_sdk_python/file/file_update_transaction.py | Implements _from_protobuf for FileUpdateTransaction (file ID, keys, contents, memo, expiry). |
| src/hiero_sdk_python/file/file_delete_transaction.py | Implements _from_protobuf for FileDeleteTransaction. |
| src/hiero_sdk_python/file/file_create_transaction.py | Implements _from_protobuf for FileCreateTransaction via existing _from_proto. |
| src/hiero_sdk_python/file/file_append_transaction.py | Implements _from_protobuf for FileAppendTransaction and improves _from_proto field guards. |
| src/hiero_sdk_python/contract/ethereum_transaction.py | Implements _from_protobuf for EthereumTransaction fields. |
| src/hiero_sdk_python/contract/contract_update_transaction.py | Implements _from_protobuf for ContractUpdateTransaction (memo oneof, keys, staking fields). |
| src/hiero_sdk_python/contract/contract_execute_transaction.py | Implements _from_protobuf for ContractExecuteTransaction (contract ID, gas, amount, params). |
| src/hiero_sdk_python/contract/contract_delete_transaction.py | Implements _from_protobuf for ContractDeleteTransaction (obtainers oneof). |
| src/hiero_sdk_python/contract/contract_create_transaction.py | Implements _from_protobuf for ContractCreateTransaction (admin key, initcode source, staking). |
| src/hiero_sdk_python/consensus/topic_update_transaction.py | Adjusts defaults and implements _from_protobuf including topic fee helpers and key lists. |
| src/hiero_sdk_python/consensus/topic_message_submit_transaction.py | Improves docs and implements _from_protobuf for topic message submissions. |
| src/hiero_sdk_python/consensus/topic_delete_transaction.py | Implements _from_protobuf for TopicDeleteTransaction. |
| src/hiero_sdk_python/consensus/topic_create_transaction.py | Implements _from_protobuf for TopicCreateTransaction including fee parsing. |
| src/hiero_sdk_python/account/account_update_transaction.py | Implements _from_protobuf for AccountUpdateTransaction (keys, staking fields, memo wrapper). |
| src/hiero_sdk_python/account/account_delete_transaction.py | Implements _from_protobuf for AccountDeleteTransaction (account + transfer IDs). |
| src/hiero_sdk_python/account/account_create_transaction.py | Makes autoRenewPeriod optional in proto build and implements _from_protobuf for account creation fields. |
| src/hiero_sdk_python/account/account_allowance_delete_transaction.py | Implements _from_protobuf for AccountAllowanceDeleteTransaction NFT allowance wipes. |
| src/hiero_sdk_python/account/account_allowance_approve_transaction.py | Implements _from_protobuf for AccountAllowanceApproveTransaction allowance lists. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Raises: | ||
| ValueError: If required fields (message) are missing. | ||
| ValueError: If required fields (topic_id, message) are missing. | ||
| """ | ||
| if self.message is None or self.message == "": | ||
| raise ValueError("Missing required fields: message.") |
| # and ensures that the correct signatures are used when submitting transactions | ||
| self._signature_map: dict[bytes, basic_types_pb2.SignatureMap] = {} | ||
| # changed from int: 2_000_000 to Hbar: 2 | ||
| # changed from int: 2_000_000 to Hbar: 0.02 |
WalkthroughTransaction deserialization now maps protobuf transaction types to concrete SDK classes and restores transaction-specific fields across account, consensus, contract, file, node, schedule, system, and token transactions. Optional field handling, memo serialization, fee validation, and airdrop parsing are also updated. ChangesTransaction deserialization
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant TransactionFromBytes
participant TransactionClassLookup
participant ConcreteTransaction
participant ProtobufHelpers
TransactionFromBytes->>TransactionClassLookup: resolve transaction body data field
TransactionClassLookup->>ConcreteTransaction: select concrete SDK transaction class
ConcreteTransaction->>ProtobufHelpers: convert identifiers, keys, fees, and timestamps
ProtobufHelpers-->>ConcreteTransaction: populated transaction fields
ConcreteTransaction-->>TransactionFromBytes: reconstructed transaction
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/hiero_sdk_python/transaction/transaction.py (1)
860-905: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick winMissing
batch_keyrestoration breaks the batch-execution guard after round-trip.
build_base_transaction_body()setstransaction_body.batch_keywhenself.batch_keyis set (line 542-543), but_from_protobuf()never restores it. AfterTransaction.from_bytes(), a batchified inner transaction comes back withbatch_key = None(the__init__default), even though its serialized body still hasbatch_keypopulated on the wire. That silently defeats the guard inexecute():if self.batch_key and not isinstance(self, (BatchTransaction)): raise ValueError("Cannot execute batchified transaction outside of BatchTransaction.")A restored inner transaction could now be executed directly outside
BatchTransaction, bypassing an intentional safety check. This is exactly the kind of round-trip/asymmetry gap the batch-transaction lifecycle guidelines call out as security critical.🐛 Proposed fix
if sig_map and sig_map.sigPair: transaction._signature_map[body_bytes] = sig_map + if transaction_body.HasField("batch_key"): + transaction.batch_key = Key.from_proto_key(transaction_body.batch_key) + return transactionAs per path instructions for
src/hiero_sdk_python/transaction/**/*.py: "Verify: ... Protobuf packing of inner signedTransactionBytes and atomic_batch field is preserved ... batchify()/set_batch_key() flow does not bypass lifecycle" and the global instruction's "3f._from_proto/_to_protosymmetry: ... Flag any asymmetry that is not accompanied by a code comment explaining why."Source: Path instructions
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 228952b4-d803-444f-b888-ae4eaeed15b8
📒 Files selected for processing (48)
src/hiero_sdk_python/account/account_allowance_approve_transaction.pysrc/hiero_sdk_python/account/account_allowance_delete_transaction.pysrc/hiero_sdk_python/account/account_create_transaction.pysrc/hiero_sdk_python/account/account_delete_transaction.pysrc/hiero_sdk_python/account/account_update_transaction.pysrc/hiero_sdk_python/consensus/topic_create_transaction.pysrc/hiero_sdk_python/consensus/topic_delete_transaction.pysrc/hiero_sdk_python/consensus/topic_message_submit_transaction.pysrc/hiero_sdk_python/consensus/topic_update_transaction.pysrc/hiero_sdk_python/contract/contract_create_transaction.pysrc/hiero_sdk_python/contract/contract_delete_transaction.pysrc/hiero_sdk_python/contract/contract_execute_transaction.pysrc/hiero_sdk_python/contract/contract_update_transaction.pysrc/hiero_sdk_python/contract/ethereum_transaction.pysrc/hiero_sdk_python/file/file_append_transaction.pysrc/hiero_sdk_python/file/file_create_transaction.pysrc/hiero_sdk_python/file/file_delete_transaction.pysrc/hiero_sdk_python/file/file_update_transaction.pysrc/hiero_sdk_python/nodes/node_create_transaction.pysrc/hiero_sdk_python/nodes/node_delete_transaction.pysrc/hiero_sdk_python/nodes/node_update_transaction.pysrc/hiero_sdk_python/prng_transaction.pysrc/hiero_sdk_python/schedule/schedule_create_transaction.pysrc/hiero_sdk_python/schedule/schedule_delete_transaction.pysrc/hiero_sdk_python/schedule/schedule_sign_transaction.pysrc/hiero_sdk_python/system/freeze_transaction.pysrc/hiero_sdk_python/tokens/custom_fixed_fee.pysrc/hiero_sdk_python/tokens/token_airdrop_claim.pysrc/hiero_sdk_python/tokens/token_airdrop_transaction.pysrc/hiero_sdk_python/tokens/token_airdrop_transaction_cancel.pysrc/hiero_sdk_python/tokens/token_associate_transaction.pysrc/hiero_sdk_python/tokens/token_burn_transaction.pysrc/hiero_sdk_python/tokens/token_create_transaction.pysrc/hiero_sdk_python/tokens/token_delete_transaction.pysrc/hiero_sdk_python/tokens/token_dissociate_transaction.pysrc/hiero_sdk_python/tokens/token_fee_schedule_update_transaction.pysrc/hiero_sdk_python/tokens/token_freeze_transaction.pysrc/hiero_sdk_python/tokens/token_grant_kyc_transaction.pysrc/hiero_sdk_python/tokens/token_mint_transaction.pysrc/hiero_sdk_python/tokens/token_pause_transaction.pysrc/hiero_sdk_python/tokens/token_reject_transaction.pysrc/hiero_sdk_python/tokens/token_revoke_kyc_transaction.pysrc/hiero_sdk_python/tokens/token_unfreeze_transaction.pysrc/hiero_sdk_python/tokens/token_unpause_transaction.pysrc/hiero_sdk_python/tokens/token_update_nfts_transaction.pysrc/hiero_sdk_python/tokens/token_update_transaction.pysrc/hiero_sdk_python/tokens/token_wipe_transaction.pysrc/hiero_sdk_python/transaction/transaction.py
| @classmethod | ||
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | ||
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | ||
| if transaction_body.HasField("consensusSubmitMessage"): | ||
| body = transaction_body.consensusSubmitMessage | ||
| if body.HasField("topicID"): | ||
| transaction.topic_id = TopicId._from_proto(body.topicID) | ||
| transaction.message = body.message.decode("utf-8") if body.message else None | ||
| transaction._total_chunks = transaction.get_required_chunks() | ||
| return transaction | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
BLOCKER: _from_protobuf doesn't restore chunkInfo, breaking multi-chunk round-trip.
_build_proto_body only emits chunkInfo for multi-chunk messages, but _from_protobuf never reads it back. Restoring a serialized chunk N of M transaction currently:
- Leaves
_initial_transaction_id = None,_current_chunk_index = 0,_transaction_ids = []— all multi-chunk position/context is lost. - Recomputes
_total_chunksfrom just that one chunk's decoded bytes (get_required_chunks()), which will normally collapse back to1, silently mis-representing a multi-chunk message as single-chunk. - Calls
body.message.decode("utf-8")on a byte slice that may split a multi-byte UTF-8 character at the chunk boundary, risking aUnicodeDecodeError.
This directly hits the consensus module's flagged BLOCKER concern around _initial_transaction_id null-safety and multi-chunk metadata for this exact class.
🐛 Proposed fix
`@classmethod`
def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map):
transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map)
if transaction_body.HasField("consensusSubmitMessage"):
body = transaction_body.consensusSubmitMessage
if body.HasField("topicID"):
transaction.topic_id = TopicId._from_proto(body.topicID)
- transaction.message = body.message.decode("utf-8") if body.message else None
- transaction._total_chunks = transaction.get_required_chunks()
+ transaction.message = body.message.decode("utf-8", errors="replace") if body.message else None
+ if body.HasField("chunkInfo"):
+ chunk_info = body.chunkInfo
+ transaction._total_chunks = chunk_info.total
+ transaction._current_chunk_index = max(chunk_info.number - 1, 0)
+ transaction._initial_transaction_id = (
+ TransactionId._from_proto(chunk_info.initialTransactionID)
+ if chunk_info.HasField("initialTransactionID")
+ else None
+ )
+ else:
+ transaction._total_chunks = transaction.get_required_chunks()
return transactionNote: even with this fix, message on a restored non-first chunk still only reflects that chunk's bytes, not the full original message — the wire format simply doesn't carry the other chunks' bytes in a single TransactionBody. Worth documenting that limitation explicitly (or restricting round-trip guarantees to single-chunk/first-chunk cases) rather than leaving it implicit.
As per path instructions for src/hiero_sdk_python/consensus/**/*.py: "_initial_transaction_id null-safety ... An explicit if self._initial_transaction_id is None guard with a descriptive ValueError MUST be present. Flag as BLOCKER." and "Nanos-overflow guard... Protobuf Timestamp.nanos is bounded..." (the same §3a section governing multi-chunk TransactionID safety for this class).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @classmethod | |
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | |
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | |
| if transaction_body.HasField("consensusSubmitMessage"): | |
| body = transaction_body.consensusSubmitMessage | |
| if body.HasField("topicID"): | |
| transaction.topic_id = TopicId._from_proto(body.topicID) | |
| transaction.message = body.message.decode("utf-8") if body.message else None | |
| transaction._total_chunks = transaction.get_required_chunks() | |
| return transaction | |
| `@classmethod` | |
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | |
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | |
| if transaction_body.HasField("consensusSubmitMessage"): | |
| body = transaction_body.consensusSubmitMessage | |
| if body.HasField("topicID"): | |
| transaction.topic_id = TopicId._from_proto(body.topicID) | |
| transaction.message = body.message.decode("utf-8", errors="replace") if body.message else None | |
| if body.HasField("chunkInfo"): | |
| chunk_info = body.chunkInfo | |
| transaction._total_chunks = chunk_info.total | |
| transaction._current_chunk_index = max(chunk_info.number - 1, 0) | |
| transaction._initial_transaction_id = ( | |
| TransactionId._from_proto(chunk_info.initialTransactionID) | |
| if chunk_info.HasField("initialTransactionID") | |
| else None | |
| ) | |
| else: | |
| transaction._total_chunks = transaction.get_required_chunks() | |
| return transaction |
Source: Path instructions
| @classmethod | ||
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | ||
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | ||
| if transaction_body.HasField("contractCall"): | ||
| body = transaction_body.contractCall | ||
| if body.HasField("contractID"): | ||
| transaction.contract_id = ContractId._from_proto(body.contractID) | ||
| transaction.gas = body.gas if body.gas else None | ||
| transaction.amount = body.amount if body.amount else None | ||
| transaction.function_parameters = body.functionParameters if body.functionParameters else None | ||
| return transaction | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Inconsistent int-field normalization vs sibling transaction classes.
gas/amount are coerced from 0 to None, unlike contract_create_transaction.py._from_protobuf, which assigns gas/initial_balance directly. See consolidated comment for the cross-file recommendation.
| @classmethod | ||
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | ||
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | ||
| if transaction_body.HasField("ethereumTransaction"): | ||
| body = transaction_body.ethereumTransaction | ||
| transaction.ethereum_data = body.ethereum_data if body.ethereum_data else None | ||
| if body.HasField("call_data"): | ||
| transaction.call_data = FileId._from_proto(body.call_data) | ||
| transaction.max_gas_allowed = body.max_gas_allowance if body.max_gas_allowance else None | ||
| return transaction | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Inconsistent int-field normalization vs sibling transaction classes.
max_gas_allowed is coerced from 0 to None, same pattern flagged in contract_execute_transaction.py. See consolidated comment.
| @classmethod | ||
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | ||
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | ||
| if transaction_body.HasField("fileCreate"): | ||
| transaction._from_proto(transaction_body.fileCreate) | ||
| return transaction | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file_create_transaction.py outline =="
ast-grep outline src/hiero_sdk_python/file/file_create_transaction.py --view expanded || true
echo
echo "== relevant implementation slice =="
sed -n '1,260p' src/hiero_sdk_python/file/file_create_transaction.py | cat -n
echo
echo "== locate file_create protobuf references =="
rg -n "file_create_pb2|expirationTime|contents|memo|HasField\\(\"expirationTime\"\\)|or None" src/hiero_sdk_python/file src/hiero_sdk_python/hapi -S || true
echo
echo "== proto/schema file candidates =="
fd -a "file_create.proto|file_create_pb2.py" src/hiero_sdk_python . 2>/dev/null || trueRepository: hiero-ledger/hiero-sdk-python
Length of output: 24824
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate generated file_create pb2 =="
fd -a "file_create_pb2.py" src . 2>/dev/null || true
echo
echo "== file_update_transaction.py relevant slice =="
sed -n '150,245p' src/hiero_sdk_python/file/file_update_transaction.py | cat -n
echo
echo "== file_info.py relevant slice =="
sed -n '1,95p' src/hiero_sdk_python/file/file_info.py | cat -n
echo
echo "== inspect file_create_pb2 descriptor fields =="
python3 - <<'PY'
from pathlib import Path
import re
paths = list(Path("src").rglob("file_create_pb2.py"))
print("paths:", [str(p) for p in paths])
if paths:
p = paths[0]
text = p.read_text(encoding="utf-8", errors="ignore")
for needle in ["FileCreateTransactionBody", "expirationTime", "contents", "memo", "keys"]:
print(f"\n-- {needle} --")
for m in re.finditer(re.escape(needle), text):
start = max(0, m.start()-200)
end = min(len(text), m.end()+200)
print(text[start:end].splitlines()[0:8][0])
break
PYRepository: hiero-ledger/hiero-sdk-python
Length of output: 8393
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate generated file_create pb2 =="
fd -a "file_create_pb2.py" src . 2>/dev/null || true
echo
echo "== file_update_transaction.py relevant slice =="
sed -n '165,245p' src/hiero_sdk_python/file/file_update_transaction.py | cat -n
echo
echo "== file_info.py relevant slice =="
sed -n '1,95p' src/hiero_sdk_python/file/file_info.py | cat -n
echo
echo "== inspect file_create_pb2 descriptor fields =="
python3 - <<'PY'
from pathlib import Path
import re
paths = list(Path("src").rglob("file_create_pb2.py"))
print("paths:", [str(p) for p in paths])
if not paths:
raise SystemExit(0)
text = paths[0].read_text(encoding="utf-8", errors="ignore")
for needle in ["FileCreateTransactionBody", "expirationTime", "contents", "memo", "keys"]:
idx = text.find(needle)
print(f"\n-- {needle} --")
if idx >= 0:
snippet = text[max(0, idx-220): idx+420]
print(snippet)
else:
print("not found")
PYRepository: hiero-ledger/hiero-sdk-python
Length of output: 7833
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for rel in [
"src/hiero_sdk_python/file/file_update_transaction.py",
"src/hiero_sdk_python/file/file_info.py",
]:
p = Path(rel)
print(f"\n== {rel} ==")
text = p.read_text(encoding="utf-8")
for i, line in enumerate(text.splitlines(), 1):
if 165 <= i <= 245 and "file_update_transaction" in rel:
print(f"{i:4}: {line}")
if 1 <= i <= 95 and "file_info" in rel:
print(f"{i:4}: {line}")
PYRepository: hiero-ledger/hiero-sdk-python
Length of output: 7594
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== search for FileCreateTransactionBody definitions/references =="
rg -n "FileCreateTransactionBody|expirationTime|contents|memo" src/hiero_sdk_python -g '*file_create*' -g '*pb2*' -S || true
echo
echo "== inspect file_update_transaction.py round-trip handling =="
sed -n '168,235p' src/hiero_sdk_python/file/file_update_transaction.py | cat -nRepository: hiero-ledger/hiero-sdk-python
Length of output: 7839
🌐 Web query:
protobuf Python generated message field truthiness HasField message field unset bool behavior proto3 Timestamp submessage
💡 Result:
In Python protobuf (proto3), field presence and truthiness behavior depend on the type of field and whether it has explicit presence [1][2]. Field Presence and HasField For most scalar fields (bool, int, string, etc.) in proto3, there is no field presence (implicit presence) [2][3]. These fields do not support the HasField method [4][5]. Accessing an unset scalar field simply returns its type-specific default value (e.g., False for bool, 0 for numeric types, empty string/bytes) [4][3]. Fields that do support HasField include: - Submessages (message types) [4][5]. - Singular fields explicitly marked with the optional label [2]. - Fields defined within a oneof group [2][5]. For these fields, HasField returns True only if the field has been explicitly set or parsed from the wire [4][3]. Calling HasField on a field that does not track presence will raise a ValueError [5][6]. Message Fields (e.g., Timestamp) Submessage fields (like google.protobuf.Timestamp) always have explicit presence [4]. Even if you have not set any fields within the submessage, the submessage field itself is considered "unset" until accessed or assigned [4]. Once you access a submessage field, or modify one of its internal fields, the parent message will recognize it as "set" (HasField will return True) [4]. Truthiness and Default Values Because proto3 scalar fields use implicit presence, you cannot distinguish between an unset field and a field explicitly set to its default value [2][3]. - If you check a boolean field for truthiness (e.g., if message.my_bool:), it will evaluate to False if the field is unset or if it is explicitly set to False [3]. - Do not rely on truthiness or equality-to-default to determine if a field was "set" by the user; if you need to distinguish these states, you must use the optional keyword in your.proto file to enable explicit presence for that scalar field [2][3]. ListFields Behavior The ListFields method only returns fields that are "present" [7][6]. In proto3, this means: - For fields with explicit presence, it returns fields for which HasField is True [7]. - For scalar fields without explicit presence, ListFields will not include the field if it is set to its default value (e.g., 0 or False), as these are treated as unset [8][7].
Citations:
- 1: https://protobuf.dev/programming-guides/field_presence/
- 2: https://github.com/protocolbuffers/protobuf/blob/main/docs/field_presence.md
- 3: https://protobuf.dev/programming-guides/proto3/
- 4: https://protobuf.dev/reference/python/python-generated/
- 5: https://github.com/protocolbuffers/protobuf/blob/58f02be5/python/google/protobuf/internal/python_message.py
- 6: https://github.com/protocolbuffers/protobuf/blob/58f02be5/python/google/protobuf/message.py
- 7: https://googleapis.dev/python/protobuf/4.21.1/google/protobuf/message.html
- 8: Python, Protobuf 3:
ListFieldsmethod doesn't return field if it has value equaled default value protocolbuffers/protobuf#1772
🌐 Web query:
hashgraph hedera-protobufs v0.72.0-rc.2 services/file_create.proto FileCreateTransactionBody expirationTime contents memo
💡 Result:
In the Hedera Protobuf definition for FileCreateTransactionBody (as utilized in Hedera services), the fields expirationTime, contents, and memo serve the following purposes: expirationTime (Timestamp): Defines the specific instant at which the file is set to expire [1][2]. If no explicit expiration is set, the network typically applies a default duration [3]. Unless extended by a FileUpdateTransaction before this time, the file's contents will become unavailable, and the file will cease to exist after expiration [1][2]. contents (bytes): Represents the raw data or bytes that constitute the file's content [1][4]. While the field is optional, there is a network-enforced limit on the total size of a transaction (typically 6KiB), which applies to the file contents [3]. memo (string): A short, publicly visible string associated with the file [1][2]. It is limited to a maximum of 100 bytes and must be encoded in UTF-8 [1][3][2]. Note that while some older network entities or node-related protobufs have deprecated memo fields [5], the memo field within FileCreateTransactionBody remains a standard metadata field for file creation [1][2].
Citations:
- 1: https://docs.hedera.com/hedera/sdks-and-apis/hedera-api/file-service/filecreate
- 2: https://dev.uniultra.xyz/docs/sdks-and-apis/hedera-api/readme-1/filecreate
- 3: https://docs.hedera.com/native/files/create
- 4: https://docs.rs/hiero-sdk/latest/src/hiero_sdk/file/file_create_transaction.rs.html
- 5: https://github.com/hashgraph/hedera-protobufs/releases
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import importlib.util
from pathlib import Path
mods = [
"hiero_sdk_python.hapi.services.file_create_pb2",
"hiero_sdk_python.hapi.services.file_update_pb2",
]
for mod in mods:
spec = importlib.util.find_spec(mod)
print(f"{mod}:")
print(" spec =", spec)
if spec and spec.origin:
print(" origin =", spec.origin)
p = Path(spec.origin)
print(" exists =", p.exists())
PYRepository: hiero-ledger/hiero-sdk-python
Length of output: 352
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
# search for any checked-in generated protobuf sources or protos
candidates = []
for root in [Path("src"), Path(".")]:
if root.exists():
for p in root.rglob("*file_create*"):
if p.is_file():
candidates.append(p)
for p in sorted(set(candidates)):
print(p)
PYRepository: hiero-ledger/hiero-sdk-python
Length of output: 358
Normalize nullable fields in FileCreateTransaction._from_proto.
expirationTime should use HasField("expirationTime"), and contents/memo should map unset protobuf defaults to None so the SDK preserves its nullable state.
Suggested helper fix
- self.contents = proto.contents
- self.expiration_time = Timestamp._from_protobuf(proto.expirationTime) if proto.expirationTime else None
- self.file_memo = proto.memo
+ self.contents = proto.contents or None
+ self.expiration_time = (
+ Timestamp._from_protobuf(proto.expirationTime)
+ if proto.HasField("expirationTime")
+ else None
+ )
+ self.file_memo = proto.memo or NoneSources: Path instructions, MCP tools
| transaction.grpc_certificate_hash = pb.grpc_certificate_hash | ||
| if pb.HasField("admin_key"): | ||
| transaction.admin_key = PublicKey._from_proto(pb.admin_key) | ||
| transaction.admin_key = Key.from_proto_key(pb.admin_key) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Type annotations not updated after switching to Key.from_proto_key. All three sites switched deserialization from PublicKey._from_proto to Key.from_proto_key, which can return any Key subtype (PublicKey, KeyList, ContractId, DelegateContractId, EvmAddress), but the corresponding attribute type annotations still declare PublicKey.
src/hiero_sdk_python/nodes/node_create_transaction.py#L321-L321: Updateadmin_keytype fromPublicKey | NonetoKey | None(attribute at line 77,set_admin_keyparameter at line 172).src/hiero_sdk_python/nodes/node_update_transaction.py#L381-L381: Updateadmin_keytype fromPublicKey | NonetoKey | None(attribute at line 86,set_admin_keyparameter at line 196).src/hiero_sdk_python/file/file_update_transaction.py#L226-L226: Updatekeystype fromlist[PublicKey] | Nonetolist[Key] | None(attribute at line 62,set_keysparameter at line 98).
📍 Affects 3 files
src/hiero_sdk_python/nodes/node_create_transaction.py#L321-L321(this comment)src/hiero_sdk_python/nodes/node_update_transaction.py#L381-L381src/hiero_sdk_python/file/file_update_transaction.py#L226-L226
| @classmethod | ||
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | ||
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | ||
| if transaction_body.HasField("scheduleCreate"): | ||
| body = transaction_body.scheduleCreate | ||
| if body.HasField("payerAccountID"): | ||
| transaction.payer_account_id = AccountId._from_proto(body.payerAccountID) | ||
| if body.HasField("adminKey"): | ||
| transaction.admin_key = PublicKey._from_proto(body.adminKey) | ||
| if body.HasField("scheduledTransactionBody"): | ||
| transaction.schedulable_body = body.scheduledTransactionBody | ||
| transaction.schedule_memo = body.memo | ||
| if body.HasField("expiration_time"): | ||
| transaction.expiration_time = Timestamp._from_protobuf(body.expiration_time) | ||
| transaction.wait_for_expiry = body.wait_for_expiry | ||
| return transaction |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect PublicKey._from_proto and the generic Key.from_proto_key helper
ast-grep run --pattern 'def _from_proto($$$) { $$$ }' --lang python src/hiero_sdk_python/crypto/public_key.py 2>/dev/null
rg -n "class PublicKey" -A 30 src/hiero_sdk_python/crypto/public_key.py
rg -n "def from_proto_key" -A 20 src/hiero_sdk_pythonRepository: hiero-ledger/hiero-sdk-python
Length of output: 2830
🏁 Script executed:
#!/bin/bash
sed -n '1,220p' src/hiero_sdk_python/crypto/public_key.py
printf '\n--- key.py ---\n'
sed -n '1,220p' src/hiero_sdk_python/crypto/key.pyRepository: hiero-ledger/hiero-sdk-python
Length of output: 12145
🏁 Script executed:
#!/bin/bash
rg -n "def _from_proto|from_proto_key" src/hiero_sdk_python/crypto/public_key.py src/hiero_sdk_python/crypto/key.py src/hiero_sdk_python/schedule/schedule_create_transaction.py -A 20 -B 5Repository: hiero-ledger/hiero-sdk-python
Length of output: 8040
🏁 Script executed:
#!/bin/bash
sed -n '1,320p' src/hiero_sdk_python/schedule/schedule_create_transaction.pyRepository: hiero-ledger/hiero-sdk-python
Length of output: 9915
🏁 Script executed:
#!/bin/bash
python3 - <<'PY'
from hiero_sdk_python.hapi.services import schedule_create_pb2
field = schedule_create_pb2.ScheduleCreateTransactionBody.DESCRIPTOR.fields_by_name["adminKey"]
print("adminKey type:", field.message_type.full_name if field.message_type else field.type)
print("adminKey cpp type:", field.cpp_type)
PYRepository: hiero-ledger/hiero-sdk-python
Length of output: 296
🏁 Script executed:
#!/bin/bash
rg -n "adminKey|ScheduleCreateTransactionBody" src/hiero_sdk_python/hapi/services/schedule_create_pb2.py src/hiero_sdk_python/hapi/services -A 5 -B 5Repository: hiero-ledger/hiero-sdk-python
Length of output: 340
Use the generic key parser for adminKey. body.adminKey is a protobuf Key, so PublicKey._from_proto(body.adminKey) will reject valid KeyList/ThresholdKey admin keys and break round-tripping for multi-signature schedules. Switch to Key.from_proto_key(body.adminKey) here; widen admin_key to Key | None if composite keys are intended.
| @classmethod | ||
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | ||
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | ||
| if transaction_body.HasField("tokenAirdrop"): | ||
| for transfer in transaction_body.tokenAirdrop.token_transfers: | ||
| if not transfer.HasField("token"): | ||
| continue | ||
| token_id = TokenId._from_proto(transfer.token) | ||
| for t in transfer.transfers: | ||
| if not t.HasField("accountID"): | ||
| continue | ||
| account_id = AccountId._from_proto(t.accountID) | ||
| expected_decimals = ( | ||
| transfer.expected_decimals.value if transfer.HasField("expected_decimals") else None | ||
| ) | ||
| transaction.token_transfers[token_id].append( | ||
| TokenTransfer(token_id, account_id, t.amount, expected_decimals, t.is_approval) | ||
| ) | ||
| for n in transfer.nftTransfers: | ||
| if not n.HasField("senderAccountID") or not n.HasField("receiverAccountID"): | ||
| continue | ||
| sender_id = AccountId._from_proto(n.senderAccountID) | ||
| receiver_id = AccountId._from_proto(n.receiverAccountID) | ||
| transaction.nft_transfers[token_id].append( | ||
| TokenNftTransfer(token_id, sender_id, receiver_id, n.serialNumber, n.is_approval) | ||
| ) | ||
| return transaction | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Duplicate protobuf→SDK mapping logic vs. the existing _from_proto classmethod.
This new _from_protobuf reimplements the same token_transfers/nft_transfers extraction that _from_proto (Lines 95-113) already performs via TokenTransfer._from_proto(transfer) / TokenNftTransfer._from_proto(transfer). Having two independently-maintained implementations of the same mapping increases the risk of silent divergence (as already seen: one path validates/skips differently than the other). Consider having _from_protobuf delegate to the same helper methods used by _from_proto for consistency.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
set -euo pipefail
printf '\n== token_airdrop_transaction.py ==\n'
sed -n '1,220p' src/hiero_sdk_python/tokens/token_airdrop_transaction.py | cat -n
printf '\n== token_transfer.py ==\n'
fd -a 'token_transfer.py' src/hiero_sdk_python | while read -r f; do
printf '\nFILE: %s\n' "$f"
sed -n '1,260p' "$f" | cat -n
done
printf '\n== token_nft_transfer.py ==\n'
fd -a 'token_nft_transfer.py' src/hiero_sdk_python | while read -r f; do
printf '\nFILE: %s\n' "$f"
sed -n '1,240p' "$f" | cat -n
done
printf '\n== transaction base methods around from_protobuf/from_proto ==\n'
fd -a 'transaction.py' src/hiero_sdk_python | while read -r f; do
printf '\nFILE: %s\n' "$f"
rg -n "def _from_protobuf|def _from_proto|def _to_proto|def build_transaction_body|def _build_proto_body" "$f"
doneRepository: hiero-ledger/hiero-sdk-python
Length of output: 24000
Fail fast on malformed transfer entries
src/hiero_sdk_python/tokens/token_airdrop_transaction.py:72-87 silently drops any transfer entry missing token, accountID, senderAccountID, or receiverAccountID. That turns corrupt or future-incompatible protobuf bytes into a truncated transaction instead of surfacing an error. Raise a ValueError (or similar) here so invalid input does not deserialize into partial state.
| @classmethod | ||
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | ||
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | ||
| if transaction_body.HasField("tokenBurn"): | ||
| body = transaction_body.tokenBurn | ||
| if body.HasField("token"): | ||
| transaction.token_id = TokenId._from_proto(body.token) | ||
| transaction.amount = body.amount if body.amount else None | ||
| transaction.serials = list(body.serialNumbers) | ||
| return transaction | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
amount normalization diverges from the sibling _from_proto method.
All mapped proto fields align with the schema at https://github.com/hashgraph/hedera-protobufs/blob/v0.72.0-rc.2/services/token_burn.proto, but the new _from_protobuf normalizes amount == 0 to None (Line 170), while the existing _from_proto (Line 185) keeps proto.amount as-is. Parsing identical wire bytes for an NFT-only burn (serials set, amount unset → defaults to 0 on the wire) through the two entry points now yields different in-memory states (amount=None vs amount=0).
🐛 Align the two deserializers (or document the intentional difference)
- transaction.amount = body.amount if body.amount else None
+ # Match `_from_proto`'s behavior for consistency across deserialization entry points
+ transaction.amount = body.amount📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @classmethod | |
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | |
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | |
| if transaction_body.HasField("tokenBurn"): | |
| body = transaction_body.tokenBurn | |
| if body.HasField("token"): | |
| transaction.token_id = TokenId._from_proto(body.token) | |
| transaction.amount = body.amount if body.amount else None | |
| transaction.serials = list(body.serialNumbers) | |
| return transaction | |
| `@classmethod` | |
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | |
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | |
| if transaction_body.HasField("tokenBurn"): | |
| body = transaction_body.tokenBurn | |
| if body.HasField("token"): | |
| transaction.token_id = TokenId._from_proto(body.token) | |
| # Match `_from_proto`'s behavior for consistency across deserialization entry points | |
| transaction.amount = body.amount | |
| transaction.serials = list(body.serialNumbers) | |
| return transaction |
|
|
||
| @classmethod | ||
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | ||
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | ||
| if transaction_body.HasField("tokenMint"): | ||
| body = transaction_body.tokenMint | ||
| if body.HasField("token"): | ||
| transaction.token_id = TokenId._from_proto(body.token) | ||
| transaction.amount = body.amount if body.amount else None | ||
| transaction.metadata = list(body.metadata) | ||
| return transaction |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Metadata round-trip loses the "not an NFT mint" sentinel.
transaction.metadata = list(body.metadata) always yields a list, even for fungible-only mints where body.metadata is empty. The constructor's tri-state contract (bytes | list[bytes] | None, default None) uses None to mean "no NFT metadata provided." After deserialization this becomes [], which is truthy-different from the pre-existing sentinel and would mislead any caller checking metadata is None to distinguish fungible vs NFT mints. The amount line just above already handles the equivalent case correctly with a falsy check.
🐛 Proposed fix
- transaction.metadata = list(body.metadata)
+ transaction.metadata = list(body.metadata) if body.metadata else None📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @classmethod | |
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | |
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | |
| if transaction_body.HasField("tokenMint"): | |
| body = transaction_body.tokenMint | |
| if body.HasField("token"): | |
| transaction.token_id = TokenId._from_proto(body.token) | |
| transaction.amount = body.amount if body.amount else None | |
| transaction.metadata = list(body.metadata) | |
| return transaction | |
| `@classmethod` | |
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): | |
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | |
| if transaction_body.HasField("tokenMint"): | |
| body = transaction_body.tokenMint | |
| if body.HasField("token"): | |
| transaction.token_id = TokenId._from_proto(body.token) | |
| transaction.amount = body.amount if body.amount else None | |
| transaction.metadata = list(body.metadata) if body.metadata else None | |
| return transaction |
| @classmethod | ||
| def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): # noqa: PLR0912 | ||
| transaction = super()._from_protobuf(transaction_body, body_bytes, sig_map) | ||
| if transaction_body.HasField("tokenUpdate"): | ||
| body = transaction_body.tokenUpdate | ||
| if body.HasField("token"): | ||
| transaction.token_id = TokenId._from_proto(body.token) | ||
| if body.HasField("treasury"): | ||
| transaction.treasury_account_id = AccountId._from_proto(body.treasury) | ||
| transaction.token_name = body.name if body.name else None | ||
| transaction.token_symbol = body.symbol if body.symbol else None | ||
| transaction.token_memo = body.memo.value if body.HasField("memo") else None | ||
| transaction.metadata = body.metadata.value if body.HasField("metadata") else None | ||
| if body.HasField("expiry"): | ||
| transaction.expiration_time = Timestamp._from_protobuf(body.expiry) | ||
| if body.HasField("autoRenewAccount"): | ||
| transaction.auto_renew_account_id = AccountId._from_proto(body.autoRenewAccount) | ||
| if body.HasField("autoRenewPeriod"): | ||
| transaction.auto_renew_period = Duration._from_proto(body.autoRenewPeriod) | ||
| transaction.token_key_verification_mode = TokenKeyValidation._from_proto(body.key_verification_mode) | ||
| if body.HasField("adminKey"): | ||
| transaction.admin_key = Key.from_proto_key(body.adminKey) | ||
| if body.HasField("freezeKey"): | ||
| transaction.freeze_key = Key.from_proto_key(body.freezeKey) | ||
| if body.HasField("wipeKey"): | ||
| transaction.wipe_key = Key.from_proto_key(body.wipeKey) | ||
| if body.HasField("supplyKey"): | ||
| transaction.supply_key = Key.from_proto_key(body.supplyKey) | ||
| if body.HasField("metadata_key"): | ||
| transaction.metadata_key = Key.from_proto_key(body.metadata_key) | ||
| if body.HasField("pause_key"): | ||
| transaction.pause_key = Key.from_proto_key(body.pause_key) | ||
| if body.HasField("kycKey"): | ||
| transaction.kyc_key = Key.from_proto_key(body.kycKey) | ||
| if body.HasField("fee_schedule_key"): | ||
| transaction.fee_schedule_key = Key.from_proto_key(body.fee_schedule_key) | ||
| return transaction | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Add round-trip tests for the new _from_protobuf deserializers.
This PR's stated goal is to fix incomplete Transaction.from_bytes() deserialization, but none of the seven reviewed token transaction classes ship a test asserting the round trip (build → to_bytes() → from_bytes() → assert all fields equal). Without this, a future refactor could silently reintroduce the exact bug this PR fixes.
src/hiero_sdk_python/tokens/token_update_transaction.py#L474-L511: add a test round-tripping token id, treasury, name/symbol/memo/metadata, expiry/auto-renew, key verification mode, and all eight key fields.src/hiero_sdk_python/tokens/token_associate_transaction.py#L105-L114: add a test round-trippingaccount_idandtoken_ids.src/hiero_sdk_python/tokens/token_burn_transaction.py#L163-L173: add a test round-trippingtoken_id,amount, andserials(both the fungible-amount and NFT-serials cases).src/hiero_sdk_python/tokens/token_delete_transaction.py#L97-L105: add a test round-trippingtoken_id.src/hiero_sdk_python/tokens/token_dissociate_transaction.py#L80-L89: add a test round-trippingaccount_idandtoken_ids.src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py#L130-L140: add a test round-trippingtoken_idandaccount_id.src/hiero_sdk_python/tokens/token_unfreeze_transaction.py#L119-L129: add a test round-trippingtoken_idandaccount_id.
📍 Affects 7 files
src/hiero_sdk_python/tokens/token_update_transaction.py#L474-L511(this comment)src/hiero_sdk_python/tokens/token_associate_transaction.py#L105-L114src/hiero_sdk_python/tokens/token_burn_transaction.py#L163-L173src/hiero_sdk_python/tokens/token_delete_transaction.py#L97-L105src/hiero_sdk_python/tokens/token_dissociate_transaction.py#L80-L89src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py#L130-L140src/hiero_sdk_python/tokens/token_unfreeze_transaction.py#L119-L129
Source: Path instructions
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## main #2438 +/- ##
==========================================
+ Coverage 94.99% 95.63% +0.64%
==========================================
Files 164 164
Lines 10449 11065 +616
==========================================
+ Hits 9926 10582 +656
+ Misses 523 483 -40 🚀 New features to boost your workflow:
|
|
Hello, this is the OfficeHourBot. This is a reminder that the Hiero Python SDK Office Hours will begin in approximately 3 hours and 28 minutes (14:00 UTC). This session provides an opportunity to ask questions regarding this Pull Request. Details:
Disclaimer: This is an automated reminder. Please verify the schedule here for any changes. From, |
…atch map Signed-off-by: Mounil Kanakhara <mounilkankhara@gmail.com>
…etadata sentinel, schedule admin key Signed-off-by: Mounil Kanakhara <mounilkankhara@gmail.com>
…each 92% patch coverage Signed-off-by: Mounil Kanakhara <mounilkankhara@gmail.com>
…ullable fields, misleading comment Signed-off-by: Mounil Kanakhara <mounilkankhara@gmail.com>
0d19c27 to
a9a4fed
Compare
|
Hi, this is WorkflowBot.
|
…tion Signed-off-by: Mounil Kanakhara <mounilkankhara@gmail.com>
Description:
Implements
_from_protobuffor all 44 transaction types so thatTransaction.from_bytes()correctly reconstructs each transaction type instead of returning an empty object.Related issue(s):
Fixes #2179
Notes for reviewer:
Key changes:
_from_protobufto all 44 transaction types (Account, Token, Consensus, File, Schedule, System, Contract, Node, Prng)_TRANSACTION_TYPE_MAPdispatch: corrects snake_case keys (token_pause,token_unpause,token_fee_schedule_update,token_update_nfts,util_prng), adds missingfreezeentry, addstokenClaimAirdrop, fixes wrong module path forTokenUnpauseTransactionKey.from_proto_key()for all keys (handles compositeKeyList/ThresholdKey)HasField()guards for optional message fields; scalar fields assigned directlyamount=0preserved asNonefor token burn/wipe/mint (proto3 scalar 0 is semantically "not set" for these operations)Checklist