Source: status-list-credential/status-list2021-credential.js

import {
  decodeList,
  createList,
  createCredential,
  StatusList, // eslint-disable-line
} from '@digitalcredentials/vc-status-list';
import { u8aToHex, u8aToU8a } from '@polkadot/util';
import { gzip, ungzip } from 'pako';
import { DockStatusList2021Qualifier } from '../utils/vc/constants';
import VerifiableCredential from '../verifiable-credential';
import { ensureStatusListId } from '../utils/type-helpers';
import { KeyDoc } from "../utils/vc/helpers"; // eslint-disable-line

/**
 * Status list 2021 verifiable credential as per https://www.w3.org/TR/vc-status-list/#statuslist2021credential.
 */
export default class StatusList2021Credential extends VerifiableCredential {
  /**
   * Create a new Status List 2021 Verifiable Credential instance.
   * @param {string} id - id of the credential
   */
  constructor(id) {
    super(id);

    // Caches decoded status list.
    Object.defineProperty(this, 'internalCachedStatusList', {
      value: { encoded: void 0, decoded: void 0 },
      writable: true,
    });
  }

  /**
   * Fail if the given verifiable credential id isn't a valid `StatusList2021Credential` id.
   * @param {*} id
   */
  static verifyID(id) {
    ensureStatusListId(id);
  }

  /**
   * Creates new `StatusList2021Credential` with supplied `id` and option `statusPurpose` = `revocation` by default,
   * `length` and `revokeIndices`. Note that credential with `statusPurpose` = `revocation` can't unsuspend its indices.
   * To allow unrevoking indices in the future, use `statusPurpose` = `suspension`.
   * The proof will be generated immediately using supplied `keyDoc`.
   *
   * @param {KeyDoc} keyDoc
   * @param {string} id - on-chain hex identifier for the `StatusList2021Credential`.
   * @param {object} [params={}]
   * @param {'revocation'|'suspension'} [params.statusPurpose=revocation] - `statusPurpose` of the `StatusList2021Credential`.
   * Can be either `revocation` or `suspension`.
   * @param {number} [params.length=1e4] - length of the underlying `StatusList`.
   * @param {Iterable<number>} [params.revokeIndices=[]] - iterable producing indices to be revoked or suspended initially
   * @returns {Promise<StatusList2021Credential>}
   */
  static async create(
    keyDoc,
    id,
    { statusPurpose = 'revocation', length = 1e4, revokeIndices = [] } = {},
  ) {
    const statusList = await createList({ length });
    this.updateStatusList(statusPurpose, statusList, revokeIndices);

    const jsonCred = await createCredential({
      id: `${this.qualifier}${id}`,
      list: statusList,
      statusPurpose,
    });

    const cred = this.fromJSON(jsonCred);

    return await cred.sign(keyDoc);
  }

  /**
   * Revokes indices and unsuspends other indices in the underlying status list, regenerating the proof.
   * If `statusPurpose` = `revocation`, indices can't be unsuspended.
   * The status list revoked (suspended)/unsuspended indices will be set atomically and in case of an error,
   * the underlying value won't be modified.
   * Throws an error if the underlying status list can't be decoded or any of the supplied indices is out of range.
   *
   * @param {KeyDoc} keyDoc
   * @param {object} [update={}]
   * @param {Iterable<number>} update.revokeIndices - indices to be revoked or suspended
   * @param {Iterable<number>} update.unsuspendIndices - indices to be unsuspended
   * @returns {Promise<this>}
   */
  async update(keyDoc, { revokeIndices = [], unsuspendIndices = [] }) {
    const currentStatusList = await this.decodedStatusList();
    const statusList = new StatusList({
      buffer: new Uint8Array((currentStatusList).bitstring.bits),
    });

    this.constructor.updateStatusList(
      this.credentialSubject.statusPurpose,
      statusList,
      revokeIndices,
      unsuspendIndices,
    );

    this.credentialSubject.encodedList = await statusList.encode();
    // Remove `proof` so that an array of `proof`s is not created by the following `sign` call.
    delete this.proof;
    this.setIssuanceDate(new Date().toISOString());

    await this.sign(keyDoc);

    return this;
  }

  /**
   * Returns a `Promise` resolving to the decoded `StatusList`.
   *
   * @returns {Promise<StatusList>}
   */
  async decodedStatusList() {
    const { encoded, decoded } = this.internalCachedStatusList;

    if (
      encoded === this.credentialSubject.encodedList
      && decoded !== void 0
    ) {
      return decoded;
    } else {
      this.internalCachedStatusList = {
        encoded: this.credentialSubject.encodedList,
        decoded: decodeList(this.credentialSubject),
      };

      return this.internalCachedStatusList.decoded;
    }
  }

  /**
   * Returns `true` if given index is revoked or suspended, `false` otherwise.
   * Throws an error if the underlying status list can't be decoded or supplied index is out of range.
   *
   * @param {number} index
   * @returns {Promise<boolean>}
   */
  async revoked(index) {
    const decodedStatusList = await this.decodedStatusList();

    return decodedStatusList.getStatus(index);
  }

  /**
   * Accepts an iterable of indices to be checked and returns an array containing `true` in the positions
   * of revoked (suspended) indices and `false` for non-revoked (non-suspended) indices.
   * Throws an error if the underlying status list can't be decoded or any of supplied indices is out of range.
   *
   * @param {Iterable<number>} indices
   * @returns {Promise<Array<boolean>>}
   */
  async revokedBatch(indices) {
    const decodedStatusList = await this.decodedStatusList();

    return [...indices].map((index) => decodedStatusList.getStatus(index));
  }

  /**
   * Decodes `StatusList2021Credential` from provided bytes.
   * @param {Uint8Array} bytes
   */
  static fromBytes(bytes) {
    const gzipBufferCred = Buffer.from(u8aToU8a(bytes));
    const stringifiedCred = ungzip(gzipBufferCred, { to: 'string' });
    const parsedCred = JSON.parse(stringifiedCred);

    return this.fromJSON(parsedCred);
  }

  /**
   * Instantiates `StatusList2021Credential` from the provided `JSON`.
   *
   * @param {object} json
   * @returns {StatusList2021Credential}
   */
  static fromJSON(json) {
    const cred = super.fromJSON(json);
    cred.validate();

    return cred;
  }

  /**
   * Converts `StatusList2021Credential` to its JSON representation.
   * @returns {object}
   */
  toJSON() {
    this.validate();

    return super.toJSON();
  }

  /**
   * Encodes `StatusList2021Credential` as bytes.
   * @returns {Uint8Array}
   */
  toBytes() {
    const jsonCred = this.toJSON();
    const stringifiedCred = JSON.stringify(jsonCred);
    const bufferCred = Buffer.from(stringifiedCred);

    return gzip(bufferCred);
  }

  /**
   * Converts given credentials to the substrate-compatible representation.
   *
   * @returns {{ StatusList2021Credential: string }}
   */
  toSubstrate() {
    return { StatusList2021Credential: u8aToHex(this.toBytes()) };
  }

  /**
   * Validates underlying `StatusList2021Credential`.
   */
  validate() {
    const { credentialSubject } = this;

    if (!credentialSubject) throw new Error('Missing `credentialSubject`');
    if (!this.constructor.statusPurposes.has(credentialSubject.statusPurpose)) {
      throw new Error(
        `Invalid \`statusPurpose\`, expected one of \`${[
          ...this.constructor.statusPurposes,
        ].join(', ')}\``,
      );
    }
    if (typeof credentialSubject.id !== 'string' || !credentialSubject.id) {
      throw new Error('Missing `credentialSubject.id`');
    }
    if (credentialSubject.type !== 'StatusList2021') {
      throw new Error(
        '`credentialSubject.type` must be set to `StatusList2021`',
      );
    }
    if (!credentialSubject.encodedList) {
      throw new Error('`credentialSubject.encodedList` must be present');
    }
  }

  /**
   * Revokes (suspends) `revokeIndices` and unsuspends `unsuspendIndices` from the supplied `StatusList`.
   *
   * Throws an error if
   * - non-empty `unsuspendIndices` passed along with `statusPurpose != suspension`
   * - any index is present in both iterables
   * - some index is out of bounds.
   *
   * @param {'revocation'|'suspension'} statusPurpose
   * @param {StatusList} statusList
   * @param {Iterable<number>} revokeIndices
   * @param {Iterable<number>} unsuspendIndices
   */
  static updateStatusList(
    statusPurpose,
    statusList,
    revokeIndices = [],
    unsuspendIndices = [],
  ) {
    const unsuspendIndiceSet = new Set(unsuspendIndices);
    if (statusPurpose !== 'suspension' && unsuspendIndiceSet.size > 0) {
      throw new Error(
        `Can't unsuspend indices for credential with \`statusPurpose\` = \`${statusPurpose}\`, it's only possible with \`statusPurpose\` = \`suspension\``,
      );
    }

    for (const idx of revokeIndices) {
      if (unsuspendIndiceSet.has(idx)) {
        throw new Error(
          `Index \`${idx}\` appears in both revoke and unsuspend sets`,
        );
      }

      statusList.setStatus(idx, true);
    }
    for (const idx of unsuspendIndiceSet) {
      statusList.setStatus(idx, false);
    }
  }
}

/**
 * Allowed status purposes for this credential type.
 */
StatusList2021Credential.statusPurposes = new Set(['revocation', 'suspension']);
StatusList2021Credential.qualifier = DockStatusList2021Qualifier;