Source: modules/trust-registry.js

import { BTreeSet, BTreeMap } from '@polkadot/types';
import { u8aToHex } from '@polkadot/util';
import { DidMethodKey, DockDid, typedHexDID } from '../utils/did';
import { isHexWithGivenByteSize } from '../utils/codec';
import { getDidNonce, ensureMatchesPattern } from '../utils/misc';

const callValueMethodOrObjectMethod = (method) => (value) => (typeof value[method] === 'function'
  ? [...value[method]()]
  : Object[method](value));

const entries = callValueMethodOrObjectMethod('entries');
const values = callValueMethodOrObjectMethod('values');

/**
 * `Trust Registry` module.
 */
export default class TrustRegistryModule {
  /**
   * Creates a new instance of `StatusListCredentialModule` and sets the api
   * @constructor
   * @param {object} api - PolkadotJS API Reference
   * @param signAndSend
   */
  constructor(api, signAndSend) {
    this.api = api;
    this.module = api.tx.trustRegistry;
    this.signAndSend = signAndSend;
  }

  /**
   * Returns Trust Registries information according to the supplied `by` argument.
   * @param by
   */
  async registriesInfo(by) {
    ensureMatchesPattern(this.constructor.RegistriesQueryByPattern, by);

    return this.parseMapEntries(
      this.parseRegistryInfo,
      await this.api.rpc.trustRegistry.registriesInfoBy(by),
    );
  }

  /**
   * Returns schemas metadata for the registry according to the supplied `by` argument.
   * @param by
   * @param {string} regId
   * @returns {Promise<object>}
   */
  async registrySchemasMetadata(by, regId) {
    ensureMatchesPattern(this.constructor.RegistryQueryByPattern, by);

    return this.parseMapEntries(
      this.parseSchemaMetadata,
      await this.api.rpc.trustRegistry.registrySchemaMetadataBy(by, regId),
    );
  }

  /**
   * Retrieves metadata for the supplied `schemaId`/`regId` combination.
   *
   * @param {string} schemaId
   * @param {string} regId
   * @returns {Promise<object>}
   */
  async schemaMetadataInRegistry(schemaId, regId) {
    return this.parseSingleEntry(
      this.parseSchemaMetadata,
      await this.api.rpc.trustRegistry.schemaMetadataInRegistry(
        schemaId,
        regId,
      ),
    );
  }

  /**
   * Retrieves metadata for the supplied `schemaId` from all registries.
   *
   * @param {string} schemaId
   * @returns {Promise<object>}
   */
  async schemaMetadata(schemaId) {
    return this.parseMapEntries(
      this.parseSchemaMetadata,
      await this.api.rpc.trustRegistry.schemaMetadata(schemaId),
    );
  }

  /**
   * Retrieves issuers for the supplied `schemaId`/`regId` combination.
   *
   * @param {string} schemaId
   * @param {string} regId
   * @returns {Promise<object>}
   */
  async schemaIssuersInRegistry(schemaId, regId) {
    return this.parseSingleEntry(
      this.parseSchemaIssuers,
      await this.api.rpc.trustRegistry.schemaIssuersInRegistry(schemaId, regId),
    );
  }

  /**
   * Retrieves issuers for the supplied `schemaId` from all registries.
   *
   * @param {string} schemaId
   * @returns {Promise<object>}
   */
  async schemaIssuers(schemaId) {
    return this.parseMapEntries(
      this.parseSchemaIssuers,
      await this.api.rpc.trustRegistry.schemaIssuers(schemaId),
    );
  }

  /**
   * Retrieves verifiers for the supplied `schemaId`/`regId` combination.
   *
   * @param {string} schemaId
   * @param {string} regId
   * @returns {Promise<object>}
   */
  async schemaVerifiersInRegistry(schemaId, regId) {
    return this.parseSingleEntry(
      this.parseSchemaVerifiers,
      await this.api.rpc.trustRegistry.schemaVerifiersInRegistry(
        schemaId,
        regId,
      ),
    );
  }

  /**
   * Retrieves verifiers for the supplied `schemaId` from all registries.
   *
   * @param {string} schemaId
   * @returns {Promise<object>}
   */
  async schemaVerifiers(schemaId) {
    return this.parseMapEntries(
      this.parseSchemaVerifiers,
      await this.api.rpc.trustRegistry.schemaVerifiers(schemaId),
    );
  }

  /**
   * Initializes Trust Registry with the supplied parameters.
   *
   * @param convenerDid
   * @param registryId
   * @param name
   * @param govFramework
   * @param signingKeyRef
   * @param nonceOrDidModule
   * @param waitForFinalization
   * @param params
   * @returns {Promise<null>}
   */
  async initOrUpdate(
    convenerDid,
    registryId,
    name,
    govFramework,
    signingKeyRef,
    nonceOrDidModule,
    waitForFinalization = true,
    params = {},
  ) {
    const tx = await this.initOrUpdateTx(
      convenerDid,
      registryId,
      name,
      govFramework,
      signingKeyRef,
      nonceOrDidModule,
    );
    return this.signAndSend(tx, waitForFinalization, params);
  }

  /**
   * Initializes Trust Registry with the supplied parameters.
   *
   * @param convenerDid
   * @param registryId
   * @param name
   * @param govFramework
   * @param signingKeyRef
   * @param nonceOrDidModule
   * @param waitForFinalization
   * @param params
   * @returns {Promise<null>}
   */
  async initOrUpdateTx(
    convenerDid,
    registryId,
    name,
    govFramework,
    signingKeyRef,
    { nonce = undefined, didModule = undefined } = {},
  ) {
    const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce(
      convenerDid,
      { nonce, didModule },
    );

    return convenerHexDid.changeState(
      this.api,
      this.module.initOrUpdateTrustRegistry,
      'InitOrUpdateTrustRegistry',
      {
        registryId,
        name,
        govFramework,
        nonce: lastNonce,
      },
      signingKeyRef,
    );
  }

  /**
   * Sets schema metadatas in the registry.
   *
   * @param convenerOrIssuerOrVerifierDid
   * @param registryId
   * @param schemas
   * @param signingKeyRef
   * @param nonceOrDidModule
   * @param waitForFinalization
   * @param params
   * @returns {Promise<null>}
   */
  async setSchemasMetadata(
    convenerOrIssuerOrVerifierDid,
    registryId,
    schemas,
    signingKeyRef,
    nonceOrDidModule,
    waitForFinalization = true,
    params = {},
  ) {
    const tx = await this.setSchemasMetadataTx(
      convenerOrIssuerOrVerifierDid,
      registryId,
      schemas,
      signingKeyRef,
      nonceOrDidModule,
    );
    return this.signAndSend(tx, waitForFinalization, params);
  }

  /**
   * Creates a transaction to update schema metadatas in the registry.
   *
   * @param convenerOrIssuerOrVerifierDid
   * @param registryId
   * @param schemas
   * @param signingKeyRef
   * @param nonce
   * @param didModule
   * @returns {Promise<null>}
   */
  async setSchemasMetadataTx(
    convenerOrIssuerOrVerifierDid,
    registryId,
    schemas,
    signingKeyRef,
    { nonce = undefined, didModule = undefined } = {},
  ) {
    const [convenerOrIssuerOrVerifierHexDid, lastNonce] = await this.getActorDidAndNonce(convenerOrIssuerOrVerifierDid, {
      nonce,
      didModule,
    });
    ensureMatchesPattern(this.constructor.SchemasUpdatePattern, schemas);

    return convenerOrIssuerOrVerifierHexDid.changeState(
      this.api,
      this.module.setSchemasMetadata,
      'SetSchemasMetadata',
      { registryId, schemas, nonce: lastNonce },
      signingKeyRef,
    );
  }

  /**
   * Suspends issuers in the registry.
   *
   * @param convenerDid
   * @param registryId
   * @param issuers
   * @param signingKeyRef
   * @param nonceOrDidModule
   * @param waitForFinalization
   * @param params
   * @returns {Promise<null>}
   */
  async suspendIssuers(
    convenerDid,
    registryId,
    issuers,
    signingKeyRef,
    nonceOrDidModule,
    waitForFinalization = true,
    params = {},
  ) {
    const tx = await this.suspendIssuersTx(
      convenerDid,
      registryId,
      issuers,
      signingKeyRef,
      nonceOrDidModule,
    );
    return this.signAndSend(tx, waitForFinalization, params);
  }

  /**
   * Suspends issuers in the registry.
   *
   * @param convenerDid
   * @param registryId
   * @param issuers
   * @param signingKeyRef
   * @param nonceOrDidModule
   * @returns {Promise<null>}
   */
  async suspendIssuersTx(
    convenerDid,
    registryId,
    issuers,
    signingKeyRef,
    { nonce = undefined, didModule = undefined } = {},
  ) {
    const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce(
      convenerDid,
      { nonce, didModule },
    );

    const hexIssuers = new BTreeSet(this.api.registry, 'Issuer');
    for (const issuer of issuers) {
      hexIssuers.add(typedHexDID(this.api, issuer));
    }

    return convenerHexDid.changeState(
      this.api,
      this.module.suspendIssuers,
      'SuspendIssuers',
      { registryId, issuers: hexIssuers, nonce: lastNonce },
      signingKeyRef,
    );
  }

  /**
   * Unsuspends issuers in the registry.
   *
   * @param convenerDid
   * @param registryId
   * @param issuers
   * @param signingKeyRef
   * @param nonceOrDidModule
   * @param waitForFinalization
   * @param params
   * @returns {Promise<null>}
   */
  async unsuspendIssuers(
    convenerDid,
    registryId,
    issuers,
    signingKeyRef,
    nonceOrDidModule,
    waitForFinalization = true,
    params = {},
  ) {
    const tx = await this.unsuspendIssuersTx(
      convenerDid,
      registryId,
      issuers,
      signingKeyRef,
      nonceOrDidModule,
    );
    return this.signAndSend(tx, waitForFinalization, params);
  }

  /**
   * Unsuspends issuers in the registry.
   *
   * @param convenerDid
   * @param registryId
   * @param issuers
   * @param signingKeyRef
   * @param nonceOrDidModule
   * @returns {Promise<null>}
   */
  async unsuspendIssuersTx(
    convenerDid,
    registryId,
    issuers,
    signingKeyRef,
    { nonce = undefined, didModule = undefined } = {},
  ) {
    const [convenerHexDid, lastNonce] = await this.getActorDidAndNonce(
      convenerDid,
      { nonce, didModule },
    );

    const hexIssuers = new BTreeSet(this.api.registry, 'Issuer');
    for (const issuer of issuers) {
      hexIssuers.add(typedHexDID(this.api, issuer));
    }

    return convenerHexDid.changeState(
      this.api,
      this.module.unsuspendIssuers,
      'UnsuspendIssuers',
      { registryId, issuers: hexIssuers, nonce: lastNonce },
      signingKeyRef,
    );
  }

  /**
   * Sets delegated issuers for the caller DID.
   *
   * @param issuerDid
   * @param registryId
   * @param issuers
   * @param delegated
   * @param signingKeyRef
   * @param params
   * @param waitForFinalization
   * @param params
   * @returns {Promise<null>}
   */
  async updateDelegatedIssuers(
    issuerDid,
    registryId,
    delegated,
    signingKeyRef,
    { nonce = undefined, didModule = undefined } = {},
    waitForFinalization = true,
    params = {},
  ) {
    const [issuerHexDid, lastNonce] = await this.getActorDidAndNonce(
      issuerDid,
      { nonce, didModule },
    );

    return this.signAndSend(
      issuerHexDid.changeState(
        this.api,
        this.module.updateDelegatedIssuers,
        'UpdateDelegatedIssuers',
        { registryId, delegated, nonce: lastNonce },
        signingKeyRef,
      ),
      waitForFinalization,
      params,
    );
  }

  /**
   * Get the DID doing the action and its corresponding nonce.
   *
   * @param actorDid
   * @param nonce
   * @param didModule
   * @returns {Promise}
   */
  async getActorDidAndNonce(
    actorDid,
    { nonce = undefined, didModule = undefined } = {},
  ) {
    const hexDID = typedHexDID(this.api, actorDid);
    const lastNonce = nonce ?? (await getDidNonce(hexDID, nonce, didModule));
    return [hexDID, lastNonce];
  }

  /**
   * Parses Trust Registry information received from the substrate side.
   *
   * @param registryInfo
   */
  parseRegistryInfo({ name, convener, govFramework }) {
    return {
      name: name.toString(),
      convener: typedHexDID(this.api, convener).toQualifiedEncodedString(),
      govFramework: u8aToHex(govFramework),
    };
  }

  /**
   * Parses map entries by converting keys to `string`s and applying supplied parser to the value.
   *
   * @template {RegId}
   * @template {Value}
   * @template {ParsedValue}
   *
   * @param {function(Value): ParsedValue} valueParser
   * @param {Map<RegId, Value>} regs
   * @returns {Object<string, ParsedValue>}
   */
  parseMapEntries(valueParser, regs) {
    return Object.fromEntries(
      entries(regs)
        .map(([key, value]) => [String(key), valueParser.call(this, value)])
        .sort(([key1], [key2]) => key1.localeCompare(key2)),
    );
  }

  /**
   * Parses a single entry.
   *
   * @template {Value}
   * @template {ParsedValue}
   * @param {function(Value): ParsedValue}
   * @returns {ParsedValue}
   */
  parseSingleEntry(parser, valueOpt) {
    let value;

    if (valueOpt.isNone) {
      return null;
    } else if (valueOpt.isSome) {
      value = valueOpt.unwrap();
    } else {
      value = valueOpt;
    }

    return parser.call(this, value);
  }

  /**
   * Parses schema metadata.
   *
   * @param {{ issuers: Map, verifiers: Set }} metadata
   * @returns {{ issuers: object, verifiers: Array }}
   */
  parseSchemaMetadata({ issuers, verifiers }) {
    return {
      issuers: this.parseSchemaIssuers(issuers),
      verifiers: this.parseSchemaVerifiers(verifiers),
    };
  }

  /**
   * Parses schema issuers.
   *
   * @param {Map} issuers
   * @returns {object}
   */
  parseSchemaIssuers(issuers) {
    return Object.fromEntries(
      (Array.isArray(issuers) ? values(issuers) : entries(issuers))
        .map((issuerWithInfo) => values(issuerWithInfo))
        .map(([issuer, info]) => [
          typedHexDID(this.api, issuer).toQualifiedEncodedString(),
          typeof info.toJSON === 'function' ? info.toJSON() : info,
        ])
        .sort(([iss1], [iss2]) => iss1.localeCompare(iss2)),
    );
  }

  /**
   * Parses schema verifiers.
   *
   * @param {Array} verifiers
   * @returns {object}
   */
  parseSchemaVerifiers(verifiers) {
    return values(verifiers)
      .map((verifier) => typedHexDID(this.api, verifier).toQualifiedEncodedString())
      .sort((ver1, ver2) => ver1.localeCompare(ver2));
  }
}

const DockDidOrDidMethodKeyPattern = {
  $anyOf: [{ $instanceOf: DockDid }, { $instanceOf: DidMethodKey }],
};

const VerificationPricePattern = {
  $anyOf: [{ $matchType: 'number' }, { $matchType: 'object' }],
};

const Hex32Pattern = {
  $ensure: (value) => {
    if (!isHexWithGivenByteSize(value, 32)) {
      throw new Error(`Expected 32-byte hex sequence, received: ${value}`);
    }
  },
};

const VerifiersPattern = {
  $instanceOf: BTreeSet,
  $iterableOf: DockDidOrDidMethodKeyPattern,
};

const VerifiersUpdatePattern = {
  $instanceOf: BTreeMap,
  $mapOf: [
    DockDidOrDidMethodKeyPattern,
    {
      $anyOf: [
        { $matchValue: 'Remove' },
        {
          $matchValue: 'Add',
        },
      ],
    },
  ],
};

const IssuerPricesPattern = {
  $instanceOf: BTreeMap,
  $mapOf: [{ $matchType: 'string' }, VerificationPricePattern],
};

const IssuerPricesUpdatePattern = {
  $instanceOf: BTreeMap,
  $mapOf: [
    { $matchType: 'string' },
    {
      $anyOf: [
        { $matchValue: 'Remove' },
        {
          $objOf: {
            Add: VerificationPricePattern,
            Set: VerificationPricePattern,
          },
        },
      ],
    },
  ],
};

const IssuersPattern = {
  $instanceOf: BTreeMap,
  $mapOf: [DockDidOrDidMethodKeyPattern, IssuerPricesPattern],
};

const IssuersUpdatePattern = {
  $instanceOf: BTreeMap,
  $mapOf: [
    DockDidOrDidMethodKeyPattern,
    {
      $objOf: {
        Modify: IssuerPricesUpdatePattern,
        Set: IssuerPricesPattern,
      },
    },
  ],
};

const SetAllSchemasPattern = {
  $instanceOf: BTreeMap,
  $mapOf: [
    Hex32Pattern,
    {
      $matchObject: {
        issuers: IssuersPattern,
        verifiers: VerifiersPattern,
      },
    },
  ],
};

const ModifySchemasPattern = {
  $instanceOf: BTreeMap,
  $mapOf: [
    Hex32Pattern,
    {
      $anyOf: [
        {
          $objOf: {
            Add: {
              $matchObject: {
                issuers: IssuersPattern,
                verifiers: VerifiersPattern,
              },
            },
            Set: {
              $matchObject: {
                issuers: IssuersPattern,
                verifiers: VerifiersPattern,
              },
            },
            Modify: {
              $matchObject: {
                issuers: {
                  $objOf: { Modify: IssuersUpdatePattern, Set: IssuersPattern },
                },
                verifiers: {
                  $objOf: {
                    Modify: VerifiersUpdatePattern,
                    Set: VerifiersPattern,
                  },
                },
              },
            },
          },
        },
        {
          $matchValue: 'Remove',
        },
      ],
    },
  ],
};
const AnyOfOrAllDockDidOrDidMethodKeyPattern = {
  $objOf: {
    All: {
      $iterableOf: DockDidOrDidMethodKeyPattern,
    },
    AnyOf: {
      $iterableOf: DockDidOrDidMethodKeyPattern,
    },
  },
};

TrustRegistryModule.SchemasUpdatePattern = {
  $objOf: {
    Set: SetAllSchemasPattern,
    Modify: ModifySchemasPattern,
  },
};
TrustRegistryModule.RegistryQueryByPattern = {
  $matchObject: {
    issuers: AnyOfOrAllDockDidOrDidMethodKeyPattern,
    verifiers: AnyOfOrAllDockDidOrDidMethodKeyPattern,
    issuersOrVerifiers: AnyOfOrAllDockDidOrDidMethodKeyPattern,
    schemaIds: {
      $iterableOf: Hex32Pattern,
    },
  },
};
TrustRegistryModule.RegistriesQueryByPattern = {
  $matchObject: {
    issuers: AnyOfOrAllDockDidOrDidMethodKeyPattern,
    verifiers: AnyOfOrAllDockDidOrDidMethodKeyPattern,
    issuersOrVerifiers: AnyOfOrAllDockDidOrDidMethodKeyPattern,
    schemaIds: {
      $objOf: {
        All: {
          $iterableOf: Hex32Pattern,
        },
        AnyOf: {
          $iterableOf: Hex32Pattern,
        },
      },
    },
  },
};