diff --git a/demo/src/Listener.ts b/demo/src/Listener.ts index 4a4c7e383a..3b98345569 100644 --- a/demo/src/Listener.ts +++ b/demo/src/Listener.ts @@ -4,7 +4,6 @@ import type { Faber } from './Faber' import type { FaberInquirer } from './FaberInquirer' import type { Agent, - BasicMessageStateChangedEvent, CredentialExchangeRecord, CredentialStateChangedEvent, TrustPingReceivedEvent, @@ -13,12 +12,15 @@ import type { V2TrustPingResponseReceivedEvent, ProofExchangeRecord, ProofStateChangedEvent, + AgentMessageProcessedEvent, + AgentBaseMessage, } from '@aries-framework/core' import type BottomBar from 'inquirer/lib/ui/bottom-bar' import { - BasicMessageEventTypes, - BasicMessageRole, + V1BasicMessage, + V2BasicMessage, + AgentEventTypes, CredentialEventTypes, CredentialState, ProofEventTypes, @@ -76,9 +78,14 @@ export class Listener { } public messageListener(agent: Agent, name: string) { - agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, async (event: BasicMessageStateChangedEvent) => { - if (event.payload.basicMessageRecord.role === BasicMessageRole.Receiver) { - this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${event.payload.message.content}\n`)) + const isBasicMessage = (message: AgentBaseMessage): message is V1BasicMessage | V2BasicMessage => + [V1BasicMessage.type.messageTypeUri, V2BasicMessage.type.messageTypeUri].includes(message.type) + + agent.events.on(AgentEventTypes.AgentMessageProcessed, async (event: AgentMessageProcessedEvent) => { + const message = event.payload.message + + if (isBasicMessage(message)) { + this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${message.content}\n`)) } }) } diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index 39ea8d521d..bf57ad5fd6 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -2,6 +2,7 @@ import type { AgentConfig } from './AgentConfig' import type { AgentApi, CustomOrDefaultApi, EmptyModuleMap, ModulesMap, WithoutDefaultModules } from './AgentModules' import type { TransportSession } from './TransportService' import type { Logger } from '../logger' +import type { BasicMessagesModule } from '../modules/basic-messages' import type { CredentialsModule } from '../modules/credentials' import type { MessagePickupModule } from '../modules/message-pìckup' import type { ProofsModule } from '../modules/proofs' @@ -51,7 +52,7 @@ export abstract class BaseAgent - public readonly basicMessages: BasicMessagesApi + public readonly basicMessages: CustomOrDefaultApi public readonly genericRecords: GenericRecordsApi public readonly discovery: DiscoverFeaturesApi public readonly dids: DidsApi @@ -99,7 +100,10 @@ export abstract class BaseAgent - this.basicMessages = this.dependencyManager.resolve(BasicMessagesApi) + this.basicMessages = this.dependencyManager.resolve(BasicMessagesApi) as CustomOrDefaultApi< + AgentModules['basicMessages'], + BasicMessagesModule + > this.genericRecords = this.dependencyManager.resolve(GenericRecordsApi) this.discovery = this.dependencyManager.resolve(DiscoverFeaturesApi) this.dids = this.dependencyManager.resolve(DidsApi) diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index ed3154f8fd..dfbf4700bd 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -5,7 +5,7 @@ import { injectable } from 'tsyringe' import { getIndySdkModules } from '../../../../indy-sdk/tests/setupIndySdkModule' import { getAgentOptions } from '../../../tests/helpers' import { InjectionSymbols } from '../../constants' -import { BasicMessageRepository, BasicMessageService } from '../../modules/basic-messages' +import { BasicMessageRepository } from '../../modules/basic-messages' import { BasicMessagesApi } from '../../modules/basic-messages/BasicMessagesApi' import { ConnectionsApi } from '../../modules/connections/ConnectionsApi' import { V1TrustPingService } from '../../modules/connections/protocols/trust-ping/v1/V1TrustPingService' @@ -167,7 +167,6 @@ describe('Agent', () => { expect(container.resolve(CredentialRepository)).toBeInstanceOf(CredentialRepository) expect(container.resolve(BasicMessagesApi)).toBeInstanceOf(BasicMessagesApi) - expect(container.resolve(BasicMessageService)).toBeInstanceOf(BasicMessageService) expect(container.resolve(BasicMessageRepository)).toBeInstanceOf(BasicMessageRepository) expect(container.resolve(MediatorApi)).toBeInstanceOf(MediatorApi) @@ -205,7 +204,6 @@ describe('Agent', () => { expect(container.resolve(CredentialRepository)).toBe(container.resolve(CredentialRepository)) expect(container.resolve(BasicMessagesApi)).toBe(container.resolve(BasicMessagesApi)) - expect(container.resolve(BasicMessageService)).toBe(container.resolve(BasicMessageService)) expect(container.resolve(BasicMessageRepository)).toBe(container.resolve(BasicMessageRepository)) expect(container.resolve(MediatorApi)).toBe(container.resolve(MediatorApi)) @@ -242,6 +240,7 @@ describe('Agent', () => { expect(protocols).toEqual( expect.arrayContaining([ 'https://didcomm.org/basicmessage/1.0', + 'https://didcomm.org/basicmessage/2.0', 'https://didcomm.org/connections/1.0', 'https://didcomm.org/coordinate-mediation/1.0', 'https://didcomm.org/issue-credential/2.0', @@ -256,6 +255,6 @@ describe('Agent', () => { 'https://didcomm.org/revocation_notification/2.0', ]) ) - expect(protocols.length).toEqual(13) + expect(protocols.length).toEqual(14) }) }) diff --git a/packages/core/src/agent/getOutboundMessageContext.ts b/packages/core/src/agent/getOutboundMessageContext.ts index ea57257da8..d6f4101b44 100644 --- a/packages/core/src/agent/getOutboundMessageContext.ts +++ b/packages/core/src/agent/getOutboundMessageContext.ts @@ -1,5 +1,5 @@ +import type { AgentBaseMessage } from './AgentBaseMessage' import type { AgentContext } from './context' -import type { DidCommV1Message } from '../didcomm/versions/v1' import type { ConnectionRecord, Routing } from '../modules/connections' import type { ResolvedDidCommService } from '../modules/didcomm' import type { OutOfBandRecord } from '../modules/oob' @@ -7,6 +7,7 @@ import type { BaseRecordAny } from '../storage/BaseRecord' import { Key } from '../crypto' import { ServiceDecorator } from '../decorators/service/ServiceDecorator' +import { DidCommV1Message, DidCommV2Message } from '../didcomm' import { AriesFrameworkError } from '../error' import { OutOfBandService, OutOfBandRole, OutOfBandRepository } from '../modules/oob' import { OutOfBandRecordMetadataKeys } from '../modules/oob/repository/outOfBandRecordMetadataTypes' @@ -37,9 +38,9 @@ export async function getOutboundMessageContext( }: { connectionRecord?: ConnectionRecord associatedRecord?: BaseRecordAny - message: DidCommV1Message - lastReceivedMessage?: DidCommV1Message - lastSentMessage?: DidCommV1Message + message: AgentBaseMessage + lastReceivedMessage?: AgentBaseMessage + lastSentMessage?: AgentBaseMessage } ) { // TODO: even if using a connection record, we should check if there's an oob record associated and this @@ -48,6 +49,17 @@ export async function getOutboundMessageContext( agentContext.config.logger.debug( `Creating outbound message context for message ${message.id} with connection ${connectionRecord.id}` ) + + // Attach 'from' and 'to' fields according to connection record (unless they are previously defined) + if (message instanceof DidCommV2Message) { + message.from = message.from ?? connectionRecord.did + const recipients = message.to ?? (connectionRecord.theirDid ? [connectionRecord.theirDid] : undefined) + if (!recipients) { + throw new AriesFrameworkError('Cannot find recipient did for message') + } + message.to = recipients + } + return new OutboundMessageContext(message, { agentContext, associatedRecord, @@ -67,6 +79,14 @@ export async function getOutboundMessageContext( ) } + if ( + !(message instanceof DidCommV1Message) || + (lastReceivedMessage !== undefined && !(lastReceivedMessage instanceof DidCommV1Message)) || + (lastSentMessage !== undefined && !(lastSentMessage instanceof DidCommV1Message)) + ) { + throw new AriesFrameworkError('No connection record associated with DIDComm V2 messages exchange') + } + // Connectionless return getConnectionlessOutboundMessageContext(agentContext, { message, diff --git a/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts b/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts index 5141e5a051..a7cad5f7a0 100644 --- a/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts +++ b/packages/core/src/decorators/thread/ThreadDecoratorExtension.ts @@ -21,6 +21,10 @@ export function ThreadDecorated(Base: return this.thread?.threadId ?? this.id } + public get parentThreadId(): string | undefined { + return this.thread?.parentThreadId + } + public setThread(options: Partial) { this.thread = new ThreadDecorator(options) } diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts index 4e17749e91..dd3927ca93 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts @@ -17,13 +17,22 @@ export type DidCommV2MessageParams = { to?: string | string[] thid?: string parentThreadId?: string + senderOrder?: number + receivedOrders?: { [key: string]: number } // TODO: Update to DIDComm V2 format createdTime?: number expiresTime?: number fromPrior?: string + language?: string attachments?: Array body?: unknown } +type DidCommV2ReceiverOrder = { + id: string + last: number + gaps: number[] +} + export class DidCommV2BaseMessage { @Matches(MessageIdRegExp) public id!: string @@ -63,11 +72,26 @@ export class DidCommV2BaseMessage { @IsOptional() public parentThreadId?: string + @Expose({ name: 'sender_order' }) + @IsNumber() + @IsOptional() + public senderOrder?: number + + @Expose({ name: 'received_orders' }) + @IsOptional() + @IsArray() + public receivedOrders?: DidCommV2ReceiverOrder[] + @Expose({ name: 'from_prior' }) @IsString() @IsOptional() public fromPrior?: string + @Expose({ name: 'lang' }) + @IsString() + @IsOptional() + public language?: string + public body!: unknown @IsOptional() @@ -86,6 +110,12 @@ export class DidCommV2BaseMessage { this.to = typeof options.to === 'string' ? [options.to] : options.to this.thid = options.thid this.parentThreadId = options.parentThreadId + this.senderOrder = options.senderOrder + this.receivedOrders = Object.entries(options.receivedOrders ?? {}).map(([id, last]) => ({ + id, + last, + gaps: [], + })) this.createdTime = options.createdTime this.expiresTime = options.expiresTime this.fromPrior = options.fromPrior diff --git a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts index a45f45da6f..1dea0dcfd4 100644 --- a/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts +++ b/packages/core/src/didcomm/versions/v2/DidCommV2Message.ts @@ -1,6 +1,7 @@ import type { PlaintextDidCommV2Message } from './types' import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' import type { ServiceDecorator } from '../../../decorators/service/ServiceDecorator' +import type { ThreadDecorator } from '../../../decorators/thread/ThreadDecorator' import type { PlaintextMessage } from '../../types' import { AriesFrameworkError } from '../../../error' @@ -23,7 +24,18 @@ export class DidCommV2Message extends DidCommV2BaseMessage implements AgentBaseM } public get threadId(): string | undefined { - return this.thid + return this.thid ?? this.id + } + + public setThread(options: Partial) { + this.thid = options.threadId + this.parentThreadId = options.parentThreadId + this.senderOrder = options.senderOrder + this.receivedOrders = Object.entries(options.receivedOrders ?? {}).map(([id, last]) => ({ + id, + last, + gaps: [], + })) } public hasAnyReturnRoute() { diff --git a/packages/core/src/modules/basic-messages/BasicMessageEvents.ts b/packages/core/src/modules/basic-messages/BasicMessageEvents.ts index f05873f5de..c02bf4832e 100644 --- a/packages/core/src/modules/basic-messages/BasicMessageEvents.ts +++ b/packages/core/src/modules/basic-messages/BasicMessageEvents.ts @@ -1,4 +1,4 @@ -import type { BasicMessage } from './messages' +import type { V1BasicMessage } from './protocols' import type { BasicMessageRecord } from './repository' import type { BaseEvent } from '../../agent/Events' @@ -8,7 +8,7 @@ export enum BasicMessageEventTypes { export interface BasicMessageStateChangedEvent extends BaseEvent { type: typeof BasicMessageEventTypes.BasicMessageStateChanged payload: { - message: BasicMessage + message: V1BasicMessage basicMessageRecord: BasicMessageRecord } } diff --git a/packages/core/src/modules/basic-messages/BasicMessagesApi.ts b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts index 82e94cf9e4..df1328b1f3 100644 --- a/packages/core/src/modules/basic-messages/BasicMessagesApi.ts +++ b/packages/core/src/modules/basic-messages/BasicMessagesApi.ts @@ -1,35 +1,59 @@ +import type { BasicMessageProtocol } from './protocols' import type { BasicMessageRecord } from './repository/BasicMessageRecord' import type { Query } from '../../storage/StorageService' import { AgentContext } from '../../agent' -import { MessageHandlerRegistry } from '../../agent/MessageHandlerRegistry' import { MessageSender } from '../../agent/MessageSender' -import { OutboundMessageContext } from '../../agent/models' +import { getOutboundMessageContext } from '../../agent/getOutboundMessageContext' +import { AriesFrameworkError } from '../../error' import { injectable } from '../../plugins' import { ConnectionService } from '../connections' -import { BasicMessageHandler } from './handlers' -import { BasicMessageService } from './services' +import { BasicMessagesModuleConfig } from './BasicMessagesModuleConfig' +import { BasicMessageRepository } from './repository' + +export interface BasicMessagesApi { + sendMessage(connectionId: string, message: string, parentThreadId?: string): Promise + + findAllByQuery(query: Query): Promise + getById(basicMessageRecordId: string): Promise + getByThreadId(threadId: string): Promise + deleteById(basicMessageRecordId: string): Promise +} @injectable() -export class BasicMessagesApi { - private basicMessageService: BasicMessageService +export class BasicMessagesApi implements BasicMessagesApi { + public readonly config: BasicMessagesModuleConfig + + private basicMessageRepository: BasicMessageRepository private messageSender: MessageSender private connectionService: ConnectionService private agentContext: AgentContext public constructor( - messageHandlerRegistry: MessageHandlerRegistry, - basicMessageService: BasicMessageService, + basicMessageRepository: BasicMessageRepository, messageSender: MessageSender, connectionService: ConnectionService, - agentContext: AgentContext + agentContext: AgentContext, + config: BasicMessagesModuleConfig ) { - this.basicMessageService = basicMessageService + this.basicMessageRepository = basicMessageRepository this.messageSender = messageSender this.connectionService = connectionService this.agentContext = agentContext - this.registerMessageHandlers(messageHandlerRegistry) + this.config = config + } + + private getProtocol(protocolVersion: PVT): BasicMessageProtocol { + const basicMessageProtocol = this.config.basicMessageProtocols.find( + (protocol) => protocol.version === protocolVersion + ) + + if (!basicMessageProtocol) { + throw new AriesFrameworkError(`No basic message protocol registered for protocol version ${protocolVersion}`) + } + + return basicMessageProtocol } /** @@ -44,15 +68,20 @@ export class BasicMessagesApi { public async sendMessage(connectionId: string, message: string, parentThreadId?: string) { const connection = await this.connectionService.getById(this.agentContext, connectionId) - const { message: basicMessage, record: basicMessageRecord } = await this.basicMessageService.createMessage( + // TODO: Parameterize in API + const basicMessageProtocol = this.getProtocol(connection.isDidCommV1Connection ? 'v1' : 'v2') + + const { message: basicMessage, record: basicMessageRecord } = await basicMessageProtocol.createMessage( this.agentContext, - message, - connection, - parentThreadId + { + content: message, + connectionRecord: connection, + parentThreadId, + } ) - const outboundMessageContext = new OutboundMessageContext(basicMessage, { - agentContext: this.agentContext, - connection, + const outboundMessageContext = await getOutboundMessageContext(this.agentContext, { + message: basicMessage, + connectionRecord: connection, associatedRecord: basicMessageRecord, }) @@ -67,7 +96,7 @@ export class BasicMessagesApi { * @returns array containing all matching records */ public async findAllByQuery(query: Query) { - return this.basicMessageService.findAllByQuery(this.agentContext, query) + return this.basicMessageRepository.findByQuery(this.agentContext, query) } /** @@ -79,7 +108,7 @@ export class BasicMessagesApi { * */ public async getById(basicMessageRecordId: string) { - return this.basicMessageService.getById(this.agentContext, basicMessageRecordId) + return this.basicMessageRepository.getById(this.agentContext, basicMessageRecordId) } /** @@ -90,8 +119,8 @@ export class BasicMessagesApi { * @throws {RecordDuplicateError} If multiple records are found * @returns The connection record */ - public async getByThreadId(basicMessageRecordId: string) { - return this.basicMessageService.getByThreadId(this.agentContext, basicMessageRecordId) + public async getByThreadId(threadId: string) { + return this.basicMessageRepository.getSingleByQuery(this.agentContext, { threadId }) } /** @@ -101,10 +130,6 @@ export class BasicMessagesApi { * @throws {RecordNotFoundError} If no record is found */ public async deleteById(basicMessageRecordId: string) { - await this.basicMessageService.deleteById(this.agentContext, basicMessageRecordId) - } - - private registerMessageHandlers(messageHandlerRegistry: MessageHandlerRegistry) { - messageHandlerRegistry.registerMessageHandler(new BasicMessageHandler(this.basicMessageService)) + await this.basicMessageRepository.deleteById(this.agentContext, basicMessageRecordId) } } diff --git a/packages/core/src/modules/basic-messages/BasicMessagesModule.ts b/packages/core/src/modules/basic-messages/BasicMessagesModule.ts index fd1fd77f6c..95a9bb1210 100644 --- a/packages/core/src/modules/basic-messages/BasicMessagesModule.ts +++ b/packages/core/src/modules/basic-messages/BasicMessagesModule.ts @@ -1,15 +1,42 @@ +import type { BasicMessagesModuleConfigOptions } from './BasicMessagesModuleConfig' +import type { BasicMessageProtocol } from './protocols/BasicMessageProtocol' import type { FeatureRegistry } from '../../agent/FeatureRegistry' -import type { DependencyManager, Module } from '../../plugins' +import type { ApiModule, DependencyManager } from '../../plugins' +import type { Optional } from '../../utils' +import type { Constructor } from '../../utils/mixins' -import { Protocol } from '../../agent/models' - -import { BasicMessageRole } from './BasicMessageRole' import { BasicMessagesApi } from './BasicMessagesApi' +import { BasicMessagesModuleConfig } from './BasicMessagesModuleConfig' +import { V1BasicMessageProtocol, V2BasicMessageProtocol } from './protocols' import { BasicMessageRepository } from './repository' -import { BasicMessageService } from './services' -export class BasicMessagesModule implements Module { - public readonly api = BasicMessagesApi +/** + * Default basicMessageProtocols that will be registered if the `basicMessageProtocols` property is not configured. + */ +export type DefaultBasicMessageProtocols = [] + +// BasicMessagesModuleOptions makes the credentialProtocols property optional from the config, as it will set it when not provided. +export type BasicMessagesModuleOptions = Optional< + BasicMessagesModuleConfigOptions, + 'basicMessageProtocols' +> + +export class BasicMessagesModule + implements ApiModule +{ + public readonly config: BasicMessagesModuleConfig + + public readonly api: Constructor> = BasicMessagesApi + + public constructor(config?: BasicMessagesModuleConfig) { + this.config = new BasicMessagesModuleConfig({ + ...config, + basicMessageProtocols: config?.basicMessageProtocols ?? [ + new V1BasicMessageProtocol(), + new V2BasicMessageProtocol(), + ], + } as BasicMessagesModuleConfig) + } /** * Registers the dependencies of the basic message module on the dependency manager. @@ -18,18 +45,15 @@ export class BasicMessagesModule implements Module { // Api dependencyManager.registerContextScoped(BasicMessagesApi) - // Services - dependencyManager.registerSingleton(BasicMessageService) + // Config + dependencyManager.registerInstance(BasicMessagesModuleConfig, this.config) // Repositories dependencyManager.registerSingleton(BasicMessageRepository) - // Features - featureRegistry.register( - new Protocol({ - id: 'https://didcomm.org/basicmessage/1.0', - roles: [BasicMessageRole.Sender, BasicMessageRole.Receiver], - }) - ) + // Protocol needs to register feature registry items and handlers + for (const basicMessageProtocols of this.config.basicMessageProtocols) { + basicMessageProtocols.register(dependencyManager, featureRegistry) + } } } diff --git a/packages/core/src/modules/basic-messages/BasicMessagesModuleConfig.ts b/packages/core/src/modules/basic-messages/BasicMessagesModuleConfig.ts new file mode 100644 index 0000000000..d87f2d9854 --- /dev/null +++ b/packages/core/src/modules/basic-messages/BasicMessagesModuleConfig.ts @@ -0,0 +1,32 @@ +import type { BasicMessageProtocol } from './protocols/BasicMessageProtocol' + +/** + * CredentialsModuleConfigOptions defines the interface for the options of the CredentialsModuleConfig class. + * This can contain optional parameters that have default values in the config class itself. + */ +export interface BasicMessagesModuleConfigOptions { + /** + * Protocols to make available to the module. + * + * When not provided, both `V1BasicMessageProtocol` and `V2BasicMessageProtocol` are registered by default. + * + * @default + * ``` + * [V1BasicMessageProtocol, V2BasicMessageProtocol] + * ``` + */ + basicMessageProtocols: BasicMessageProtocols +} + +export class BasicMessagesModuleConfig { + private options: BasicMessagesModuleConfigOptions + + public constructor(options: BasicMessagesModuleConfigOptions) { + this.options = options + } + + /** See {@link BasicMessagesModuleConfigOptions.basicMessageProtocols} */ + public get basicMessageProtocols() { + return this.options.basicMessageProtocols + } +} diff --git a/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts b/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts index 4a9f106810..8fd849e156 100644 --- a/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts +++ b/packages/core/src/modules/basic-messages/__tests__/BasicMessagesModule.test.ts @@ -2,8 +2,8 @@ import { FeatureRegistry } from '../../../agent/FeatureRegistry' import { DependencyManager } from '../../../plugins/DependencyManager' import { BasicMessagesApi } from '../BasicMessagesApi' import { BasicMessagesModule } from '../BasicMessagesModule' +import { BasicMessagesModuleConfig } from '../BasicMessagesModuleConfig' import { BasicMessageRepository } from '../repository' -import { BasicMessageService } from '../services' jest.mock('../../../plugins/DependencyManager') const DependencyManagerMock = DependencyManager as jest.Mock @@ -17,13 +17,16 @@ const featureRegistry = new FeatureRegistryMock() describe('BasicMessagesModule', () => { test('registers dependencies on the dependency manager', () => { - new BasicMessagesModule().register(dependencyManager, featureRegistry) + const module = new BasicMessagesModule() + module.register(dependencyManager, featureRegistry) expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(BasicMessagesApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(BasicMessageService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(BasicMessageRepository) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(BasicMessagesModuleConfig, module.config) }) }) diff --git a/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts b/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts index a18aa98754..2f14ca7353 100644 --- a/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts +++ b/packages/core/src/modules/basic-messages/__tests__/basic-messages.e2e.test.ts @@ -1,25 +1,29 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type { SubjectMessage } from '../../../../../../tests/transport/SubjectInboundTransport' import type { ConnectionRecord } from '../../../modules/connections' +import type { V2BasicMessage } from '../protocols' import { Subject } from 'rxjs' +import { describeRunInNodeVersion } from '../../../../../../tests/runInVersion' import { SubjectInboundTransport } from '../../../../../../tests/transport/SubjectInboundTransport' import { SubjectOutboundTransport } from '../../../../../../tests/transport/SubjectOutboundTransport' -import { getIndySdkModules } from '../../../../../indy-sdk/tests/setupIndySdkModule' +import { getAskarAnonCredsIndyModules } from '../../../../../anoncreds/tests/legacyAnonCredsSetup' import { getAgentOptions, makeConnection, waitForBasicMessage } from '../../../../tests/helpers' import testLogger from '../../../../tests/logger' import { Agent } from '../../../agent/Agent' import { MessageSendingError, RecordNotFoundError } from '../../../error' -import { BasicMessage } from '../messages' +import { OutOfBandVersion } from '../../oob' +import { V1BasicMessage } from '../protocols' import { BasicMessageRecord } from '../repository' const faberConfig = getAgentOptions( 'Faber Basic Messages', { endpoints: ['rxjs:faber'], + logger: testLogger, }, - getIndySdkModules() + getAskarAnonCredsIndyModules() ) const aliceConfig = getAgentOptions( @@ -27,21 +31,35 @@ const aliceConfig = getAgentOptions( { endpoints: ['rxjs:alice'], }, - getIndySdkModules() + getAskarAnonCredsIndyModules() ) -describe('Basic Messages E2E', () => { +const bobConfig = getAgentOptions( + 'Bob Basic Messages', + { + endpoints: ['rxjs:bob'], + logger: testLogger, + }, + getAskarAnonCredsIndyModules() +) + +describeRunInNodeVersion([18], 'Basic Messages E2E', () => { let faberAgent: Agent let aliceAgent: Agent + let bobAgent: Agent let faberConnection: ConnectionRecord let aliceConnection: ConnectionRecord + let bobConnection: ConnectionRecord + let faberBobConnection: ConnectionRecord beforeEach(async () => { const faberMessages = new Subject() const aliceMessages = new Subject() + const bobMessages = new Subject() const subjectMap = { 'rxjs:faber': faberMessages, 'rxjs:alice': aliceMessages, + 'rxjs:bob': bobMessages, } faberAgent = new Agent(faberConfig) @@ -54,6 +72,12 @@ describe('Basic Messages E2E', () => { aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) await aliceAgent.initialize() ;[aliceConnection, faberConnection] = await makeConnection(aliceAgent, faberAgent) + + bobAgent = new Agent(bobConfig) + bobAgent.registerInboundTransport(new SubjectInboundTransport(bobMessages)) + bobAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + await bobAgent.initialize() + ;[bobConnection, faberBobConnection] = await makeConnection(bobAgent, faberAgent, OutOfBandVersion.V2) }) afterEach(async () => { @@ -61,6 +85,8 @@ describe('Basic Messages E2E', () => { await faberAgent.wallet.delete() await aliceAgent.shutdown() await aliceAgent.wallet.delete() + await bobAgent.shutdown() + await bobAgent.wallet.delete() }) test('Alice and Faber exchange messages', async () => { @@ -105,7 +131,7 @@ describe('Basic Messages E2E', () => { content: 'How are you?', }) expect(replyMessage.content).toBe('How are you?') - expect(replyMessage.thread?.parentThreadId).toBe(helloMessage.id) + expect(replyMessage.parentThreadId).toBe(helloMessage.id) // Both sender and recipient shall be able to find the threaded messages // Hello message @@ -133,6 +159,59 @@ describe('Basic Messages E2E', () => { expect(faberReplyMessages[0]).toMatchObject(replyRecord) }) + test('Bob and Faber exchange messages using V2 protocol', async () => { + testLogger.test('Bob sends message to Faber') + const helloRecord = await bobAgent.basicMessages.sendMessage(bobConnection.id, 'Hello') + + expect(helloRecord.content).toBe('Hello') + + testLogger.test('Faber waits for message from Bob') + const helloMessage = await waitForBasicMessage(faberAgent, { + content: 'Hello', + }) + + testLogger.test('Faber sends message to Bob') + const replyRecord = await faberAgent.basicMessages.sendMessage( + faberBobConnection.id, + 'How are you?', + helloMessage.id + ) + expect(replyRecord.content).toBe('How are you?') + expect(replyRecord.parentThreadId).toBe(helloMessage.id) + + testLogger.test('Bob waits until he receives message from faber') + const replyMessage = (await waitForBasicMessage(bobAgent, { + content: 'How are you?', + })) as V2BasicMessage + expect(replyMessage.body.content).toBe('How are you?') + expect(replyMessage.parentThreadId).toBe(helloMessage.id) + + // Both sender and recipient shall be able to find the threaded messages + // Hello message + const bobHelloMessage = await bobAgent.basicMessages.getByThreadId(helloMessage.id) + const faberHelloMessage = await faberAgent.basicMessages.getByThreadId(helloMessage.id) + expect(bobHelloMessage).toMatchObject({ + content: helloRecord.content, + threadId: helloRecord.threadId, + }) + expect(faberHelloMessage).toMatchObject({ + content: helloRecord.content, + threadId: helloRecord.threadId, + }) + + // Reply message + const bobReplyMessages = await bobAgent.basicMessages.findAllByQuery({ parentThreadId: helloMessage.id }) + const faberReplyMessages = await faberAgent.basicMessages.findAllByQuery({ parentThreadId: helloMessage.id }) + expect(bobReplyMessages.length).toBe(1) + expect(bobReplyMessages[0]).toMatchObject({ + content: replyRecord.content, + parentThreadId: replyRecord.parentThreadId, + threadId: replyRecord.threadId, + }) + expect(faberReplyMessages.length).toBe(1) + expect(faberReplyMessages[0]).toMatchObject(replyRecord) + }) + test('Alice is unable to send a message', async () => { testLogger.test('Alice sends message to Faber that is undeliverable') @@ -151,8 +230,8 @@ describe('Basic Messages E2E', () => { testLogger.test('Error thrown includes the outbound message and recently created record id') expect(thrownError.outboundMessageContext.associatedRecord).toBeInstanceOf(BasicMessageRecord) - expect(thrownError.outboundMessageContext.message).toBeInstanceOf(BasicMessage) - expect((thrownError.outboundMessageContext.message as BasicMessage).content).toBe('Hello undeliverable') + expect(thrownError.outboundMessageContext.message).toBeInstanceOf(V1BasicMessage) + expect((thrownError.outboundMessageContext.message as V1BasicMessage).content).toBe('Hello undeliverable') testLogger.test('Created record can be found and deleted by id') const storedRecord = await aliceAgent.basicMessages.getById( diff --git a/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts b/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts deleted file mode 100644 index cec6931983..0000000000 --- a/packages/core/src/modules/basic-messages/handlers/BasicMessageHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { MessageHandler, MessageHandlerInboundMessage } from '../../../agent/MessageHandler' -import type { BasicMessageService } from '../services/BasicMessageService' - -import { BasicMessage } from '../messages' - -export class BasicMessageHandler implements MessageHandler { - private basicMessageService: BasicMessageService - public supportedMessages = [BasicMessage] - - public constructor(basicMessageService: BasicMessageService) { - this.basicMessageService = basicMessageService - } - - public async handle(messageContext: MessageHandlerInboundMessage) { - const connection = messageContext.assertReadyConnection() - await this.basicMessageService.save(messageContext, connection) - } -} diff --git a/packages/core/src/modules/basic-messages/handlers/index.ts b/packages/core/src/modules/basic-messages/handlers/index.ts deleted file mode 100644 index 64f421dd88..0000000000 --- a/packages/core/src/modules/basic-messages/handlers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BasicMessageHandler' diff --git a/packages/core/src/modules/basic-messages/index.ts b/packages/core/src/modules/basic-messages/index.ts index e0ca5207d1..123c503837 100644 --- a/packages/core/src/modules/basic-messages/index.ts +++ b/packages/core/src/modules/basic-messages/index.ts @@ -1,7 +1,8 @@ -export * from './messages' -export * from './services' +export * from './protocols' export * from './repository' export * from './BasicMessageEvents' export * from './BasicMessagesApi' export * from './BasicMessageRole' export * from './BasicMessagesModule' + +export { V1BasicMessage as BasicMessage } from './protocols' diff --git a/packages/core/src/modules/basic-messages/messages/index.ts b/packages/core/src/modules/basic-messages/messages/index.ts deleted file mode 100644 index 40d57b1840..0000000000 --- a/packages/core/src/modules/basic-messages/messages/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BasicMessage' diff --git a/packages/core/src/modules/basic-messages/protocols/BaseBasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/BaseBasicMessageProtocol.ts new file mode 100644 index 0000000000..5ddbc08d08 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/BaseBasicMessageProtocol.ts @@ -0,0 +1,56 @@ +import type { BasicMessageProtocol } from './BasicMessageProtocol' +import type { BasicMessageProtocolMsgReturnType, CreateMessageOptions } from './BasicMessageProtocolOptions' +import type { AgentContext } from '../../../agent' +import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { Query } from '../../../storage/StorageService' +import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' +import type { BasicMessageRecord } from '../repository' + +import { BasicMessageRepository } from '../repository' + +export abstract class BaseBasicMessageProtocol implements BasicMessageProtocol { + public abstract readonly version: string + + public abstract createMessage( + agentContext: AgentContext, + options: CreateMessageOptions + ): Promise> + + /** + * @todo use connection from message context + */ + public abstract save( + { message, agentContext }: InboundMessageContext, + connection: ConnectionRecord + ): Promise + + public async findAllByQuery(agentContext: AgentContext, query: Query) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.findByQuery(agentContext, query) + } + + public async getById(agentContext: AgentContext, basicMessageRecordId: string) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.getById(agentContext, basicMessageRecordId) + } + + public async getByThreadId(agentContext: AgentContext, threadId: string) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.getSingleByQuery(agentContext, { threadId }) + } + + public async findAllByParentThreadId(agentContext: AgentContext, parentThreadId: string) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.findByQuery(agentContext, { parentThreadId }) + } + + public async deleteById(agentContext: AgentContext, basicMessageRecordId: string) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + return basicMessageRepository.deleteById(agentContext, basicMessageRecordId) + } + + public abstract register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void +} diff --git a/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocol.ts new file mode 100644 index 0000000000..d34024e7b0 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocol.ts @@ -0,0 +1,20 @@ +import type { BasicMessageProtocolMsgReturnType, CreateMessageOptions } from './BasicMessageProtocolOptions' +import type { AgentContext } from '../../../agent' +import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' +import type { FeatureRegistry } from '../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../plugins' +import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' + +export interface BasicMessageProtocol { + readonly version: string + + createMessage( + agentContext: AgentContext, + options: CreateMessageOptions + ): Promise> + + save({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord): Promise + + register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry): void +} diff --git a/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocolOptions.ts b/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocolOptions.ts new file mode 100644 index 0000000000..b71d942346 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/BasicMessageProtocolOptions.ts @@ -0,0 +1,14 @@ +import type { AgentBaseMessage } from '../../../agent/AgentBaseMessage' +import type { ConnectionRecord } from '../../connections' +import type { BasicMessageRecord } from '../repository' + +export interface CreateMessageOptions { + connectionRecord: ConnectionRecord + content: string + parentThreadId?: string +} + +export interface BasicMessageProtocolMsgReturnType { + message: MessageType + record: BasicMessageRecord +} diff --git a/packages/core/src/modules/basic-messages/protocols/index.ts b/packages/core/src/modules/basic-messages/protocols/index.ts new file mode 100644 index 0000000000..4806c2a7b2 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/index.ts @@ -0,0 +1,4 @@ +export * from './v1' +export * from './v2' +export * from './BasicMessageProtocol' +export * from './BasicMessageProtocolOptions' diff --git a/packages/core/src/modules/basic-messages/protocols/v1/V1BasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/v1/V1BasicMessageProtocol.ts new file mode 100644 index 0000000000..8ad13e3bfa --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/V1BasicMessageProtocol.ts @@ -0,0 +1,99 @@ +import type { AgentContext } from '../../../../agent' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { ConnectionRecord } from '../../../connections/repository/ConnectionRecord' +import type { BasicMessageStateChangedEvent } from '../../BasicMessageEvents' +import type { CreateMessageOptions } from '../BasicMessageProtocolOptions' + +import { EventEmitter } from '../../../../agent/EventEmitter' +import { Protocol } from '../../../../agent/models' +import { injectable } from '../../../../plugins' +import { BasicMessageEventTypes } from '../../BasicMessageEvents' +import { BasicMessageRole } from '../../BasicMessageRole' +import { BasicMessageRecord, BasicMessageRepository } from '../../repository' +import { BaseBasicMessageProtocol } from '../BaseBasicMessageProtocol' + +import { V1BasicMessageHandler } from './handlers' +import { V1BasicMessage } from './messages' + +@injectable() +export class V1BasicMessageProtocol extends BaseBasicMessageProtocol { + /** + * The version of Basic Messages this class supports + */ + public readonly version = 'v1' as const + + /** + * Registers the protocol implementation (handlers, feature registry) on the agent. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for Basic Message V1 Protocol + dependencyManager.registerMessageHandlers([new V1BasicMessageHandler(this)]) + + // Register in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/basicmessage/1.0', + roles: [BasicMessageRole.Sender, BasicMessageRole.Receiver], + }) + ) + } + + public async createMessage(agentContext: AgentContext, options: CreateMessageOptions) { + const { content, parentThreadId, connectionRecord } = options + const basicMessage = new V1BasicMessage({ content }) + + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + + // If no parentThreadid is defined, there is no need to explicitly send a thread decorator + if (parentThreadId) { + basicMessage.setThread({ parentThreadId }) + } + + const basicMessageRecord = new BasicMessageRecord({ + sentTime: basicMessage.sentTime.toISOString(), + content: basicMessage.content, + connectionId: connectionRecord.id, + role: BasicMessageRole.Sender, + threadId: basicMessage.threadId, + parentThreadId, + }) + + await basicMessageRepository.save(agentContext, basicMessageRecord) + this.emitStateChangedEvent(agentContext, basicMessageRecord, basicMessage) + + return { message: basicMessage, record: basicMessageRecord } + } + + /** + * @todo use connection from message context + */ + public async save({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + + const basicMessageRecord = new BasicMessageRecord({ + sentTime: message.sentTime.toISOString(), + content: message.content, + connectionId: connection.id, + role: BasicMessageRole.Receiver, + threadId: message.threadId, + parentThreadId: message.thread?.parentThreadId, + }) + + await basicMessageRepository.save(agentContext, basicMessageRecord) + this.emitStateChangedEvent(agentContext, basicMessageRecord, message) + } + + protected emitStateChangedEvent( + agentContext: AgentContext, + basicMessageRecord: BasicMessageRecord, + basicMessage: V1BasicMessage + ) { + const eventEmitter = agentContext.dependencyManager.resolve(EventEmitter) + eventEmitter.emit(agentContext, { + type: BasicMessageEventTypes.BasicMessageStateChanged, + payload: { message: basicMessage, basicMessageRecord: basicMessageRecord.clone() }, + }) + } +} diff --git a/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts b/packages/core/src/modules/basic-messages/protocols/v1/__tests__/V1BasicMessageProtocol.test.ts similarity index 63% rename from packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts rename to packages/core/src/modules/basic-messages/protocols/v1/__tests__/V1BasicMessageProtocol.test.ts index 83dd0c4c01..a62a23aca9 100644 --- a/packages/core/src/modules/basic-messages/__tests__/BasicMessageService.test.ts +++ b/packages/core/src/modules/basic-messages/protocols/v1/__tests__/V1BasicMessageProtocol.test.ts @@ -1,36 +1,44 @@ -import { getAgentContext, getMockConnection } from '../../../../tests/helpers' -import { EventEmitter } from '../../../agent/EventEmitter' -import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import { BasicMessageRole } from '../BasicMessageRole' -import { BasicMessage } from '../messages' -import { BasicMessageRecord } from '../repository/BasicMessageRecord' -import { BasicMessageRepository } from '../repository/BasicMessageRepository' -import { BasicMessageService } from '../services' +import { getAgentContext, getMockConnection } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { BasicMessageRole } from '../../../BasicMessageRole' +import { BasicMessageRecord } from '../../../repository/BasicMessageRecord' +import { BasicMessageRepository } from '../../../repository/BasicMessageRepository' +import { V1BasicMessageProtocol } from '../V1BasicMessageProtocol' +import { V1BasicMessage } from '../messages' -jest.mock('../repository/BasicMessageRepository') +jest.mock('../../../repository/BasicMessageRepository') const BasicMessageRepositoryMock = BasicMessageRepository as jest.Mock const basicMessageRepository = new BasicMessageRepositoryMock() -jest.mock('../../../agent/EventEmitter') +jest.mock('../../../../../agent/EventEmitter') const EventEmitterMock = EventEmitter as jest.Mock const eventEmitter = new EventEmitterMock() -const agentContext = getAgentContext() +const agentContext = getAgentContext({ + registerInstances: [ + [BasicMessageRepository, basicMessageRepository], + [EventEmitter, eventEmitter], + ], +}) -describe('BasicMessageService', () => { - let basicMessageService: BasicMessageService +describe('V1BasicMessageProtocol', () => { + let basicMessageProtocol: V1BasicMessageProtocol const mockConnectionRecord = getMockConnection({ id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', }) beforeEach(() => { - basicMessageService = new BasicMessageService(basicMessageRepository, eventEmitter) + basicMessageProtocol = new V1BasicMessageProtocol() }) describe('createMessage', () => { it(`creates message and record, and emits message and basic message record`, async () => { - const { message } = await basicMessageService.createMessage(agentContext, 'hello', mockConnectionRecord) + const { message } = await basicMessageProtocol.createMessage(agentContext, { + content: 'hello', + connectionRecord: mockConnectionRecord, + }) expect(message.content).toBe('hello') @@ -53,14 +61,14 @@ describe('BasicMessageService', () => { describe('save', () => { it(`stores record and emits message and basic message record`, async () => { - const basicMessage = new BasicMessage({ + const basicMessage = new V1BasicMessage({ id: '123', content: 'message', }) const messageContext = new InboundMessageContext(basicMessage, { agentContext }) - await basicMessageService.save(messageContext, mockConnectionRecord) + await basicMessageProtocol.save(messageContext, mockConnectionRecord) expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) expect(eventEmitter.emit).toHaveBeenCalledWith(agentContext, { diff --git a/packages/core/src/modules/basic-messages/protocols/v1/handlers/V1BasicMessageHandler.ts b/packages/core/src/modules/basic-messages/protocols/v1/handlers/V1BasicMessageHandler.ts new file mode 100644 index 0000000000..d91428c3ef --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/handlers/V1BasicMessageHandler.ts @@ -0,0 +1,18 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V1BasicMessageProtocol } from '../V1BasicMessageProtocol' + +import { V1BasicMessage } from '../messages' + +export class V1BasicMessageHandler implements MessageHandler { + private basicMessageProtocol: V1BasicMessageProtocol + public supportedMessages = [V1BasicMessage] + + public constructor(basicMessageProtocol: V1BasicMessageProtocol) { + this.basicMessageProtocol = basicMessageProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const connection = messageContext.assertReadyConnection() + await this.basicMessageProtocol.save(messageContext, connection) + } +} diff --git a/packages/core/src/modules/basic-messages/protocols/v1/handlers/index.ts b/packages/core/src/modules/basic-messages/protocols/v1/handlers/index.ts new file mode 100644 index 0000000000..b7fa7fb3f2 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/handlers/index.ts @@ -0,0 +1 @@ +export * from './V1BasicMessageHandler' diff --git a/packages/core/src/modules/basic-messages/protocols/v1/index.ts b/packages/core/src/modules/basic-messages/protocols/v1/index.ts new file mode 100644 index 0000000000..9e581eae0d --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/index.ts @@ -0,0 +1,3 @@ +export * from './V1BasicMessageProtocol' +export * from './handlers' +export * from './messages' diff --git a/packages/core/src/modules/basic-messages/messages/BasicMessage.ts b/packages/core/src/modules/basic-messages/protocols/v1/messages/V1BasicMessage.ts similarity index 71% rename from packages/core/src/modules/basic-messages/messages/BasicMessage.ts rename to packages/core/src/modules/basic-messages/protocols/v1/messages/V1BasicMessage.ts index 1b720dceec..cef62224a3 100644 --- a/packages/core/src/modules/basic-messages/messages/BasicMessage.ts +++ b/packages/core/src/modules/basic-messages/protocols/v1/messages/V1BasicMessage.ts @@ -1,11 +1,11 @@ import { Expose, Transform } from 'class-transformer' import { IsDate, IsString } from 'class-validator' -import { DidCommV1Message } from '../../../didcomm' -import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' -import { DateParser } from '../../../utils/transformers' +import { DidCommV1Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { DateParser } from '../../../../../utils/transformers' -export class BasicMessage extends DidCommV1Message { +export class V1BasicMessage extends DidCommV1Message { public readonly allowDidSovPrefix = true /** @@ -24,8 +24,8 @@ export class BasicMessage extends DidCommV1Message { } } - @IsValidMessageType(BasicMessage.type) - public readonly type = BasicMessage.type.messageTypeUri + @IsValidMessageType(V1BasicMessage.type) + public readonly type = V1BasicMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/basicmessage/1.0/message') @Expose({ name: 'sent_time' }) diff --git a/packages/core/src/modules/basic-messages/protocols/v1/messages/index.ts b/packages/core/src/modules/basic-messages/protocols/v1/messages/index.ts new file mode 100644 index 0000000000..0e2b67bf45 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v1/messages/index.ts @@ -0,0 +1 @@ +export * from './V1BasicMessage' diff --git a/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts new file mode 100644 index 0000000000..a2dcc38855 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/V2BasicMessageProtocol.ts @@ -0,0 +1,84 @@ +import type { AgentContext } from '../../../../agent' +import type { FeatureRegistry } from '../../../../agent/FeatureRegistry' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { DependencyManager } from '../../../../plugins' +import type { ConnectionRecord } from '../../../connections/repository/ConnectionRecord' +import type { CreateMessageOptions } from '../BasicMessageProtocolOptions' + +import { Protocol } from '../../../../agent/models' +import { injectable } from '../../../../plugins' +import { BasicMessageRole } from '../../BasicMessageRole' +import { BasicMessageRecord, BasicMessageRepository } from '../../repository' +import { BaseBasicMessageProtocol } from '../BaseBasicMessageProtocol' + +import { V2BasicMessageHandler } from './handlers' +import { V2BasicMessage } from './messages' + +@injectable() +export class V2BasicMessageProtocol extends BaseBasicMessageProtocol { + /** + * The version of Basic Messages this class supports + */ + public readonly version = 'v2' as const + + /** + * Registers the protocol implementation (handlers, feature registry) on the agent. + */ + public register(dependencyManager: DependencyManager, featureRegistry: FeatureRegistry) { + // Register message handlers for Basic Message V2 Protocol + dependencyManager.registerMessageHandlers([new V2BasicMessageHandler(this)]) + + // Register in feature registry, with supported roles + featureRegistry.register( + new Protocol({ + id: 'https://didcomm.org/basicmessage/2.0', + roles: [BasicMessageRole.Sender, BasicMessageRole.Receiver], + }) + ) + } + + public async createMessage(agentContext: AgentContext, options: CreateMessageOptions) { + const { content, parentThreadId, connectionRecord } = options + const basicMessage = new V2BasicMessage({ content }) + + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + + // If no parentThreadid is defined, there is no need to explicitly send a thread decorator + if (parentThreadId) { + basicMessage.parentThreadId = parentThreadId + } + + const basicMessageRecord = new BasicMessageRecord({ + sentTime: new Date(basicMessage.createdTime).toISOString(), + content: basicMessage.body.content, + connectionId: connectionRecord.id, + role: BasicMessageRole.Sender, + threadId: basicMessage.threadId, + parentThreadId, + }) + + await basicMessageRepository.save(agentContext, basicMessageRecord) + // TODO: Emit BasicStateChangedEvent when it is updated to accept V2 Basic Messages + + return { message: basicMessage, record: basicMessageRecord } + } + + /** + * @todo use connection from message context + */ + public async save({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord) { + const basicMessageRepository = agentContext.dependencyManager.resolve(BasicMessageRepository) + + const basicMessageRecord = new BasicMessageRecord({ + sentTime: new Date(message.createdTime).toISOString(), + content: message.body.content, + connectionId: connection.id, + role: BasicMessageRole.Receiver, + threadId: message.threadId, + parentThreadId: message.parentThreadId, + }) + + await basicMessageRepository.save(agentContext, basicMessageRecord) + // TODO: Emit BasicStateChangedEvent when it is updated to accept V2 Basic Messages + } +} diff --git a/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts new file mode 100644 index 0000000000..2420a61884 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/__tests__/V2BasicMessageProtocol.test.ts @@ -0,0 +1,89 @@ +import { getAgentContext, getMockConnection } from '../../../../../../tests/helpers' +import { EventEmitter } from '../../../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../../../agent/models/InboundMessageContext' +import { BasicMessageRole } from '../../../BasicMessageRole' +import { BasicMessageRecord } from '../../../repository/BasicMessageRecord' +import { BasicMessageRepository } from '../../../repository/BasicMessageRepository' +import { V2BasicMessageProtocol } from '../V2BasicMessageProtocol' +import { V2BasicMessage } from '../messages' + +jest.mock('../../../repository/BasicMessageRepository') +const BasicMessageRepositoryMock = BasicMessageRepository as jest.Mock +const basicMessageRepository = new BasicMessageRepositoryMock() + +jest.mock('../../../../../agent/EventEmitter') +const EventEmitterMock = EventEmitter as jest.Mock +const eventEmitter = new EventEmitterMock() + +const agentContext = getAgentContext({ + registerInstances: [ + [BasicMessageRepository, basicMessageRepository], + [EventEmitter, eventEmitter], + ], +}) + +describe('V2BasicMessageProtocol', () => { + let basicMessageProtocol: V2BasicMessageProtocol + const mockConnectionRecord = getMockConnection({ + id: 'd3849ac3-c981-455b-a1aa-a10bea6cead8', + did: 'did:sov:C2SsBf5QUQpqSAQfhu3sd2', + }) + + beforeEach(() => { + basicMessageProtocol = new V2BasicMessageProtocol() + }) + + describe('createMessage', () => { + it(`creates message and record, and emits message and basic message record`, async () => { + const { message } = await basicMessageProtocol.createMessage(agentContext, { + content: 'hello', + connectionRecord: mockConnectionRecord, + }) + + expect(message.body.content).toBe('hello') + + expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) + expect(eventEmitter.emit).not.toHaveBeenCalledWith(agentContext, { + type: 'BasicMessageStateChanged', + payload: { + basicMessageRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + id: expect.any(String), + sentTime: expect.any(String), + content: 'hello', + role: BasicMessageRole.Sender, + }), + message, + }, + }) + }) + }) + + describe('save', () => { + it(`stores record and emits message and basic message record`, async () => { + const basicMessage = new V2BasicMessage({ + id: '123', + content: 'message', + }) + + const messageContext = new InboundMessageContext(basicMessage, { agentContext }) + + await basicMessageProtocol.save(messageContext, mockConnectionRecord) + + expect(basicMessageRepository.save).toHaveBeenCalledWith(agentContext, expect.any(BasicMessageRecord)) + expect(eventEmitter.emit).not.toHaveBeenCalledWith(agentContext, { + type: 'BasicMessageStateChanged', + payload: { + basicMessageRecord: expect.objectContaining({ + connectionId: mockConnectionRecord.id, + id: expect.any(String), + sentTime: new Date(basicMessage.createdTime).toISOString(), + content: basicMessage.body.content, + role: BasicMessageRole.Receiver, + }), + message: messageContext.message, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/basic-messages/protocols/v2/handlers/V2BasicMessageHandler.ts b/packages/core/src/modules/basic-messages/protocols/v2/handlers/V2BasicMessageHandler.ts new file mode 100644 index 0000000000..2995d643b5 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/handlers/V2BasicMessageHandler.ts @@ -0,0 +1,18 @@ +import type { MessageHandler, MessageHandlerInboundMessage } from '../../../../../agent/MessageHandler' +import type { V2BasicMessageProtocol } from '../V2BasicMessageProtocol' + +import { V2BasicMessage } from '../messages' + +export class V2BasicMessageHandler implements MessageHandler { + private basicMessageProtocol: V2BasicMessageProtocol + public supportedMessages = [V2BasicMessage] + + public constructor(basicMessageProtocol: V2BasicMessageProtocol) { + this.basicMessageProtocol = basicMessageProtocol + } + + public async handle(messageContext: MessageHandlerInboundMessage) { + const connection = messageContext.assertReadyConnection() + await this.basicMessageProtocol.save(messageContext, connection) + } +} diff --git a/packages/core/src/modules/basic-messages/protocols/v2/handlers/index.ts b/packages/core/src/modules/basic-messages/protocols/v2/handlers/index.ts new file mode 100644 index 0000000000..a35412c989 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/handlers/index.ts @@ -0,0 +1 @@ +export * from './V2BasicMessageHandler' diff --git a/packages/core/src/modules/basic-messages/protocols/v2/index.ts b/packages/core/src/modules/basic-messages/protocols/v2/index.ts new file mode 100644 index 0000000000..ca75f6a6e8 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/index.ts @@ -0,0 +1,3 @@ +export * from './V2BasicMessageProtocol' +export * from './handlers' +export * from './messages' diff --git a/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts new file mode 100644 index 0000000000..7429e3dee0 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/messages/V2BasicMessage.ts @@ -0,0 +1,51 @@ +import { Type } from 'class-transformer' +import { IsNotEmpty, IsObject, IsString, ValidateNested } from 'class-validator' + +import { DidCommV2Message } from '../../../../../didcomm' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' + +class V2BasicMessageBody { + public constructor(options: { content: string }) { + if (options) { + this.content = options.content + } + } + @IsString() + public content!: string +} + +export class V2BasicMessage extends DidCommV2Message { + public readonly allowDidSovPrefix = false + + @IsNotEmpty() + public createdTime!: number + + @IsObject() + @ValidateNested() + @Type(() => V2BasicMessageBody) + public body!: V2BasicMessageBody + + /** + * Create new BasicMessage instance. + * sentTime will be assigned to new Date if not passed, id will be assigned to uuid/v4 if not passed + * @param options + */ + public constructor(options: { content: string; sentTime?: Date; id?: string; locale?: string }) { + super() + + if (options) { + this.id = options.id || this.generateId() + this.createdTime = options.sentTime?.getTime() || new Date().getTime() + this.body = new V2BasicMessageBody({ content: options.content }) + this.language = options.locale || 'en' + } + } + + public get content() { + return this.body.content + } + + @IsValidMessageType(V2BasicMessage.type) + public readonly type = V2BasicMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/basicmessage/2.0/message') +} diff --git a/packages/core/src/modules/basic-messages/protocols/v2/messages/index.ts b/packages/core/src/modules/basic-messages/protocols/v2/messages/index.ts new file mode 100644 index 0000000000..171b735eb1 --- /dev/null +++ b/packages/core/src/modules/basic-messages/protocols/v2/messages/index.ts @@ -0,0 +1 @@ +export * from './V2BasicMessage' diff --git a/packages/core/src/modules/basic-messages/services/BasicMessageService.ts b/packages/core/src/modules/basic-messages/services/BasicMessageService.ts deleted file mode 100644 index d58ec06564..0000000000 --- a/packages/core/src/modules/basic-messages/services/BasicMessageService.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { AgentContext } from '../../../agent' -import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import type { Query } from '../../../storage/StorageService' -import type { ConnectionRecord } from '../../connections/repository/ConnectionRecord' -import type { BasicMessageStateChangedEvent } from '../BasicMessageEvents' - -import { EventEmitter } from '../../../agent/EventEmitter' -import { injectable } from '../../../plugins' -import { BasicMessageEventTypes } from '../BasicMessageEvents' -import { BasicMessageRole } from '../BasicMessageRole' -import { BasicMessage } from '../messages' -import { BasicMessageRecord, BasicMessageRepository } from '../repository' - -@injectable() -export class BasicMessageService { - private basicMessageRepository: BasicMessageRepository - private eventEmitter: EventEmitter - - public constructor(basicMessageRepository: BasicMessageRepository, eventEmitter: EventEmitter) { - this.basicMessageRepository = basicMessageRepository - this.eventEmitter = eventEmitter - } - - public async createMessage( - agentContext: AgentContext, - message: string, - connectionRecord: ConnectionRecord, - parentThreadId?: string - ) { - const basicMessage = new BasicMessage({ content: message }) - - // If no parentThreadid is defined, there is no need to explicitly send a thread decorator - if (parentThreadId) { - basicMessage.setThread({ parentThreadId }) - } - - const basicMessageRecord = new BasicMessageRecord({ - sentTime: basicMessage.sentTime.toISOString(), - content: basicMessage.content, - connectionId: connectionRecord.id, - role: BasicMessageRole.Sender, - threadId: basicMessage.threadId, - parentThreadId, - }) - - await this.basicMessageRepository.save(agentContext, basicMessageRecord) - this.emitStateChangedEvent(agentContext, basicMessageRecord, basicMessage) - - return { message: basicMessage, record: basicMessageRecord } - } - - /** - * @todo use connection from message context - */ - public async save({ message, agentContext }: InboundMessageContext, connection: ConnectionRecord) { - const basicMessageRecord = new BasicMessageRecord({ - sentTime: message.sentTime.toISOString(), - content: message.content, - connectionId: connection.id, - role: BasicMessageRole.Receiver, - threadId: message.threadId, - parentThreadId: message.thread?.parentThreadId, - }) - - await this.basicMessageRepository.save(agentContext, basicMessageRecord) - this.emitStateChangedEvent(agentContext, basicMessageRecord, message) - } - - private emitStateChangedEvent( - agentContext: AgentContext, - basicMessageRecord: BasicMessageRecord, - basicMessage: BasicMessage - ) { - this.eventEmitter.emit(agentContext, { - type: BasicMessageEventTypes.BasicMessageStateChanged, - payload: { message: basicMessage, basicMessageRecord: basicMessageRecord.clone() }, - }) - } - - public async findAllByQuery(agentContext: AgentContext, query: Query) { - return this.basicMessageRepository.findByQuery(agentContext, query) - } - - public async getById(agentContext: AgentContext, basicMessageRecordId: string) { - return this.basicMessageRepository.getById(agentContext, basicMessageRecordId) - } - - public async getByThreadId(agentContext: AgentContext, threadId: string) { - return this.basicMessageRepository.getSingleByQuery(agentContext, { threadId }) - } - - public async findAllByParentThreadId(agentContext: AgentContext, parentThreadId: string) { - return this.basicMessageRepository.findByQuery(agentContext, { parentThreadId }) - } - - public async deleteById(agentContext: AgentContext, basicMessageRecordId: string) { - const basicMessageRecord = await this.getById(agentContext, basicMessageRecordId) - return this.basicMessageRepository.delete(agentContext, basicMessageRecord) - } -} diff --git a/packages/core/src/modules/basic-messages/services/index.ts b/packages/core/src/modules/basic-messages/services/index.ts deleted file mode 100644 index a48826839a..0000000000 --- a/packages/core/src/modules/basic-messages/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BasicMessageService' diff --git a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts index 6e5a60b3b0..7d0a88c555 100644 --- a/packages/core/src/storage/didcomm/DidCommMessageRecord.ts +++ b/packages/core/src/storage/didcomm/DidCommMessageRecord.ts @@ -58,13 +58,13 @@ export class DidCommMessageRecord extends BaseRecord } public getTags() { - const messageId = this.message['@id'] as string - const messageType = this.message['@type'] as string + const messageId = (this.message['@id'] ?? this.message['id']) as string + const messageType = (this.message['@type'] ?? this.message['type']) as string const { protocolName, protocolMajorVersion, protocolMinorVersion, messageName } = parseMessageType(messageType) const thread = this.message['~thread'] - let threadId = messageId + let threadId = (this.message['thid'] ?? messageId) as string if (isJsonObject(thread) && typeof thread.thid === 'string') { threadId = thread.thid @@ -89,7 +89,7 @@ export class DidCommMessageRecord extends BaseRecord public getMessageInstance( messageClass: MessageClass ): InstanceType { - const messageType = parseMessageType(this.message['@type'] as string) + const messageType = parseMessageType((this.message['@type'] ?? this.message['type']) as string) if (!canHandleMessageType(messageClass, messageType)) { throw new AriesFrameworkError('Provided message class type does not match type of stored message') diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 4fe9732007..a907165bd9 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -2,8 +2,6 @@ import type { AgentDependencies, BaseEvent, - BasicMessage, - BasicMessageStateChangedEvent, ConnectionRecordProps, CredentialStateChangedEvent, InitConfig, @@ -14,6 +12,7 @@ import type { CredentialState, ConnectionStateChangedEvent, Buffer, + AgentMessageProcessedEvent, } from '../src' import type { AgentModulesInput, EmptyModuleMap } from '../src/agent/AgentModules' import type { @@ -31,13 +30,15 @@ import { catchError, filter, map, take, timeout } from 'rxjs/operators' import { agentDependencies, IndySdkPostgresWalletScheme } from '../../node/src' import { + V1BasicMessage, + V2BasicMessage, + OutOfBandVersion, OutOfBandDidCommService, ConnectionsModule, ConnectionEventTypes, TypedArrayEncoder, AgentConfig, AgentContext, - BasicMessageEventTypes, ConnectionRecord, CredentialEventTypes, DependencyManager, @@ -47,6 +48,7 @@ import { InjectionSymbols, ProofEventTypes, TrustPingEventTypes, + AgentEventTypes, } from '../src' import { Key, KeyType } from '../src/crypto' import { DidKey } from '../src/modules/dids/methods/key' @@ -217,9 +219,11 @@ const isCredentialStateChangedEvent = (e: BaseEvent): e is CredentialStateChange const isConnectionStateChangedEvent = (e: BaseEvent): e is ConnectionStateChangedEvent => e.type === ConnectionEventTypes.ConnectionStateChanged const isTrustPingReceivedEvent = (e: BaseEvent): e is TrustPingReceivedEvent => - e.type === TrustPingEventTypes.TrustPingReceivedEvent + e.type === TrustPingEventTypes.TrustPingReceivedEvent || e.type === TrustPingEventTypes.V2TrustPingReceivedEvent const isTrustPingResponseReceivedEvent = (e: BaseEvent): e is TrustPingResponseReceivedEvent => e.type === TrustPingEventTypes.TrustPingResponseReceivedEvent +const isAgentMessageProcessedEvent = (e: BaseEvent): e is AgentMessageProcessedEvent => + e.type === AgentEventTypes.AgentMessageProcessed export function waitForProofExchangeRecordSubject( subject: ReplaySubject | Observable, @@ -267,11 +271,16 @@ export function waitForProofExchangeRecordSubject( export async function waitForTrustPingReceivedEvent( agent: Agent, options: { + protocolVersion?: 'v1' | 'v2' threadId?: string timeoutMs?: number } ) { - const observable = agent.events.observable(TrustPingEventTypes.TrustPingReceivedEvent) + const observable = agent.events.observable( + options.protocolVersion === 'v2' + ? TrustPingEventTypes.V2TrustPingReceivedEvent + : TrustPingEventTypes.TrustPingReceivedEvent + ) return waitForTrustPingReceivedEventSubject(observable, options) } @@ -442,20 +451,40 @@ export async function waitForConnectionRecord( return waitForConnectionRecordSubject(observable, options) } -export async function waitForBasicMessage(agent: Agent, { content }: { content?: string }): Promise { - return new Promise((resolve) => { - const listener = (event: BasicMessageStateChangedEvent) => { - const contentMatches = content === undefined || event.payload.message.content === content - - if (contentMatches) { - agent.events.off(BasicMessageEventTypes.BasicMessageStateChanged, listener) +export async function waitForBasicMessage( + agent: Agent, + { content, timeoutMs }: { content?: string; timeoutMs?: number } +): Promise { + const observable = agent.events.observable(AgentEventTypes.AgentMessageProcessed) + return waitForBasicMessageSubject(observable, { content, timeoutMs }) +} - resolve(event.payload.message) - } - } +export function waitForBasicMessageSubject( + subject: ReplaySubject | Observable, + { + content, + timeoutMs = 5000, + }: { + content?: string + timeoutMs?: number + } +) { + const observable = subject instanceof ReplaySubject ? subject.asObservable() : subject - agent.events.on(BasicMessageEventTypes.BasicMessageStateChanged, listener) - }) + return firstValueFrom( + observable.pipe( + filter(isAgentMessageProcessedEvent), + map((e) => e.payload.message), + filter((e): e is V1BasicMessage | V2BasicMessage => + [V1BasicMessage.type.messageTypeUri, V2BasicMessage.type.messageTypeUri].includes(e.type) + ), + filter((e) => content === undefined || e.content === content), + timeout(timeoutMs), + catchError(() => { + throw new Error(`Basic Message not received within specified timeout: { content: ${content} }`) + }) + ) + ) } export function getMockConnection({ @@ -532,20 +561,36 @@ export function getMockOutOfBand({ return outOfBandRecord } -export async function makeConnection(agentA: Agent, agentB: Agent) { - const agentAOutOfBand = await agentA.oob.createInvitation({ - handshakeProtocols: [HandshakeProtocol.Connections], - }) +export async function makeConnection(agentA: Agent, agentB: Agent, version?: OutOfBandVersion) { + if (version === OutOfBandVersion.V2) { + const agentAOutOfBand = await agentA.oob.createInvitation({ + version, + }) - let { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation( - agentAOutOfBand.getOutOfBandInvitation() - ) + const { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation( + agentAOutOfBand.v2OutOfBandInvitation! + ) + if (!agentBConnection) throw new Error('No connection for receiver') + await agentB.connections.sendPing(agentBConnection.id, {}) + await waitForTrustPingReceivedEvent(agentA, { protocolVersion: 'v2', timeoutMs: 4000 }) + const [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) + if (!agentAConnection) throw new Error('No connection for inviter') + return [agentAConnection, agentBConnection] + } else { + const agentAOutOfBand = await agentA.oob.createInvitation({ + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + let { connectionRecord: agentBConnection } = await agentB.oob.receiveInvitation( + agentAOutOfBand.getOutOfBandInvitation() + ) - agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection!.id) - let [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) - agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection!.id) + agentBConnection = await agentB.connections.returnWhenIsConnected(agentBConnection!.id) + let [agentAConnection] = await agentA.connections.findAllByOutOfBandId(agentAOutOfBand.id) + agentAConnection = await agentA.connections.returnWhenIsConnected(agentAConnection!.id) - return [agentAConnection, agentBConnection] + return [agentAConnection, agentBConnection] + } } /**