openwallet-foundation / credo-ts

Typescript framework for building decentralized identity and verifiable credential solutions
https://credo.js.org
Apache License 2.0
273 stars 200 forks source link

Create AnonCreds service that can handle multiple AnonCredsRegistries #1123

Closed karimStekelenburg closed 1 year ago

karimStekelenburg commented 1 year ago

Description

One of the things we're building into AFJ is the ability to talk to multiple ledgers. In the architecture we're building (as depicted in the image below), we separate the credential format-related logic (orange box) from the logic that deals with communication to the ledgers (red box). Connection management and ledger communication are handled by implementations of the AnonCredsRegistry interface. In order to support multiple ledgers, we need to create an AnonCredsService that manages these AnonCredsRegistrys.

Knowing which ledger to use

With support for multiple ledgers, the AnonCredsService will need to figure out what AnonCredsRegistry instance it should use when a call is being made. This can be derived from the identifiers that are passed via the method parameters. The AnonCredsService needs a regex-like mechanism to match this identifier to the corresponding registry.

Identifiers

Indy networks can be identified in two ways: did:indy identifiers and legacy identifiers. More information on did:indy identifiers can be found here.

The other network that we will add support for is cheqd. This network uses the did:cheqd identifier. More info here.

šŸ’” The logic required here is similar to That that of the DidRegistrarService and DidResolverService.

Approaches

There are two possible approaches to implementing this.

Using AnonCredsService as a bridge

Here the AnonCredsService fetches the right AnonCredsRegistry and calls it.

class AnonCredsService {
  holder: AnonCredsHolder;
  issuer: AnonCredsIssuer;
  verifier: AnonCredsVerifier;

  registries: AnonCredsRegistry[];

  private getRegistryForIdentifier(identifier: string): AnonCredsRegistry { /** not important **/ }

  public async createCredentialDefinition(options: CreateCredentialOptions) {
    const registry = getRegistryForIdentifier(options.schemaId);
    const credentialDefinition = await this.issuer.createCredentialDefinition(options);

    // Here AnonCredsService is responsible for calling the registry
    await registry.writeCredentialDefinition(credentialDefinition);

    return credentialDefinition
  }
}

Using AnonCredsApi as a bridge

Another option is to move the responsibility of calling the AnonCredsRegistry one level up, and let it be handled by the AnonCredsApi.


class AnonCredsService {
  holder: AnonCredsHolder;
  issuer: AnonCredsIssuer;
  verifier: AnonCredsVerifier;

  registries: AnonCredsRegistry[];

  public getRegistryForIdentifier(identifier: string): AnonCredsRegistry { /** not important **/ }

  public async createCredentialDefinition(options: CreateCredentialOptions) {
    // We don't bother dealing with the registry here
    const credentialDefinition = await this.issuer.createCredentialDefinition(options);
    return credentialDefinition
  }
}

class AnonCredsApi {
  service: AnonCredsService

  private getRegistryForIdentifier(identifier: string): AnonCredsRegistry { /** not important **/ }

  public async createCredentialDefinition(options: CreateCredentialOptions) {
    const registry = service.getRegistryForIdentifier(options.schemaId);
    const credentialDefinition = await service.createCredentialDefinition(options);

    // Here AnonCredsApi is responsible for calling the registry
    await registry.writeCredentialDefinition(credentialDefinition);

    return credentialDefinition
  }
}

Todo

DOD: There is an AnonCredsRegistryService that can be called to find the registry instance for a specific anoncreds model identifier (schema id, credential definition id, revocation registry id)

TimoGlastra commented 1 year ago

I think for the second approach we could move it up even more by making the AnonCreds service more an AnonCredsRegistryService (like we have the IndyPoolService) and only let it manage the registires. The code in the API will then look something like:

class AnonCredsRegistryService {
  registries: AnonCredsRegistry[];

  public getRegistryForIdentifier(identifier: string): AnonCredsRegistry { /** not important **/ }
}

class AnonCredsApi {
  registryService: AnonCredsRegistryService
  issuer: AnonCredsIssuerService

  private getRegistryForIdentifier(identifier: string): AnonCredsRegistry { /** not important **/ }

  public async createCredentialDefinition(options: CreateCredentialOptions) {
    const registry = registryService.getRegistryForIdentifier(options.schemaId);
    const credentialDefinition = await issuer.createCredentialDefinition(options);

    // Here AnonCredsApi is responsible for calling the registry
    await registry.writeCredentialDefinition(credentialDefinition);

    return credentialDefinition
  }
}

Just posting it here for comparison. I don't have strong preferences. But we should probably look at what the purpose of each service is. If we need to expose all methods from the issuer / holder /verifier services on the AnonCreds service that will be quite some. Or will it only add methods related to reading / writing to/from the registry?