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: allow sending problem report when declining a proof request #1408

2 changes: 1 addition & 1 deletion demo/src/AliceInquirer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
68 changes: 52 additions & 16 deletions packages/core/src/modules/proofs/ProofsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
SelectCredentialsForProofRequestOptions,
SelectCredentialsForProofRequestReturn,
SendProofProblemReportOptions,
DeclineProofRequestOptions,
} from './ProofsApiOptions'
import type { ProofProtocol } from './protocol/ProofProtocol'
import type { ProofFormatsFromProtocols } from './protocol/ProofProtocolOptions'
Expand Down Expand Up @@ -49,7 +50,7 @@ export interface ProofsApi<PPs extends ProofProtocol[]> {
// Request methods
requestProof(options: RequestProofOptions<PPs>): Promise<ProofExchangeRecord>
acceptRequest(options: AcceptProofRequestOptions<PPs>): Promise<ProofExchangeRecord>
declineRequest(proofRecordId: string): Promise<ProofExchangeRecord>
declineRequest(options: DeclineProofRequestOptions): Promise<ProofExchangeRecord>
negotiateRequest(options: NegotiateProofRequestOptions<PPs>): Promise<ProofExchangeRecord>

// Present
Expand Down Expand Up @@ -368,11 +369,15 @@ export class ProofsApi<PPs extends ProofProtocol[]> implements ProofsApi<PPs> {
}
}

public async declineRequest(proofRecordId: string): Promise<ProofExchangeRecord> {
const proofRecord = await this.getById(proofRecordId)
public async declineRequest(options: DeclineProofRequestOptions): Promise<ProofExchangeRecord> {
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
Expand Down Expand Up @@ -555,29 +560,60 @@ export class ProofsApi<PPs extends ProofProtocol[]> implements ProofsApi<PPs> {
*/
public async sendProblemReport(options: SendProofProblemReportOptions): Promise<ProofExchangeRecord> {
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, {
Copy link
Contributor

Choose a reason for hiding this comment

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

@TimoGlastra is this the correct place to do this logic? If possbile I think it would be good to defer this to another method (creating the service decorator and adding it as serviceParams).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The logical changes for send Problem Report function (handle connectionless request) repeats logic from accept Request method.

Copy link
Contributor

Choose a reason for hiding this comment

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

We need to refactor conecctionless in general. Once we integrate it with oob we can generalize the implementation and get rid of repeating the same code everywhere. For now I think it's fine to do it like this

agentContext: this.agentContext,
connection: connectionRecord,
associatedRecord: proofRecord,
})

await this.messageSender.sendMessage(outboundMessageContext)
return proofRecord
} else if (requestMessage?.service) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if we want to send a problem report as the verifier? We'd send the message to ourselves in that case (as we're the ones that created the request). I think we shouldn't assume the role here, and instead infer it based on the state

Copy link
Contributor Author

@Artemkaaas Artemkaaas Apr 4, 2023

Choose a reason for hiding this comment

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

Added check that state is ProofState.RequestReceived but I had to move sending of the problem report before updating the state to Declined

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<GetProofFormatDataReturn<ProofFormatsFromProtocols<PPs>>> {
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/modules/proofs/ProofsApiOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,14 @@ export abstract class BaseProofProtocol<PFs extends ProofFormatService[] = Proof
public async processProblemReport(
messageContext: InboundMessageContext<ProblemReportMessage>
): Promise<ProofExchangeRecord> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
makeConnection,
testLogger,
setupEventReplaySubjects,
waitForProofExchangeRecord,
} from '../../../../../../tests'
import { Agent } from '../../../../../agent/Agent'
import { Attachment, AttachmentData } from '../../../../../decorators/attachment/Attachment'
Expand Down Expand Up @@ -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,
})
})
})