Skip to content

Commit 545f3fb

Browse files
feat: expand functionality token associate (#846)
Signed-off-by: Antonio Ceppellini <antonio.ceppellini@gmail.com> Signed-off-by: AntonioCeppellini <128388022+AntonioCeppellini@users.noreply.github.com>
1 parent 7947622 commit 545f3fb

File tree

5 files changed

+287
-31
lines changed

5 files changed

+287
-31
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
3535
- `staked_account_id`, `staked_node_id` and `decline_staking_reward` fields to AccountInfo
3636
- Added `examples/token_create_transaction_supply_key.py` to demonstrate token creation with and without a supply key.
3737
- Added `examples/token_create_transaction_kyc_key.py` to demonstrate KYC key functionality, including creating tokens with/without KYC keys, granting/revoking KYC status, and understanding KYC requirements for token transfers.
38+
- Add `set_token_ids`, `_from_proto`, `_validate_checksum` to TokenAssociateTransaction [#795]
3839
- Added BatchTransaction class
3940
- Add support for token metadata (bytes, max 100 bytes) in `TokenCreateTransaction`, including a new `set_metadata` setter, example, and tests. [#799]
4041
- Added `examples/token_create_transaction_token_fee_schedule.py` to demonstrate creating tokens with custom fee schedules and the consequences of not having it.
4142
- Added `examples/token_create_transaction_wipe_key.py` to demonstrate token wiping and the role of the wipe key.
4243
- Added `examples/account_allowance_approve_transaction_hbar.py` and `examples/account_allowance_delete_transaction_hbar.py`, deleted `examples/account_allowance_hbar.py`. [#775]
4344
- Added `docs\sdk_developers\training\receipts.md` as a training guide for users to understand hedera receipts.
45+
- Add `set_token_ids`, `_from_proto`, `_validate_checksum` to TokenAssociateTransaction [#795]
4446
- docs: added `network_and_client.md` with a table of contents, and added external example scripts (`client.py`).
4547

4648
### Changed

examples/tokens/token_associate_transaction.py

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ def setup_client():
5454
print("❌ Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.")
5555
sys.exit(1)
5656

57-
5857
def create_test_account(client, operator_key):
5958
"""
6059
Create a new test account for demonstration.
@@ -158,7 +157,7 @@ def associate_token_with_account(client, token_id, account_id, account_key):
158157
.sign(account_key)
159158
.execute(client)
160159
)
161-
160+
162161
if receipt.status != ResponseCode.SUCCESS:
163162
print(
164163
f"❌ Token association failed with status: {ResponseCode(receipt.status).name}"
@@ -170,6 +169,83 @@ def associate_token_with_account(client, token_id, account_id, account_key):
170169
print(f"❌ Error associating token with account: {e}")
171170
sys.exit(1)
172171

172+
173+
def associate_two_tokens_mixed_types_with_set_token_ids(client, token_id_1, token_id_2, account_id, account_key):
174+
"""
175+
Associate two tokens using set_token_ids() with mixed types:
176+
- first as TokenId
177+
- second as string
178+
"""
179+
try:
180+
receipt = (
181+
TokenAssociateTransaction()
182+
.set_account_id(account_id)
183+
.set_token_ids(
184+
[
185+
token_id_1, # TokenId instance
186+
str(token_id_2), # string representation → converted internally
187+
]
188+
)
189+
.freeze_with(client)
190+
.sign(account_key)
191+
.execute(client)
192+
)
193+
194+
if receipt.status != ResponseCode.SUCCESS:
195+
print(
196+
f"❌ Token association (mixed types) failed with status: "
197+
f"{ResponseCode(receipt.status).name}"
198+
)
199+
sys.exit(1)
200+
201+
print("✅ Success! Token association completed.")
202+
print(
203+
f" Account {account_id} can now hold and transfer tokens {token_id_1} and {token_id_2}"
204+
)
205+
except Exception as e:
206+
print(f"❌ Error in while associating tokens: {e}")
207+
sys.exit(1)
208+
209+
210+
def demonstrate_invalid_set_token_ids_usage(client, account_id, account_key):
211+
"""
212+
Example 4: demonstrate that set_token_ids() rejects invalid types,
213+
i.e. values that are neither TokenId nor string.
214+
"""
215+
print("`set_token_ids()` only accepts a list of TokenId or strings (also mixed)")
216+
invalid_value = 123 # ❌ This type is not supported from `set_token_ids()`
217+
print(f"Trying to associate a token using a list of {type(invalid_value)}")
218+
219+
try:
220+
receipt = (
221+
TokenAssociateTransaction()
222+
.set_account_id(account_id)
223+
.set_token_ids([invalid_value]) # this should fail
224+
.freeze_with(client)
225+
.sign(account_key)
226+
.execute(client)
227+
)
228+
229+
if receipt.status != ResponseCode.SUCCESS:
230+
print(
231+
f"❌ Token creation failed with status: {ResponseCode(receipt.status).name}"
232+
)
233+
sys.exit(1)
234+
235+
except Exception as e:
236+
if (
237+
type(e) == TypeError
238+
and "Invalid token_id type: expected TokenId or str, got" in e.args[0]
239+
):
240+
print(
241+
"✅ Correct behavior: invalid token_id type was rejected from `set_token_ids`"
242+
)
243+
print(f" Error: {e}")
244+
else:
245+
print(f"❌ Unexpected error while creating transaction: {e}")
246+
sys.exit(1)
247+
248+
173249
def verify_token_association(client, account_id, token_id):
174250
"""
175251
Verify that a token is properly associated with an account.
@@ -231,23 +307,43 @@ def main():
231307
print("\nSTEP 2: Creating a new account...")
232308
account_id, account_private_key = create_test_account(client, operator_key)
233309

234-
# Step 3: Create a new token
235-
print("\nSTEP 3: Creating a new fungible token...")
236-
token_id = create_fungible_token(client, operator_id, operator_key)
237-
238-
# Step 4: Associate the token with the new account
239-
print(f"\nSTEP 4: Associating token {token_id} with account {account_id}...")
240-
associate_token_with_account(client, token_id, account_id, account_private_key)
241-
242-
# Step 5: Verify the token association
243-
print(f"\nSTEP 5: Verifying token association...")
244-
is_associated = verify_token_association(client, account_id, token_id)
245-
310+
# step 3: How to not use set_token_ids
311+
print("\nSTEP 3: Demonstrating invalid input handling in set_token_ids...")
312+
demonstrate_invalid_set_token_ids_usage(client, account_id, account_private_key)
313+
314+
# Step 4: new tokens
315+
print("\nSTEP 4: Creating new fungible tokens...")
316+
token_id_0 = create_fungible_token(client, operator_id, operator_key)
317+
token_id_1 = create_fungible_token(client, operator_id, operator_key)
318+
token_id_2 = create_fungible_token(client, operator_id, operator_key)
319+
320+
# Step 5: Associate a single token with the new account
321+
print(f"\nSTEP 5: Associating token {token_id_0} with account {account_id}...")
322+
associate_token_with_account(client, token_id_0, account_id, account_private_key)
323+
324+
# Step 6: Associate multiple tokens with the new account
325+
print(
326+
f"\nSTEP 6: Associating token {token_id_1} and token {token_id_2} with account {account_id}..."
327+
)
328+
associate_two_tokens_mixed_types_with_set_token_ids(
329+
client, token_id_1, token_id_2, account_id, account_private_key
330+
)
331+
332+
# Step 7: Verify the token association
333+
print(f"\nSTEP 7: Verifying token association...")
334+
is_associated = verify_token_association(client, account_id, token_id_0)
335+
is_associated = verify_token_association(client, account_id, token_id_1)
336+
is_associated = verify_token_association(client, account_id, token_id_2)
337+
338+
tokens = [token_id_0, token_id_1, token_id_2]
246339
# Summary
247340
print("\n" + "=" * 50)
248341
print("🎉 Token Association Demo Completed Successfully!")
249342
print(f" New Account: {account_id}")
250-
print(f" New Token: {token_id}")
343+
print(" New Tokens:")
344+
345+
for token in tokens:
346+
print(f" -{token}")
251347
print(f" Association: {'✅ VERIFIED' if is_associated else '❌ FAILED'}")
252348
print("=" * 50)
253349

src/hiero_sdk_python/tokens/token_associate_transaction.py

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,28 @@
55
Provides TokenAssociateTransaction, a subclass of Transaction for associating
66
tokens with accounts on the Hedera network using the Hedera Token Service (HTS) API.
77
"""
8-
from typing import Optional, List
8+
9+
from typing import Optional, List, Union
910

1011
from hiero_sdk_python.account.account_id import AccountId
1112
from hiero_sdk_python.channels import _Channel
1213
from hiero_sdk_python.executable import _Method
1314
from hiero_sdk_python.hapi.services import token_associate_pb2, transaction_pb2
14-
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
15-
SchedulableTransactionBody,
16-
)
15+
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import SchedulableTransactionBody
16+
1717
from hiero_sdk_python.tokens.token_id import TokenId
1818
from hiero_sdk_python.transaction.transaction import Transaction
1919

20+
TokenIdLike = Union[TokenId, str]
21+
2022

2123
class TokenAssociateTransaction(Transaction):
2224
"""
2325
Represents a token associate transaction on the Hedera network.
2426
2527
This transaction associates the specified tokens with an account,
2628
allowing the account to hold and transact with those tokens.
27-
29+
2830
Inherits from the base Transaction class and implements the required methods
2931
to build and execute a token association transaction.
3032
"""
@@ -47,7 +49,7 @@ def __init__(
4749
self._default_transaction_fee: int = 500_000_000
4850

4951
def set_account_id(self, account_id: AccountId) -> "TokenAssociateTransaction":
50-
"""
52+
"""
5153
Sets the account ID for the token association transaction.
5254
Args:
5355
account_id (AccountId): The account ID to associate tokens with.
@@ -64,13 +66,41 @@ def add_token_id(self, token_id: TokenId) -> "TokenAssociateTransaction":
6466
self.token_ids.append(token_id)
6567
return self
6668

69+
def set_token_ids(self, token_ids: List[TokenId]) -> "TokenAssociateTransaction":
70+
"""
71+
Sets the list of token IDs for the token association transaction.
72+
73+
This mirrors the JavaScript SDK's `setTokenIds()` API,
74+
providing a convenient way to associate multiple tokens at once.
75+
76+
Args:
77+
token_ids: Iterable of TokenId instances or string representations.
78+
79+
Returns:
80+
TokenAssociateTransaction.
81+
"""
82+
self._require_not_frozen()
83+
tokens_to_add: List[TokenId] = []
84+
for token_id in token_ids:
85+
if isinstance(token_id, TokenId):
86+
tokens_to_add.append(token_id)
87+
elif isinstance(token_id, str):
88+
tokens_to_add.append(TokenId.from_string(token_id))
89+
else:
90+
raise TypeError(
91+
f"Invalid token_id type: expected TokenId or str, got {type(token_id).__name__}"
92+
)
93+
94+
self.token_ids = tokens_to_add
95+
return self
96+
6797
def _build_proto_body(self) -> token_associate_pb2.TokenAssociateTransactionBody:
6898
"""
6999
Returns the protobuf body for the token associate transaction.
70-
100+
71101
Returns:
72102
TokenAssociateTransactionBody: The protobuf body for this transaction.
73-
103+
74104
Raises:
75105
ValueError: If account ID or token IDs are not set.
76106
"""
@@ -81,7 +111,26 @@ def _build_proto_body(self) -> token_associate_pb2.TokenAssociateTransactionBody
81111
account=self.account_id._to_proto(),
82112
tokens=[token_id._to_proto() for token_id in self.token_ids]
83113
)
84-
114+
115+
@classmethod
116+
def _from_proto(
117+
cls, body: token_associate_pb2.TokenAssociateTransactionBody
118+
) -> "TokenAssociateTransaction":
119+
"""
120+
Construct a TokenAssociateTransaction from its protobuf.
121+
"""
122+
account_id = AccountId._from_proto(body.account)
123+
token_ids: List[TokenId] = []
124+
125+
for proto_token in body.tokens:
126+
token_id = TokenId._from_proto(proto_token)
127+
token_ids.append(token_id)
128+
129+
return cls(
130+
account_id=account_id,
131+
token_ids=token_ids,
132+
)
133+
85134
def build_transaction_body(self) -> transaction_pb2.TransactionBody:
86135
"""
87136
Builds and returns the protobuf transaction body for token association.
@@ -94,7 +143,7 @@ def build_transaction_body(self) -> transaction_pb2.TransactionBody:
94143
transaction_body.tokenAssociate.CopyFrom(token_associate_body)
95144

96145
return transaction_body
97-
146+
98147
def build_scheduled_body(self) -> SchedulableTransactionBody:
99148
"""
100149
Builds the scheduled transaction body for this token associate transaction.
@@ -107,6 +156,19 @@ def build_scheduled_body(self) -> SchedulableTransactionBody:
107156
schedulable_body.tokenAssociate.CopyFrom(token_associate_body)
108157
return schedulable_body
109158

159+
def _validate_checksums(self, client) -> None:
160+
"""
161+
Validate the checksums for all IDs (account + tokens) in this transaction.
162+
163+
Mirrors the style used across the SDK: each ID calls its own
164+
validate_checksum(client) method.
165+
"""
166+
if self.account_id is not None:
167+
self.account_id.validate_checksum(client)
168+
169+
for token_id in self.token_ids:
170+
token_id.validate_checksum(client)
171+
110172
def _get_method(self, channel: _Channel) -> _Method:
111173
return _Method(
112174
transaction_func=channel.token.associateTokens,

tests/integration/token_associate_transaction_e2e_test.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,25 @@ def test_integration_token_associate_transaction_can_execute():
3333
token_id = create_fungible_token(env)
3434
assert token_id is not None, "TokenID not found in receipt. Token may not have been created."
3535

36-
associate_transaction = TokenAssociateTransaction(
37-
account_id=new_account_id,
38-
token_ids=[token_id]
36+
associate_transaction = (
37+
TokenAssociateTransaction()
38+
.set_account_id(new_account_id)
39+
.set_token_ids([token_id])
3940
)
40-
41+
4142
associate_transaction.freeze_with(env.client)
43+
44+
# building transaction body
45+
transaction_body = associate_transaction.build_transaction_body()
46+
47+
proto_body = transaction_body.tokenAssociate
48+
49+
# Recreating transaction from protobuf
50+
transaction_from_proto = TokenAssociateTransaction._from_proto(proto_body)
51+
52+
assert transaction_from_proto.account_id == associate_transaction.account_id
53+
assert transaction_from_proto.token_ids == associate_transaction.token_ids
54+
4255
associate_transaction.sign(new_account_private_key)
4356
associate_receipt = associate_transaction.execute(env.client)
4457

@@ -55,4 +68,4 @@ def test_integration_token_associate_transaction_can_execute():
5568

5669
assert dissociate_receipt.status == ResponseCode.SUCCESS, f"Token dissociation failed with status: {ResponseCode(dissociate_receipt.status).name}"
5770
finally:
58-
env.close()
71+
env.close()

0 commit comments

Comments
 (0)