Skip to content

feat: implement Transaction.from_bytes() deserialization for all transaction types#2438

Draft
Mounil2005 wants to merge 5 commits into
hiero-ledger:mainfrom
Mounil2005:fix/from-protobuf-implementations
Draft

feat: implement Transaction.from_bytes() deserialization for all transaction types#2438
Mounil2005 wants to merge 5 commits into
hiero-ledger:mainfrom
Mounil2005:fix/from-protobuf-implementations

Conversation

@Mounil2005

@Mounil2005 Mounil2005 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Description:

Implements _from_protobuf for all 44 transaction types so that Transaction.from_bytes() correctly reconstructs each transaction type instead of returning an empty object.

Related issue(s):

Fixes #2179

Notes for reviewer:

Key changes:

  • Adds _from_protobuf to all 44 transaction types (Account, Token, Consensus, File, Schedule, System, Contract, Node, Prng)
  • Fixes _TRANSACTION_TYPE_MAP dispatch: corrects snake_case keys (token_pause, token_unpause, token_fee_schedule_update, token_update_nfts, util_prng), adds missing freeze entry, adds tokenClaimAirdrop, fixes wrong module path for TokenUnpauseTransaction
  • Moves dispatch map to a module-level constant for efficiency (was rebuilt on every call)
  • Uses Key.from_proto_key() for all keys (handles composite KeyList/ThresholdKey)
  • Uses HasField() guards for optional message fields; scalar fields assigned directly
  • amount=0 preserved as None for token burn/wipe/mint (proto3 scalar 0 is semantically "not set" for these operations)

Checklist

  • Documented (Code comments, README, etc.)
  • Tested (unit, integration, etc.)

@Mounil2005
Mounil2005 requested review from a team as code owners July 13, 2026 18:09
@github-actions github-actions Bot added lang: python Uses Python programming language scope: DLT involves engineering for distributed ledger technology labels Jul 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_protobuf implementations 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.

Comment on lines 185 to 189
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
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

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

Changes

Transaction deserialization

Layer / File(s) Summary
Restore dispatch and base serialization
src/hiero_sdk_python/transaction/transaction.py
Centralizes transaction-type lookup, adds corrected protobuf field mappings, and normalizes absent base and scheduled memos to empty strings.
Account and consensus restoration
src/hiero_sdk_python/account/*, src/hiero_sdk_python/consensus/*
Restores account allowance, account lifecycle, topic creation/update/deletion, and topic message fields from protobuf bodies.
Contract transaction restoration
src/hiero_sdk_python/contract/*
Restores contract create, delete, execute, update, and Ethereum transaction fields, including keys, staking targets, initcode sources, and call data.
File, node, system, schedule, and PRNG restoration
src/hiero_sdk_python/file/*, src/hiero_sdk_python/nodes/*, src/hiero_sdk_python/system/*, src/hiero_sdk_python/schedule/*, src/hiero_sdk_python/prng_transaction.py
Adds protobuf reconstruction for file, node delete, freeze, schedule, and PRNG transactions and updates node key conversion.
Token definitions and fees
src/hiero_sdk_python/tokens/custom_fixed_fee.py, src/hiero_sdk_python/tokens/token_create_transaction.py, src/hiero_sdk_python/tokens/token_update_transaction.py, src/hiero_sdk_python/tokens/token_fee_schedule_update_transaction.py
Restores token parameters, keys, renewal settings, metadata, verification modes, and fee schedules, with fixed-fee presence validation.
Token operations and airdrops
src/hiero_sdk_python/tokens/token_*
Adds protobuf restoration for token operations and airdrops, including fungible/NFT transfers, pending airdrops, metadata, serials, and optional identifiers; enforces airdrop transfer-count limits.

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
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning TokenAirdropTransaction adds transfer-count validation and transaction.py changes memo handling, which are unrelated to the deserialization/map-fix scope. Remove or split unrelated behavior changes, such as airdrop transfer-count validation and memo-default handling, into a separate PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 56.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements _from_protobuf for the affected transaction types and fixes the dispatch map as requested.
Title check ✅ Passed The title accurately summarizes the main change: adding deserialization support for transaction types.
Description check ✅ Passed The description matches the changeset and explains the new deserialization logic and dispatch-map fixes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Missing batch_key restoration breaks the batch-execution guard after round-trip.

build_base_transaction_body() sets transaction_body.batch_key when self.batch_key is set (line 542-543), but _from_protobuf() never restores it. After Transaction.from_bytes(), a batchified inner transaction comes back with batch_key = None (the __init__ default), even though its serialized body still has batch_key populated on the wire. That silently defeats the guard in execute():

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 transaction

As 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_proto symmetry: ... 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

📥 Commits

Reviewing files that changed from the base of the PR and between ec5baa2 and 20f5403.

📒 Files selected for processing (48)
  • src/hiero_sdk_python/account/account_allowance_approve_transaction.py
  • src/hiero_sdk_python/account/account_allowance_delete_transaction.py
  • src/hiero_sdk_python/account/account_create_transaction.py
  • src/hiero_sdk_python/account/account_delete_transaction.py
  • src/hiero_sdk_python/account/account_update_transaction.py
  • src/hiero_sdk_python/consensus/topic_create_transaction.py
  • src/hiero_sdk_python/consensus/topic_delete_transaction.py
  • src/hiero_sdk_python/consensus/topic_message_submit_transaction.py
  • src/hiero_sdk_python/consensus/topic_update_transaction.py
  • src/hiero_sdk_python/contract/contract_create_transaction.py
  • src/hiero_sdk_python/contract/contract_delete_transaction.py
  • src/hiero_sdk_python/contract/contract_execute_transaction.py
  • src/hiero_sdk_python/contract/contract_update_transaction.py
  • src/hiero_sdk_python/contract/ethereum_transaction.py
  • src/hiero_sdk_python/file/file_append_transaction.py
  • src/hiero_sdk_python/file/file_create_transaction.py
  • src/hiero_sdk_python/file/file_delete_transaction.py
  • src/hiero_sdk_python/file/file_update_transaction.py
  • src/hiero_sdk_python/nodes/node_create_transaction.py
  • src/hiero_sdk_python/nodes/node_delete_transaction.py
  • src/hiero_sdk_python/nodes/node_update_transaction.py
  • src/hiero_sdk_python/prng_transaction.py
  • src/hiero_sdk_python/schedule/schedule_create_transaction.py
  • src/hiero_sdk_python/schedule/schedule_delete_transaction.py
  • src/hiero_sdk_python/schedule/schedule_sign_transaction.py
  • src/hiero_sdk_python/system/freeze_transaction.py
  • src/hiero_sdk_python/tokens/custom_fixed_fee.py
  • src/hiero_sdk_python/tokens/token_airdrop_claim.py
  • src/hiero_sdk_python/tokens/token_airdrop_transaction.py
  • src/hiero_sdk_python/tokens/token_airdrop_transaction_cancel.py
  • src/hiero_sdk_python/tokens/token_associate_transaction.py
  • src/hiero_sdk_python/tokens/token_burn_transaction.py
  • src/hiero_sdk_python/tokens/token_create_transaction.py
  • src/hiero_sdk_python/tokens/token_delete_transaction.py
  • src/hiero_sdk_python/tokens/token_dissociate_transaction.py
  • src/hiero_sdk_python/tokens/token_fee_schedule_update_transaction.py
  • src/hiero_sdk_python/tokens/token_freeze_transaction.py
  • src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py
  • src/hiero_sdk_python/tokens/token_mint_transaction.py
  • src/hiero_sdk_python/tokens/token_pause_transaction.py
  • src/hiero_sdk_python/tokens/token_reject_transaction.py
  • src/hiero_sdk_python/tokens/token_revoke_kyc_transaction.py
  • src/hiero_sdk_python/tokens/token_unfreeze_transaction.py
  • src/hiero_sdk_python/tokens/token_unpause_transaction.py
  • src/hiero_sdk_python/tokens/token_update_nfts_transaction.py
  • src/hiero_sdk_python/tokens/token_update_transaction.py
  • src/hiero_sdk_python/tokens/token_wipe_transaction.py
  • src/hiero_sdk_python/transaction/transaction.py

Comment on lines +250 to +260
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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_chunks from just that one chunk's decoded bytes (get_required_chunks()), which will normally collapse back to 1, 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 a UnicodeDecodeError.

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 transaction

Note: 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.

Suggested change
@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

Comment on lines +166 to +177
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +125 to +135
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +185 to +191
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 || true

Repository: 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
PY

Repository: 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")
PY

Repository: 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}")
PY

Repository: 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 -n

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


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


🏁 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())
PY

Repository: 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)
PY

Repository: 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 None

Sources: 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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: Update admin_key type from PublicKey | None to Key | None (attribute at line 77, set_admin_key parameter at line 172).
  • src/hiero_sdk_python/nodes/node_update_transaction.py#L381-L381: Update admin_key type from PublicKey | None to Key | None (attribute at line 86, set_admin_key parameter at line 196).
  • src/hiero_sdk_python/file/file_update_transaction.py#L226-L226: Update keys type from list[PublicKey] | None to list[Key] | None (attribute at line 62, set_keys parameter 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-L381
  • src/hiero_sdk_python/file/file_update_transaction.py#L226-L226

Comment on lines +233 to +248
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Repository: 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.py

Repository: 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 5

Repository: 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.py

Repository: 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)
PY

Repository: 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 5

Repository: 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.

Comment on lines +67 to +94
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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"
done

Repository: 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.

Comment on lines +163 to +173
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
@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

Comment on lines +139 to +149

@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
@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

Comment on lines +474 to +511
@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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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-tripping account_id and token_ids.
  • src/hiero_sdk_python/tokens/token_burn_transaction.py#L163-L173: add a test round-tripping token_id, amount, and serials (both the fungible-amount and NFT-serials cases).
  • src/hiero_sdk_python/tokens/token_delete_transaction.py#L97-L105: add a test round-tripping token_id.
  • src/hiero_sdk_python/tokens/token_dissociate_transaction.py#L80-L89: add a test round-tripping account_id and token_ids.
  • src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py#L130-L140: add a test round-tripping token_id and account_id.
  • src/hiero_sdk_python/tokens/token_unfreeze_transaction.py#L119-L129: add a test round-tripping token_id and account_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-L114
  • src/hiero_sdk_python/tokens/token_burn_transaction.py#L163-L173
  • src/hiero_sdk_python/tokens/token_delete_transaction.py#L97-L105
  • src/hiero_sdk_python/tokens/token_dissociate_transaction.py#L80-L89
  • src/hiero_sdk_python/tokens/token_grant_kyc_transaction.py#L130-L140
  • src/hiero_sdk_python/tokens/token_unfreeze_transaction.py#L119-L129

Source: Path instructions

@github-actions github-actions Bot added open to community review PR is open for community review and feedback queue:junior-committer PR awaiting initial quality review labels Jul 13, 2026
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.84399% with 1 line in your changes missing coverage. Please review.

Impacted file tree graph

@@            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:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mounil2005
Mounil2005 marked this pull request as draft July 13, 2026 19:30
@github-actions

Copy link
Copy Markdown

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,
The Python SDK Team

…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>
@Mounil2005
Mounil2005 force-pushed the fix/from-protobuf-implementations branch from 0d19c27 to a9a4fed Compare July 15, 2026 20:52
@github-actions

Copy link
Copy Markdown

Hi, this is WorkflowBot.
Your pull request cannot be merged as it is not passing all our workflow checks.
Please click on each check to review the logs and resolve issues so all checks pass.
To help you:

…tion

Signed-off-by: Mounil Kanakhara <mounilkankhara@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lang: python Uses Python programming language open to community review PR is open for community review and feedback queue:junior-committer PR awaiting initial quality review scope: DLT involves engineering for distributed ledger technology

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transaction.from_bytes() returns incomplete objects for most transaction types — missing _from_protobuf implementations

4 participants