diff --git a/demo/src/AliceInquirer.ts b/demo/src/AliceInquirer.ts index 9752eab5aa..a665d5e80b 100644 --- a/demo/src/AliceInquirer.ts +++ b/demo/src/AliceInquirer.ts @@ -81,7 +81,7 @@ export class AliceInquirer extends BaseInquirer { public async acceptProofRequest(proofRecord: ProofExchangeRecord) { const confirm = await prompt([this.inquireConfirmation(Title.ProofRequestTitle)]) if (confirm.options === ConfirmOptions.No) { - await this.alice.agent.proofs.declineRequest(proofRecord.id) + await this.alice.agent.proofs.declineRequest({ proofRecordId: proofRecord.id }) } else if (confirm.options === ConfirmOptions.Yes) { await this.alice.acceptProofRequest(proofRecord) } diff --git a/packages/core/src/modules/proofs/ProofsApi.ts b/packages/core/src/modules/proofs/ProofsApi.ts index 1a327f71b8..ed78afec2d 100644 --- a/packages/core/src/modules/proofs/ProofsApi.ts +++ b/packages/core/src/modules/proofs/ProofsApi.ts @@ -17,6 +17,7 @@ import type { SelectCredentialsForProofRequestOptions, SelectCredentialsForProofRequestReturn, SendProofProblemReportOptions, + DeclineProofRequestOptions, } from './ProofsApiOptions' import type { ProofProtocol } from './protocol/ProofProtocol' import type { ProofFormatsFromProtocols } from './protocol/ProofProtocolOptions' @@ -49,7 +50,7 @@ export interface ProofsApi { // Request methods requestProof(options: RequestProofOptions): Promise acceptRequest(options: AcceptProofRequestOptions): Promise - declineRequest(proofRecordId: string): Promise + declineRequest(options: DeclineProofRequestOptions): Promise negotiateRequest(options: NegotiateProofRequestOptions): Promise // Present @@ -368,11 +369,15 @@ export class ProofsApi implements ProofsApi { } } - public async declineRequest(proofRecordId: string): Promise { - const proofRecord = await this.getById(proofRecordId) + public async declineRequest(options: DeclineProofRequestOptions): Promise { + const proofRecord = await this.getById(options.proofRecordId) proofRecord.assertState(ProofState.RequestReceived) const protocol = this.getProtocol(proofRecord.protocolVersion) + if (options.sendProblemReport) { + await this.sendProblemReport({ proofRecordId: options.proofRecordId, description: 'Request declined' }) + } + await protocol.updateState(this.agentContext, proofRecord, ProofState.Declined) return proofRecord @@ -555,29 +560,60 @@ export class ProofsApi implements ProofsApi { */ public async sendProblemReport(options: SendProofProblemReportOptions): Promise { const proofRecord = await this.getById(options.proofRecordId) - if (!proofRecord.connectionId) { - throw new AriesFrameworkError(`No connectionId found for proof record '${proofRecord.id}'.`) - } const protocol = this.getProtocol(proofRecord.protocolVersion) - const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) - // Assert - connectionRecord.assertReady() + const requestMessage = await protocol.findRequestMessage(this.agentContext, proofRecord.id) const { message: problemReport } = await protocol.createProblemReport(this.agentContext, { proofRecord, description: options.description, }) - const outboundMessageContext = new OutboundMessageContext(problemReport, { - agentContext: this.agentContext, - connection: connectionRecord, - associatedRecord: proofRecord, - }) + if (proofRecord.connectionId) { + const connectionRecord = await this.connectionService.getById(this.agentContext, proofRecord.connectionId) - await this.messageSender.sendMessage(outboundMessageContext) - return proofRecord + // Assert + connectionRecord.assertReady() + + const outboundMessageContext = new OutboundMessageContext(problemReport, { + agentContext: this.agentContext, + connection: connectionRecord, + associatedRecord: proofRecord, + }) + + await this.messageSender.sendMessage(outboundMessageContext) + return proofRecord + } else if (requestMessage?.service) { + proofRecord.assertState(ProofState.RequestReceived) + + // Create ~service decorator + const routing = await this.routingService.getRouting(this.agentContext) + const ourService = new ServiceDecorator({ + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.recipientKey.publicKeyBase58], + routingKeys: routing.routingKeys.map((key) => key.publicKeyBase58), + }) + const recipientService = requestMessage.service + + await this.messageSender.sendMessageToService( + new OutboundMessageContext(problemReport, { + agentContext: this.agentContext, + serviceParams: { + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }, + }) + ) + + return proofRecord + } + // Cannot send message without connectionId or ~service decorator + else { + throw new AriesFrameworkError( + `Cannot send problem report without connectionId or ~service decorator on presentation request.` + ) + } } public async getFormatData(proofRecordId: string): Promise>> { diff --git a/packages/core/src/modules/proofs/ProofsApiOptions.ts b/packages/core/src/modules/proofs/ProofsApiOptions.ts index 9fcb5cefa3..0e9911febc 100644 --- a/packages/core/src/modules/proofs/ProofsApiOptions.ts +++ b/packages/core/src/modules/proofs/ProofsApiOptions.ts @@ -173,3 +173,11 @@ export interface SendProofProblemReportOptions { proofRecordId: string description: string } + +/** + * Interface for ProofsApi.declineRequest. Decline a received proof request and optionally send a problem-report message to Verifier + */ +export interface DeclineProofRequestOptions { + proofRecordId: string + sendProblemReport?: boolean +} diff --git a/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts b/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts index 9e4e9e8b1c..ce2a30df05 100644 --- a/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts +++ b/packages/core/src/modules/proofs/protocol/BaseProofProtocol.ts @@ -110,16 +110,14 @@ export abstract class BaseProofProtocol ): Promise { - const { message: proofProblemReportMessage, agentContext } = messageContext - - const connection = messageContext.assertReadyConnection() + const { message: proofProblemReportMessage, agentContext, connection } = messageContext agentContext.config.logger.debug(`Processing problem report with message id ${proofProblemReportMessage.id}`) const proofRecord = await this.getByThreadAndConnectionId( agentContext, proofProblemReportMessage.threadId, - connection.id + connection?.id ) // Update record diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts index 3312a27f19..55f1e8bbbb 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-indy-connectionless-proofs.e2e.test.ts @@ -18,6 +18,7 @@ import { makeConnection, testLogger, setupEventReplaySubjects, + waitForProofExchangeRecord, } from '../../../../../../tests' import { Agent } from '../../../../../agent/Agent' import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment' @@ -543,4 +544,98 @@ describe('V2 Connectionless Proofs - Indy', () => { threadId: requestMessage.threadId, }) }) + + test('Faber starts with connection-less proof requests to Alice but gets Problem Reported', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const { + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + credentialDefinitionId, + issuerHolderConnectionId: faberConnectionId, + } = await setupAnonCredsTests({ + issuerName: 'Faber connection-less Proofs v2 - Reject Request', + holderName: 'Alice connection-less Proofs v2 - Reject Request', + autoAcceptProofs: AutoAcceptProof.Never, + attributeNames: ['name', 'age'], + }) + + await issueLegacyAnonCredsCredential({ + issuerAgent: faberAgent, + issuerReplay: faberReplay, + holderAgent: aliceAgent, + holderReplay: aliceReplay, + issuerHolderConnectionId: faberConnectionId, + offer: { + credentialDefinitionId, + attributes: [ + { + name: 'name', + value: 'Alice', + }, + { + name: 'age', + value: '99', + }, + ], + }, + }) + + agents = [aliceAgent, faberAgent] + + // eslint-disable-next-line prefer-const + // eslint-disable-next-line prefer-const + let { message, proofRecord: faberProofExchangeRecord } = await faberAgent.proofs.createRequest({ + protocolVersion: 'v2', + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + requested_attributes: { + name: { + name: 'name', + restrictions: [ + { + cred_def_id: credentialDefinitionId, + }, + ], + }, + }, + requested_predicates: {}, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + }) + + const { message: requestMessage } = await faberAgent.oob.createLegacyConnectionlessInvitation({ + recordId: faberProofExchangeRecord.id, + message, + domain: 'rxjs:faber', + }) + + for (const transport of faberAgent.outboundTransports) { + await faberAgent.unregisterOutboundTransport(transport) + } + + const aliceProofExchangeRecordPromise = waitForProofExchangeRecord(aliceAgent, { + state: ProofState.RequestReceived, + }) + + await aliceAgent.receiveMessage(requestMessage.toJSON()) + const aliceProofExchangeRecord = await aliceProofExchangeRecordPromise + + await aliceAgent.proofs.declineRequest({ proofRecordId: aliceProofExchangeRecord.id, sendProblemReport: true }) + + await waitForProofExchangeRecordSubject(aliceReplay, { + state: ProofState.Declined, + threadId: requestMessage.threadId, + }) + + await waitForProofExchangeRecordSubject(faberReplay, { + state: ProofState.Abandoned, + threadId: requestMessage.threadId, + }) + }) })