|
| 1 | +# Coding Token Transactions |
| 2 | + |
| 3 | +This guide provides a high-level overview of how token transactions are constructed in the Hiero Python SDK. It is intended for developers adding new transaction types (e.g., `TokenMintTransaction`, `TokenBurnTransaction`) to understand the architectural requirements. |
| 4 | + |
| 5 | +## 1. Inheritance Structure |
| 6 | + |
| 7 | +All token transactions must inherit from the base `Transaction` class. |
| 8 | + |
| 9 | +```python |
| 10 | +from hiero_sdk_python.transaction.transaction import Transaction |
| 11 | + |
| 12 | +class TokenAssociateTransaction(Transaction): |
| 13 | + """ |
| 14 | + Represents a token associate transaction on the Hedera network. |
| 15 | + """ |
| 16 | + |
| 17 | +``` |
| 18 | + |
| 19 | +By inheriting from `Transaction`, your new class automatically gains essential functionality: |
| 20 | + |
| 21 | +- **Signing:** `sign(private_key)` |
| 22 | +- **Freezing:** `freeze()` and `freeze_with(client)` |
| 23 | +- **Execution:** `execute(client)` |
| 24 | +- **Fee Calculation:** Handling `transaction_fee` and `max_transaction_fee` |
| 25 | +- **ID Management:** `transaction_id`, `node_account_id` |
| 26 | + |
| 27 | +Your job in the subclass is to define **what** data is being sent, while the base class handles **how** it is sent. |
| 28 | + |
| 29 | +## 2. Initialization and Defaults |
| 30 | + |
| 31 | +In the Python SDK, we prefer a flexible initialization pattern. Constructors (`__init__`) should generally default arguments to `None`. This allows users to instantiate an empty transaction and configure it later using setters (Method Chaining). |
| 32 | + |
| 33 | +### Pattern: |
| 34 | + |
| 35 | +```python |
| 36 | +def __init__( |
| 37 | + self, |
| 38 | + account_id: Optional[AccountId] = None, |
| 39 | + token_ids: Optional[List[TokenId]] = None |
| 40 | +) -> None: |
| 41 | + super().__init__() # Initialize the base Transaction |
| 42 | + self.account_id = account_id |
| 43 | + self.token_ids = list(token_ids) if token_ids is not None else [] |
| 44 | + |
| 45 | +``` |
| 46 | + |
| 47 | +- **Why?** It supports both "all-in-one" instantiation and progressive building. |
| 48 | +- **Note:** Always call `super().__init__()` to initialize base fields like the signature map and transaction ID. |
| 49 | + |
| 50 | +## 3. Setters and Method Chaining |
| 51 | + |
| 52 | +To provide a "Pythonic" builder interface, every field should have a corresponding setter method. These methods must: |
| 53 | + |
| 54 | +1. Check that the transaction is not frozen (immutable). |
| 55 | +2. Return `self` to allow chaining (e.g., `.set_foo().set_bar()`). |
| 56 | + |
| 57 | +### Pattern: |
| 58 | + |
| 59 | +```python |
| 60 | +def set_account_id(self, account_id: AccountId) -> "TokenAssociateTransaction": |
| 61 | + """Sets the account ID for the token association transaction.""" |
| 62 | + self._require_not_frozen() # Critical check from base class |
| 63 | + self.account_id = account_id |
| 64 | + return self |
| 65 | + |
| 66 | +``` |
| 67 | + |
| 68 | +## 4. Protobuf Conversion |
| 69 | + |
| 70 | +The Hedera network communicates via Protocol Buffers (Protobuf). Your transaction class is responsible for converting its Python fields into a Protobuf message. |
| 71 | + |
| 72 | +This is typically done in three steps: |
| 73 | + |
| 74 | +### Step A: Internal Helper `_build_proto_body()` |
| 75 | + |
| 76 | +Create a helper method that constructs the _specific_ body for your transaction type (e.g., `TokenAssociateTransactionBody`). |
| 77 | + |
| 78 | +```python |
| 79 | +def _build_proto_body(self) -> token_associate_pb2.TokenAssociateTransactionBody: |
| 80 | + if not self.account_id or not self.token_ids: |
| 81 | + raise ValueError("Account ID and token IDs must be set.") |
| 82 | + |
| 83 | + return token_associate_pb2.TokenAssociateTransactionBody( |
| 84 | + account=self.account_id._to_proto(), |
| 85 | + tokens=[token_id._to_proto() for token_id in self.token_ids] |
| 86 | + ) |
| 87 | + |
| 88 | +``` |
| 89 | + |
| 90 | +### Step B: Implement `build_transaction_body()` |
| 91 | + |
| 92 | +This abstract method from `Transaction` must be implemented. It packages your specific body into the main `TransactionBody`. |
| 93 | + |
| 94 | +```python |
| 95 | +def build_transaction_body(self) -> transaction_pb2.TransactionBody: |
| 96 | + # 1. Build your specific proto body |
| 97 | + token_associate_body = self._build_proto_body() |
| 98 | + |
| 99 | + # 2. Get the base body (contains NodeID, TxID, Fees, Memo) |
| 100 | + transaction_body = self.build_base_transaction_body() |
| 101 | + |
| 102 | + # 3. Attach your specific body to the main transaction |
| 103 | + transaction_body.tokenAssociate.CopyFrom(token_associate_body) |
| 104 | + |
| 105 | + return transaction_body |
| 106 | + |
| 107 | +``` |
| 108 | + |
| 109 | +### Step C: Implement `build_scheduled_body()` |
| 110 | + |
| 111 | +To support scheduled transactions, you must also implement this method. It is nearly identical to step B but uses `SchedulableTransactionBody`. |
| 112 | + |
| 113 | +```python |
| 114 | +def build_scheduled_body(self) -> SchedulableTransactionBody: |
| 115 | + token_associate_body = self._build_proto_body() |
| 116 | + schedulable_body = self.build_base_scheduled_body() |
| 117 | + schedulable_body.tokenAssociate.CopyFrom(token_associate_body) |
| 118 | + return schedulable_body |
| 119 | + |
| 120 | +``` |
| 121 | + |
| 122 | +## 5. Network Routing (`_get_method`) |
| 123 | + |
| 124 | +Finally, you must tell the base class which gRPC method to call on the node. This routes the transaction to the correct service (e.g., Token Service vs. Crypto Service). |
| 125 | + |
| 126 | +```python |
| 127 | +def _get_method(self, channel: _Channel) -> _Method: |
| 128 | + return _Method( |
| 129 | + transaction_func=channel.token.associateTokens, # The gRPC function |
| 130 | + query_func=None |
| 131 | + ) |
| 132 | + |
| 133 | +``` |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## Summary Checklist |
| 138 | + |
| 139 | +When creating a new token transaction, ensure you have: |
| 140 | + |
| 141 | +- [ ] **Inherited** from `Transaction`. |
| 142 | +- [ ] **Initialized** fields to `None` in `__init__`. |
| 143 | +- [ ] **Implemented Setters** that call `self._require_not_frozen()` and return `self`. |
| 144 | +- [ ] **Implemented** `_build_proto_body()` to map Python fields to Protobuf. |
| 145 | +- [ ] **Implemented** `build_transaction_body()` to merge your body into the main transaction. |
| 146 | +- [ ] **Implemented** `build_scheduled_body()` for scheduling support. |
| 147 | +- [ ] **Implemented** `_get_method()` to define the gRPC endpoint. |
| 148 | + |
| 149 | +The base `Transaction` class will handle the heavy lifting of freezing, signing, serialization, and execution. |
| 150 | + |
| 151 | +--- |
0 commit comments