Skip to content

Commit cbc8575

Browse files
authored
✨ Create Cheqd DIDs (#1548)
* ⬆️ Upgrade CloudController * ⬆️ Update lock file * 🎨 Deduplicate supported methods * ✨ Allow creation of cheqd did's * 🐛 Fix reading options from DIDCreate * ⬆️ Update lock file * 🎨 Use correct model for acapy call * 🧪 Fix test_create_did * ✅ Test coverage for creating a cheqd did * 📝 Update OpenAPI Specs * 🎨
1 parent 6428cde commit cbc8575

File tree

11 files changed

+255
-107
lines changed

11 files changed

+255
-107
lines changed

app/models/wallet.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,15 @@ class DIDCreate(BaseModel):
3636
# Downstream processes should use the `to_acapy_options` method to convert the model's fields
3737
# into the `DIDCreateOptions` structure expected by ACA-Py.
3838

39+
_supported_methods = ["cheqd", "sov", "key", "web", "did:peer:2", "did:peer:4"]
40+
3941
method: Optional[StrictStr] = Field(
4042
default="sov",
4143
description=(
42-
"Method for the requested DID. Supported methods are 'sov', 'key', 'web', 'did:peer:2', or 'did:peer:4'."
44+
"Method for the requested DID. Supported methods are "
45+
f"{', '.join(_supported_methods)}."
4346
),
44-
examples=["sov", "key", "web", "did:peer:2", "did:peer:4"],
47+
examples=_supported_methods,
4548
)
4649
seed: Optional[StrictStr] = Field(
4750
default=None,

app/routes/wallet/dids.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,10 @@ async def create_did(
4949
if not did_create:
5050
did_create = DIDCreate()
5151

52-
# Convert our custom DIDCreate model to ACA-Py's DIDCreate model
53-
acapy_did_create_request = did_create.to_acapy_request()
54-
5552
async with client_from_auth(auth) as aries_controller:
56-
logger.debug("Creating DID with request: {}", acapy_did_create_request)
53+
logger.debug("Creating DID with request: {}", did_create)
5754
result = await acapy_wallet.create_did(
58-
controller=aries_controller, did_create=acapy_did_create_request
55+
controller=aries_controller, did_create=did_create
5956
)
6057

6158
logger.debug("Successfully created DID.")

app/services/acapy_wallet.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from typing import Optional
22

3-
from aries_cloudcontroller import DID, AcaPyClient, DIDCreate
3+
from aries_cloudcontroller import DID, AcaPyClient, CreateCheqdDIDRequest
44

55
from app.exceptions import CloudApiException, handle_acapy_call
6+
from app.models.wallet import DIDCreate
67
from app.util.did import qualified_did_sov
78
from shared.log_config import get_logger
89

@@ -51,16 +52,52 @@ async def create_did(
5152
if did_create is None:
5253
did_create = DIDCreate()
5354

54-
did_response = await handle_acapy_call(
55-
logger=logger, acapy_call=controller.wallet.create_did, body=did_create
56-
)
57-
58-
result = did_response.result
59-
if not result or not result.did or not result.verkey:
60-
logger.error("Failed to create DID: `{}`.", did_response)
61-
raise CloudApiException("Error creating did.")
62-
63-
logger.debug("Successfully created local DID.")
55+
did_method = did_create.method
56+
57+
if did_method == "cheqd":
58+
create_cheqd_did_options = did_create.to_acapy_options().to_dict()
59+
if did_create.seed:
60+
create_cheqd_did_options["seed"] = did_create.seed
61+
# Notes:
62+
# - supported options: seed, network, verification_method
63+
# - key_type option is not implemented (default is ed25519)
64+
65+
request = CreateCheqdDIDRequest(options=create_cheqd_did_options)
66+
logger.debug("Creating cheqd DID: `{}`", request)
67+
cheqd_did_response = await handle_acapy_call(
68+
logger=logger,
69+
acapy_call=controller.did.did_cheqd_create_post,
70+
body=request,
71+
)
72+
verkey = cheqd_did_response.verkey
73+
did = cheqd_did_response.did
74+
75+
# Note: neither `success` nor `did_state` is populated in the response
76+
77+
if not verkey or not did:
78+
logger.error("Failed to create cheqd DID: `{}`.", cheqd_did_response)
79+
raise CloudApiException("Error creating cheqd did.")
80+
81+
result = DID(
82+
did=did,
83+
method=did_method,
84+
verkey=verkey,
85+
key_type="ed25519",
86+
posture="posted",
87+
)
88+
else:
89+
did_response = await handle_acapy_call(
90+
logger=logger,
91+
acapy_call=controller.wallet.create_did,
92+
body=did_create.to_acapy_request(),
93+
)
94+
95+
result = did_response.result
96+
if not result or not result.did or not result.verkey:
97+
logger.error("Failed to create DID: `{}`.", did_response)
98+
raise CloudApiException("Error creating did.")
99+
100+
logger.debug("Successfully created local {} DID.", did_method)
64101
return result
65102

66103

app/tests/e2e/test_did_rotate.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ async def test_rotate_did(
4242

4343
# Create a new did for Alice and rotate
4444
did_create_request = DIDCreate(method=did_method)
45-
acapy_did_create_request = did_create_request.to_acapy_request()
4645
new_did = await acapy_wallet.create_did(
47-
controller=alice_acapy_client, did_create=acapy_did_create_request
46+
controller=alice_acapy_client, did_create=did_create_request
4847
)
4948
alice_new_did = new_did.did
5049

app/tests/routes/wallet/dids/test_create_did.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from unittest import mock
12
from unittest.mock import AsyncMock, patch
23

34
import pytest
@@ -94,22 +95,24 @@
9495
)
9596
async def test_create_did_success(request_body, create_body):
9697
mock_aries_controller = AsyncMock()
97-
mock_create_did = AsyncMock()
98+
mock_aries_controller.wallet.create_did = AsyncMock()
9899

99100
with patch(
100101
"app.routes.wallet.dids.client_from_auth"
101102
) as mock_client_from_auth, patch(
102-
"app.services.acapy_wallet.create_did", mock_create_did
103-
):
103+
"app.services.acapy_wallet.handle_acapy_call"
104+
) as mock_handle_acapy_call:
104105
# Configure client_from_auth to return our mocked aries_controller on enter
105106
mock_client_from_auth.return_value.__aenter__.return_value = (
106107
mock_aries_controller
107108
)
108109

109110
await create_did(did_create=request_body, auth="mocked_auth")
110111

111-
mock_create_did.assert_awaited_once_with(
112-
did_create=create_body, controller=mock_aries_controller
112+
mock_handle_acapy_call.assert_awaited_once_with(
113+
logger=mock.ANY,
114+
acapy_call=mock_aries_controller.wallet.create_did,
115+
body=create_body,
113116
)
114117

115118

app/tests/services/test_acapy_wallet.py

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import pytest
2-
from aries_cloudcontroller import DID, AcaPyClient, DIDResult
2+
from aries_cloudcontroller import (
3+
DID,
4+
AcaPyClient,
5+
CreateCheqdDIDRequest,
6+
CreateCheqdDIDResponse,
7+
DIDResult,
8+
)
39

410
from app.exceptions import CloudApiException
11+
from app.models.wallet import DIDCreate
512
from app.services import acapy_wallet
613

714

@@ -44,3 +51,98 @@ async def test_error_on_assign_pub_did(mock_agent_controller: AcaPyClient):
4451
await acapy_wallet.set_public_did(mock_agent_controller, did="did")
4552
assert exc.value.status_code == 400
4653
assert "Error setting public did" in exc.value.detail
54+
55+
56+
@pytest.mark.anyio
57+
async def test_create_cheqd_did_success(mock_agent_controller: AcaPyClient):
58+
did_create_request = DIDCreate(
59+
method="cheqd",
60+
seed="testseed000000000000000000000001",
61+
)
62+
63+
expected_did = "did:cheqd:testnet:abcdef1234567890"
64+
expected_verkey = "WgWxqztrNooG92RXvxSTWvWgWxqztrNooG92RXvxSTWv"
65+
66+
mock_agent_controller.did.did_cheqd_create_post.return_value = (
67+
CreateCheqdDIDResponse(
68+
did=expected_did,
69+
verkey=expected_verkey,
70+
)
71+
)
72+
73+
created_did = await acapy_wallet.create_did(
74+
mock_agent_controller, did_create=did_create_request
75+
)
76+
77+
assert created_did.did == expected_did
78+
assert created_did.verkey == expected_verkey
79+
assert created_did.method == "cheqd"
80+
assert created_did.key_type == "ed25519" # Default key_type for cheqd
81+
assert created_did.posture == "posted" # Default posture for cheqd
82+
83+
mock_agent_controller.did.did_cheqd_create_post.assert_called_once()
84+
call_args = mock_agent_controller.did.did_cheqd_create_post.call_args
85+
assert call_args is not None
86+
assert isinstance(call_args.kwargs["body"], CreateCheqdDIDRequest)
87+
assert (
88+
call_args.kwargs["body"].options["seed"] == "testseed000000000000000000000001"
89+
)
90+
91+
92+
@pytest.mark.anyio
93+
async def test_create_cheqd_did_success_no_seed(mock_agent_controller: AcaPyClient):
94+
did_create_request = DIDCreate(method="cheqd")
95+
96+
expected_did = "did:cheqd:testnet:zxywv987654321"
97+
expected_verkey = "HkPgtEv9hrGjGkVSkL4sT8HkPgtEv9hrGjGkVSkL4sT8"
98+
99+
mock_agent_controller.did.did_cheqd_create_post.return_value = (
100+
CreateCheqdDIDResponse(
101+
did=expected_did,
102+
verkey=expected_verkey,
103+
)
104+
)
105+
106+
created_did = await acapy_wallet.create_did(
107+
mock_agent_controller, did_create=did_create_request
108+
)
109+
110+
assert created_did.did == expected_did
111+
assert created_did.verkey == expected_verkey
112+
assert created_did.method == "cheqd"
113+
114+
mock_agent_controller.did.did_cheqd_create_post.assert_called_once()
115+
call_args = mock_agent_controller.did.did_cheqd_create_post.call_args
116+
assert call_args is not None
117+
assert isinstance(call_args.kwargs["body"], CreateCheqdDIDRequest)
118+
assert (
119+
"seed" not in call_args.kwargs["body"].options
120+
) # Ensure seed is not passed if not provided
121+
122+
123+
@pytest.mark.anyio
124+
async def test_create_cheqd_did_fail_missing_did(mock_agent_controller: AcaPyClient):
125+
did_create_request = DIDCreate(method="cheqd", options={"network": "testnet"})
126+
127+
mock_agent_controller.did.did_cheqd_create_post.return_value = (
128+
CreateCheqdDIDResponse(verkey="some_verkey") # Missing did
129+
)
130+
131+
with pytest.raises(CloudApiException, match="Error creating cheqd did."):
132+
await acapy_wallet.create_did(
133+
mock_agent_controller, did_create=did_create_request
134+
)
135+
136+
137+
@pytest.mark.anyio
138+
async def test_create_cheqd_did_fail_missing_verkey(mock_agent_controller: AcaPyClient):
139+
did_create_request = DIDCreate(method="cheqd", options={"network": "testnet"})
140+
141+
mock_agent_controller.did.did_cheqd_create_post.return_value = (
142+
CreateCheqdDIDResponse(did="some_did") # Missing verkey
143+
)
144+
145+
with pytest.raises(CloudApiException, match="Error creating cheqd did."):
146+
await acapy_wallet.create_did(
147+
mock_agent_controller, did_create=did_create_request
148+
)

docs/openapi/tenant-openapi.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6204,9 +6204,10 @@
62046204
}
62056205
],
62066206
"title": "Method",
6207-
"description": "Method for the requested DID. Supported methods are 'sov', 'key', 'web', 'did:peer:2', or 'did:peer:4'.",
6207+
"description": "Method for the requested DID. Supported methods are cheqd, sov, key, web, did:peer:2, did:peer:4.",
62086208
"default": "sov",
62096209
"examples": [
6210+
"cheqd",
62106211
"sov",
62116212
"key",
62126213
"web",

docs/openapi/tenant-openapi.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3626,9 +3626,10 @@ components:
36263626
- type: string
36273627
- type: 'null'
36283628
title: Method
3629-
description: Method for the requested DID. Supported methods are 'sov', 'key', 'web', 'did:peer:2', or 'did:peer:4'.
3629+
description: Method for the requested DID. Supported methods are cheqd, sov, key, web, did:peer:2, did:peer:4.
36303630
default: sov
36313631
examples:
3632+
- cheqd
36323633
- sov
36333634
- key
36343635
- web

0 commit comments

Comments
 (0)