diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/tests/test_transaction_acknowledgement_handler.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/tests/test_transaction_acknowledgement_handler.py new file mode 100644 index 0000000000..ed4a864727 --- /dev/null +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/tests/test_transaction_acknowledgement_handler.py @@ -0,0 +1,84 @@ +from asynctest import ( + mock as async_mock, + TestCase as AsyncTestCase, +) + +from ......connections.models.conn_record import ConnRecord +from ......messaging.request_context import RequestContext +from ......messaging.responder import MockResponder +from ......transport.inbound.receipt import MessageReceipt + +from ...handlers import transaction_acknowledgement_handler as test_module +from ...messages.transaction_acknowledgement import TransactionAcknowledgement + + +class TestTransactionAcknowledgementHandler(AsyncTestCase): + async def test_called(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + with async_mock.patch.object( + test_module, "TransactionManager", autospec=True + ) as mock_tran_mgr: + mock_tran_mgr.return_value.receive_transaction_acknowledgement = ( + async_mock.CoroutineMock() + ) + request_context.message = TransactionAcknowledgement() + request_context.connection_record = ConnRecord( + connection_id="b5dc1636-a19a-4209-819f-e8f9984d9897" + ) + request_context.connection_ready = True + handler = test_module.TransactionAcknowledgementHandler() + responder = MockResponder() + await handler.handle(request_context, responder) + + mock_tran_mgr.return_value.receive_transaction_acknowledgement.assert_called_once_with( + request_context.message, request_context.connection_record.connection_id + ) + assert not responder.messages + + async def test_called_not_ready(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + request_context.connection_record = async_mock.MagicMock() + + with async_mock.patch.object( + test_module, "TransactionManager", autospec=True + ) as mock_tran_mgr: + mock_tran_mgr.return_value.receive_transaction_acknowledgement = ( + async_mock.CoroutineMock() + ) + request_context.message = TransactionAcknowledgement() + request_context.connection_ready = False + handler = test_module.TransactionAcknowledgementHandler() + responder = MockResponder() + with self.assertRaises(test_module.HandlerException): + await handler.handle(request_context, responder) + + assert not responder.messages + + async def test_called_x(self): + request_context = RequestContext.test_context() + request_context.message_receipt = MessageReceipt() + + with async_mock.patch.object( + test_module, "TransactionManager", autospec=True + ) as mock_tran_mgr: + mock_tran_mgr.return_value.receive_transaction_acknowledgement = ( + async_mock.CoroutineMock( + side_effect=test_module.TransactionManagerError() + ) + ) + request_context.message = TransactionAcknowledgement() + request_context.connection_record = ConnRecord( + connection_id="b5dc1636-a19a-4209-819f-e8f9984d9897" + ) + request_context.connection_ready = True + handler = test_module.TransactionAcknowledgementHandler() + responder = MockResponder() + await handler.handle(request_context, responder) + + mock_tran_mgr.return_value.receive_transaction_acknowledgement.assert_called_once_with( + request_context.message, request_context.connection_record.connection_id + ) + assert not responder.messages diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/transaction_acknowledgement_handler.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/transaction_acknowledgement_handler.py new file mode 100644 index 0000000000..5c999661ea --- /dev/null +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/handlers/transaction_acknowledgement_handler.py @@ -0,0 +1,41 @@ +"""Transaction acknowledgement message handler.""" + +from .....messaging.base_handler import ( + BaseHandler, + BaseResponder, + HandlerException, + RequestContext, +) + +from ..manager import TransactionManager, TransactionManagerError +from ..messages.transaction_acknowledgement import TransactionAcknowledgement + + +class TransactionAcknowledgementHandler(BaseHandler): + """Message handler class for Acknowledging transaction.""" + + async def handle(self, context: RequestContext, responder: BaseResponder): + """ + Handle transaction acknowledgement message. + + Args: + context: Request context + responder: Responder callback + """ + + self._logger.debug( + f"TransactionAcknowledgementHandler called with context {context}" + ) + assert isinstance(context.message, TransactionAcknowledgement) + + if not context.connection_ready: + raise HandlerException("No connection established") + + profile_session = await context.session() + mgr = TransactionManager(profile_session) + try: + await mgr.receive_transaction_acknowledgement( + context.message, context.connection_record.connection_id + ) + except TransactionManagerError: + self._logger.exception("Error receiving transaction acknowledgement") diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py index 621a4ec650..5d4b9c6df5 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py @@ -1,23 +1,33 @@ """Class to manage transactions.""" -from aiohttp import web +import json import logging import uuid -from .models.transaction_record import TransactionRecord -from .messages.transaction_request import TransactionRequest -from .messages.endorsed_transaction_response import EndorsedTransactionResponse -from .messages.refused_transaction_response import RefusedTransactionResponse -from .messages.cancel_transaction import CancelTransaction -from .messages.transaction_resend import TransactionResend -from .messages.transaction_job_to_send import TransactionJobToSend +from asyncio import shield +from time import time from ....connections.models.conn_record import ConnRecord -from ....transport.inbound.receipt import MessageReceipt -from ....storage.error import StorageNotFoundError - from ....core.error import BaseError from ....core.profile import ProfileSession +from ....indy.issuer import IndyIssuerError +from ....ledger.base import BaseLedger +from ....ledger.error import LedgerError +from ....messaging.credential_definitions.util import CRED_DEF_SENT_RECORD_TYPE +from ....messaging.schemas.util import SCHEMA_SENT_RECORD_TYPE +from ....storage.base import StorageRecord +from ....storage.error import StorageNotFoundError +from ....transport.inbound.receipt import MessageReceipt + +from .messages.cancel_transaction import CancelTransaction +from .messages.endorsed_transaction_response import EndorsedTransactionResponse +from .messages.refused_transaction_response import RefusedTransactionResponse +from .messages.transaction_acknowledgement import TransactionAcknowledgement +from .messages.transaction_job_to_send import TransactionJobToSend +from .messages.transaction_request import TransactionRequest +from .messages.transaction_resend import TransactionResend +from .models.transaction_record import TransactionRecord +from .transaction_jobs import TransactionJob class TransactionManagerError(BaseError): @@ -283,7 +293,7 @@ async def complete_transaction(self, transaction: TransactionRecord): """ Complete a transaction. - This is the final state after the received ledger transaction + This is the final state where the received ledger transaction is written to the ledger. Args: @@ -294,11 +304,85 @@ async def complete_transaction(self, transaction: TransactionRecord): """ - transaction.state = TransactionRecord.STATE_TRANSACTION_COMPLETED profile_session = await self.session + transaction.state = TransactionRecord.STATE_TRANSACTION_ACKED + async with profile_session.profile.session() as session: await transaction.save(session, reason="Completed transaction") + connection_id = transaction.connection_id + + async with profile_session.profile.session() as session: + connection_record = await ConnRecord.retrieve_by_id(session, connection_id) + jobs = await connection_record.metadata_get(self._session, "transaction_jobs") + if not jobs: + raise TransactionManagerError( + "The transaction related jobs are not set up in " + "connection metadata for this connection record" + ) + if "transaction_my_job" not in jobs.keys(): + raise TransactionManagerError( + 'The "transaction_my_job" is not set in "transaction_jobs"' + " in connection metadata for this connection record" + ) + if jobs["transaction_my_job"] == TransactionJob.TRANSACTION_AUTHOR.name: + await self.store_record_in_wallet(transaction) + + transaction_acknowledgement_message = TransactionAcknowledgement( + thread_id=transaction._id + ) + + return transaction, transaction_acknowledgement_message + + async def receive_transaction_acknowledgement( + self, response: TransactionAcknowledgement, connection_id: str + ): + """ + Update the transaction record after receiving the transaction acknowledgement. + + Args: + response: The transaction acknowledgement + connection_id: The connection_id related to this Transaction Record + """ + + profile_session = await self.session + async with profile_session.profile.session() as session: + transaction = await TransactionRecord.retrieve_by_connection_and_thread( + session, connection_id, response.thread_id + ) + + if transaction.state != TransactionRecord.STATE_TRANSACTION_ENDORSED: + raise TransactionManagerError( + "Only an endorsed transaction can be written to the ledger." + ) + + transaction.state = TransactionRecord.STATE_TRANSACTION_ACKED + async with profile_session.profile.session() as session: + await transaction.save(session, reason="Received a transaction ack") + + connection_id = transaction.connection_id + + try: + async with profile_session.profile.session() as session: + connection_record = await ConnRecord.retrieve_by_id( + session, connection_id + ) + except StorageNotFoundError as err: + raise TransactionManagerError(err.roll_up) from err + jobs = await connection_record.metadata_get(self._session, "transaction_jobs") + if not jobs: + raise TransactionManagerError( + "The transaction related jobs are not set up in " + "connection metadata for this connection record" + ) + if "transaction_my_job" not in jobs.keys(): + raise TransactionManagerError( + 'The "transaction_my_job" is not set in "transaction_jobs"' + " in connection metadata for this connection record" + ) + if jobs["transaction_my_job"] == TransactionJob.TRANSACTION_AUTHOR.name: + await self.store_record_in_wallet(transaction) + return transaction async def create_refuse_response( @@ -531,7 +615,7 @@ async def set_transaction_their_job( self._session, receipt.sender_did, receipt.recipient_did ) except StorageNotFoundError as err: - raise web.HTTPNotFound(reason=err.roll_up) from err + raise TransactionManagerError(err.roll_up) from err value = await connection.metadata_get(self._session, "transaction_jobs") if value: @@ -541,3 +625,85 @@ async def set_transaction_their_job( await connection.metadata_set( self._session, key="transaction_jobs", value=value ) + + async def store_record_in_wallet(self, transaction: TransactionRecord): + """ + Store record in wallet. + + Args: + transaction: The transaction from which the schema/cred_def + would be stored in wallet. + """ + + ledger_transaction = transaction.messages_attach[0]["data"]["json"] + + ledger = self._session.inject(BaseLedger) + if not ledger: + reason = "No ledger available" + if not self._session.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise TransactionManagerError(reason) + + async with ledger: + try: + ledger_response_json = await shield( + ledger.txn_submit(ledger_transaction, sign=False, taa_accept=False) + ) + except (IndyIssuerError, LedgerError) as err: + raise TransactionManagerError(err.roll_up) from err + + ledger_response = json.loads(ledger_response_json) + + # write the wallet non-secrets record + # TODO refactor this code (duplicated from ledger.indy.py) + if ledger_response["result"]["txn"]["type"] == "101": + # schema transaction + schema_id = ledger_response["result"]["txnMetadata"]["txnId"] + schema_id_parts = schema_id.split(":") + public_did = ledger_response["result"]["txn"]["metadata"]["from"] + schema_tags = { + "schema_id": schema_id, + "schema_issuer_did": public_did, + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "epoch": str(int(time())), + } + record = StorageRecord(SCHEMA_SENT_RECORD_TYPE, schema_id, schema_tags) + # TODO refactor this code? + async with ledger: + storage = ledger.get_indy_storage() + await storage.add_record(record) + + elif ledger_response["result"]["txn"]["type"] == "102": + # cred def transaction + async with ledger: + try: + schema_seq_no = str(ledger_response["result"]["txn"]["data"]["ref"]) + schema_response = await shield(ledger.get_schema(schema_seq_no)) + except (IndyIssuerError, LedgerError) as err: + raise TransactionManagerError(err.roll_up) from err + + schema_id = schema_response["id"] + schema_id_parts = schema_id.split(":") + public_did = ledger_response["result"]["txn"]["metadata"]["from"] + credential_definition_id = ledger_response["result"]["txnMetadata"]["txnId"] + cred_def_tags = { + "schema_id": schema_id, + "schema_issuer_did": schema_id_parts[0], + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "issuer_did": public_did, + "cred_def_id": credential_definition_id, + "epoch": str(int(time())), + } + record = StorageRecord( + CRED_DEF_SENT_RECORD_TYPE, credential_definition_id, cred_def_tags + ) + # TODO refactor this code? + async with ledger: + storage = ledger.get_indy_storage() + await storage.add_record(record) + + else: + # TODO unknown ledger transaction type, just ignore for now ... + pass diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/message_types.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/message_types.py index b8f43fb1ed..ea17bdc9b7 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/message_types.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/message_types.py @@ -10,6 +10,7 @@ CANCEL_TRANSACTION = "transactions/1.0/cancel" TRANSACTION_RESEND = "transactions/1.0/resend" TRANSACTION_JOB_TO_SEND = "transactions/1.0/transaction_my_job" +TRANSACTION_ACKNOWLEDGEMENT = "transactions/1.0/ack" ATTACHED_MESSAGE = "transactions/1.0/message" PROTOCOL_PACKAGE = "aries_cloudagent.protocols.endorse_transaction.v1_0" @@ -36,5 +37,9 @@ TRANSACTION_JOB_TO_SEND: ( f"{PROTOCOL_PACKAGE}.messages.transaction_job_to_send.TransactionJobToSend" ), + TRANSACTION_ACKNOWLEDGEMENT: ( + f"{PROTOCOL_PACKAGE}.messages.transaction_acknowledgement" + ".TransactionAcknowledgement" + ), } ) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/messages/tests/test_transaction_acknowledgement.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/messages/tests/test_transaction_acknowledgement.py new file mode 100644 index 0000000000..09bc6b0a1e --- /dev/null +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/messages/tests/test_transaction_acknowledgement.py @@ -0,0 +1,77 @@ +from asynctest import TestCase as AsyncTestCase +from unittest import mock, TestCase + +from .....didcomm_prefix import DIDCommPrefix + +from ...message_types import TRANSACTION_ACKNOWLEDGEMENT + +from ..transaction_acknowledgement import TransactionAcknowledgement + + +class TestConfig: + test_thread_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6" + + +class TestTransactionAcknowledgement(TestCase, TestConfig): + def setUp(self): + self.transaction_acknowledgement = TransactionAcknowledgement( + thread_id=self.test_thread_id + ) + + def test_init(self): + """Test initialization.""" + assert self.transaction_acknowledgement.thread_id == self.test_thread_id + + def test_type(self): + """Test type.""" + assert self.transaction_acknowledgement._type == DIDCommPrefix.qualify_current( + TRANSACTION_ACKNOWLEDGEMENT + ) + + @mock.patch( + "aries_cloudagent.protocols.endorse_transaction.v1_0.messages." + "transaction_acknowledgement.TransactionAcknowledgementSchema.load" + ) + def test_deserialize(self, mock_transaction_acknowledgement_schema_load): + """ + Test deserialization. + """ + obj = self.transaction_acknowledgement + + transaction_acknowledgement = TransactionAcknowledgement.deserialize(obj) + mock_transaction_acknowledgement_schema_load.assert_called_once_with(obj) + + assert ( + transaction_acknowledgement + is mock_transaction_acknowledgement_schema_load.return_value + ) + + @mock.patch( + "aries_cloudagent.protocols.endorse_transaction.v1_0.messages." + "transaction_acknowledgement.TransactionAcknowledgementSchema.dump" + ) + def test_serialize(self, mock_transaction_acknowledgement_schema_dump): + """ + Test serialization. + """ + transaction_acknowledgement_dict = self.transaction_acknowledgement.serialize() + mock_transaction_acknowledgement_schema_dump.assert_called_once_with( + self.transaction_acknowledgement + ) + + assert ( + transaction_acknowledgement_dict + is mock_transaction_acknowledgement_schema_dump.return_value + ) + + +class TestTransactionAcknowledgementSchema(AsyncTestCase, TestConfig): + """Test transaction acknowledgement schema.""" + + async def test_make_model(self): + transaction_acknowledgement = TransactionAcknowledgement( + thread_id=self.test_thread_id + ) + data = transaction_acknowledgement.serialize() + model_instance = TransactionAcknowledgement.deserialize(data) + assert type(model_instance) is type(transaction_acknowledgement) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/messages/transaction_acknowledgement.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/messages/transaction_acknowledgement.py new file mode 100644 index 0000000000..2765eddb6a --- /dev/null +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/messages/transaction_acknowledgement.py @@ -0,0 +1,51 @@ +"""Represents a transaction acknowledgement message.""" + +from marshmallow import EXCLUDE, fields + +from .....messaging.ack.message import Ack, AckSchema +from .....messaging.valid import UUIDFour + +from ..message_types import TRANSACTION_ACKNOWLEDGEMENT, PROTOCOL_PACKAGE + +HANDLER_CLASS = ( + f"{PROTOCOL_PACKAGE}.handlers" + ".transaction_acknowledgement_handler.TransactionAcknowledgementHandler" +) + + +class TransactionAcknowledgement(Ack): + """Class representing a transaction acknowledgement message.""" + + class Meta: + """Metadata for a transaction acknowledgement message.""" + + handler_class = HANDLER_CLASS + message_type = TRANSACTION_ACKNOWLEDGEMENT + schema_class = "TransactionAcknowledgementSchema" + + def __init__( + self, + *, + thread_id: str = None, + **kwargs, + ): + """ + Initialize a transaction acknowledgement object. + + Args: + thread_id: Thread id of transaction record + """ + super().__init__(**kwargs) + self.thread_id = thread_id + + +class TransactionAcknowledgementSchema(AckSchema): + """Transaction Acknowledgement schema class.""" + + class Meta: + """Transaction Acknowledgement metadata.""" + + model_class = TransactionAcknowledgement + unknown = EXCLUDE + + thread_id = fields.Str(required=True, example=UUIDFour.EXAMPLE) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/models/transaction_record.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/models/transaction_record.py index 99354774ae..2bc980106b 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/models/transaction_record.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/models/transaction_record.py @@ -2,15 +2,13 @@ from marshmallow import fields +from .....core.profile import ProfileSession from .....messaging.models.base_record import ( BaseExchangeRecord, BaseExchangeSchema, ) - from .....messaging.valid import UUIDFour -from .....core.profile import ProfileSession - class TransactionRecord(BaseExchangeRecord): """Represents a single transaction record.""" @@ -51,7 +49,7 @@ class Meta: STATE_TRANSACTION_RESENT = "transaction_resent" STATE_TRANSACTION_RESENT_RECEIEVED = "transaction_resent_received" STATE_TRANSACTION_CANCELLED = "transaction_cancelled" - STATE_TRANSACTION_COMPLETED = "transaction_completed" + STATE_TRANSACTION_ACKED = "transaction_acked" def __init__( self, diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py index 9689ca341d..be39b084f7 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py @@ -1,5 +1,4 @@ """Endorse Transaction handling admin routes.""" -import json from aiohttp import web from aiohttp_apispec import ( @@ -11,19 +10,15 @@ ) from asyncio import shield from marshmallow import fields, validate -from time import time from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....indy.issuer import IndyIssuerError from ....ledger.base import BaseLedger from ....ledger.error import LedgerError -from ....messaging.credential_definitions.util import CRED_DEF_SENT_RECORD_TYPE +from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema -from ....messaging.schemas.util import SCHEMA_SENT_RECORD_TYPE from ....messaging.valid import UUIDFour -from ....messaging.models.base import BaseModelError -from ....storage.base import StorageRecord from ....storage.error import StorageError, StorageNotFoundError from ....wallet.base import BaseWallet @@ -126,10 +121,8 @@ async def transactions_list(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The transaction list response - """ context: AdminRequestContext = request["context"] @@ -158,10 +151,8 @@ async def transactions_retrieve(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The transaction record response - """ context: AdminRequestContext = request["context"] @@ -194,10 +185,8 @@ async def transaction_create_request(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The transaction record - """ context: AdminRequestContext = request["context"] @@ -276,10 +265,8 @@ async def endorse_transaction_response(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The updated transaction record details - """ context: AdminRequestContext = request["context"] @@ -380,10 +367,8 @@ async def refuse_transaction_response(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The updated transaction record details - """ context: AdminRequestContext = request["context"] @@ -461,10 +446,8 @@ async def cancel_transaction(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The updated transaction record details - """ context: AdminRequestContext = request["context"] @@ -528,10 +511,8 @@ async def transaction_resend(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The updated transaction record details - """ context: AdminRequestContext = request["context"] @@ -596,10 +577,8 @@ async def set_endorser_role(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The assigned transaction jobs - """ context: AdminRequestContext = request["context"] @@ -638,10 +617,8 @@ async def set_endorser_info(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The assigned endorser information - """ context: AdminRequestContext = request["context"] @@ -693,7 +670,7 @@ async def set_endorser_info(request: web.BaseRequest): @docs( tags=["endorse-transaction"], - summary="For Author to write an endorsed transaction to the ledger", + summary="For Author / Endorser to write an endorsed transaction to the ledger", ) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) @@ -703,13 +680,12 @@ async def transaction_write(request: web.BaseRequest): Args: request: aiohttp request object - Returns: The returned ledger response - """ context: AdminRequestContext = request["context"] + outbound_handler = request["outbound_message_router"] transaction_id = request.match_info["tran_id"] try: @@ -717,42 +693,24 @@ async def transaction_write(request: web.BaseRequest): transaction = await TransactionRecord.retrieve_by_id( session, transaction_id ) - connection_record = await ConnRecord.retrieve_by_id( - session, transaction.connection_id - ) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err except BaseModelError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - session = await context.session() - jobs = await connection_record.metadata_get(session, "transaction_jobs") - if not jobs: - raise web.HTTPForbidden( - reason=( - "The transaction related jobs are not set up in " - "connection metadata for this connection record" - ) - ) - if jobs["transaction_my_job"] != TransactionJob.TRANSACTION_AUTHOR.name: - raise web.HTTPForbidden( - reason="Only a TRANSACTION_AUTHOR can write a transaction to the ledger" - ) - if transaction.state != TransactionRecord.STATE_TRANSACTION_ENDORSED: raise web.HTTPForbidden( reason="Only an endorsed transaction can be written to the ledger" ) + """ ledger_transaction = transaction.messages_attach[0]["data"]["json"] - ledger = context.inject(BaseLedger, required=False) if not ledger: reason = "No ledger available" if not context.settings.get_value("wallet.type"): reason += ": missing wallet-type?" raise web.HTTPForbidden(reason=reason) - async with ledger: try: ledger_response_json = await shield( @@ -760,9 +718,7 @@ async def transaction_write(request: web.BaseRequest): ) except (IndyIssuerError, LedgerError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - ledger_response = json.loads(ledger_response_json) - # write the wallet non-secrets record # TODO refactor this code (duplicated from ledger.indy.py) if ledger_response["result"]["txn"]["type"] == "101": @@ -782,7 +738,6 @@ async def transaction_write(request: web.BaseRequest): async with ledger: storage = ledger.get_indy_storage() await storage.add_record(record) - elif ledger_response["result"]["txn"]["type"] == "102": # cred def transaction async with ledger: @@ -791,7 +746,6 @@ async def transaction_write(request: web.BaseRequest): schema_response = await shield(ledger.get_schema(schema_seq_no)) except (IndyIssuerError, LedgerError) as err: raise web.HTTPBadRequest(reason=err.roll_up) from err - schema_id = schema_response["id"] schema_id_parts = schema_id.split(":") public_did = ledger_response["result"]["txn"]["metadata"]["from"] @@ -812,20 +766,26 @@ async def transaction_write(request: web.BaseRequest): async with ledger: storage = ledger.get_indy_storage() await storage.add_record(record) - else: # TODO unknown ledger transaction type, just ignore for now ... pass + """ # update the final transaction status + session = await context.session() transaction_mgr = TransactionManager(session) try: - tx_completed = await transaction_mgr.complete_transaction( - transaction=transaction - ) + ( + tx_completed, + transaction_acknowledgement_message, + ) = await transaction_mgr.complete_transaction(transaction=transaction) except StorageError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err + await outbound_handler( + transaction_acknowledgement_message, connection_id=transaction.connection_id + ) + return web.json_response(tx_completed.serialize()) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py index 13e57d6043..bd6d52fefe 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_manager.py @@ -1,21 +1,30 @@ +import json import uuid from aiohttp import web -from asynctest import TestCase as AsyncTestCase from asynctest import mock as async_mock +from asynctest import TestCase as AsyncTestCase from .....cache.base import BaseCache from .....cache.in_memory import InMemoryCache from .....connections.models.conn_record import ConnRecord from .....core.in_memory import InMemoryProfile +from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError from ..manager import TransactionManager, TransactionManagerError -from ..models.transaction_record import TransactionRecord from ..messages.messages_attach import MessagesAttach +from ..messages.transaction_acknowledgement import TransactionAcknowledgement +from ..messages.transaction_request import TransactionRequest +from ..models.transaction_record import TransactionRecord from ..transaction_jobs import TransactionJob -from ..messages.transaction_request import TransactionRequest + +TEST_DID = "LjgpST2rjsoxYegQDRm7EL" +SCHEMA_NAME = "bc-reg" +SCHEMA_TXN = 12 +SCHEMA_ID = f"{TEST_DID}:2:{SCHEMA_NAME}:1.0" +CRED_DEF_ID = f"{TEST_DID}:3:CL:12:tag1" class TestTransactionManager(AsyncTestCase): @@ -97,6 +106,9 @@ async def setUp(self): self.test_endorser_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" self.test_refuser_did = "AGDEjaMunDtFtBVrn1qPKQ" + self.ledger = async_mock.create_autospec(BaseLedger) + self.session.context.injector.bind_instance(BaseLedger, self.ledger) + self.manager = TransactionManager(self.session) assert self.manager.session @@ -363,15 +375,45 @@ async def test_complete_transaction(self): messages_attach=self.test_messages_attach, connection_id=self.test_connection_id, ) + + self.ledger.get_indy_storage = async_mock.MagicMock( + return_value=async_mock.MagicMock(add_record=async_mock.CoroutineMock()) + ) + self.ledger.txn_submit = async_mock.CoroutineMock( + return_value=json.dumps( + { + "result": { + "txn": {"type": "101", "metadata": {"from": TEST_DID}}, + "txnMetadata": {"txnId": SCHEMA_ID}, + } + } + ) + ) + with async_mock.patch.object( TransactionRecord, "save", autospec=True - ) as save_record: - transaction_record = await self.manager.complete_transaction( - transaction_record + ) as save_record, async_mock.patch.object( + ConnRecord, "retrieve_by_id" + ) as mock_conn_rec_retrieve: + + mock_conn_rec_retrieve.return_value = async_mock.MagicMock( + metadata_get=async_mock.CoroutineMock( + return_value={ + "transaction_their_job": ( + TransactionJob.TRANSACTION_ENDORSER.name + ), + "transaction_my_job": (TransactionJob.TRANSACTION_AUTHOR.name), + } + ) ) + + ( + transaction_record, + transaction_acknowledgement_message, + ) = await self.manager.complete_transaction(transaction_record) save_record.assert_called_once() - assert transaction_record.state == TransactionRecord.STATE_TRANSACTION_COMPLETED + assert transaction_record.state == TransactionRecord.STATE_TRANSACTION_ACKED async def test_create_refuse_response_bad_state(self): transaction_record = await self.manager.create_record( @@ -661,5 +703,5 @@ async def test_set_transaction_their_job_conn_not_found(self): ) as mock_retrieve: mock_retrieve.side_effect = StorageNotFoundError() - with self.assertRaises(web.HTTPNotFound): + with self.assertRaises(TransactionManagerError): await self.manager.set_transaction_their_job(mock_job, mock_receipt) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py index 367767a772..1e478c098b 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py @@ -6,13 +6,13 @@ from .....connections.models.conn_record import ConnRecord from .....core.in_memory import InMemoryProfile from .....ledger.base import BaseLedger -from .....wallet.did_method import DIDMethod -from .....wallet.key_type import KeyType from .....wallet.base import BaseWallet from .....wallet.did_info import DIDInfo +from .....wallet.did_method import DIDMethod +from .....wallet.key_type import KeyType -from .. import routes as test_module from ..models.transaction_record import TransactionRecord +from .. import routes as test_module TEST_DID = "LjgpST2rjsoxYegQDRm7EL" @@ -1450,42 +1450,30 @@ async def test_set_endorser_info_my_wrong_job_x(self): async def test_transaction_write_schema_txn(self): self.request.match_info = {"tran_id": "dummy"} with async_mock.patch.object( - ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_conn_rec_retrieve, async_mock.patch.object( TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() ) as mock_txn_rec_retrieve, async_mock.patch.object( test_module, "TransactionManager", async_mock.MagicMock() ) as mock_txn_mgr, async_mock.patch.object( test_module.web, "json_response" ) as mock_response: - mock_txn_mgr.return_value = async_mock.MagicMock( - complete_transaction=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # txn record - serialize=async_mock.MagicMock(return_value={"...": "..."}) - ) - ) - ) - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( - metadata_get=async_mock.CoroutineMock( - return_value={ - "transaction_my_job": ( - test_module.TransactionJob.TRANSACTION_AUTHOR.name - ), - "transaction_their_job": ( - test_module.TransactionJob.TRANSACTION_ENDORSER.name - ), - } - ) + + mock_txn_mgr.return_value.complete_transaction = async_mock.CoroutineMock() + + mock_txn_mgr.return_value.complete_transaction.return_value = ( + async_mock.CoroutineMock( + serialize=async_mock.MagicMock(return_value={"...": "..."}) + ), + async_mock.CoroutineMock(), ) + mock_txn_rec_retrieve.return_value = async_mock.MagicMock( - serialize=async_mock.MagicMock(return_value={"...": "..."}), + serialize=async_mock.MagicMock(), state=TransactionRecord.STATE_TRANSACTION_ENDORSED, messages_attach=[ {"data": {"json": json.dumps({"message": "attached"})}} ], ) await test_module.transaction_write(self.request) - mock_response.assert_called_once_with({"...": "..."}) async def test_transaction_write_not_found_x(self): @@ -1510,102 +1498,15 @@ async def test_transaction_write_base_model_x(self): with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.transaction_write(self.request) - async def test_transaction_write_no_jobs_x(self): - self.request.match_info = {"tran_id": "dummy"} - - with async_mock.patch.object( - ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_conn_rec_retrieve, async_mock.patch.object( - TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_txn_rec_retrieve: - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( - metadata_get=async_mock.CoroutineMock(return_value=None) - ) - mock_txn_rec_retrieve.return_value = async_mock.MagicMock( - serialize=async_mock.MagicMock(return_value={"...": "..."}) - ) - - with self.assertRaises(test_module.web.HTTPForbidden): - await test_module.transaction_write(self.request) - - async def test_transaction_write_my_wrong_job_x(self): - self.request.match_info = {"tran_id": "dummy"} - - with async_mock.patch.object( - ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_conn_rec_retrieve, async_mock.patch.object( - TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_txn_rec_retrieve: - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( - metadata_get=async_mock.CoroutineMock( - return_value={ - "transaction_their_job": ( - test_module.TransactionJob.TRANSACTION_ENDORSER.name - ), - "transaction_my_job": "a suffusion of yellow", - } - ) - ) - mock_txn_rec_retrieve.return_value = async_mock.MagicMock( - serialize=async_mock.MagicMock(return_value={"...": "..."}) - ) - - with self.assertRaises(test_module.web.HTTPForbidden): - await test_module.transaction_write(self.request) - async def test_transaction_write_wrong_state_x(self): self.request.match_info = {"tran_id": "dummy"} with async_mock.patch.object( - ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_conn_rec_retrieve, async_mock.patch.object( TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() ) as mock_txn_rec_retrieve: - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( - metadata_get=async_mock.CoroutineMock( - return_value={ - "transaction_my_job": ( - test_module.TransactionJob.TRANSACTION_AUTHOR.name - ), - "transaction_their_job": ( - test_module.TransactionJob.TRANSACTION_ENDORSER.name - ), - } - ) - ) - mock_txn_rec_retrieve.return_value = async_mock.MagicMock( - serialize=async_mock.MagicMock(return_value={"...": "..."}), - state=TransactionRecord.STATE_TRANSACTION_CREATED, - messages_attach=[ - {"data": {"json": json.dumps({"message": "attached"})}} - ], - ) - - with self.assertRaises(test_module.web.HTTPForbidden): - await test_module.transaction_write(self.request) - async def test_transaction_write_no_ledger_x(self): - self.request.match_info = {"tran_id": "dummy"} - self.context.injector.clear_binding(BaseLedger) - with async_mock.patch.object( - ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_conn_rec_retrieve, async_mock.patch.object( - TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_txn_rec_retrieve: - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( - metadata_get=async_mock.CoroutineMock( - return_value={ - "transaction_my_job": ( - test_module.TransactionJob.TRANSACTION_AUTHOR.name - ), - "transaction_their_job": ( - test_module.TransactionJob.TRANSACTION_ENDORSER.name - ), - } - ) - ) mock_txn_rec_retrieve.return_value = async_mock.MagicMock( serialize=async_mock.MagicMock(return_value={"...": "..."}), - state=TransactionRecord.STATE_TRANSACTION_ENDORSED, + state=TransactionRecord.STATE_TRANSACTION_CREATED, messages_attach=[ {"data": {"json": json.dumps({"message": "attached"})}} ], @@ -1614,146 +1515,9 @@ async def test_transaction_write_no_ledger_x(self): with self.assertRaises(test_module.web.HTTPForbidden): await test_module.transaction_write(self.request) - async def test_transaction_write_ledger_txn_submit_x(self): - self.request.match_info = {"tran_id": "dummy"} - self.ledger.txn_submit = async_mock.CoroutineMock( - side_effect=test_module.LedgerError() - ) - with async_mock.patch.object( - ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_conn_rec_retrieve, async_mock.patch.object( - TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_txn_rec_retrieve: - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( - metadata_get=async_mock.CoroutineMock( - return_value={ - "transaction_my_job": ( - test_module.TransactionJob.TRANSACTION_AUTHOR.name - ), - "transaction_their_job": ( - test_module.TransactionJob.TRANSACTION_ENDORSER.name - ), - } - ) - ) - mock_txn_rec_retrieve.return_value = async_mock.MagicMock( - serialize=async_mock.MagicMock(return_value={"...": "..."}), - state=TransactionRecord.STATE_TRANSACTION_ENDORSED, - messages_attach=[ - {"data": {"json": json.dumps({"message": "attached"})}} - ], - ) - - with self.assertRaises(test_module.web.HTTPBadRequest): - await test_module.transaction_write(self.request) - - async def test_transaction_write_cred_def_txn(self): - self.request.match_info = {"tran_id": "dummy"} - self.ledger.txn_submit = async_mock.CoroutineMock( - return_value=json.dumps( - { - "result": { - "txn": { - "type": "102", - "metadata": {"from": TEST_DID}, - "data": {"ref": 1000}, - }, - "txnMetadata": {"txnId": SCHEMA_ID}, - } - } - ) - ) - with async_mock.patch.object( - ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_conn_rec_retrieve, async_mock.patch.object( - TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_txn_rec_retrieve, async_mock.patch.object( - test_module, "TransactionManager", async_mock.MagicMock() - ) as mock_txn_mgr, async_mock.patch.object( - test_module.web, "json_response" - ) as mock_response: - mock_txn_mgr.return_value = async_mock.MagicMock( - complete_transaction=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # txn record - serialize=async_mock.MagicMock(return_value={"...": "..."}) - ) - ) - ) - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( - metadata_get=async_mock.CoroutineMock( - return_value={ - "transaction_my_job": ( - test_module.TransactionJob.TRANSACTION_AUTHOR.name - ), - "transaction_their_job": ( - test_module.TransactionJob.TRANSACTION_ENDORSER.name - ), - } - ) - ) - mock_txn_rec_retrieve.return_value = async_mock.MagicMock( - serialize=async_mock.MagicMock(return_value={"...": "..."}), - state=TransactionRecord.STATE_TRANSACTION_ENDORSED, - messages_attach=[ - {"data": {"json": json.dumps({"message": "attached"})}} - ], - ) - await test_module.transaction_write(self.request) - - mock_response.assert_called_once_with({"...": "..."}) - - async def test_transaction_write_ledger_cred_def_txn_ledger_get_schema_x(self): - self.request.match_info = {"tran_id": "dummy"} - self.ledger.txn_submit = async_mock.CoroutineMock( - return_value=json.dumps( - { - "result": { - "txn": { - "type": "102", - "metadata": {"from": TEST_DID}, - "data": {"ref": 1000}, - }, - "txnMetadata": {"txnId": SCHEMA_ID}, - } - } - ) - ) - self.ledger.get_schema = async_mock.CoroutineMock( - side_effect=test_module.LedgerError() - ) - with async_mock.patch.object( - ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_conn_rec_retrieve, async_mock.patch.object( - TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_txn_rec_retrieve: - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( - metadata_get=async_mock.CoroutineMock( - return_value={ - "transaction_my_job": ( - test_module.TransactionJob.TRANSACTION_AUTHOR.name - ), - "transaction_their_job": ( - test_module.TransactionJob.TRANSACTION_ENDORSER.name - ), - } - ) - ) - mock_txn_rec_retrieve.return_value = async_mock.MagicMock( - serialize=async_mock.MagicMock(return_value={"...": "..."}), - state=TransactionRecord.STATE_TRANSACTION_ENDORSED, - messages_attach=[ - {"data": {"json": json.dumps({"message": "attached"})}} - ], - ) - - with self.assertRaises(test_module.web.HTTPBadRequest): - await test_module.transaction_write(self.request) - async def test_transaction_write_schema_txn_complete_x(self): self.request.match_info = {"tran_id": "dummy"} with async_mock.patch.object( - ConnRecord, "retrieve_by_id", async_mock.CoroutineMock() - ) as mock_conn_rec_retrieve, async_mock.patch.object( TransactionRecord, "retrieve_by_id", async_mock.CoroutineMock() ) as mock_txn_rec_retrieve, async_mock.patch.object( test_module, "TransactionManager", async_mock.MagicMock() @@ -1763,18 +1527,7 @@ async def test_transaction_write_schema_txn_complete_x(self): side_effect=test_module.StorageError() ) ) - mock_conn_rec_retrieve.return_value = async_mock.MagicMock( - metadata_get=async_mock.CoroutineMock( - return_value={ - "transaction_my_job": ( - test_module.TransactionJob.TRANSACTION_AUTHOR.name - ), - "transaction_their_job": ( - test_module.TransactionJob.TRANSACTION_ENDORSER.name - ), - } - ) - ) + mock_txn_rec_retrieve.return_value = async_mock.MagicMock( serialize=async_mock.MagicMock(return_value={"...": "..."}), state=TransactionRecord.STATE_TRANSACTION_ENDORSED, diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index 7cd035ceef..92c2265700 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -1,6 +1,4 @@ -from behave import given, when, then import json -from time import sleep import time from bdd_support.agent_backchannel_client import ( @@ -11,7 +9,9 @@ async_sleep, read_json_data, ) +from behave import given, when, then from runners.agent_container import AgentContainer +from time import sleep # This step is defined in another feature file @@ -160,7 +160,7 @@ def step_impl(context, agent_name): agent["agent"], "/transactions/" + txn_id + "/write" ) - assert written_txn["state"] == "transaction_completed" + assert written_txn["state"] == "transaction_acked" @then('"{agent_name}" has written the schema {schema_name} to the ledger')