Skip to content

Commit

Permalink
Merge pull request #38 from AlexanderShenshin/did-sdk-compatibility
Browse files Browse the repository at this point in the history
Updates for DID SDK compatibility
  • Loading branch information
nadineloepfe authored Jan 30, 2025
2 parents 92c12c0 + 2571ffb commit f7a2447
Show file tree
Hide file tree
Showing 16 changed files with 139 additions and 234 deletions.
18 changes: 0 additions & 18 deletions src/hedera_sdk_python/account/account_create_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,21 +171,3 @@ def _execute_transaction(self, client, transaction_proto):

receipt = self.get_receipt(client)
return receipt

def get_receipt(self, client, timeout=60):
"""
Retrieves the receipt for the transaction.
Args:
client (Client): The client instance to query.
timeout (int): Maximum time in seconds to wait for the receipt.
Returns:
TransactionReceipt: The transaction receipt from the network.
Raises:
Exception: If the transaction ID is not set or if receipt retrieval fails.
"""
if self.transaction_id is None:
raise Exception("Transaction ID is not set. Did you forget to sign or execute the transaction?")
return client.get_transaction_receipt(self.transaction_id, timeout)
28 changes: 12 additions & 16 deletions src/hedera_sdk_python/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,19 @@ def get_node_account_ids(self):

def get_transaction_receipt(self, transaction_id, max_attempts=10, sleep_seconds=2):
for attempt in range(max_attempts):
try:
receipt_query = TransactionGetReceiptQuery()
receipt_query.set_transaction_id(transaction_id)
receipt = receipt_query.execute(self)
status = receipt.status

if status == ResponseCode.SUCCESS:
return receipt
elif status in (ResponseCode.UNKNOWN, ResponseCode.RECEIPT_NOT_FOUND):
time.sleep(sleep_seconds)
continue
else:
status_message = ResponseCode.get_name(status)
raise Exception(f"Transaction failed with status: {status_message}")
except Exception as e:
print(f"Error retrieving transaction receipt: {e}")
receipt_query = TransactionGetReceiptQuery()
receipt_query.set_transaction_id(transaction_id)
receipt = receipt_query.execute(self)
status = receipt.status

if status == ResponseCode.SUCCESS:
return receipt
elif status in (ResponseCode.UNKNOWN, ResponseCode.BUSY, ResponseCode.RECEIPT_NOT_FOUND, ResponseCode.RECORD_NOT_FOUND, ResponseCode.PLATFORM_NOT_ACTIVE):
time.sleep(sleep_seconds)
continue
else:
status_message = ResponseCode.get_name(status)
raise Exception(f"Error retrieving transaction receipt: {status_message}")
raise Exception("Exceeded maximum attempts to fetch transaction receipt.")

def send_query(self, query, node_account_id, timeout=60):
Expand Down
20 changes: 0 additions & 20 deletions src/hedera_sdk_python/consensus/topic_create_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,3 @@ def _execute_transaction(self, client, transaction_proto):

receipt = self.get_receipt(client)
return receipt

def get_receipt(self, client, timeout=60):
"""
Retrieves the receipt for the transaction.
Args:
client (Client): The client instance.
timeout (int): Maximum time in seconds to wait for the receipt.
Returns:
TransactionReceipt: The transaction receipt from the network.
Raises:
Exception: If the transaction ID is not set or if receipt retrieval fails.
"""
if self.transaction_id is None:
raise Exception("Transaction ID is not set.")

receipt = client.get_transaction_receipt(self.transaction_id, timeout)
return receipt
20 changes: 0 additions & 20 deletions src/hedera_sdk_python/consensus/topic_delete_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,4 @@ def _execute_transaction(self, client, transaction_proto):
raise Exception(f"Error during transaction submission: {error_code} ({error_message})")

receipt = self.get_receipt(client)
return receipt

def get_receipt(self, client, timeout=60):
"""
Retrieves the receipt for the transaction.
Args:
client (Client): The client instance.
timeout (int): Maximum time in seconds to wait for the receipt.
Returns:
TransactionReceipt: The transaction receipt from the network.
Raises:
Exception: If the transaction ID is not set or if receipt retrieval fails.
"""
if self.transaction_id is None:
raise Exception("Transaction ID is not set.")

receipt = client.get_transaction_receipt(self.transaction_id, timeout)
return receipt
Original file line number Diff line number Diff line change
Expand Up @@ -85,23 +85,3 @@ def _execute_transaction(self, client, transaction_proto):

receipt = self.get_receipt(client)
return receipt

def get_receipt(self, client, timeout=60):
"""
Retrieves the receipt for the transaction.
Args:
client (Client): The client instance.
timeout (int): Maximum time in seconds to wait for the receipt.
Returns:
TransactionReceipt: The transaction receipt from the network.
Raises:
Exception: If the transaction ID is not set or if receipt retrieval fails.
"""
if self.transaction_id is None:
raise Exception("Transaction ID is not set.")

receipt = client.get_transaction_receipt(self.transaction_id, timeout)
return receipt
22 changes: 1 addition & 21 deletions src/hedera_sdk_python/consensus/topic_update_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(
):
super().__init__()
self.topic_id = topic_id
self.memo = memo
self.memo = memo or ""
self.admin_key = admin_key
self.submit_key = submit_key
self.auto_renew_period = auto_renew_period
Expand Down Expand Up @@ -171,23 +171,3 @@ def _execute_transaction(self, client, transaction_proto):

receipt = self.get_receipt(client)
return receipt

def get_receipt(self, client, timeout=60):
"""
Retrieves the receipt for the transaction.
Args:
client (Client): The client instance.
timeout (int): Maximum time in seconds to wait for the receipt.
Returns:
TransactionReceipt: The transaction receipt from the network.
Raises:
Exception: If the transaction ID is not set or if receipt retrieval fails.
"""
if self.transaction_id is None:
raise Exception("Transaction ID is not set.")

receipt = client.get_transaction_receipt(self.transaction_id, timeout)
return receipt
38 changes: 28 additions & 10 deletions src/hedera_sdk_python/crypto/private_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class PrivateKey:
Represents a private key that can be either Ed25519 or ECDSA (secp256k1).
"""

def __init__(self, private_key):
def __init__(self, private_key: ec.EllipticCurvePrivateKey | ed25519.Ed25519PrivateKey):
"""
Initializes a PrivateKey from a cryptography PrivateKey object.
"""
Expand Down Expand Up @@ -56,26 +56,21 @@ def generate_ecdsa(cls):
return cls(private_key)

@classmethod
def from_string(cls, key_str):
def from_bytes(cls, key_bytes: bytes):
"""
Load a private key from a hex-encoded string. For Ed25519, expects 32 bytes.
Load a private key from bytes. For Ed25519, expects 32 bytes.
For ECDSA (secp256k1), also expects 32 bytes (raw scalar).
If it's DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
If the key is DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
Args:
key_str (str): The hex-encoded private key string.
key_bytes (bytes): Private key bytes.
Returns:
PrivateKey: A new instance of PrivateKey.
Raises:
ValueError: If the key is invalid or unsupported.
"""
try:
key_bytes = bytes.fromhex(key_str)
except ValueError:
raise ValueError("Invalid hex-encoded private key string.")

if len(key_bytes) == 32:
try:
ed_priv = ed25519.Ed25519PrivateKey.from_private_bytes(key_bytes)
Expand Down Expand Up @@ -104,6 +99,29 @@ def from_string(cls, key_str):

raise ValueError("Unsupported private key type.")

@classmethod
def from_string(cls, key_str):
"""
Load a private key from a hex-encoded string. For Ed25519, expects 32 bytes.
For ECDSA (secp256k1), also expects 32 bytes (raw scalar).
If the key is DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
Args:
key_str (str): The hex-encoded private key string.
Returns:
PrivateKey: A new instance of PrivateKey.
Raises:
ValueError: If the key is invalid or unsupported.
"""
try:
key_bytes = bytes.fromhex(key_str.removeprefix("0x"))
except ValueError:
raise ValueError("Invalid hex-encoded private key string.")

return cls.from_bytes(key_bytes)

def sign(self, data: bytes) -> bytes:
"""
Signs the given data using this private key (Ed25519 or ECDSA).
Expand Down
66 changes: 65 additions & 1 deletion src/hedera_sdk_python/crypto/public_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,76 @@ class PublicKey:
Represents a public key that can be either Ed25519 or ECDSA (secp256k1).
"""

def __init__(self, public_key):
def __init__(self, public_key: ec.EllipticCurvePublicKey | ed25519.Ed25519PublicKey):
"""
Initializes a PublicKey from a cryptography PublicKey object.
"""
self._public_key = public_key

@classmethod
def from_bytes(cls, key_bytes: bytes):
"""
Load a public key from bytes.
For Ed25519, expects 32 bytes. Raw bytes for ECDSA are not supported for now.
If the key is DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
Args:
key_bytes (bytes): Public key bytes.
Returns:
PublicKey: A new instance of PublicKey.
Raises:
ValueError: If the key is invalid or unsupported.
"""

if len(key_bytes) == 32:
ed_public = ed25519.Ed25519PublicKey.from_public_bytes(key_bytes)
return cls(ed_public)
# TODO: Consider adding support for creating ECDSA public key instance from raw encoded point
# Java SDK example: https://github.com/hiero-ledger/hiero-sdk-java/blob/main/sdk-java/src/main/java/org/hiero/sdk/java/PublicKeyECDSA.java#L46
#
# Potential approach for Python:
# ec_public = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), key_bytes)

try:
public_key = serialization.load_der_public_key(key_bytes)
except Exception as e:
raise ValueError(f"Failed to load public key (DER): {e}")

if isinstance(public_key, ed25519.Ed25519PublicKey):
return cls(public_key)

if isinstance(public_key, ec.EllipticCurvePublicKey):
if not isinstance(public_key.curve, ec.SECP256K1):
raise ValueError("Only secp256k1 ECDSA is supported.")
return cls(public_key)

raise ValueError("Unsupported public key type.")

@classmethod
def from_string(cls, key_str):
"""
Load a public key from a hex-encoded string.
For Ed25519, expects 32 bytes. Raw bytes string for ECDSA is not supported for now.
If the key is DER-encoded, tries to parse and detect Ed25519 vs ECDSA.
Args:
key_str (str): The hex-encoded public key string.
Returns:
PublicKey: A new instance of PublicKey.
Raises:
ValueError: If the key is invalid or unsupported.
"""
try:
key_bytes = bytes.fromhex(key_str.removeprefix("0x"))
except ValueError:
raise ValueError("Invalid hex-encoded public key string.")

return cls.from_bytes(key_bytes)

def verify(self, signature: bytes, data: bytes) -> None:
"""
Verifies a signature for the given data using this public key.
Expand Down
23 changes: 12 additions & 11 deletions src/hedera_sdk_python/timestamp.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import random
import time

from hedera_sdk_python.hapi.services.timestamp_pb2 import Timestamp as TimestampProto


class Timestamp:
"""
Expand Down Expand Up @@ -72,7 +74,7 @@ def to_date(self) -> datetime:
Returns:
datetime: A `datetime` instance.
"""
return datetime.fromtimestamp(self.seconds) + timedelta(
return datetime.fromtimestamp(self.seconds, tz=timezone.utc) + timedelta(
microseconds=self.nanos // 1000
)

Expand All @@ -92,29 +94,28 @@ def plus_nanos(self, nanos: int) -> "Timestamp":

return Timestamp(new_seconds, new_nanos)

def to_protobuf(self) -> dict:
def to_protobuf(self) -> TimestampProto:
"""
Convert the `Timestamp` to a protobuf-compatible dictionary.
Convert the `Timestamp` to corresponding protobuf object.
Returns:
dict: A dictionary representation of the `Timestamp`.
dict: A protobuf representation of the `Timestamp`.
"""
return {"seconds": self.seconds, "nanos": self.nanos}
return TimestampProto(seconds=self.seconds, nanos=self.nanos)

@staticmethod
def from_protobuf(pb_obj) -> "Timestamp":
def from_protobuf(pb_obj: TimestampProto) -> "Timestamp":
"""
Create a `Timestamp` from a protobuf object.
Args:
pb_obj (dict): A protobuf-like dictionary with `seconds` and `nanos`.
pb_obj (timestamp_pb2.Timestamp): A protobuf Timestamp object.
Returns:
Timestamp: A `Timestamp` instance.
"""
seconds = pb_obj.get("seconds", 0)
nanos = pb_obj.get("nanos", 0)
return Timestamp(seconds, nanos)

return Timestamp(pb_obj.seconds, pb_obj.nanos)

def __str__(self) -> str:
"""
Expand Down
20 changes: 0 additions & 20 deletions src/hedera_sdk_python/tokens/token_associate_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,23 +83,3 @@ def _execute_transaction(self, client, transaction_proto):

receipt = self.get_receipt(client)
return receipt

def get_receipt(self, client, timeout=60):
"""
Retrieves the receipt for the transaction.
Args:
client (Client): The client instance.
timeout (int): Maximum time in seconds to wait for the receipt.
Returns:
TransactionReceipt: The transaction receipt from the network.
Raises:
Exception: If the transaction ID is not set or if receipt retrieval fails.
"""
if self.transaction_id is None:
raise Exception("Transaction ID is not set.")

receipt = client.get_transaction_receipt(self.transaction_id, timeout)
return receipt
Loading

0 comments on commit f7a2447

Please sign in to comment.