Skip to content

Commit cd8a8de

Browse files
authored
feat: implement TokenWipeTransaction (#90)
* feat: add wipe_key attribute to TokenCreateTransaction Signed-off-by: dosi <[email protected]> * feat: implement class TokenWipeTransaction Signed-off-by: dosi <[email protected]> * test: add unit and integration tests Signed-off-by: dosi <[email protected]> * docs: add comprehensive token wipe transaction example Signed-off-by: dosi <[email protected]> * docs: add token wipe transaction documentation to README Signed-off-by: dosi <[email protected]> * docs: split token_wipe_transaction example into smaller functions Signed-off-by: dosi <[email protected]> * chore: address PR review comments for files in examples folder Signed-off-by: dosi <[email protected]> * chore: rename token_wipe_transaction to token_wipe Signed-off-by: dosi <[email protected]> * style: remove spaces around equals in function parameters Signed-off-by: dosi <[email protected]> --------- Signed-off-by: dosi <[email protected]>
1 parent 944dc9f commit cd8a8de

File tree

7 files changed

+881
-2
lines changed

7 files changed

+881
-2
lines changed

examples/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ You can choose either syntax or even mix both styles in your projects.
2020
- [Associating a Token](#associating-a-token)
2121
- [Dissociating a Token](#dissociating-a-token)
2222
- [Transferring Tokens](#transferring-tokens)
23+
- [Wiping Tokens](#wiping-tokens)
2324
- [Deleting a Token](#deleting-a-token)
2425
- [Freezing a Token](#freezing-a-token)
2526
- [Unfreezing a Token](#unfreezing-a-token)
@@ -261,6 +262,32 @@ transaction.execute(client)
261262
transaction.execute(client)
262263
```
263264

265+
### Wiping tokens
266+
267+
#### Pythonic Syntax:
268+
```
269+
transaction = TokenWipeTransaction(
270+
token_id=token_id,
271+
account_id=account_id,
272+
amount=amount
273+
).freeze_with(client)
274+
275+
transaction.execute(client)
276+
277+
```
278+
#### Method Chaining:
279+
```
280+
transaction = (
281+
TokenWipeTransaction()
282+
.set_token_id(token_id)
283+
.set_account_id(account_id)
284+
.set_amount(amount)
285+
.freeze_with(client)
286+
)
287+
288+
transaction.execute(client)
289+
```
290+
264291
### Deleting a Token
265292

266293
#### Pythonic Syntax:

examples/token_wipe.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import os
2+
import sys
3+
from dotenv import load_dotenv
4+
5+
from hiero_sdk_python import (
6+
Client,
7+
AccountId,
8+
PrivateKey,
9+
Network,
10+
TransferTransaction,
11+
TokenAssociateTransaction,
12+
)
13+
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
14+
from hiero_sdk_python.hbar import Hbar
15+
from hiero_sdk_python.response_code import ResponseCode
16+
from hiero_sdk_python.tokens.supply_type import SupplyType
17+
from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction
18+
from hiero_sdk_python.tokens.token_type import TokenType
19+
from hiero_sdk_python.tokens.token_wipe_transaction import TokenWipeTransaction
20+
21+
load_dotenv()
22+
23+
def setup_client():
24+
"""Initialize and set up the client with operator account"""
25+
# Initialize network and client
26+
network = Network(network='testnet')
27+
client = Client(network)
28+
29+
# Set up operator account
30+
operator_id = AccountId.from_string(os.getenv('OPERATOR_ID'))
31+
operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY'))
32+
client.set_operator(operator_id, operator_key)
33+
34+
return client, operator_id, operator_key
35+
36+
def create_test_account(client):
37+
"""Create a new account for testing"""
38+
# Generate private key for new account
39+
new_account_private_key = PrivateKey.generate()
40+
new_account_public_key = new_account_private_key.public_key()
41+
42+
# Create new account with initial balance of 1 HBAR
43+
transaction = (
44+
AccountCreateTransaction()
45+
.set_key(new_account_public_key)
46+
.set_initial_balance(Hbar(1))
47+
.freeze_with(client)
48+
)
49+
50+
receipt = transaction.execute(client)
51+
52+
# Check if account creation was successful
53+
if receipt.status != ResponseCode.SUCCESS:
54+
print(f"Account creation failed with status: {ResponseCode.get_name(receipt.status)}")
55+
sys.exit(1)
56+
57+
# Get account ID from receipt
58+
account_id = receipt.accountId
59+
print(f"New account created with ID: {account_id}")
60+
61+
return account_id, new_account_private_key
62+
63+
def create_token(client, operator_id, operator_key):
64+
"""Create a fungible token"""
65+
# Create fungible token
66+
# Note: The wipe key is required to perform token wipe operations
67+
transaction = (
68+
TokenCreateTransaction()
69+
.set_token_name("Token123")
70+
.set_token_symbol("T123")
71+
.set_decimals(2)
72+
.set_initial_supply(10)
73+
.set_treasury_account_id(operator_id)
74+
.set_token_type(TokenType.FUNGIBLE_COMMON)
75+
.set_supply_type(SupplyType.FINITE)
76+
.set_max_supply(100)
77+
.set_admin_key(operator_key) # For token management
78+
.set_supply_key(operator_key) # For minting/burning
79+
.set_freeze_key(operator_key) # For freezing accounts
80+
.set_wipe_key(operator_key) # Required for wiping tokens
81+
.freeze_with(client)
82+
)
83+
84+
receipt = transaction.execute(client)
85+
86+
# Check if token creation was successful
87+
if receipt.status != ResponseCode.SUCCESS:
88+
print(f"Token creation failed with status: {ResponseCode.get_name(receipt.status)}")
89+
sys.exit(1)
90+
91+
# Get token ID from receipt
92+
token_id = receipt.tokenId
93+
print(f"Token created with ID: {token_id}")
94+
95+
return token_id
96+
97+
def associate_token(client, account_id, token_id, account_private_key):
98+
"""Associate a token with an account"""
99+
# Associate the token with the new account
100+
# Note: Accounts must be associated with tokens before they can receive them
101+
associate_transaction = (
102+
TokenAssociateTransaction()
103+
.set_account_id(account_id)
104+
.add_token_id(token_id)
105+
.freeze_with(client)
106+
.sign(account_private_key) # Has to be signed by new account's key
107+
)
108+
109+
receipt = associate_transaction.execute(client)
110+
111+
if receipt.status != ResponseCode.SUCCESS:
112+
print(f"Token association failed with status: {ResponseCode.get_name(receipt.status)}")
113+
sys.exit(1)
114+
115+
print("Token successfully associated with account")
116+
117+
def transfer_tokens(client, token_id, operator_id, account_id, amount):
118+
"""Transfer tokens from operator to the specified account"""
119+
# Transfer tokens to the new account
120+
# Note: Negative amount for sender, positive for receiver
121+
transfer_transaction = (
122+
TransferTransaction()
123+
.add_token_transfer(token_id, operator_id, -amount) # From operator
124+
.add_token_transfer(token_id, account_id, amount) # To new account
125+
.freeze_with(client)
126+
)
127+
128+
receipt = transfer_transaction.execute(client)
129+
130+
# Check if token transfer was successful
131+
if receipt.status != ResponseCode.SUCCESS:
132+
print(f"Token transfer failed with status: {ResponseCode.get_name(receipt.status)}")
133+
sys.exit(1)
134+
135+
print(f"Successfully transferred {amount} tokens to account {account_id}")
136+
137+
def wipe_tokens(client, token_id, account_id, amount):
138+
"""Wipe tokens from the specified account"""
139+
# Wipe the tokens from the account
140+
# Note: This requires the wipe key that was specified during token creation
141+
transaction = (
142+
TokenWipeTransaction()
143+
.set_token_id(token_id)
144+
.set_account_id(account_id)
145+
.set_amount(amount)
146+
.freeze_with(client)
147+
)
148+
149+
receipt = transaction.execute(client)
150+
151+
if receipt.status != ResponseCode.SUCCESS:
152+
print(f"Token wipe failed with status: {ResponseCode.get_name(receipt.status)}")
153+
sys.exit(1)
154+
155+
print(f"Successfully wiped {amount} tokens from account {account_id}")
156+
157+
def token_wipe():
158+
"""
159+
Demonstrates the token wipe functionality by:
160+
1. Creating a new account
161+
2. Creating a fungible token with wipe capability
162+
3. Associating the token with the new account
163+
4. Transferring tokens to the new account
164+
5. Wiping the tokens from the account
165+
"""
166+
client, operator_id, operator_key = setup_client()
167+
account_id, new_account_private_key = create_test_account(client)
168+
token_id = create_token(client, operator_id, operator_key)
169+
associate_token(client, account_id, token_id, new_account_private_key)
170+
171+
amount = 10
172+
transfer_tokens(client, token_id, operator_id, account_id, amount)
173+
wipe_tokens(client, token_id, account_id, amount)
174+
175+
if __name__ == "__main__":
176+
token_wipe()

src/hiero_sdk_python/tokens/token_create_transaction.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,13 @@ class TokenKeys:
177177
admin_key: The admin key for the token to update and delete.
178178
supply_key: The supply key for the token to mint and burn.
179179
freeze_key: The freeze key for the token to freeze and unfreeze.
180+
wipe_key: The wipe key for the token to wipe tokens from an account.
180181
"""
181182

182183
admin_key: Optional[PrivateKey] = None
183184
supply_key: Optional[PrivateKey] = None
184185
freeze_key: Optional[PrivateKey] = None
186+
wipe_key: Optional[PrivateKey] = None
185187

186188
class TokenCreateTransaction(Transaction):
187189
"""
@@ -312,6 +314,11 @@ def set_freeze_key(self, key):
312314
self._require_not_frozen()
313315
self._keys.freeze_key = key
314316
return self
317+
318+
def set_wipe_key(self, key):
319+
self._require_not_frozen()
320+
self._keys.wipe_key = key
321+
return self
315322

316323
def build_transaction_body(self):
317324
"""
@@ -344,7 +351,11 @@ def build_transaction_body(self):
344351
if self._keys.freeze_key:
345352
freeze_public_key_bytes = self._keys.freeze_key.public_key().to_bytes_raw()
346353
freeze_key_proto = basic_types_pb2.Key(ed25519=freeze_public_key_bytes)
347-
354+
355+
wipe_key_proto = None
356+
if self._keys.wipe_key:
357+
wipe_public_key_bytes = self._keys.wipe_key.public_key().to_bytes_raw()
358+
wipe_key_proto = basic_types_pb2.Key(ed25519=wipe_public_key_bytes)
348359

349360
# Ensure token type is correctly set with default to fungible
350361
if self._token_params.token_type is None:
@@ -376,6 +387,7 @@ def build_transaction_body(self):
376387
adminKey=admin_key_proto,
377388
supplyKey=supply_key_proto,
378389
freezeKey=freeze_key_proto,
390+
wipeKey=wipe_key_proto
379391
)
380392
# Build the base transaction body and attach the token creation details
381393
transaction_body = self.build_base_transaction_body()
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from hiero_sdk_python.tokens.token_id import TokenId
2+
from hiero_sdk_python.account.account_id import AccountId
3+
from hiero_sdk_python.transaction.transaction import Transaction
4+
from hiero_sdk_python.hapi.services.token_wipe_account_pb2 import TokenWipeAccountTransactionBody
5+
from hiero_sdk_python.channels import _Channel
6+
from hiero_sdk_python.executable import _Method
7+
8+
class TokenWipeTransaction(Transaction):
9+
"""
10+
Represents a token wipe transaction on the Hedera network.
11+
12+
This transaction wipes (removes) tokens from an account.
13+
14+
Inherits from the base Transaction class and implements the required methods
15+
to build and execute a token wipe transaction.
16+
"""
17+
def __init__(self, token_id=None, account_id=None, amount=None, serial=[]):
18+
"""
19+
Initializes a new TokenWipeTransaction instance with optional token_id and account_id.
20+
21+
Args:
22+
token_id (TokenId, optional): The ID of the token to be wiped.
23+
account_id (AccountId, optional): The ID of the account to have their tokens wiped.
24+
amount (int, optional): The amount of tokens to wipe.
25+
serial (list[int], optional): The serial numbers of NFTs to wipe.
26+
"""
27+
super().__init__()
28+
self.token_id : TokenId = token_id
29+
self.account_id : AccountId = account_id
30+
self.amount : int = amount
31+
self.serial : list[int] = serial
32+
33+
def set_token_id(self, token_id):
34+
"""
35+
Sets the ID of the token to be wiped.
36+
37+
Args:
38+
token_id (TokenId): The ID of the token to be wiped.
39+
40+
Returns:
41+
TokenWipeTransaction: Returns self for method chaining.
42+
"""
43+
self._require_not_frozen()
44+
self.token_id = token_id
45+
return self
46+
47+
def set_account_id(self, account_id):
48+
self._require_not_frozen()
49+
self.account_id = account_id
50+
return self
51+
52+
def set_amount(self, amount):
53+
self._require_not_frozen()
54+
self.amount = amount
55+
return self
56+
57+
def set_serial(self, serial):
58+
self._require_not_frozen()
59+
self.serial = serial
60+
return self
61+
62+
def build_transaction_body(self):
63+
"""
64+
Builds and returns the protobuf transaction body for token wipe.
65+
66+
Returns:
67+
TransactionBody: The protobuf transaction body containing the token wipe details.
68+
"""
69+
token_wipe_body = TokenWipeAccountTransactionBody(
70+
token=self.token_id and self.token_id.to_proto(),
71+
account=self.account_id and self.account_id.to_proto(),
72+
amount=self.amount,
73+
serialNumbers=self.serial
74+
)
75+
transaction_body = self.build_base_transaction_body()
76+
transaction_body.tokenWipe.CopyFrom(token_wipe_body)
77+
return transaction_body
78+
79+
def _get_method(self, channel: _Channel) -> _Method:
80+
return _Method(
81+
transaction_func=channel.token.wipeTokenAccount,
82+
query_func=None
83+
)
84+
85+
def _from_proto(self, proto: TokenWipeAccountTransactionBody):
86+
"""
87+
Deserializes a TokenWipeAccountTransactionBody from a protobuf object.
88+
89+
Args:
90+
proto (TokenWipeAccountTransactionBody): The protobuf object to deserialize.
91+
92+
Returns:
93+
TokenWipeTransaction: Returns self for method chaining.
94+
"""
95+
self.token_id = TokenId.from_proto(proto.token)
96+
self.account_id = AccountId.from_proto(proto.account)
97+
self.amount = proto.amount
98+
self.serial = proto.serialNumbers
99+
return self

0 commit comments

Comments
 (0)