Skip to content

Commit ea885d3

Browse files
committed
TokenDissociateTransaction
1 parent bdaffef commit ea885d3

File tree

5 files changed

+306
-1
lines changed

5 files changed

+306
-1
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ submitting messages.
1313
- [Creating an Account](#creating-an-account)
1414
- [Creating a Token](#creating-a-token)
1515
- [Associating a Token](#associating-a-token)
16+
- [Dissociating a Token](#dissociating-a-token)
1617
- [Transferring Tokens](#transferring-tokens)
1718
- [Deleting a Token](#deleting-a-token)
1819
- [Transferring HBAR](#transferring-hbar)
@@ -97,13 +98,14 @@ New Account Private Key: 228a06c363b0eb328434d51xxx...
9798
New Account Public Key: 8f444e36e8926def492adxxx...
9899
Token creation successful. Token ID: 0.0.5025xxx
99100
Token association successful.
101+
Token dissociation successful.
100102
Token transfer successful.
101103
Token deletion successful.
102104
```
103105

104106
## Usage
105107

106-
Below are examples of how to use the SDK for creating tokens, associating them with accounts, and transferring or deleting tokens (also see 'examples' directiory)
108+
Below are examples of how to use the SDK for creating tokens, associating them with accounts, dissociating them from accounts, and transferring or deleting tokens (also see 'examples' directiory)
107109

108110
### Creating an Account
109111

@@ -153,6 +155,20 @@ transaction = (
153155
transaction.execute(client)
154156
```
155157

158+
### Dissociating a Token
159+
160+
```
161+
transaction = (
162+
TokenDissociateTransaction()
163+
.set_account_id(recipient_id)
164+
.add_token_id(token_id)
165+
.freeze_with(client)
166+
.sign(recipient_key)
167+
)
168+
169+
transaction.execute(client)
170+
```
171+
156172
### Transfering a Token
157173

158174
```

examples/token_dissociate.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import os
2+
import sys
3+
from dotenv import load_dotenv
4+
5+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
6+
sys.path.insert(0, project_root)
7+
8+
from hedera_sdk_python.client.client import Client
9+
from hedera_sdk_python.account.account_id import AccountId
10+
from hedera_sdk_python.crypto.private_key import PrivateKey
11+
from hedera_sdk_python.client.network import Network
12+
from hedera_sdk_python.tokens.token_id import TokenId
13+
from hedera_sdk_python.tokens.token_dissociate_transaction import TokenDissociateTransaction
14+
15+
load_dotenv()
16+
17+
def dissociate_token(): #Single token
18+
network = Network(network='testnet')
19+
client = Client(network)
20+
21+
recipient_id = AccountId.from_string(os.getenv('OPERATOR_ID'))
22+
recipient_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY'))
23+
token_id = TokenId.from_string('TOKEN_ID')
24+
25+
client.set_operator(recipient_id, recipient_key)
26+
27+
transaction = (
28+
TokenDissociateTransaction()
29+
.set_account_id(recipient_id)
30+
.add_token_id(token_id)
31+
.freeze_with(client)
32+
.sign(recipient_key)
33+
)
34+
35+
try:
36+
receipt = transaction.execute(client)
37+
print("Token dissociation successful.")
38+
except Exception as e:
39+
print(f"Token dissociation failed: {str(e)}")
40+
sys.exit(1)
41+
42+
def dissociate_tokens(): # Multiple tokens
43+
network = Network(network='testnet')
44+
client = Client(network)
45+
46+
recipient_id = AccountId.from_string(os.getenv('OPERATOR_ID'))
47+
recipient_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY'))
48+
token_ids = [TokenId.from_string('TOKEN_ID_1'), TokenId.from_string('TOKEN_ID_2')]
49+
50+
client.set_operator(recipient_id, recipient_key)
51+
52+
transaction = (
53+
TokenDissociateTransaction()
54+
.set_account_id(recipient_id)
55+
)
56+
57+
for token_id in token_ids:
58+
transaction.add_token_id(token_id)
59+
60+
transaction = (
61+
transaction
62+
.freeze_with(client)
63+
.sign(recipient_key)
64+
)
65+
66+
try:
67+
receipt = transaction.execute(client)
68+
print("Token dissociations successful.")
69+
except Exception as e:
70+
print(f"Token dissociations failed: {str(e)}")
71+
sys.exit(1)
72+
73+
if __name__ == "__main__":
74+
dissociate_token() # For single token dissociation
75+
# dissociate_tokens() # For multiple token dissociation
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from hedera_sdk_python.transaction.transaction import Transaction
2+
from hedera_sdk_python.hapi import token_dissociate_pb2
3+
from hedera_sdk_python.response_code import ResponseCode
4+
5+
class TokenDissociateTransaction(Transaction):
6+
"""
7+
Represents a token dissociate transaction on the Hedera network.
8+
9+
This transaction dissociates the specified tokens with an account,
10+
meaning the account can no longer hold or transact with those tokens.
11+
12+
Inherits from the base Transaction class and implements the required methods
13+
to build and execute a token dissociate transaction.
14+
"""
15+
16+
def __init__(self):
17+
"""
18+
Initializes a new TokenDissociateTransaction instance with default values.
19+
"""
20+
super().__init__()
21+
self.account_id = None
22+
self.token_ids = []
23+
24+
self._default_transaction_fee = 500_000_000
25+
26+
def set_account_id(self, account_id):
27+
self._require_not_frozen()
28+
self.account_id = account_id
29+
return self
30+
31+
def add_token_id(self, token_id):
32+
self._require_not_frozen()
33+
self.token_ids.append(token_id)
34+
return self
35+
36+
def build_transaction_body(self):
37+
"""
38+
Builds and returns the protobuf transaction body for token dissociation.
39+
40+
Returns:
41+
TransactionBody: The protobuf transaction body containing the token dissociation details.
42+
43+
Raises:
44+
ValueError: If account ID or token IDs are not set.
45+
"""
46+
if not self.account_id or not self.token_ids:
47+
raise ValueError("Account ID and token IDs must be set.")
48+
49+
token_dissociate_body = token_dissociate_pb2.TokenDissociateTransactionBody(
50+
account=self.account_id.to_proto(),
51+
tokens=[token_id.to_proto() for token_id in self.token_ids]
52+
)
53+
54+
transaction_body = self.build_base_transaction_body()
55+
transaction_body.tokenDissociate.CopyFrom(token_dissociate_body)
56+
57+
return transaction_body
58+
59+
def _execute_transaction(self, client, transaction_proto):
60+
"""
61+
Executes the token dissociation transaction using the provided client.
62+
63+
Args:
64+
client (Client): The client instance to use for execution.
65+
transaction_proto (Transaction): The protobuf Transaction message.
66+
67+
Returns:
68+
TransactionReceipt: The receipt from the network after transaction execution.
69+
70+
Raises:
71+
Exception: If the transaction submission fails or receives an error response.
72+
"""
73+
response = client.token_stub.dissociateTokens(transaction_proto)
74+
75+
if response.nodeTransactionPrecheckCode != ResponseCode.OK:
76+
error_code = response.nodeTransactionPrecheckCode
77+
error_message = ResponseCode.get_name(error_code)
78+
raise Exception(f"Error during transaction submission: {error_code} ({error_message})")
79+
80+
receipt = self.get_receipt(client)
81+
return receipt
82+
83+
def get_receipt(self, client, timeout=60):
84+
"""
85+
Retrieves the receipt for the transaction.
86+
87+
Args:
88+
client (Client): The client instance.
89+
timeout (int): Maximum time in seconds to wait for the receipt.
90+
91+
Returns:
92+
TransactionReceipt: The transaction receipt from the network.
93+
94+
Raises:
95+
Exception: If the transaction ID is not set or if receipt retrieval fails.
96+
"""
97+
if self.transaction_id is None:
98+
raise Exception("Transaction ID is not set.")
99+
100+
receipt = client.get_transaction_receipt(self.transaction_id, timeout)
101+
return receipt

test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from hedera_sdk_python.crypto.public_key import PublicKey
1010
from hedera_sdk_python.tokens.token_create_transaction import TokenCreateTransaction
1111
from hedera_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction
12+
from hedera_sdk_python.tokens.token_dissociate_transaction import TokenDissociateTransaction
1213
from hedera_sdk_python.transaction.transfer_transaction import TransferTransaction
1314
from hedera_sdk_python.response_code import ResponseCode
1415
from hedera_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction
@@ -111,6 +112,28 @@ def associate_token(client, recipient_id, recipient_private_key, token_id):
111112
print(f"Token association failed: {str(e)}")
112113
sys.exit(1)
113114

115+
116+
def dissociate_token(client, recipient_id, recipient_private_key, token_id):
117+
"""Dissociate the specified token with the recipient account."""
118+
transaction = (
119+
TokenDissociateTransaction()
120+
.set_account_id(recipient_id)
121+
.add_token_id(token_id)
122+
.freeze_with(client)
123+
)
124+
transaction.sign(client.operator_private_key) # sign with operator's key (payer)
125+
transaction.sign(recipient_private_key) # sign with newly created account's key (recipient)
126+
127+
try:
128+
receipt = transaction.execute(client)
129+
if receipt.status != ResponseCode.SUCCESS:
130+
status_message = ResponseCode.get_name(receipt.status)
131+
raise Exception(f"Token dissociation failed with status: {status_message}")
132+
print("Token dissociation successful.")
133+
except Exception as e:
134+
print(f"Token dissociation failed: {str(e)}")
135+
sys.exit(1)
136+
114137
def transfer_token(client, recipient_id, token_id):
115138
"""Transfer the specified token to the recipient account."""
116139
transaction = (
@@ -195,6 +218,7 @@ def main():
195218
recipient_id, recipient_private_key = create_new_account(client)
196219
token_id = create_token(client, operator_id, admin_key)
197220
associate_token(client, recipient_id, recipient_private_key, token_id)
221+
dissociate_token(client, recipient_id, recipient_private_key, token_id)
198222
transfer_token(client, recipient_id, token_id)
199223
delete_token(client, token_id, admin_key)
200224

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import pytest
2+
from unittest.mock import MagicMock
3+
from hedera_sdk_python.tokens.token_dissociate_transaction import TokenDissociateTransaction
4+
from hedera_sdk_python.hapi import timestamp_pb2
5+
from hedera_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+
import time
10+
current_time = time.time()
11+
timestamp_seconds = int(current_time)
12+
timestamp_nanos = int((current_time - timestamp_seconds) * 1e9)
13+
14+
tx_timestamp = timestamp_pb2.Timestamp(seconds=timestamp_seconds, nanos=timestamp_nanos)
15+
16+
tx_id = TransactionId(
17+
valid_start=tx_timestamp,
18+
account_id=account_id_proto
19+
)
20+
return tx_id
21+
22+
def test_build_transaction_body(mock_account_ids):
23+
"""Test building the token dissociate transaction body with valid account ID and token IDs."""
24+
account_id, _, node_account_id, token_id_1, token_id_2 = mock_account_ids
25+
dissociate_tx = TokenDissociateTransaction()
26+
27+
dissociate_tx.set_account_id(account_id)
28+
dissociate_tx.add_token_id(token_id_1)
29+
dissociate_tx.add_token_id(token_id_2)
30+
dissociate_tx.transaction_id = generate_transaction_id(account_id)
31+
dissociate_tx.node_account_id = node_account_id
32+
33+
transaction_body = dissociate_tx.build_transaction_body()
34+
35+
assert transaction_body.tokenDissociate.account.shardNum == account_id.shard
36+
assert transaction_body.tokenDissociate.account.realmNum == account_id.realm
37+
assert transaction_body.tokenDissociate.account.accountNum == account_id.num
38+
assert len(transaction_body.tokenDissociate.tokens) == 2
39+
assert transaction_body.tokenDissociate.tokens[0].tokenNum == token_id_1.num
40+
assert transaction_body.tokenDissociate.tokens[1].tokenNum == token_id_2.num
41+
42+
43+
def test_missing_fields():
44+
"""Test that building the transaction without account ID or token IDs raises a ValueError."""
45+
dissociate_tx = TokenDissociateTransaction()
46+
47+
with pytest.raises(ValueError, match="Account ID and token IDs must be set."):
48+
dissociate_tx.build_transaction_body()
49+
50+
def test_sign_transaction(mock_account_ids):
51+
"""Test signing the token dissociate transaction with a private key."""
52+
account_id, _, node_account_id, token_id_1, _ = mock_account_ids
53+
dissociate_tx = TokenDissociateTransaction()
54+
dissociate_tx.set_account_id(account_id)
55+
dissociate_tx.add_token_id(token_id_1)
56+
dissociate_tx.transaction_id = generate_transaction_id(account_id)
57+
dissociate_tx.node_account_id = node_account_id
58+
59+
private_key = MagicMock()
60+
private_key.sign.return_value = b'signature'
61+
private_key.public_key().public_bytes.return_value = b'public_key'
62+
63+
dissociate_tx.sign(private_key)
64+
65+
assert len(dissociate_tx.signature_map.sigPair) == 1
66+
sig_pair = dissociate_tx.signature_map.sigPair[0]
67+
68+
assert sig_pair.pubKeyPrefix == b'public_key'
69+
assert sig_pair.ed25519 == b'signature'
70+
71+
72+
def test_to_proto(mock_account_ids):
73+
"""Test converting the token dissociate transaction to protobuf format after signing."""
74+
account_id, _, node_account_id, token_id_1, _ = mock_account_ids
75+
dissociate_tx = TokenDissociateTransaction()
76+
dissociate_tx.set_account_id(account_id)
77+
dissociate_tx.add_token_id(token_id_1)
78+
dissociate_tx.transaction_id = generate_transaction_id(account_id)
79+
dissociate_tx.node_account_id = node_account_id
80+
81+
private_key = MagicMock()
82+
private_key.sign.return_value = b'signature'
83+
private_key.public_key().public_bytes.return_value = b'public_key'
84+
85+
dissociate_tx.sign(private_key)
86+
proto = dissociate_tx.to_proto()
87+
88+
assert proto.signedTransactionBytes
89+
assert len(proto.signedTransactionBytes) > 0

0 commit comments

Comments
 (0)