Skip to content

Commit de65379

Browse files
authored
feat: add _require_frozen() (#92)
* feat: implement require_frozen functionality in Transaction class Signed-off-by: dosi <dosi.kolev@limechain.tech> * refactor: update sign() and to_proto() to use _require_frozen() Signed-off-by: dosi <dosi.kolev@limechain.tech> * test: add mock_client() in conftest as fixture Signed-off-by: dosi <dosi.kolev@limechain.tech> * test: fix unit tests Signed-off-by: dosi <dosi.kolev@limechain.tech> * test: add test for signing without freezing Signed-off-by: dosi <dosi.kolev@limechain.tech> --------- Signed-off-by: dosi <dosi.kolev@limechain.tech>
1 parent cd8a8de commit de65379

11 files changed

+164
-89
lines changed

src/hiero_sdk_python/transaction/transaction.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ def sign(self, private_key):
146146
Raises:
147147
Exception: If the transaction body has not been built.
148148
"""
149-
if self.transaction_body_bytes is None:
150-
self.transaction_body_bytes = self.build_transaction_body().SerializeToString()
149+
# We require the transaction to be frozen before signing
150+
self._require_frozen()
151151

152152
signature = private_key.sign(self.transaction_body_bytes)
153153

@@ -172,8 +172,8 @@ def to_proto(self):
172172
Raises:
173173
Exception: If the transaction body has not been built.
174174
"""
175-
if self.transaction_body_bytes is None:
176-
raise Exception("Transaction must be signed before calling to_proto()")
175+
# We require the transaction to be frozen before converting to protobuf
176+
self._require_frozen()
177177

178178
signed_transaction = transaction_contents_pb2.SignedTransaction(
179179
bodyBytes=self.transaction_body_bytes,
@@ -320,6 +320,19 @@ def _require_not_frozen(self):
320320
if self.transaction_body_bytes is not None:
321321
raise Exception("Transaction is immutable; it has been frozen.")
322322

323+
def _require_frozen(self):
324+
"""
325+
Ensures the transaction is frozen before allowing operations that require a frozen transaction.
326+
327+
This method checks if the transaction has been frozen by verifying that transaction_body_bytes
328+
has been set.
329+
330+
Raises:
331+
Exception: If the transaction has not been frozen yet.
332+
"""
333+
if self.transaction_body_bytes is None:
334+
raise Exception("Transaction is not frozen")
335+
323336
def set_transaction_memo(self, memo):
324337
"""
325338
Sets the memo field for the transaction.

tests/unit/conftest.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import pytest
22
from hiero_sdk_python.account.account_id import AccountId
3+
from hiero_sdk_python.client.network import Network
4+
from hiero_sdk_python.client.client import Client
5+
from hiero_sdk_python.logger.log_level import LogLevel
6+
from hiero_sdk_python.node import _Node
37
from hiero_sdk_python.consensus.topic_id import TopicId
48
from hiero_sdk_python.crypto.private_key import PrivateKey
59
from hiero_sdk_python.tokens.token_id import TokenId
@@ -38,4 +42,18 @@ def private_key():
3842
@pytest.fixture
3943
def topic_id():
4044
"""Fixture to create a topic ID for testing."""
41-
return TopicId(0, 0, 1234)
45+
return TopicId(0, 0, 1234)
46+
47+
@pytest.fixture
48+
def mock_client():
49+
"""Fixture to provide a mock client with hardcoded nodes for testing purposes."""
50+
nodes = [_Node(AccountId(0, 0, 3), "node1.example.com:50211", None)]
51+
network = Network(nodes=nodes)
52+
client = Client(network)
53+
client.logger.set_level(LogLevel.DISABLED)
54+
55+
operator_key = PrivateKey.generate()
56+
operator_id = AccountId(0, 0, 1984)
57+
client.set_operator(operator_id, operator_key)
58+
59+
return client

tests/unit/test_account_create_transaction.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,25 @@ def test_account_create_transaction():
131131
assert receipt.status == ResponseCode.SUCCESS, "Transaction should have succeeded"
132132
assert receipt.accountId.num == 1234, "Should have created account with ID 1234"
133133

134+
def test_sign_account_create_without_freezing_raises_error(mock_account_ids):
135+
"""Test that signing a transaction without freezing it first raises an error."""
136+
operator_id, node_account_id = mock_account_ids
137+
138+
new_private_key = PrivateKey.generate()
139+
new_public_key = new_private_key.public_key()
140+
141+
account_tx = (
142+
AccountCreateTransaction()
143+
.set_key(new_public_key)
144+
.set_initial_balance(100000000)
145+
.set_account_memo("Test account")
146+
)
147+
account_tx.transaction_id = generate_transaction_id(operator_id)
148+
account_tx.node_account_id = node_account_id
149+
150+
with pytest.raises(Exception, match="Transaction is not frozen"):
151+
account_tx.sign(new_private_key)
152+
134153
@pytest.fixture
135154
def mock_account_ids():
136155
"""Fixture to provide mock account IDs for testing."""

tests/unit/test_token_associate_transaction.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,24 @@ def test_missing_fields():
5050
with pytest.raises(ValueError, match="Account ID and token IDs must be set."):
5151
associate_tx.build_transaction_body()
5252

53-
# This test uses fixture mock_account_ids as parameter
54-
def test_sign_transaction(mock_account_ids):
53+
# This test uses fixture (mock_account_ids, mock_client) as parameter
54+
def test_sign_transaction(mock_account_ids, mock_client):
5555
"""Test signing the token associate transaction with a private key."""
56-
account_id, _, node_account_id, token_id_1, _ = mock_account_ids
56+
account_id, _, _, token_id_1, _ = mock_account_ids
57+
5758
associate_tx = TokenAssociateTransaction()
5859
associate_tx.set_account_id(account_id)
5960
associate_tx.add_token_id(token_id_1)
6061
associate_tx.transaction_id = generate_transaction_id(account_id)
61-
associate_tx.node_account_id = node_account_id
6262

6363
private_key = MagicMock()
6464
private_key.sign.return_value = b'signature'
6565
private_key.public_key().to_bytes_raw.return_value = b'public_key'
66-
66+
67+
# Freeze the transaction
68+
associate_tx.freeze_with(mock_client)
69+
70+
# Sign the transaction
6771
associate_tx.sign(private_key)
6872

6973
assert len(associate_tx.signature_map.sigPair) == 1
@@ -72,20 +76,22 @@ def test_sign_transaction(mock_account_ids):
7276
assert sig_pair.pubKeyPrefix == b'public_key'
7377
assert sig_pair.ed25519 == b'signature'
7478

75-
# This test uses fixture mock_account_ids as parameter
76-
def test_to_proto(mock_account_ids):
79+
# This test uses fixture (mock_account_ids, mock_client) as parameter
80+
def test_to_proto(mock_account_ids, mock_client):
7781
"""Test converting the token associate transaction to protobuf format after signing."""
78-
account_id, _, node_account_id, token_id_1, _ = mock_account_ids
82+
account_id, _, _, token_id_1, _ = mock_account_ids
83+
7984
associate_tx = TokenAssociateTransaction()
8085
associate_tx.set_account_id(account_id)
8186
associate_tx.add_token_id(token_id_1)
8287
associate_tx.transaction_id = generate_transaction_id(account_id)
83-
associate_tx.node_account_id = node_account_id
8488

8589
private_key = MagicMock()
8690
private_key.sign.return_value = b'signature'
8791
private_key.public_key().to_bytes_raw.return_value = b'public_key'
8892

93+
associate_tx.freeze_with(mock_client)
94+
8995
associate_tx.sign(private_key)
9096
proto = associate_tx.to_proto()
9197

tests/unit/test_token_create_transaction.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,10 @@ def test_token_creation_validation(
237237

238238
########### Tests for Signing and Protobuf Conversion ###########
239239

240-
# This test uses fixture mock_account_ids as parameter
241-
def test_sign_transaction(mock_account_ids):
240+
# This test uses fixture (mock_account_ids, mock_client) as parameter
241+
def test_sign_transaction(mock_account_ids, mock_client):
242242
"""Test signing the token creation transaction that has multiple keys."""
243-
treasury_account, _, node_account_id, _, _ = mock_account_ids
243+
treasury_account, _, _, _, _ = mock_account_ids
244244

245245
# Mock keys
246246
private_key = MagicMock()
@@ -270,7 +270,8 @@ def test_sign_transaction(mock_account_ids):
270270
token_tx.set_freeze_key(private_key_freeze)
271271

272272
token_tx.transaction_id = generate_transaction_id(treasury_account)
273-
token_tx.node_account_id = node_account_id
273+
274+
token_tx.freeze_with(mock_client)
274275

275276
# Sign with both sign keys
276277
token_tx.sign(private_key) # Necessary
@@ -291,10 +292,10 @@ def test_sign_transaction(mock_account_ids):
291292
for sig_pair in token_tx.signature_map.sigPair:
292293
assert sig_pair.pubKeyPrefix not in (b"supply_public_key", b"freeze_public_key")
293294

294-
# This test uses fixture mock_account_ids as parameter
295-
def test_to_proto_without_keys(mock_account_ids):
295+
# This test uses fixture (mock_account_ids, mock_client) as parameter
296+
def test_to_proto_without_keys(mock_account_ids, mock_client):
296297
"""Test protobuf conversion when keys are not set."""
297-
treasury_account, _, node_account_id, _, _ = mock_account_ids
298+
treasury_account, _, _, _, _ = mock_account_ids
298299

299300
token_tx = TokenCreateTransaction()
300301
token_tx.set_token_name("MyToken")
@@ -303,13 +304,14 @@ def test_to_proto_without_keys(mock_account_ids):
303304
token_tx.set_initial_supply(1000)
304305
token_tx.set_treasury_account_id(treasury_account)
305306
token_tx.transaction_id = generate_transaction_id(treasury_account)
306-
token_tx.node_account_id = node_account_id
307307

308308
# Mock treasury/operator key
309309
private_key = MagicMock()
310310
private_key.sign.return_value = b"signature"
311311
private_key.public_key().to_bytes_raw.return_value = b"public_key"
312312

313+
token_tx.freeze_with(mock_client)
314+
313315
# Sign with treasury key
314316
token_tx.sign(private_key)
315317

@@ -335,10 +337,10 @@ def test_to_proto_without_keys(mock_account_ids):
335337

336338
assert not transaction_body.tokenCreation.HasField("adminKey")
337339

338-
# This test uses fixture mock_account_ids as parameter
339-
def test_to_proto_with_keys(mock_account_ids):
340+
# This test uses fixture (mock_account_ids, mock_client) as parameter
341+
def test_to_proto_with_keys(mock_account_ids, mock_client):
340342
"""Test converting the token creation transaction to protobuf format after signing."""
341-
treasury_account, _, node_account_id, _, _ = mock_account_ids
343+
treasury_account, _, _, _, _ = mock_account_ids
342344

343345
# Mock keys
344346
private_key = MagicMock()
@@ -369,10 +371,11 @@ def test_to_proto_with_keys(mock_account_ids):
369371
token_tx.set_freeze_key(private_key_freeze)
370372

371373
token_tx.transaction_id = generate_transaction_id(treasury_account)
372-
token_tx.node_account_id = node_account_id
374+
375+
token_tx.freeze_with(mock_client)
373376

374377
# Sign with required sign keys
375-
token_tx.sign(private_key)
378+
token_tx.sign(private_key)
376379
token_tx.sign(private_key_admin)
377380

378381
# Convert to protobuf
@@ -472,13 +475,13 @@ def test_transaction_execution_failure(mock_account_ids):
472475
# Verify _execute was called with client
473476
mock_execute.assert_called_once_with(token_tx.client)
474477

475-
# This test uses fixture mock_account_ids as parameter
476-
def test_overwrite_defaults(mock_account_ids):
478+
# This test uses fixture (mock_account_ids, mock_client) as parameter
479+
def test_overwrite_defaults(mock_account_ids, mock_client):
477480
"""
478481
Demonstrates that defaults in TokenCreateTransaction can be overwritten
479482
by calling set_* methods, and the final protobuf reflects the updated values.
480483
"""
481-
treasury_account, _, node_account_id, _, _ = mock_account_ids
484+
treasury_account, _, _, _, _ = mock_account_ids
482485

483486
# Create a new TokenCreateTransaction with all default params
484487
token_tx = TokenCreateTransaction()
@@ -500,7 +503,8 @@ def test_overwrite_defaults(mock_account_ids):
500503

501504
# Set transaction/node IDs so can sign
502505
token_tx.transaction_id = generate_transaction_id(treasury_account)
503-
token_tx.node_account_id = node_account_id
506+
507+
token_tx.freeze_with(mock_client)
504508

505509
# Mock a private key and sign the transaction
506510
private_key = MagicMock()
@@ -621,13 +625,13 @@ def test_build_transaction_body_non_fungible(mock_account_ids):
621625
assert not transaction_body.tokenCreation.HasField("supplyKey")
622626
assert not transaction_body.tokenCreation.HasField("freezeKey")
623627

624-
# This test uses fixture mock_account_ids as parameter
625-
def test_build_and_sign_nft_transaction_to_proto(mock_account_ids):
628+
# This test uses fixture (mock_account_ids, mock_client) as parameter
629+
def test_build_and_sign_nft_transaction_to_proto(mock_account_ids, mock_client):
626630
"""
627631
Test building, signing, and protobuf serialization of
628632
a valid Non-Fungible Unique token creation transaction.
629633
"""
630-
treasury_account, _, node_account_id, _, _ = mock_account_ids
634+
treasury_account, _, _, _, _ = mock_account_ids
631635

632636
# Mock keys
633637
private_key_private = MagicMock()
@@ -659,7 +663,8 @@ def test_build_and_sign_nft_transaction_to_proto(mock_account_ids):
659663
token_tx.set_freeze_key(private_key_freeze)
660664

661665
token_tx.transaction_id = generate_transaction_id(treasury_account)
662-
token_tx.node_account_id = node_account_id
666+
667+
token_tx.freeze_with(mock_client)
663668

664669
# Sign the transaction
665670
token_tx.sign(private_key_private)

tests/unit/test_token_delete_transaction.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,20 @@ def test_missing_token_id():
4343
with pytest.raises(ValueError, match="Missing required TokenID."):
4444
delete_tx.build_transaction_body()
4545

46-
# This test uses fixture mock_account_ids as parameter
47-
def test_sign_transaction(mock_account_ids):
46+
# This test uses fixtures (mock_account_ids, mock_client) as parameters
47+
def test_sign_transaction(mock_account_ids, mock_client):
4848
"""Test signing the token delete transaction with a private key."""
49-
operator_id, _, node_account_id, token_id, _= mock_account_ids
49+
operator_id, _, _, token_id, _= mock_account_ids
50+
5051
delete_tx = TokenDeleteTransaction()
5152
delete_tx.set_token_id(token_id)
5253
delete_tx.transaction_id = generate_transaction_id(operator_id)
53-
delete_tx.node_account_id = node_account_id
5454

5555
private_key = MagicMock()
5656
private_key.sign.return_value = b'signature'
5757
private_key.public_key().to_bytes_raw.return_value = b'public_key'
58+
59+
delete_tx.freeze_with(mock_client)
5860

5961
delete_tx.sign(private_key)
6062

@@ -63,18 +65,20 @@ def test_sign_transaction(mock_account_ids):
6365
assert sig_pair.pubKeyPrefix == b'public_key'
6466
assert sig_pair.ed25519 == b'signature'
6567

66-
# This test uses fixture mock_account_ids as parameter
67-
def test_to_proto(mock_account_ids):
68+
# This test uses fixtures (mock_account_ids, mock_client) as parameters
69+
def test_to_proto(mock_account_ids, mock_client):
6870
"""Test converting the token delete transaction to protobuf format after signing."""
69-
operator_id, _, node_account_id, token_id, _= mock_account_ids
71+
operator_id, _, _, token_id, _= mock_account_ids
72+
7073
delete_tx = TokenDeleteTransaction()
7174
delete_tx.set_token_id(token_id)
7275
delete_tx.transaction_id = generate_transaction_id(operator_id)
73-
delete_tx.node_account_id = node_account_id
7476

7577
private_key = MagicMock()
7678
private_key.sign.return_value = b'signature'
7779
private_key.public_key().to_bytes_raw.return_value = b'public_key'
80+
81+
delete_tx.freeze_with(mock_client)
7882

7983
delete_tx.sign(private_key)
8084
proto = delete_tx.to_proto()

tests/unit/test_token_dissociate_transaction.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,20 @@ def test_missing_fields():
7272
with pytest.raises(ValueError, match="Account ID and token IDs must be set."):
7373
dissociate_tx.build_transaction_body()
7474

75-
def test_sign_transaction(mock_account_ids):
75+
def test_sign_transaction(mock_account_ids, mock_client):
7676
"""Test signing the token dissociate transaction with a private key."""
77-
account_id, _, node_account_id, token_id_1, _ = mock_account_ids
77+
account_id, _, _, token_id_1, _ = mock_account_ids
78+
7879
dissociate_tx = TokenDissociateTransaction()
7980
dissociate_tx.set_account_id(account_id)
8081
dissociate_tx.add_token_id(token_id_1)
8182
dissociate_tx.transaction_id = generate_transaction_id(account_id)
82-
dissociate_tx.node_account_id = node_account_id
8383

8484
private_key = MagicMock()
8585
private_key.sign.return_value = b'signature'
8686
private_key.public_key().to_bytes_raw.return_value = b'public_key'
87+
88+
dissociate_tx.freeze_with(mock_client)
8789

8890
dissociate_tx.sign(private_key)
8991

@@ -93,18 +95,20 @@ def test_sign_transaction(mock_account_ids):
9395
assert sig_pair.pubKeyPrefix == b'public_key'
9496
assert sig_pair.ed25519 == b'signature'
9597

96-
def test_to_proto(mock_account_ids):
98+
def test_to_proto(mock_account_ids, mock_client):
9799
"""Test converting the token dissociate transaction to protobuf format after signing."""
98-
account_id, _, node_account_id, token_id_1, _ = mock_account_ids
100+
account_id, _, _, token_id_1, _ = mock_account_ids
101+
99102
dissociate_tx = TokenDissociateTransaction()
100103
dissociate_tx.set_account_id(account_id)
101104
dissociate_tx.add_token_id(token_id_1)
102-
dissociate_tx.transaction_id = generate_transaction_id(account_id)
103-
dissociate_tx.node_account_id = node_account_id
104-
105+
dissociate_tx.transaction_id = generate_transaction_id(account_id)
106+
105107
private_key = MagicMock()
106108
private_key.sign.return_value = b'signature'
107109
private_key.public_key().to_bytes_raw.return_value = b'public_key'
110+
111+
dissociate_tx.freeze_with(mock_client)
108112

109113
dissociate_tx.sign(private_key)
110114
proto = dissociate_tx.to_proto()

0 commit comments

Comments
 (0)