diff --git a/.env.example b/.env.example index 1c7169f..05ab82a 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,5 @@ ADMIN_KEY= TOKEN_ID= TOKEN_NAME= TOKEN_SYMBOL= +TOPIC_ID= NETWORK= \ No newline at end of file diff --git a/examples/account_create.py b/examples/account_create.py index d136f67..42aab5b 100644 --- a/examples/account_create.py +++ b/examples/account_create.py @@ -2,9 +2,6 @@ import sys from dotenv import load_dotenv -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, project_root) - from hedera_sdk_python.client.client import Client from hedera_sdk_python.account.account_id import AccountId from hedera_sdk_python.crypto.private_key import PrivateKey diff --git a/examples/query_balance.py b/examples/query_balance.py index cf42ede..89333cb 100644 --- a/examples/query_balance.py +++ b/examples/query_balance.py @@ -3,9 +3,6 @@ import time from dotenv import load_dotenv -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, project_root) - from hedera_sdk_python.account.account_id import AccountId from hedera_sdk_python.crypto.private_key import PrivateKey from hedera_sdk_python.client.network import Network diff --git a/examples/query_receipt.py b/examples/query_receipt.py index 0d422ea..9dcc430 100644 --- a/examples/query_receipt.py +++ b/examples/query_receipt.py @@ -2,9 +2,6 @@ import sys from dotenv import load_dotenv -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, project_root) - from hedera_sdk_python.account.account_id import AccountId from hedera_sdk_python.crypto.private_key import PrivateKey from hedera_sdk_python.client.network import Network diff --git a/examples/query_topic_info.py b/examples/query_topic_info.py new file mode 100644 index 0000000..ec7f8f5 --- /dev/null +++ b/examples/query_topic_info.py @@ -0,0 +1,30 @@ +import os +from dotenv import load_dotenv + +from hedera_sdk_python.client.network import Network +from hedera_sdk_python.client.client import Client +from hedera_sdk_python.consensus.topic_id import TopicId +from hedera_sdk_python.query.topic_info_query import TopicInfoQuery +from hedera_sdk_python.account.account_id import AccountId +from hedera_sdk_python.crypto.private_key import PrivateKey + +load_dotenv() + +def query_topic_info(): + operator_id = AccountId.from_string(os.getenv('OPERATOR_ID')) + operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY')) + topic_id = TopicId.from_string(os.getenv('TOPIC_ID')) + + network = Network(network='testnet') + client = Client(network) + client.set_operator(operator_id, operator_key) + + topic_info_query = TopicInfoQuery().set_topic_id(topic_id) + try: + topic_info = topic_info_query.execute(client) + print(topic_info) + except Exception as e: + print(f"Failed to retrieve topic info: {e}") + +if __name__ == "__main__": + query_topic_info() diff --git a/examples/query_topic_message.py b/examples/query_topic_message.py new file mode 100644 index 0000000..aa45e56 --- /dev/null +++ b/examples/query_topic_message.py @@ -0,0 +1,42 @@ +import os +from dotenv import load_dotenv + +from hedera_sdk_python.client.network import Network +from hedera_sdk_python.client.client import Client +from hedera_sdk_python.consensus.topic_id import TopicId +from hedera_sdk_python.query.topic_message_query import TopicMessageQuery +from datetime import datetime +import time + +load_dotenv() + +def query_topic_messages(): + + network = Network(network='testnet') + client = Client(network) + + def on_message_handler(msg): + print("Received topic message:", msg) + + def on_error_handler(e): + print("Subscription error:", e) + + query = ( + TopicMessageQuery() + .set_topic_id("0.0.12345") + .set_start_time(datetime.utcnow()) + .set_chunking_enabled(True) + ) + + query.subscribe( + client, + on_message=on_message_handler, + on_error=on_error_handler + ) + + time.sleep(10) + print("Done waiting. Exiting.") + + +if __name__ == "__main__": + query_topic_messages() diff --git a/examples/token_associate.py b/examples/token_associate.py index 5ae0b4a..0a5566a 100644 --- a/examples/token_associate.py +++ b/examples/token_associate.py @@ -2,9 +2,6 @@ import sys from dotenv import load_dotenv -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, project_root) - from hedera_sdk_python.client.client import Client from hedera_sdk_python.account.account_id import AccountId from hedera_sdk_python.crypto.private_key import PrivateKey diff --git a/examples/token_create.py b/examples/token_create.py index 129e90d..ebea937 100644 --- a/examples/token_create.py +++ b/examples/token_create.py @@ -2,9 +2,6 @@ import sys from dotenv import load_dotenv -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, project_root) - from hedera_sdk_python.client.client import Client from hedera_sdk_python.account.account_id import AccountId from hedera_sdk_python.crypto.private_key import PrivateKey diff --git a/examples/transfer_hbar.py b/examples/transfer_hbar.py index aadc857..97d5082 100644 --- a/examples/transfer_hbar.py +++ b/examples/transfer_hbar.py @@ -2,9 +2,6 @@ import sys from dotenv import load_dotenv -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, project_root) - from hedera_sdk_python.client.client import Client from hedera_sdk_python.account.account_id import AccountId from hedera_sdk_python.crypto.private_key import PrivateKey diff --git a/examples/transfer_token.py b/examples/transfer_token.py index 72a82ac..ca97d91 100644 --- a/examples/transfer_token.py +++ b/examples/transfer_token.py @@ -2,9 +2,6 @@ import sys from dotenv import load_dotenv -project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, project_root) - from hedera_sdk_python.client.client import Client from hedera_sdk_python.account.account_id import AccountId from hedera_sdk_python.crypto.private_key import PrivateKey diff --git a/generate_proto.sh b/generate_proto.sh index 7a2b9b1..c285dd6 100755 --- a/generate_proto.sh +++ b/generate_proto.sh @@ -1,41 +1,58 @@ -#!/bin/bash +#!/usr/bin/env bash -# Source the activate script to set up the PATH for this shell session -source ./.venv/bin/activate +set -e hapi_version=v0.57.3 protos_dir=.protos -mkdir -p $protos_dir -rm -rf $protos_dir/* +mkdir -p "$protos_dir" +rm -rf "$protos_dir"/* -# Download the tarball of a specific tag and immediately extract the subdirectory -curl -sL "https://github.com/hashgraph/hedera-protobufs/archive/refs/tags/${hapi_version}.tar.gz" | tar -xz -C $protos_dir --strip-components=1 -# Keep 'platform' and 'services', remove everything else in the current directory -find "$protos_dir" -mindepth 1 -maxdepth 1 ! -name platform ! -name services -exec rm -r {} + +curl -sL "https://github.com/hashgraph/hedera-protobufs/archive/refs/tags/${hapi_version}.tar.gz" \ + | tar -xz -C "$protos_dir" --strip-components=1 + +find "$protos_dir" -mindepth 1 -maxdepth 1 \ + ! -name platform \ + ! -name mirror \ + ! -name services \ + -exec rm -r {} + rm -rf src/hedera_sdk_python/hapi/* mkdir -p src/hedera_sdk_python/hapi/auxiliary/tss mkdir -p src/hedera_sdk_python/hapi/event +mkdir -p src/hedera_sdk_python/hapi/mirror touch src/hedera_sdk_python/hapi/__init__.py + +python -m grpc_tools.protoc \ + --proto_path="$protos_dir/platform" \ + --proto_path="$protos_dir/services" \ + --pyi_out=./src/hedera_sdk_python/hapi \ + --python_out=./src/hedera_sdk_python/hapi \ + --grpc_python_out=./src/hedera_sdk_python/hapi \ + "$protos_dir"/services/*.proto \ + "$protos_dir"/services/auxiliary/tss/*.proto \ + "$protos_dir"/platform/event/*.proto + python -m grpc_tools.protoc \ - --proto_path=$protos_dir/platform \ - --proto_path=$protos_dir/services \ - --pyi_out=./src/hedera_sdk_python/hapi \ - --python_out=./src/hedera_sdk_python/hapi \ - --grpc_python_out=./src/hedera_sdk_python/hapi \ - $protos_dir/services/*.proto $protos_dir/services/auxiliary/tss/*.proto $protos_dir/platform/event/*.proto - -# Modify the script for the specific import changes + --proto_path="$protos_dir/mirror" \ + --proto_path="$protos_dir/services" \ + --proto_path="$protos_dir/platform" \ + --pyi_out=./src/hedera_sdk_python/hapi/mirror \ + --python_out=./src/hedera_sdk_python/hapi/mirror \ + --grpc_python_out=./src/hedera_sdk_python/hapi/mirror \ + $(find "$protos_dir/mirror" -name '*.proto') + if [[ "$OSTYPE" == "darwin"* ]]; then - find ./src/hedera_sdk_python/hapi -type f -name "*.py" -exec sed -i '' \ - -e '/^import .*_pb2 as .*__pb2/s/^/from . /' \ - -e 's/^from auxiliary\.tss/from .auxiliary.tss/' \ - -e 's/^from event/from .event/' {} + + find ./src/hedera_sdk_python/hapi -type f -name "*.py" -exec sed -i '' \ + -e '/^import .*_pb2 as .*__pb2/s/^/from . /' \ + -e 's/^from auxiliary\.tss/from .auxiliary.tss/' \ + -e 's/^from event/from .event/' {} + else - find ./src/hedera_sdk_python/hapi -type f -name "*.py" -exec sed -i \ - -e '/^import .*_pb2 as .*__pb2/s/^/from . /' \ - -e 's/^from auxiliary\.tss/from .auxiliary.tss/' \ - -e 's/^from event/from .event/' {} + + find ./src/hedera_sdk_python/hapi -type f -name "*.py" -exec sed -i \ + -e '/^import .*_pb2 as .*__pb2/s/^/from . /' \ + -e 's/^from auxiliary\.tss/from .auxiliary.tss/' \ + -e 's/^from event/from .event/' {} + fi + +echo "All done generating protos." diff --git a/src/.DS_Store b/src/.DS_Store index deed783..596063c 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/hedera_sdk_python/client/client.py b/src/hedera_sdk_python/client/client.py index 29752ae..40399f0 100644 --- a/src/hedera_sdk_python/client/client.py +++ b/src/hedera_sdk_python/client/client.py @@ -98,16 +98,22 @@ def send_query(self, query, node_account_id, timeout=60): None. Exceptions are caught and printed, returning None on failure. """ self._switch_node(node_account_id) - stub = self.crypto_stub try: request = query._make_request() + if hasattr(request, 'cryptogetAccountBalance'): - response = stub.cryptoGetBalance(request, timeout=timeout) + response = self.crypto_stub.cryptoGetBalance(request, timeout=timeout) + elif hasattr(request, 'transactionGetReceipt'): - response = stub.getTransactionReceipts(request, timeout=timeout) + response = self.crypto_stub.getTransactionReceipts(request, timeout=timeout) + + elif hasattr(request, 'consensusGetTopicInfo'): + response = self.topic_stub.getTopicInfo(request, timeout=timeout) + else: raise Exception("Unsupported query type.") + return response except grpc.RpcError as e: print(f"gRPC error during query execution: {e}") diff --git a/src/hedera_sdk_python/consensus/topic_info.py b/src/hedera_sdk_python/consensus/topic_info.py new file mode 100644 index 0000000..85e0e59 --- /dev/null +++ b/src/hedera_sdk_python/consensus/topic_info.py @@ -0,0 +1,66 @@ +from hedera_sdk_python.hapi.basic_types_pb2 import Key, AccountID +from hedera_sdk_python.hapi.timestamp_pb2 import Timestamp +from hedera_sdk_python.hapi.duration_pb2 import Duration + +class TopicInfo: + """ + Represents the information retrieved from ConsensusService.getTopicInfo() about a topic. + """ + + def __init__( + self, + memo: str, + running_hash: bytes, + sequence_number: int, + expiration_time: Timestamp, + admin_key: Key, + submit_key: Key, + auto_renew_period: Duration, + auto_renew_account: AccountID, + ledger_id: bytes, + ): + self.memo = memo + self.running_hash = running_hash + self.sequence_number = sequence_number + self.expiration_time = expiration_time + self.admin_key = admin_key + self.submit_key = submit_key + self.auto_renew_period = auto_renew_period + self.auto_renew_account = auto_renew_account + self.ledger_id = ledger_id + + @classmethod + def from_proto(cls, topic_info_proto): + """ + Constructs a TopicInfo object from a protobuf ConsensusTopicInfo message. + + Args: + topic_info_proto (ConsensusTopicInfo): The protobuf message with topic info. + + Returns: + TopicInfo: A new instance populated from the protobuf message. + """ + return cls( + memo=topic_info_proto.memo, + running_hash=topic_info_proto.runningHash, + sequence_number=topic_info_proto.sequenceNumber, + expiration_time=topic_info_proto.expirationTime, + admin_key=topic_info_proto.adminKey if topic_info_proto.HasField("adminKey") else None, + submit_key=topic_info_proto.submitKey if topic_info_proto.HasField("submitKey") else None, + auto_renew_period=topic_info_proto.autoRenewPeriod + if topic_info_proto.HasField("autoRenewPeriod") + else None, + auto_renew_account=topic_info_proto.autoRenewAccount + if topic_info_proto.HasField("autoRenewAccount") + else None, + ledger_id=topic_info_proto.ledger_id, + ) + + def __repr__(self): + return ( + f"TopicInfo(memo={self.memo!r}, running_hash={self.running_hash!r}, " + f"sequence_number={self.sequence_number}, expiration_time={self.expiration_time}, " + f"admin_key={self.admin_key}, submit_key={self.submit_key}, " + f"auto_renew_period={self.auto_renew_period}, auto_renew_account={self.auto_renew_account}, " + f"ledger_id={self.ledger_id!r})" + ) diff --git a/src/hedera_sdk_python/py.typed b/src/hedera_sdk_python/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/src/hedera_sdk_python/query/topic_info_query.py b/src/hedera_sdk_python/query/topic_info_query.py new file mode 100644 index 0000000..0969965 --- /dev/null +++ b/src/hedera_sdk_python/query/topic_info_query.py @@ -0,0 +1,48 @@ +from hedera_sdk_python.query.query import Query +from hedera_sdk_python.hapi import query_pb2, consensus_get_topic_info_pb2 +from hedera_sdk_python.consensus.topic_id import TopicId +from hedera_sdk_python.consensus.topic_info import TopicInfo + +class TopicInfoQuery(Query): + """ + A query to retrieve information about a specific Hedera topic. + """ + + def __init__(self): + super().__init__() + self.topic_id = None + + def set_topic_id(self, topic_id: TopicId): + self.topic_id = topic_id + return self + + def _make_request(self): + if not self.topic_id: + raise ValueError("Topic ID must be set before making the request.") + + query_header = self._make_request_header() + + topic_info_query = consensus_get_topic_info_pb2.ConsensusGetTopicInfoQuery() + topic_info_query.header.CopyFrom(query_header) + topic_info_query.topicID.CopyFrom(self.topic_id.to_proto()) + + query = query_pb2.Query() + query.consensusGetTopicInfo.CopyFrom(topic_info_query) + + return query + + def _get_status_from_response(self, response): + """ + Must read nodeTransactionPrecheckCode from response.consensusGetTopicInfo.header + """ + return response.consensusGetTopicInfo.header.nodeTransactionPrecheckCode + + def _map_response(self, response): + """ + Return a TopicInfo instance built from the protobuf + """ + if not response.consensusGetTopicInfo.topicInfo: + raise Exception("No topicInfo returned in the response.") + + proto_topic_info = response.consensusGetTopicInfo.topicInfo + return TopicInfo.from_proto(proto_topic_info) diff --git a/src/hedera_sdk_python/query/topic_message_query.py b/src/hedera_sdk_python/query/topic_message_query.py new file mode 100644 index 0000000..6d0630f --- /dev/null +++ b/src/hedera_sdk_python/query/topic_message_query.py @@ -0,0 +1,92 @@ +import time +import threading +from datetime import datetime +from typing import Optional, Callable + +from hedera_sdk_python.hapi.mirror import consensus_service_pb2 as mirror_proto +from hedera_sdk_python.hapi import basic_types_pb2, timestamp_pb2 + + +class TopicMessageQuery: + """ + A query to subscribe to messages from a specific HCS topic, via a mirror node. + """ + + def __init__(self): + self._topic_id = None + self._start_time = None + self._end_time = None + self._limit = None + + def set_topic_id(self, shard: int, realm: int, topic: int): + self._topic_id = basic_types_pb2.TopicID( + shardNum=shard, + realmNum=realm, + topicNum=topic + ) + return self + + def set_start_time(self, dt: datetime): + """ + Only receive messages with a consensus timestamp >= dt. + """ + seconds = int(dt.timestamp()) + nanos = int((dt.timestamp() - seconds) * 1e9) + + self._start_time = timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos) + return self + + def set_end_time(self, dt: datetime): + """ + Only receive messages with a consensus timestamp < dt. + """ + seconds = int(dt.timestamp()) + nanos = int((dt.timestamp() - seconds) * 1e9) + + self._end_time = timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos) + return self + + def set_limit(self, limit: int): + """ + Receive at most `limit` messages, then end the subscription. + """ + self._limit = limit + return self + + def subscribe( + self, + client, + on_message: Callable[[mirror_proto.ConsensusTopicResponse], None], + on_error: Optional[Callable[[Exception], None]] = None, + ): + """ + Opens a streaming subscription to the mirror node in the given client, calling on_message() + for each received message. Returns immediately, streaming in a background thread. + """ + + if not self._topic_id: + raise ValueError("Topic ID must be set before subscribing.") + if not client.mirror_stub: + raise ValueError("Client has no mirror_stub. Did you configure a mirror node address?") + + request = mirror_proto.ConsensusTopicQuery( + topicID=self._topic_id + ) + if self._start_time: + request.consensusStartTime.CopyFrom(self._start_time) + if self._end_time: + request.consensusEndTime.CopyFrom(self._end_time) + if self._limit is not None: + request.limit = self._limit + + def run_stream(): + try: + message_stream = client.mirror_stub.subscribeTopic(request) + for message in message_stream: + on_message(message) + except Exception as e: + if on_error: + on_error(e) + + thread = threading.Thread(target=run_stream, daemon=True) + thread.start() diff --git a/src/hedera_sdk_python/response_code.py b/src/hedera_sdk_python/response_code.py index cd75492..c41878e 100644 --- a/src/hedera_sdk_python/response_code.py +++ b/src/hedera_sdk_python/response_code.py @@ -22,6 +22,271 @@ class ResponseCode: INVALID_SOLIDITY_ID = 20 UNKNOWN = 21 SUCCESS = 22 + FAIL_INVALID = 23 + FAIL_FEE = 24 + FAIL_BALANCE = 25 + KEY_REQUIRED = 26 + BAD_ENCODING = 27 + INSUFFICIENT_ACCOUNT_BALANCE = 28 + INVALID_SOLIDITY_ADDRESS = 29 + INSUFFICIENT_GAS = 30 + CONTRACT_SIZE_LIMIT_EXCEEDED = 31 + LOCAL_CALL_MODIFICATION_EXCEPTION = 32 + CONTRACT_REVERT_EXECUTED = 33 + CONTRACT_EXECUTION_EXCEPTION = 34 + INVALID_RECEIVING_NODE_ACCOUNT = 35 + MISSING_QUERY_HEADER = 36 + ACCOUNT_UPDATE_FAILED = 37 + INVALID_KEY_ENCODING = 38 + NULL_SOLIDITY_ADDRESS = 39 + CONTRACT_UPDATE_FAILED = 40 + INVALID_QUERY_HEADER = 41 + INVALID_FEE_SUBMITTED = 42 + INVALID_PAYER_SIGNATURE = 43 + KEY_NOT_PROVIDED = 44 + INVALID_EXPIRATION_TIME = 45 + NO_WACL_KEY = 46 + FILE_CONTENT_EMPTY = 47 + INVALID_ACCOUNT_AMOUNTS = 48 + EMPTY_TRANSACTION_BODY = 49 + INVALID_TRANSACTION_BODY = 50 + INVALID_SIGNATURE_TYPE_MISMATCHING_KEY = 51 + INVALID_SIGNATURE_COUNT_MISMATCHING_KEY = 52 + EMPTY_LIVE_HASH_BODY = 53 + EMPTY_LIVE_HASH = 54 + EMPTY_LIVE_HASH_KEYS = 55 + INVALID_LIVE_HASH_SIZE = 56 + EMPTY_QUERY_BODY = 57 + EMPTY_LIVE_HASH_QUERY = 58 + LIVE_HASH_NOT_FOUND = 59 + ACCOUNT_ID_DOES_NOT_EXIST = 60 + LIVE_HASH_ALREADY_EXISTS = 61 + INVALID_FILE_WACL = 62 + SERIALIZATION_FAILED = 63 + TRANSACTION_OVERSIZE = 64 + TRANSACTION_TOO_MANY_LAYERS = 65 + CONTRACT_DELETED = 66 + PLATFORM_NOT_ACTIVE = 67 + KEY_PREFIX_MISMATCH = 68 + PLATFORM_TRANSACTION_NOT_CREATED = 69 + INVALID_RENEWAL_PERIOD = 70 + INVALID_PAYER_ACCOUNT_ID = 71 + ACCOUNT_DELETED = 72 + FILE_DELETED = 73 + ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS = 74 + SETTING_NEGATIVE_ACCOUNT_BALANCE = 75 + OBTAINER_REQUIRED = 76 + OBTAINER_SAME_CONTRACT_ID = 77 + OBTAINER_DOES_NOT_EXIST = 78 + MODIFYING_IMMUTABLE_CONTRACT = 79 + FILE_SYSTEM_EXCEPTION = 80 + AUTORENEW_DURATION_NOT_IN_RANGE = 81 + ERROR_DECODING_BYTESTRING = 82 + CONTRACT_FILE_EMPTY = 83 + CONTRACT_BYTECODE_EMPTY = 84 + INVALID_INITIAL_BALANCE = 85 + INVALID_RECEIVE_RECORD_THRESHOLD = 86 # [Deprecated] + INVALID_SEND_RECORD_THRESHOLD = 87 # [Deprecated] + ACCOUNT_IS_NOT_GENESIS_ACCOUNT = 88 + PAYER_ACCOUNT_UNAUTHORIZED = 89 + INVALID_FREEZE_TRANSACTION_BODY = 90 + FREEZE_TRANSACTION_BODY_NOT_FOUND = 91 + TRANSFER_LIST_SIZE_LIMIT_EXCEEDED = 92 + RESULT_SIZE_LIMIT_EXCEEDED = 93 + NOT_SPECIAL_ACCOUNT = 94 + CONTRACT_NEGATIVE_GAS = 95 + CONTRACT_NEGATIVE_VALUE = 96 + INVALID_FEE_FILE = 97 + INVALID_EXCHANGE_RATE_FILE = 98 + INSUFFICIENT_LOCAL_CALL_GAS = 99 + ENTITY_NOT_ALLOWED_TO_DELETE = 100 + AUTHORIZATION_FAILED = 101 + FILE_UPLOADED_PROTO_INVALID = 102 + FILE_UPLOADED_PROTO_NOT_SAVED_TO_DISK = 103 + FEE_SCHEDULE_FILE_PART_UPLOADED = 104 + EXCHANGE_RATE_CHANGE_LIMIT_EXCEEDED = 105 + MAX_CONTRACT_STORAGE_EXCEEDED = 106 + TRANSFER_ACCOUNT_SAME_AS_DELETE_ACCOUNT = 107 + TOTAL_LEDGER_BALANCE_INVALID = 108 + EXPIRATION_REDUCTION_NOT_ALLOWED = 110 + MAX_GAS_LIMIT_EXCEEDED = 111 + MAX_FILE_SIZE_EXCEEDED = 112 + + INVALID_TOPIC_ID = 150 + INVALID_ADMIN_KEY = 155 + INVALID_SUBMIT_KEY = 156 + UNAUTHORIZED = 157 + INVALID_TOPIC_MESSAGE = 158 + INVALID_AUTORENEW_ACCOUNT = 159 + AUTORENEW_ACCOUNT_NOT_ALLOWED = 160 + TOPIC_EXPIRED = 162 + INVALID_CHUNK_NUMBER = 163 + INVALID_CHUNK_TRANSACTION_ID = 164 + ACCOUNT_FROZEN_FOR_TOKEN = 165 + TOKENS_PER_ACCOUNT_LIMIT_EXCEEDED = 166 + INVALID_TOKEN_ID = 167 + INVALID_TOKEN_DECIMALS = 168 + INVALID_TOKEN_INITIAL_SUPPLY = 169 + INVALID_TREASURY_ACCOUNT_FOR_TOKEN = 170 + INVALID_TOKEN_SYMBOL = 171 + TOKEN_HAS_NO_FREEZE_KEY = 172 + TRANSFERS_NOT_ZERO_SUM_FOR_TOKEN = 173 + MISSING_TOKEN_SYMBOL = 174 + TOKEN_SYMBOL_TOO_LONG = 175 + ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN = 176 + TOKEN_HAS_NO_KYC_KEY = 177 + INSUFFICIENT_TOKEN_BALANCE = 178 + TOKEN_WAS_DELETED = 179 + TOKEN_HAS_NO_SUPPLY_KEY = 180 + TOKEN_HAS_NO_WIPE_KEY = 181 + INVALID_TOKEN_MINT_AMOUNT = 182 + INVALID_TOKEN_BURN_AMOUNT = 183 + TOKEN_NOT_ASSOCIATED_TO_ACCOUNT = 184 + CANNOT_WIPE_TOKEN_TREASURY_ACCOUNT = 185 + INVALID_KYC_KEY = 186 + INVALID_WIPE_KEY = 187 + INVALID_FREEZE_KEY = 188 + INVALID_SUPPLY_KEY = 189 + MISSING_TOKEN_NAME = 190 + TOKEN_NAME_TOO_LONG = 191 + INVALID_WIPING_AMOUNT = 192 + TOKEN_IS_IMMUTABLE = 193 + TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT = 194 + TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES = 195 + ACCOUNT_IS_TREASURY = 196 + TOKEN_ID_REPEATED_IN_TOKEN_LIST = 197 + TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED = 198 + EMPTY_TOKEN_TRANSFER_BODY = 199 + EMPTY_TOKEN_TRANSFER_ACCOUNT_AMOUNTS = 200 + INVALID_SCHEDULE_ID = 201 + SCHEDULE_IS_IMMUTABLE = 202 + INVALID_SCHEDULE_PAYER_ID = 203 + INVALID_SCHEDULE_ACCOUNT_ID = 204 + NO_NEW_VALID_SIGNATURES = 205 + UNRESOLVABLE_REQUIRED_SIGNERS = 206 + SCHEDULED_TRANSACTION_NOT_IN_WHITELIST = 207 + SOME_SIGNATURES_WERE_INVALID = 208 + TRANSACTION_ID_FIELD_NOT_ALLOWED = 209 + IDENTICAL_SCHEDULE_ALREADY_CREATED = 210 + INVALID_ZERO_BYTE_IN_STRING = 211 + SCHEDULE_ALREADY_DELETED = 212 + SCHEDULE_ALREADY_EXECUTED = 213 + MESSAGE_SIZE_TOO_LARGE = 214 + OPERATION_REPEATED_IN_BUCKET_GROUPS = 215 + BUCKET_CAPACITY_OVERFLOW = 216 + NODE_CAPACITY_NOT_SUFFICIENT_FOR_OPERATION = 217 + BUCKET_HAS_NO_THROTTLE_GROUPS = 218 + THROTTLE_GROUP_HAS_ZERO_OPS_PER_SEC = 219 + SUCCESS_BUT_MISSING_EXPECTED_OPERATION = 220 + UNPARSEABLE_THROTTLE_DEFINITIONS = 221 + INVALID_THROTTLE_DEFINITIONS = 222 + ACCOUNT_EXPIRED_AND_PENDING_REMOVAL = 223 + INVALID_TOKEN_MAX_SUPPLY = 224 + INVALID_TOKEN_NFT_SERIAL_NUMBER = 225 + INVALID_NFT_ID = 226 + METADATA_TOO_LONG = 227 + BATCH_SIZE_LIMIT_EXCEEDED = 228 + INVALID_QUERY_RANGE = 229 + FRACTION_DIVIDES_BY_ZERO = 230 + INSUFFICIENT_PAYER_BALANCE_FOR_CUSTOM_FEE = 231 + CUSTOM_FEES_LIST_TOO_LONG = 232 + INVALID_CUSTOM_FEE_COLLECTOR = 233 + INVALID_TOKEN_ID_IN_CUSTOM_FEES = 234 + TOKEN_NOT_ASSOCIATED_TO_FEE_COLLECTOR = 235 + TOKEN_MAX_SUPPLY_REACHED = 236 + SENDER_DOES_NOT_OWN_NFT_SERIAL_NO = 237 + CUSTOM_FEE_NOT_FULLY_SPECIFIED = 238 + CUSTOM_FEE_MUST_BE_POSITIVE = 239 + TOKEN_HAS_NO_FEE_SCHEDULE_KEY = 240 + CUSTOM_FEE_OUTSIDE_NUMERIC_RANGE = 241 + ROYALTY_FRACTION_CANNOT_EXCEED_ONE = 242 + FRACTIONAL_FEE_MAX_AMOUNT_LESS_THAN_MIN_AMOUNT = 243 + CUSTOM_SCHEDULE_ALREADY_HAS_NO_FEES = 244 + CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON = 245 + CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON = 246 + INVALID_CUSTOM_FEE_SCHEDULE_KEY = 247 + INVALID_TOKEN_MINT_METADATA = 248 + INVALID_TOKEN_BURN_METADATA = 249 + CURRENT_TREASURY_STILL_OWNS_NFTS = 250 + ACCOUNT_STILL_OWNS_NFTS = 251 + TREASURY_MUST_OWN_BURNED_NFT = 252 + ACCOUNT_DOES_NOT_OWN_WIPED_NFT = 253 + ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON = 254 + MAX_NFTS_IN_PRICE_REGIME_HAVE_BEEN_MINTED = 255 + PAYER_ACCOUNT_DELETED = 256 + CUSTOM_FEE_CHARGING_EXCEEDED_MAX_RECURSION_DEPTH = 257 + CUSTOM_FEE_CHARGING_EXCEEDED_MAX_ACCOUNT_AMOUNTS = 258 + INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE = 259 + SERIAL_NUMBER_LIMIT_REACHED = 260 + CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE = 261 + NO_REMAINING_AUTOMATIC_ASSOCIATIONS = 262 + EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT = 263 + REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT = 264 + TOKEN_IS_PAUSED = 265 + TOKEN_HAS_NO_PAUSE_KEY = 266 + INVALID_PAUSE_KEY = 267 + FREEZE_UPDATE_FILE_DOES_NOT_EXIST = 268 + FREEZE_UPDATE_FILE_HASH_DOES_NOT_MATCH = 269 + NO_UPGRADE_HAS_BEEN_PREPARED = 270 + NO_FREEZE_IS_SCHEDULED = 271 + UPDATE_FILE_HASH_CHANGED_SINCE_PREPARE_UPGRADE = 272 + FREEZE_START_TIME_MUST_BE_FUTURE = 273 + PREPARED_UPDATE_FILE_IS_IMMUTABLE = 274 + FREEZE_ALREADY_SCHEDULED = 275 + FREEZE_UPGRADE_IN_PROGRESS = 276 + UPDATE_FILE_ID_DOES_NOT_MATCH_PREPARED = 277 + UPDATE_FILE_HASH_DOES_NOT_MATCH_PREPARED = 278 + CONSENSUS_GAS_EXHAUSTED = 279 + REVERTED_SUCCESS = 280 + MAX_STORAGE_IN_PRICE_REGIME_HAS_BEEN_USED = 281 + INVALID_ALIAS_KEY = 282 + UNEXPECTED_TOKEN_DECIMALS = 283 + INVALID_PROXY_ACCOUNT_ID = 284 # [Deprecated] + INVALID_TRANSFER_ACCOUNT_ID = 285 + INVALID_FEE_COLLECTOR_ACCOUNT_ID = 286 + ALIAS_IS_IMMUTABLE = 287 + SPENDER_ACCOUNT_SAME_AS_OWNER = 288 + AMOUNT_EXCEEDS_TOKEN_MAX_SUPPLY = 289 + NEGATIVE_ALLOWANCE_AMOUNT = 290 + CANNOT_APPROVE_FOR_ALL_FUNGIBLE_COMMON = 291 # [Deprecated] + SPENDER_DOES_NOT_HAVE_ALLOWANCE = 292 + AMOUNT_EXCEEDS_ALLOWANCE = 293 + MAX_ALLOWANCES_EXCEEDED = 294 + EMPTY_ALLOWANCES = 295 + SPENDER_ACCOUNT_REPEATED_IN_ALLOWANCES = 296 # [Deprecated] + REPEATED_SERIAL_NUMS_IN_NFT_ALLOWANCES = 297 # [Deprecated] + FUNGIBLE_TOKEN_IN_NFT_ALLOWANCES = 298 + NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES = 299 + INVALID_ALLOWANCE_OWNER_ID = 300 + INVALID_ALLOWANCE_SPENDER_ID = 301 + REPEATED_ALLOWANCES_TO_DELETE = 302 # [Deprecated] + INVALID_DELEGATING_SPENDER = 303 + DELEGATING_SPENDER_CANNOT_GRANT_APPROVE_FOR_ALL = 304 + DELEGATING_SPENDER_DOES_NOT_HAVE_APPROVE_FOR_ALL = 305 + SCHEDULE_EXPIRATION_TIME_TOO_FAR_IN_FUTURE = 306 + SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME = 307 + SCHEDULE_FUTURE_THROTTLE_EXCEEDED = 308 + SCHEDULE_FUTURE_GAS_LIMIT_EXCEEDED = 309 + INVALID_ETHEREUM_TRANSACTION = 310 + WRONG_CHAIN_ID = 311 + WRONG_NONCE = 312 + ACCESS_LIST_UNSUPPORTED = 313 + SCHEDULE_PENDING_EXPIRATION = 314 + CONTRACT_IS_TOKEN_TREASURY = 315 + CONTRACT_HAS_NON_ZERO_TOKEN_BALANCES = 316 + CONTRACT_EXPIRED_AND_PENDING_REMOVAL = 317 + CONTRACT_HAS_NO_AUTO_RENEW_ACCOUNT = 318 + PERMANENT_REMOVAL_REQUIRES_SYSTEM_INITIATION = 319 + PROXY_ACCOUNT_ID_FIELD_IS_DEPRECATED = 320 + SELF_STAKING_IS_NOT_ALLOWED = 321 + INVALID_STAKING_ID = 322 + STAKING_NOT_ENABLED = 323 + INVALID_PRNG_RANGE = 324 + MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED = 325 + INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE = 326 + INSUFFICIENT_BALANCES_FOR_STORAGE_RENT = 327 + MAX_CHILD_RECORDS_EXCEEDED = 328 + INSUFFICIENT_BALANCES_FOR_RENEWAL_FEES = 329 _code_to_name = { 0: "OK", @@ -46,9 +311,277 @@ class ResponseCode: 19: "RECORD_NOT_FOUND", 20: "INVALID_SOLIDITY_ID", 21: "UNKNOWN", - 22: "SUCCESS" + 22: "SUCCESS", + 23: "FAIL_INVALID", + 24: "FAIL_FEE", + 25: "FAIL_BALANCE", + 26: "KEY_REQUIRED", + 27: "BAD_ENCODING", + 28: "INSUFFICIENT_ACCOUNT_BALANCE", + 29: "INVALID_SOLIDITY_ADDRESS", + 30: "INSUFFICIENT_GAS", + 31: "CONTRACT_SIZE_LIMIT_EXCEEDED", + 32: "LOCAL_CALL_MODIFICATION_EXCEPTION", + 33: "CONTRACT_REVERT_EXECUTED", + 34: "CONTRACT_EXECUTION_EXCEPTION", + 35: "INVALID_RECEIVING_NODE_ACCOUNT", + 36: "MISSING_QUERY_HEADER", + 37: "ACCOUNT_UPDATE_FAILED", + 38: "INVALID_KEY_ENCODING", + 39: "NULL_SOLIDITY_ADDRESS", + 40: "CONTRACT_UPDATE_FAILED", + 41: "INVALID_QUERY_HEADER", + 42: "INVALID_FEE_SUBMITTED", + 43: "INVALID_PAYER_SIGNATURE", + 44: "KEY_NOT_PROVIDED", + 45: "INVALID_EXPIRATION_TIME", + 46: "NO_WACL_KEY", + 47: "FILE_CONTENT_EMPTY", + 48: "INVALID_ACCOUNT_AMOUNTS", + 49: "EMPTY_TRANSACTION_BODY", + 50: "INVALID_TRANSACTION_BODY", + 51: "INVALID_SIGNATURE_TYPE_MISMATCHING_KEY", + 52: "INVALID_SIGNATURE_COUNT_MISMATCHING_KEY", + 53: "EMPTY_LIVE_HASH_BODY", + 54: "EMPTY_LIVE_HASH", + 55: "EMPTY_LIVE_HASH_KEYS", + 56: "INVALID_LIVE_HASH_SIZE", + 57: "EMPTY_QUERY_BODY", + 58: "EMPTY_LIVE_HASH_QUERY", + 59: "LIVE_HASH_NOT_FOUND", + 60: "ACCOUNT_ID_DOES_NOT_EXIST", + 61: "LIVE_HASH_ALREADY_EXISTS", + 62: "INVALID_FILE_WACL", + 63: "SERIALIZATION_FAILED", + 64: "TRANSACTION_OVERSIZE", + 65: "TRANSACTION_TOO_MANY_LAYERS", + 66: "CONTRACT_DELETED", + 67: "PLATFORM_NOT_ACTIVE", + 68: "KEY_PREFIX_MISMATCH", + 69: "PLATFORM_TRANSACTION_NOT_CREATED", + 70: "INVALID_RENEWAL_PERIOD", + 71: "INVALID_PAYER_ACCOUNT_ID", + 72: "ACCOUNT_DELETED", + 73: "FILE_DELETED", + 74: "ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS", + 75: "SETTING_NEGATIVE_ACCOUNT_BALANCE", + 76: "OBTAINER_REQUIRED", + 77: "OBTAINER_SAME_CONTRACT_ID", + 78: "OBTAINER_DOES_NOT_EXIST", + 79: "MODIFYING_IMMUTABLE_CONTRACT", + 80: "FILE_SYSTEM_EXCEPTION", + 81: "AUTORENEW_DURATION_NOT_IN_RANGE", + 82: "ERROR_DECODING_BYTESTRING", + 83: "CONTRACT_FILE_EMPTY", + 84: "CONTRACT_BYTECODE_EMPTY", + 85: "INVALID_INITIAL_BALANCE", + 86: "INVALID_RECEIVE_RECORD_THRESHOLD", # [Deprecated] + 87: "INVALID_SEND_RECORD_THRESHOLD", # [Deprecated] + 88: "ACCOUNT_IS_NOT_GENESIS_ACCOUNT", + 89: "PAYER_ACCOUNT_UNAUTHORIZED", + 90: "INVALID_FREEZE_TRANSACTION_BODY", + 91: "FREEZE_TRANSACTION_BODY_NOT_FOUND", + 92: "TRANSFER_LIST_SIZE_LIMIT_EXCEEDED", + 93: "RESULT_SIZE_LIMIT_EXCEEDED", + 94: "NOT_SPECIAL_ACCOUNT", + 95: "CONTRACT_NEGATIVE_GAS", + 96: "CONTRACT_NEGATIVE_VALUE", + 97: "INVALID_FEE_FILE", + 98: "INVALID_EXCHANGE_RATE_FILE", + 99: "INSUFFICIENT_LOCAL_CALL_GAS", + 100: "ENTITY_NOT_ALLOWED_TO_DELETE", + 101: "AUTHORIZATION_FAILED", + 102: "FILE_UPLOADED_PROTO_INVALID", + 103: "FILE_UPLOADED_PROTO_NOT_SAVED_TO_DISK", + 104: "FEE_SCHEDULE_FILE_PART_UPLOADED", + 105: "EXCHANGE_RATE_CHANGE_LIMIT_EXCEEDED", + 106: "MAX_CONTRACT_STORAGE_EXCEEDED", + 107: "TRANSFER_ACCOUNT_SAME_AS_DELETE_ACCOUNT", + 108: "TOTAL_LEDGER_BALANCE_INVALID", + 110: "EXPIRATION_REDUCTION_NOT_ALLOWED", + 111: "MAX_GAS_LIMIT_EXCEEDED", + 112: "MAX_FILE_SIZE_EXCEEDED", + 150: "INVALID_TOPIC_ID", + 155: "INVALID_ADMIN_KEY", + 156: "INVALID_SUBMIT_KEY", + 157: "UNAUTHORIZED", + 158: "INVALID_TOPIC_MESSAGE", + 159: "INVALID_AUTORENEW_ACCOUNT", + 160: "AUTORENEW_ACCOUNT_NOT_ALLOWED", + 162: "TOPIC_EXPIRED", + 163: "INVALID_CHUNK_NUMBER", + 164: "INVALID_CHUNK_TRANSACTION_ID", + 165: "ACCOUNT_FROZEN_FOR_TOKEN", + 166: "TOKENS_PER_ACCOUNT_LIMIT_EXCEEDED", + 167: "INVALID_TOKEN_ID", + 168: "INVALID_TOKEN_DECIMALS", + 169: "INVALID_TOKEN_INITIAL_SUPPLY", + 170: "INVALID_TREASURY_ACCOUNT_FOR_TOKEN", + 171: "INVALID_TOKEN_SYMBOL", + 172: "TOKEN_HAS_NO_FREEZE_KEY", + 173: "TRANSFERS_NOT_ZERO_SUM_FOR_TOKEN", + 174: "MISSING_TOKEN_SYMBOL", + 175: "TOKEN_SYMBOL_TOO_LONG", + 176: "ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN", + 177: "TOKEN_HAS_NO_KYC_KEY", + 178: "INSUFFICIENT_TOKEN_BALANCE", + 179: "TOKEN_WAS_DELETED", + 180: "TOKEN_HAS_NO_SUPPLY_KEY", + 181: "TOKEN_HAS_NO_WIPE_KEY", + 182: "INVALID_TOKEN_MINT_AMOUNT", + 183: "INVALID_TOKEN_BURN_AMOUNT", + 184: "TOKEN_NOT_ASSOCIATED_TO_ACCOUNT", + 185: "CANNOT_WIPE_TOKEN_TREASURY_ACCOUNT", + 186: "INVALID_KYC_KEY", + 187: "INVALID_WIPE_KEY", + 188: "INVALID_FREEZE_KEY", + 189: "INVALID_SUPPLY_KEY", + 190: "MISSING_TOKEN_NAME", + 191: "TOKEN_NAME_TOO_LONG", + 192: "INVALID_WIPING_AMOUNT", + 193: "TOKEN_IS_IMMUTABLE", + 194: "TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT", + 195: "TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES", + 196: "ACCOUNT_IS_TREASURY", + 197: "TOKEN_ID_REPEATED_IN_TOKEN_LIST", + 198: "TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED", + 199: "EMPTY_TOKEN_TRANSFER_BODY", + 200: "EMPTY_TOKEN_TRANSFER_ACCOUNT_AMOUNTS", + 201: "INVALID_SCHEDULE_ID", + 202: "SCHEDULE_IS_IMMUTABLE", + 203: "INVALID_SCHEDULE_PAYER_ID", + 204: "INVALID_SCHEDULE_ACCOUNT_ID", + 205: "NO_NEW_VALID_SIGNATURES", + 206: "UNRESOLVABLE_REQUIRED_SIGNERS", + 207: "SCHEDULED_TRANSACTION_NOT_IN_WHITELIST", + 208: "SOME_SIGNATURES_WERE_INVALID", + 209: "TRANSACTION_ID_FIELD_NOT_ALLOWED", + 210: "IDENTICAL_SCHEDULE_ALREADY_CREATED", + 211: "INVALID_ZERO_BYTE_IN_STRING", + 212: "SCHEDULE_ALREADY_DELETED", + 213: "SCHEDULE_ALREADY_EXECUTED", + 214: "MESSAGE_SIZE_TOO_LARGE", + 215: "OPERATION_REPEATED_IN_BUCKET_GROUPS", + 216: "BUCKET_CAPACITY_OVERFLOW", + 217: "NODE_CAPACITY_NOT_SUFFICIENT_FOR_OPERATION", + 218: "BUCKET_HAS_NO_THROTTLE_GROUPS", + 219: "THROTTLE_GROUP_HAS_ZERO_OPS_PER_SEC", + 220: "SUCCESS_BUT_MISSING_EXPECTED_OPERATION", + 221: "UNPARSEABLE_THROTTLE_DEFINITIONS", + 222: "INVALID_THROTTLE_DEFINITIONS", + 223: "ACCOUNT_EXPIRED_AND_PENDING_REMOVAL", + 224: "INVALID_TOKEN_MAX_SUPPLY", + 225: "INVALID_TOKEN_NFT_SERIAL_NUMBER", + 226: "INVALID_NFT_ID", + 227: "METADATA_TOO_LONG", + 228: "BATCH_SIZE_LIMIT_EXCEEDED", + 229: "INVALID_QUERY_RANGE", + 230: "FRACTION_DIVIDES_BY_ZERO", + 231: "INSUFFICIENT_PAYER_BALANCE_FOR_CUSTOM_FEE", + 232: "CUSTOM_FEES_LIST_TOO_LONG", + 233: "INVALID_CUSTOM_FEE_COLLECTOR", + 234: "INVALID_TOKEN_ID_IN_CUSTOM_FEES", + 235: "TOKEN_NOT_ASSOCIATED_TO_FEE_COLLECTOR", + 236: "TOKEN_MAX_SUPPLY_REACHED", + 237: "SENDER_DOES_NOT_OWN_NFT_SERIAL_NO", + 238: "CUSTOM_FEE_NOT_FULLY_SPECIFIED", + 239: "CUSTOM_FEE_MUST_BE_POSITIVE", + 240: "TOKEN_HAS_NO_FEE_SCHEDULE_KEY", + 241: "CUSTOM_FEE_OUTSIDE_NUMERIC_RANGE", + 242: "ROYALTY_FRACTION_CANNOT_EXCEED_ONE", + 243: "FRACTIONAL_FEE_MAX_AMOUNT_LESS_THAN_MIN_AMOUNT", + 244: "CUSTOM_SCHEDULE_ALREADY_HAS_NO_FEES", + 245: "CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON", + 246: "CUSTOM_FRACTIONAL_FEE_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON", + 247: "INVALID_CUSTOM_FEE_SCHEDULE_KEY", + 248: "INVALID_TOKEN_MINT_METADATA", + 249: "INVALID_TOKEN_BURN_METADATA", + 250: "CURRENT_TREASURY_STILL_OWNS_NFTS", + 251: "ACCOUNT_STILL_OWNS_NFTS", + 252: "TREASURY_MUST_OWN_BURNED_NFT", + 253: "ACCOUNT_DOES_NOT_OWN_WIPED_NFT", + 254: "ACCOUNT_AMOUNT_TRANSFERS_ONLY_ALLOWED_FOR_FUNGIBLE_COMMON", + 255: "MAX_NFTS_IN_PRICE_REGIME_HAVE_BEEN_MINTED", + 256: "PAYER_ACCOUNT_DELETED", + 257: "CUSTOM_FEE_CHARGING_EXCEEDED_MAX_RECURSION_DEPTH", + 258: "CUSTOM_FEE_CHARGING_EXCEEDED_MAX_ACCOUNT_AMOUNTS", + 259: "INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE", + 260: "SERIAL_NUMBER_LIMIT_REACHED", + 261: "CUSTOM_ROYALTY_FEE_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE", + 262: "NO_REMAINING_AUTOMATIC_ASSOCIATIONS", + 263: "EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT", + 264: "REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT", + 265: "TOKEN_IS_PAUSED", + 266: "TOKEN_HAS_NO_PAUSE_KEY", + 267: "INVALID_PAUSE_KEY", + 268: "FREEZE_UPDATE_FILE_DOES_NOT_EXIST", + 269: "FREEZE_UPDATE_FILE_HASH_DOES_NOT_MATCH", + 270: "NO_UPGRADE_HAS_BEEN_PREPARED", + 271: "NO_FREEZE_IS_SCHEDULED", + 272: "UPDATE_FILE_HASH_CHANGED_SINCE_PREPARE_UPGRADE", + 273: "FREEZE_START_TIME_MUST_BE_FUTURE", + 274: "PREPARED_UPDATE_FILE_IS_IMMUTABLE", + 275: "FREEZE_ALREADY_SCHEDULED", + 276: "FREEZE_UPGRADE_IN_PROGRESS", + 277: "UPDATE_FILE_ID_DOES_NOT_MATCH_PREPARED", + 278: "UPDATE_FILE_HASH_DOES_NOT_MATCH_PREPARED", + 279: "CONSENSUS_GAS_EXHAUSTED", + 280: "REVERTED_SUCCESS", + 281: "MAX_STORAGE_IN_PRICE_REGIME_HAS_BEEN_USED", + 282: "INVALID_ALIAS_KEY", + 283: "UNEXPECTED_TOKEN_DECIMALS", + 284: "INVALID_PROXY_ACCOUNT_ID", # [Deprecated] + 285: "INVALID_TRANSFER_ACCOUNT_ID", + 286: "INVALID_FEE_COLLECTOR_ACCOUNT_ID", + 287: "ALIAS_IS_IMMUTABLE", + 288: "SPENDER_ACCOUNT_SAME_AS_OWNER", + 289: "AMOUNT_EXCEEDS_TOKEN_MAX_SUPPLY", + 290: "NEGATIVE_ALLOWANCE_AMOUNT", + 291: "CANNOT_APPROVE_FOR_ALL_FUNGIBLE_COMMON", # [Deprecated] + 292: "SPENDER_DOES_NOT_HAVE_ALLOWANCE", + 293: "AMOUNT_EXCEEDS_ALLOWANCE", + 294: "MAX_ALLOWANCES_EXCEEDED", + 295: "EMPTY_ALLOWANCES", + 296: "SPENDER_ACCOUNT_REPEATED_IN_ALLOWANCES", # [Deprecated] + 297: "REPEATED_SERIAL_NUMS_IN_NFT_ALLOWANCES", # [Deprecated] + 298: "FUNGIBLE_TOKEN_IN_NFT_ALLOWANCES", + 299: "NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES", + 300: "INVALID_ALLOWANCE_OWNER_ID", + 301: "INVALID_ALLOWANCE_SPENDER_ID", + 302: "REPEATED_ALLOWANCES_TO_DELETE", # [Deprecated] + 303: "INVALID_DELEGATING_SPENDER", + 304: "DELEGATING_SPENDER_CANNOT_GRANT_APPROVE_FOR_ALL", + 305: "DELEGATING_SPENDER_DOES_NOT_HAVE_APPROVE_FOR_ALL", + 306: "SCHEDULE_EXPIRATION_TIME_TOO_FAR_IN_FUTURE", + 307: "SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME", + 308: "SCHEDULE_FUTURE_THROTTLE_EXCEEDED", + 309: "SCHEDULE_FUTURE_GAS_LIMIT_EXCEEDED", + 310: "INVALID_ETHEREUM_TRANSACTION", + 311: "WRONG_CHAIN_ID", + 312: "WRONG_NONCE", + 313: "ACCESS_LIST_UNSUPPORTED", + 314: "SCHEDULE_PENDING_EXPIRATION", + 315: "CONTRACT_IS_TOKEN_TREASURY", + 316: "CONTRACT_HAS_NON_ZERO_TOKEN_BALANCES", + 317: "CONTRACT_EXPIRED_AND_PENDING_REMOVAL", + 318: "CONTRACT_HAS_NO_AUTO_RENEW_ACCOUNT", + 319: "PERMANENT_REMOVAL_REQUIRES_SYSTEM_INITIATION", + 320: "PROXY_ACCOUNT_ID_FIELD_IS_DEPRECATED", + 321: "SELF_STAKING_IS_NOT_ALLOWED", + 322: "INVALID_STAKING_ID", + 323: "STAKING_NOT_ENABLED", + 324: "INVALID_PRNG_RANGE", + 325: "MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED", + 326: "INVALID_FULL_PREFIX_SIGNATURE_FOR_PRECOMPILE", + 327: "INSUFFICIENT_BALANCES_FOR_STORAGE_RENT", + 328: "MAX_CHILD_RECORDS_EXCEEDED", + 329: "INSUFFICIENT_BALANCES_FOR_RENEWAL_FEES", } @classmethod def get_name(cls, code): + """ + Return the name of the response code if available. + If not, return UNKNOWN_CODE_. + """ return cls._code_to_name.get(code, f"UNKNOWN_CODE_{code}") diff --git a/src/hedera_sdk_python/timestamp.py b/src/hedera_sdk_python/timestamp.py new file mode 100644 index 0000000..5432a90 --- /dev/null +++ b/src/hedera_sdk_python/timestamp.py @@ -0,0 +1,165 @@ +from datetime import datetime, timedelta +import random +import time + + +class Timestamp: + """ + Represents a specific moment in time with nanosecond precision. + """ + + MAX_NS = 1_000_000_000 + + def __init__(self, seconds: int, nanos: int): + """ + Initialize a new `Timestamp` instance. + + Args: + seconds (int): Number of seconds since epoch. + nanos (int): Number of nanoseconds past the last second. + """ + self.seconds = seconds + self.nanos = nanos + + @staticmethod + def generate(has_jitter=True) -> "Timestamp": + """ + Generate a `Timestamp` with optional jitter. + + Args: + has_jitter (bool): Whether to introduce random jitter. Default is True. + + Returns: + Timestamp: A new `Timestamp` instance. + """ + jitter = random.randint(3000, 8000) if has_jitter else 0 + now_ms = int(round(time.time() * 1000)) - jitter + seconds = now_ms // 1000 + nanos = (now_ms % 1000) * 1_000_000 + random.randint(0, 999_999) + + return Timestamp(seconds, nanos) + + @staticmethod + def from_date(date) -> "Timestamp": + """ + Create a `Timestamp` from a Python `datetime` object, timestamp, or string. + + Args: + date (datetime | int | str): A `datetime`, timestamp (int), or ISO 8601 string. + + Returns: + Timestamp: A `Timestamp` instance. + """ + if isinstance(date, datetime): + seconds = int(date.timestamp()) + nanos = int((date.timestamp() % 1) * Timestamp.MAX_NS) + elif isinstance(date, int): + seconds = date + nanos = 0 + elif isinstance(date, str): + parsed_date = datetime.fromisoformat(date) + seconds = int(parsed_date.timestamp()) + nanos = int((parsed_date.timestamp() % 1) * Timestamp.MAX_NS) + else: + raise ValueError("Invalid type for 'date'. Must be datetime, int, or str.") + + return Timestamp(seconds, nanos) + + def to_date(self) -> datetime: + """ + Convert the `Timestamp` to a Python `datetime` object. + + Returns: + datetime: A `datetime` instance. + """ + return datetime.fromtimestamp(self.seconds) + timedelta( + microseconds=self.nanos // 1000 + ) + + def plus_nanos(self, nanos: int) -> "Timestamp": + """ + Add nanoseconds to the current `Timestamp`. + + Args: + nanos (int): The number of nanoseconds to add. + + Returns: + Timestamp: A new `Timestamp` instance. + """ + total_nanos = self.nanos + nanos + new_seconds = self.seconds + total_nanos // Timestamp.MAX_NS + new_nanos = total_nanos % Timestamp.MAX_NS + + return Timestamp(new_seconds, new_nanos) + + def to_protobuf(self) -> dict: + """ + Convert the `Timestamp` to a protobuf-compatible dictionary. + + Returns: + dict: A dictionary representation of the `Timestamp`. + """ + return {"seconds": self.seconds, "nanos": self.nanos} + + @staticmethod + def from_protobuf(pb_obj) -> "Timestamp": + """ + Create a `Timestamp` from a protobuf object. + + Args: + pb_obj (dict): A protobuf-like dictionary with `seconds` and `nanos`. + + Returns: + Timestamp: A `Timestamp` instance. + """ + seconds = pb_obj.get("seconds", 0) + nanos = pb_obj.get("nanos", 0) + return Timestamp(seconds, nanos) + + def __str__(self) -> str: + """ + Get a string representation of the `Timestamp`. + + Returns: + str: The string representation in the format `seconds.nanos`. + """ + return f"{self.seconds}.{str(self.nanos).zfill(9)}" + + def compare(self, other: "Timestamp") -> int: + """ + Compare the current `Timestamp` with another. + + Args: + other (Timestamp): The `Timestamp` to compare with. + + Returns: + int: -1 if this `Timestamp` is earlier, 1 if later, 0 if equal. + """ + if self.seconds != other.seconds: + return -1 if self.seconds < other.seconds else 1 + if self.nanos != other.nanos: + return -1 if self.nanos < other.nanos else 1 + return 0 + + def __eq__(self, other: object) -> bool: + """ + Check equality with another object. + + Args: + other (object): The object to compare with. + + Returns: + bool: True if equal, False otherwise. + """ + if not isinstance(other, Timestamp): + return False + return self.seconds == other.seconds and self.nanos == other.nanos + + def __hash__(self) -> int: + """ + Get the hash value of the `Timestamp`. + + Returns: + int: The hash value. + """ + return hash((self.seconds, self.nanos)) diff --git a/uv.lock b/uv.lock index db93e76..b495081 100644 --- a/uv.lock +++ b/uv.lock @@ -331,7 +331,7 @@ wheels = [ [[package]] name = "hedera-sdk-python" -version = "0.1.dev84+ge8dc25f.d20241220" +version = "0.1.dev91+gd172e1a.d20241220" source = { editable = "." } dependencies = [ { name = "cryptography" },