Skip to content

Commit f2015e9

Browse files
authored
feat(tck): add approveAllowance methods for the TCK module (hiero-ledger#2323)
Signed-off-by: Ntege Daniel <danientege785@gmail.com>
1 parent df8bbf0 commit f2015e9

9 files changed

Lines changed: 322 additions & 22 deletions

File tree

src/hiero_sdk_python/account/account_delete_transaction.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,10 @@ def _build_proto_body(self):
8585
Returns:
8686
CryptoDeleteTransactionBody: The protobuf body for this transaction.
8787
88-
Raises:
89-
ValueError: If account_id or transfer_account_id is not set.
9088
"""
91-
if self.account_id is None:
92-
raise ValueError("Missing required AccountID")
93-
94-
if self.transfer_account_id is None:
95-
raise ValueError("Missing AccountID for transfer")
96-
9789
return CryptoDeleteTransactionBody(
98-
deleteAccountID=self.account_id._to_proto(),
99-
transferAccountID=(self.transfer_account_id._to_proto() if self.transfer_account_id else None),
90+
deleteAccountID=(self.account_id._to_proto() if self.account_id is not None else None),
91+
transferAccountID=(self.transfer_account_id._to_proto() if self.transfer_account_id is not None else None),
10092
)
10193

10294
def build_transaction_body(self):
@@ -106,8 +98,6 @@ def build_transaction_body(self):
10698
Returns:
10799
TransactionBody: The built transaction body.
108100
109-
Raises:
110-
ValueError: If account_id or transfer_account_id is not set.
111101
"""
112102
account_delete_body = self._build_proto_body()
113103
transaction_body = self.build_base_transaction_body()

tck/handlers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Import all handler modules to trigger @rpc_method decorators
55
from . import (
66
account,
7+
allowance,
78
key,
89
sdk, # setup, reset
910
topic,

tck/handlers/account.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
6+
from hiero_sdk_python.account.account_delete_transaction import AccountDeleteTransaction
67
from hiero_sdk_python.account.account_id import AccountId
78
from hiero_sdk_python.account.account_info import AccountInfo
89
from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction
@@ -13,9 +14,10 @@
1314
from hiero_sdk_python.timestamp import Timestamp
1415
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
1516
from tck.handlers.registry import rpc_method
16-
from tck.param.account import CreateAccountParams, GetAccountInfoParams, UpdateAccountParams
17+
from tck.param.account import CreateAccountParams, DeleteAccountParams, GetAccountInfoParams, UpdateAccountParams
1718
from tck.response.account import (
1819
CreateAccountResponse,
20+
DeleteAccountResponse,
1921
GetAccountInfoResponse,
2022
StakingInfoResponse,
2123
TokenRelationshipResponse,
@@ -230,3 +232,25 @@ def get_account_info(params: GetAccountInfoParams) -> GetAccountInfoResponse:
230232

231233
info = query.execute(client)
232234
return _build_account_info_response(info)
235+
236+
237+
@rpc_method("deleteAccount")
238+
def delete_account(params: DeleteAccountParams) -> DeleteAccountResponse:
239+
"""Delete an account using TCK deleteAccount parameters."""
240+
client = get_client(params.sessionId)
241+
242+
transaction = AccountDeleteTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
243+
244+
if params.deleteAccountId is not None:
245+
transaction.set_account_id(AccountId.from_string(params.deleteAccountId))
246+
247+
if params.transferAccountId is not None:
248+
transaction.set_transfer_account_id(AccountId.from_string(params.transferAccountId))
249+
250+
if params.commonTransactionParams is not None:
251+
params.commonTransactionParams.apply_common_params(transaction, client)
252+
253+
response = transaction.execute(client, wait_for_receipt=False)
254+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
255+
256+
return DeleteAccountResponse(status=ResponseCode(receipt.status).name)

tck/handlers/allowance.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""TCK RPC handler for the approveAllowance endpoint."""
2+
3+
from __future__ import annotations
4+
5+
from hiero_sdk_python.account.account_allowance_approve_transaction import (
6+
AccountAllowanceApproveTransaction,
7+
)
8+
from hiero_sdk_python.account.account_id import AccountId
9+
from hiero_sdk_python.hbar import Hbar
10+
from hiero_sdk_python.response_code import ResponseCode
11+
from hiero_sdk_python.tokens.nft_id import NftId
12+
from hiero_sdk_python.tokens.token_id import TokenId
13+
from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt
14+
from tck.handlers.registry import rpc_method
15+
from tck.param.allowance import AllowanceEntry, ApproveAllowanceParams
16+
from tck.response.allowance import ApproveAllowanceResponse
17+
from tck.util.client_utils import get_client
18+
from tck.util.constants import DEFAULT_GRPC_TIMEOUT
19+
20+
21+
def _build_approve_allowance_transaction(
22+
params: ApproveAllowanceParams,
23+
) -> AccountAllowanceApproveTransaction:
24+
"""Build an AccountAllowanceApproveTransaction from TCK params."""
25+
transaction = AccountAllowanceApproveTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT)
26+
27+
if not params.allowances:
28+
raise ValueError("allowances list cannot be empty")
29+
30+
for entry in params.allowances:
31+
_apply_allowance_entry(transaction, entry)
32+
33+
return transaction
34+
35+
36+
def _parse_optional_account_id(value: str | None) -> AccountId | None:
37+
"""Parse an optional account ID string, raising on empty string."""
38+
if value is None:
39+
return None
40+
if not value.strip():
41+
raise ValueError("Account ID cannot be an empty string")
42+
return AccountId.from_string(value)
43+
44+
45+
def _apply_allowance_entry(
46+
transaction: AccountAllowanceApproveTransaction,
47+
entry: AllowanceEntry,
48+
) -> None:
49+
"""Apply a single allowance entry to the transaction."""
50+
owner_account_id = _parse_optional_account_id(entry.ownerAccountId)
51+
spender_account_id = _parse_optional_account_id(entry.spenderAccountId)
52+
53+
if entry.hbar is not None:
54+
if entry.hbar.amount is None:
55+
raise ValueError("hbar allowance requires an amount")
56+
amount = int(entry.hbar.amount)
57+
transaction.approve_hbar_allowance(
58+
owner_account_id,
59+
spender_account_id,
60+
Hbar.from_tinybars(amount),
61+
)
62+
63+
if entry.token is not None:
64+
if entry.token.tokenId is None or entry.token.amount is None:
65+
raise ValueError("token allowance requires tokenId and amount")
66+
token_id = TokenId.from_string(entry.token.tokenId)
67+
amount = int(entry.token.amount)
68+
transaction.approve_token_allowance(
69+
token_id,
70+
owner_account_id,
71+
spender_account_id,
72+
amount,
73+
)
74+
75+
if entry.nft is not None:
76+
token_id = TokenId.from_string(entry.nft.tokenId)
77+
78+
if entry.nft.approvedForAll is True:
79+
transaction.approve_token_nft_allowance_all_serials(
80+
token_id,
81+
owner_account_id,
82+
spender_account_id,
83+
)
84+
elif entry.nft.approvedForAll is False:
85+
transaction.delete_token_nft_allowance_all_serials(
86+
token_id,
87+
owner_account_id,
88+
spender_account_id,
89+
)
90+
elif entry.nft.serialNumbers is not None:
91+
delegating_spender = _parse_optional_account_id(entry.nft.delegateSpenderAccountId)
92+
93+
for serial in entry.nft.serialNumbers:
94+
nft_id = NftId(token_id=token_id, serial_number=int(serial))
95+
if delegating_spender is not None:
96+
transaction.approve_token_nft_allowance_with_delegating_spender(
97+
nft_id,
98+
owner_account_id,
99+
spender_account_id,
100+
delegating_spender,
101+
)
102+
else:
103+
transaction.approve_token_nft_allowance(
104+
nft_id,
105+
owner_account_id,
106+
spender_account_id,
107+
)
108+
else:
109+
# nft object present with only tokenId — this is DeleteNftAllowanceAllSerials
110+
# with no approvedForAll field (defaults to delete)
111+
transaction.delete_token_nft_allowance_all_serials(
112+
token_id,
113+
owner_account_id,
114+
spender_account_id,
115+
)
116+
117+
118+
@rpc_method("approveAllowance")
119+
def approve_allowance(params: ApproveAllowanceParams) -> ApproveAllowanceResponse:
120+
"""Approve allowances using TCK approveAllowance parameters."""
121+
client = get_client(params.sessionId)
122+
123+
transaction = _build_approve_allowance_transaction(params)
124+
125+
if params.commonTransactionParams is not None:
126+
params.commonTransactionParams.apply_common_params(transaction, client)
127+
128+
response = transaction.execute(client, wait_for_receipt=False)
129+
receipt: TransactionReceipt = response.get_receipt(client, validate_status=True)
130+
131+
return ApproveAllowanceResponse(status=ResponseCode(receipt.status).name)

tck/param/account.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,24 @@ def parse_json_params(cls, params: dict) -> UpdateAccountParams:
8585
)
8686

8787

88+
@dataclass
89+
class DeleteAccountParams(BaseTransactionParams):
90+
"""Request parameters for the deleteAccount endpoint."""
91+
92+
deleteAccountId: str | None = None
93+
transferAccountId: str | None = None
94+
95+
@classmethod
96+
def parse_json_params(cls, params: dict) -> DeleteAccountParams:
97+
"""Parse JSON-RPC params into a DeleteAccountParams instance."""
98+
return cls(
99+
deleteAccountId=params.get("deleteAccountId"),
100+
transferAccountId=params.get("transferAccountId"),
101+
sessionId=parse_session_id(params),
102+
commonTransactionParams=parse_common_transaction_params(params),
103+
)
104+
105+
88106
@dataclass
89107
class GetAccountInfoParams(BaseParams):
90108
"""Request parameters for the getAccountInfo endpoint."""

tck/param/allowance.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""TCK request parameter models for approveAllowance endpoint."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
7+
from tck.param.base import BaseTransactionParams
8+
from tck.util.param_utils import (
9+
parse_common_transaction_params,
10+
parse_session_id,
11+
to_bool,
12+
)
13+
14+
15+
@dataclass
16+
class HbarAllowanceParams:
17+
"""Nested hbar allowance parameters."""
18+
19+
amount: str | None = None
20+
21+
@classmethod
22+
def parse_json_params(cls, params: dict) -> HbarAllowanceParams:
23+
return cls(amount=params.get("amount"))
24+
25+
26+
@dataclass
27+
class TokenAllowanceParams:
28+
"""Nested token allowance parameters."""
29+
30+
tokenId: str | None = None
31+
amount: str | None = None
32+
33+
@classmethod
34+
def parse_json_params(cls, params: dict) -> TokenAllowanceParams:
35+
return cls(
36+
tokenId=params.get("tokenId"),
37+
amount=params.get("amount"),
38+
)
39+
40+
41+
@dataclass
42+
class NftAllowanceParams:
43+
"""Nested NFT allowance parameters."""
44+
45+
tokenId: str | None = None
46+
serialNumbers: list[str] | None = None
47+
approvedForAll: bool | None = None
48+
delegateSpenderAccountId: str | None = None
49+
50+
@classmethod
51+
def parse_json_params(cls, params: dict) -> NftAllowanceParams:
52+
return cls(
53+
tokenId=params.get("tokenId"),
54+
serialNumbers=params.get("serialNumbers"),
55+
approvedForAll=to_bool(params.get("approvedForAll")),
56+
delegateSpenderAccountId=params.get("delegateSpenderAccountId"),
57+
)
58+
59+
60+
@dataclass
61+
class AllowanceEntry:
62+
"""A single allowance entry in the allowances list."""
63+
64+
ownerAccountId: str | None = None
65+
spenderAccountId: str | None = None
66+
hbar: HbarAllowanceParams | None = None
67+
token: TokenAllowanceParams | None = None
68+
nft: NftAllowanceParams | None = None
69+
70+
@classmethod
71+
def parse_json_params(cls, params: dict) -> AllowanceEntry:
72+
hbar = params.get("hbar")
73+
token = params.get("token")
74+
nft = params.get("nft")
75+
76+
return cls(
77+
ownerAccountId=params.get("ownerAccountId"),
78+
spenderAccountId=params.get("spenderAccountId"),
79+
hbar=HbarAllowanceParams.parse_json_params(hbar) if hbar is not None else None,
80+
token=TokenAllowanceParams.parse_json_params(token) if token is not None else None,
81+
nft=NftAllowanceParams.parse_json_params(nft) if nft is not None else None,
82+
)
83+
84+
85+
@dataclass
86+
class ApproveAllowanceParams(BaseTransactionParams):
87+
"""Request parameters for the approveAllowance endpoint."""
88+
89+
allowances: list[AllowanceEntry] | None = None
90+
91+
@classmethod
92+
def parse_json_params(cls, params: dict) -> ApproveAllowanceParams:
93+
"""Parse JSON-RPC params into an ApproveAllowanceParams instance."""
94+
raw_allowances = params.get("allowances")
95+
allowances = None
96+
if raw_allowances is not None:
97+
allowances = [AllowanceEntry.parse_json_params(entry) for entry in raw_allowances]
98+
99+
return cls(
100+
allowances=allowances,
101+
sessionId=parse_session_id(params),
102+
commonTransactionParams=parse_common_transaction_params(params),
103+
)

tck/response/account.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ class UpdateAccountResponse:
2020
status: str | None = None
2121

2222

23+
@dataclass
24+
class DeleteAccountResponse:
25+
"""Response payload for deleteAccount."""
26+
27+
status: str | None = None
28+
29+
2330
@dataclass
2431
class StakingInfoResponse:
2532
"""Nested staking fields in the getAccountInfo response."""

tck/response/allowance.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""TCK response models for approveAllowance endpoint."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
7+
8+
@dataclass
9+
class ApproveAllowanceResponse:
10+
"""Response payload for approveAllowance."""
11+
12+
status: str | None = None

0 commit comments

Comments
 (0)