Skip to content

Commit c3558cb

Browse files
authored
feat: Add FileDeleteTransaction (#182)
* feat: implement FileDeleteTransaction Signed-off-by: dosi <[email protected]> * test: add integration tests for FileDeleteTransaction: Signed-off-by: dosi <[email protected]> * test: add unit tests for FileDeleteTransaction Signed-off-by: dosi <[email protected]> * docs: add FileDeleteTransaction in examples README Signed-off-by: dosi <[email protected]> * docs: add file delete example Signed-off-by: dosi <[email protected]> * chore: add FileDeleteTransaction to __init__.py Signed-off-by: dosi <[email protected]> * chore: move default transaction fee to a constant and change it to 2 Hbars Signed-off-by: dosi <[email protected]> --------- Signed-off-by: dosi <[email protected]>
1 parent b84a43d commit c3558cb

File tree

7 files changed

+436
-3
lines changed

7 files changed

+436
-3
lines changed

examples/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ You can choose either syntax or even mix both styles in your projects.
4949
- [File Transactions](#file-transactions)
5050
- [Creating a File](#creating-a-file)
5151
- [Querying File Info](#querying-file-info)
52+
- [Deleting a File](#deleting-a-file)
5253
- [Miscellaneous Queries](#miscellaneous-queries)
5354
- [Querying Transaction Record](#querying-transaction-record)
5455

@@ -962,6 +963,31 @@ print(file_info)
962963
963964
```
964965

966+
### Deleting a File
967+
968+
#### Pythonic Syntax:
969+
```
970+
transaction = FileDeleteTransaction(
971+
file_id=file_id
972+
).freeze_with(client)
973+
974+
transaction.sign(operator_key)
975+
transaction.execute(client)
976+
```
977+
978+
#### Method Chaining:
979+
```
980+
transaction = (
981+
FileDeleteTransaction()
982+
.set_file_id(file_id)
983+
.freeze_with(client)
984+
)
985+
986+
transaction.sign(operator_key)
987+
transaction.execute(client)
988+
989+
```
990+
965991
## Miscellaneous Queries
966992

967993
### Querying Transaction Record

examples/file_delete.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
This example demonstrates how to delete a file using the Python SDK.
3+
"""
4+
5+
import os
6+
import sys
7+
8+
from dotenv import load_dotenv
9+
10+
from hiero_sdk_python import AccountId, Client, Network, PrivateKey
11+
from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction
12+
from hiero_sdk_python.file.file_delete_transaction import FileDeleteTransaction
13+
from hiero_sdk_python.file.file_id import FileId
14+
from hiero_sdk_python.file.file_info_query import FileInfoQuery
15+
from hiero_sdk_python.response_code import ResponseCode
16+
17+
load_dotenv()
18+
19+
20+
def setup_client():
21+
"""Initialize and set up the client with operator account"""
22+
network = Network(network="testnet")
23+
client = Client(network)
24+
25+
operator_id = AccountId.from_string(os.getenv("OPERATOR_ID"))
26+
operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY"))
27+
client.set_operator(operator_id, operator_key)
28+
29+
return client
30+
31+
32+
def create_file(client: Client):
33+
"""Create a test file and return its ID along with the private key"""
34+
35+
file_private_key = PrivateKey.generate_ed25519()
36+
37+
receipt = (
38+
FileCreateTransaction()
39+
.set_keys(file_private_key.public_key())
40+
.set_contents(b"Hello, this is a test file that will be deleted!")
41+
.set_file_memo("Test file for deletion example")
42+
.freeze_with(client)
43+
.sign(file_private_key)
44+
.execute(client)
45+
)
46+
47+
if receipt.status != ResponseCode.SUCCESS:
48+
print(f"File creation failed with status: {ResponseCode(receipt.status).name}")
49+
sys.exit(1)
50+
51+
file_id = receipt.file_id
52+
print(f"File created successfully with ID: {file_id}")
53+
54+
return file_id, file_private_key
55+
56+
57+
def query_file_info(client: Client, file_id: FileId):
58+
"""Query file info and display the results"""
59+
info = FileInfoQuery().set_file_id(file_id).execute(client)
60+
61+
print(info)
62+
63+
64+
def file_delete():
65+
"""
66+
Demonstrates the complete file lifecycle by:
67+
1. Creating a file
68+
2. Querying file info (before deletion)
69+
3. Deleting the file
70+
4. Querying file info (after deletion)
71+
"""
72+
# Setup client
73+
client = setup_client()
74+
75+
# Create file
76+
file_id, file_private_key = create_file(client)
77+
78+
# Query file info before deletion
79+
query_file_info(client, file_id)
80+
81+
# Delete the file
82+
receipt = (
83+
FileDeleteTransaction()
84+
.set_file_id(file_id)
85+
.freeze_with(client)
86+
.sign(file_private_key) # File key is required to delete
87+
.execute(client)
88+
)
89+
90+
if receipt.status != ResponseCode.SUCCESS:
91+
print(f"File deletion failed with status: {ResponseCode(receipt.status).name}")
92+
sys.exit(1)
93+
94+
print(f"\nFile deleted successfully with ID: {file_id}\n")
95+
print("Check if file is deleted by querying info:")
96+
97+
# Query file info after deletion
98+
query_file_info(client, file_id)
99+
100+
101+
if __name__ == "__main__":
102+
file_delete()

src/hiero_sdk_python/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
from .file.file_create_transaction import FileCreateTransaction
8484
from .file.file_info_query import FileInfoQuery
8585
from .file.file_info import FileInfo
86+
from .file.file_delete_transaction import FileDeleteTransaction
8687

8788
__all__ = [
8889
# Client
@@ -147,11 +148,11 @@
147148
"TokenNftInfoQuery",
148149
"TokenInfoQuery",
149150
"AccountInfoQuery",
150-
151+
151152
# Address book
152153
"Endpoint",
153154
"NodeAddress",
154-
155+
155156
# Logger
156157
"Logger",
157158
"LogLevel",
@@ -161,9 +162,10 @@
161162
"ResponseCode",
162163
"Timestamp",
163164
"Duration",
164-
165+
165166
# File
166167
"FileCreateTransaction",
167168
"FileInfoQuery",
168169
"FileInfo",
170+
"FileDeleteTransaction",
169171
]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
Transaction to delete a file on the network.
3+
"""
4+
5+
from typing import Optional
6+
7+
from hiero_sdk_python.channels import _Channel
8+
from hiero_sdk_python.executable import _Method
9+
from hiero_sdk_python.file.file_id import FileId
10+
from hiero_sdk_python.hapi.services.file_delete_pb2 import FileDeleteTransactionBody
11+
from hiero_sdk_python.hbar import Hbar
12+
from hiero_sdk_python.transaction.transaction import Transaction
13+
14+
DEFAULT_TRANSACTION_FEE = Hbar(2).to_tinybars()
15+
16+
17+
class FileDeleteTransaction(Transaction):
18+
"""
19+
Represents a file deletion transaction on the network.
20+
21+
This transaction deletes a specified file, rendering it inactive.
22+
23+
Inherits from the base Transaction class and implements the required methods
24+
to build and execute a file deletion transaction.
25+
"""
26+
27+
def __init__(self, file_id: Optional[FileId] = None):
28+
"""
29+
Initializes a new FileDeleteTransaction instance with optional file_id.
30+
31+
Args:
32+
file_id (FileId, optional): The ID of the file to be deleted.
33+
"""
34+
super().__init__()
35+
self.file_id = file_id
36+
self._default_transaction_fee = DEFAULT_TRANSACTION_FEE
37+
38+
def set_file_id(self, file_id: FileId) -> "FileDeleteTransaction":
39+
"""
40+
Sets the ID of the file to be deleted.
41+
42+
Args:
43+
file_id (FileId): The ID of the file to be deleted.
44+
45+
Returns:
46+
FileDeleteTransaction: Returns self for method chaining.
47+
"""
48+
self._require_not_frozen()
49+
self.file_id = file_id
50+
return self
51+
52+
def build_transaction_body(self):
53+
"""
54+
Builds and returns the protobuf transaction body for file deletion.
55+
56+
Returns:
57+
TransactionBody: The protobuf transaction body containing the file deletion details.
58+
"""
59+
if self.file_id is None:
60+
raise ValueError("Missing required FileID")
61+
62+
file_delete_body = FileDeleteTransactionBody(fileID=self.file_id._to_proto())
63+
64+
transaction_body = self.build_base_transaction_body()
65+
transaction_body.fileDelete.CopyFrom(file_delete_body)
66+
return transaction_body
67+
68+
def _get_method(self, channel: _Channel) -> _Method:
69+
"""
70+
Gets the method to execute the file delete transaction.
71+
72+
This internal method returns a _Method object containing the appropriate gRPC
73+
function to call when executing this transaction on the network.
74+
75+
Args:
76+
channel (_Channel): The channel containing service stubs
77+
78+
Returns:
79+
_Method: An object containing the transaction function to delete a file.
80+
"""
81+
return _Method(transaction_func=channel.file.deleteFile, query_func=None)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
Integration tests for FileDeleteTransaction.
3+
"""
4+
5+
import pytest
6+
7+
from hiero_sdk_python.crypto.private_key import PrivateKey
8+
from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction
9+
from hiero_sdk_python.file.file_delete_transaction import FileDeleteTransaction
10+
from hiero_sdk_python.file.file_id import FileId
11+
from hiero_sdk_python.file.file_info_query import FileInfoQuery
12+
from hiero_sdk_python.response_code import ResponseCode
13+
from tests.integration.utils_for_test import env
14+
15+
16+
@pytest.mark.integration
17+
def test_integration_file_delete_transaction_can_execute(env):
18+
"""Test that the FileDeleteTransaction can execute a file deletion transaction."""
19+
# Create a file
20+
receipt = (
21+
FileCreateTransaction()
22+
.set_keys(env.operator_key.public_key())
23+
.set_contents(b"Hello, World")
24+
.set_file_memo("go sdk e2e tests")
25+
.execute(env.client)
26+
)
27+
assert (
28+
receipt.status == ResponseCode.SUCCESS
29+
), f"Create file failed with status: {ResponseCode(receipt.status).name}"
30+
31+
file_id = receipt.file_id
32+
assert file_id is not None, "File ID is None"
33+
34+
# Then delete the file
35+
receipt = FileDeleteTransaction().set_file_id(file_id).execute(env.client)
36+
assert (
37+
receipt.status == ResponseCode.SUCCESS
38+
), f"Delete file failed with status: {ResponseCode(receipt.status).name}"
39+
40+
# Query the file info
41+
info = FileInfoQuery().set_file_id(file_id).execute(env.client)
42+
assert info.is_deleted is True, "File should be deleted"
43+
44+
45+
@pytest.mark.integration
46+
def test_integration_file_delete_transaction_fails_when_deleted_twice(env):
47+
"""Test that deleting a file twice fails."""
48+
# Create a file
49+
receipt = (
50+
FileCreateTransaction()
51+
.set_keys(env.operator_key.public_key())
52+
.set_contents(b"Hello, World")
53+
.set_file_memo("go sdk e2e tests")
54+
.execute(env.client)
55+
)
56+
assert receipt.status == ResponseCode.SUCCESS
57+
file_id = receipt.file_id
58+
assert file_id is not None
59+
60+
# Delete once
61+
receipt = FileDeleteTransaction().set_file_id(file_id).execute(env.client)
62+
assert receipt.status == ResponseCode.SUCCESS
63+
64+
# Try to delete again
65+
receipt = FileDeleteTransaction().set_file_id(file_id).execute(env.client)
66+
assert receipt.status == ResponseCode.FILE_DELETED, (
67+
f"File deletion should have failed with FILE_DELETED status but got: "
68+
f"{ResponseCode(receipt.status).name}"
69+
)
70+
71+
72+
@pytest.mark.integration
73+
def test_integration_file_delete_transaction_fails_when_file_does_not_exist(env):
74+
"""Test that deleting a non-existing file fails."""
75+
# Create a file ID that doesn't exist on the network
76+
file_id = FileId(0, 0, 999999999)
77+
78+
receipt = FileDeleteTransaction().set_file_id(file_id).execute(env.client)
79+
assert receipt.status == ResponseCode.INVALID_FILE_ID, (
80+
f"File deletion should have failed with INVALID_FILE_ID status but got: "
81+
f"{ResponseCode(receipt.status).name}"
82+
)
83+
84+
85+
@pytest.mark.integration
86+
def test_integration_file_delete_transaction_fails_when_key_is_invalid(env):
87+
"""Test that deleting a file without proper key raises an exception."""
88+
key = PrivateKey.generate_ed25519()
89+
90+
# Create a file
91+
receipt = (
92+
FileCreateTransaction()
93+
.set_keys(key.public_key())
94+
.set_contents(b"Test file")
95+
.freeze_with(env.client)
96+
.sign(key) # sign with the private key
97+
.execute(env.client)
98+
)
99+
assert receipt.status == ResponseCode.SUCCESS
100+
file_id = receipt.file_id
101+
assert file_id is not None
102+
103+
# Try to delete the file without the required key signature
104+
receipt = FileDeleteTransaction().set_file_id(file_id).execute(env.client)
105+
assert receipt.status == ResponseCode.INVALID_SIGNATURE, (
106+
f"File deletion should have failed with INVALID_SIGNATURE status but got: "
107+
f"{ResponseCode(receipt.status).name}"
108+
)

tests/unit/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from hiero_sdk_python.account.account_id import AccountId
44
from hiero_sdk_python.client.network import Network
55
from hiero_sdk_python.client.client import Client
6+
from hiero_sdk_python.file.file_id import FileId
67
from hiero_sdk_python.logger.log_level import LogLevel
78
from hiero_sdk_python.node import _Node
89
from hiero_sdk_python.consensus.topic_id import TopicId
@@ -59,6 +60,11 @@ def token_id():
5960
"""Fixture to provide a mock TokenId instance."""
6061
return TokenId(shard=0, realm=0, num=3)
6162

63+
@pytest.fixture
64+
def file_id():
65+
"""Fixture to provide a mock FileId instance."""
66+
return FileId(shard=0, realm=0, file=2)
67+
6268
@pytest.fixture
6369
def mock_client():
6470
"""Fixture to provide a mock client with hardcoded nodes for testing purposes."""

0 commit comments

Comments
 (0)