From 85cba7e85566ebb46ed2ba25ce5f2b86882f0ddd Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:53:39 +0530 Subject: [PATCH 01/20] feat: anchor schema and verifiable credentials (VC) to Cord --- .env | 19 ++ docker-compose.yml | 5 + services/credential-schema/.env.sample | 12 + .../credential-schema/docker-compose-test.yml | 2 + services/credential-schema/docker-compose.yml | 2 + .../migration.sql | 2 + .../credential-schema/prisma/schema.prisma | 1 + .../src/schema/entities/VCItem.entity.ts | 1 + .../src/schema/schema.fixtures.ts | 2 + .../src/schema/schema.service.ts | 28 ++ .../src/utils/utils.service.ts | 27 ++ services/credentials-service/.env.sample | 18 ++ .../credentials-service/docker-compose.yml | 3 + .../src/credentials/credentials.service.ts | 255 +++++++++++++++--- .../credentials/utils/cord.utils.service.ts | 63 +++++ .../credentials/utils/schema.utils.service.ts | 17 +- services/identity-service/.gitignore | 5 +- 17 files changed, 422 insertions(+), 40 deletions(-) create mode 100644 services/credential-schema/prisma/migrations/20240920194207_add_cord_schema_id/migration.sql create mode 100644 services/credentials-service/src/credentials/utils/cord.utils.service.ts diff --git a/.env b/.env index 30579c304..45396758c 100644 --- a/.env +++ b/.env @@ -29,3 +29,22 @@ SCHEMA_BASE_URL=http://credential-schema:3333 CREDENTIAL_SERVICE_BASE_URL=https://example.com/credentials JWKS_URI= ENABLE_AUTH=false + + +# Anchor to cord block chain +# Flag to enable/disable anchoring to Cord blockchain +ANCHOR_TO_CORD=true + +# Base URL for Issuer Agent +# This is the service responsible for issuing credentials +# Example: https:///api/v1 +ISSUER_AGENT_BASE_URL=https:///api/v1 + +# Base URL for Verification Middleware +# This service is responsible for verifying credentials +# Example: https:///api/v1/verify +VERIFICATION_MIDDLEWARE_BASE_URL=https:///api/v1/verify + +# Additional Resources: +# - For more details on Issuer Agent, visit: https://github.com/dhiway/issuer-agent +# - For more details on Verification Middleware, visit: https://github.com/dhiway/verification-middleware \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 90da2ec54..297612a92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -263,6 +263,8 @@ services: - IDENTITY_BASE_URL=${IDENTITY_BASE_URL} - JWKS_URI=${JWKS_URI} - ENABLE_AUTH=${ENABLE_AUTH} + - ANCHOR_TO_CORD=${ANCHOR_TO_CORD} + - ISSUER_AGENT_BASE_URL=${ISSUER_AGENT_BASE_URL} healthcheck: test: [ "CMD-SHELL", "curl -f http://localhost:3333/health || exit 1" ] @@ -288,6 +290,9 @@ services: - JWKS_URI=${JWKS_URI} - ENABLE_AUTH=${ENABLE_AUTH} - QR_TYPE=${QR_TYPE} + - ANCHOR_TO_CORD=${ANCHOR_TO_CORD} + - ISSUER_AGENT_BASE_URL=${ISSUER_AGENT_BASE_URL} + - VERIFICATION_MIDDLEWARE_BASE_URL=${VERIFICATION_MIDDLEWARE_BASE_URL} healthcheck: test: [ "CMD-SHELL", "curl -f http://localhost:3000/health || exit 1" ] diff --git a/services/credential-schema/.env.sample b/services/credential-schema/.env.sample index 425eb0038..3c10b7ee6 100644 --- a/services/credential-schema/.env.sample +++ b/services/credential-schema/.env.sample @@ -11,3 +11,15 @@ IDENTITY_BASE_URL= # URL of the identity service to facilitate DID creation # Service VARS PORT=3000 SCHEMA_BASE_URL= + + +# Flag to enable/disable anchoring to Cord blockchain +ANCHOR_TO_CORD=true + +# Base URL for Issuer Agent +# This is the service responsible for issuing credentials +# Example: https:///api/v1 +ISSUER_AGENT_BASE_URL=https:///api/v1 + +# Additional Resources: +# - For more details on Issuer Agent, visit: https://github.com/dhiway/issuer-agent \ No newline at end of file diff --git a/services/credential-schema/docker-compose-test.yml b/services/credential-schema/docker-compose-test.yml index 53aa4210b..3a3b73eb5 100644 --- a/services/credential-schema/docker-compose-test.yml +++ b/services/credential-schema/docker-compose-test.yml @@ -25,6 +25,8 @@ services: DATABASE_URL: postgres://postgres:postgres@db-test:5432/postgres IDENTITY_BASE_URL: "http://identity-service:3332" ENABLE_AUTH: "false" + ANCHOR_TO_CORD: "${ANCHOR_TO_CORD}" + ISSUER_AGENT_BASE_URL: "${ISSUER_AGENT_BASE_URL}" networks: test: rcw-test: diff --git a/services/credential-schema/docker-compose.yml b/services/credential-schema/docker-compose.yml index 53d491fb9..f731c1242 100644 --- a/services/credential-schema/docker-compose.yml +++ b/services/credential-schema/docker-compose.yml @@ -26,6 +26,8 @@ services: DATABASE_URL: postgres://postgres:postgres@db:5432/postgres IDENTITY_BASE_URL: "http://identity-service:3332" ENABLE_AUTH: "false" + ANCHOR_TO_CORD: "${ANCHOR_TO_CORD}" + ISSUER_AGENT_BASE_URL: "${ISSUER_AGENT_BASE_URL}" networks: rcw-test: default: diff --git a/services/credential-schema/prisma/migrations/20240920194207_add_cord_schema_id/migration.sql b/services/credential-schema/prisma/migrations/20240920194207_add_cord_schema_id/migration.sql new file mode 100644 index 000000000..036d6d1bc --- /dev/null +++ b/services/credential-schema/prisma/migrations/20240920194207_add_cord_schema_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" ADD COLUMN "cordSchemaId" TEXT; diff --git a/services/credential-schema/prisma/schema.prisma b/services/credential-schema/prisma/schema.prisma index b038f3712..89b98b382 100644 --- a/services/credential-schema/prisma/schema.prisma +++ b/services/credential-schema/prisma/schema.prisma @@ -35,6 +35,7 @@ model VerifiableCredentialSchema { tags String[] status SchemaStatus @default(DRAFT) deprecatedId String? + cordSchemaId String? @@id([id, version]) @@index([type], type: Hash) diff --git a/services/credential-schema/src/schema/entities/VCItem.entity.ts b/services/credential-schema/src/schema/entities/VCItem.entity.ts index 1c7f7829d..92ae2ad9a 100644 --- a/services/credential-schema/src/schema/entities/VCItem.entity.ts +++ b/services/credential-schema/src/schema/entities/VCItem.entity.ts @@ -34,4 +34,5 @@ export class VCItem implements VerifiableCredentialSchema { createdBy: string; updatedBy: string; deprecatedId: string; + cordSchemaId: string | null; } diff --git a/services/credential-schema/src/schema/schema.fixtures.ts b/services/credential-schema/src/schema/schema.fixtures.ts index 59bb9ce9b..6b025d60d 100644 --- a/services/credential-schema/src/schema/schema.fixtures.ts +++ b/services/credential-schema/src/schema/schema.fixtures.ts @@ -85,6 +85,7 @@ export const testSchemaRespose1: VerifiableCredentialSchema = { tags: ['degree', 'computer science', 'bachelor'], status: 'DRAFT', deprecatedId: null, + cordSchemaId: null }; export const testSchemaRespose2: VerifiableCredentialSchema = { @@ -104,4 +105,5 @@ export const testSchemaRespose2: VerifiableCredentialSchema = { tags: ['certification', 'blockchain', 'expert'], status: 'DRAFT', deprecatedId: null, + cordSchemaId: null, }; \ No newline at end of file diff --git a/services/credential-schema/src/schema/schema.service.ts b/services/credential-schema/src/schema/schema.service.ts index f9a2bbc3a..b7fdfb27d 100644 --- a/services/credential-schema/src/schema/schema.service.ts +++ b/services/credential-schema/src/schema/schema.service.ts @@ -58,6 +58,7 @@ export class SchemaService { createdBy: schema.createdBy, updatedBy: schema.updatedBy, deprecatedId: schema.deprecatedId, + cordSchemaId: schema.cordSchemaId, } as GetCredentialSchemaDTO; } else { this.logger.error('schema not found for userInput', userWhereUniqueInput); @@ -118,6 +119,7 @@ export class SchemaService { createdBy: schema.createdBy, updatedBy: schema.updatedBy, deprecatedId: schema.deprecatedId, + cordSchemaId: schema.cordSchemaId, }; }); } @@ -129,6 +131,7 @@ export class SchemaService { ) { const data = createCredentialDto.schema; const tags = createCredentialDto.tags; + let cordSchemaId: string | null = null; // verify the Credential Schema if (validate(data)) { @@ -152,6 +155,28 @@ export class SchemaService { this.logger.debug('DID received from identity service', did); } + // Anchor the schema to Cord blockchain only if ANCHOR_TO_CORD is set to 'True' or 'true' + if ( + process.env.ANCHOR_TO_CORD && + process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' + ) { + try { + const anchorResponse = await this.utilService.anchorSchema({ + schema: data, + }); + + cordSchemaId = anchorResponse.schemaId; + this.logger.debug( + 'Schema successfully anchored to Cord blockchain', + anchorResponse, + ); + } catch (err) { + this.logger.error('Failed to anchor schema to Cord blockchain', err); + throw new InternalServerErrorException( + 'Failed to anchor schema to Cord blockchain', + ); + } + } const credSchema = { schema: { type: data.type, @@ -166,6 +191,7 @@ export class SchemaService { tags: tags, status: createCredentialDto.status, deprecatedId: createCredentialDto.deprecatedId, + cordSchemaId: cordSchemaId, }; // sign the credential schema (only the schema part of the credSchema object above since it is the actual schema) @@ -189,6 +215,7 @@ export class SchemaService { proof: credSchema.schema.proof as Prisma.JsonValue || undefined, tags: credSchema.tags as string[], deprecatedId: deprecatedId, + cordSchemaId: (credSchema.cordSchemaId as string) || null, }, }); @@ -235,6 +262,7 @@ export class SchemaService { createdBy: schema?.createdBy, updatedBy: schema?.updatedBy, deprecatedId: schema?.deprecatedId, + cordSchemaId: schema?.cordSchemaId, }), ); } diff --git a/services/credential-schema/src/utils/utils.service.ts b/services/credential-schema/src/utils/utils.service.ts index c2adbd5c3..373ccd5fa 100644 --- a/services/credential-schema/src/utils/utils.service.ts +++ b/services/credential-schema/src/utils/utils.service.ts @@ -38,4 +38,31 @@ export class UtilsService { throw new InternalServerErrorException('Can not generate a new DID'); } } + + async anchorSchema(body: any): Promise { + try { + const response = await this.httpService.axiosRef.post( + `${process.env.ISSUER_AGENT_BASE_URL}/schema`, + body, + ); + return response.data; + } catch (err) { + const errorDetails = { + message: err.message, + status: err.response?.status, + statusText: err.response?.statusText, + data: err.response?.data, + headers: err.response?.headers, + request: err.config, + }; + + this.logger.error( + 'Error anchoring schema to Cord blockchain', + errorDetails, + ); + throw new InternalServerErrorException( + 'Failed to anchor schema to Cord blockchain', + ); + } + } } diff --git a/services/credentials-service/.env.sample b/services/credentials-service/.env.sample index de575c4d0..d29788336 100644 --- a/services/credentials-service/.env.sample +++ b/services/credentials-service/.env.sample @@ -12,3 +12,21 @@ IDENTITY_BASE_URL= # URL of the identity service to facilitate DID creation SCHEMA_BASE_URL= CREDENTIAL_SERVICE_BASE_URL="" + + +# Flag to enable/disable anchoring to Cord blockchain +ANCHOR_TO_CORD=true + +# Base URL for Issuer Agent +# This is the service responsible for issuing credentials +# Example: https:///api/v1 +ISSUER_AGENT_BASE_URL=https:///api/v1 + +# Base URL for Verification Middleware +# This service is responsible for verifying credentials +# Example: https:///api/v1/verify +VERIFICATION_MIDDLEWARE_BASE_URL=https:///api/v1/verify + +# Additional Resources: +# - For more details on Issuer Agent, visit: https://github.com/dhiway/issuer-agent +# - For more details on Verification Middleware, visit: https://github.com/dhiway/verification-middleware diff --git a/services/credentials-service/docker-compose.yml b/services/credentials-service/docker-compose.yml index 06802a63a..c6f9a103f 100644 --- a/services/credentials-service/docker-compose.yml +++ b/services/credentials-service/docker-compose.yml @@ -27,6 +27,9 @@ services: SCHEMA_BASE_URL: ENABLE_AUTH: "false" JWKS_URI: "" + ANCHOR_TO_CORD: "${ANCHOR_TO_CORD}" + ISSUER_AGENT_BASE_URL: "${ISSUER_AGENT_BASE_URL}" + VERIFICATION_MIDDLEWARE_BASE_URL: "${VERIFICATION_MIDDLEWARE_BASE_URL}" healthcheck: test: [ "CMD-SHELL", "curl -f http://localhost:3000/health || exit 1" ] diff --git a/services/credentials-service/src/credentials/credentials.service.ts b/services/credentials-service/src/credentials/credentials.service.ts index 0a6d6365f..4a0438f5e 100644 --- a/services/credentials-service/src/credentials/credentials.service.ts +++ b/services/credentials-service/src/credentials/credentials.service.ts @@ -13,6 +13,7 @@ import { JwtCredentialSubject } from 'src/app.interface'; import { SchemaUtilsSerivce } from './utils/schema.utils.service'; import { IdentityUtilsService } from './utils/identity.utils.service'; import { RenderingUtilsService } from './utils/rendering.utils.service'; +import { AnchorCordUtilsServices } from './utils/cord.utils.service'; import * as jsigs from 'jsonld-signatures'; import * as jsonld from 'jsonld'; import { DOCUMENTS } from './documents'; @@ -41,7 +42,8 @@ export class CredentialsService { private readonly prisma: PrismaClient, private readonly identityUtilsService: IdentityUtilsService, private readonly renderingUtilsService: RenderingUtilsService, - private readonly schemaUtilsService: SchemaUtilsSerivce + private readonly schemaUtilsService: SchemaUtilsSerivce, + private readonly anchorCordUtilsServices: AnchorCordUtilsServices ) { this.init(); } @@ -148,6 +150,11 @@ export class CredentialsService { async verifyCredential(credToVerify: Verifiable, status?: VCStatus) { try { + + // If ANCHOR_TO_CORD is true, delegate verification to Cord Verification MiddleWare service + if (this.shouldAnchorToCord()) { + return await this.anchorCordUtilsServices.verifyCredentialOnCord(credToVerify); + } // calling identity service to verify the issuer DID const issuerId = (credToVerify.issuer?.id || credToVerify.issuer) as string; const did: DIDDocument = await this.identityUtilsService.resolveDID( @@ -265,44 +272,204 @@ export class CredentialsService { }) } + // async issueCredential(issueRequest: IssueCredentialDTO) { + // this.logger.debug(`Received issue credential request`); + // const credInReq = issueRequest.credential; + // // check for issuance date + // if (!credInReq.issuanceDate) + // credInReq.issuanceDate = new Date(Date.now()).toISOString(); + // // Verify the credential with the credential schema using ajv + // // get the credential schema + // const schema = await this.schemaUtilsService.getCredentialSchema( + // issueRequest.credentialSchemaId, + // issueRequest.credentialSchemaVersion + // ); + // this.logger.debug('fetched schema'); + // const { valid, errors } = + // await this.schemaUtilsService.verifyCredentialSubject( + // credInReq, + // schema.schema + // ); + // if (!valid) { + // this.logger.error('Invalid credential schema', errors); + // throw new BadRequestException(errors); + // } + // this.logger.debug('validated schema'); + // // generate the DID for credential + // const credDID: ReadonlyArray = + // await this.identityUtilsService.generateDID( + // [], + // issueRequest.method + // ); + // this.logger.debug('generated DID'); + // try { + // credInReq.id = credDID[0].id; + // } catch (err) { + // this.logger.error('Invalid response from generate DID', err); + // throw new InternalServerErrorException('Problem creating DID'); + // } + // // sign the credential + // let signedCredential: W3CCredential = {}; + // try { + // signedCredential = await this.identityUtilsService.signVC( + // credInReq as CredentialPayload, + // credInReq.issuer + // ); + // } catch (err) { + // this.logger.error('Error signing the credential'); + // throw new InternalServerErrorException('Problem signing the credential'); + // } + + // this.logger.debug('signed credential'); + + // // TODO: add created by and updated by + // const newCred = await this.prisma.verifiableCredentials.create({ + // data: { + // id: signedCredential.id, + // type: signedCredential.type, + // issuer: signedCredential.issuer as IssuerType as string, + // issuanceDate: signedCredential.issuanceDate, + // expirationDate: signedCredential.expirationDate, + // subject: signedCredential.credentialSubject as JwtCredentialSubject, + // subjectId: (signedCredential.credentialSubject as JwtCredentialSubject).id, + // proof: signedCredential.proof as Proof, + // credential_schema: issueRequest.credentialSchemaId, //because they can't refer to the schema db from here through an ID + // signed: signedCredential as object, + // tags: issueRequest.tags, + // }, + // }); + + // if (!newCred) { + // this.logger.error('Problem saving credential to db'); + // throw new InternalServerErrorException('Problem saving credential to db'); + // } + // this.logger.debug('saved credential to db'); + + // const res = newCred.signed; + // delete res['options']; + // return { + // credential: res, + // credentialSchemaId: newCred.credential_schema, + // createdAt: newCred.updated_at.toISOString(), + // updatedAt: newCred.updated_at.toISOString(), + // createdBy: '', + // updatedBy: '', + // tags: newCred.tags, + // }; + // } + + async issueCredential(issueRequest: IssueCredentialDTO) { this.logger.debug(`Received issue credential request`); const credInReq = issueRequest.credential; - // check for issuance date - if (!credInReq.issuanceDate) + + // Check for issuance date + if (!credInReq.issuanceDate) { credInReq.issuanceDate = new Date(Date.now()).toISOString(); - // Verify the credential with the credential schema using ajv - // get the credential schema - const schema = await this.schemaUtilsService.getCredentialSchema( + } + + // Get the credential schema and Cord schema ID + const { schema, cordSchemaId } = await this.schemaUtilsService.getCredentialSchema( issueRequest.credentialSchemaId, issueRequest.credentialSchemaVersion ); - this.logger.debug('fetched schema'); - const { valid, errors } = - await this.schemaUtilsService.verifyCredentialSubject( - credInReq, - schema.schema - ); + + this.logger.debug('fetched schema', schema); + + const { valid, errors } = await this.schemaUtilsService.verifyCredentialSubject( + credInReq, + schema.schema + ); + if (!valid) { this.logger.error('Invalid credential schema', errors); throw new BadRequestException(errors); } + this.logger.debug('validated schema'); - // generate the DID for credential - const credDID: ReadonlyArray = - await this.identityUtilsService.generateDID( - [], - issueRequest.method - ); + + // Generate the DID for the credential + const credDID: ReadonlyArray = await this.identityUtilsService.generateDID([], issueRequest.method); this.logger.debug('generated DID'); + try { credInReq.id = credDID[0].id; } catch (err) { this.logger.error('Invalid response from generate DID', err); throw new InternalServerErrorException('Problem creating DID'); } - // sign the credential + + this.logger.debug('Generated DID and validated schema'); + + let response: any = null; + + // Check if ANCHOR_TO_CORD is true, anchor the unsigned credential using cordSchemaId from the schema + if (this.shouldAnchorToCord()) { + response = await this.anchorCredentialToCord(credInReq, cordSchemaId, issueRequest); + } else { + response = await this.signAndStoreCredential(credInReq, issueRequest); + } + + return response; + } + + /** + * Determines if ANCHOR_TO_CORD environment variable is true + */ + private shouldAnchorToCord(): boolean { + return process.env.ANCHOR_TO_CORD && process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true'; + } + + /** + * Anchors the credential to Cord blockchain and saves to the DB + */ + private async anchorCredentialToCord(credInReq: any, cordSchemaId: string, issueRequest: IssueCredentialDTO) { + if (!cordSchemaId) { + this.logger.error('Cord Schema ID is required for anchoring but is missing'); + throw new BadRequestException('Cord Schema ID is missing'); + } + + try { + this.logger.debug('Anchoring unsigned credential to Cord blockchain with schema ID:', cordSchemaId); + + const anchorResponse = await this.anchorCordUtilsServices.anchorCredential({ + ...credInReq, + cordSchemaId, + }); + + this.logger.debug('Credential successfully anchored to Cord:', anchorResponse); + + const { + id , issuer, issuanceDate, validUntil: expirationDate, credentialSubject, proof , + } = anchorResponse.vc; + + const anchoredCredentialData = { + id, + type: issueRequest.credential.type, + issuer, + issuanceDate, + expirationDate, + subject: credentialSubject, + subjectId: (credentialSubject as JwtCredentialSubject).id, + proof, + credential_schema: issueRequest.credentialSchemaId, + signed: anchorResponse.vc as object, + tags: issueRequest.tags, + }; + + return this.saveCredentialToDatabase(anchoredCredentialData); + } catch (err) { + this.logger.error('Error anchoring credential to Cord blockchain:', err); + throw new InternalServerErrorException('Error anchoring credential to Cord blockchain'); + } + } + + /** + * Signs the credential locally and saves it to the database + */ + private async signAndStoreCredential(credInReq: any, issueRequest: IssueCredentialDTO) { let signedCredential: W3CCredential = {}; + try { signedCredential = await this.identityUtilsService.signVC( credInReq as CredentialPayload, @@ -312,34 +479,46 @@ export class CredentialsService { this.logger.error('Error signing the credential'); throw new InternalServerErrorException('Problem signing the credential'); } - - this.logger.debug('signed credential'); - - // TODO: add created by and updated by + + this.logger.debug('Signed credential'); + + const newCredData = { + id: signedCredential.id, + type: signedCredential.type, + issuer: signedCredential.issuer as IssuerType as string, + issuanceDate: signedCredential.issuanceDate, + expirationDate: signedCredential.expirationDate, + subject: signedCredential.credentialSubject as JwtCredentialSubject, + subjectId: (signedCredential.credentialSubject as JwtCredentialSubject).id, + proof: signedCredential.proof as Proof, + credential_schema: issueRequest.credentialSchemaId, + signed: signedCredential as object, + tags: issueRequest.tags, + }; + + return this.saveCredentialToDatabase(newCredData); + } + + /** + * Saves the credential to the database and returns the response + */ + private async saveCredentialToDatabase(credentialData: any) { + const newCred = await this.prisma.verifiableCredentials.create({ - data: { - id: signedCredential.id, - type: signedCredential.type, - issuer: signedCredential.issuer as IssuerType as string, - issuanceDate: signedCredential.issuanceDate, - expirationDate: signedCredential.expirationDate, - subject: signedCredential.credentialSubject as JwtCredentialSubject, - subjectId: (signedCredential.credentialSubject as JwtCredentialSubject).id, - proof: signedCredential.proof as Proof, - credential_schema: issueRequest.credentialSchemaId, //because they can't refer to the schema db from here through an ID - signed: signedCredential as object, - tags: issueRequest.tags, - }, + data: credentialData, }); if (!newCred) { this.logger.error('Problem saving credential to db'); throw new InternalServerErrorException('Problem saving credential to db'); } + this.logger.debug('saved credential to db'); - + const res = newCred.signed; + delete res['options']; + return { credential: res, credentialSchemaId: newCred.credential_schema, @@ -350,7 +529,7 @@ export class CredentialsService { tags: newCred.tags, }; } - + async deleteCredential(id: string) { try { const credential = await this.prisma.verifiableCredentials.update({ diff --git a/services/credentials-service/src/credentials/utils/cord.utils.service.ts b/services/credentials-service/src/credentials/utils/cord.utils.service.ts new file mode 100644 index 000000000..742dfed3b --- /dev/null +++ b/services/credentials-service/src/credentials/utils/cord.utils.service.ts @@ -0,0 +1,63 @@ +import { HttpService } from '@nestjs/axios'; +import { AxiosResponse } from '@nestjs/terminus/dist/health-indicator/http/axios.interfaces'; +import { + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; +import { W3CCredential, Verifiable } from 'vc.types'; +@Injectable() +export class AnchorCordUtilsServices { + private ISSUER_AGENT_BASE_URL: string; + private VERIFICATION_MIDDLEWARE_BASE_URL: string + + constructor(private readonly httpService: HttpService) { + this.ISSUER_AGENT_BASE_URL = process.env.ISSUER_AGENT_BASE_URL; + this.VERIFICATION_MIDDLEWARE_BASE_URL = process.env.VERIFICATION_MIDDLEWARE_BASE_URL; + } + + private logger = new Logger(AnchorCordUtilsServices.name); + + async anchorCredential(credentialPayload: any): Promise { + try { + this.logger.debug('url', this.ISSUER_AGENT_BASE_URL); + const anchorResponse: AxiosResponse = + await this.httpService.axiosRef.post( + `${this.ISSUER_AGENT_BASE_URL}/cred`, + { + credential: credentialPayload, + } + ); + + this.logger.debug('Credential successfully anchored'); + return anchorResponse.data; + } catch (err) { + this.logger.error('Error anchoring credential:', err); + + throw new InternalServerErrorException(`Error anchoring credential : ${err.response.data.details}`); + } + } + + async verifyCredentialOnCord( + credToVerify: Verifiable + ): Promise { + try { + const response = await this.httpService.axiosRef.post( + `${this.VERIFICATION_MIDDLEWARE_BASE_URL}/credentials/verify`, + credToVerify + ); + + if (response.status !== 200) { + this.logger.error('Cord verification failed:', response.data); + throw new InternalServerErrorException('Cord verification failed'); + } + + return response.data; + } catch (err) { + this.logger.error('Error calling Cord verification API:', err); + throw new InternalServerErrorException( + 'Error verifying credential on Cord' + ); + } + } +} diff --git a/services/credentials-service/src/credentials/utils/schema.utils.service.ts b/services/credentials-service/src/credentials/utils/schema.utils.service.ts index c0c59eeb0..5b6d5c8bc 100644 --- a/services/credentials-service/src/credentials/utils/schema.utils.service.ts +++ b/services/credentials-service/src/credentials/utils/schema.utils.service.ts @@ -14,6 +14,7 @@ export class SchemaUtilsSerivce { private logger = new Logger(SchemaUtilsSerivce.name); async getCredentialSchema(schemaId: string, version: string) { let credSchema: AxiosResponse; + let cordSchemaId: string | null = null; try { credSchema = await this.httpService.axiosRef.get( `${process.env.SCHEMA_BASE_URL}/credential-schema/${schemaId}/${version}` @@ -37,8 +38,22 @@ export class SchemaUtilsSerivce { `Error fetching credential schema` ); } + // If ANCHOR_TO_CORD is true, retrieve cordSchemaId + + if (process.env.ANCHOR_TO_CORD && process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true') { + this.logger.debug('Fetching cordSchemaId as ANCHOR_TO_CORD is enabled'); + cordSchemaId = credSchema?.data?.cordSchemaId || null; - return credSchema?.data?.schema; + if (!cordSchemaId) { + this.logger.warn(`Cord Schema ID not found for schema ID ${schemaId}`); + throw new BadRequestException(`Cord Schema ID missing for schema ID ${schemaId}`); + } + } + + return { + schema: credSchema?.data?.schema, + cordSchemaId: cordSchemaId + }; } async getTemplateById(templateId: string) { diff --git a/services/identity-service/.gitignore b/services/identity-service/.gitignore index 53c37a166..f95f4062c 100644 --- a/services/identity-service/.gitignore +++ b/services/identity-service/.gitignore @@ -1 +1,4 @@ -dist \ No newline at end of file +dist + +#Environment files +.env \ No newline at end of file From 2f1e66302534e34da41e9674f132e83e1f8bff1f Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:16:49 +0530 Subject: [PATCH 02/20] fix: import AnchorCordUtilsServices service --- .../credentials-service/src/credentials/credentials.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/credentials-service/src/credentials/credentials.module.ts b/services/credentials-service/src/credentials/credentials.module.ts index 9c34c0912..2cdc048b0 100644 --- a/services/credentials-service/src/credentials/credentials.module.ts +++ b/services/credentials-service/src/credentials/credentials.module.ts @@ -5,11 +5,12 @@ import { HttpModule, HttpService } from '@nestjs/axios'; import { IdentityUtilsService } from './utils/identity.utils.service'; import { RenderingUtilsService } from './utils/rendering.utils.service'; import { SchemaUtilsSerivce } from './utils/schema.utils.service'; +import {AnchorCordUtilsServices} from './utils/cord.utils.service' import { PrismaClient } from '@prisma/client'; @Module({ imports: [HttpModule], - providers: [HttpService, CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce], + providers: [HttpService, CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce,AnchorCordUtilsServices], controllers: [CredentialsController], exports: [IdentityUtilsService] }) From 80d2cecfaf0318f4b8b4512b0b29f9854d941adc Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:21:21 +0530 Subject: [PATCH 03/20] refactor:remove commented codes --- .../src/credentials/credentials.service.ts | 86 ------------------- 1 file changed, 86 deletions(-) diff --git a/services/credentials-service/src/credentials/credentials.service.ts b/services/credentials-service/src/credentials/credentials.service.ts index 4a0438f5e..df275efd1 100644 --- a/services/credentials-service/src/credentials/credentials.service.ts +++ b/services/credentials-service/src/credentials/credentials.service.ts @@ -272,92 +272,6 @@ export class CredentialsService { }) } - // async issueCredential(issueRequest: IssueCredentialDTO) { - // this.logger.debug(`Received issue credential request`); - // const credInReq = issueRequest.credential; - // // check for issuance date - // if (!credInReq.issuanceDate) - // credInReq.issuanceDate = new Date(Date.now()).toISOString(); - // // Verify the credential with the credential schema using ajv - // // get the credential schema - // const schema = await this.schemaUtilsService.getCredentialSchema( - // issueRequest.credentialSchemaId, - // issueRequest.credentialSchemaVersion - // ); - // this.logger.debug('fetched schema'); - // const { valid, errors } = - // await this.schemaUtilsService.verifyCredentialSubject( - // credInReq, - // schema.schema - // ); - // if (!valid) { - // this.logger.error('Invalid credential schema', errors); - // throw new BadRequestException(errors); - // } - // this.logger.debug('validated schema'); - // // generate the DID for credential - // const credDID: ReadonlyArray = - // await this.identityUtilsService.generateDID( - // [], - // issueRequest.method - // ); - // this.logger.debug('generated DID'); - // try { - // credInReq.id = credDID[0].id; - // } catch (err) { - // this.logger.error('Invalid response from generate DID', err); - // throw new InternalServerErrorException('Problem creating DID'); - // } - // // sign the credential - // let signedCredential: W3CCredential = {}; - // try { - // signedCredential = await this.identityUtilsService.signVC( - // credInReq as CredentialPayload, - // credInReq.issuer - // ); - // } catch (err) { - // this.logger.error('Error signing the credential'); - // throw new InternalServerErrorException('Problem signing the credential'); - // } - - // this.logger.debug('signed credential'); - - // // TODO: add created by and updated by - // const newCred = await this.prisma.verifiableCredentials.create({ - // data: { - // id: signedCredential.id, - // type: signedCredential.type, - // issuer: signedCredential.issuer as IssuerType as string, - // issuanceDate: signedCredential.issuanceDate, - // expirationDate: signedCredential.expirationDate, - // subject: signedCredential.credentialSubject as JwtCredentialSubject, - // subjectId: (signedCredential.credentialSubject as JwtCredentialSubject).id, - // proof: signedCredential.proof as Proof, - // credential_schema: issueRequest.credentialSchemaId, //because they can't refer to the schema db from here through an ID - // signed: signedCredential as object, - // tags: issueRequest.tags, - // }, - // }); - - // if (!newCred) { - // this.logger.error('Problem saving credential to db'); - // throw new InternalServerErrorException('Problem saving credential to db'); - // } - // this.logger.debug('saved credential to db'); - - // const res = newCred.signed; - // delete res['options']; - // return { - // credential: res, - // credentialSchemaId: newCred.credential_schema, - // createdAt: newCred.updated_at.toISOString(), - // updatedAt: newCred.updated_at.toISOString(), - // createdBy: '', - // updatedBy: '', - // tags: newCred.tags, - // }; - // } - async issueCredential(issueRequest: IssueCredentialDTO) { this.logger.debug(`Received issue credential request`); From 560601d3557fb116c2ebba054b2b05c1666c29b9 Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:26:44 +0530 Subject: [PATCH 04/20] feat:Put Anchor to cord Default : false --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 45396758c..eaad13cbe 100644 --- a/.env +++ b/.env @@ -33,7 +33,7 @@ ENABLE_AUTH=false # Anchor to cord block chain # Flag to enable/disable anchoring to Cord blockchain -ANCHOR_TO_CORD=true +ANCHOR_TO_CORD=false # Base URL for Issuer Agent # This is the service responsible for issuing credentials From 6a28159e4c2756f587d01ac58abed6a045eeff18 Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:04:28 +0530 Subject: [PATCH 05/20] refact:anchor schema --- .../src/schema/schema.service.ts | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/services/credential-schema/src/schema/schema.service.ts b/services/credential-schema/src/schema/schema.service.ts index b7fdfb27d..7e2e19945 100644 --- a/services/credential-schema/src/schema/schema.service.ts +++ b/services/credential-schema/src/schema/schema.service.ts @@ -123,6 +123,32 @@ export class SchemaService { }; }); } + private shouldAnchorToCord(): boolean { + return ( + process.env.ANCHOR_TO_CORD && + process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' + ); + } + + private async anchorSchemaToCord(schemaData: any): Promise { + try { + const anchorResponse = await this.utilService.anchorSchema({ + schema: schemaData, + }); + + this.logger.debug( + 'Schema successfully anchored to Cord blockchain', + anchorResponse, + ); + + return anchorResponse.schemaId; + } catch (err) { + this.logger.error('Failed to anchor schema to Cord blockchain', err); + throw new InternalServerErrorException( + 'Failed to anchor schema to Cord blockchain', + ); + } + } async createCredentialSchema( createCredentialDto: CreateCredentialDTO, @@ -156,26 +182,8 @@ export class SchemaService { } // Anchor the schema to Cord blockchain only if ANCHOR_TO_CORD is set to 'True' or 'true' - if ( - process.env.ANCHOR_TO_CORD && - process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' - ) { - try { - const anchorResponse = await this.utilService.anchorSchema({ - schema: data, - }); - - cordSchemaId = anchorResponse.schemaId; - this.logger.debug( - 'Schema successfully anchored to Cord blockchain', - anchorResponse, - ); - } catch (err) { - this.logger.error('Failed to anchor schema to Cord blockchain', err); - throw new InternalServerErrorException( - 'Failed to anchor schema to Cord blockchain', - ); - } + if (this.shouldAnchorToCord()) { + cordSchemaId = await this.anchorSchemaToCord(data); } const credSchema = { schema: { From ef24ec5abf49a87fbf5b7d7d3d9efacdc95fbbdf Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Wed, 9 Oct 2024 18:56:38 +0530 Subject: [PATCH 06/20] add column blockchainStatus:credential schema --- .../migration.sql | 2 - .../migration.sql | 5 + .../credential-schema/prisma/schema.prisma | 8 +- .../src/schema/entities/VCItem.entity.ts | 5 +- .../src/schema/schema.fixtures.ts | 4 +- .../src/schema/schema.service.ts | 181 +++++++++--------- .../credentials-service/src/app.module.ts | 2 +- .../src/credentials/credentials.module.ts | 2 +- .../src/credentials/credentials.service.ts | 15 +- .../credentials/utils/schema.utils.service.ts | 17 +- 10 files changed, 123 insertions(+), 118 deletions(-) delete mode 100644 services/credential-schema/prisma/migrations/20240920194207_add_cord_schema_id/migration.sql create mode 100644 services/credential-schema/prisma/migrations/20241009132114_add_blockchain_status_field/migration.sql diff --git a/services/credential-schema/prisma/migrations/20240920194207_add_cord_schema_id/migration.sql b/services/credential-schema/prisma/migrations/20240920194207_add_cord_schema_id/migration.sql deleted file mode 100644 index 036d6d1bc..000000000 --- a/services/credential-schema/prisma/migrations/20240920194207_add_cord_schema_id/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "VerifiableCredentialSchema" ADD COLUMN "cordSchemaId" TEXT; diff --git a/services/credential-schema/prisma/migrations/20241009132114_add_blockchain_status_field/migration.sql b/services/credential-schema/prisma/migrations/20241009132114_add_blockchain_status_field/migration.sql new file mode 100644 index 000000000..f9038ad60 --- /dev/null +++ b/services/credential-schema/prisma/migrations/20241009132114_add_blockchain_status_field/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "BlockchainStatus" AS ENUM ('PENDING', 'ANCHORED', 'FAILED'); + +-- AlterTable +ALTER TABLE "VerifiableCredentialSchema" ADD COLUMN "blockchainStatus" "BlockchainStatus" NOT NULL DEFAULT 'PENDING'; diff --git a/services/credential-schema/prisma/schema.prisma b/services/credential-schema/prisma/schema.prisma index 89b98b382..a4233b134 100644 --- a/services/credential-schema/prisma/schema.prisma +++ b/services/credential-schema/prisma/schema.prisma @@ -18,6 +18,12 @@ enum SchemaStatus { REVOKED } +enum BlockchainStatus { + PENDING + ANCHORED + FAILED +} + model VerifiableCredentialSchema { id String type String @@ -35,7 +41,7 @@ model VerifiableCredentialSchema { tags String[] status SchemaStatus @default(DRAFT) deprecatedId String? - cordSchemaId String? + blockchainStatus BlockchainStatus @default(PENDING) @@id([id, version]) @@index([type], type: Hash) diff --git a/services/credential-schema/src/schema/entities/VCItem.entity.ts b/services/credential-schema/src/schema/entities/VCItem.entity.ts index 92ae2ad9a..f332b9303 100644 --- a/services/credential-schema/src/schema/entities/VCItem.entity.ts +++ b/services/credential-schema/src/schema/entities/VCItem.entity.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Prisma, VerifiableCredentialSchema } from '@prisma/client'; +import { Prisma, VerifiableCredentialSchema ,BlockchainStatus} from '@prisma/client'; // represents the schema stored in Prisma export class VCItem implements VerifiableCredentialSchema { @@ -34,5 +34,6 @@ export class VCItem implements VerifiableCredentialSchema { createdBy: string; updatedBy: string; deprecatedId: string; - cordSchemaId: string | null; + @ApiProperty({ enum: BlockchainStatus, description: 'Blockchain status' }) + blockchainStatus: BlockchainStatus | null; } diff --git a/services/credential-schema/src/schema/schema.fixtures.ts b/services/credential-schema/src/schema/schema.fixtures.ts index 6b025d60d..7a0d06acf 100644 --- a/services/credential-schema/src/schema/schema.fixtures.ts +++ b/services/credential-schema/src/schema/schema.fixtures.ts @@ -85,7 +85,7 @@ export const testSchemaRespose1: VerifiableCredentialSchema = { tags: ['degree', 'computer science', 'bachelor'], status: 'DRAFT', deprecatedId: null, - cordSchemaId: null + blockchainStatus: null }; export const testSchemaRespose2: VerifiableCredentialSchema = { @@ -105,5 +105,5 @@ export const testSchemaRespose2: VerifiableCredentialSchema = { tags: ['certification', 'blockchain', 'expert'], status: 'DRAFT', deprecatedId: null, - cordSchemaId: null, + blockchainStatus: null, }; \ No newline at end of file diff --git a/services/credential-schema/src/schema/schema.service.ts b/services/credential-schema/src/schema/schema.service.ts index 7e2e19945..cc4764cb1 100644 --- a/services/credential-schema/src/schema/schema.service.ts +++ b/services/credential-schema/src/schema/schema.service.ts @@ -58,7 +58,7 @@ export class SchemaService { createdBy: schema.createdBy, updatedBy: schema.updatedBy, deprecatedId: schema.deprecatedId, - cordSchemaId: schema.cordSchemaId, + blockchainStatus: schema.blockchainStatus, } as GetCredentialSchemaDTO; } else { this.logger.error('schema not found for userInput', userWhereUniqueInput); @@ -119,7 +119,7 @@ export class SchemaService { createdBy: schema.createdBy, updatedBy: schema.updatedBy, deprecatedId: schema.deprecatedId, - cordSchemaId: schema.cordSchemaId, + blockchainStatus: schema.blockchainStatus, }; }); } @@ -158,97 +158,106 @@ export class SchemaService { const data = createCredentialDto.schema; const tags = createCredentialDto.tags; let cordSchemaId: string | null = null; - - // verify the Credential Schema - if (validate(data)) { - let did; - if (generateDID) { - const didBody = { - content: [ - { - alsoKnownAs: [data.author, data.schema.$id], - services: [ - { - id: 'CredentialSchemaService', - type: 'CredentialSchema', - }, - ], - method: 'schema', - }, - ], - }; - did = await this.utilService.generateDID(didBody); - this.logger.debug('DID received from identity service', did); - } - - // Anchor the schema to Cord blockchain only if ANCHOR_TO_CORD is set to 'True' or 'true' - if (this.shouldAnchorToCord()) { - cordSchemaId = await this.anchorSchemaToCord(data); - } - const credSchema = { - schema: { - type: data.type, - id: did ? did.id : data.id, - version: data.version ? data.version : '0.0.0', - name: data.name, - author: data.author, - authored: data.authored, - schema: data.schema, - proof: data.proof, - }, - tags: tags, - status: createCredentialDto.status, - deprecatedId: createCredentialDto.deprecatedId, - cordSchemaId: cordSchemaId, - }; - - // sign the credential schema (only the schema part of the credSchema object above since it is the actual schema) - // const proof = await this.utilService.sign( - // credSchema.schema.author, - // credSchema.schema, - // ); - // credSchema.schema.proof = proof; - - try { - const resp = await this.prisma.verifiableCredentialSchema.create({ - data: { - id: credSchema.schema.id, - type: credSchema.schema?.type as string, - version: credSchema.schema.version, - name: credSchema.schema.name as string, - author: credSchema.schema.author as string, - authored: credSchema.schema.authored, - schema: credSchema.schema.schema as Prisma.JsonValue, - status: credSchema.status as SchemaStatus, - proof: credSchema.schema.proof as Prisma.JsonValue || undefined, - tags: credSchema.tags as string[], - deprecatedId: deprecatedId, - cordSchemaId: (credSchema.cordSchemaId as string) || null, - }, - }); - - credSchema['createdAt'] = resp.createdAt; - credSchema['updatedAt'] = resp.updatedAt; - credSchema['deletedAt'] = resp.deletedAt; - credSchema['createdBy'] = resp.createdBy; - credSchema['updatedBy'] = resp.updatedBy; - } catch (err) { - this.logger.error('Error saving schema to db', err); - throw new InternalServerErrorException('Error saving schema to db'); - } - return credSchema; - } else { + let did: string | null = null; + + // Validate the credential schema + if (!validate(data)) { this.logger.log('Schema validation failed', validate.errors.join('\n')); for (const err of validate.errors as DefinedError[]) { this.logger.error(err, err.message); } throw new BadRequestException( - `Schema validation failed with the following errors: ${validate.errors.join( - '\n', - )}`, + `Schema validation failed with the following errors: ${validate.errors.join('\n')}`, ); } + + // Check if ANCHOR_TO_CORD is enabled, and anchor the schema to Cord if it is + if (this.shouldAnchorToCord()) { + // Anchor the schema to Cord blockchain and retrieve the cordSchemaId (which acts as the DID) + cordSchemaId = await this.anchorSchemaToCord(data); + did = cordSchemaId; + } else if (generateDID) { + // Generate a DID if anchoring is not enabled and generateDID is true + const didBody = { + content: [ + { + alsoKnownAs: [data.author, data.schema.$id], + services: [ + { + id: 'CredentialSchemaService', + type: 'CredentialSchema', + }, + ], + method: 'schema', + }, + ], + }; + + // Generate DID + const generatedDidResponse = await this.utilService.generateDID(didBody); + this.logger.debug('DID received from identity service', generatedDidResponse); + did = generatedDidResponse?.id || null; + + } + + + const credSchema = { + schema: { + type: data.type, + id: did ? did : data.id, + version: data.version ? data.version : '0.0.0', + name: data.name, + author: data.author, + authored: data.authored, + schema: data.schema, + proof: data.proof, + }, + tags: tags, + status: createCredentialDto.status, + deprecatedId: createCredentialDto.deprecatedId, + blockchainStatus: cordSchemaId ? 'ANCHORED' : 'PENDING', + }; + + // // sign the credential schema (only the schema part of the credSchema object above since it is the actual schema) + // // const proof = await this.utilService.sign( + // // credSchema.schema.author, + // // credSchema.schema, + // // ); + // // credSchema.schema.proof = proof; + + // Save the credential schema to the database + try { + const resp = await this.prisma.verifiableCredentialSchema.create({ + data: { + id: credSchema.schema.id, + type: credSchema.schema.type as string, + version: credSchema.schema.version, + name: credSchema.schema.name as string, + author: credSchema.schema.author as string, + authored: credSchema.schema.authored, + schema: credSchema.schema.schema as Prisma.JsonValue, + status: credSchema.status as SchemaStatus, + proof: credSchema.schema.proof as Prisma.JsonValue || undefined, + tags: credSchema.tags as string[], + deprecatedId: deprecatedId, + blockchainStatus: cordSchemaId ? 'ANCHORED' : 'PENDING', + }, + }); + + + credSchema['createdAt'] = resp.createdAt; + credSchema['updatedAt'] = resp.updatedAt; + credSchema['deletedAt'] = resp.deletedAt; + credSchema['createdBy'] = resp.createdBy; + credSchema['updatedBy'] = resp.updatedBy; + + return credSchema; + } catch (err) { + this.logger.error('Error saving schema to db', err); + throw new InternalServerErrorException('Error saving schema to db'); + } } + private formatResponse(schema: VerifiableCredentialSchema) { return JSON.parse( @@ -270,7 +279,7 @@ export class SchemaService { createdBy: schema?.createdBy, updatedBy: schema?.updatedBy, deprecatedId: schema?.deprecatedId, - cordSchemaId: schema?.cordSchemaId, + blockchainStatus: schema?.blockchainStatus, }), ); } diff --git a/services/credentials-service/src/app.module.ts b/services/credentials-service/src/app.module.ts index 13e59c4fa..89eef9b9c 100644 --- a/services/credentials-service/src/app.module.ts +++ b/services/credentials-service/src/app.module.ts @@ -21,6 +21,6 @@ import { RevocationListModule } from './revocation-list/revocation-list.module'; RevocationListModule, ], controllers: [AppController], - providers: [HttpService, HealthCheckService, AppService, ConfigService, PrismaClient, HealthCheckUtilsService, RevocationList, RevocationListImpl, RevocationListService], + providers: [ AppService, ConfigService, PrismaClient, HealthCheckUtilsService, RevocationList, RevocationListImpl, RevocationListService], }) export class AppModule {} diff --git a/services/credentials-service/src/credentials/credentials.module.ts b/services/credentials-service/src/credentials/credentials.module.ts index 2cdc048b0..bd62d4435 100644 --- a/services/credentials-service/src/credentials/credentials.module.ts +++ b/services/credentials-service/src/credentials/credentials.module.ts @@ -10,7 +10,7 @@ import { PrismaClient } from '@prisma/client'; @Module({ imports: [HttpModule], - providers: [HttpService, CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce,AnchorCordUtilsServices], + providers: [ CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce,AnchorCordUtilsServices], controllers: [CredentialsController], exports: [IdentityUtilsService] }) diff --git a/services/credentials-service/src/credentials/credentials.service.ts b/services/credentials-service/src/credentials/credentials.service.ts index df275efd1..0de104d9d 100644 --- a/services/credentials-service/src/credentials/credentials.service.ts +++ b/services/credentials-service/src/credentials/credentials.service.ts @@ -283,7 +283,7 @@ export class CredentialsService { } // Get the credential schema and Cord schema ID - const { schema, cordSchemaId } = await this.schemaUtilsService.getCredentialSchema( + const schema = await this.schemaUtilsService.getCredentialSchema( issueRequest.credentialSchemaId, issueRequest.credentialSchemaVersion ); @@ -319,7 +319,7 @@ export class CredentialsService { // Check if ANCHOR_TO_CORD is true, anchor the unsigned credential using cordSchemaId from the schema if (this.shouldAnchorToCord()) { - response = await this.anchorCredentialToCord(credInReq, cordSchemaId, issueRequest); + response = await this.anchorCredentialToCord(credInReq, issueRequest); } else { response = await this.signAndStoreCredential(credInReq, issueRequest); } @@ -337,18 +337,19 @@ export class CredentialsService { /** * Anchors the credential to Cord blockchain and saves to the DB */ - private async anchorCredentialToCord(credInReq: any, cordSchemaId: string, issueRequest: IssueCredentialDTO) { - if (!cordSchemaId) { - this.logger.error('Cord Schema ID is required for anchoring but is missing'); + private async anchorCredentialToCord(credInReq: any, issueRequest: IssueCredentialDTO) { + if (!issueRequest.credentialSchemaId) { + + this.logger.error('Credential SchemaId Schema ID is required for anchoring but is missing'); throw new BadRequestException('Cord Schema ID is missing'); } try { - this.logger.debug('Anchoring unsigned credential to Cord blockchain with schema ID:', cordSchemaId); + this.logger.debug('Anchoring unsigned credential to Cord blockchain with schema ID:', issueRequest.credentialSchemaId); const anchorResponse = await this.anchorCordUtilsServices.anchorCredential({ ...credInReq, - cordSchemaId, + schemaId: issueRequest.credentialSchemaId, }); this.logger.debug('Credential successfully anchored to Cord:', anchorResponse); diff --git a/services/credentials-service/src/credentials/utils/schema.utils.service.ts b/services/credentials-service/src/credentials/utils/schema.utils.service.ts index 5b6d5c8bc..c0c59eeb0 100644 --- a/services/credentials-service/src/credentials/utils/schema.utils.service.ts +++ b/services/credentials-service/src/credentials/utils/schema.utils.service.ts @@ -14,7 +14,6 @@ export class SchemaUtilsSerivce { private logger = new Logger(SchemaUtilsSerivce.name); async getCredentialSchema(schemaId: string, version: string) { let credSchema: AxiosResponse; - let cordSchemaId: string | null = null; try { credSchema = await this.httpService.axiosRef.get( `${process.env.SCHEMA_BASE_URL}/credential-schema/${schemaId}/${version}` @@ -38,22 +37,8 @@ export class SchemaUtilsSerivce { `Error fetching credential schema` ); } - // If ANCHOR_TO_CORD is true, retrieve cordSchemaId - - if (process.env.ANCHOR_TO_CORD && process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true') { - this.logger.debug('Fetching cordSchemaId as ANCHOR_TO_CORD is enabled'); - cordSchemaId = credSchema?.data?.cordSchemaId || null; - if (!cordSchemaId) { - this.logger.warn(`Cord Schema ID not found for schema ID ${schemaId}`); - throw new BadRequestException(`Cord Schema ID missing for schema ID ${schemaId}`); - } - } - - return { - schema: credSchema?.data?.schema, - cordSchemaId: cordSchemaId - }; + return credSchema?.data?.schema; } async getTemplateById(templateId: string) { From ca99d4adb12ffabb12bbc0ad9db301982461817f Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:11:08 +0530 Subject: [PATCH 07/20] add block chains status field: credential --- .../migration.sql | 5 +++++ services/credentials-service/prisma/schema.prisma | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 services/credentials-service/prisma/migrations/20241010105751_add_blockchain_status_field/migration.sql diff --git a/services/credentials-service/prisma/migrations/20241010105751_add_blockchain_status_field/migration.sql b/services/credentials-service/prisma/migrations/20241010105751_add_blockchain_status_field/migration.sql new file mode 100644 index 000000000..c8eb5a20a --- /dev/null +++ b/services/credentials-service/prisma/migrations/20241010105751_add_blockchain_status_field/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "BlockchainStatusEnum" AS ENUM ('PENDING', 'ANCHORED', 'FAILED'); + +-- AlterTable +ALTER TABLE "VerifiableCredentials" ADD COLUMN "blockchainStatus" "BlockchainStatusEnum" NOT NULL DEFAULT 'PENDING'; diff --git a/services/credentials-service/prisma/schema.prisma b/services/credentials-service/prisma/schema.prisma index 784778ab6..d594b4423 100644 --- a/services/credentials-service/prisma/schema.prisma +++ b/services/credentials-service/prisma/schema.prisma @@ -18,6 +18,12 @@ enum VCStatus { REVOKED } +enum BlockchainStatusEnum { + PENDING + ANCHORED + FAILED +} + model VerifiableCredentials { id String @id @default(uuid()) type String[] @@ -36,6 +42,7 @@ model VerifiableCredentials { createdBy String? updatedBy String? tags String[] + blockchainStatus BlockchainStatusEnum @default(PENDING) } model RevocationLists { From cb25e434d7f195e79e9c96bd563277c5f75c2f66 Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:24:20 +0530 Subject: [PATCH 08/20] update blockchainstatus --- .../credentials-service/src/credentials/credentials.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/credentials-service/src/credentials/credentials.service.ts b/services/credentials-service/src/credentials/credentials.service.ts index 0de104d9d..6716ca2d9 100644 --- a/services/credentials-service/src/credentials/credentials.service.ts +++ b/services/credentials-service/src/credentials/credentials.service.ts @@ -370,6 +370,8 @@ export class CredentialsService { credential_schema: issueRequest.credentialSchemaId, signed: anchorResponse.vc as object, tags: issueRequest.tags, + blockchainStatus:"ANCHORED" + }; return this.saveCredentialToDatabase(anchoredCredentialData); From ea6b2850dd55b513e34700fbfa9f3024c5ad09bb Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:52:39 +0530 Subject: [PATCH 09/20] feat:anchor did to cord block chain --- docker-compose.yml | 2 + .../src/credentials/credentials.service.ts | 231 +++++++++--------- services/identity-service/.env.sample | 10 +- services/identity-service/docker-compose.yml | 3 + services/identity-service/src/app.module.ts | 3 +- .../src/did/did.controller.ts | 4 + .../identity-service/src/did/did.module.ts | 2 + .../identity-service/src/did/did.service.ts | 161 +++++++----- .../src/utils/cord.service.ts | 40 +++ services/identity-service/src/vc/vc.module.ts | 3 +- 10 files changed, 277 insertions(+), 182 deletions(-) create mode 100644 services/identity-service/src/utils/cord.service.ts diff --git a/docker-compose.yml b/docker-compose.yml index 297612a92..d199e9552 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -243,6 +243,8 @@ services: - JWKS_URI=${JWKS_URI} - ENABLE_AUTH=${ENABLE_AUTH} - WEB_DID_BASE_URL=${WEB_DID_BASE_URL} + - ANCHOR_TO_CORD=${ANCHOR_TO_CORD} + - ISSUER_AGENT_BASE_URL=${ISSUER_AGENT_BASE_URL} healthcheck: test: [ "CMD-SHELL", "curl -f http://localhost:3332/health || exit 1" ] diff --git a/services/credentials-service/src/credentials/credentials.service.ts b/services/credentials-service/src/credentials/credentials.service.ts index 6716ca2d9..d84498402 100644 --- a/services/credentials-service/src/credentials/credentials.service.ts +++ b/services/credentials-service/src/credentials/credentials.service.ts @@ -19,10 +19,10 @@ import * as jsonld from 'jsonld'; import { DOCUMENTS } from './documents'; import { RSAKeyPair } from 'crypto-ld'; const AssertionProofPurpose = jsigs.purposes.AssertionProofPurpose; -import { +import { W3CCredential, Verifiable, DIDDocument, CredentialPayload, IssuerType, Proof, VerificationMethod - } from 'vc.types'; +} from 'vc.types'; import { RevocationListDTO } from './dto/revocaiton-list.dto'; @Injectable() @@ -50,10 +50,10 @@ export class CredentialsService { async init() { const vc = await import('@digitalbazaar/vc'); - const {Ed25519VerificationKey2020} = await import('@digitalbazaar/ed25519-verification-key-2020'); - const {Ed25519Signature2020} = await import('@digitalbazaar/ed25519-signature-2020'); - const {Ed25519VerificationKey2018} = await import('@digitalbazaar/ed25519-verification-key-2018'); - const {Ed25519Signature2018} = await import('@digitalbazaar/ed25519-signature-2018'); + const { Ed25519VerificationKey2020 } = await import('@digitalbazaar/ed25519-verification-key-2020'); + const { Ed25519Signature2020 } = await import('@digitalbazaar/ed25519-signature-2020'); + const { Ed25519VerificationKey2018 } = await import('@digitalbazaar/ed25519-verification-key-2018'); + const { Ed25519Signature2018 } = await import('@digitalbazaar/ed25519-signature-2018'); this.map.Ed25519VerificationKey2020 = Ed25519VerificationKey2020; this.map.JsonWebKey2020 = Ed25519VerificationKey2020; this.map.Ed25519Signature2020 = Ed25519Signature2020; @@ -150,8 +150,7 @@ export class CredentialsService { async verifyCredential(credToVerify: Verifiable, status?: VCStatus) { try { - - // If ANCHOR_TO_CORD is true, delegate verification to Cord Verification MiddleWare service + // If ANCHOR_TO_CORD is true, delegate verification to Cord Verification MiddleWare service if (this.shouldAnchorToCord()) { return await this.anchorCordUtilsServices.verifyCredentialOnCord(credToVerify); } @@ -167,7 +166,7 @@ export class CredentialsService { const vm = did.verificationMethod?.find(d => (d.id === credVerificationMethod || d.id === credVerificationMethod?.id)); const suite = await this.getSuite(vm, credToVerify?.proof?.type); let results; - if(credToVerify?.proof?.type === "RsaSignature2018") { + if (credToVerify?.proof?.type === "RsaSignature2018") { this.map.vc._checkCredential({ credential: credToVerify }) @@ -186,14 +185,14 @@ export class CredentialsService { documentLoader: this.getDocumentLoader(did) }); } - if(!results?.verified) { + if (!results?.verified) { this.logger.error('Error in verifying credentials: ', results); } return { status: status, checks: [ { - ...(status && {revoked: status === VCStatus.REVOKED ? 'NOK' : 'OK'}), // NOK represents revoked + ...(status && { revoked: status === VCStatus.REVOKED ? 'NOK' : 'OK' }), // NOK represents revoked expired: new Date(credToVerify.expirationDate).getTime() < Date.now() ? 'NOK' @@ -214,14 +213,14 @@ export class CredentialsService { // getting the credential from the db const stored = (await this.prisma.verifiableCredentials.findUnique({ - where: { - id: credId, - }, - select: { - signed: true, - status: true, - }, - })); + where: { + id: credId, + }, + select: { + signed: true, + status: true, + }, + })); const { signed: credToVerify, status } = (stored || {}) as { signed: Verifiable; status: VCStatus }; this.logger.debug('Fetched credntial from db to verify'); @@ -240,28 +239,28 @@ export class CredentialsService { "Ed25519Signature2018": ["Ed25519VerificationKey2018"], "RsaSignature2018": ["RsaVerificationKey2018"], }; - if(!(signatureType in supportedSignatures)) { + if (!(signatureType in supportedSignatures)) { throw new NotFoundException("Suite for signature type not found"); } - if(!supportedSignatures[signatureType].includes(verificationMethod?.type)) { + if (!supportedSignatures[signatureType].includes(verificationMethod?.type)) { throw new NotFoundException("Suite for verification type not found"); } - if(!this.map[verificationMethod?.type]) await this.init(); - if(!this.map[verificationMethod?.type]) throw new NotFoundException("Library not loaded"); + if (!this.map[verificationMethod?.type]) await this.init(); + if (!this.map[verificationMethod?.type]) throw new NotFoundException("Library not loaded"); let keyPair = await this.map[verificationMethod?.type].from(verificationMethod); - return new this.map[signatureType]({key: keyPair}); + return new this.map[signatureType]({ key: keyPair }); } getDocumentLoader(didDoc: DIDDocument) { return jsigs.extendContextLoader(async url => { - if(url === didDoc?.id) { + if (url === didDoc?.id) { return { contextUrl: null, documentUrl: url, document: didDoc }; } - if(DOCUMENTS[url]) { + if (DOCUMENTS[url]) { return { contextUrl: null, documentUrl: url, @@ -276,117 +275,113 @@ export class CredentialsService { async issueCredential(issueRequest: IssueCredentialDTO) { this.logger.debug(`Received issue credential request`); const credInReq = issueRequest.credential; - - // Check for issuance date - if (!credInReq.issuanceDate) { - credInReq.issuanceDate = new Date(Date.now()).toISOString(); - } - - // Get the credential schema and Cord schema ID - const schema = await this.schemaUtilsService.getCredentialSchema( - issueRequest.credentialSchemaId, - issueRequest.credentialSchemaVersion - ); - - this.logger.debug('fetched schema', schema); - - const { valid, errors } = await this.schemaUtilsService.verifyCredentialSubject( - credInReq, - schema.schema - ); - - if (!valid) { - this.logger.error('Invalid credential schema', errors); - throw new BadRequestException(errors); - } - - this.logger.debug('validated schema'); - - // Generate the DID for the credential - const credDID: ReadonlyArray = await this.identityUtilsService.generateDID([], issueRequest.method); - this.logger.debug('generated DID'); - - try { - credInReq.id = credDID[0].id; - } catch (err) { - this.logger.error('Invalid response from generate DID', err); - throw new InternalServerErrorException('Problem creating DID'); - } - - this.logger.debug('Generated DID and validated schema'); - + let response: any = null; - - // Check if ANCHOR_TO_CORD is true, anchor the unsigned credential using cordSchemaId from the schema + + // Check if ANCHOR_TO_CORD is true if (this.shouldAnchorToCord()) { response = await this.anchorCredentialToCord(credInReq, issueRequest); } else { + // Check for issuance date + if (!credInReq.issuanceDate) { + credInReq.issuanceDate = new Date(Date.now()).toISOString(); + } + + // Get the credential schema ID + const schema = await this.schemaUtilsService.getCredentialSchema( + issueRequest.credentialSchemaId, + issueRequest.credentialSchemaVersion + ); + + this.logger.debug('fetched schema', schema); + + const { valid, errors } = await this.schemaUtilsService.verifyCredentialSubject( + credInReq, + schema.schema + ); + + if (!valid) { + this.logger.error('Invalid credential schema', errors); + throw new BadRequestException(errors); + } + + // Generate the DID for the credential + const credDID: ReadonlyArray = await this.identityUtilsService.generateDID([], issueRequest.method); + + try { + credInReq.id = credDID[0].id; + } catch (err) { + this.logger.error('Invalid response from generate DID', err); + throw new InternalServerErrorException('Problem creating DID'); + } + + this.logger.debug('Generated DID and validated schema'); response = await this.signAndStoreCredential(credInReq, issueRequest); } - + return response; } - + /** * Determines if ANCHOR_TO_CORD environment variable is true */ private shouldAnchorToCord(): boolean { return process.env.ANCHOR_TO_CORD && process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true'; } - + /** * Anchors the credential to Cord blockchain and saves to the DB */ private async anchorCredentialToCord(credInReq: any, issueRequest: IssueCredentialDTO) { if (!issueRequest.credentialSchemaId) { - + this.logger.error('Credential SchemaId Schema ID is required for anchoring but is missing'); throw new BadRequestException('Cord Schema ID is missing'); } - + try { this.logger.debug('Anchoring unsigned credential to Cord blockchain with schema ID:', issueRequest.credentialSchemaId); - + const anchorResponse = await this.anchorCordUtilsServices.anchorCredential({ ...credInReq, schemaId: issueRequest.credentialSchemaId, }); - + this.logger.debug('Credential successfully anchored to Cord:', anchorResponse); - const { - id , issuer, issuanceDate, validUntil: expirationDate, credentialSubject, proof , - } = anchorResponse.vc; - - const anchoredCredentialData = { - id, - type: issueRequest.credential.type, - issuer, - issuanceDate, - expirationDate, - subject: credentialSubject, - subjectId: (credentialSubject as JwtCredentialSubject).id, - proof, - credential_schema: issueRequest.credentialSchemaId, - signed: anchorResponse.vc as object, - tags: issueRequest.tags, - blockchainStatus:"ANCHORED" - - }; - + const { + id, issuer, issuanceDate, validUntil: expirationDate, credentialSubject, proof, + } = anchorResponse.vc; + + const anchoredCredentialData = { + id, + type: issueRequest.credential.type, + issuer, + issuanceDate, + expirationDate, + subject: credentialSubject, + subjectId: (credentialSubject as JwtCredentialSubject).id, + proof, + credential_schema: issueRequest.credentialSchemaId, + signed: anchorResponse.vc as object, + tags: issueRequest.tags, + blockchainStatus: "ANCHORED" + + }; + return this.saveCredentialToDatabase(anchoredCredentialData); } catch (err) { this.logger.error('Error anchoring credential to Cord blockchain:', err); throw new InternalServerErrorException('Error anchoring credential to Cord blockchain'); } } - + /** * Signs the credential locally and saves it to the database */ private async signAndStoreCredential(credInReq: any, issueRequest: IssueCredentialDTO) { let signedCredential: W3CCredential = {}; - + try { signedCredential = await this.identityUtilsService.signVC( credInReq as CredentialPayload, @@ -396,9 +391,9 @@ export class CredentialsService { this.logger.error('Error signing the credential'); throw new InternalServerErrorException('Problem signing the credential'); } - + this.logger.debug('Signed credential'); - + const newCredData = { id: signedCredential.id, type: signedCredential.type, @@ -412,15 +407,15 @@ export class CredentialsService { signed: signedCredential as object, tags: issueRequest.tags, }; - + return this.saveCredentialToDatabase(newCredData); } - + /** * Saves the credential to the database and returns the response */ private async saveCredentialToDatabase(credentialData: any) { - + const newCred = await this.prisma.verifiableCredentials.create({ data: credentialData, }); @@ -429,13 +424,13 @@ export class CredentialsService { this.logger.error('Problem saving credential to db'); throw new InternalServerErrorException('Problem saving credential to db'); } - + this.logger.debug('saved credential to db'); - + const res = newCred.signed; delete res['options']; - + return { credential: res, credentialSchemaId: newCred.credential_schema, @@ -446,7 +441,7 @@ export class CredentialsService { tags: newCred.tags, }; } - + async deleteCredential(id: string) { try { const credential = await this.prisma.verifiableCredentials.update({ @@ -473,11 +468,11 @@ export class CredentialsService { issuer: getCreds.issuer?.id, AND: filteringSubject ? Object.keys(filteringSubject).map((key: string) => ({ - subject: { - path: [key.toString()], - equals: filteringSubject[key], - }, - })) + subject: { + path: [key.toString()], + equals: filteringSubject[key], + }, + })) : [], }, select: { @@ -508,14 +503,14 @@ export class CredentialsService { async getRevocationList( issuerId: string, page = 1, - limit= 1000, - ){ + limit = 1000, + ) { let revocationList: RevocationListDTO[] if (issuerId === "") { throw new InternalServerErrorException('Please provide a valid issuer ID'); } - + try { revocationList = await this.prisma.verifiableCredentials.findMany({ where: { @@ -524,20 +519,20 @@ export class CredentialsService { }, select: { id: true, - tags : true, - issuer : true, + tags: true, + issuer: true, issuanceDate: true }, - skip: (page -1) * limit, + skip: (page - 1) * limit, take: limit, orderBy: { issuanceDate: 'desc', }, - }); + }); } catch (error) { this.logger.error('Error fetching RevocationList'); throw new InternalServerErrorException('Error fetching revocationList'); } - return revocationList + return revocationList } } diff --git a/services/identity-service/.env.sample b/services/identity-service/.env.sample index cfba06cec..ffdd8f8b5 100644 --- a/services/identity-service/.env.sample +++ b/services/identity-service/.env.sample @@ -18,4 +18,12 @@ VAULT_PROXY='' #this is supposed to be a boolean flag given as a string # Configs for the server PORT=3332 -ENABLE_AUTH=false \ No newline at end of file +ENABLE_AUTH=false + +# Flag to enable/disable anchoring to Cord blockchain +ANCHOR_TO_CORD=true + +# Base URL for Issuer Agent +# This is the service responsible for create did in cord block chain +# Example: https:///api/v1 +ISSUER_AGENT_BASE_URL=https:///api/v1 \ No newline at end of file diff --git a/services/identity-service/docker-compose.yml b/services/identity-service/docker-compose.yml index 32dd8dfd0..d9cba7a5d 100644 --- a/services/identity-service/docker-compose.yml +++ b/services/identity-service/docker-compose.yml @@ -59,6 +59,9 @@ services: JWKS_URI: ENABLE_AUTH: "false" WEB_DID_BASE_URL: + ANCHOR_TO_CORD: "false" + ISSUER_AGENT_BASE_URL : + healthcheck: test: [ "CMD-SHELL", "curl -f http://localhost:3332/health || exit 1" ] diff --git a/services/identity-service/src/app.module.ts b/services/identity-service/src/app.module.ts index 9a0bedaba..313f9ebba 100644 --- a/services/identity-service/src/app.module.ts +++ b/services/identity-service/src/app.module.ts @@ -13,6 +13,7 @@ import { AuthGuard } from './auth/auth.guard'; import { TerminusModule } from '@nestjs/terminus'; import { PrismaHealthIndicator } from './utils/prisma.health'; import { VaultHealthIndicator } from './utils/vault.health'; +import { AnchorCordService } from './utils/cord.service'; @Module({ imports: [ @@ -26,7 +27,7 @@ import { VaultHealthIndicator } from './utils/vault.health'; ], controllers: [AppController, DidController], providers: [ - PrismaService, DidService, VaultService, + PrismaService, DidService, VaultService,AnchorCordService, { provide: APP_GUARD, useClass: AuthGuard, diff --git a/services/identity-service/src/did/did.controller.ts b/services/identity-service/src/did/did.controller.ts index 1a5bb8e20..550bd7e28 100644 --- a/services/identity-service/src/did/did.controller.ts +++ b/services/identity-service/src/did/did.controller.ts @@ -3,6 +3,7 @@ import { Controller, Get, InternalServerErrorException, + HttpException, Logger, Param, Post, @@ -44,6 +45,9 @@ export class DidController { try { return await Promise.all(promises); } catch (err) { + if (err instanceof HttpException) { + throw err; + } Logger.error(err); throw new InternalServerErrorException(err?.message); } diff --git a/services/identity-service/src/did/did.module.ts b/services/identity-service/src/did/did.module.ts index 1955642da..8c8fd9d09 100644 --- a/services/identity-service/src/did/did.module.ts +++ b/services/identity-service/src/did/did.module.ts @@ -5,6 +5,7 @@ import { PrismaService } from 'src/utils/prisma.service'; import { DidController } from './did.controller'; import { DidService } from './did.service'; import { VaultService } from '../utils/vault.service'; +import { AnchorCordService } from 'src/utils/cord.service'; @Module({ imports: [HttpModule], @@ -13,6 +14,7 @@ import { VaultService } from '../utils/vault.service'; DidService, PrismaService, VaultService, + AnchorCordService ], }) export class DidModule {} diff --git a/services/identity-service/src/did/did.service.ts b/services/identity-service/src/did/did.service.ts index 73db04f23..89233884f 100644 --- a/services/identity-service/src/did/did.service.ts +++ b/services/identity-service/src/did/did.service.ts @@ -1,10 +1,11 @@ -import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException, Logger, NotFoundException ,BadRequestException,HttpException} from '@nestjs/common'; import { PrismaService } from '../utils/prisma.service'; import { v4 as uuid } from 'uuid'; import { VaultService } from '../utils/vault.service'; import { Identity } from '@prisma/client'; import { RSAKeyPair } from 'crypto-ld'; import { GenerateDidDTO } from './dtos/GenerateDidRequest.dto'; +import { AnchorCordService } from 'src/utils/cord.service'; const { DIDDocument } = require('did-resolver'); type DIDDocument = typeof DIDDocument; @@ -21,7 +22,7 @@ export class DidService { webDidPrefix: string; signingAlgorithm: string; didResolver: any; - constructor(private prisma: PrismaService, private vault: VaultService) { + constructor(private prisma: PrismaService, private vault: VaultService , private anchorcord:AnchorCordService) { let baseUrl: string = process.env.WEB_DID_BASE_URL; this.webDidPrefix = this.getDidPrefixForBaseUrl(baseUrl); this.signingAlgorithm = process.env.SIGNING_ALGORITHM; @@ -97,86 +98,117 @@ export class DidService { if(name && !verificationKey) throw new NotFoundException("Verification Key '" + name + "' not found"); return verificationKey; } - async generateDID(doc: GenerateDidDTO): Promise { - // Create a UUID for the DID using uuidv4 - const didUri: string = this.generateDidUri(doc?.method, doc?.id, doc?.webDidBaseUrl); - - // Create private/public key pair - let authnKeys; + let didUri: string; + let document: DIDDocument; let privateKeys: object; - let verificationKey = await this.getVerificationKeyByName(doc?.keyPairType); - if(!verificationKey) verificationKey = await this.getVerificationKey(this.signingAlgorithm); - try { - const keyPair = await (verificationKey as any)?.key.generate({ - id: `${didUri}#key-0`, - controller: didUri - }); - const exportedKey = await keyPair.export({ - publicKey: true, privateKey: true, includeContext: true - }); - let privateKey = {}; - if(verificationKey?.name === "Ed25519VerificationKey2020") { - const {privateKeyMultibase, ...rest } = exportedKey; - authnKeys = rest; - privateKey = {privateKeyMultibase}; - } else if(verificationKey?.name === "Ed25519VerificationKey2018") { - const {privateKeyBase58, ...rest } = exportedKey; - authnKeys = rest; - privateKey = {privateKeyBase58}; - } else if(verificationKey?.name === "RsaVerificationKey2018") { - const {privateKeyPem, ...rest } = exportedKey; - authnKeys = {...rest}; - privateKey = {privateKeyPem}; - } else { - throw new NotFoundException("VerificationKey type not found"); + let blockchainStatus: boolean = false; + + if (this.shouldAnchorToCord()) { + try { + if (doc.method !== 'cord') { + throw new BadRequestException('Invalid method: only "cord" is allowed for anchoring to Cord.'); + } + const response = await this.anchorcord.anchorDid(doc); + didUri = response.document.uri; + document = response.document; + + // store mnemonic and delegate keys in to vault + privateKeys = { + "mnemonic":response.mnemonic, + "delegateKeys":response.delegateKeys + }; + blockchainStatus = true; + } catch (err) { + if (err instanceof HttpException) { + throw err; + } + Logger.error(`Error anchoring to Cord: ${err}`); + throw new InternalServerErrorException('Failed to anchor DID to Cord blockchain'); + } + } else { + + didUri = this.generateDidUri(doc?.method, doc?.id, doc?.webDidBaseUrl); + + let authnKeys; + let verificationKey = await this.getVerificationKeyByName(doc?.keyPairType); + if (!verificationKey) verificationKey = await this.getVerificationKey(this.signingAlgorithm); + + try { + const keyPair = await (verificationKey as any)?.key.generate({ + id: `${didUri}#key-0`, + controller: didUri + }); + const exportedKey = await keyPair.export({ + publicKey: true, privateKey: true, includeContext: true + }); + + let privateKey = {}; + if (verificationKey?.name === "Ed25519VerificationKey2020") { + const { privateKeyMultibase, ...rest } = exportedKey; + authnKeys = rest; + privateKey = { privateKeyMultibase }; + } else if (verificationKey?.name === "Ed25519VerificationKey2018") { + const { privateKeyBase58, ...rest } = exportedKey; + authnKeys = rest; + privateKey = { privateKeyBase58 }; + } else if (verificationKey?.name === "RsaVerificationKey2018") { + const { privateKeyPem, ...rest } = exportedKey; + authnKeys = { ...rest }; + privateKey = { privateKeyPem }; + } else { + throw new NotFoundException("VerificationKey type not found"); + } + + privateKeys = { + [authnKeys.id]: privateKey + }; + + const keyId = authnKeys?.id; + + document = { + '@context': [ + "https://www.w3.org/ns/did/v1" + ], + id: didUri, + alsoKnownAs: doc.alsoKnownAs, + service: doc.services, + verificationMethod: [ + authnKeys, + ], + authentication: [keyId], + assertionMethod: [keyId] + }; + + } catch (err: any) { + Logger.error(`Error generating key pair: ${err}`); + throw new InternalServerErrorException('Error generating key pair: ' + err.message); } - privateKeys = { - [authnKeys.id]: privateKey - }; - } catch (err: any) { - Logger.error(`Error generating key pair: ${err}`); - throw new InternalServerErrorException('Error generating key pair: ' + err.message); } - - const keyId = authnKeys?.id; - - // Create a DID Document - const document: DIDDocument = { - '@context': [ - "https://www.w3.org/ns/did/v1" - ], - id: didUri, - alsoKnownAs: doc.alsoKnownAs, - service: doc.services, - verificationMethod: [ - authnKeys, - ], - authentication: [keyId], - assertionMethod: [keyId] - }; - + try { await this.prisma.identity.create({ data: { id: didUri, didDoc: JSON.stringify(document), + blockchainStatus: blockchainStatus, }, }); } catch (err) { - Logger.error(`Error writing DID to database ${err}`); + Logger.error(`Error writing DID to database: ${err}`); throw new InternalServerErrorException('Error writing DID to database'); } - + try { await this.vault.writePvtKey(privateKeys, didUri); } catch (err) { - Logger.error(err); + Logger.error(`Error writing private key to vault: ${err}`); throw new InternalServerErrorException('Error writing private key to vault'); } - + return document; } + async resolveDID(id: string): Promise { let artifact: Identity; @@ -211,4 +243,11 @@ export class DidService { const webDidId = this.getWebDidIdForId(id); return this.resolveDID(webDidId); } + + private shouldAnchorToCord(): boolean { + return ( + process.env.ANCHOR_TO_CORD && + process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' + ); + } } diff --git a/services/identity-service/src/utils/cord.service.ts b/services/identity-service/src/utils/cord.service.ts new file mode 100644 index 000000000..ea20f244e --- /dev/null +++ b/services/identity-service/src/utils/cord.service.ts @@ -0,0 +1,40 @@ +import { HttpService } from '@nestjs/axios'; +import { + HttpException, + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; + +@Injectable() +export class AnchorCordService { + constructor(private readonly httpService: HttpService) {} + private logger = new Logger(AnchorCordService.name); + + async anchorDid(body: any): Promise { + try { + const response = await this.httpService.axiosRef.post( + `${process.env.ISSUER_AGENT_BASE_URL}/did/create/`, + body, + ); + return response.data; + } catch (err) { + const errorDetails = { + message: err.message, + status: err.response?.status, + statusText: err.response?.statusText, + data: err.response?.data, + headers: err.response?.headers, + request: err.config, + }; + + this.logger.error( + 'Error anchoring did to Cord blockchain', + errorDetails, + ); + throw new InternalServerErrorException( + 'Failed to anchor did to Cord blockchain', + ); + } + } +} diff --git a/services/identity-service/src/vc/vc.module.ts b/services/identity-service/src/vc/vc.module.ts index 4038eef50..42583550a 100644 --- a/services/identity-service/src/vc/vc.module.ts +++ b/services/identity-service/src/vc/vc.module.ts @@ -5,10 +5,11 @@ import { VaultService } from 'src/utils/vault.service'; import { PrismaService } from 'src/utils/prisma.service'; import { VcController } from './vc.controller'; import VcService from './vc.service'; +import { AnchorCordService } from 'src/utils/cord.service'; @Module({ imports: [HttpModule], controllers: [VcController], - providers: [VcService, PrismaService, DidService, VaultService], + providers: [VcService, PrismaService, DidService, VaultService,AnchorCordService], }) export class VcModule {} From dc1cae0359fd59701151969bfc07bf46d2d029d5 Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:22:06 +0530 Subject: [PATCH 10/20] Add necessary environment variables to identity Docker Compose for CORD anchoring --- services/identity-service/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/identity-service/docker-compose.yml b/services/identity-service/docker-compose.yml index d9cba7a5d..9592fc4ee 100644 --- a/services/identity-service/docker-compose.yml +++ b/services/identity-service/docker-compose.yml @@ -10,6 +10,8 @@ services: - VAULT_ADDR=http://0.0.0.0:8200 - VAULT_API_ADDR=http://0.0.0.0:8200 - VAULT_ADDRESS=http://0.0.0.0:8200 + - ANCHOR_TO_CORD=false + - ISSUER_AGENT_BASE_URL= cap_add: - IPC_LOCK command: vault server -config=/vault/config/vault.json From 654471c1bf6c9165e9dda9d47d631017ef71ee64 Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:18:47 +0530 Subject: [PATCH 11/20] test: add AnchorCordService to test file-didservice --- services/identity-service/src/did/did.controller.spec.ts | 3 ++- services/identity-service/src/did/did.service.spec.ts | 3 ++- services/identity-service/src/vc/vc.service.spec.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/services/identity-service/src/did/did.controller.spec.ts b/services/identity-service/src/did/did.controller.spec.ts index b006b43ba..a80bd5fd3 100644 --- a/services/identity-service/src/did/did.controller.spec.ts +++ b/services/identity-service/src/did/did.controller.spec.ts @@ -4,6 +4,7 @@ import { InternalServerErrorException } from '@nestjs/common'; import { DidService } from './did.service'; import { PrismaService } from '../utils/prisma.service'; import { VaultService } from '../utils/vault.service'; +import { AnchorCordService } from 'src/utils/cord.service'; import { ConfigService } from '@nestjs/config'; describe('DidController', () => { @@ -38,7 +39,7 @@ describe('DidController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [DidController], - providers: [DidService, PrismaService, ConfigService, VaultService] + providers: [DidService, PrismaService, ConfigService, VaultService,AnchorCordService] }).compile(); controller = module.get(DidController); diff --git a/services/identity-service/src/did/did.service.spec.ts b/services/identity-service/src/did/did.service.spec.ts index dead5270e..06dd00f43 100644 --- a/services/identity-service/src/did/did.service.spec.ts +++ b/services/identity-service/src/did/did.service.spec.ts @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DidService } from './did.service'; import { PrismaService } from '../utils/prisma.service'; import { VaultService } from '../utils/vault.service'; +import { AnchorCordService } from 'src/utils/cord.service'; import { GenerateDidDTO, VerificationKeyType } from './dtos/GenerateDidRequest.dto'; import { ConfigService } from '@nestjs/config'; @@ -32,7 +33,7 @@ describe('DidService', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [DidService, PrismaService, VaultService, ConfigService], + providers: [DidService, PrismaService, VaultService, ConfigService,AnchorCordService], }).compile(); service = module.get(DidService); diff --git a/services/identity-service/src/vc/vc.service.spec.ts b/services/identity-service/src/vc/vc.service.spec.ts index 7c8c29d25..2b3c71b10 100644 --- a/services/identity-service/src/vc/vc.service.spec.ts +++ b/services/identity-service/src/vc/vc.service.spec.ts @@ -3,7 +3,7 @@ import VcService from './vc.service'; import { PrismaService } from '../utils/prisma.service'; import { DidService } from '../did/did.service'; import { VaultService } from '../utils/vault.service'; - +import { AnchorCordService } from 'src/utils/cord.service'; describe('DidService', () => { let service: VcService; let didService: DidService; @@ -17,7 +17,7 @@ describe('DidService', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [VcService, PrismaService, DidService, VaultService], + providers: [VcService, PrismaService, DidService, VaultService,AnchorCordService], }).compile(); service = module.get(VcService); From b18c6296829c4bc0440add42eaf9f3316804f57c Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:45:12 +0530 Subject: [PATCH 12/20] test: add BlockchainStatus field in to specs.ts --- services/credential-schema/.env.sample | 2 +- services/credential-schema/src/schema/schema.service.spec.ts | 1 + services/credentials-service/.env.sample | 2 +- services/credentials-service/docker-compose-test.yml | 3 +++ services/identity-service/docker-compose-test.yml | 2 ++ 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/services/credential-schema/.env.sample b/services/credential-schema/.env.sample index 3c10b7ee6..4eddbad4e 100644 --- a/services/credential-schema/.env.sample +++ b/services/credential-schema/.env.sample @@ -14,7 +14,7 @@ SCHEMA_BASE_URL= # Flag to enable/disable anchoring to Cord blockchain -ANCHOR_TO_CORD=true +ANCHOR_TO_CORD=false # Base URL for Issuer Agent # This is the service responsible for issuing credentials diff --git a/services/credential-schema/src/schema/schema.service.spec.ts b/services/credential-schema/src/schema/schema.service.spec.ts index 244e3efae..ff14d868f 100644 --- a/services/credential-schema/src/schema/schema.service.spec.ts +++ b/services/credential-schema/src/schema/schema.service.spec.ts @@ -320,6 +320,7 @@ describe('SchemaService', () => { createdBy: schema.createdBy, updatedBy: schema.updatedBy, deprecatedId: schema.deprecatedId, + blockchainStatus:schema.blockchainStatus, }))); }); diff --git a/services/credentials-service/.env.sample b/services/credentials-service/.env.sample index d29788336..398c5e168 100644 --- a/services/credentials-service/.env.sample +++ b/services/credentials-service/.env.sample @@ -15,7 +15,7 @@ CREDENTIAL_SERVICE_BASE_URL="" # Flag to enable/disable anchoring to Cord blockchain -ANCHOR_TO_CORD=true +ANCHOR_TO_CORD=false # Base URL for Issuer Agent # This is the service responsible for issuing credentials diff --git a/services/credentials-service/docker-compose-test.yml b/services/credentials-service/docker-compose-test.yml index 272f1c64f..6cf743715 100644 --- a/services/credentials-service/docker-compose-test.yml +++ b/services/credentials-service/docker-compose-test.yml @@ -25,6 +25,9 @@ services: ENABLE_AUTH: "false" JWKS_URI: "" SIGNING_ALGORITHM: "Ed25519Signature2020" + ANCHOR_TO_CORD: "${ANCHOR_TO_CORD}" + ISSUER_AGENT_BASE_URL: "${ISSUER_AGENT_BASE_URL}" + VERIFICATION_MIDDLEWARE_BASE_URL: "${VERIFICATION_MIDDLEWARE_BASE_URL}" networks: rcw-test: default: diff --git a/services/identity-service/docker-compose-test.yml b/services/identity-service/docker-compose-test.yml index 19c605ae7..e2b601d9f 100644 --- a/services/identity-service/docker-compose-test.yml +++ b/services/identity-service/docker-compose-test.yml @@ -58,6 +58,8 @@ services: JWKS_URI: "" ENABLE_AUTH: "false" WEB_DID_BASE_URL: "https://example.com/identity" + ANCHOR_TO_CORD: "false" + ISSUER_AGENT_BASE_URL : networks: test: healthcheck: From 1c4cacff653c64d39e6178c7f9dcf2b1ba101f03 Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:26:29 +0530 Subject: [PATCH 13/20] test:add AnchorCordUtilsServices in credentials service specs files --- .../src/credentials/credentials.service.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/credentials-service/src/credentials/credentials.service.spec.ts b/services/credentials-service/src/credentials/credentials.service.spec.ts index ca94ff28e..50f6d49be 100644 --- a/services/credentials-service/src/credentials/credentials.service.spec.ts +++ b/services/credentials-service/src/credentials/credentials.service.spec.ts @@ -5,6 +5,7 @@ import { UnsignedVCValidator, VCValidator } from './types/validators'; import { SchemaUtilsSerivce } from './utils/schema.utils.service'; import { IdentityUtilsService } from './utils/identity.utils.service'; import { RenderingUtilsService } from './utils/rendering.utils.service'; +import { AnchorCordUtilsServices } from './utils/cord.utils.service'; import { PrismaClient } from '@prisma/client'; import { generateCredentialRequestPayload, @@ -56,6 +57,7 @@ describe('CredentialsService', () => { RenderingUtilsService, SchemaUtilsSerivce, IdentityUtilsService, + AnchorCordUtilsServices, ], }).compile(); From aa5b102d2a28b75c9a01eeb621c2a57d98c841bd Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:55:00 +0530 Subject: [PATCH 14/20] Identity-service:Interface implemented --- services/identity-service/src/app.module.ts | 6 +- .../src/did/did.controller.spec.ts | 4 +- .../identity-service/src/did/did.module.ts | 7 ++- .../src/did/did.service.spec.ts | 5 +- .../identity-service/src/did/did.service.ts | 61 +++++++++++++++---- .../factories/blockchain-anchor.factory.ts | 38 ++++++++++++ .../implementations/anchor-cord.service.ts | 26 ++++++++ .../interfaces/blockchain_anchor.interface.ts | 9 +++ .../src/utils/cord.service.ts | 40 ------------ services/identity-service/src/vc/vc.module.ts | 7 ++- .../src/vc/vc.service.spec.ts | 5 +- 11 files changed, 141 insertions(+), 67 deletions(-) create mode 100644 services/identity-service/src/did/factories/blockchain-anchor.factory.ts create mode 100644 services/identity-service/src/did/implementations/anchor-cord.service.ts create mode 100644 services/identity-service/src/did/interfaces/blockchain_anchor.interface.ts delete mode 100644 services/identity-service/src/utils/cord.service.ts diff --git a/services/identity-service/src/app.module.ts b/services/identity-service/src/app.module.ts index 313f9ebba..9b7ba38e8 100644 --- a/services/identity-service/src/app.module.ts +++ b/services/identity-service/src/app.module.ts @@ -13,8 +13,8 @@ import { AuthGuard } from './auth/auth.guard'; import { TerminusModule } from '@nestjs/terminus'; import { PrismaHealthIndicator } from './utils/prisma.health'; import { VaultHealthIndicator } from './utils/vault.health'; -import { AnchorCordService } from './utils/cord.service'; - +import { AnchorCordService } from './did/implementations/anchor-cord.service'; +import { BlockchainAnchorFactory } from './did/factories/blockchain-anchor.factory'; @Module({ imports: [ DidModule, @@ -27,7 +27,7 @@ import { AnchorCordService } from './utils/cord.service'; ], controllers: [AppController, DidController], providers: [ - PrismaService, DidService, VaultService,AnchorCordService, + PrismaService, DidService, VaultService,BlockchainAnchorFactory,AnchorCordService, { provide: APP_GUARD, useClass: AuthGuard, diff --git a/services/identity-service/src/did/did.controller.spec.ts b/services/identity-service/src/did/did.controller.spec.ts index a80bd5fd3..02fc32f60 100644 --- a/services/identity-service/src/did/did.controller.spec.ts +++ b/services/identity-service/src/did/did.controller.spec.ts @@ -4,7 +4,6 @@ import { InternalServerErrorException } from '@nestjs/common'; import { DidService } from './did.service'; import { PrismaService } from '../utils/prisma.service'; import { VaultService } from '../utils/vault.service'; -import { AnchorCordService } from 'src/utils/cord.service'; import { ConfigService } from '@nestjs/config'; describe('DidController', () => { @@ -39,7 +38,8 @@ describe('DidController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [DidController], - providers: [DidService, PrismaService, ConfigService, VaultService,AnchorCordService] + // providers: [DidService, PrismaService, ConfigService, VaultService,AnchorCordService] + providers: [DidService, PrismaService, ConfigService, VaultService] }).compile(); controller = module.get(DidController); diff --git a/services/identity-service/src/did/did.module.ts b/services/identity-service/src/did/did.module.ts index 8c8fd9d09..d0d705233 100644 --- a/services/identity-service/src/did/did.module.ts +++ b/services/identity-service/src/did/did.module.ts @@ -5,8 +5,8 @@ import { PrismaService } from 'src/utils/prisma.service'; import { DidController } from './did.controller'; import { DidService } from './did.service'; import { VaultService } from '../utils/vault.service'; -import { AnchorCordService } from 'src/utils/cord.service'; - +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; +import { AnchorCordService } from './implementations/anchor-cord.service'; @Module({ imports: [HttpModule], controllers: [DidController], @@ -14,7 +14,8 @@ import { AnchorCordService } from 'src/utils/cord.service'; DidService, PrismaService, VaultService, - AnchorCordService + AnchorCordService, + BlockchainAnchorFactory ], }) export class DidModule {} diff --git a/services/identity-service/src/did/did.service.spec.ts b/services/identity-service/src/did/did.service.spec.ts index 06dd00f43..db8ef10c9 100644 --- a/services/identity-service/src/did/did.service.spec.ts +++ b/services/identity-service/src/did/did.service.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DidService } from './did.service'; import { PrismaService } from '../utils/prisma.service'; import { VaultService } from '../utils/vault.service'; -import { AnchorCordService } from 'src/utils/cord.service'; +// import { AnchorCordService } from 'src/utils/cord.service'; import { GenerateDidDTO, VerificationKeyType } from './dtos/GenerateDidRequest.dto'; import { ConfigService } from '@nestjs/config'; @@ -33,7 +33,8 @@ describe('DidService', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [DidService, PrismaService, VaultService, ConfigService,AnchorCordService], + // providers: [DidService, PrismaService, VaultService, ConfigService,AnchorCordService], + providers: [DidService, PrismaService, VaultService, ConfigService], }).compile(); service = module.get(DidService); diff --git a/services/identity-service/src/did/did.service.ts b/services/identity-service/src/did/did.service.ts index 89233884f..a7f31d32d 100644 --- a/services/identity-service/src/did/did.service.ts +++ b/services/identity-service/src/did/did.service.ts @@ -5,7 +5,8 @@ import { VaultService } from '../utils/vault.service'; import { Identity } from '@prisma/client'; import { RSAKeyPair } from 'crypto-ld'; import { GenerateDidDTO } from './dtos/GenerateDidRequest.dto'; -import { AnchorCordService } from 'src/utils/cord.service'; +// import { AnchorCordService } from 'src/utils/cord.service'; +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; const { DIDDocument } = require('did-resolver'); type DIDDocument = typeof DIDDocument; @@ -22,7 +23,7 @@ export class DidService { webDidPrefix: string; signingAlgorithm: string; didResolver: any; - constructor(private prisma: PrismaService, private vault: VaultService , private anchorcord:AnchorCordService) { + constructor(private prisma: PrismaService, private vault: VaultService , private blockchainFactory:BlockchainAnchorFactory) { let baseUrl: string = process.env.WEB_DID_BASE_URL; this.webDidPrefix = this.getDidPrefixForBaseUrl(baseUrl); this.signingAlgorithm = process.env.SIGNING_ALGORITHM; @@ -103,18 +104,22 @@ export class DidService { let document: DIDDocument; let privateKeys: object; let blockchainStatus: boolean = false; - - if (this.shouldAnchorToCord()) { + + // Check if anchoring to blockchain is enabled and get the method + const method = this.shouldAnchorToBlockchain(); + + if (method) { try { - if (doc.method !== 'cord') { - throw new BadRequestException('Invalid method: only "cord" is allowed for anchoring to Cord.'); - } - const response = await this.anchorcord.anchorDid(doc); - didUri = response.document.uri; - document = response.document; - - // store mnemonic and delegate keys in to vault - privateKeys = { + // Get the appropriate service from the factory + const anchorService = this.blockchainFactory.getAnchorService(method); + // Use the service to anchor the DID + const response = await anchorService.anchorDid(doc); + + didUri = response.document.uri; + document = response.document; + + // store mnemonic and delegate keys in to vault + privateKeys = { "mnemonic":response.mnemonic, "delegateKeys":response.delegateKeys }; @@ -250,4 +255,34 @@ export class DidService { process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' ); } + +/** + * Determines if anchoring to a blockchain is enabled based on environment variables. + * Checks for specific blockchain configurations and returns the appropriate method. + * @returns The blockchain method (e.g., 'cord', 'solana') if anchoring is enabled; otherwise, null. + */ +private shouldAnchorToBlockchain(): string | null { + // Check if the environment variable ANCHOR_TO_CORD is set to 'true' for the CORD blockchain + if ( + process.env.ANCHOR_TO_CORD && + process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' + ) { + return 'cord'; // Return 'cord' as the service method if CORD anchoring is enabled + } + + // Add additional checks here for other blockchains, e.g.,Solana, Ethereum, Polkadot + /* + if ( + process.env.ANCHOR_TO_SOLANA && + process.env.ANCHOR_TO_SOLANA.toLowerCase().trim() === 'true' + ) { + return 'solana'; // Return 'solana' if solana anchoring is enabled + } + */ + + return null; // Return null if no blockchain anchoring is required } + + +} + diff --git a/services/identity-service/src/did/factories/blockchain-anchor.factory.ts b/services/identity-service/src/did/factories/blockchain-anchor.factory.ts new file mode 100644 index 000000000..67d48cbff --- /dev/null +++ b/services/identity-service/src/did/factories/blockchain-anchor.factory.ts @@ -0,0 +1,38 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { AnchorCordService } from '../implementations/anchor-cord.service'; +import { BlockchainAnchor } from '../interfaces/blockchain_anchor.interface'; + +/** + * Factory class to dynamically resolve the appropriate BlockchainAnchor service. + * It uses the specified method to determine which implementation to return. + */ +@Injectable() +export class BlockchainAnchorFactory { + /** + * Constructor for the BlockchainAnchorFactory. + * @param cordService - An instance of AnchorCordService, which handles CORD-specific anchoring logic. + */ + constructor(private readonly cordService: AnchorCordService) {} + + /** + * Resolves the appropriate BlockchainAnchor service based on the provided method. + * @param method - The blockchain method (e.g., 'cord'). + * @returns The service instance corresponding to the specified method or null if no method is provided. + * @throws + */ + getAnchorService(method?: string): BlockchainAnchor | null { + // If no method is specified, return null to indicate no anchoring is required + if (!method) { + return null; + } + + // Determine the appropriate service implementation based on the method + switch (method) { + case 'cord': + // Return the CORD-specific implementation + return this.cordService; + default: + throw new BadRequestException(`Unsupported blockchain method: ${method}`); + } + } +} diff --git a/services/identity-service/src/did/implementations/anchor-cord.service.ts b/services/identity-service/src/did/implementations/anchor-cord.service.ts new file mode 100644 index 000000000..e6c4d221e --- /dev/null +++ b/services/identity-service/src/did/implementations/anchor-cord.service.ts @@ -0,0 +1,26 @@ +import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { BlockchainAnchor } from '../interfaces/blockchain_anchor.interface'; +import {BadRequestException} from '@nestjs/common'; +@Injectable() +export class AnchorCordService implements BlockchainAnchor { + private readonly logger = new Logger(AnchorCordService.name); + + constructor(private readonly httpService: HttpService) {} + + async anchorDid(body: any): Promise<{ document: any; mnemonic: string; delegateKeys: object }> { + try { + if (body.method !== 'cord') { + throw new BadRequestException('Invalid method: only "cord" is allowed for anchoring to Cord.'); + } + const response = await this.httpService.axiosRef.post( + `${process.env.ISSUER_AGENT_BASE_URL}/did/create/`, + body, + ); + return response.data.result; + } catch (err) { + this.logger.error('Error anchoring DID to CORD blockchain', err); + throw new InternalServerErrorException('Failed to anchor DID to CORD blockchain'); + } + } +} diff --git a/services/identity-service/src/did/interfaces/blockchain_anchor.interface.ts b/services/identity-service/src/did/interfaces/blockchain_anchor.interface.ts new file mode 100644 index 000000000..6e4d3998b --- /dev/null +++ b/services/identity-service/src/did/interfaces/blockchain_anchor.interface.ts @@ -0,0 +1,9 @@ +export interface BlockchainAnchor { + /** + * Anchors a DID document to the blockchain. + * @param body The request payload for anchoring. + * @returns The anchored DID document or related data. + */ + anchorDid(body: any): Promise; + } + \ No newline at end of file diff --git a/services/identity-service/src/utils/cord.service.ts b/services/identity-service/src/utils/cord.service.ts deleted file mode 100644 index ea20f244e..000000000 --- a/services/identity-service/src/utils/cord.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { HttpService } from '@nestjs/axios'; -import { - HttpException, - Injectable, - InternalServerErrorException, - Logger, -} from '@nestjs/common'; - -@Injectable() -export class AnchorCordService { - constructor(private readonly httpService: HttpService) {} - private logger = new Logger(AnchorCordService.name); - - async anchorDid(body: any): Promise { - try { - const response = await this.httpService.axiosRef.post( - `${process.env.ISSUER_AGENT_BASE_URL}/did/create/`, - body, - ); - return response.data; - } catch (err) { - const errorDetails = { - message: err.message, - status: err.response?.status, - statusText: err.response?.statusText, - data: err.response?.data, - headers: err.response?.headers, - request: err.config, - }; - - this.logger.error( - 'Error anchoring did to Cord blockchain', - errorDetails, - ); - throw new InternalServerErrorException( - 'Failed to anchor did to Cord blockchain', - ); - } - } -} diff --git a/services/identity-service/src/vc/vc.module.ts b/services/identity-service/src/vc/vc.module.ts index 42583550a..9e71d5686 100644 --- a/services/identity-service/src/vc/vc.module.ts +++ b/services/identity-service/src/vc/vc.module.ts @@ -5,11 +5,14 @@ import { VaultService } from 'src/utils/vault.service'; import { PrismaService } from 'src/utils/prisma.service'; import { VcController } from './vc.controller'; import VcService from './vc.service'; -import { AnchorCordService } from 'src/utils/cord.service'; +import { BlockchainAnchorFactory } from 'src/did/factories/blockchain-anchor.factory'; +import { AnchorCordService } from 'src/did/implementations/anchor-cord.service'; +// import { AnchorCordService } from 'src/utils/cord.service'; @Module({ imports: [HttpModule], controllers: [VcController], - providers: [VcService, PrismaService, DidService, VaultService,AnchorCordService], + providers: [VcService, PrismaService, DidService, VaultService,BlockchainAnchorFactory,AnchorCordService], + // providers: [VcService, PrismaService, DidService, VaultService,AnchorCordService], }) export class VcModule {} diff --git a/services/identity-service/src/vc/vc.service.spec.ts b/services/identity-service/src/vc/vc.service.spec.ts index 2b3c71b10..22e10f545 100644 --- a/services/identity-service/src/vc/vc.service.spec.ts +++ b/services/identity-service/src/vc/vc.service.spec.ts @@ -3,7 +3,7 @@ import VcService from './vc.service'; import { PrismaService } from '../utils/prisma.service'; import { DidService } from '../did/did.service'; import { VaultService } from '../utils/vault.service'; -import { AnchorCordService } from 'src/utils/cord.service'; +// import { AnchorCordService } from 'src/utils/cord.service'; describe('DidService', () => { let service: VcService; let didService: DidService; @@ -17,7 +17,8 @@ describe('DidService', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [VcService, PrismaService, DidService, VaultService,AnchorCordService], + providers: [VcService, PrismaService, DidService, VaultService], + // providers: [VcService, PrismaService, DidService, VaultService,AnchorCordService], }).compile(); service = module.get(VcService); From 391e05e1f4c4cd90e2b0621fddbca880fd357640 Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:09:32 +0530 Subject: [PATCH 15/20] Schema-service:Interface Implemented --- .env | 2 +- services/credential-schema/src/app.module.ts | 4 + .../rendering-templates.module.ts | 4 + .../factories/blockchain-anchor.factory.ts | 38 +++++++ .../implementations/anchor-cord.service.ts | 37 +++++++ .../interfaces/blockchain_anchor.interface.ts | 9 ++ .../src/schema/schema.module.ts | 4 +- .../src/schema/schema.service.ts | 104 ++++++++++-------- .../src/utils/utils.service.ts | 26 ----- .../identity-service/src/did/did.service.ts | 6 - 10 files changed, 152 insertions(+), 82 deletions(-) create mode 100644 services/credential-schema/src/schema/factories/blockchain-anchor.factory.ts create mode 100644 services/credential-schema/src/schema/implementations/anchor-cord.service.ts create mode 100644 services/credential-schema/src/schema/interfaces/blockchain_anchor.interface.ts diff --git a/.env b/.env index eaad13cbe..45396758c 100644 --- a/.env +++ b/.env @@ -33,7 +33,7 @@ ENABLE_AUTH=false # Anchor to cord block chain # Flag to enable/disable anchoring to Cord blockchain -ANCHOR_TO_CORD=false +ANCHOR_TO_CORD=true # Base URL for Issuer Agent # This is the service responsible for issuing credentials diff --git a/services/credential-schema/src/app.module.ts b/services/credential-schema/src/app.module.ts index 1b496a625..f49ebb1fd 100644 --- a/services/credential-schema/src/app.module.ts +++ b/services/credential-schema/src/app.module.ts @@ -11,6 +11,8 @@ import { PrismaHealthIndicator } from './utils/prisma.health'; import { PrismaClient } from '@prisma/client'; import { APP_GUARD } from '@nestjs/core'; import { AuthGuard } from './auth/auth.guard'; +import { AnchorCordService } from './schema/implementations/anchor-cord.service'; +import { BlockchainAnchorFactory } from './schema/factories/blockchain-anchor.factory'; @Module({ imports: [ @@ -32,6 +34,8 @@ import { AuthGuard } from './auth/auth.guard'; UtilsService, PrismaHealthIndicator, PrismaClient, + AnchorCordService, + BlockchainAnchorFactory, { provide: APP_GUARD, useClass: AuthGuard, diff --git a/services/credential-schema/src/rendering-templates/rendering-templates.module.ts b/services/credential-schema/src/rendering-templates/rendering-templates.module.ts index ecc150b2d..aa4241e9c 100644 --- a/services/credential-schema/src/rendering-templates/rendering-templates.module.ts +++ b/services/credential-schema/src/rendering-templates/rendering-templates.module.ts @@ -6,6 +6,8 @@ import { SchemaService } from '../schema/schema.service'; import { HttpModule } from '@nestjs/axios'; import { UtilsService } from '../utils/utils.service'; import { PrismaClient } from '@prisma/client'; +import { BlockchainAnchorFactory } from 'src/schema/factories/blockchain-anchor.factory'; +import { AnchorCordService } from 'src/schema/implementations/anchor-cord.service'; @Module({ imports: [HttpModule], @@ -15,6 +17,8 @@ import { PrismaClient } from '@prisma/client'; ValidateTemplateService, SchemaService, UtilsService, + BlockchainAnchorFactory, + AnchorCordService, ], controllers: [RenderingTemplatesController], }) diff --git a/services/credential-schema/src/schema/factories/blockchain-anchor.factory.ts b/services/credential-schema/src/schema/factories/blockchain-anchor.factory.ts new file mode 100644 index 000000000..67d48cbff --- /dev/null +++ b/services/credential-schema/src/schema/factories/blockchain-anchor.factory.ts @@ -0,0 +1,38 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { AnchorCordService } from '../implementations/anchor-cord.service'; +import { BlockchainAnchor } from '../interfaces/blockchain_anchor.interface'; + +/** + * Factory class to dynamically resolve the appropriate BlockchainAnchor service. + * It uses the specified method to determine which implementation to return. + */ +@Injectable() +export class BlockchainAnchorFactory { + /** + * Constructor for the BlockchainAnchorFactory. + * @param cordService - An instance of AnchorCordService, which handles CORD-specific anchoring logic. + */ + constructor(private readonly cordService: AnchorCordService) {} + + /** + * Resolves the appropriate BlockchainAnchor service based on the provided method. + * @param method - The blockchain method (e.g., 'cord'). + * @returns The service instance corresponding to the specified method or null if no method is provided. + * @throws + */ + getAnchorService(method?: string): BlockchainAnchor | null { + // If no method is specified, return null to indicate no anchoring is required + if (!method) { + return null; + } + + // Determine the appropriate service implementation based on the method + switch (method) { + case 'cord': + // Return the CORD-specific implementation + return this.cordService; + default: + throw new BadRequestException(`Unsupported blockchain method: ${method}`); + } + } +} diff --git a/services/credential-schema/src/schema/implementations/anchor-cord.service.ts b/services/credential-schema/src/schema/implementations/anchor-cord.service.ts new file mode 100644 index 000000000..5d39b6846 --- /dev/null +++ b/services/credential-schema/src/schema/implementations/anchor-cord.service.ts @@ -0,0 +1,37 @@ +import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { BlockchainAnchor } from '../interfaces/blockchain_anchor.interface'; +import {BadRequestException} from '@nestjs/common'; +@Injectable() +export class AnchorCordService implements BlockchainAnchor { + private readonly logger = new Logger(AnchorCordService.name); + + constructor(private readonly httpService: HttpService) {} + + async anchorSchema(body: any): Promise { + try { + const response = await this.httpService.axiosRef.post( + `${process.env.ISSUER_AGENT_BASE_URL}/schema`, + body, + ); + return response.data.result; + } catch (err) { + const errorDetails = { + message: err.message, + status: err.response?.status, + statusText: err.response?.statusText, + data: err.response?.data, + headers: err.response?.headers, + request: err.config, + }; + + this.logger.error( + 'Error anchoring schema to Cord blockchain', + errorDetails, + ); + throw new InternalServerErrorException( + 'Failed to anchor schema to Cord blockchain', + ); + } + } +} diff --git a/services/credential-schema/src/schema/interfaces/blockchain_anchor.interface.ts b/services/credential-schema/src/schema/interfaces/blockchain_anchor.interface.ts new file mode 100644 index 000000000..f99d35a6a --- /dev/null +++ b/services/credential-schema/src/schema/interfaces/blockchain_anchor.interface.ts @@ -0,0 +1,9 @@ +export interface BlockchainAnchor { + /** + * Anchors a Scheam to the blockchain. + * @param body The request payload for anchoring. + * @returns The anchored Schema or related data. + */ + anchorSchema(body: any): Promise; + } + \ No newline at end of file diff --git a/services/credential-schema/src/schema/schema.module.ts b/services/credential-schema/src/schema/schema.module.ts index b041d26fb..a9cd34b8a 100644 --- a/services/credential-schema/src/schema/schema.module.ts +++ b/services/credential-schema/src/schema/schema.module.ts @@ -4,10 +4,12 @@ import { SchemaService } from './schema.service'; import { HttpModule } from '@nestjs/axios'; import { UtilsService } from '../utils/utils.service'; import { PrismaClient } from '@prisma/client'; +import { AnchorCordService } from './implementations/anchor-cord.service'; +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; @Module({ imports: [HttpModule], controllers: [SchemaController], - providers: [SchemaService, PrismaClient, UtilsService], + providers: [SchemaService, PrismaClient, UtilsService,AnchorCordService,BlockchainAnchorFactory], }) export class SchemaModule {} diff --git a/services/credential-schema/src/schema/schema.service.ts b/services/credential-schema/src/schema/schema.service.ts index cc4764cb1..da0b81ddb 100644 --- a/services/credential-schema/src/schema/schema.service.ts +++ b/services/credential-schema/src/schema/schema.service.ts @@ -16,13 +16,15 @@ import { DefinedError } from 'ajv'; import { CreateCredentialDTO } from './dto/create-credentials.dto'; import { UtilsService } from '../utils/utils.service'; import { GetCredentialSchemaDTO } from './dto/getCredentialSchema.dto'; +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; @Injectable() export class SchemaService { constructor( private readonly prisma: PrismaClient, private readonly utilService: UtilsService, - ) {} + private readonly blockchainFactory: BlockchainAnchorFactory + ) { } private logger = new Logger(SchemaService.name); private semanticVersionRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; @@ -123,31 +125,33 @@ export class SchemaService { }; }); } - private shouldAnchorToCord(): boolean { - return ( + + + /** + * Determines if anchoring to a blockchain is enabled based on environment variables. + * Checks for specific blockchain configurations and returns the appropriate method. + * @returns The blockchain method (e.g., 'cord', 'solana') if anchoring is enabled; otherwise, null. + */ + private shouldAnchorToBlockchain(): string | null { + // Check if the environment variable ANCHOR_TO_CORD is set to 'true' for the CORD blockchain + if ( process.env.ANCHOR_TO_CORD && process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' - ); - } + ) { + return 'cord'; // Return 'cord' as the service method if CORD anchoring is enabled + } - private async anchorSchemaToCord(schemaData: any): Promise { - try { - const anchorResponse = await this.utilService.anchorSchema({ - schema: schemaData, - }); - - this.logger.debug( - 'Schema successfully anchored to Cord blockchain', - anchorResponse, - ); - - return anchorResponse.schemaId; - } catch (err) { - this.logger.error('Failed to anchor schema to Cord blockchain', err); - throw new InternalServerErrorException( - 'Failed to anchor schema to Cord blockchain', - ); + // Add additional checks here for other blockchains, e.g.,Solana, Ethereum, Polkadot + /* + if ( + process.env.ANCHOR_TO_SOLANA && + process.env.ANCHOR_TO_SOLANA.toLowerCase().trim() === 'true' + ) { + return 'solana'; // Return 'solana' if solana anchoring is enabled } + */ + + return null; // Return null if no blockchain anchoring is required } async createCredentialSchema( @@ -157,9 +161,9 @@ export class SchemaService { ) { const data = createCredentialDto.schema; const tags = createCredentialDto.tags; - let cordSchemaId: string | null = null; + let blockchainSchemaId: string | null = null; let did: string | null = null; - + // Validate the credential schema if (!validate(data)) { this.logger.log('Schema validation failed', validate.errors.join('\n')); @@ -170,12 +174,16 @@ export class SchemaService { `Schema validation failed with the following errors: ${validate.errors.join('\n')}`, ); } - - // Check if ANCHOR_TO_CORD is enabled, and anchor the schema to Cord if it is - if (this.shouldAnchorToCord()) { - // Anchor the schema to Cord blockchain and retrieve the cordSchemaId (which acts as the DID) - cordSchemaId = await this.anchorSchemaToCord(data); - did = cordSchemaId; + // Check if anchoring to blockchain is enabled and get the method + const method = this.shouldAnchorToBlockchain(); + + if (method) { + // Get the appropriate service from the factory + const anchorService = this.blockchainFactory.getAnchorService(method); + const response = await anchorService.anchorSchema(data); + + blockchainSchemaId = response.schemaId; + did = blockchainSchemaId; } else if (generateDID) { // Generate a DID if anchoring is not enabled and generateDID is true const didBody = { @@ -192,19 +200,19 @@ export class SchemaService { }, ], }; - + // Generate DID const generatedDidResponse = await this.utilService.generateDID(didBody); this.logger.debug('DID received from identity service', generatedDidResponse); - did = generatedDidResponse?.id || null; - + did = generatedDidResponse?.id || null; + } - + const credSchema = { schema: { type: data.type, - id: did ? did : data.id, + id: did ? did : data.id, version: data.version ? data.version : '0.0.0', name: data.name, author: data.author, @@ -215,16 +223,16 @@ export class SchemaService { tags: tags, status: createCredentialDto.status, deprecatedId: createCredentialDto.deprecatedId, - blockchainStatus: cordSchemaId ? 'ANCHORED' : 'PENDING', + blockchainStatus: blockchainSchemaId ? 'ANCHORED' : 'PENDING', }; - // // sign the credential schema (only the schema part of the credSchema object above since it is the actual schema) - // // const proof = await this.utilService.sign( - // // credSchema.schema.author, - // // credSchema.schema, - // // ); - // // credSchema.schema.proof = proof; - + // // sign the credential schema (only the schema part of the credSchema object above since it is the actual schema) + // // const proof = await this.utilService.sign( + // // credSchema.schema.author, + // // credSchema.schema, + // // ); + // // credSchema.schema.proof = proof; + // Save the credential schema to the database try { const resp = await this.prisma.verifiableCredentialSchema.create({ @@ -240,24 +248,24 @@ export class SchemaService { proof: credSchema.schema.proof as Prisma.JsonValue || undefined, tags: credSchema.tags as string[], deprecatedId: deprecatedId, - blockchainStatus: cordSchemaId ? 'ANCHORED' : 'PENDING', + blockchainStatus: blockchainSchemaId ? 'ANCHORED' : 'PENDING', }, }); - - + + credSchema['createdAt'] = resp.createdAt; credSchema['updatedAt'] = resp.updatedAt; credSchema['deletedAt'] = resp.deletedAt; credSchema['createdBy'] = resp.createdBy; credSchema['updatedBy'] = resp.updatedBy; - + return credSchema; } catch (err) { this.logger.error('Error saving schema to db', err); throw new InternalServerErrorException('Error saving schema to db'); } } - + private formatResponse(schema: VerifiableCredentialSchema) { return JSON.parse( diff --git a/services/credential-schema/src/utils/utils.service.ts b/services/credential-schema/src/utils/utils.service.ts index 373ccd5fa..4ee065735 100644 --- a/services/credential-schema/src/utils/utils.service.ts +++ b/services/credential-schema/src/utils/utils.service.ts @@ -39,30 +39,4 @@ export class UtilsService { } } - async anchorSchema(body: any): Promise { - try { - const response = await this.httpService.axiosRef.post( - `${process.env.ISSUER_AGENT_BASE_URL}/schema`, - body, - ); - return response.data; - } catch (err) { - const errorDetails = { - message: err.message, - status: err.response?.status, - statusText: err.response?.statusText, - data: err.response?.data, - headers: err.response?.headers, - request: err.config, - }; - - this.logger.error( - 'Error anchoring schema to Cord blockchain', - errorDetails, - ); - throw new InternalServerErrorException( - 'Failed to anchor schema to Cord blockchain', - ); - } - } } diff --git a/services/identity-service/src/did/did.service.ts b/services/identity-service/src/did/did.service.ts index a7f31d32d..3e55d6e5d 100644 --- a/services/identity-service/src/did/did.service.ts +++ b/services/identity-service/src/did/did.service.ts @@ -249,12 +249,6 @@ export class DidService { return this.resolveDID(webDidId); } - private shouldAnchorToCord(): boolean { - return ( - process.env.ANCHOR_TO_CORD && - process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' - ); - } /** * Determines if anchoring to a blockchain is enabled based on environment variables. From a66c279775d4c957b25a36b46029d6b48194c135 Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:10:21 +0530 Subject: [PATCH 16/20] Credential-service:Interface Implemented --- .../src/credentials/credentials.module.ts | 7 +- .../src/credentials/credentials.service.ts | 97 ++++++++----------- .../factories/blockchain-anchor.factory.ts | 38 ++++++++ .../implementations/anchor-cord.service.ts | 97 +++++++++++++++++++ .../interfaces/blockchain_anchor.interface.ts | 9 ++ 5 files changed, 190 insertions(+), 58 deletions(-) create mode 100644 services/credentials-service/src/credentials/factories/blockchain-anchor.factory.ts create mode 100644 services/credentials-service/src/credentials/implementations/anchor-cord.service.ts create mode 100644 services/credentials-service/src/credentials/interfaces/blockchain_anchor.interface.ts diff --git a/services/credentials-service/src/credentials/credentials.module.ts b/services/credentials-service/src/credentials/credentials.module.ts index bd62d4435..c83417126 100644 --- a/services/credentials-service/src/credentials/credentials.module.ts +++ b/services/credentials-service/src/credentials/credentials.module.ts @@ -5,13 +5,14 @@ import { HttpModule, HttpService } from '@nestjs/axios'; import { IdentityUtilsService } from './utils/identity.utils.service'; import { RenderingUtilsService } from './utils/rendering.utils.service'; import { SchemaUtilsSerivce } from './utils/schema.utils.service'; -import {AnchorCordUtilsServices} from './utils/cord.utils.service' +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; +import { AnchorCordService } from './implementations/anchor-cord.service'; import { PrismaClient } from '@prisma/client'; @Module({ imports: [HttpModule], - providers: [ CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce,AnchorCordUtilsServices], + providers: [CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce, BlockchainAnchorFactory, AnchorCordService], controllers: [CredentialsController], exports: [IdentityUtilsService] }) -export class CredentialsModule {} +export class CredentialsModule { } diff --git a/services/credentials-service/src/credentials/credentials.service.ts b/services/credentials-service/src/credentials/credentials.service.ts index d84498402..257323023 100644 --- a/services/credentials-service/src/credentials/credentials.service.ts +++ b/services/credentials-service/src/credentials/credentials.service.ts @@ -13,7 +13,8 @@ import { JwtCredentialSubject } from 'src/app.interface'; import { SchemaUtilsSerivce } from './utils/schema.utils.service'; import { IdentityUtilsService } from './utils/identity.utils.service'; import { RenderingUtilsService } from './utils/rendering.utils.service'; -import { AnchorCordUtilsServices } from './utils/cord.utils.service'; +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; + import * as jsigs from 'jsonld-signatures'; import * as jsonld from 'jsonld'; import { DOCUMENTS } from './documents'; @@ -43,7 +44,7 @@ export class CredentialsService { private readonly identityUtilsService: IdentityUtilsService, private readonly renderingUtilsService: RenderingUtilsService, private readonly schemaUtilsService: SchemaUtilsSerivce, - private readonly anchorCordUtilsServices: AnchorCordUtilsServices + private readonly blockchainFactory: BlockchainAnchorFactory ) { this.init(); } @@ -150,9 +151,14 @@ export class CredentialsService { async verifyCredential(credToVerify: Verifiable, status?: VCStatus) { try { - // If ANCHOR_TO_CORD is true, delegate verification to Cord Verification MiddleWare service - if (this.shouldAnchorToCord()) { - return await this.anchorCordUtilsServices.verifyCredentialOnCord(credToVerify); + // Check if anchoring to blockchain is enabled and get the method + const method = this.shouldAnchorToBlockchain(); + + if (method) { + // Get the appropriate service from the factory + const anchorService = this.blockchainFactory.getAnchorService(method); + // delegate verification to appropriate service + return await anchorService.verifyCredential(credToVerify); } // calling identity service to verify the issuer DID const issuerId = (credToVerify.issuer?.id || credToVerify.issuer) as string; @@ -278,9 +284,15 @@ export class CredentialsService { let response: any = null; + // Check if anchoring to blockchain is enabled and get the method + const method = this.shouldAnchorToBlockchain(); + // Check if ANCHOR_TO_CORD is true - if (this.shouldAnchorToCord()) { - response = await this.anchorCredentialToCord(credInReq, issueRequest); + if (method) { + // Get the appropriate service from the factory + const anchorService = this.blockchainFactory.getAnchorService(method); + const anchoredCredentialData = await anchorService.anchorCredential(issueRequest); + response = this.saveCredentialToDatabase(anchoredCredentialData) } else { // Check for issuance date if (!credInReq.issuanceDate) { @@ -322,60 +334,36 @@ export class CredentialsService { return response; } - /** - * Determines if ANCHOR_TO_CORD environment variable is true - */ - private shouldAnchorToCord(): boolean { - return process.env.ANCHOR_TO_CORD && process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true'; - } - /** - * Anchors the credential to Cord blockchain and saves to the DB - */ - private async anchorCredentialToCord(credInReq: any, issueRequest: IssueCredentialDTO) { - if (!issueRequest.credentialSchemaId) { - this.logger.error('Credential SchemaId Schema ID is required for anchoring but is missing'); - throw new BadRequestException('Cord Schema ID is missing'); + /** + * Determines if anchoring to a blockchain is enabled based on environment variables. + * Checks for specific blockchain configurations and returns the appropriate method. + * @returns The blockchain method (e.g., 'cord', 'solana') if anchoring is enabled; otherwise, null. + */ + private shouldAnchorToBlockchain(): string | null { + // Check if the environment variable ANCHOR_TO_CORD is set to 'true' for the CORD blockchain + if ( + process.env.ANCHOR_TO_CORD && + process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' + ) { + return 'cord'; // Return 'cord' as the service method if CORD anchoring is enabled } - try { - this.logger.debug('Anchoring unsigned credential to Cord blockchain with schema ID:', issueRequest.credentialSchemaId); - - const anchorResponse = await this.anchorCordUtilsServices.anchorCredential({ - ...credInReq, - schemaId: issueRequest.credentialSchemaId, - }); - - this.logger.debug('Credential successfully anchored to Cord:', anchorResponse); - - const { - id, issuer, issuanceDate, validUntil: expirationDate, credentialSubject, proof, - } = anchorResponse.vc; - - const anchoredCredentialData = { - id, - type: issueRequest.credential.type, - issuer, - issuanceDate, - expirationDate, - subject: credentialSubject, - subjectId: (credentialSubject as JwtCredentialSubject).id, - proof, - credential_schema: issueRequest.credentialSchemaId, - signed: anchorResponse.vc as object, - tags: issueRequest.tags, - blockchainStatus: "ANCHORED" - - }; - - return this.saveCredentialToDatabase(anchoredCredentialData); - } catch (err) { - this.logger.error('Error anchoring credential to Cord blockchain:', err); - throw new InternalServerErrorException('Error anchoring credential to Cord blockchain'); + // Add additional checks here for other blockchains, e.g.,Solana, Ethereum, Polkadot + /* + if ( + process.env.ANCHOR_TO_SOLANA && + process.env.ANCHOR_TO_SOLANA.toLowerCase().trim() === 'true' + ) { + return 'solana'; // Return 'solana' if solana anchoring is enabled } + */ + + return null; // Return null if no blockchain anchoring is required } + /** * Signs the credential locally and saves it to the database */ @@ -415,7 +403,6 @@ export class CredentialsService { * Saves the credential to the database and returns the response */ private async saveCredentialToDatabase(credentialData: any) { - const newCred = await this.prisma.verifiableCredentials.create({ data: credentialData, }); diff --git a/services/credentials-service/src/credentials/factories/blockchain-anchor.factory.ts b/services/credentials-service/src/credentials/factories/blockchain-anchor.factory.ts new file mode 100644 index 000000000..0a7bfd5c3 --- /dev/null +++ b/services/credentials-service/src/credentials/factories/blockchain-anchor.factory.ts @@ -0,0 +1,38 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { AnchorCordService } from '../implementations/anchor-cord.service'; +import { BlockchainAnchor } from '../interfaces/blockchain_anchor.interface'; + +/** + * Factory class to dynamically resolve the appropriate BlockchainAnchor service. + * It uses the specified method to determine which implementation to return. + */ +@Injectable() +export class BlockchainAnchorFactory { + /** + * Constructor for the BlockchainAnchorFactory. + * @param cordService - An instance of AnchorCordService, which handles CORD-specific anchoring logic. + */ + constructor(private readonly cordService: AnchorCordService) { } + + /** + * Resolves the appropriate BlockchainAnchor service based on the provided method. + * @param method - The blockchain method (e.g., 'cord'). + * @returns The service instance corresponding to the specified method or null if no method is provided. + * @throws + */ + getAnchorService(method?: string): BlockchainAnchor | null { + // If no method is specified, return null to indicate no anchoring is required + if (!method) { + return null; + } + + // Determine the appropriate service implementation based on the method + switch (method) { + case 'cord': + // Return the CORD-specific implementation + return this.cordService; + default: + throw new BadRequestException(`Unsupported blockchain method: ${method}`); + } + } +} diff --git a/services/credentials-service/src/credentials/implementations/anchor-cord.service.ts b/services/credentials-service/src/credentials/implementations/anchor-cord.service.ts new file mode 100644 index 000000000..665174eb8 --- /dev/null +++ b/services/credentials-service/src/credentials/implementations/anchor-cord.service.ts @@ -0,0 +1,97 @@ +import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { BlockchainAnchor } from '../interfaces/blockchain_anchor.interface'; +import { AxiosResponse } from '@nestjs/terminus/dist/health-indicator/http/axios.interfaces'; +import { IssueCredentialDTO } from '../dto/issue-credential.dto'; +import { JwtCredentialSubject } from 'src/app.interface'; +import { W3CCredential, Verifiable } from 'vc.types'; + +@Injectable() +export class AnchorCordService implements BlockchainAnchor { + + private readonly logger = new Logger(AnchorCordService.name); + + + constructor(private readonly httpService: HttpService) { + + } + + async anchorCredential(issueRequest: IssueCredentialDTO): Promise { + try { + const credInReq = issueRequest.credential; + if (!issueRequest.credentialSchemaId) { + + this.logger.error('Credential SchemaId Schema ID is required for anchoring but is missing'); + throw new BadRequestException('Cord Schema ID is missing'); + } + + this.logger.debug('url', process.env.ISSUER_AGENT_BASE_URL); + this.logger.debug('Anchoring unsigned credential to Cord blockchain with schema ID:', issueRequest.credentialSchemaId); + const credentialPayload = { + ...credInReq, + schemaId: issueRequest.credentialSchemaId, + } + let anchorHttpResponse: AxiosResponse = + await this.httpService.axiosRef.post( + `${process.env.ISSUER_AGENT_BASE_URL}/cred`, + { + credential: credentialPayload, + } + ); + + this.logger.debug('Credential successfully anchored'); + let anchoredResult = anchorHttpResponse.data.result; + this.logger.debug('Credential successfully anchored to Cord:', anchoredResult); + const { + id, issuer, issuanceDate, validUntil: expirationDate, credentialSubject, proof, + } = anchoredResult.vc; + + const anchoredCredentialData = { + id, + type: issueRequest.credential.type, + issuer, + issuanceDate, + expirationDate, + subject: credentialSubject, + subjectId: (credentialSubject as JwtCredentialSubject).id, + proof, + credential_schema: issueRequest.credentialSchemaId, + signed: anchoredResult.vc as object, + tags: issueRequest.tags, + blockchainStatus: "ANCHORED", + + }; + return anchoredCredentialData; + } catch (err) { + this.logger.error('Error anchoring credential:', err); + + throw new InternalServerErrorException(`Error anchoring credential : ${err.response.data.details}`); + } + } + + + async verifyCredential( + credToVerify: Verifiable + ): Promise { + try { + this.logger.debug(`${process.env.VERIFICATION_MIDDLEWARE_BASE_URL}/credentials/verify}`) + const response = await this.httpService.axiosRef.post( + `${process.env.VERIFICATION_MIDDLEWARE_BASE_URL}/credentials/verify`, + credToVerify + ); + + if (response.status !== 200) { + this.logger.error('Cord verification failed:', response.data); + throw new InternalServerErrorException('Cord verification failed'); + } + + return response.data; + } catch (err) { + this.logger.error('Error calling Cord verification API:', err); + throw new InternalServerErrorException( + 'Error verifying credential on Cord' + ); + } + } + +} diff --git a/services/credentials-service/src/credentials/interfaces/blockchain_anchor.interface.ts b/services/credentials-service/src/credentials/interfaces/blockchain_anchor.interface.ts new file mode 100644 index 000000000..b70d8f24e --- /dev/null +++ b/services/credentials-service/src/credentials/interfaces/blockchain_anchor.interface.ts @@ -0,0 +1,9 @@ +export interface BlockchainAnchor { + /** + * Anchors a Scheam to the blockchain. + * @param body The request payload for anchoring. + * @returns The anchored Schema or related data. + */ + anchorCredential(body: any): Promise; + verifyCredential(body: any): Promise; +} From 7776e56d0c70b817b8836bdf967e9c30fc25568c Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:53:07 +0530 Subject: [PATCH 17/20] remove cord.utils.service --- .../credentials/utils/cord.utils.service.ts | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 services/credentials-service/src/credentials/utils/cord.utils.service.ts diff --git a/services/credentials-service/src/credentials/utils/cord.utils.service.ts b/services/credentials-service/src/credentials/utils/cord.utils.service.ts deleted file mode 100644 index 742dfed3b..000000000 --- a/services/credentials-service/src/credentials/utils/cord.utils.service.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { HttpService } from '@nestjs/axios'; -import { AxiosResponse } from '@nestjs/terminus/dist/health-indicator/http/axios.interfaces'; -import { - Injectable, - InternalServerErrorException, - Logger, -} from '@nestjs/common'; -import { W3CCredential, Verifiable } from 'vc.types'; -@Injectable() -export class AnchorCordUtilsServices { - private ISSUER_AGENT_BASE_URL: string; - private VERIFICATION_MIDDLEWARE_BASE_URL: string - - constructor(private readonly httpService: HttpService) { - this.ISSUER_AGENT_BASE_URL = process.env.ISSUER_AGENT_BASE_URL; - this.VERIFICATION_MIDDLEWARE_BASE_URL = process.env.VERIFICATION_MIDDLEWARE_BASE_URL; - } - - private logger = new Logger(AnchorCordUtilsServices.name); - - async anchorCredential(credentialPayload: any): Promise { - try { - this.logger.debug('url', this.ISSUER_AGENT_BASE_URL); - const anchorResponse: AxiosResponse = - await this.httpService.axiosRef.post( - `${this.ISSUER_AGENT_BASE_URL}/cred`, - { - credential: credentialPayload, - } - ); - - this.logger.debug('Credential successfully anchored'); - return anchorResponse.data; - } catch (err) { - this.logger.error('Error anchoring credential:', err); - - throw new InternalServerErrorException(`Error anchoring credential : ${err.response.data.details}`); - } - } - - async verifyCredentialOnCord( - credToVerify: Verifiable - ): Promise { - try { - const response = await this.httpService.axiosRef.post( - `${this.VERIFICATION_MIDDLEWARE_BASE_URL}/credentials/verify`, - credToVerify - ); - - if (response.status !== 200) { - this.logger.error('Cord verification failed:', response.data); - throw new InternalServerErrorException('Cord verification failed'); - } - - return response.data; - } catch (err) { - this.logger.error('Error calling Cord verification API:', err); - throw new InternalServerErrorException( - 'Error verifying credential on Cord' - ); - } - } -} From a562302f1efa276e7690a9a93f29d7d4eef8438f Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:56:38 +0530 Subject: [PATCH 18/20] Identiy Service : added test cases --- .../src/did/anchor-cord.service.spec.ts | 170 ++++++++++++++++++ .../src/did/did.service.spec.ts | 6 +- .../src/vc/vc.service.spec.ts | 6 +- 3 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 services/identity-service/src/did/anchor-cord.service.spec.ts diff --git a/services/identity-service/src/did/anchor-cord.service.spec.ts b/services/identity-service/src/did/anchor-cord.service.spec.ts new file mode 100644 index 000000000..817677da4 --- /dev/null +++ b/services/identity-service/src/did/anchor-cord.service.spec.ts @@ -0,0 +1,170 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DidService } from './did.service'; +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; +import { VaultService } from '../utils/vault.service'; +import { PrismaService } from '../utils/prisma.service'; +import { GenerateDidDTO } from './dtos/GenerateDidRequest.dto'; +import { InternalServerErrorException } from '@nestjs/common'; + + +describe('DidService - generateDID', () => { + let service: DidService; + let blockchainFactory: BlockchainAnchorFactory; + let vaultService: VaultService; + let prismaService: PrismaService; + + const mockBlockchainService = { + anchorDid: jest.fn(), + }; + + const mockVaultService = { + writePvtKey: jest.fn(), + }; + + const mockPrismaService = { + identity: { + create: jest.fn(), + }, + }; + + const mockGenerateDidDTO: GenerateDidDTO = { + "services": [ + { + "id": "IdentityHub", + "type": "IdentityHub", + "serviceEndpoint": { + "instance": [ + "https://cord.network.in" + ] + } + } + ] + , "method": "cord" + } + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DidService, + { + provide: BlockchainAnchorFactory, + useValue: { + getAnchorService: jest.fn(() => mockBlockchainService), + }, + }, + { + provide: VaultService, + useValue: mockVaultService, + }, + { + provide: PrismaService, + useValue: mockPrismaService, + }, + ], + }).compile(); + + service = module.get(DidService); + blockchainFactory = module.get(BlockchainAnchorFactory); + vaultService = module.get(VaultService); + prismaService = module.get(PrismaService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await prismaService?.$disconnect?.(); + }); + + it('should verify ANCHOR_TO_CORD is true', () => { + const anchorToCord = process.env.ANCHOR_TO_CORD; + const isTrue = anchorToCord?.toLowerCase().trim() === 'true'; + expect(isTrue).toBe(true); + }); + + + it('should verify the environment variable is a valid URL', () => { + const ISSUER_AGENT_BASE_URL = process.env.ISSUER_AGENT_BASE_URL; + function isValidUrl(value: string): boolean { + try { + new URL(value); + return true; + } catch (err) { + return false; + } + } + expect(isValidUrl(ISSUER_AGENT_BASE_URL)).toBe(true); + }); + + it('should anchor a DID to the blockchain and validate JSON keys', async () => { + const mockResponse = { + document: { + uri: 'did:cord:test123', + authentication: ['did:cord:test123#key-1'], + service: mockGenerateDidDTO.services, + keyAgreement: ['did:cord:test123#key-2'], + capabilityDelegation: ['did:cord:test123#key-3'], + assertionMethod: ['did:cord:test123#key-4'], + }, + mnemonic: 'mock-mnemonic', + delegateKeys: ['key1', 'key2'], + }; + + mockBlockchainService.anchorDid.mockResolvedValueOnce(mockResponse); + + const result = await service.generateDID(mockGenerateDidDTO); + console.log("result", result); + expect(blockchainFactory.getAnchorService).toHaveBeenCalledWith('cord'); + expect(mockBlockchainService.anchorDid).toHaveBeenCalledWith(mockGenerateDidDTO); + + + expect(result).toBeDefined(); + + + const expectedKeys = [ + 'uri', + 'authentication', + 'service', + 'keyAgreement', + 'capabilityDelegation', + 'assertionMethod', + ]; + + expectedKeys.forEach((key) => { + expect(result).toHaveProperty(key); + }); + }); + + + + + it('should throw an error if blockchain anchoring fails', async () => { + mockBlockchainService.anchorDid.mockRejectedValueOnce(new Error('Blockchain Error')); + + await expect(service.generateDID(mockGenerateDidDTO)).rejects.toThrow(InternalServerErrorException); + }); + + it('should throw an error if writing to the vault fails', async () => { + const mockResponse = { + document: { uri: 'did:cord:test123' }, + mnemonic: 'mock-mnemonic', + delegateKeys: ['key1', 'key2'], + }; + mockBlockchainService.anchorDid.mockResolvedValueOnce(mockResponse); + mockVaultService.writePvtKey.mockRejectedValueOnce(new Error('Vault Error')); + + await expect(service.generateDID(mockGenerateDidDTO)).rejects.toThrow(InternalServerErrorException); + }); + + it('should throw an error if writing to the database fails', async () => { + const mockResponse = { + document: { uri: 'did:cord:test123' }, + mnemonic: 'mock-mnemonic', + delegateKeys: ['key1', 'key2'], + }; + mockBlockchainService.anchorDid.mockResolvedValueOnce(mockResponse); + mockPrismaService.identity.create.mockRejectedValueOnce(new Error('Database Error')); + + await expect(service.generateDID(mockGenerateDidDTO)).rejects.toThrow(InternalServerErrorException); + }); +}); diff --git a/services/identity-service/src/did/did.service.spec.ts b/services/identity-service/src/did/did.service.spec.ts index db8ef10c9..ebec83cd2 100644 --- a/services/identity-service/src/did/did.service.spec.ts +++ b/services/identity-service/src/did/did.service.spec.ts @@ -2,7 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DidService } from './did.service'; import { PrismaService } from '../utils/prisma.service'; import { VaultService } from '../utils/vault.service'; -// import { AnchorCordService } from 'src/utils/cord.service'; +import { AnchorCordService } from './implementations/anchor-cord.service'; +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; import { GenerateDidDTO, VerificationKeyType } from './dtos/GenerateDidRequest.dto'; import { ConfigService } from '@nestjs/config'; @@ -33,8 +34,7 @@ describe('DidService', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ - // providers: [DidService, PrismaService, VaultService, ConfigService,AnchorCordService], - providers: [DidService, PrismaService, VaultService, ConfigService], + providers: [DidService, PrismaService, VaultService, ConfigService,BlockchainAnchorFactory,AnchorCordService], }).compile(); service = module.get(DidService); diff --git a/services/identity-service/src/vc/vc.service.spec.ts b/services/identity-service/src/vc/vc.service.spec.ts index 22e10f545..ce243c672 100644 --- a/services/identity-service/src/vc/vc.service.spec.ts +++ b/services/identity-service/src/vc/vc.service.spec.ts @@ -3,7 +3,8 @@ import VcService from './vc.service'; import { PrismaService } from '../utils/prisma.service'; import { DidService } from '../did/did.service'; import { VaultService } from '../utils/vault.service'; -// import { AnchorCordService } from 'src/utils/cord.service'; +import { BlockchainAnchorFactory } from 'src/did/factories/blockchain-anchor.factory'; +import { AnchorCordService } from 'src/did/implementations/anchor-cord.service'; describe('DidService', () => { let service: VcService; let didService: DidService; @@ -17,8 +18,7 @@ describe('DidService', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [VcService, PrismaService, DidService, VaultService], - // providers: [VcService, PrismaService, DidService, VaultService,AnchorCordService], + providers: [VcService, PrismaService, DidService, VaultService,BlockchainAnchorFactory,AnchorCordService], }).compile(); service = module.get(VcService); From e25f62cc73be90ae2a70f79c1f91ffa6428d88ef Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Mon, 16 Dec 2024 21:02:32 +0530 Subject: [PATCH 19/20] credential schema - Test case added --- .../src/schema/anchor-cord-service.spec.ts | 114 ++++++++++++++++++ .../src/schema/schema.service.ts | 2 +- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 services/credential-schema/src/schema/anchor-cord-service.spec.ts diff --git a/services/credential-schema/src/schema/anchor-cord-service.spec.ts b/services/credential-schema/src/schema/anchor-cord-service.spec.ts new file mode 100644 index 000000000..0626c72e5 --- /dev/null +++ b/services/credential-schema/src/schema/anchor-cord-service.spec.ts @@ -0,0 +1,114 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SchemaService } from './schema.service'; +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; +import { PrismaClient } from '@prisma/client'; +import { CreateCredentialDTO } from './dto/create-credentials.dto'; +import { UtilsService } from '../utils/utils.service'; +import { generateCredentialSchemaTestBody } from './schema.fixtures'; +import { BadRequestException, InternalServerErrorException } from '@nestjs/common'; + +describe('SchemaService - createCredentialSchema', () => { + let service: SchemaService; + let blockchainFactory: BlockchainAnchorFactory; + let utilsService: UtilsService; + + const mockBlockchainService = { + anchorSchema: jest.fn(), + }; + + const mockPrismaService = { + verifiableCredentialSchema: { + create: jest.fn(), + findMany: jest.fn(), + }, + }; + + const mockUtilsService = { + generateDID: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SchemaService, + { + provide: BlockchainAnchorFactory, + useValue: { + getAnchorService: jest.fn(() => mockBlockchainService), + }, + }, + { + provide: PrismaClient, + useValue: mockPrismaService, + }, + { + provide: UtilsService, + useValue: mockUtilsService, + }, + ], + }).compile(); + + service = module.get(SchemaService); + blockchainFactory = module.get(BlockchainAnchorFactory); + utilsService = module.get(UtilsService); + }); + + afterEach(() => { + jest.clearAllMocks(); // Clear mocks after each test + }); + + afterAll(async () => { + await mockPrismaService.verifiableCredentialSchema.create.mockClear(); + }); + + it('should verify ANCHOR_TO_CORD is true', () => { + const anchorToCord = process.env.ANCHOR_TO_CORD; + const isTrue = anchorToCord?.toLowerCase().trim() === 'true'; + expect(isTrue).toBe(true); + }); + + it('should verify ISSUER_AGENT_BASE_URL is a valid URL', () => { + const isValidUrl = (url: string) => { + try { + new URL(url); + return true; + } catch { + return false; + } + }; + expect(isValidUrl(process.env.ISSUER_AGENT_BASE_URL)).toBe(true); + }); + + it('should successfully anchor a credential schema to the blockchain', async () => { + const mockRequestBody = generateCredentialSchemaTestBody(); + const mockResponse = { schemaId: 'schema-id-blockchain' }; + + mockBlockchainService.anchorSchema.mockResolvedValueOnce(mockResponse); + mockPrismaService.verifiableCredentialSchema.create.mockResolvedValueOnce({ + ...mockRequestBody.schema, + blockchainStatus: 'ANCHORED', + }); + + const result = await service.createCredentialSchema(mockRequestBody); + + expect(blockchainFactory.getAnchorService).toHaveBeenCalledWith('cord'); + expect(mockBlockchainService.anchorSchema).toHaveBeenCalledWith(mockRequestBody.schema); + expect(result).toBeDefined(); + expect(result.schema.id).toEqual(mockResponse.schemaId); + expect(result.blockchainStatus).toEqual('ANCHORED'); + }); + + it('should throw an error if blockchain anchoring fails', async () => { + const mockRequestBody = generateCredentialSchemaTestBody(); + + mockBlockchainService.anchorSchema.mockRejectedValueOnce( + new InternalServerErrorException('Blockchain anchoring failed') + ); + await expect(service.createCredentialSchema(mockRequestBody)).rejects.toThrow( + InternalServerErrorException + ); + }); + + + +}); diff --git a/services/credential-schema/src/schema/schema.service.ts b/services/credential-schema/src/schema/schema.service.ts index da0b81ddb..51dc9017f 100644 --- a/services/credential-schema/src/schema/schema.service.ts +++ b/services/credential-schema/src/schema/schema.service.ts @@ -138,7 +138,7 @@ export class SchemaService { process.env.ANCHOR_TO_CORD && process.env.ANCHOR_TO_CORD.toLowerCase().trim() === 'true' ) { - return 'cord'; // Return 'cord' as the service method if CORD anchoring is enabled + return 'cord'; } // Add additional checks here for other blockchains, e.g.,Solana, Ethereum, Polkadot From 616658628427a468972cb9a2fadc65bd6afdf2eb Mon Sep 17 00:00:00 2001 From: ashwin275 <110539449+ashwin275@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:51:26 +0530 Subject: [PATCH 20/20] Credential-service:add test case for anchor credential to cord --- .../credentials/anchor-cord-service.spec.ts | 156 ++++++++++++++++++ .../credentials/credentials.service.spec.ts | 4 +- 2 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 services/credentials-service/src/credentials/anchor-cord-service.spec.ts diff --git a/services/credentials-service/src/credentials/anchor-cord-service.spec.ts b/services/credentials-service/src/credentials/anchor-cord-service.spec.ts new file mode 100644 index 000000000..58c9deb42 --- /dev/null +++ b/services/credentials-service/src/credentials/anchor-cord-service.spec.ts @@ -0,0 +1,156 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CredentialsService } from './credentials.service'; +import { SchemaUtilsSerivce } from './utils/schema.utils.service'; +import { IdentityUtilsService } from './utils/identity.utils.service'; +import { RenderingUtilsService } from './utils/rendering.utils.service'; +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; +import { AnchorCordService } from './implementations/anchor-cord.service'; +import { PrismaClient } from '@prisma/client'; +import { Logger } from '@nestjs/common'; +import { HttpModule, HttpService } from '@nestjs/axios'; + +import { + generateCredentialRequestPayload, + generateCredentialSchemaTestBody, +} from './credentials.fixtures'; + +describe('CredentialsService - Integration', () => { + const logger = new Logger('CredentialsServiceTest'); + + let service: CredentialsService; + let httpService: HttpService; + let identityUtilsService: IdentityUtilsService; + let prismaClient: PrismaClient; + + let subjectDID: string; + let credentialSchemaID: string; + let sampleCredReqPayload: any; + + + const isValidUrl = (url: string) => { + try { + new URL(url); + return true; + } catch { + return false; + } + }; + + let module: TestingModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [HttpModule], + providers: [ + CredentialsService, + SchemaUtilsSerivce, + IdentityUtilsService, + RenderingUtilsService, + BlockchainAnchorFactory, + PrismaClient, + AnchorCordService, + ], + }).compile(); + + service = module.get(CredentialsService); + httpService = module.get(HttpService); + identityUtilsService = module.get(IdentityUtilsService); + prismaClient = module.get(PrismaClient); + + }); + + beforeEach(() => { + jest.restoreAllMocks(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + jest.resetModules(); + + if (prismaClient) { + await prismaClient.$disconnect(); + } + + if (module) { + await module.close(); + } + }); + + describe('Environment Variable Checks', () => { + it('should verify ANCHOR_TO_CORD is true', () => { + const anchorToCord = process.env.ANCHOR_TO_CORD; + expect(anchorToCord?.toLowerCase().trim()).toBe('true'); + }); + + it('should verify ISSUER_AGENT_BASE_URL is a valid URL', () => { + expect(isValidUrl(process.env.ISSUER_AGENT_BASE_URL)).toBe(true); + }); + + it('should verify VERIFICATION_MIDDLEWARE_BASE_URL is a valid URL', () => { + expect(isValidUrl(process.env.VERIFICATION_MIDDLEWARE_BASE_URL)).toBe(true); + }); + }); + + describe('Generate DID, Create Schema, and Issue Credential', () => { + it('should generate DIDs for subject', async () => { + try { + const Did = await identityUtilsService.generateDID(['CORD TESTING'], 'cord'); + subjectDID = Did[0].uri; + } catch (error) { + throw error; + } + + }, 10000); + + it('should create a credential schema', async () => { + const schemaPayload = generateCredentialSchemaTestBody(); + + const response = await httpService.axiosRef.post( + `${process.env.SCHEMA_BASE_URL}/credential-schema`, + schemaPayload, + ); + + credentialSchemaID = response.data.schema.id; + expect(credentialSchemaID).toBeDefined(); + }, 10000); + + it('should issue a credential', async () => { + sampleCredReqPayload = generateCredentialRequestPayload( + subjectDID, + subjectDID, + credentialSchemaID, + '1.0.0', + ); + delete sampleCredReqPayload.credential.credentialSubject.type; + + const issuedCredential = await service.issueCredential(sampleCredReqPayload); + + expect(issuedCredential).toBeDefined(); + expect(issuedCredential.credential.id).toBeDefined(); + }, 10000); + + it('should verify the issued credential', async () => { + const issuedCredential = await service.issueCredential(sampleCredReqPayload); + + const verifyRes = await service.verifyCredentialById(issuedCredential.credential.id); + + expect(verifyRes).toBeDefined(); + }, 10000); + }); + + describe('Error Handling', () => { + it('should throw if blockchain anchoring fails', async () => { + jest + .spyOn(service, 'issueCredential') + .mockRejectedValueOnce(new Error('Blockchain anchoring failed')); + + await expect(service.issueCredential(sampleCredReqPayload)).rejects.toThrow( + 'Blockchain anchoring failed', + ); + }); + + it('should throw if credential verification fails', async () => { + await expect(service.verifyCredentialById('invalid-credential')).rejects.toThrow(); + }); + }); +}); diff --git a/services/credentials-service/src/credentials/credentials.service.spec.ts b/services/credentials-service/src/credentials/credentials.service.spec.ts index 50f6d49be..61aeddabd 100644 --- a/services/credentials-service/src/credentials/credentials.service.spec.ts +++ b/services/credentials-service/src/credentials/credentials.service.spec.ts @@ -5,7 +5,7 @@ import { UnsignedVCValidator, VCValidator } from './types/validators'; import { SchemaUtilsSerivce } from './utils/schema.utils.service'; import { IdentityUtilsService } from './utils/identity.utils.service'; import { RenderingUtilsService } from './utils/rendering.utils.service'; -import { AnchorCordUtilsServices } from './utils/cord.utils.service'; +import { BlockchainAnchorFactory } from './factories/blockchain-anchor.factory'; import { PrismaClient } from '@prisma/client'; import { generateCredentialRequestPayload, @@ -57,7 +57,7 @@ describe('CredentialsService', () => { RenderingUtilsService, SchemaUtilsSerivce, IdentityUtilsService, - AnchorCordUtilsServices, + BlockchainAnchorFactory, ], }).compile();