Skip to content

feat: Basic Messages V2 for DIDComm V2 #1546

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

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 all 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
19 changes: 13 additions & 6 deletions demo/src/Listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { Faber } from './Faber'
import type { FaberInquirer } from './FaberInquirer'
import type {
Agent,
BasicMessageStateChangedEvent,
CredentialExchangeRecord,
CredentialStateChangedEvent,
TrustPingReceivedEvent,
Expand All @@ -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,
Expand Down Expand Up @@ -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`))
}
})
}
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)
})
})
28 changes: 24 additions & 4 deletions packages/core/src/agent/getOutboundMessageContext.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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'
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'
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down
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
30 changes: 30 additions & 0 deletions packages/core/src/didcomm/versions/v2/DidCommV2BaseMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<V2Attachment>
body?: unknown
}

type DidCommV2ReceiverOrder = {
id: string
last: number
gaps: number[]
}

export class DidCommV2BaseMessage {
@Matches(MessageIdRegExp)
public id!: string
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
14 changes: 13 additions & 1 deletion packages/core/src/didcomm/versions/v2/DidCommV2Message.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<ThreadDecorator>) {
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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -8,7 +8,7 @@ export enum BasicMessageEventTypes {
export interface BasicMessageStateChangedEvent extends BaseEvent {
type: typeof BasicMessageEventTypes.BasicMessageStateChanged
payload: {
message: BasicMessage
message: V1BasicMessage
basicMessageRecord: BasicMessageRecord
}
}
Loading