diff --git a/src/agent/Dispatcher.ts b/src/agent/Dispatcher.ts index edf5cfe05e..9d611f46c2 100644 --- a/src/agent/Dispatcher.ts +++ b/src/agent/Dispatcher.ts @@ -43,9 +43,14 @@ class Dispatcher { outboundMessage.payload.setReturnRouting(ReturnRouteTypes.all) } - // check for return routing, with thread id + // Check for return routing, with thread id if (message.hasReturnRouting(threadId)) { - return await this.messageSender.packMessage(outboundMessage) + const keys = { + recipientKeys: messageContext.senderVerkey ? [messageContext.senderVerkey] : [], + routingKeys: [], + senderKey: messageContext.connection?.verkey || null, + } + return await this.messageSender.packMessage(outboundMessage, keys) } await this.messageSender.sendMessage(outboundMessage) diff --git a/src/agent/EnvelopeService.ts b/src/agent/EnvelopeService.ts index 65c7ff0d6b..2d937ecb6e 100644 --- a/src/agent/EnvelopeService.ts +++ b/src/agent/EnvelopeService.ts @@ -1,5 +1,7 @@ import type { Logger } from '../logger' -import type { OutboundMessage, UnpackedMessageContext } from '../types' +import type { UnpackedMessageContext } from '../types' +import type { AgentMessage } from './AgentMessage' +import type { Verkey } from 'indy-sdk' import { inject, scoped, Lifecycle } from 'tsyringe' @@ -9,6 +11,12 @@ import { Wallet } from '../wallet/Wallet' import { AgentConfig } from './AgentConfig' +export interface EnvelopeKeys { + recipientKeys: Verkey[] + routingKeys: Verkey[] + senderKey: Verkey | null +} + @scoped(Lifecycle.ContainerScoped) class EnvelopeService { private wallet: Wallet @@ -19,10 +27,12 @@ class EnvelopeService { this.logger = agentConfig.logger } - public async packMessage(outboundMessage: OutboundMessage): Promise { - const { routingKeys, recipientKeys, senderVk, payload } = outboundMessage + public async packMessage(payload: AgentMessage, keys: EnvelopeKeys): Promise { + const { routingKeys, recipientKeys, senderKey: senderVk } = keys const message = payload.toJSON() + this.logger.debug('Pack outbound message', { message }) + let wireMessage = await this.wallet.pack(message, recipientKeys, senderVk) if (routingKeys && routingKeys.length > 0) { diff --git a/src/agent/MessageSender.ts b/src/agent/MessageSender.ts index ececd5f2e6..809cd97555 100644 --- a/src/agent/MessageSender.ts +++ b/src/agent/MessageSender.ts @@ -1,5 +1,6 @@ import type { OutboundTransporter } from '../transport/OutboundTransporter' import type { OutboundMessage, OutboundPackage } from '../types' +import type { EnvelopeKeys } from './EnvelopeService' import { inject, Lifecycle, scoped } from 'tsyringe' @@ -35,23 +36,54 @@ export class MessageSender { return this._outboundTransporter } - public async packMessage(outboundMessage: OutboundMessage): Promise { + public async packMessage(outboundMessage: OutboundMessage, keys: EnvelopeKeys): Promise { const { connection, payload } = outboundMessage - const { verkey, theirKey } = connection - const endpoint = this.transportService.findEndpoint(connection) - const message = payload.toJSON() - this.logger.debug('outboundMessage', { verkey, theirKey, message }) - const responseRequested = outboundMessage.payload.hasReturnRouting() - const wireMessage = await this.envelopeService.packMessage(outboundMessage) - return { connection, payload: wireMessage, endpoint, responseRequested } + const wireMessage = await this.envelopeService.packMessage(payload, keys) + return { connection, payload: wireMessage } } public async sendMessage(outboundMessage: OutboundMessage): Promise { if (!this.outboundTransporter) { throw new AriesFrameworkError('Agent has no outbound transporter!') } - const outboundPackage = await this.packMessage(outboundMessage) - outboundPackage.session = this.transportService.findSession(outboundMessage.connection.id) - await this.outboundTransporter.sendMessage(outboundPackage) + + const { connection, payload } = outboundMessage + const { id, verkey, theirKey } = connection + const message = payload.toJSON() + this.logger.debug('Send outbound message', { + messageId: message.id, + connection: { id, verkey, theirKey }, + }) + + const services = this.transportService.findDidCommServices(connection) + if (services.length === 0) { + throw new AriesFrameworkError(`Connection with id ${connection.id} has no service!`) + } + + for await (const service of services) { + this.logger.debug(`Sending outbound message to service:`, { messageId: message.id, service }) + try { + const keys = { + recipientKeys: service.recipientKeys, + routingKeys: service.routingKeys || [], + senderKey: connection.verkey, + } + const outboundPackage = await this.packMessage(outboundMessage, keys) + outboundPackage.session = this.transportService.findSession(connection.id) + outboundPackage.endpoint = service.serviceEndpoint + outboundPackage.responseRequested = outboundMessage.payload.hasReturnRouting() + + await this.outboundTransporter.sendMessage(outboundPackage) + break + } catch (error) { + this.logger.debug( + `Sending outbound message to service with id ${service.id} failed with the following error:`, + { + message: error.message, + error: error, + } + ) + } + } } } diff --git a/src/agent/TransportService.ts b/src/agent/TransportService.ts index 43322fe30c..980f34b4a2 100644 --- a/src/agent/TransportService.ts +++ b/src/agent/TransportService.ts @@ -3,9 +3,8 @@ import type { ConnectionRecord } from '../modules/connections/repository' import { Lifecycle, scoped, inject } from 'tsyringe' import { DID_COMM_TRANSPORT_QUEUE, InjectionSymbols } from '../constants' -import { AriesFrameworkError } from '../error' import { Logger } from '../logger' -import { ConnectionRole } from '../modules/connections/models' +import { ConnectionRole, DidCommService } from '../modules/connections/models' @scoped(Lifecycle.ContainerScoped) export class TransportService { @@ -28,23 +27,24 @@ export class TransportService { return this.transportSessionTable[connectionId] } - public findEndpoint(connection: ConnectionRecord) { + public findDidCommServices(connection: ConnectionRecord): DidCommService[] { if (connection.theirDidDoc) { - const endpoint = connection.theirDidDoc.didCommServices[0].serviceEndpoint - if (endpoint) { - this.logger.debug(`Taking service endpoint ${endpoint} from their DidDoc`) - return endpoint - } + return connection.theirDidDoc.didCommServices } if (connection.role === ConnectionRole.Invitee && connection.invitation) { - const endpoint = connection.invitation.serviceEndpoint - if (endpoint) { - this.logger.debug(`Taking service endpoint ${endpoint} from invitation`) - return endpoint + const { invitation } = connection + if (invitation.serviceEndpoint) { + const service = new DidCommService({ + id: `${connection.id}-invitation`, + serviceEndpoint: invitation.serviceEndpoint, + recipientKeys: invitation.recipientKeys || [], + routingKeys: invitation.routingKeys || [], + }) + return [service] } } - throw new AriesFrameworkError(`No endpoint found for connection with id ${connection.id}`) + return [] } } diff --git a/src/agent/__tests__/MessageSender.test.ts b/src/agent/__tests__/MessageSender.test.ts index 5188578ca0..a48cbcacec 100644 --- a/src/agent/__tests__/MessageSender.test.ts +++ b/src/agent/__tests__/MessageSender.test.ts @@ -1,10 +1,12 @@ import type { ConnectionRecord } from '../../modules/connections' import type { OutboundTransporter } from '../../transport' +import type { OutboundMessage } from '../../types' import type { TransportSession } from '../TransportService' import { getMockConnection, mockFunction } from '../../__tests__/helpers' import testLogger from '../../__tests__/logger' import { ReturnRouteTypes } from '../../decorators/transport/TransportDecorator' +import { DidCommService } from '../../modules/connections' import { AgentMessage } from '../AgentMessage' import { EnvelopeService as EnvelopeServiceImpl } from '../EnvelopeService' import { MessageSender } from '../MessageSender' @@ -51,50 +53,111 @@ describe('MessageSender', () => { const enveloperService = new EnvelopeService() const envelopeServicePackMessageMock = mockFunction(enveloperService.packMessage) - envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(wireMessage)) const transportService = new TransportService() const session = new DummyTransportSession() const transportServiceFindSessionMock = mockFunction(transportService.findSession) - transportServiceFindSessionMock.mockReturnValue(session) - const endpoint = 'https://www.exampleEndpoint.com' - const transportServiceFindEndpointMock = mockFunction(transportService.findEndpoint) - transportServiceFindEndpointMock.mockReturnValue(endpoint) + const firstDidCommService = new DidCommService({ + id: `;indy`, + serviceEndpoint: 'https://www.first-endpoint.com', + recipientKeys: ['verkey'], + }) + const secondDidCommService = new DidCommService({ + id: `;indy`, + serviceEndpoint: 'https://www.second-endpoint.com', + recipientKeys: ['verkey'], + }) + const transportServiceFindServicesMock = mockFunction(transportService.findDidCommServices) let messageSender: MessageSender let outboundTransporter: OutboundTransporter let connection: ConnectionRecord + let outboundMessage: OutboundMessage describe('sendMessage', () => { beforeEach(() => { outboundTransporter = new DummyOutboundTransporter() messageSender = new MessageSender(enveloperService, transportService, logger) connection = getMockConnection({ id: 'test-123' }) + + outboundMessage = createOutboundMessage(connection, new AgentMessage()) + + envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(wireMessage)) + transportServiceFindServicesMock.mockReturnValue([firstDidCommService, secondDidCommService]) + transportServiceFindSessionMock.mockReturnValue(session) + }) + + afterEach(() => { + jest.resetAllMocks() }) test('throws error when there is no outbound transport', async () => { - const message = new AgentMessage() - const outboundMessage = createOutboundMessage(connection, message) await expect(messageSender.sendMessage(outboundMessage)).rejects.toThrow(`Agent has no outbound transporter!`) }) - test('calls transporter with connection, payload and endpoint', async () => { - const message = new AgentMessage() - const spy = jest.spyOn(outboundTransporter, 'sendMessage') - const outboundMessage = createOutboundMessage(connection, message) + test('throws error when there is no service', async () => { + messageSender.setOutboundTransporter(outboundTransporter) + transportServiceFindServicesMock.mockReturnValue([]) + + await expect(messageSender.sendMessage(outboundMessage)).rejects.toThrow( + `Connection with id test-123 has no service!` + ) + }) + + test('calls send message with connection, payload and endpoint from first DidComm service', async () => { messageSender.setOutboundTransporter(outboundTransporter) + const sendMessageSpy = jest.spyOn(outboundTransporter, 'sendMessage') + + await messageSender.sendMessage(outboundMessage) + + expect(sendMessageSpy).toHaveBeenCalledWith({ + connection, + payload: wireMessage, + endpoint: firstDidCommService.serviceEndpoint, + responseRequested: false, + session, + }) + expect(sendMessageSpy).toHaveBeenCalledTimes(1) + }) + + test('calls send message with connection, payload and endpoint from second DidComm service when the first fails', async () => { + messageSender.setOutboundTransporter(outboundTransporter) + const sendMessageSpy = jest.spyOn(outboundTransporter, 'sendMessage') + + // Simulate the case when the first call fails + sendMessageSpy.mockRejectedValueOnce(new Error()) await messageSender.sendMessage(outboundMessage) - const [[sendMessageCall]] = spy.mock.calls - expect(sendMessageCall).toEqual({ + expect(sendMessageSpy).toHaveBeenNthCalledWith(2, { connection, payload: wireMessage, - endpoint, + endpoint: secondDidCommService.serviceEndpoint, responseRequested: false, session, }) + expect(sendMessageSpy).toHaveBeenCalledTimes(2) + }) + + test('calls send message with responseRequested when message has return route', async () => { + messageSender.setOutboundTransporter(outboundTransporter) + const sendMessageSpy = jest.spyOn(outboundTransporter, 'sendMessage') + + const message = new AgentMessage() + message.setReturnRouting(ReturnRouteTypes.all) + const outboundMessage = createOutboundMessage(connection, message) + + await messageSender.sendMessage(outboundMessage) + + expect(sendMessageSpy).toHaveBeenCalledWith({ + connection, + payload: wireMessage, + endpoint: firstDidCommService.serviceEndpoint, + responseRequested: true, + session, + }) + expect(sendMessageSpy).toHaveBeenCalledTimes(1) }) }) @@ -103,30 +166,29 @@ describe('MessageSender', () => { outboundTransporter = new DummyOutboundTransporter() messageSender = new MessageSender(enveloperService, transportService, logger) connection = getMockConnection({ id: 'test-123' }) + + envelopeServicePackMessageMock.mockReturnValue(Promise.resolve(wireMessage)) + }) + + afterEach(() => { + jest.resetAllMocks() }) test('returns outbound message context with connection, payload and endpoint', async () => { const message = new AgentMessage() const outboundMessage = createOutboundMessage(connection, message) - const result = await messageSender.packMessage(outboundMessage) + const keys = { + recipientKeys: ['service.recipientKeys'], + routingKeys: [], + senderKey: connection.verkey, + } + const result = await messageSender.packMessage(outboundMessage, keys) expect(result).toEqual({ connection, payload: wireMessage, - endpoint, - responseRequested: false, }) }) - - test('when message has return route returns outbound message context with responseRequested', async () => { - const message = new AgentMessage() - message.setReturnRouting(ReturnRouteTypes.all) - const outboundMessage = createOutboundMessage(connection, message) - - const result = await messageSender.packMessage(outboundMessage) - - expect(result.responseRequested).toEqual(true) - }) }) }) diff --git a/src/agent/__tests__/TransportService.test.ts b/src/agent/__tests__/TransportService.test.ts index 541fb4fe87..22e3d536f2 100644 --- a/src/agent/__tests__/TransportService.test.ts +++ b/src/agent/__tests__/TransportService.test.ts @@ -1,56 +1,50 @@ import { getMockConnection } from '../../__tests__/helpers' import testLogger from '../../__tests__/logger' -import { ConnectionInvitationMessage, ConnectionRole, DidDoc, IndyAgentService } from '../../modules/connections' +import { ConnectionInvitationMessage, ConnectionRole, DidCommService, DidDoc } from '../../modules/connections' import { TransportService } from '../TransportService' const logger = testLogger describe('TransportService', () => { - describe('findEndpoint', () => { + describe('findServices', () => { let transportService: TransportService let theirDidDoc: DidDoc + const testDidCommService = new DidCommService({ + id: `;indy`, + serviceEndpoint: 'https://example.com', + recipientKeys: ['verkey'], + }) beforeEach(() => { theirDidDoc = new DidDoc({ id: 'test-456', publicKey: [], authentication: [], - service: [ - new IndyAgentService({ - id: `;indy`, - serviceEndpoint: 'https://example.com', - recipientKeys: ['verkey'], - }), - ], + service: [testDidCommService], }) transportService = new TransportService(logger) }) - test(`throws error when there is no their DidDoc and role is ${ConnectionRole.Inviter}`, () => { + test(`returns empty array when there is no their DidDoc and role is ${ConnectionRole.Inviter}`, () => { const connection = getMockConnection({ id: 'test-123', role: ConnectionRole.Inviter }) connection.theirDidDoc = undefined - expect(() => transportService.findEndpoint(connection)).toThrow( - `No endpoint found for connection with id test-123` - ) + expect(transportService.findDidCommServices(connection)).toEqual([]) }) - test(`throws error when there is no their DidDoc, no invitation and role is ${ConnectionRole.Invitee}`, () => { + test(`returns empty array when there is no their DidDoc, no invitation and role is ${ConnectionRole.Invitee}`, () => { const connection = getMockConnection({ id: 'test-123', role: ConnectionRole.Invitee }) connection.theirDidDoc = undefined connection.invitation = undefined - expect(() => transportService.findEndpoint(connection)).toThrow( - `No endpoint found for connection with id test-123` - ) + expect(transportService.findDidCommServices(connection)).toEqual([]) }) - test(`returns endpoint from their DidDoc`, () => { - theirDidDoc.service[0].serviceEndpoint = 'ws://theirDidDocEndpoint.com' + test(`returns service from their DidDoc`, () => { const connection = getMockConnection({ id: 'test-123', theirDidDoc }) - expect(transportService.findEndpoint(connection)).toEqual('ws://theirDidDocEndpoint.com') + expect(transportService.findDidCommServices(connection)).toEqual([testDidCommService]) }) - test(`returns endpoint from invitation when there is no their DidDoc and role is ${ConnectionRole.Invitee}`, () => { + test(`returns service from invitation when there is no their DidDoc and role is ${ConnectionRole.Invitee}`, () => { const invitation = new ConnectionInvitationMessage({ label: 'test', recipientKeys: ['verkey'], @@ -58,7 +52,14 @@ describe('TransportService', () => { }) const connection = getMockConnection({ id: 'test-123', role: ConnectionRole.Invitee, invitation }) connection.theirDidDoc = undefined - expect(transportService.findEndpoint(connection)).toEqual('ws://invitationEndpoint.com') + expect(transportService.findDidCommServices(connection)).toEqual([ + new DidCommService({ + id: 'test-123-invitation', + serviceEndpoint: 'ws://invitationEndpoint.com', + routingKeys: [], + recipientKeys: ['verkey'], + }), + ]) }) }) }) diff --git a/src/agent/helpers.ts b/src/agent/helpers.ts index de8c5ea15a..46675c0b90 100644 --- a/src/agent/helpers.ts +++ b/src/agent/helpers.ts @@ -1,39 +1,13 @@ -import type { ConnectionRecord, ConnectionInvitationMessage } from '../modules/connections' +import type { ConnectionRecord } from '../modules/connections' import type { OutboundMessage } from '../types' import type { AgentMessage } from './AgentMessage' -import { AriesFrameworkError } from '../error' - export function createOutboundMessage( connection: ConnectionRecord, - payload: T, - invitation?: ConnectionInvitationMessage + payload: T ): OutboundMessage { - if (invitation) { - // TODO: invitation recipientKeys, routingKeys, endpoint could be missing - // When invitation uses DID - return { - connection, - payload, - recipientKeys: invitation.recipientKeys || [], - routingKeys: invitation.routingKeys || [], - senderVk: connection.verkey, - } - } - - const { theirDidDoc } = connection - - if (!theirDidDoc) { - throw new AriesFrameworkError(`DidDoc for connection with verkey ${connection.verkey} not found!`) - } - - const [service] = theirDidDoc.didCommServices - return { connection, payload, - recipientKeys: service.recipientKeys, - routingKeys: service.routingKeys ?? [], - senderVk: connection.verkey, } } diff --git a/src/modules/connections/ConnectionsModule.ts b/src/modules/connections/ConnectionsModule.ts index dc481156c6..246d4a7ddb 100644 --- a/src/modules/connections/ConnectionsModule.ts +++ b/src/modules/connections/ConnectionsModule.ts @@ -127,7 +127,7 @@ export class ConnectionsModule { await this.consumerRoutingService.createRoute(connectionRecord.verkey) } - const outbound = createOutboundMessage(connectionRecord, message, connectionRecord.invitation) + const outbound = createOutboundMessage(connectionRecord, message) await this.messageSender.sendMessage(outbound) return connectionRecord diff --git a/src/modules/routing/RoutingModule.ts b/src/modules/routing/RoutingModule.ts index adf7601301..478fea22cd 100644 --- a/src/modules/routing/RoutingModule.ts +++ b/src/modules/routing/RoutingModule.ts @@ -65,7 +65,7 @@ export class RoutingModule { const connectionRecord = await this.connectionService.processInvitation(mediatorInvitation, { alias }) const { message: connectionRequest } = await this.connectionService.createRequest(connectionRecord.id) - const outboundMessage = createOutboundMessage(connectionRecord, connectionRequest, connectionRecord.invitation) + const outboundMessage = createOutboundMessage(connectionRecord, connectionRequest) outboundMessage.payload.setReturnRouting(ReturnRouteTypes.all) await this.messageSender.sendMessage(outboundMessage) diff --git a/src/types.ts b/src/types.ts index 6be93cfd97..b98287fec7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,11 +49,7 @@ export interface UnpackedMessageContext { export interface OutboundMessage { connection: ConnectionRecord - endpoint?: string payload: T - recipientKeys: Verkey[] - routingKeys: Verkey[] - senderVk: Verkey | null } export interface OutboundPackage {