From 547da0a04c28813be1b97ba07055ef583e70ed0d Mon Sep 17 00:00:00 2001
From: Berend Sliedrecht <berend@animo.id>
Date: Mon, 4 Dec 2023 12:58:32 +0100
Subject: [PATCH] fix(ssi): do not register did by default but make it an event

Signed-off-by: Berend Sliedrecht <berend@animo.id>
---
 .../src/events/credentialDefinitionEvents.ts  |  74 ++++++++++
 apps/shared/src/index.ts                      |   1 +
 .../credentialDefinitions.controller.spec.ts  |  84 +++++++++++
 .../credentialDefinitions.controller.ts       |  49 +++++++
 .../credentialDefinitions.module.ts           |  13 ++
 .../credentialDefinitions.service.ts          |  88 +++++++++++
 .../__tests__/schemas.controller.spec.ts      |   2 +-
 .../src/agent/schemas/schemas.service.ts      |   8 +-
 apps/ssi-abstraction/src/app.module.ts        |   3 +
 .../test/credentialDefinitions.e2e-spec.ts    | 137 ++++++++++++++++++
 apps/ssi-abstraction/test/schemas.e2e-spec.ts |   2 +-
 11 files changed, 456 insertions(+), 5 deletions(-)
 create mode 100644 apps/shared/src/events/credentialDefinitionEvents.ts
 create mode 100644 apps/ssi-abstraction/src/agent/credentialDefinitions/__tests__/credentialDefinitions.controller.spec.ts
 create mode 100644 apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.controller.ts
 create mode 100644 apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.module.ts
 create mode 100644 apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.service.ts
 create mode 100644 apps/ssi-abstraction/test/credentialDefinitions.e2e-spec.ts

diff --git a/apps/shared/src/events/credentialDefinitionEvents.ts b/apps/shared/src/events/credentialDefinitionEvents.ts
new file mode 100644
index 0000000..cc5741a
--- /dev/null
+++ b/apps/shared/src/events/credentialDefinitionEvents.ts
@@ -0,0 +1,74 @@
+import type { BaseEventInput } from './baseEvents.js';
+import type { AnonCredsCredentialDefinition } from '@aries-framework/anoncreds';
+
+import { BaseEvent } from './baseEvents.js';
+
+export type CredentialDefinitionWithId = AnonCredsCredentialDefinition & {
+  credentialDefinitionId: string;
+};
+
+export type EventAnonCredsCredentialDefinitionsGetAllInput = BaseEventInput;
+export class EventAnonCredsCredentialDefinitionsGetAll extends BaseEvent<
+  Array<CredentialDefinitionWithId>
+> {
+  public static token = 'anoncreds.credentialDefinitions.getAll';
+
+  public get instance() {
+    return this.data;
+  }
+
+  public static fromEvent(e: EventAnonCredsCredentialDefinitionsGetAll) {
+    return new EventAnonCredsCredentialDefinitionsGetAll(
+      e.data,
+      e.tenantId,
+      e.id,
+      e.type,
+      e.timestamp,
+    );
+  }
+}
+
+export type EventAnonCredsCredentialDefinitionsGetByIdInput = BaseEventInput<{
+  credentialDefinitionId: string;
+}>;
+export class EventAnonCredsCredentialDefinitionsGetById extends BaseEvent<CredentialDefinitionWithId | null> {
+  public static token = 'anoncreds.credentialDefinitions.getById';
+
+  public get instance() {
+    return this.data;
+  }
+
+  public static fromEvent(e: EventAnonCredsCredentialDefinitionsGetById) {
+    return new EventAnonCredsCredentialDefinitionsGetById(
+      e.data,
+      e.tenantId,
+      e.id,
+      e.type,
+      e.timestamp,
+    );
+  }
+}
+
+export type EventAnonCredsCredentialDefinitionsRegisterInput = BaseEventInput<{
+  schemaId: string;
+  tag: string;
+  issuerDid: string;
+}>;
+
+export class EventAnonCredsCredentialDefinitionsRegister extends BaseEvent<CredentialDefinitionWithId> {
+  public static token = 'anoncreds.credentialDefinitions.register';
+
+  public get instance() {
+    return this.data;
+  }
+
+  public static fromEvent(e: EventAnonCredsCredentialDefinitionsRegister) {
+    return new EventAnonCredsCredentialDefinitionsRegister(
+      e.data,
+      e.tenantId,
+      e.id,
+      e.type,
+      e.timestamp,
+    );
+  }
+}
diff --git a/apps/shared/src/index.ts b/apps/shared/src/index.ts
index 7f8ae5e..8fa24d3 100644
--- a/apps/shared/src/index.ts
+++ b/apps/shared/src/index.ts
@@ -8,3 +8,4 @@ export * from './events/connectionEvents.js';
 export * from './events/didEvents.js';
 export * from './events/tenantEvents.js';
 export * from './events/schemaEvents.js';
+export * from './events/credentialDefinitionEvents.js';
diff --git a/apps/ssi-abstraction/src/agent/credentialDefinitions/__tests__/credentialDefinitions.controller.spec.ts b/apps/ssi-abstraction/src/agent/credentialDefinitions/__tests__/credentialDefinitions.controller.spec.ts
new file mode 100644
index 0000000..b90afbc
--- /dev/null
+++ b/apps/ssi-abstraction/src/agent/credentialDefinitions/__tests__/credentialDefinitions.controller.spec.ts
@@ -0,0 +1,84 @@
+import type { AnonCredsCredentialDefinition } from '@aries-framework/anoncreds';
+
+import { Test } from '@nestjs/testing';
+
+import { mockConfigModule } from '../../../config/__tests__/mockConfig.js';
+import { AgentModule } from '../../agent.module.js';
+import { CredentialDefinitionsController } from '../credentialDefinitions.controller.js';
+import { CredentialDefinitionsService } from '../credentialDefinitions.service.js';
+
+describe('CredentialDefinitionsController', () => {
+  let credentialDefinitionsController: CredentialDefinitionsController;
+  let credentialDefinitionsService: CredentialDefinitionsService;
+
+  beforeEach(async () => {
+    const moduleRef = await Test.createTestingModule({
+      imports: [mockConfigModule(), AgentModule],
+      controllers: [CredentialDefinitionsController],
+      providers: [CredentialDefinitionsService],
+    }).compile();
+
+    credentialDefinitionsService = moduleRef.get(CredentialDefinitionsService);
+    credentialDefinitionsController = moduleRef.get(
+      CredentialDefinitionsController,
+    );
+  });
+
+  describe('get all', () => {
+    it('should get all the registered credentialDefinitions of the agent', async () => {
+      const result: Array<AnonCredsCredentialDefinition> = [];
+      jest
+        .spyOn(credentialDefinitionsService, 'getAll')
+        .mockResolvedValue(result);
+
+      const event = await credentialDefinitionsController.getAll({
+        tenantId: 'some-id',
+      });
+
+      expect(event.data).toStrictEqual(result);
+    });
+  });
+
+  describe('get by id', () => {
+    it('should get a credentialDefinition by id', async () => {
+      const result: AnonCredsCredentialDefinition | null = null;
+      jest
+        .spyOn(credentialDefinitionsService, 'getById')
+        .mockResolvedValue(result);
+
+      const event = await credentialDefinitionsController.getById({
+        credentialDefinitionId: 'id',
+        tenantId: 'some-id',
+      });
+
+      expect(event.data).toStrictEqual(result);
+    });
+  });
+
+  describe('register credentialDefinition', () => {
+    it('should register a credentialDefinition on a ledger', async () => {
+      const result: AnonCredsCredentialDefinition = {
+        tag: 'some-tag',
+        type: 'CL',
+        issuerId: 'did:indy:issuer',
+        schemaId: 'schemaid:123:default',
+        value: {
+          primary: {},
+        },
+      };
+
+      jest
+        .spyOn(credentialDefinitionsService, 'register')
+        .mockResolvedValue(result);
+
+      const event = await credentialDefinitionsController.register({
+        tenantId: 'some-tenant-id',
+        tag: 'some-tag',
+        issuerDid: 'did:indy:issuer',
+        schemaId: 'schemaid:123:default',
+      });
+
+      expect(event.data).toStrictEqual(result);
+    });
+  });
+});
diff --git a/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.controller.ts b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.controller.ts
new file mode 100644
index 0000000..2e75b94
--- /dev/null
+++ b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.controller.ts
@@ -0,0 +1,49 @@
+import { Controller } from '@nestjs/common';
+import { MessagePattern } from '@nestjs/microservices';
+import {
+  EventAnonCredsCredentialDefinitionsGetAll,
+  EventAnonCredsCredentialDefinitionsGetAllInput,
+  EventAnonCredsCredentialDefinitionsGetById,
+  EventAnonCredsCredentialDefinitionsGetByIdInput,
+  EventAnonCredsCredentialDefinitionsRegister,
+  EventAnonCredsCredentialDefinitionsRegisterInput,
+} from '@ocm/shared';
+
+import { CredentialDefinitionsService } from './credentialDefinitions.service.js';
+
+@Controller('credentialDefinitions')
+export class CredentialDefinitionsController {
+  public constructor(
+    private credentialDefinitionsService: CredentialDefinitionsService,
+  ) {}
+
+  @MessagePattern(EventAnonCredsCredentialDefinitionsGetAll.token)
+  public async getAll(
+    options: EventAnonCredsCredentialDefinitionsGetAllInput,
+  ): Promise<EventAnonCredsCredentialDefinitionsGetAll> {
+    return new EventAnonCredsCredentialDefinitionsGetAll(
+      await this.credentialDefinitionsService.getAll(options),
+      options.tenantId,
+    );
+  }
+
+  @MessagePattern(EventAnonCredsCredentialDefinitionsGetById.token)
+  public async getById(
+    options: EventAnonCredsCredentialDefinitionsGetByIdInput,
+  ): Promise<EventAnonCredsCredentialDefinitionsGetById> {
+    return new EventAnonCredsCredentialDefinitionsGetById(
+      await this.credentialDefinitionsService.getById(options),
+      options.tenantId,
+    );
+  }
+
+  @MessagePattern(EventAnonCredsCredentialDefinitionsRegister.token)
+  public async register(
+    options: EventAnonCredsCredentialDefinitionsRegisterInput,
+  ): Promise<EventAnonCredsCredentialDefinitionsRegister> {
+    return new EventAnonCredsCredentialDefinitionsRegister(
+      await this.credentialDefinitionsService.register(options),
+      options.tenantId,
+    );
+  }
+}
diff --git a/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.module.ts b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.module.ts
new file mode 100644
index 0000000..0bff1d8
--- /dev/null
+++ b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.module.ts
@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common';
+
+import { AgentModule } from '../agent.module.js';
+
+import { CredentialDefinitionsController } from './credentialDefinitions.controller.js';
+import { CredentialDefinitionsService } from './credentialDefinitions.service.js';
+
+@Module({
+  imports: [AgentModule],
+  providers: [CredentialDefinitionsService],
+  controllers: [CredentialDefinitionsController],
+})
+export class CredentialDefinitionsModule {}
diff --git a/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.service.ts b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.service.ts
new file mode 100644
index 0000000..32b2b61
--- /dev/null
+++ b/apps/ssi-abstraction/src/agent/credentialDefinitions/credentialDefinitions.service.ts
@@ -0,0 +1,88 @@
+import type { AnonCredsCredentialDefinition } from '@aries-framework/anoncreds';
+import type { IndyVdrRegisterCredentialDefinitionOptions } from '@aries-framework/indy-vdr';
+import type {
+  EventAnonCredsCredentialDefinitionsGetAllInput,
+  EventAnonCredsCredentialDefinitionsGetByIdInput,
+  EventAnonCredsCredentialDefinitionsRegisterInput,
+} from '@ocm/shared';
+
+import { Injectable } from '@nestjs/common';
+
+import { WithTenantService } from '../withTenantService.js';
+
+@Injectable()
+export class CredentialDefinitionsService {
+  public withTenantService: WithTenantService;
+
+  public constructor(withTenantService: WithTenantService) {
+    this.withTenantService = withTenantService;
+  }
+
+  public async getAll({
+    tenantId,
+  }: EventAnonCredsCredentialDefinitionsGetAllInput): Promise<
+    Array<AnonCredsCredentialDefinition>
+  > {
+    return this.withTenantService.invoke(tenantId, async (t) =>
+      (await t.modules.anoncreds.getCreatedCredentialDefinitions({})).map(
+        (r) => r.credentialDefinition,
+      ),
+    );
+  }
+
+  public async getById({
+    tenantId,
+    credentialDefinitionId,
+  }: EventAnonCredsCredentialDefinitionsGetByIdInput): Promise<AnonCredsCredentialDefinition | null> {
+    return this.withTenantService.invoke(tenantId, async (t) => {
+      const { credentialDefinition } =
+        await t.modules.anoncreds.getCredentialDefinition(
+          credentialDefinitionId,
+        );
+      return credentialDefinition ?? null;
+    });
+  }
+
+  public async register({
+    tenantId,
+    schemaId,
+    issuerDid,
+    tag,
+  }: EventAnonCredsCredentialDefinitionsRegisterInput): Promise<
+    AnonCredsCredentialDefinition & { credentialDefinitionId: string }
+  > {
+    return this.withTenantService.invoke(tenantId, async (t) => {
+      const { credentialDefinitionState } =
+        await t.modules.anoncreds.registerCredentialDefinition<IndyVdrRegisterCredentialDefinitionOptions>(
+          {
+            credentialDefinition: {
+              issuerId: issuerDid,
+              type: 'CL',
+              schemaId,
+              tag,
+            },
+            options: {
+              endorserMode: 'internal',
+              endorserDid: issuerDid,
+            },
+          },
+        );
+
+      if (credentialDefinitionState.state !== 'finished') {
+        throw new Error(
+          `Error registering credentialDefinition: ${
+            credentialDefinitionState.state === 'failed'
+              ? credentialDefinitionState.reason
+              : 'Not Finished'
+          }`,
+        );
+      }
+
+      return {
+        credentialDefinitionId:
+          credentialDefinitionState.credentialDefinitionId,
+        ...credentialDefinitionState.credentialDefinition,
+      };
+    });
+  }
+}
diff --git a/apps/ssi-abstraction/src/agent/schemas/__tests__/schemas.controller.spec.ts b/apps/ssi-abstraction/src/agent/schemas/__tests__/schemas.controller.spec.ts
index d761a21..95fb19a 100644
--- a/apps/ssi-abstraction/src/agent/schemas/__tests__/schemas.controller.spec.ts
+++ b/apps/ssi-abstraction/src/agent/schemas/__tests__/schemas.controller.spec.ts
@@ -7,7 +7,7 @@ import { AgentModule } from '../../agent.module.js';
 import { SchemasController } from '../schemas.controller.js';
 import { SchemasService } from '../schemas.service.js';
 
-describe('ConnectionsController', () => {
+describe('SchemassController', () => {
   let schemasController: SchemasController;
   let schemasService: SchemasService;
 
diff --git a/apps/ssi-abstraction/src/agent/schemas/schemas.service.ts b/apps/ssi-abstraction/src/agent/schemas/schemas.service.ts
index da3a7dc..e72d2c4 100644
--- a/apps/ssi-abstraction/src/agent/schemas/schemas.service.ts
+++ b/apps/ssi-abstraction/src/agent/schemas/schemas.service.ts
@@ -42,7 +42,9 @@ export class SchemasService {
     version,
     issuerDid,
     attributeNames,
-  }: EventAnonCredsSchemasRegisterInput): Promise<AnonCredsSchema> {
+  }: EventAnonCredsSchemasRegisterInput): Promise<
+    AnonCredsSchema & { schemaId: string }
+  > {
     return this.withTenantService.invoke(tenantId, async (t) => {
       const { schemaState } =
         await t.modules.anoncreds.registerSchema<IndyVdrRegisterSchemaOptions>({
@@ -53,7 +55,7 @@ export class SchemasService {
             attrNames: attributeNames,
           },
           options: {
-            endorserMode: 'external',
+            endorserMode: 'internal',
             endorserDid: issuerDid,
           },
         });
@@ -66,7 +68,7 @@ export class SchemasService {
         );
       }
 
-      return schemaState.schema;
+      return { schemaId: schemaState.schemaId, ...schemaState.schema };
     });
   }
 }
diff --git a/apps/ssi-abstraction/src/app.module.ts b/apps/ssi-abstraction/src/app.module.ts
index 52662f0..876afc5 100644
--- a/apps/ssi-abstraction/src/app.module.ts
+++ b/apps/ssi-abstraction/src/app.module.ts
@@ -6,6 +6,7 @@ import { HealthController } from '@ocm/shared';
 
 import { AgentModule } from './agent/agent.module.js';
 import { ConnectionsModule } from './agent/connections/connections.module.js';
+import { CredentialDefinitionsModule } from './agent/credentialDefinitions/credentialDefinitions.module.js';
 import { SchemasModule } from './agent/schemas/schemas.module.js';
 import { TenantsModule } from './agent/tenants/tenants.module.js';
 import { config } from './config/config.js';
@@ -21,6 +22,8 @@ import { validationSchema } from './config/validation.js';
     }),
     AgentModule,
     ConnectionsModule,
+    SchemasModule,
+    CredentialDefinitionsModule,
     DidsModule,
     SchemasModule,
     TenantsModule,
diff --git a/apps/ssi-abstraction/test/credentialDefinitions.e2e-spec.ts b/apps/ssi-abstraction/test/credentialDefinitions.e2e-spec.ts
new file mode 100644
index 0000000..2f87be6
--- /dev/null
+++ b/apps/ssi-abstraction/test/credentialDefinitions.e2e-spec.ts
@@ -0,0 +1,137 @@
+import type { INestApplication } from '@nestjs/common';
+import type { ClientProxy } from '@nestjs/microservices';
+import type {
+  EventAnonCredsCredentialDefinitionsGetAllInput,
+  EventAnonCredsCredentialDefinitionsGetByIdInput,
+  EventAnonCredsCredentialDefinitionsRegisterInput,
+} from '@ocm/shared';
+
+import { ClientsModule, Transport } from '@nestjs/microservices';
+import { Test } from '@nestjs/testing';
+import {
+  EventAnonCredsCredentialDefinitionsGetById,
+  EventAnonCredsCredentialDefinitionsGetAll,
+  EventAnonCredsCredentialDefinitionsRegister,
+} from '@ocm/shared';
+import { firstValueFrom } from 'rxjs';
+
+import { AgentModule } from '../src/agent/agent.module.js';
+import { CredentialDefinitionsModule } from '../src/agent/credentialDefinitions/credentialDefinitions.module.js';
+import { DidsModule } from '../src/agent/dids/dids.module.js';
+import { DidsService } from '../src/agent/dids/dids.service.js';
+import { SchemasModule } from '../src/agent/schemas/schemas.module.js';
+import { SchemasService } from '../src/agent/schemas/schemas.service.js';
+import { TenantsModule } from '../src/agent/tenants/tenants.module.js';
+import { TenantsService } from '../src/agent/tenants/tenants.service.js';
+import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
+
+describe('CredentialDefinitions', () => {
+  const TOKEN = 'CREDENTIAL_DEFINITIONS_CLIENT_SERVICE';
+  let app: INestApplication;
+  let client: ClientProxy;
+  let tenantId: string;
+
+  let issuerDid: string;
+  let schemaId: string;
+
+  beforeAll(async () => {
+    const moduleRef = await Test.createTestingModule({
+      imports: [
+        mockConfigModule(3004, true),
+        AgentModule,
+        SchemasModule,
+        CredentialDefinitionsModule,
+        TenantsModule,
+        DidsModule,
+        ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]),
+      ],
+    }).compile();
+
+    app = moduleRef.createNestApplication();
+
+    app.connectMicroservice({ transport: Transport.NATS });
+
+    await app.startAllMicroservices();
+    await app.init();
+
+    client = app.get(TOKEN);
+    await client.connect();
+
+    const tenantsService = app.get(TenantsService);
+    const { id } = await tenantsService.create(TOKEN);
+    tenantId = id;
+
+    const didsService = app.get(DidsService);
+    const [did] = await didsService.registerDidIndyFromSeed({
+      tenantId,
+      seed: '12312367897123300000000000000000',
+    });
+    issuerDid = did;
+
+    const schemaService = app.get(SchemasService);
+    const { schemaId: sid } = await schemaService.register({
+      issuerDid,
+      tenantId,
+      name: 'test-schema-name',
+      version: `1.${new Date().getTime()}`,
+      attributeNames: ['none'],
+    });
+    schemaId = sid;
+  });
+
+  afterAll(async () => {
+    await app.close();
+    client.close();
+  });
+
+  it(EventAnonCredsCredentialDefinitionsGetAll.token, async () => {
+    const response$ = client.send<
+      EventAnonCredsCredentialDefinitionsGetAll,
+      EventAnonCredsCredentialDefinitionsGetAllInput
+    >(EventAnonCredsCredentialDefinitionsGetAll.token, { tenantId });
+    const response = await firstValueFrom(response$);
+    const eventInstance =
+      EventAnonCredsCredentialDefinitionsGetAll.fromEvent(response);
+
+    expect(eventInstance.instance).toEqual(expect.arrayContaining([]));
+  });
+
+  it(EventAnonCredsCredentialDefinitionsGetById.token, async () => {
+    const response$ = client.send<
+      EventAnonCredsCredentialDefinitionsGetById,
+      EventAnonCredsCredentialDefinitionsGetByIdInput
+    >(EventAnonCredsCredentialDefinitionsGetById.token, {
+      tenantId,
+      credentialDefinitionId: 'some-id',
+    });
+    const response = await firstValueFrom(response$);
+    const eventInstance =
+      EventAnonCredsCredentialDefinitionsGetById.fromEvent(response);
+
+    expect(eventInstance.instance).toEqual(null);
+  });
+
+  it(EventAnonCredsCredentialDefinitionsRegister.token, async () => {
+    const tag = `tag:${new Date().getTime()}`;
+    const response$ = client.send<
+      EventAnonCredsCredentialDefinitionsRegister,
+      EventAnonCredsCredentialDefinitionsRegisterInput
+    >(EventAnonCredsCredentialDefinitionsRegister.token, {
+      tenantId,
+      schemaId,
+      issuerDid,
+      tag,
+    });
+
+    const response = await firstValueFrom(response$);
+    const eventInstance =
+      EventAnonCredsCredentialDefinitionsRegister.fromEvent(response);
+
+    expect(eventInstance.instance).toMatchObject({
+      schemaId,
+      tag,
+      issuerId: issuerDid,
+      type: 'CL',
+    });
+  });
+});
diff --git a/apps/ssi-abstraction/test/schemas.e2e-spec.ts b/apps/ssi-abstraction/test/schemas.e2e-spec.ts
index acfe669..018b1d3 100644
--- a/apps/ssi-abstraction/test/schemas.e2e-spec.ts
+++ b/apps/ssi-abstraction/test/schemas.e2e-spec.ts
@@ -92,7 +92,7 @@ describe('Schemas', () => {
   });
 
   it(EventAnonCredsSchemasRegister.token, async () => {
-    const version = new Date().getTime().toString();
+    const version = `1.${new Date().getTime()}`;
     const attributeNames = ['names', 'age'];
     const name = 'my-schema';
     const response$ = client.send<
-- 
GitLab