Skip to content

Commit 91d86fb

Browse files
authored
Merge branch 'hiero-ledger:main' into main
2 parents 1f4f5cf + de658a2 commit 91d86fb

18 files changed

+1286
-83
lines changed

.github/workflows/examples.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
egress-policy: audit
2020

2121
- name: Checkout repository
22-
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
22+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
2323
with:
2424
fetch-depth: 0
2525

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
egress-policy: audit
2424

2525
- name: Checkout repository
26-
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
26+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
2727

2828
- name: Set up Python
2929
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0

.github/workflows/test.yml

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
name: Hiero Solo Integration Tests
1+
name: Hiero Solo Integration & Unit Tests
22

33
on:
44
push:
55
branches:
66
- '**'
7-
workflow_dispatch:
8-
pull_request:
7+
pull_request: {}
8+
workflow_dispatch: {}
99

1010
permissions:
1111
contents: read
@@ -27,7 +27,7 @@ jobs:
2727
egress-policy: audit
2828

2929
- name: Checkout repository
30-
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
30+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
3131

3232
- name: Set up Python ${{ matrix.python-version }}
3333
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
@@ -62,17 +62,52 @@ jobs:
6262
6363
- name: Install your package
6464
run: pip install -e .
65-
66-
- name: Run integration tests
65+
- name: Run all integration tests
66+
id: integration
67+
continue-on-error: true
68+
shell: bash
6769
env:
6870
OPERATOR_ID: ${{ steps.solo.outputs.accountId }}
6971
OPERATOR_KEY: ${{ steps.solo.outputs.privateKey }}
7072
ADMIN_KEY: ${{ steps.solo.outputs.privateKey }}
7173
PUBLIC_KEY: ${{ steps.solo.outputs.publicKey }}
7274
NETWORK: solo
7375
run: |
74-
uv run pytest -m integration
75-
76-
- name: Run unit tests
76+
set -o pipefail
77+
echo "🚀 Running integration tests..."
78+
uv run pytest tests/integration -v --disable-warnings --continue-on-collection-errors 2>&1 | tee result_integration.log
79+
pytest_exit=${PIPESTATUS[0]}
80+
echo "integration_failed=$pytest_exit" >> "$GITHUB_OUTPUT"
81+
cat result_integration.log
82+
if [ $pytest_exit -ne 0 ]; then
83+
echo "❌ Some integration tests failed"
84+
else
85+
echo "✅ All integration tests passed"
86+
fi
87+
88+
- name: Run all unit tests
89+
id: unit
90+
continue-on-error: true
91+
shell: bash
92+
run: |
93+
set -o pipefail
94+
echo "🚀 Running unit tests..."
95+
uv run pytest tests/unit -v --disable-warnings --continue-on-collection-errors 2>&1 | tee result_unit.log
96+
pytest_exit=${PIPESTATUS[0]}
97+
echo "unit_failed=$pytest_exit" >> "$GITHUB_OUTPUT"
98+
cat result_unit.log
99+
if [ $pytest_exit -ne 0 ]; then
100+
echo "❌ Some unit tests failed"
101+
else
102+
echo "✅ All unit tests passed"
103+
fi
104+
105+
- name: Fail workflow if any tests failed
106+
shell: bash
77107
run: |
78-
uv run pytest -m unit
108+
if [ "${{ steps.integration.outputs.integration_failed }}" != "0" ] || [ "${{ steps.unit.outputs.unit_failed }}" != "0" ]; then
109+
echo "❌ Some tests failed. Failing workflow."
110+
exit 1
111+
else
112+
echo "✅ All tests passed!"
113+
fi

.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: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
88

99

1010
### Added
11+
- Add `examples/token_create_transaction_max_automatic_token_associations_0.py` to demonstrate how `max_automatic_token_associations=0` behaves.
1112
- Add `examples/topic_id.py` to demonstrate `TopicId` opeartions
1213
- Add `examples/topic_message.py` to demonstrate `TopicMessage` and `TopicMessageChunk` with local mock data.
1314
- Added missing validation logic `fee_schedule_key` in integration `token_create_transaction_e2e_test.py` and ``token_update_transaction_e2e_test.py`.
1415
- Add `account_balance_query.py` example to demonstrate how to use the CryptoGetAccountBalanceQuery class.
1516
- Add `examples/token_create_transaction_admin_key.py` demonstrating admin key privileges for token management including token updates, key changes, and deletion (#798)
1617
- Add `examples/account_info.py` to demonstrate `AccountInfo` opeartions
1718
- Added `HbarUnit` class and Extend `Hbar` class to handle floating-point numbers
18-
19+
- feat: Allow `PrivateKey` to be used for keys in `TopicCreateTransaction` for consistency.
20+
- EvmAddress class
21+
- `alias`, `staked_account_id`, `staked_node_id` and `decline_staking_reward` fields to AccountCreateTransaction
22+
- `staked_account_id`, `staked_node_id` and `decline_staking_reward` fields to AccountInfo
1923

2024
### Changed
2125
- Refactored token-related example scripts (`token_delete.py`, `token_dissociate.py`, etc.) for improved readability and modularity. [#370]
@@ -24,10 +28,12 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
2428
- chore: bump protobuf from 6.33.0 to 6.33.1 (#796)
2529
- fix: Allow `max_automatic_token_associations` to be set to -1 (unlimited) in `AccountCreateTransaction` and add field to `AccountInfo`.
2630
- Allow `PrivateKey` to be used for keys in `TopicCreateTransaction` for consistency.
27-
31+
- Update github actions checkout from 5.0.0 to 5.0.1 (#814)
2832

2933
### Fixed
34+
- chore: fix test.yml workflow to log import errors (#740)
3035
- chore: fixed integration test names without a test prefix or postfix
36+
- Staked node ID id issue in the account_create_transationt_e2e_test
3137

3238

3339

@@ -496,4 +502,4 @@ contract_call_local_pb2.ContractLoginfo -> contract_types_pb2.ContractLoginfo
496502

497503
### Removed
498504

499-
- N/A
505+
- 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)