From a862461673f31da7a3f8c98e1f7a2619e09e5c9b Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Mon, 22 Apr 2024 21:15:03 -0400 Subject: [PATCH] feat: add connectionless issuance example Signed-off-by: Daniel Bluhm --- examples/connectionless/docker-compose.yml | 93 ++++++++ examples/connectionless/example.py | 241 +++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 examples/connectionless/docker-compose.yml create mode 100644 examples/connectionless/example.py diff --git a/examples/connectionless/docker-compose.yml b/examples/connectionless/docker-compose.yml new file mode 100644 index 0000000..72ef20e --- /dev/null +++ b/examples/connectionless/docker-compose.yml @@ -0,0 +1,93 @@ +version: "3" +services: + alice: + image: ghcr.io/hyperledger/aries-cloudagent-python:py3.9-0.11.0 + ports: + - "3001:3001" + environment: + RUST_LOG: 'aries-askar::log::target=error' + command: > + start + --label Alice + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://alice:3000 + --admin 0.0.0.0 3001 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_testnet_genesis + --wallet-type askar + --wallet-name alice + --wallet-key insecure + --auto-provision + --log-level debug + --debug-webhooks + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + depends_on: + tails: + condition: service_started + + bob: + image: ghcr.io/hyperledger/aries-cloudagent-python:py3.9-0.11.0 + ports: + - "3002:3001" + environment: + RUST_LOG: 'aries-askar::log::target=error' + command: > + start + --label Bob + --inbound-transport http 0.0.0.0 3000 + --outbound-transport http + --endpoint http://bob:3000 + --admin 0.0.0.0 3001 + --admin-insecure-mode + --tails-server-base-url http://tails:6543 + --genesis-url https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_testnet_genesis + --wallet-type askar + --wallet-name bob + --wallet-key insecure + --auto-provision + --log-level debug + --debug-webhooks + --monitor-revocation-notification + healthcheck: + test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null + start_period: 30s + interval: 7s + timeout: 5s + retries: 5 + + tails: + image: ghcr.io/bcgov/tails-server:latest + ports: + - 6543:6543 + environment: + - GENESIS_URL=https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_testnet_genesis + command: > + tails-server + --host 0.0.0.0 + --port 6543 + --storage-path /tmp/tails-files + --log-level INFO + + example: + container_name: controller + build: + context: ../.. + environment: + - ALICE=http://alice:3001 + - BOB=http://bob:3001 + volumes: + - ../../acapy_controller:/usr/src/app/acapy_controller:z + - ./example.py:/usr/src/app/example.py:ro,z + command: python -m example + depends_on: + alice: + condition: service_healthy + bob: + condition: service_healthy diff --git a/examples/connectionless/example.py b/examples/connectionless/example.py new file mode 100644 index 0000000..a0dcc78 --- /dev/null +++ b/examples/connectionless/example.py @@ -0,0 +1,241 @@ +"""Minimal reproducible example script. + +This script is for you to use to reproduce a bug or demonstrate a feature. +""" + +import asyncio +from dataclasses import dataclass +from os import getenv + +from acapy_controller import Controller +from acapy_controller.controller import Minimal +from acapy_controller.logging import logging_to_stdout +from acapy_controller.protocols import ( + InvitationRecord, + V20CredExRecordDetail, + V20CredExRecordIndy, + indy_anoncred_onboard, + indy_anoncred_credential_artifacts, +) + +ALICE = getenv("ALICE", "http://alice:3001") +BOB = getenv("BOB", "http://bob:3001") + + +@dataclass +class ConnectionlessV20CredExRecord(Minimal): + """Minimal record for connectionless v2 cred ex record.""" + + cred_ex_id: str + + +async def icv2(): + """Test Controller protocols.""" + async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB) as bob: + await indy_anoncred_onboard(alice) + _, cred_def = await indy_anoncred_credential_artifacts( + alice, ["firstname", "lastname"] + ) + + attributes = {"firstname": "Bob", "lastname": "Builder"} + offer = await alice.post( + "/issue-credential-2.0/create-offer", + json={ + "auto_issue": False, + "auto_remove": False, + "comment": "Credential from minimal example", + "trace": False, + "filter": {"indy": {"cred_def_id": cred_def.credential_definition_id}}, + "credential_preview": { + "type": "issue-credential-2.0/2.0/credential-preview", # pyright: ignore + "attributes": [ + { + "mime_type": None, + "name": name, + "value": value, + } + for name, value in attributes.items() + ], + }, + }, + response=ConnectionlessV20CredExRecord, + ) + invite = await alice.post( + "/out-of-band/create-invitation", + json={ + "attachments": [{"id": offer.cred_ex_id, "type": "credential-offer"}] + }, + response=InvitationRecord, + ) + bob.event_queue.flush() + await bob.post("/out-of-band/receive-invitation", json=invite.invitation) + bob_cred_ex = await bob.event_with_values( + topic="issue_credential_v2_0", + state="offer-received", + event_type=ConnectionlessV20CredExRecord, + ) + bob_cred_ex_id = bob_cred_ex.cred_ex_id + + alice.event_queue.flush() + bob_cred_ex = await bob.post( + f"/issue-credential-2.0/records/{bob_cred_ex_id}/send-request", + response=ConnectionlessV20CredExRecord, + ) + + alice_cred_ex = await alice.event_with_values( + topic="issue_credential_v2_0", + state="request-received", + event_type=ConnectionlessV20CredExRecord, + ) + alice_cred_ex_id = alice_cred_ex.cred_ex_id + + alice_cred_ex = await alice.post( + f"/issue-credential-2.0/records/{alice_cred_ex_id}/issue", + json={}, + response=V20CredExRecordDetail, + ) + + await bob.event_with_values( + topic="issue_credential_v2_0", + cred_ex_id=bob_cred_ex_id, + state="credential-received", + ) + + bob_cred_ex = await bob.post( + f"/issue-credential-2.0/records/{bob_cred_ex_id}/store", + json={}, + response=V20CredExRecordDetail, + ) + alice_cred_ex = await alice.event_with_values( + topic="issue_credential_v2_0", + event_type=ConnectionlessV20CredExRecord, + cred_ex_id=alice_cred_ex_id, + state="done", + ) + await alice.event_with_values( + topic="issue_credential_v2_0_indy", + event_type=V20CredExRecordIndy, + ) + + bob_cred_ex = await bob.event_with_values( + topic="issue_credential_v2_0", + event_type=ConnectionlessV20CredExRecord, + cred_ex_id=bob_cred_ex_id, + state="done", + ) + await bob.event_with_values( + topic="issue_credential_v2_0_indy", + event_type=V20CredExRecordIndy, + ) + + +@dataclass +class ConnectionlessV10CredExRecord(Minimal): + """Minimal record for v1 cred ex record.""" + + credential_exchange_id: str + + +async def icv1(): + """Issue credential v1.""" + async with Controller(base_url=ALICE) as alice, Controller(base_url=BOB) as bob: + await indy_anoncred_onboard(alice) + _, cred_def = await indy_anoncred_credential_artifacts( + alice, ["firstname", "lastname"] + ) + + attributes = {"firstname": "Bob", "lastname": "Builder"} + offer = await alice.post( + "/issue-credential/create-offer", + json={ + "auto_issue": False, + "auto_remove": False, + "comment": "Credential from minimal example", + "trace": False, + "cred_def_id": cred_def.credential_definition_id, + "credential_preview": { + "@type": "issue-credential/1.0/credential-preview", + "attributes": [ + { + "mime_type": None, + "name": name, + "value": value, + } + for name, value in attributes.items() + ], + }, + }, + response=ConnectionlessV10CredExRecord, + ) + invite = await alice.post( + "/out-of-band/create-invitation", + json={ + "attachments": [ + {"id": offer.credential_exchange_id, "type": "credential-offer"} + ] + }, + response=InvitationRecord, + ) + bob.event_queue.flush() + await bob.post("/out-of-band/receive-invitation", json=invite.invitation) + bob_cred_ex = await bob.event_with_values( + topic="issue_credential", + state="offer_received", + event_type=ConnectionlessV10CredExRecord, + ) + bob_cred_ex_id = bob_cred_ex.credential_exchange_id + + alice.event_queue.flush() + bob_cred_ex = await bob.post( + f"/issue-credential/records/{bob_cred_ex_id}/send-request", + response=ConnectionlessV10CredExRecord, + ) + + alice_cred_ex = await alice.event_with_values( + topic="issue_credential", + state="request_received", + event_type=ConnectionlessV10CredExRecord, + ) + alice_cred_ex_id = alice_cred_ex.credential_exchange_id + + alice_cred_ex = await alice.post( + f"/issue-credential/records/{alice_cred_ex_id}/issue", + json={}, + response=ConnectionlessV10CredExRecord, + ) + + await bob.event_with_values( + topic="issue_credential", + credential_exchange_id=bob_cred_ex_id, + state="credential_received", + ) + + bob_cred_ex = await bob.post( + f"/issue-credential/records/{bob_cred_ex_id}/store", + json={}, + response=ConnectionlessV10CredExRecord, + ) + alice_cred_ex = await alice.event_with_values( + topic="issue_credential", + event_type=ConnectionlessV10CredExRecord, + credential_exchange_id=alice_cred_ex_id, + state="credential_acked", + ) + + bob_cred_ex = await bob.event_with_values( + topic="issue_credential", + event_type=ConnectionlessV10CredExRecord, + credential_exchange_id=bob_cred_ex_id, + state="credential_acked", + ) + + +async def main(): + """Run.""" + await icv1() + await icv2() + + +if __name__ == "__main__": + logging_to_stdout() + asyncio.run(main())