Skip to content

Commit

Permalink
📝 Update docstrings wallet/dids (#817)
Browse files Browse the repository at this point in the history
* Update docstrings

* remove full stop

* review changes

* add custom did create model

* update imports

* pass payload down

* 🎨

* update tests

* add default value

* 🎨

* add classmethod decorator and update descriptions

* import sort

* extend AcaPy model

* 🎨

* test more iterations

* options takes priority

* update docstrings

* if not

* add some types

* pass down acapy model

* update test with new model

* black .

* edits

* tweak validator

* 🎨 Update DIDCreate model, add to_acapy_options, and update create_did endpoint docstrings.

Handle deprecated `options` field in different way. Only overwrites key_type and did if options are indeed passed.

* 🎨 Update docstrings

* 🎨 Add one more example

* ✅ Fix new expected assertion

* 🎨 Fix model description and type

---------

Co-authored-by: ff137 <[email protected]>
Co-authored-by: cl0ete <[email protected]>
  • Loading branch information
cl0ete and ff137 authored Nov 20, 2024
1 parent 72b35e1 commit 8077903
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 25 deletions.
75 changes: 74 additions & 1 deletion app/models/wallet.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import List, Optional

from aries_cloudcontroller import DIDCreate as DIDCreateAcaPy
from aries_cloudcontroller.models.did_create_options import DIDCreateOptions
from aries_cloudcontroller.models.indy_cred_info import (
IndyCredInfo as IndyCredInfoAcaPy,
)
from aries_cloudcontroller.models.vc_record import VCRecord as VCRecordAcaPy
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, StrictStr, model_validator


class SetDidEndpointRequest(BaseModel):
Expand Down Expand Up @@ -41,3 +43,74 @@ class IndyCredInfo(IndyCredInfoAcaPy):

class CredInfoList(BaseModel):
results: Optional[List[IndyCredInfo]] = None


class DIDCreate(DIDCreateAcaPy):
"""
Extends the AcapyDIDCreate model with smart defaults and a simplified interface.
Handles deprecated `options` field from client requests by populating `key_type` and `did`.
Downstream processes should use the appropriate `options` structure based on the model's fields.
"""

method: Optional[StrictStr] = Field(
default="sov",
description=(
"Method for the requested DID. Supported methods are 'sov', 'key', 'web', 'did:peer:2', or 'did:peer:4'."
),
examples=["sov", "key", "web", "did:peer:2", "did:peer:4"],
)
options: Optional[DIDCreateOptions] = Field(
default=None,
deprecated=True,
description="(Deprecated) Define a key type and/or a DID depending on the chosen DID method.",
examples=[{"key_type": "ed25519", "did": "did:peer:2"}],
)
seed: Optional[StrictStr] = Field(
default=None,
description="Optional seed to use for DID. Must be enabled in configuration before use.",
)
key_type: Optional[StrictStr] = Field(
default="ed25519",
description="Key type to use for the DID key_pair. Validated with the chosen DID method's supported key types.",
examples=["ed25519", "bls12381g2"],
)
did: Optional[StrictStr] = Field(
default=None,
description="Specify the final value of DID (including `did:<method>:` prefix) if the method supports it.",
)

@model_validator(mode="before")
@classmethod
def handle_deprecated_options(cls, values: dict) -> dict:
"""
Handle deprecated `options` field from client requests.
Populate `key_type` and `did` fields based on `options` if they aren't explicitly provided.
Do not duplicate data by setting `options` based on `key_type` and `did`.
Args:
values: Dictionary containing the model fields
Returns:
Updated values dict with `key_type` and `did` populated from `options` if necessary
"""
options = values.get("options")

if options:
# Populate `key_type` from `options` if not explicitly provided
if not values.get("key_type"):
values["key_type"] = options.get("key_type", "ed25519")

# Populate `did` from `options` if not explicitly provided
if not values.get("did"):
values["did"] = options.get("did")

return values

def to_acapy_options(self) -> DIDCreateOptions:
"""
Convert the model's fields into the `DIDCreateOptions` structure expected by ACA-Py.
Returns:
An instance of `DIDCreateOptions` populated with `key_type` and `did`.
"""
return DIDCreateOptions(key_type=self.key_type, did=self.did)
149 changes: 129 additions & 20 deletions app/routes/wallet/dids.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import List, Optional

from aries_cloudcontroller import DID, DIDCreate, DIDEndpoint, DIDEndpointWithType
from aries_cloudcontroller import DID
from aries_cloudcontroller import DIDCreate as DIDCreateAcaPy
from aries_cloudcontroller import DIDEndpoint, DIDEndpointWithType
from fastapi import APIRouter, Depends

from app.dependencies.acapy_clients import client_from_auth
Expand All @@ -10,7 +12,7 @@
handle_acapy_call,
handle_model_with_validation,
)
from app.models.wallet import SetDidEndpointRequest
from app.models.wallet import DIDCreate, SetDidEndpointRequest
from app.services import acapy_wallet
from shared.log_config import get_logger

Expand All @@ -19,30 +21,72 @@
router = APIRouter(prefix="/v1/wallet/dids", tags=["wallet"])


@router.post("", response_model=DID)
@router.post("", response_model=DID, summary="Create Local DID")
async def create_did(
did_create: Optional[DIDCreate] = None,
auth: AcaPyAuth = Depends(acapy_auth_from_header),
):
"""Create Local DID."""
logger.debug("POST request received: Create DID")
) -> DID:
"""
Create Local DID
---
This endpoint allows you to create a new DID in the wallet.
The `method` parameter is optional and can be set to
'sov', 'key', 'web', 'did:peer:2', or 'did:peer:4'.
The `options` field is deprecated and has been flattened, such that `did` and
`key_type` are now top-level fields. The `options` field will still
take precedence over the top-level fields if it is present.
Request Body:
---
DIDCreate (Optional):
method (str, optional): Method for the requested DID.
options (DIDCreateOptions, optional): Deprecated.
seed (str, optional): Optional seed for DID.
key_type (str, optional): Key type for the DID.
did (str, optional): Specific DID value.
Response:
---
Returns the created DID object.
"""
logger.debug("POST request received: Create DID with data: %s", did_create)

if not did_create:
did_create = DIDCreate()

# Convert the custom DIDCreate model to Acapy's DIDCreateOptions
did_create_options = did_create.to_acapy_options()

# Initialize the Acapy DIDCreate model with necessary fields
acapy_did_create = DIDCreateAcaPy(
method=did_create.method, options=did_create_options, seed=did_create.seed
)

async with client_from_auth(auth) as aries_controller:
logger.debug("Creating DID")
logger.debug("Creating DID with request: %s", acapy_did_create)
result = await acapy_wallet.create_did(
did_create=did_create, controller=aries_controller
did_create=acapy_did_create, controller=aries_controller
)

logger.debug("Successfully created DID.")
return result


@router.get("", response_model=List[DID])
@router.get("", response_model=List[DID], summary="List DIDs")
async def list_dids(
auth: AcaPyAuth = Depends(acapy_auth_from_header),
) -> List[DID]:
"""
Retrieve list of DIDs.
Retrieve List of DIDs
---
This endpoint allows you to retrieve a list of DIDs in the wallet.
Response:
---
Returns a list of DID objects.
"""
logger.debug("GET request received: Retrieve list of DIDs")

Expand All @@ -60,12 +104,20 @@ async def list_dids(
return did_result.results


@router.get("/public", response_model=DID)
@router.get("/public", response_model=DID, summary="Fetch Public DID")
async def get_public_did(
auth: AcaPyAuth = Depends(acapy_auth_from_header),
) -> DID:
"""
Fetch the current public DID.
Fetch the Current Public DID
---
This endpoint allows you to fetch the current public DID.
By default, only issuers will have public DIDs.
Response:
---
Returns the public DID.
"""
logger.debug("GET request received: Fetch public DID")

Expand All @@ -82,12 +134,29 @@ async def get_public_did(
return result.result


@router.put("/public", response_model=DID)
@router.put("/public", response_model=DID, summary="Set Public DID")
async def set_public_did(
did: str,
auth: AcaPyAuth = Depends(acapy_auth_from_header),
) -> DID:
"""Set the current public DID."""
"""
Set the Current Public DID
---
This endpoint allows you to set the current public DID.
**Notes:**
- Requires an active endorser connection to make a DID public.
- By default, only issuers can have and update public DIDs.
Parameters:
---
did: str
Response:
---
Returns the public DID.
"""
logger.debug("PUT request received: Set public DID")

async with client_from_auth(auth) as aries_controller:
Expand All @@ -98,28 +167,55 @@ async def set_public_did(
return result


@router.patch("/{did}/rotate-keypair", status_code=204)
@router.patch("/{did}/rotate-keypair", status_code=204, summary="Rotate Key Pair")
async def rotate_keypair(
did: str,
auth: AcaPyAuth = Depends(acapy_auth_from_header),
) -> None:
"""
Rotate Key Pair for DID
---
This endpoint allows you to rotate the key pair for a DID.
Parameters:
---
did: str
Response:
---
204 No Content
"""
bound_logger = logger.bind(body={"did": did})
bound_logger.debug("PATCH request received: Rotate keypair for DID")
async with client_from_auth(auth) as aries_controller:
bound_logger.debug("Rotating keypair")
bound_logger.debug("Rotating key pair")
await handle_acapy_call(
logger=logger, acapy_call=aries_controller.wallet.rotate_keypair, did=did
)

bound_logger.debug("Successfully rotated keypair.")


@router.get("/{did}/endpoint", response_model=DIDEndpoint)
@router.get("/{did}/endpoint", response_model=DIDEndpoint, summary="Get DID Endpoint")
async def get_did_endpoint(
did: str,
auth: AcaPyAuth = Depends(acapy_auth_from_header),
) -> DIDEndpoint:
"""Get DID endpoint."""
"""
Get DID Endpoint
---
This endpoint allows you to fetch the endpoint for a DID.
Parameters:
---
did: str
Response:
---
Returns the endpoint for the DID.
"""
bound_logger = logger.bind(body={"did": did})
bound_logger.debug("GET request received: Get endpoint for DID")
async with client_from_auth(auth) as aries_controller:
Expand All @@ -132,14 +228,27 @@ async def get_did_endpoint(
return result


@router.post("/{did}/endpoint", status_code=204)
@router.post("/{did}/endpoint", status_code=204, summary="Set DID Endpoint")
async def set_did_endpoint(
did: str,
body: SetDidEndpointRequest,
auth: AcaPyAuth = Depends(acapy_auth_from_header),
) -> None:
"""Update Endpoint in wallet and on ledger if posted to it."""
"""
Update Endpoint of DID in Wallet (and on Ledger, if it is a Public DID)
---
This endpoint allows you to update the endpoint for a DID.
Parameters:
---
did: str
Request Body:
---
SetDidEndpointRequest:
endpoint: str
"""
# "Endpoint" type is for making connections using public indy DIDs
bound_logger = logger.bind(body={"did": did, "body": body})
bound_logger.debug("POST request received: Get endpoint for DID")
Expand Down
Loading

0 comments on commit 8077903

Please sign in to comment.