Skip to content

Commit 0bf5a9c

Browse files
authored
chore: add example for max automatic token associations (hiero-ledger#819)
Signed-off-by: advay-sinha <advaysinhaa@gmail.com> Signed-off-by: Advay Sinha <advaysinhaa@gmail.com>
1 parent 7d14c4a commit 0bf5a9c

File tree

3 files changed

+246
-3
lines changed

3 files changed

+246
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ src/hiero_sdk_python/hapi
3333

3434
# Lock files
3535
uv.lock
36-
pdm.lock
36+
pdm.lock
37+
pubkey.asc

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
88

99

1010
### Added
11-
11+
- Add `examples/token_create_transaction_max_automatic_token_associations_0.py` to demonstrate how `max_automatic_token_associations=0` behaves.
1212
- Add `examples/topic_id.py` to demonstrate `TopicId` opeartions
1313
- Add `examples/topic_message.py` to demonstrate `TopicMessage` and `TopicMessageChunk` with local mock data.
1414
- Added missing validation logic `fee_schedule_key` in integration `token_create_transaction_e2e_test.py` and ``token_update_transaction_e2e_test.py`.
@@ -498,4 +498,4 @@ contract_call_local_pb2.ContractLoginfo -> contract_types_pb2.ContractLoginfo
498498

499499
### Removed
500500

501-
- N/A
501+
- N/A
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
"""
2+
Example: demonstrate how max_automatic_token_associations=0 behaves.
3+
The script walks through:
4+
1. Creating a fungible token on Hedera testnet (default network).
5+
2. Creating an account whose max automatic associations is zero.
6+
3. Attempting a token transfer (it fails because no association exists).
7+
4. Associating the token for that account.
8+
5. Transferring again, this time succeeding.
9+
Run with:
10+
uv run examples/token_create_transaction_max_automatic_token_associations_0
11+
"""
12+
13+
import os
14+
import sys
15+
from typing import Tuple
16+
17+
from dotenv import load_dotenv
18+
19+
from hiero_sdk_python import (
20+
AccountCreateTransaction,
21+
AccountId,
22+
AccountInfoQuery,
23+
Client,
24+
Hbar,
25+
Network,
26+
PrivateKey,
27+
ResponseCode,
28+
TokenAssociateTransaction,
29+
TokenCreateTransaction,
30+
TokenId,
31+
TransferTransaction,
32+
)
33+
from hiero_sdk_python.exceptions import PrecheckError
34+
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
35+
36+
load_dotenv()
37+
network_name = os.getenv("NETWORK", "testnet").lower()
38+
TOKENS_TO_TRANSFER = 10
39+
40+
41+
def setup_client() -> Tuple[Client, AccountId, PrivateKey]:
42+
"""Initialize a client using OPERATOR_ID and OPERATOR_KEY from the environment."""
43+
network = Network(network_name)
44+
print(f"Connecting to Hedera {network_name} network.")
45+
client = Client(network)
46+
47+
try:
48+
operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", ""))
49+
operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", ""))
50+
except (TypeError, ValueError):
51+
print("ERROR: Please set OPERATOR_ID and OPERATOR_KEY in your .env file.")
52+
sys.exit(1)
53+
54+
client.set_operator(operator_id, operator_key)
55+
return client, operator_id, operator_key
56+
57+
58+
def create_demo_token(
59+
client: Client, operator_id: AccountId, operator_key: PrivateKey
60+
) -> TokenId:
61+
"""Create a fungible token whose treasury is the operator."""
62+
print("\nSTEP 1: Creating the fungible demo token...")
63+
# Build and sign the fungible token creation transaction using the operator as treasury.
64+
tx = (
65+
TokenCreateTransaction()
66+
.set_token_name("MaxAssociationsToken")
67+
.set_token_symbol("MAX0")
68+
.set_decimals(0)
69+
.set_initial_supply(1_000)
70+
.set_treasury_account_id(operator_id)
71+
.freeze_with(client)
72+
.sign(operator_key)
73+
)
74+
receipt = _receipt_from_response(tx.execute(client), client)
75+
status = _as_response_code(receipt.status)
76+
if status != ResponseCode.SUCCESS or receipt.token_id is None:
77+
print(f"ERROR: Token creation failed with status {status.name}.")
78+
sys.exit(1)
79+
80+
print(f"Token created: {receipt.token_id}")
81+
return receipt.token_id
82+
83+
84+
def create_max_account(client: Client, operator_key: PrivateKey) -> Tuple[AccountId, PrivateKey]:
85+
"""Create an account whose max automatic associations equals zero."""
86+
print("\nSTEP 2: Creating account 'max' with max automatic associations set to 0...")
87+
max_key = PrivateKey.generate()
88+
# Configure the new account to require explicit associations before accepting tokens.
89+
tx = (
90+
AccountCreateTransaction()
91+
.set_key(max_key.public_key())
92+
.set_initial_balance(Hbar(5))
93+
.set_account_memo("max (auto-assoc = 0)")
94+
.set_max_automatic_token_associations(0)
95+
.freeze_with(client)
96+
.sign(operator_key)
97+
)
98+
receipt = _receipt_from_response(tx.execute(client), client)
99+
status = _as_response_code(receipt.status)
100+
if status != ResponseCode.SUCCESS or receipt.account_id is None:
101+
print(f"ERROR: Account creation failed with status {status.name}.")
102+
sys.exit(1)
103+
104+
print(f"Account created: {receipt.account_id}")
105+
return receipt.account_id, max_key
106+
107+
108+
def show_account_settings(client: Client, account_id: AccountId) -> None:
109+
"""Print the account's max automatic associations and known token relationships."""
110+
print("\nSTEP 3: Querying account info...")
111+
# Fetch account information to verify configuration before attempting transfers.
112+
info = AccountInfoQuery(account_id).execute(client)
113+
print(
114+
f"Account {account_id} max_automatic_token_associations: "
115+
f"{info.max_automatic_token_associations}"
116+
)
117+
print(f"Token relationships currently tracked: {len(info.token_relationships)}")
118+
119+
120+
def try_transfer(
121+
client: Client,
122+
operator_id: AccountId,
123+
operator_key: PrivateKey,
124+
receiver_id: AccountId,
125+
token_id: TokenId,
126+
expect_success: bool,
127+
) -> bool:
128+
"""Attempt a token transfer and return True if it succeeds."""
129+
desired = "should succeed" if expect_success else "expected to fail"
130+
print(
131+
f"\nSTEP 4: Attempting to transfer {TOKENS_TO_TRANSFER} tokens ({desired}) "
132+
f"from {operator_id} to {receiver_id}..."
133+
)
134+
try:
135+
# Transfer tokens from the operator treasury to the new account.
136+
tx = (
137+
TransferTransaction()
138+
.add_token_transfer(token_id, operator_id, -TOKENS_TO_TRANSFER)
139+
.add_token_transfer(token_id, receiver_id, TOKENS_TO_TRANSFER)
140+
.freeze_with(client)
141+
.sign(operator_key)
142+
)
143+
receipt = _receipt_from_response(tx.execute(client), client)
144+
status = _as_response_code(receipt.status)
145+
success = status == ResponseCode.SUCCESS
146+
if success:
147+
print(f"Transfer status: {status.name}")
148+
else:
149+
print(f"Transfer failed with status: {status.name}")
150+
return success
151+
except PrecheckError as err:
152+
print(f"Precheck failed with status { _response_code_name(err.status) }")
153+
return False
154+
except Exception as exc: # pragma: no cover - unexpected runtime/network failures
155+
print(f"Unexpected error while transferring tokens: {exc}")
156+
return False
157+
158+
159+
def associate_token(
160+
client: Client, account_id: AccountId, account_key: PrivateKey, token_id: TokenId
161+
) -> None:
162+
"""Explicitly associate the token so the account can hold balances."""
163+
print("\nSTEP 5: Associating the token for account 'max'...")
164+
# Submit the token association signed by the new account's private key.
165+
tx = (
166+
TokenAssociateTransaction()
167+
.set_account_id(account_id)
168+
.add_token_id(token_id)
169+
.freeze_with(client)
170+
.sign(account_key)
171+
)
172+
receipt = _receipt_from_response(tx.execute(client), client)
173+
status = _as_response_code(receipt.status)
174+
if status != ResponseCode.SUCCESS:
175+
print(f"ERROR: Token association failed with status {status.name}.")
176+
sys.exit(1)
177+
print(f"Token {token_id} successfully associated with account {account_id}.")
178+
179+
180+
def _receipt_from_response(result, client) -> TransactionReceipt:
181+
"""Normalize transaction return types to a TransactionReceipt instance."""
182+
if isinstance(result, TransactionReceipt):
183+
return result
184+
return result.get_receipt(client)
185+
186+
187+
def _response_code_name(status) -> str:
188+
"""Convert an enum/int status into a readable ResponseCode name."""
189+
return _as_response_code(status).name
190+
191+
192+
def _as_response_code(value) -> ResponseCode:
193+
"""Ensure we always treat codes as ResponseCode enums.
194+
195+
Some transactions return raw integer codes rather than ResponseCode enums,
196+
so we normalize before accessing `.name`.
197+
"""
198+
if isinstance(value, ResponseCode):
199+
return value
200+
return ResponseCode(value)
201+
202+
203+
def main() -> None:
204+
"""Execute the entire flow end-to-end."""
205+
client, operator_id, operator_key = setup_client()
206+
token_id = create_demo_token(client, operator_id, operator_key)
207+
max_account_id, max_account_key = create_max_account(client, operator_key)
208+
show_account_settings(client, max_account_id)
209+
210+
first_attempt_success = try_transfer(
211+
client,
212+
operator_id,
213+
operator_key,
214+
max_account_id,
215+
token_id,
216+
expect_success=False,
217+
)
218+
if first_attempt_success:
219+
print(
220+
"WARNING: transfer succeeded even though no association existed. "
221+
"The account may already be associated with this token."
222+
)
223+
else:
224+
associate_token(client, max_account_id, max_account_key, token_id)
225+
second_attempt_success = try_transfer(
226+
client,
227+
operator_id,
228+
operator_key,
229+
max_account_id,
230+
token_id,
231+
expect_success=True,
232+
)
233+
if second_attempt_success:
234+
print("\nTransfer succeeded after explicitly associating the token.")
235+
else:
236+
print(
237+
"\nTransfer still failed after associating the token. "
238+
"Verify balances, association status, and token configuration."
239+
)
240+
241+
if __name__ == "__main__":
242+
main()

0 commit comments

Comments
 (0)