Skip to content
Draft
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
## [Unreleased]

### Added
- chore: add `scripts/src_vs_proto` and contents to pull fields from the proto and compare them to the setters and attributes we provide functionality in our token transactions, thus enabling more automatic issue management"

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto-generated token classes with proto attributes and setters

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# No errors encountered
101 changes: 101 additions & 0 deletions scripts/src_vs_proto/step_1_transactions_proto_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# scripts/generate_transactions_mapping.py
import inspect
from pathlib import Path
import importlib
import ast

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent # go up to hedera_sdk_python
TOKENS_DIR = PROJECT_ROOT / "src" / "hiero_sdk_python" / "tokens"
OUTPUT_DIR = PROJECT_ROOT / "scripts" / "src_vs_proto"
OUTPUT_FILE = OUTPUT_DIR / "step_2_transactions_mapping.py"

def find_proto_import(file_path):
"""Parse transaction file to find the protobuf import."""
with open(file_path, "r") as f:
tree = ast.parse(f.read(), filename=str(file_path))
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
if node.module and node.module.startswith("hiero_sdk_python.hapi.services"):
for alias in node.names:
if alias.name.endswith("TransactionBody"):
return node.module, alias.name
return None, None

transactions_mapping = {}
unmatched_transactions = []

# Keep track of modules to import
token_modules_set = set()
proto_modules_set = set()

token_files = list(TOKENS_DIR.glob("token_*_transaction.py"))

for token_file in token_files:
module_name = f"hiero_sdk_python.tokens.{token_file.stem}"
token_modules_set.add(token_file.stem)
try:
module = importlib.import_module(module_name)
except Exception as e:
print(f"Failed to import {module_name}: {e}")
continue

# Find the Transaction class
transaction_class = None
for name, obj in inspect.getmembers(module, inspect.isclass):
if "Transaction" in [base.__name__ for base in obj.__bases__]:
transaction_class = obj
transaction_class_name = obj.__name__
break

if not transaction_class:
continue

# Find proto import
proto_module_name, proto_class_name = find_proto_import(token_file)
proto_cls_str = proto_class_name if proto_class_name else "None"
if proto_module_name:
proto_modules_set.add(proto_module_name)
else:
unmatched_transactions.append(transaction_class_name)
proto_module_name = None

transactions_mapping[transaction_class_name] = {
"cls": f"{token_file.stem}.{transaction_class_name}", # will become proper import
"proto_cls": proto_cls_str,
"proto_module": proto_module_name
}

# Write to file
with open(OUTPUT_FILE, "w") as f:
f.write("# Auto-generated transactions mapping\n\n")

# Write token imports
f.write("from hiero_sdk_python.tokens import (\n")
for module in sorted(token_modules_set):
f.write(f" {module},\n")
f.write(")\n\n")

# Write proto imports
f.write("from hiero_sdk_python.hapi.services import (\n")
for module in sorted(proto_modules_set):
# Only write the last part of the module for correct import
short_module = module.split('.')[-1]
f.write(f" {short_module},\n")
f.write(")\n\n")

# Write TRANSACTIONS dictionary
f.write("TRANSACTIONS = {\n")
for k, v in transactions_mapping.items():
cls_module, cls_name = v['cls'].split(".")
cls_str = f"{cls_module}.{cls_name}"
proto_cls_str = f"{v['proto_module'].split('.')[-1]}.{v['proto_cls']}" if v['proto_module'] else "None"
f.write(f" '{k}': {{'cls': {cls_str}, 'proto_cls': {proto_cls_str}}},\n")
f.write("}\n\n")

# Summary
f.write("# Summary\n")
f.write(f"TOTAL_TOKENS = {len(token_files)}\n")
f.write(f"PROTO_IDENTIFIED = {len(token_files)-len(unmatched_transactions)}\n")
f.write(f"UNMATCHED_TRANSACTIONS = {unmatched_transactions}\n")

print(f"Mapping written to {OUTPUT_FILE}")
Empty file.
60 changes: 60 additions & 0 deletions scripts/src_vs_proto/step_2_transactions_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Auto-generated transactions mapping

from hiero_sdk_python.tokens import (
token_airdrop_transaction,
token_associate_transaction,
token_burn_transaction,
token_create_transaction,
token_delete_transaction,
token_dissociate_transaction,
token_fee_schedule_update_transaction,
token_freeze_transaction,
token_grant_kyc_transaction,
token_mint_transaction,
token_pause_transaction,
token_reject_transaction,
token_revoke_kyc_transaction,
token_unfreeze_transaction,
token_unpause_transaction,
token_update_nfts_transaction,
token_update_transaction,
token_wipe_transaction,
)

from hiero_sdk_python.hapi.services import (
schedulable_transaction_body_pb2,
token_burn_pb2,
token_grant_kyc_pb2,
token_mint_pb2,
token_pause_pb2,
token_reject_pb2,
token_revoke_kyc_pb2,
token_update_nfts_pb2,
token_wipe_account_pb2,
)

TRANSACTIONS = {
'TokenFeeScheduleUpdateTransaction': {'cls': token_fee_schedule_update_transaction.TokenFeeScheduleUpdateTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenFreezeTransaction': {'cls': token_freeze_transaction.TokenFreezeTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenRevokeKycTransaction': {'cls': token_revoke_kyc_transaction.TokenRevokeKycTransaction, 'proto_cls': token_revoke_kyc_pb2.TokenRevokeKycTransactionBody},
'AbstractTokenTransferTransaction': {'cls': token_airdrop_transaction.AbstractTokenTransferTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenUpdateNftsTransaction': {'cls': token_update_nfts_transaction.TokenUpdateNftsTransaction, 'proto_cls': token_update_nfts_pb2.TokenUpdateNftsTransactionBody},
'TokenAssociateTransaction': {'cls': token_associate_transaction.TokenAssociateTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenMintTransaction': {'cls': token_mint_transaction.TokenMintTransaction, 'proto_cls': token_mint_pb2.TokenMintTransactionBody},
'TokenCreateTransaction': {'cls': token_create_transaction.TokenCreateTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenWipeTransaction': {'cls': token_wipe_transaction.TokenWipeTransaction, 'proto_cls': token_wipe_account_pb2.TokenWipeAccountTransactionBody},
'TokenPauseTransaction': {'cls': token_pause_transaction.TokenPauseTransaction, 'proto_cls': token_pause_pb2.TokenPauseTransactionBody},
'TokenBurnTransaction': {'cls': token_burn_transaction.TokenBurnTransaction, 'proto_cls': token_burn_pb2.TokenBurnTransactionBody},
'TokenUnfreezeTransaction': {'cls': token_unfreeze_transaction.TokenUnfreezeTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenRejectTransaction': {'cls': token_reject_transaction.TokenRejectTransaction, 'proto_cls': token_reject_pb2.TokenRejectTransactionBody},
'TokenDissociateTransaction': {'cls': token_dissociate_transaction.TokenDissociateTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenDeleteTransaction': {'cls': token_delete_transaction.TokenDeleteTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenUnpauseTransaction': {'cls': token_unpause_transaction.TokenUnpauseTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenUpdateTransaction': {'cls': token_update_transaction.TokenUpdateTransaction, 'proto_cls': schedulable_transaction_body_pb2.SchedulableTransactionBody},
'TokenGrantKycTransaction': {'cls': token_grant_kyc_transaction.TokenGrantKycTransaction, 'proto_cls': token_grant_kyc_pb2.TokenGrantKycTransactionBody},
}

# Summary
TOTAL_TOKENS = 18
PROTO_IDENTIFIED = 18
UNMATCHED_TRANSACTIONS = []
82 changes: 82 additions & 0 deletions scripts/src_vs_proto/step_3_missing_functionality.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# scripts/missing_functionality.py
import inspect
from step_2_transactions_mapping import TRANSACTIONS

# Helper functions for terminal styling
def bold(text):
return f"\033[1m{text}\033[0m"

def red(text):
return f"\033[91m{text}\033[0m"

def green(text):
return f"\033[92m{text}\033[0m"

def get_transaction_methods(cls):
return set(func for func, _ in inspect.getmembers(cls, predicate=inspect.isfunction))

def get_transaction_attributes(cls):
init_sig = inspect.signature(cls.__init__)
return set(init_sig.parameters.keys()) - {'self'}

def get_proto_fields(proto_cls, transaction_cls_name=None):
"""
Return only the protobuf fields relevant to this transaction class.

If transaction_cls_name is given, filter to the one field that matches
this transaction type (e.g., TokenUnpauseTransaction -> token_unpause).
"""
all_fields = set(proto_cls.DESCRIPTOR.fields_by_name.keys())

if transaction_cls_name:
# Convert class name like TokenUnpauseTransaction -> token_unpause
relevant_field = transaction_cls_name.replace("Transaction", "")
relevant_field = relevant_field[0].lower() + relevant_field[1:] # lowercase first char
relevant_field = relevant_field.replace("Token", "token_") if "Token" in transaction_cls_name else relevant_field
# Keep only the relevant proto field if it exists
return {f for f in all_fields if f.endswith(relevant_field)}

return all_fields

def check_transaction(transaction_cls, proto_cls):
proto_fields = get_proto_fields(proto_cls, transaction_cls.__name__)
methods = get_transaction_methods(transaction_cls)
attributes = get_transaction_attributes(transaction_cls)

actual_setters = {m for m in methods if m.startswith("set_")}
missing_setters = {f"set_{field}" for field in proto_fields} - actual_setters

actual_attrs = attributes
missing_attrs = proto_fields - actual_attrs

return {
"transaction": transaction_cls.__name__,
"proto_fields": proto_fields,
"attributes": attributes,
"actual_attrs": actual_attrs,
"methods": methods,
"actual_setters": actual_setters,
"missing_setters": missing_setters,
"missing_attrs": missing_attrs
}

def main():
for name, mapping in TRANSACTIONS.items():
cls = mapping["cls"]
proto_cls = mapping["proto_cls"]
result = check_transaction(cls, proto_cls)

print(f"\n{bold(f'=== {name} ===')}")
print(f"{bold('Proto fields:')} {sorted(result['proto_fields'])}")
print(f"{bold('Attributes in __init__:')} {sorted(result['attributes'])}")

print(f"\n{bold('Actual found')}")
print(f" {bold('Attributes:')} {green(sorted(result['actual_attrs']))}")
print(f" {bold('Setter methods:')} {green(sorted(result['actual_setters']))}")

print(f"\n{bold('Missing')}")
print(f" {bold('Setter methods:')} {red(sorted(result['missing_setters']))}")
print(f" {bold('Attributes:')} {red(sorted(result['missing_attrs']))}")

if __name__ == "__main__":
main()
53 changes: 53 additions & 0 deletions scripts/src_vs_proto/steps_1_extracted_token_classes_with_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Auto-generated imports for token classes

from hiero_sdk_python.tokens.abstract_token_transfer_transaction import AbstractTokenTransferTransaction
from hiero_sdk_python.tokens.custom_fee import CustomFee
from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee
from hiero_sdk_python.tokens.custom_fractional_fee import CustomFractionalFee
from hiero_sdk_python.tokens.custom_royalty_fee import CustomRoyaltyFee
from hiero_sdk_python.tokens.fee_assessment_method import FeeAssessmentMethod
from hiero_sdk_python.tokens.hbar_allowance import HbarAllowance
from hiero_sdk_python.tokens.hbar_transfer import HbarTransfer
from hiero_sdk_python.tokens.nft_id import NftId
from hiero_sdk_python.tokens.supply_type import SupplyType
from hiero_sdk_python.tokens.token_airdrop_claim import TokenClaimAirdropTransaction
from hiero_sdk_python.tokens.token_airdrop_pending_id import PendingAirdropId
from hiero_sdk_python.tokens.token_airdrop_pending_record import PendingAirdropRecord
from hiero_sdk_python.tokens.token_airdrop_transaction import TokenAirdropTransaction
from hiero_sdk_python.tokens.token_airdrop_transaction_cancel import TokenCancelAirdropTransaction
from hiero_sdk_python.tokens.token_allowance import TokenAllowance
from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction
from hiero_sdk_python.tokens.token_burn_transaction import TokenBurnTransaction
from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction
from hiero_sdk_python.tokens.token_create_transaction import TokenCreateValidator
from hiero_sdk_python.tokens.token_create_transaction import TokenKeys
from hiero_sdk_python.tokens.token_create_transaction import TokenParams
from hiero_sdk_python.tokens.token_delete_transaction import TokenDeleteTransaction
from hiero_sdk_python.tokens.token_dissociate_transaction import TokenDissociateTransaction
from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import TokenFeeScheduleUpdateTransaction
from hiero_sdk_python.tokens.token_freeze_status import TokenFreezeStatus
from hiero_sdk_python.tokens.token_freeze_transaction import TokenFreezeTransaction
from hiero_sdk_python.tokens.token_grant_kyc_transaction import TokenGrantKycTransaction
from hiero_sdk_python.tokens.token_id import TokenId
from hiero_sdk_python.tokens.token_info import TokenInfo
from hiero_sdk_python.tokens.token_key_validation import TokenKeyValidation
from hiero_sdk_python.tokens.token_kyc_status import TokenKycStatus
from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction
from hiero_sdk_python.tokens.token_nft_allowance import TokenNftAllowance
from hiero_sdk_python.tokens.token_nft_info import TokenNftInfo
from hiero_sdk_python.tokens.token_nft_transfer import TokenNftTransfer
from hiero_sdk_python.tokens.token_pause_status import TokenPauseStatus
from hiero_sdk_python.tokens.token_pause_transaction import TokenPauseTransaction
from hiero_sdk_python.tokens.token_reject_transaction import TokenRejectTransaction
from hiero_sdk_python.tokens.token_relationship import TokenRelationship
from hiero_sdk_python.tokens.token_revoke_kyc_transaction import TokenRevokeKycTransaction
from hiero_sdk_python.tokens.token_transfer import TokenTransfer
from hiero_sdk_python.tokens.token_transfer_list import TokenTransferList
from hiero_sdk_python.tokens.token_type import TokenType
from hiero_sdk_python.tokens.token_unfreeze_transaction import TokenUnfreezeTransaction
from hiero_sdk_python.tokens.token_unpause_transaction import TokenUnpauseTransaction
from hiero_sdk_python.tokens.token_update_nfts_transaction import TokenUpdateNftsTransaction
from hiero_sdk_python.tokens.token_update_transaction import TokenUpdateKeys
from hiero_sdk_python.tokens.token_update_transaction import TokenUpdateParams
from hiero_sdk_python.tokens.token_update_transaction import TokenUpdateTransaction
from hiero_sdk_python.tokens.token_wipe_transaction import TokenWipeTransaction
53 changes: 53 additions & 0 deletions scripts/src_vs_proto/steps_1_find_all_file_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# scripts/extract_token_classes_with_paths.py
"""
Scans the hiero_sdk_python tokens source directory and extracts
all top-level classes in each Python file. Generates a Python file
with explicit imports for each class from its file.

Example output:

from hiero_sdk_python.tokens.token_freeze_transaction import TokenFreezeTransaction
from hiero_sdk_python.tokens.custom_fee import CustomFee
...
"""

import ast
from pathlib import Path

# Paths (adjust if needed)
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
TOKENS_DIR = PROJECT_ROOT / "src" / "hiero_sdk_python" / "tokens"
OUTPUT_DIR = PROJECT_ROOT / "scripts" / "src_vs_proto"
OUTPUT_FILE = OUTPUT_DIR / "steps_1_extracted_token_classes_with_paths.py"

def extract_classes_from_file(file_path: Path):
"""Return a list of top-level class names defined in a Python file."""
with open(file_path, "r", encoding="utf-8") as f:
tree = ast.parse(f.read(), filename=str(file_path))
return [node.name for node in tree.body if isinstance(node, ast.ClassDef)]

def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

imports = []

for py_file in TOKENS_DIR.glob("*.py"):
classes = extract_classes_from_file(py_file)
if not classes:
continue
module_name = py_file.stem # file name without .py
for cls in classes:
import_line = f"from hiero_sdk_python.tokens.{module_name} import {cls}"
imports.append(import_line)
print(f"Found class {cls} in {module_name}.py")

# Write the output file
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
f.write("# Auto-generated imports for token classes\n\n")
for line in sorted(imports):
f.write(f"{line}\n")

print(f"\nAll token classes with proper paths written to {OUTPUT_FILE}")

if __name__ == "__main__":
main()
Loading