Skip to content

Commit 391a62f

Browse files
authored
feat: Adding in TokenUnfreezeTransaction (#54)
* Add tokenUnfreezeTransaction functionality Signed-off-by: Angelina <[email protected]> * Correct import name to hiero Signed-off-by: Angelina <[email protected]> * Corrected issues pertaining to testing Signed-off-by: Angelina <[email protected]> * Correcting typo Signed-off-by: Angelina <[email protected]> * correct token and account typo Signed-off-by: Angelina <[email protected]> * Correct typos for token ID Signed-off-by: Angelina <[email protected]> * fix: Token ID typos Signed-off-by: Angelina <[email protected]> * Typo and order corrections Signed-off-by: Angelina <[email protected]> * typo Signed-off-by: Angelina <[email protected]> * correct account_id Signed-off-by: Angelina <[email protected]> * fix: Correct Readmes and transaction Signed-off-by: Angelina <[email protected]> * Delete uv.lock File meant to be ignored Signed-off-by: aceppaluni <[email protected]> * fix: correct sync issues from merge Signed-off-by: Angelina <[email protected]> --------- Signed-off-by: Angelina <[email protected]> Signed-off-by: aceppaluni <[email protected]>
1 parent 7cc9ebc commit 391a62f

File tree

7 files changed

+289
-1
lines changed

7 files changed

+289
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This is a Python SDK for interacting with the Hedera Hashgraph platform. It allows developers to:
44

5-
- Manage Token Transactions like Create, Mint Fungible, Mint Non-Fungible, Associate, Dissociate, Transfer, Freeze & Delete
5+
- Manage Token Transactions like Create, Mint Fungible, Mint Non-Fungible, Associate, Dissociate, Transfer, Freeze, Unfreeze & Delete
66
- Manage Consensus Transactions like Topic Create, Update, Delete
77
- Submit Topic Messages
88
- Query Account Balance, Transaction Receipts, Topic Infos and Messages
@@ -134,6 +134,7 @@ Token dissociation successful.
134134
Token minting successful.
135135
Token transfer successful.
136136
Token freeze successful.
137+
Token Unfreeze successful.
137138
Token deletion successful.
138139
Topic creation successful.
139140
Topic Message submitted.

examples/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ You can choose either syntax or even mix both styles in your projects.
2222
- [Transferring Tokens](#transferring-tokens)
2323
- [Deleting a Token](#deleting-a-token)
2424
- [Freezing a Token](#freezing-a-token)
25+
- [Unfreezing a Token](#unfreezing-a-token)
2526
- [HBAR Transactions](#hbar-transactions)
2627
- [Transferring HBAR](#transferring-hbar)
2728
- [Topic Transactions](#topic-transactions)
@@ -309,6 +310,28 @@ transaction.execute(client)
309310
transaction.sign(freeze_key) # Freeze key must also have been set in Token Create
310311
transaction.execute(client)
311312
```
313+
### Unfreezing a Token
314+
315+
#### Pythonic Syntax:
316+
```
317+
transaction = TokenUnfreezeTransaction(
318+
token_id=token_id
319+
account_id=account_id
320+
).freeze_with(client)
321+
transaction.sign(freeze_key)
322+
transaction.execute(client)
323+
```
324+
#### Method Chaining:
325+
```
326+
transaction = (
327+
TokenUnfreezeTransaction()
328+
.set_token_id(token_id)
329+
.set_account_id(account_id)
330+
.freeze_with(client)
331+
)
332+
transaction.sign(freeze_key)
333+
transaction.execute(client)
334+
```
312335

313336
## HBAR Transactions
314337

examples/token_unfreeze.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import os
2+
import sys
3+
from dotenv import load_dotenv
4+
5+
from hiero_sdk_python.client.client import Client
6+
from hiero_sdk_python.account.account_id import AccountId
7+
from hiero_sdk_python.crypto.private_key import PrivateKey
8+
from hiero_sdk_python.client.network import Network
9+
from hiero_sdk_python.tokens.token_id import TokenId
10+
from hiero_sdk_python.tokens.token_unfreeze_transaction import TokenUnfreezeTransaction
11+
12+
load_dotenv()
13+
14+
def unfreeze_token(): # Single Token
15+
network = Network(network='testnet')
16+
client = Client(network)
17+
18+
operator_id = AccountId.from_string(os.getenv('OPERATOR_ID'))
19+
operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY'))
20+
freeze_key = PrivateKey.from_string(os.getenv('FREEZE_KEY'))
21+
token_id = TokenId.from_string(os.getenv('TOKEN_ID'))
22+
account_id = AccountId.from_string(os.getenv('FREEZE_ACCOUNT_ID'))
23+
24+
client.set_operator(operator_id, operator_key)
25+
26+
transaction = (
27+
TokenUnfreezeTransaction()
28+
.set_token_id(token_id)
29+
.set_account_id(account_id)
30+
.freeze_with(client)
31+
.sign(freeze_key)
32+
)
33+
34+
try:
35+
receipt = transaction.execute(client)
36+
if receipt is not None and receipt.status == "SUCCESS":
37+
print('Token unfreeze Successful')
38+
else:
39+
print("Token freeze failed.")
40+
sys.exit(1)
41+
except Exception as e:
42+
print(f"Token unfreeze failed: {str(e)}")
43+
sys.exit(1)
44+
45+
46+
if __name__ == "__main__":
47+
unfreeze_token() # For single token unfreeze

src/hiero_sdk_python/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .tokens.token_delete_transaction import TokenDeleteTransaction
1818
from .tokens.token_mint_transaction import TokenMintTransaction
1919
from .tokens.token_freeze_transaction import TokenFreezeTransaction
20+
from .tokens.token_unfreeze_transaction import TokenUnfreezeTransaction
2021
from .tokens.token_id import TokenId
2122
from .tokens.nft_id import NftId
2223

@@ -70,6 +71,7 @@
7071
"TokenDeleteTransaction",
7172
"TokenMintTransaction",
7273
"TokenFreezeTransaction",
74+
"TokenUnfreezeTransaction",
7375
"TokenId",
7476
"NftId",
7577

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from hiero_sdk_python.transaction.transaction import Transaction
2+
from hiero_sdk_python.hapi.services import token_unfreeze_account_pb2
3+
from hiero_sdk_python.response_code import ResponseCode
4+
5+
class TokenUnfreezeTransaction(Transaction):
6+
"""
7+
Represents a token unfreeze transaction on the Hedera network.
8+
9+
This transaction unfreezes specified tokens for a given account.
10+
11+
Inherits from the base Transaction class and implements the required methods
12+
to build and execute a token unfreeze transaction.
13+
"""
14+
15+
def __init__(self, account_id=None, token_id=None):
16+
"""
17+
Initializes a new TokenUnfreezeTransaction instance with default values.
18+
"""
19+
super().__init__()
20+
self.token_id = token_id
21+
self.account_id = account_id
22+
self._default_transaction_fee = 3_000_000_000
23+
self._is_frozen = False
24+
25+
def set_token_id(self, token_id):
26+
self.__require_not_frozen()
27+
self.token_id = token_id
28+
return self
29+
30+
def set_account_id(self, account_id):
31+
self.__require_not_frozen()
32+
self.account_id = account_id
33+
return self
34+
35+
def __require_not_frozen(self):
36+
if self._is_frozen:
37+
raise ValueError("Transaction is already frozen and cannot be modified.")
38+
39+
def build_transaction_body(self):
40+
"""
41+
Builds and returns the protobuf transaction body for token unfreeze.
42+
43+
Returns:
44+
TransactionBody: The protobuf transaction body containing the token unfreeze details.
45+
46+
Raises:
47+
ValueError: If account ID or token IDs are not set.
48+
49+
"""
50+
if not self.token_id:
51+
raise ValueError("Missing required TokenID.")
52+
53+
if not self.account_id:
54+
raise ValueError("Missing required AccountID.")
55+
56+
token_unfreeze_body = token_unfreeze_account_pb2.TokenUnfreezeAccountTransactionBody(
57+
account=self.account_id.to_proto(),
58+
token=self.token_id.to_proto()
59+
)
60+
61+
transaction_body = self.build_base_transaction_body()
62+
transaction_body.tokenUnfreeze.CopyFrom(token_unfreeze_body)
63+
64+
return transaction_body
65+
66+
def _execute_transaction(self, client, transaction_proto):
67+
"""
68+
Executes the token unfreeze transaction using the provided client.
69+
Args:
70+
client (Client): The client instance to use for execution.
71+
transaction_proto (Transaction): The protobuf Transaction message.
72+
Returns:
73+
TransactionReceipt: The receipt from the network after transaction execution.
74+
Raises:
75+
Exception: If the transaction submission fails or receives an error response.
76+
"""
77+
78+
response = client.token_stub.unfreezeTokenAccount(transaction_proto)
79+
80+
if response.nodeTransactionPrecheckCode != ResponseCode.OK:
81+
error_code = response.nodeTransactionPrecheckCode
82+
error_message = ResponseCode.get_name(error_code)
83+
raise Exception(f"Error during transaction submission: {error_code} ({error_message})")
84+
receipt = self.get_receipt(client)
85+
return receipt
86+

test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction
5353

5454
# Topic related imports
55+
from hiero_sdk_python.tokens.token_unfreeze_transaction import TokenUnfreezeTransaction
56+
from hiero_sdk_python.response_code import ResponseCode
5557
from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction
5658
from hiero_sdk_python.consensus.topic_message_submit_transaction import (
5759
TopicMessageSubmitTransaction
@@ -309,6 +311,24 @@ def freeze_token(client, token_id, account_id, freeze_key):
309311
print(traceback.format_exc())
310312
sys.exit(1)
311313

314+
def unfreeze_token(client, token_id_1, recipient_id, freeze_key):
315+
"""Unfreeze the specified token with the given account."""
316+
transaction = TokenUnfreezeTransaction(account_id=recipient_id, token_id=token_id_1)
317+
318+
transaction.freeze_with(client)
319+
transaction.sign(client.operator_private_key)
320+
transaction.sign(freeze_key)
321+
322+
try:
323+
receipt = transaction.execute(client)
324+
if receipt.status != ResponseCode.SUCCESS:
325+
status_message = ResponseCode.get_name(receipt.status)
326+
raise Exception(f"Token unfreeze failed with status: {status_message}")
327+
print("Token unfreeze successful.")
328+
except Exception as e:
329+
print(f"Token unfreeze failed: {str(e)}")
330+
sys.exit(1)
331+
312332
def mint_fungible_token(client, token_id, supply_key, amount=2000):
313333
"""Tests fungible token minting"""
314334
transaction = TokenMintTransaction(token_id=token_id, amount=amount)
@@ -496,6 +516,10 @@ def main():
496516
freeze_token(client, token_id_1, recipient_id, freeze_key)
497517
freeze_token(client, token_id_nft_1, recipient_id, freeze_key)
498518

519+
# Test unfreezing fungible and nft tokens. In this case from the recipient that just received token 1.
520+
unfreeze_token(client, token_id_1, recipient_id, freeze_key)
521+
unfreeze_token(client, token_id_nft_1, recipient_id, freeze_key)
522+
499523
# Test dissociating a fungible and nft token. In this case the tokens that were not transferred or frozen.
500524
dissociate_token(client, recipient_id, recipient_private_key, [token_id_2])
501525
dissociate_token(client, recipient_id, recipient_private_key, [token_id_nft_2])
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import pytest
2+
from unittest.mock import MagicMock
3+
from hiero_sdk_python.tokens.token_unfreeze_transaction import TokenUnfreezeTransaction
4+
from hiero_sdk_python.hapi.services import timestamp_pb2
5+
from hiero_sdk_python.transaction.transaction_id import TransactionId
6+
7+
def generate_transaction_id(account_id_proto):
8+
"""Generate a unique transaction ID based on the account ID and the current timestamp."""
9+
10+
import time
11+
current_time = time.time()
12+
timestamp_seconds = int(current_time)
13+
timestamp_nanos = int((current_time - timestamp_seconds) * 1e9)
14+
15+
tx_timestamp = timestamp_pb2.Timestamp(seconds=timestamp_seconds, nanos=timestamp_nanos)
16+
tx_id = TransactionId(
17+
valid_start=tx_timestamp,
18+
account_id=account_id_proto
19+
)
20+
21+
return tx_id
22+
23+
def test_build_transaction_body(mock_account_ids):
24+
"""Test building the token unfreeze transaction body with valid account ID and token ID."""
25+
26+
account_id, freeze_id, node_account_id, token_id, _= mock_account_ids
27+
28+
unfreeze_tx = TokenUnfreezeTransaction()
29+
unfreeze_tx.set_token_id(token_id)
30+
unfreeze_tx.set_account_id(freeze_id)
31+
unfreeze_tx.transaction_id = generate_transaction_id(account_id)
32+
unfreeze_tx.node_account_id = node_account_id
33+
transaction_body = unfreeze_tx.build_transaction_body()
34+
35+
assert transaction_body.tokenUnfreeze.token.shardNum == 1
36+
assert transaction_body.tokenUnfreeze.token.realmNum == 1
37+
assert transaction_body.tokenUnfreeze.token.tokenNum == 1
38+
39+
proto_account = freeze_id.to_proto()
40+
assert transaction_body.tokenUnfreeze.account == proto_account
41+
42+
def test_missing_token_id(mock_account_ids):
43+
"""Test that building a transaction without setting
44+
TokenID raises a ValueError."""
45+
46+
account_id, freeze_id, node_account_id, token_id, _= mock_account_ids
47+
48+
49+
unfreeze_tx = TokenUnfreezeTransaction()
50+
unfreeze_tx.set_account_id(freeze_id)
51+
with pytest.raises(ValueError,match = "Missing required TokenID."):
52+
unfreeze_tx.build_transaction_body()
53+
54+
def test_missing_account_id(mock_account_ids):
55+
"""Test that building a transaction without setting AccountID raises a ValueError."""
56+
account_id, freeze_id, node_account_id, token_id, _= mock_account_ids
57+
58+
unfreeze_tx = TokenUnfreezeTransaction()
59+
unfreeze_tx.set_token_id(token_id)
60+
with pytest.raises(ValueError, match="Missing required AccountID."):
61+
unfreeze_tx.build_transaction_body()
62+
63+
def test_sign_transaction(mock_account_ids):
64+
"""Test signing the token unfreeze transaction with a freeze key."""
65+
66+
account_id, freeze_id, node_account_id, token_id, _= mock_account_ids
67+
68+
unfreeze_tx = TokenUnfreezeTransaction()
69+
unfreeze_tx.set_token_id(token_id)
70+
unfreeze_tx.set_account_id(freeze_id)
71+
unfreeze_tx.transaction_id = generate_transaction_id(account_id)
72+
unfreeze_tx.node_account_id = node_account_id
73+
74+
freeze_key = MagicMock()
75+
freeze_key.sign.return_value = b'signature'
76+
freeze_key.public_key().to_bytes_raw.return_value = b'public_key'
77+
78+
unfreeze_tx.sign(freeze_key)
79+
80+
assert len(unfreeze_tx.signature_map.sigPair) == 1
81+
sig_pair = unfreeze_tx.signature_map.sigPair[0]
82+
83+
assert sig_pair.pubKeyPrefix == b'public_key'
84+
assert sig_pair.ed25519 == b'signature'
85+
86+
def test_to_proto(mock_account_ids):
87+
"""Test converting the token unfreeze transaction to protobuf format after signing."""
88+
89+
account_id, freeze_id, node_account_id, token_id, _= mock_account_ids
90+
91+
unfreeze_tx = TokenUnfreezeTransaction()
92+
unfreeze_tx.set_token_id(token_id)
93+
unfreeze_tx.set_account_id(freeze_id)
94+
unfreeze_tx.transaction_id = generate_transaction_id(account_id)
95+
unfreeze_tx.node_account_id = node_account_id
96+
97+
freeze_key = MagicMock()
98+
freeze_key.sign.return_value = b'signature'
99+
freeze_key.public_key().to_bytes_raw.return_value = b'mock_pubkey'
100+
101+
unfreeze_tx.sign(freeze_key)
102+
proto = unfreeze_tx.to_proto()
103+
104+
assert proto.signedTransactionBytes
105+
assert len(proto.signedTransactionBytes) > 0

0 commit comments

Comments
 (0)