Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Basic Messages V2 for DIDComm V2 #1546

Draft
wants to merge 8 commits into
base: feat/didcomm-v2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion demo/src/Listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ProofEventTypes,
ProofState,
TrustPingEventTypes,
V1BasicMessage,
} from '@aries-framework/core'
import { ui } from 'inquirer'

Expand Down Expand Up @@ -78,7 +79,9 @@ 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 message = event.payload.message
const content = message instanceof V1BasicMessage ? message.content : message.body.content
this.ui.updateBottomBar(purpleText(`\n${name} received a message: ${content}\n`))
}
})
}
Expand Down
8 changes: 4 additions & 4 deletions packages/askar/src/wallet/__tests__/packing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { WalletConfig, WalletPackOptions, WalletUnpackOptions } from '@arie
import type { JwkProps } from '@hyperledger/aries-askar-shared'

import {
BasicMessage,
V1BasicMessage,
DidCommMessageVersion,
JsonTransformer,
KeyDerivationMethod,
Expand Down Expand Up @@ -71,7 +71,7 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => {
})

describe('DIDComm V1 packing and unpacking', () => {
const message = new BasicMessage({ content: 'hello' })
const message = new V1BasicMessage({ content: 'hello' })

test('Authcrypt', async () => {
// Create both sender and recipient keys
Expand All @@ -86,7 +86,7 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => {

const encryptedMessage = await askarWallet.pack(message.toJSON(), params)
const plainTextMessage = await askarWallet.unpack(encryptedMessage)
expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message)
expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, V1BasicMessage)).toEqual(message)
})

test('Anoncrypt', async () => {
Expand All @@ -101,7 +101,7 @@ describeRunInNodeVersion([18], 'askarWallet packing', () => {

const encryptedMessage = await askarWallet.pack(message.toJSON(), params)
const plainTextMessage = await askarWallet.unpack(encryptedMessage)
expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, BasicMessage)).toEqual(message)
expect(JsonTransformer.fromJSON(plainTextMessage.plaintextMessage, V1BasicMessage)).toEqual(message)
})
})

Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/agent/BaseAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -51,7 +52,7 @@ export abstract class BaseAgent<AgentModules extends ModulesMap = EmptyModuleMap
public readonly mediator: MediatorApi
public readonly mediationRecipient: MediationRecipientApi
public readonly messagePickup: CustomOrDefaultApi<AgentModules['messagePickup'], MessagePickupModule>
public readonly basicMessages: BasicMessagesApi
public readonly basicMessages: CustomOrDefaultApi<AgentModules['basicMessages'], BasicMessagesModule>
public readonly genericRecords: GenericRecordsApi
public readonly discovery: DiscoverFeaturesApi
public readonly dids: DidsApi
Expand Down Expand Up @@ -99,7 +100,10 @@ export abstract class BaseAgent<AgentModules extends ModulesMap = EmptyModuleMap
AgentModules['messagePickup'],
MessagePickupModule
>
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)
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/agent/__tests__/Agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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',
Expand All @@ -256,6 +255,6 @@ describe('Agent', () => {
'https://didcomm.org/revocation_notification/2.0',
])
)
expect(protocols.length).toEqual(13)
expect(protocols.length).toEqual(14)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export function ThreadDecorated<T extends DidComV1BaseMessageConstructor>(Base:
return this.thread?.threadId ?? this.id
}

public get parentThreadId(): string | undefined {
return this.thread?.parentThreadId
}

public setThread(options: Partial<ThreadDecorator>) {
this.thread = new ThreadDecorator(options)
}
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type DidCommV2MessageParams = {
createdTime?: number
expiresTime?: number
fromPrior?: string
language?: string
attachments?: Array<V2Attachment>
body?: unknown
}
Expand Down Expand Up @@ -68,6 +69,11 @@ export class DidCommV2BaseMessage {
@IsOptional()
public fromPrior?: string

@Expose({ name: 'lang' })
@IsString()
@IsOptional()
public language?: string

public body!: unknown

@IsOptional()
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/didcomm/versions/v2/DidCommV2Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class DidCommV2Message extends DidCommV2BaseMessage implements AgentBaseM
}

public get threadId(): string | undefined {
return this.thid
return this.thid ?? this.id
}

public hasAnyReturnRoute() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BasicMessage } from './messages'
import type { V1BasicMessage, V2BasicMessage } from './protocols'
import type { BasicMessageRecord } from './repository'
import type { BaseEvent } from '../../agent/Events'

Expand All @@ -8,7 +8,7 @@ export enum BasicMessageEventTypes {
export interface BasicMessageStateChangedEvent extends BaseEvent {
type: typeof BasicMessageEventTypes.BasicMessageStateChanged
payload: {
message: BasicMessage
message: V1BasicMessage | V2BasicMessage
basicMessageRecord: BasicMessageRecord
}
}
73 changes: 49 additions & 24 deletions packages/core/src/modules/basic-messages/BasicMessagesApi.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,59 @@
import type { BasicMessageProtocol, V2BasicMessage } 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 { 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<BMPs extends BasicMessageProtocol[]> {
sendMessage(connectionId: string, message: string, parentThreadId?: string): Promise<BasicMessageRecord>

findAllByQuery(query: Query<BasicMessageRecord>): Promise<BasicMessageRecord[]>
getById(basicMessageRecordId: string): Promise<BasicMessageRecord>
getByThreadId(threadId: string): Promise<BasicMessageRecord>
deleteById(basicMessageRecordId: string): Promise<void>
}

@injectable()
export class BasicMessagesApi {
private basicMessageService: BasicMessageService
export class BasicMessagesApi<BMPs extends BasicMessageProtocol[]> implements BasicMessagesApi<BMPs> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it's needed to register the protocols dynamically in this case? It's consistent with issue and prove, but those have reasons to not support e.g. v1 (anoncreds only).

But if both v1 and v2 are registered by default, there's no reason to not make it configurable i guess

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern was also followed in message pickup, I think mainly to allow:

  • Adding new protocol versions (or override behaviour of some of them) without the need of changing AFJ core
  • Creating 'pure DIDComm V2' agents where only the newer protocols are supported (this is the case if you are using a Wallet that does not support legacy DIDComm packing/unpacking). Also pure DIDComm V1 agents (e.g. Indy SDK or a browser wallet like this one)

For the second case, I think it will be useful to think about a "BaseProtocol" class/interface that forces protocols to indicate DIDComm versions where it is applicable for, and probably the same for Wallet (declare DIDComm versions supported), in order to register protocols only if the wallet is capable of handling messages for it.

public readonly config: BasicMessagesModuleConfig<BMPs>

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<BMPs>
) {
this.basicMessageService = basicMessageService
this.basicMessageRepository = basicMessageRepository
this.messageSender = messageSender
this.connectionService = connectionService
this.agentContext = agentContext
this.registerMessageHandlers(messageHandlerRegistry)
this.config = config
}

private getProtocol<PVT extends BMPs[number]['version']>(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
}

/**
Expand All @@ -44,13 +68,18 @@ 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, {
const outboundMessageContext = new OutboundMessageContext(basicMessage as V2BasicMessage, {
agentContext: this.agentContext,
connection,
associatedRecord: basicMessageRecord,
Expand All @@ -67,7 +96,7 @@ export class BasicMessagesApi {
* @returns array containing all matching records
*/
public async findAllByQuery(query: Query<BasicMessageRecord>) {
return this.basicMessageService.findAllByQuery(this.agentContext, query)
return this.basicMessageRepository.findByQuery(this.agentContext, query)
}

/**
Expand All @@ -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)
}

/**
Expand All @@ -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 })
}

/**
Expand All @@ -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)
}
}
56 changes: 40 additions & 16 deletions packages/core/src/modules/basic-messages/BasicMessagesModule.ts
Original file line number Diff line number Diff line change
@@ -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<BasicMessagesProtocols extends BasicMessageProtocol[]> = Optional<
BasicMessagesModuleConfigOptions<BasicMessagesProtocols>,
'basicMessageProtocols'
>

export class BasicMessagesModule<BasicMessageProtocols extends BasicMessageProtocol[] = DefaultBasicMessageProtocols>
implements ApiModule
{
public readonly config: BasicMessagesModuleConfig<BasicMessageProtocols>

public readonly api: Constructor<BasicMessagesApi<BasicMessageProtocols>> = BasicMessagesApi

public constructor(config?: BasicMessagesModuleConfig<BasicMessageProtocols>) {
this.config = new BasicMessagesModuleConfig({
...config,
basicMessageProtocols: config?.basicMessageProtocols ?? [
new V1BasicMessageProtocol(),
new V2BasicMessageProtocol(),
],
} as BasicMessagesModuleConfig<BasicMessageProtocols>)
}

/**
* Registers the dependencies of the basic message module on the dependency manager.
Expand All @@ -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)
}
}
}
Loading