Skip to content

Commit

Permalink
feat(credentials): add get format data method (#877)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed Jun 20, 2022
1 parent 448a29d commit 521d489
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 8 deletions.
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<
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
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

0 comments on commit 521d489

Please sign in to comment.