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(credentials): add get format data method #877

Merged
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
53 changes: 53 additions & 0 deletions packages/core/src/modules/credentials/CredentialServiceOptions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,62 @@
import type { AgentMessage } from '../../agent/AgentMessage'
import type { ConnectionRecord } from '../connections/repository/ConnectionRecord'
import type { CredentialFormat, CredentialFormatPayload } from './formats'
import type { CredentialPreviewAttributeOptions } from './models'
import type { AutoAcceptCredential } from './models/CredentialAutoAcceptType'
import type { CredentialExchangeRecord } from './repository/CredentialExchangeRecord'

/**
* Get the format data payload for a specific message from a list of CredentialFormat interfaces and a message
*
* For an indy offer, this resolves to the cred abstract format as defined here:
* https://github.com/hyperledger/aries-rfcs/tree/b3a3942ef052039e73cd23d847f42947f8287da2/features/0592-indy-attachments#cred-abstract-format
*
* @example
* ```
*
* type OfferFormatData = FormatDataMessagePayload<[IndyCredentialFormat, JsonLdCredentialFormat], 'offer'>
*
* // equal to
* type OfferFormatData = {
* indy: {
* // ... payload for indy offer attachment as defined in RFC 0592 ...
* },
* jsonld: {
* // ... payload for jsonld offer attachment as defined in RFC 0593 ...
* }
* }
* ```
*/
export type FormatDataMessagePayload<
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
CFs extends CredentialFormat[] = CredentialFormat[],
M extends keyof CredentialFormat['formatData'] = keyof CredentialFormat['formatData']
> = {
[CredentialFormat in CFs[number] as CredentialFormat['formatKey']]?: CredentialFormat['formatData'][M]
}

/**
* Get format data return value. Each key holds a mapping of credential format key to format data.
*
* @example
* ```
* {
* proposal: {
* indy: {
* cred_def_id: string
* }
* }
* }
* ```
*/
export type GetFormatDataReturn<CFs extends CredentialFormat[] = CredentialFormat[]> = {
proposalAttributes?: CredentialPreviewAttributeOptions[]
proposal?: FormatDataMessagePayload<CFs, 'proposal'>
offer?: FormatDataMessagePayload<CFs, 'offer'>
offerAttributes?: CredentialPreviewAttributeOptions[]
request?: FormatDataMessagePayload<CFs, 'request'>
credential?: FormatDataMessagePayload<CFs, 'credential'>
}

export interface CreateProposalOptions<CFs extends CredentialFormat[]> {
connection: ConnectionRecord
credentialFormats: CredentialFormatPayload<CFs, 'createProposal'>
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/modules/credentials/CredentialsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
ProposeCredentialOptions,
ServiceMap,
CreateOfferOptions,
GetFormatDataReturn,
} from './CredentialsModuleOptions'
import type { CredentialFormat } from './formats'
import type { IndyCredentialFormat } from './formats/indy/IndyCredentialFormat'
Expand Down Expand Up @@ -68,6 +69,7 @@ export interface CredentialsModule<CFs extends CredentialFormat[], CSs extends C
getById(credentialRecordId: string): Promise<CredentialExchangeRecord>
findById(credentialRecordId: string): Promise<CredentialExchangeRecord | null>
deleteById(credentialRecordId: string, options?: DeleteCredentialOptions): Promise<void>
getFormatData(credentialRecordId: string): Promise<GetFormatDataReturn<CFs>>
}

@scoped(Lifecycle.ContainerScoped)
Expand Down Expand Up @@ -505,6 +507,13 @@ export class CredentialsModule<
}
}

public async getFormatData(credentialRecordId: string): Promise<GetFormatDataReturn<CFs>> {
const credentialRecord = await this.getById(credentialRecordId)
const service = this.getService(credentialRecord.protocolVersion)

return service.getFormatData(credentialRecordId)
}

/**
* Retrieve a credential record by id
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { GetFormatDataReturn } from './CredentialServiceOptions'
import type { CredentialFormat, CredentialFormatPayload } from './formats'
import type { AutoAcceptCredential } from './models/CredentialAutoAcceptType'
import type { CredentialService } from './services'

// re-export GetFormatDataReturn type from service, as it is also used in the module
export type { GetFormatDataReturn }

/**
* Get the supported protocol versions based on the provided credential services.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@ export interface CredentialFormat {
createRequest: unknown
acceptRequest: unknown
}
formatData: {
proposal: unknown
offer: unknown
request: unknown
credential: unknown
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { LinkedAttachment } from '../../../../utils/LinkedAttachment'
import type { CredentialPreviewAttributeOptions } from '../../models'
import type { CredentialFormat } from '../CredentialFormat'
import type { IndyCredProposeOptions } from './models/IndyCredPropose'
import type { Cred, CredOffer, CredReq } from 'indy-sdk'

/**
* This defines the module payload for calling CredentialsModule.createProposal
Expand Down Expand Up @@ -51,4 +52,19 @@ export interface IndyCredentialFormat extends CredentialFormat {
createRequest: never // cannot start from createRequest
acceptRequest: Record<string, never> // empty object
}
// Format data is based on RFC 0592
// https://github.com/hyperledger/aries-rfcs/tree/main/features/0592-indy-attachments
formatData: {
proposal: {
schema_issuer_did?: string
schema_name?: string
schema_version?: string
schema_id?: string
issuer_did?: string
cred_def_id?: string
}
offer: CredOffer
request: CredReq
credential: Cred
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type {
NegotiateOfferOptions,
NegotiateProposalOptions,
} from '../../CredentialServiceOptions'
import type { GetFormatDataReturn } from '../../CredentialsModuleOptions'
import type { CredentialFormat } from '../../formats'
import type { IndyCredentialFormat } from '../../formats/indy/IndyCredentialFormat'

import { Lifecycle, scoped } from 'tsyringe'
Expand Down Expand Up @@ -210,7 +212,11 @@ export class V1CredentialService extends CredentialService<[IndyCredentialFormat

await this.formatService.processProposal({
credentialRecord,
attachment: this.rfc0592ProposalAttachmentFromV1ProposeMessage(proposalMessage),
attachment: new Attachment({
data: new AttachmentData({
json: JsonTransformer.toJSON(this.rfc0592ProposalFromV1ProposeMessage(proposalMessage)),
}),
}),
})

// Update record
Expand Down Expand Up @@ -279,7 +285,11 @@ export class V1CredentialService extends CredentialService<[IndyCredentialFormat
attachId: INDY_CREDENTIAL_OFFER_ATTACHMENT_ID,
credentialFormats,
credentialRecord,
proposalAttachment: this.rfc0592ProposalAttachmentFromV1ProposeMessage(proposalMessage),
proposalAttachment: new Attachment({
data: new AttachmentData({
json: JsonTransformer.toJSON(this.rfc0592ProposalFromV1ProposeMessage(proposalMessage)),
}),
}),
})

if (!previewAttributes) {
Expand Down Expand Up @@ -1045,6 +1055,49 @@ export class V1CredentialService extends CredentialService<[IndyCredentialFormat
})
}

public async getFormatData(credentialExchangeId: string): Promise<GetFormatDataReturn<CredentialFormat[]>> {
// TODO: we could looking at fetching all record using a single query and then filtering based on the type of the message.
const [proposalMessage, offerMessage, requestMessage, credentialMessage] = await Promise.all([
this.findProposalMessage(credentialExchangeId),
this.findOfferMessage(credentialExchangeId),
this.findRequestMessage(credentialExchangeId),
this.findCredentialMessage(credentialExchangeId),
])

const indyProposal = proposalMessage
? JsonTransformer.toJSON(this.rfc0592ProposalFromV1ProposeMessage(proposalMessage))
: undefined

const indyOffer = offerMessage?.indyCredentialOffer ?? undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesnt offerMessage?.indyCredentialOffer short-circuit to undefined when it is undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No it resolves to null (as in nothing went wrong but we don't have a value). We aren't very consistent however in using null vs undefined and there's probably a lot of places where we should be using null

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah null vs undefined is very annoying. Might be worth in the future to check how we can clean this up.

const indyRequest = requestMessage?.indyCredentialRequest ?? undefined
const indyCredential = credentialMessage?.indyCredential ?? undefined

return {
proposalAttributes: proposalMessage?.credentialProposal?.attributes,
proposal: proposalMessage
? {
indy: indyProposal,
}
: undefined,
offerAttributes: offerMessage?.credentialPreview?.attributes,
offer: offerMessage
? {
indy: indyOffer,
}
: undefined,
request: requestMessage
? {
indy: indyRequest,
}
: undefined,
credential: credentialMessage
? {
indy: indyCredential,
}
: undefined,
}
}

protected registerHandlers() {
this.dispatcher.registerHandler(new V1ProposeCredentialHandler(this, this.agentConfig))
this.dispatcher.registerHandler(
Expand All @@ -1063,7 +1116,7 @@ export class V1CredentialService extends CredentialService<[IndyCredentialFormat
this.dispatcher.registerHandler(new V1CredentialProblemReportHandler(this))
}

private rfc0592ProposalAttachmentFromV1ProposeMessage(proposalMessage: V1ProposeCredentialMessage) {
private rfc0592ProposalFromV1ProposeMessage(proposalMessage: V1ProposeCredentialMessage) {
const indyCredentialProposal = new IndyCredPropose({
credentialDefinitionId: proposalMessage.credentialDefinitionId,
schemaId: proposalMessage.schemaId,
Expand All @@ -1073,11 +1126,7 @@ export class V1CredentialService extends CredentialService<[IndyCredentialFormat
schemaVersion: proposalMessage.schemaVersion,
})

return new Attachment({
data: new AttachmentData({
json: JsonTransformer.toJSON(indyCredentialProposal),
}),
})
return indyCredentialProposal
}

private assertOnlyIndyFormat(credentialFormats: Record<string, unknown>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,107 @@ describe('v1 credentials', () => {
threadId: faberCredentialRecord.threadId,
state: CredentialState.Done,
})

const formatData = await aliceAgent.credentials.getFormatData(aliceCredentialRecord.id)

expect(formatData).toMatchObject({
proposalAttributes: [
{
name: 'name',
mimeType: 'text/plain',
value: 'John',
},
{
name: 'age',
mimeType: 'text/plain',
value: '99',
},
{
name: 'x-ray',
mimeType: 'text/plain',
value: 'some x-ray',
},
{
name: 'profile_picture',
mimeType: 'text/plain',
value: 'profile picture',
},
],
proposal: {
indy: {
schema_issuer_did: expect.any(String),
schema_id: expect.any(String),
schema_name: expect.any(String),
schema_version: expect.any(String),
cred_def_id: expect.any(String),
issuer_did: expect.any(String),
},
},
offer: {
indy: {
schema_id: expect.any(String),
cred_def_id: expect.any(String),
key_correctness_proof: expect.any(Object),
nonce: expect.any(String),
},
},
offerAttributes: [
{
name: 'name',
mimeType: 'text/plain',
value: 'John',
},
{
name: 'age',
mimeType: 'text/plain',
value: '99',
},
{
name: 'x-ray',
mimeType: 'text/plain',
value: 'some x-ray',
},
{
name: 'profile_picture',
mimeType: 'text/plain',
value: 'profile picture',
},
],
request: {
indy: {
prover_did: expect.any(String),
cred_def_id: expect.any(String),
blinded_ms: expect.any(Object),
blinded_ms_correctness_proof: expect.any(Object),
nonce: expect.any(String),
},
},
credential: {
indy: {
schema_id: expect.any(String),
cred_def_id: expect.any(String),
rev_reg_id: null,
values: {
age: { raw: '99', encoded: '99' },
profile_picture: {
raw: 'profile picture',
encoded: '28661874965215723474150257281172102867522547934697168414362313592277831163345',
},
name: {
raw: 'John',
encoded: '76355713903561865866741292988746191972523015098789458240077478826513114743258',
},
'x-ray': {
raw: 'some x-ray',
encoded: '43715611391396952879378357808399363551139229809726238083934532929974486114650',
},
},
signature: expect.any(Object),
signature_correctness_proof: expect.any(Object),
rev_reg: null,
witness: null,
},
},
})
})
})
Loading