Skip to content

Commit

Permalink
Migrate to native Hedera Python SDK (#2)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Shenshin <[email protected]>
  • Loading branch information
AlexanderShenshin authored Feb 5, 2025
1 parent c61dd42 commit 85a9260
Show file tree
Hide file tree
Showing 50 changed files with 804 additions and 966 deletions.
6 changes: 0 additions & 6 deletions .github/actions/setup-poetry-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ runs:
with:
python-version: ${{ inputs.python-version }}

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "21"

- name: Install Poetry
env:
POETRY_VERSION: "1.8.4"
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ test:
.PHONY: test-unit
test-unit:
@echo "🚀 Testing code: Running pytest (unit tests only)"
@HEDERA_DID_SDK_LOG_LEVEL="DEBUG" HEDERA_DID_SDK_LOG_FORMAT="%d %level: %logger: %msg%n" poetry run pytest -s --verbose ./tests/unit
@HEDERA_DID_SDK_LOG_LEVEL="DEBUG" HEDERA_DID_SDK_LOG_FORMAT="%(asctime)s %(levelname)s: %(filename)s: %(message)s" poetry run pytest -s --verbose ./tests/unit

.PHONY: test-integration
test-integration:
@echo "🚀 Testing code: Running pytest (integration tests only)"
@HEDERA_DID_SDK_LOG_LEVEL="DEBUG" HEDERA_DID_SDK_LOG_FORMAT="%d %level: %logger: %msg%n" poetry run pytest -s --verbose ./tests/integration
@HEDERA_DID_SDK_LOG_LEVEL="DEBUG" HEDERA_DID_SDK_LOG_FORMAT="%(asctime)s %(levelname)s: %(filename)s: %(message)s" poetry run pytest -s --verbose ./tests/integration

.PHONY: build
build: clean ## Build wheel file using poetry
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ Documentation:
- Python 3.12+
- [Poetry](https://python-poetry.org/) (at least 1.8.4)
- NodeJS and npm (used by pre-commit hooks)
- JDK 21 (required for Hedera Python SDK which is a wrapper around Java SDK)
- The Temurin builds of [Eclipse Adoptium](https://adoptium.net/) are strongly recommended
- Tools for Makefile support (Windows only)
- Can be installed with [chocolatey](https://chocolatey.org/): `choco install make`

Expand Down
5 changes: 0 additions & 5 deletions did_sdk_py/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
RevRegDefValue,
)
from .did import DidDocument, DidErrorCode, DidException, HederaDid, HederaDidResolver
from .hedera_client_provider import HederaClientProvider, NetworkConfig, NetworkName, OperatorConfig
from .utils.cache import Cache, MemoryCache
from .utils.logger import LogLevel, configure_logger

Expand All @@ -41,10 +40,6 @@
"AnonCredsRevRegDef",
"RevRegDefValue",
"AnonCredsRevList",
"HederaClientProvider",
"OperatorConfig",
"NetworkName",
"NetworkConfig",
"Cache",
"MemoryCache",
]
30 changes: 13 additions & 17 deletions did_sdk_py/anoncreds/hedera_anoncreds_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from itertools import chain
from typing import cast

from hedera import PrivateKey, TopicMessageSubmitTransaction, Transaction
from hedera_sdk_python import Client, PrivateKey, Timestamp, TopicMessageSubmitTransaction
from hedera_sdk_python.transaction.transaction import Transaction

from ..hcs import (
HcsFileService,
Expand All @@ -14,9 +15,7 @@
HcsTopicService,
)
from ..hcs.constants import MAX_TRANSACTION_FEE
from ..hedera_client_provider import HederaClientProvider
from ..utils.cache import Cache, MemoryCache
from ..utils.timestamp import Timestamp
from .models import (
AnonCredsCredDef,
AnonCredsRevList,
Expand Down Expand Up @@ -50,18 +49,18 @@ class HederaAnonCredsRegistry:
"""Anoncreds objects registry (resolver + registrar) implementation that leverage Hedera HCS as VDR.
Args:
client_provider: Hedera Client provider
client: Hedera Client
cache_instance: Custom cache instance. If not provided, in-memory cache is used
"""

def __init__(
self,
client_provider: HederaClientProvider,
client: Client,
cache_instance: Cache[str, object] | None = None,
):
self._client = client_provider.get_client()
self._hcs_file_service = HcsFileService(client_provider)
self._hcs_topic_service = HcsTopicService(client_provider)
self._client = client
self._hcs_file_service = HcsFileService(client)
self._hcs_topic_service = HcsTopicService(client)

cache_instance = cache_instance or MemoryCache[str, object]()

Expand Down Expand Up @@ -331,9 +330,9 @@ async def register_rev_reg_def(
object: Revocation registry definition registration result
"""
try:
issuer_key = PrivateKey.fromString(issuer_key_der)
issuer_key = PrivateKey.from_string(issuer_key_der)

entries_topic_options = HcsTopicOptions(submit_key=issuer_key.getPublicKey())
entries_topic_options = HcsTopicOptions(submit_key=issuer_key.public_key())
entries_topic_id = await self._hcs_topic_service.create_topic(entries_topic_options, [issuer_key])

rev_reg_def_with_metadata = RevRegDefWithHcsMetadata(
Expand Down Expand Up @@ -471,7 +470,7 @@ async def get_rev_list(self, rev_reg_id: str, timestamp: int) -> GetRevListResul
# If returned entries list is empty, we need to fetch the first message and check if list is registered
# It's possible that requested timestamp is before the actual registration of rev list -> we want to return initial state for the list (by adding first message to entries)

# The second request looks redundant here, but it should be the rare case that will e subsequently handled by cache
# The second request looks redundant here, but it should be the rare case that will be subsequently handled by cache
entries_messages = await HcsMessageResolver(
topic_id=entries_topic_id,
message_type=HcsRevRegEntryMessage,
Expand Down Expand Up @@ -507,7 +506,7 @@ async def get_rev_list(self, rev_reg_id: str, timestamp: int) -> GetRevListResul
revocation_registry_id=rev_reg_id,
resolution_metadata={
"error": "otherError",
"message": f"unable to resolve revocation list: ${error!s}",
"message": f"Unable to resolve revocation list: ${error!s}",
},
revocation_list_metadata={},
)
Expand Down Expand Up @@ -613,11 +612,8 @@ async def _submit_rev_list_entry(
def build_message_submit_transaction(
message_submit_transaction: TopicMessageSubmitTransaction,
) -> Transaction:
return (
message_submit_transaction.setMaxTransactionFee(MAX_TRANSACTION_FEE)
.freezeWith(self._client)
.sign(PrivateKey.fromString(issuer_key_der))
)
message_submit_transaction.transaction_fee = MAX_TRANSACTION_FEE.to_tinybars() # pyright: ignore [reportAttributeAccessIssue]
return message_submit_transaction.freeze_with(self._client).sign(PrivateKey.from_string(issuer_key_der))

await HcsMessageTransaction(entries_topic_id, entry_message, build_message_submit_transaction).execute(
self._client
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import ClassVar

from hedera import PublicKey
from hedera_sdk_python import PublicKey

from .....did.types import SupportedKeyType
from .....utils.encoding import b58_to_bytes, bytes_to_b58
Expand All @@ -27,15 +27,15 @@ def get_owner_def(self):
"id": self.id_,
"type": self.type_,
"controller": self.controller,
"publicKeyBase58": bytes_to_b58(bytes(self.public_key.toBytesRaw())),
"publicKeyBase58": bytes_to_b58(self.public_key.to_bytes_raw()),
}

@classmethod
def from_json_payload(cls, payload: dict):
event_json = payload[cls.event_target]
match event_json:
case {"id": id_, "type": type_, "controller": controller, "publicKeyBase58": public_key_base58}:
public_key = PublicKey.fromBytes(b58_to_bytes(public_key_base58))
public_key = PublicKey.from_bytes(b58_to_bytes(public_key_base58))
return cls(id_=id_, type_=type_, controller=controller, public_key=public_key)
case _:
raise Exception(f"{cls.__name__} JSON parsing failed: Invalid JSON structure")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import ClassVar

from hedera import PublicKey
from hedera_sdk_python import PublicKey

from .....utils.encoding import b58_to_bytes, bytes_to_b58
from ....types import SupportedKeyType
Expand All @@ -27,15 +27,15 @@ def get_verification_method_def(self):
"id": self.id_,
"type": self.type_,
"controller": self.controller,
"publicKeyBase58": bytes_to_b58(bytes(self.public_key.toBytesRaw())),
"publicKeyBase58": bytes_to_b58(self.public_key.to_bytes_raw()),
}

@classmethod
def from_json_payload(cls, payload: dict):
event_json = payload[cls.event_target]
match event_json:
case {"id": id_, "type": type_, "controller": controller, "publicKeyBase58": public_key_base58}:
public_key = PublicKey.fromBytes(b58_to_bytes(public_key_base58))
public_key = PublicKey.from_bytes(b58_to_bytes(public_key_base58))
return cls(id_=id_, type_=type_, controller=controller, public_key=public_key)
case _:
raise Exception(f"{cls.__name__} JSON parsing failed: Invalid JSON structure")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import ClassVar

from hedera import PublicKey
from hedera_sdk_python import PublicKey

from .....utils.encoding import b58_to_bytes, bytes_to_b58
from ....types import SupportedKeyType, VerificationRelationshipType
Expand All @@ -28,7 +28,7 @@ def get_verification_method_def(self):
"id": self.id_,
"type": self.type_,
"controller": self.controller,
"publicKeyBase58": bytes_to_b58(bytes(self.public_key.toBytesRaw())),
"publicKeyBase58": bytes_to_b58(self.public_key.to_bytes_raw()),
}

@classmethod
Expand All @@ -42,7 +42,7 @@ def from_json_payload(cls, payload: dict):
"publicKeyBase58": public_key_base58,
"relationshipType": relationship_type,
}:
public_key = PublicKey.fromBytes(b58_to_bytes(public_key_base58))
public_key = PublicKey.from_bytes(b58_to_bytes(public_key_base58))
return cls(
id_=id_,
type_=type_,
Expand Down
52 changes: 21 additions & 31 deletions did_sdk_py/did/hedera_did.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import logging
from typing import Literal, cast

from hedera import (
PrivateKey,
PublicKey,
TopicMessageSubmitTransaction,
Transaction,
)
from hedera_sdk_python import Client, PrivateKey, PublicKey, TopicMessageSubmitTransaction
from hedera_sdk_python.transaction.transaction import Transaction

from ..hcs import HcsMessageResolver, HcsMessageTransaction, HcsTopicOptions, HcsTopicService
from ..hcs.constants import MAX_TRANSACTION_FEE
from ..hedera_client_provider import HederaClientProvider
from ..utils.encoding import multibase_encode
from ..utils.keys import get_key_type
from .did_document import DidDocument
Expand Down Expand Up @@ -42,21 +37,19 @@ class HederaDid:
Class representing Hedera DID instance, provides access to DID management API.
Args:
client_provider: Hedera Client provider
client: Hedera Client
identifier: DID identifier (for existing DIDs)
private_key_der: DID Owner (controller) private key encoded in DER format. Can be empty for read-only access
"""

def __init__(
self, client_provider: HederaClientProvider, identifier: str | None = None, private_key_der: str | None = None
):
def __init__(self, client: Client, identifier: str | None = None, private_key_der: str | None = None):
if not identifier and not private_key_der:
raise DidException("'identifier' and 'private_key_der' cannot both be empty")

self._client = client_provider.get_client()
self._hcs_topic_service = HcsTopicService(client_provider)
self._client = client
self._hcs_topic_service = HcsTopicService(client)

self._private_key = PrivateKey.fromString(private_key_der) if private_key_der else None
self._private_key = PrivateKey.from_string(private_key_der) if private_key_der else None
self._key_type: SupportedKeyType | None = (
cast(SupportedKeyType, get_key_type(self._private_key)) if self._private_key else None
)
Expand All @@ -83,22 +76,22 @@ async def register(self):
raise DidException("DID is already registered")
else:
topic_options = HcsTopicOptions(
admin_key=self._private_key.getPublicKey(), submit_key=self._private_key.getPublicKey()
admin_key=self._private_key.public_key(), submit_key=self._private_key.public_key()
)

self.topic_id = await self._hcs_topic_service.create_topic(topic_options, [self._private_key])

self.network = self._client.ledgerId.toString()
self.network = self._client.network.network
self.identifier = build_identifier(
self.network,
multibase_encode(bytes(self._private_key.getPublicKey().toBytesRaw()), "base58btc"),
multibase_encode(bytes(self._private_key.public_key().to_bytes_raw()), "base58btc"),
self.topic_id,
)

hcs_event = HcsDidUpdateDidOwnerEvent(
id_=f"{self.identifier}#did-root-key",
controller=self.identifier,
public_key=self._private_key.getPublicKey(),
public_key=self._private_key.public_key(),
type_=self._key_type,
)

Expand All @@ -118,14 +111,14 @@ async def change_owner(self, controller: str, new_private_key_der: str):
if not document.controller:
raise DidException("DID is not registered or was recently deleted. DID has to be registered first")

new_private_key = PrivateKey.fromString(new_private_key_der)
new_private_key = PrivateKey.from_string(new_private_key_der)
new_key_type = get_key_type(new_private_key)

topic_update_options = HcsTopicOptions(
admin_key=new_private_key.getPublicKey(), submit_key=new_private_key.getPublicKey()
admin_key=new_private_key.public_key(), submit_key=new_private_key.public_key()
)
await self._hcs_topic_service.update_topic(
cast(str, self.topic_id), topic_update_options, [self._private_key, new_private_key]
cast(str, self.topic_id), topic_update_options, [cast(PrivateKey, self._private_key), new_private_key]
)

self._private_key = new_private_key
Expand All @@ -134,7 +127,7 @@ async def change_owner(self, controller: str, new_private_key_der: str):
hcs_event = HcsDidUpdateDidOwnerEvent(
id_=f"{self.identifier}#did-root-key",
controller=controller,
public_key=self._private_key.getPublicKey(),
public_key=self._private_key.public_key(),
type_=self._key_type,
)

Expand Down Expand Up @@ -215,7 +208,7 @@ async def add_verification_method(
DidDocumentOperation.CREATE,
id_=id_,
controller=controller,
public_key=PublicKey.fromString(public_key_der),
public_key=PublicKey.from_string(public_key_der),
type_=type_,
)

Expand All @@ -238,7 +231,7 @@ async def update_verification_method(
DidDocumentOperation.UPDATE,
id_=id_,
controller=controller,
public_key=PublicKey.fromString(public_key_der),
public_key=PublicKey.from_string(public_key_der),
type_=type_,
)

Expand Down Expand Up @@ -274,7 +267,7 @@ async def add_verification_relationship(
DidDocumentOperation.CREATE,
id_=id_,
controller=controller,
public_key=PublicKey.fromString(public_key_der),
public_key=PublicKey.from_string(public_key_der),
relationship_type=relationship_type,
type_=type_,
)
Expand All @@ -299,7 +292,7 @@ async def update_verification_relationship(
await self._add_or_update_verification_relationship(
DidDocumentOperation.UPDATE,
id_=id_,
public_key=PublicKey.fromString(public_key_der),
public_key=PublicKey.from_string(public_key_der),
controller=controller,
relationship_type=relationship_type,
type_=type_,
Expand All @@ -326,11 +319,8 @@ async def _submit_transaction(self, operation: DidDocumentOperation, event: HcsD
envelope.sign(self._private_key)

def build_did_transaction(message_submit_transaction: TopicMessageSubmitTransaction) -> Transaction:
return (
message_submit_transaction.setMaxTransactionFee(MAX_TRANSACTION_FEE)
.freezeWith(self._client)
.sign(self._private_key)
)
message_submit_transaction.transaction_fee = MAX_TRANSACTION_FEE.to_tinybars() # pyright: ignore [reportAttributeAccessIssue]
return message_submit_transaction.freeze_with(self._client).sign(self._private_key)

await HcsMessageTransaction(self.topic_id, envelope, build_did_transaction).execute(self._client)

Expand Down
Loading

0 comments on commit 85a9260

Please sign in to comment.