Source

client/FabricClientAdapter.ts

import {
  CouchDBAdapter,
  CouchDBKeys,
  type MangoQuery,
} from "@decaf-ts/for-couchdb";
import { Client } from "@grpc/grpc-js";
import * as grpc from "@grpc/grpc-js";
import { Model, type Serializer } from "@decaf-ts/decorator-validation";
import { debug, final, Logging } from "@decaf-ts/logging";
import { type FabricFlags, type PeerConfig } from "../shared/types";
import {
  connect,
  ConnectOptions,
  Gateway,
  Network,
  ProposalOptions,
  Contract as Contrakt,
  type Signer,
} from "@hyperledger/fabric-gateway";
import { getIdentity, getSigner } from "./fabric-fs";
import {
  BaseError,
  InternalError,
  OperationKeys,
  SerializationError,
  BulkCrudOperationKeys,
  NotFoundError,
  ConflictError,
  BadRequestError,
} from "@decaf-ts/db-decorators";
import { Context, type PrimaryKeyType } from "@decaf-ts/db-decorators";
import {
  Adapter,
  AuthorizationError,
  ConnectionError,
  ContextOf,
  ForbiddenError,
  MigrationError,
  ObserverError,
  PagingError,
  PersistenceKeys,
  PreparedModel,
  QueryError,
  Repository,
  UnsupportedError,
} from "@decaf-ts/core";
import type { ContextualArgs, MaybeContextualArg } from "@decaf-ts/core";
import { FabricClientRepository } from "./FabricClientRepository";
import { FabricFlavour } from "../shared/constants";
import { ClientSerializer } from "../shared/ClientSerializer";
import type { FabricClientDispatch } from "./FabricClientDispatch";
import { HSMSignerFactoryCustom } from "./fabric-hsm";
import { type Constructor } from "@decaf-ts/decoration";

export class FabricClientContext extends Context<FabricFlags> {}
/**
 * @description Adapter for interacting with Hyperledger Fabric networks
 * @summary The FabricAdapter extends CouchDBAdapter to provide a seamless interface for interacting with Hyperledger Fabric networks.
 * It handles connection management, transaction submission, and CRUD operations against Fabric chaincode.
 * @template PeerConfig - Configuration type for connecting to a Fabric peer
 * @template FabricFlags - Flags specific to Fabric operations
 * @template Context<FabricFlags> - Context type containing Fabric-specific flags
 * @param config - Configuration for connecting to a Fabric peer
 * @param alias - Optional alias for the adapter instance
 * @class FabricClientAdapter
 * @example
 * ```typescript
 * // Create a new FabricAdapter instance
 * const config: PeerConfig = {
 *   mspId: 'Org1MSP',
 *   peerEndpoint: 'localhost:7051',
 *   channelName: 'mychannel',
 *   chaincodeName: 'mycc',
 *   contractName: 'mycontract',
 *   tlsCertPath: '/path/to/tls/cert',
 *   certDirectoryPath: '/path/to/cert/dir',
 *   keyDirectoryPath: '/path/to/key/dir'
 * };
 *
 * const adapter = new FabricAdapter(config, 'org1-adapter');
 *
 * // Use the adapter to interact with the Fabric network
 * const result = await adapter.read('users', 'user1', mySerializer);
 * ```
 * @mermaid
 * sequenceDiagram
 *   participant Client
 *   participant FabricAdapter
 *   participant Gateway
 *   participant Network
 *   participant Contract
 *   participant Chaincode
 *
 *   Client->>FabricAdapter: create(tableName, id, model, transient, serializer)
 *   FabricAdapter->>FabricAdapter: submitTransaction(OperationKeys.CREATE, [serializedModel], transient)
 *   FabricAdapter->>Gateway: connect()
 *   Gateway->>Network: getNetwork(channelName)
 *   Network->>Contract: getContract(chaincodeName, contractName)
 *   FabricAdapter->>Contract: submit(api, proposalOptions)
 *   Contract->>Chaincode: invoke
 *   Chaincode-->>Contract: response
 *   Contract-->>FabricAdapter: result
 *   FabricAdapter->>FabricAdapter: decode(result)
 *   FabricAdapter->>FabricAdapter: serializer.deserialize(decodedResult)
 *   FabricAdapter-->>Client: deserializedResult
 */
export class FabricClientAdapter extends CouchDBAdapter<
  PeerConfig,
  Client,
  FabricClientContext
> {
  /**
   * @description Static text decoder for converting Uint8Array to string
   */
  private static decoder = new TextDecoder("utf8");

  private static serializer = new ClientSerializer();

  protected static log = Logging.for(FabricClientAdapter);

  protected readonly serializer: Serializer<any> =
    FabricClientAdapter.serializer;

  /**
   * @description Creates a new FabricAdapter instance
   * @summary Initializes a new adapter for interacting with a Hyperledger Fabric network
   * @param {PeerConfig} config - Configuration for connecting to a Fabric peer
   * @param {string} [alias] - Optional alias for the adapter instance
   */
  constructor(config: PeerConfig, alias?: string) {
    super(config, FabricFlavour, alias);
  }

  override async context<M extends Model>(
    operation:
      | OperationKeys.CREATE
      | OperationKeys.READ
      | OperationKeys.UPDATE
      | OperationKeys.DELETE
      | string,
    overrides: Partial<FabricFlags>,
    model: Constructor<M> | Constructor<M>[],
    ...args: any[]
  ): Promise<FabricClientContext> {
    const log = this.log.for(this.context);
    log.debug(
      `Creating new context for ${operation} operation on ${Array.isArray(model) ? model.map((m) => m.name) : model.name} model with flag overrides: ${JSON.stringify(overrides)}`
    );
    const flags = await this.flags(operation, model, overrides, ...args);
    return new FabricClientContext().accumulate(flags) as FabricClientContext;
  }

  /**
   * @description Decodes a Uint8Array to a string
   * @summary Converts binary data received from Fabric to a string using UTF-8 encoding
   * @param {Uint8Array} data - The binary data to decode
   * @return {string} The decoded string
   */
  decode(data: Uint8Array): string {
    return FabricClientAdapter.decoder.decode(data);
  }

  override repository<
    R extends Repository<
      any,
      Adapter<PeerConfig, Client, MangoQuery, FabricClientContext>
    >,
  >(): Constructor<R> {
    return FabricClientRepository as unknown as Constructor<R>;
  }

  protected override createPrefix<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: MaybeContextualArg<ContextOf<FabricClientAdapter>>
  ): [Constructor<M>, PrimaryKeyType, Record<string, any>, ...any[], Context] {
    const { ctxArgs } = this.logCtx(args, this.createPrefix);
    const tableName = Model.tableName(clazz);
    const record: Record<string, any> = {};
    record[CouchDBKeys.TABLE] = tableName;
    // record[CouchDBKeys.ID] = this.generateId(tableName, id as any);
    Object.assign(record, model);
    return [clazz, id, record, ...ctxArgs];
  }

  /**
   * @description Prepares multiple records for creation
   * @summary Adds necessary CouchDB fields to multiple records before creation
   * @param {string} tableName - The name of the table
   * @param {string[]|number[]} ids - The IDs of the records
   * @param models - The models to prepare
   * @return A tuple containing the tableName, ids, and prepared records
   * @throws {InternalError} If ids and models arrays have different lengths
   */
  protected override createAllPrefix<M extends Model>(
    clazz: Constructor<M>,
    ids: string[] | number[],
    models: Record<string, any>[],
    ...args: MaybeContextualArg<ContextOf<FabricClientAdapter>>
  ) {
    const tableName = Model.tableName(clazz);
    if (ids.length !== models.length)
      throw new InternalError("Ids and models must have the same length");
    const { ctxArgs } = this.logCtx(args, this.createAllPrefix);
    const records = ids.map((id, count) => {
      const record: Record<string, any> = {};
      record[CouchDBKeys.TABLE] = tableName;
      // record[CouchDBKeys.ID] = this.generateId(tableName, id);
      Object.assign(record, models[count]);
      return record;
    });
    return [clazz, ids, records, ...ctxArgs];
  }

  protected override updateAllPrefix<M extends Model>(
    clazz: Constructor<M>,
    ids: PrimaryKeyType[],
    models: Record<string, any>[],
    ...args: MaybeContextualArg<ContextOf<FabricClientAdapter>>
  ) {
    const tableName = Model.tableName(clazz);
    if (ids.length !== models.length)
      throw new InternalError("Ids and models must have the same length");
    const { ctxArgs } = this.logCtx(args, this.updateAllPrefix);
    const records = ids.map((id, count) => {
      const record: Record<string, any> = {};
      record[CouchDBKeys.TABLE] = tableName;
      // record[CouchDBKeys.ID] = this.generateId(tableName, id);
      // // const rev = models[count][PersistenceKeys.METADATA];
      // if (!rev)
      //   throw new InternalError(
      //     `No revision number found for record with id ${id}`
      //   );
      // Object.assign(record, models[count]);
      // record[CouchDBKeys.REV] = rev;
      return record;
    });
    return [clazz, ids, records, ...ctxArgs];
  }

  /**
   * @description Creates multiple records in a single transaction
   * @summary Submits a transaction to create multiple records in the Fabric ledger
   * @param {string} tableName - The name of the table/collection
   * @param {string[] | number[]} ids - Array of record identifiers
   * @param {Array<Record<string, any>>} models - Array of record data
   * @param {Record<string, any>} transient - Transient data for the transaction
   * @return {Promise<Array<Record<string, any>>>} Promise resolving to the created records
   */
  override async createAll<M extends Model>(
    clazz: Constructor<M>,
    ids: PrimaryKeyType[],
    models: Record<string, any>[],
    ...args: ContextualArgs<FabricClientContext>
  ): Promise<Record<string, any>[]> {
    if (ids.length !== models.length)
      throw new InternalError("Ids and models must have the same length");
    const ctxArgs = [...(args as unknown as any[])];
    const transient = ctxArgs.shift() as Record<string, any>;
    const { log } = this.logCtx(
      ctxArgs as ContextualArgs<FabricClientContext>,
      this.createAll
    );
    const tableName = Model.tableName(clazz);

    log.info(`adding ${ids.length} entries to ${tableName} table`);
    log.verbose(`pks: ${ids}`);
    const result = await this.submitTransaction(
      BulkCrudOperationKeys.CREATE_ALL,
      [ids, models.map((m) => this.serializer.serialize(m, clazz.name))],
      transient,
      undefined,
      tableName
    );
    try {
      return JSON.parse(this.decode(result)).map((r: any) => JSON.parse(r));
    } catch (e: unknown) {
      throw new SerializationError(e as Error);
    }
  }

  /**
   * @description Reads multiple records in a single transaction
   * @summary Submits a transaction to read multiple records from the Fabric ledger
   * @param {string} tableName - The name of the table/collection
   * @param {string[] | number[]} ids - Array of record identifiers to read
   * @return {Promise<Array<Record<string, any>>>} Promise resolving to the retrieved records
   */
  override async readAll<M extends Model>(
    clazz: Constructor<M>,
    ids: PrimaryKeyType[],
    ...args: ContextualArgs<FabricClientContext>
  ): Promise<Record<string, any>[]> {
    const { log } = this.logCtx(args, this.readAll);
    const tableName = Model.tableName(clazz);
    log.info(`reading ${ids.length} entries to ${tableName} table`);
    log.verbose(`pks: ${ids}`);
    const result = await this.evaluateTransaction(
      BulkCrudOperationKeys.READ_ALL,
      [ids],
      undefined,
      undefined,
      tableName
    );
    try {
      return JSON.parse(this.decode(result)).map((r: any) => JSON.parse(r));
    } catch (e: unknown) {
      throw new SerializationError(e as Error);
    }
  }

  /**
   * @description Updates multiple records in a single transaction
   * @summary Submits a transaction to update multiple records in the Fabric ledger
   * @param {string} tableName - The name of the table/collection
   * @param {string[] | number[]} ids - Array of record identifiers
   * @param {Array<Record<string, any>>} models - Array of updated record data
   * @param {Record<string, any>} transient - Transient data for the transaction
   * @return {Promise<Array<Record<string, any>>>} Promise resolving to the updated records
   */
  override async updateAll<M extends Model>(
    clazz: Constructor<M>,
    ids: PrimaryKeyType[],
    models: Record<string, any>[],
    ...args: ContextualArgs<FabricClientContext>
  ): Promise<Record<string, any>[]> {
    if (ids.length !== models.length)
      throw new InternalError("Ids and models must have the same length");
    const ctxArgs = [...(args as unknown as any[])];
    const transient = ctxArgs.shift() as Record<string, any>;
    const { log } = this.logCtx(
      ctxArgs as ContextualArgs<FabricClientContext>,
      this.updateAll
    );
    const tableName = Model.tableName(clazz);
    log.info(`updating ${ids.length} entries to ${tableName} table`);
    log.verbose(`pks: ${ids}`);

    const result = await this.submitTransaction(
      BulkCrudOperationKeys.UPDATE_ALL,
      [ids, models.map((m) => this.serializer.serialize(m, clazz.name))],
      transient,
      undefined,
      clazz.name
    );
    try {
      return JSON.parse(this.decode(result)).map((r: any) => JSON.parse(r));
    } catch (e: unknown) {
      throw new SerializationError(e as Error);
    }
  }

  /**
   * @description Deletes multiple records in a single transaction
   * @summary Submits a transaction to delete multiple records from the Fabric ledger
   * @param {string} tableName - The name of the table/collection
   * @param {Array<string | number | bigint>} ids - Array of record identifiers to delete
   * @param {Serializer<any>} serializer - Serializer for the model data
   * @return {Promise<Array<Record<string, any>>>} Promise resolving to the deleted records
   */
  override async deleteAll<M extends Model>(
    clazz: Constructor<M>,
    ids: PrimaryKeyType[],
    ...args: ContextualArgs<FabricClientContext>
  ): Promise<Record<string, any>[]> {
    const { log } = Adapter.logCtx(args, this.deleteAll);
    const tableName = Model.tableName(clazz);
    log.info(`deleting ${ids.length} entries to ${tableName} table`);
    log.verbose(`pks: ${ids}`);
    const result = await this.submitTransaction(
      BulkCrudOperationKeys.DELETE_ALL,
      [ids],
      undefined,
      undefined,
      clazz.name
    );
    try {
      return JSON.parse(this.decode(result)).map((r: any) => JSON.parse(r));
    } catch (e: unknown) {
      throw new SerializationError(e as Error);
    }
  }

  /**
   * @description Prepares a model for persistence
   * @summary Converts a model instance into a format suitable for database storage,
   * handling column mapping and separating transient properties
   * @template M - The model type
   * @param {M} model - The model instance to prepare
   * @param pk - The primary key property name
   * @return The prepared data
   */
  override prepare<M extends Model>(
    model: M,
    ...args: ContextualArgs<FabricClientContext>
  ): PreparedModel {
    const { log } = this.logCtx(args, this.prepare);
    const split = Model.segregate(model);
    if ((model as any)[PersistenceKeys.METADATA]) {
      log.silly(
        `Passing along persistence metadata for ${(model as any)[PersistenceKeys.METADATA]}`
      );
      Object.defineProperty(split.model, PersistenceKeys.METADATA, {
        enumerable: false,
        writable: false,
        configurable: true,
        value: (model as any)[PersistenceKeys.METADATA],
      });
    }

    return {
      record: split.model,
      id: model[Model.pk(model.constructor as Constructor<M>)] as string,
      transient: split.transient,
    };
  }

  /**
   * @description Converts database data back into a model instance
   * @summary Reconstructs a model instance from database data, handling column mapping
   * and reattaching transient properties
   * @template M - The model type
   * @param obj - The database record
   * @param {string|Constructor<M>} clazz - The model class or name
   * @param pk - The primary key property name
   * @param {string|number|bigint} id - The primary key value
   * @param [transient] - Transient properties to reattach
   * @return {M} The reconstructed model instance
   */
  override revert<M extends Model>(
    obj: Record<string, any>,
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    transient?: Record<string, any>,
    ...args: ContextualArgs<FabricClientContext>
  ): M {
    const { log, ctx } = this.logCtx(args, this.revert);
    const ob: Record<string, any> = {};
    const pk = Model.pk(clazz) as string;
    ob[pk] = id;
    const m = new clazz(ob) as M;
    log.silly(`Rebuilding model ${m.constructor.name} id ${id}`);
    const metadata = obj[PersistenceKeys.METADATA];
    const result = Object.keys(m).reduce((accum: M, key) => {
      (accum as Record<string, any>)[key] = obj[key];
      return accum;
    }, m);

    if (ctx.get("rebuildWithTransient") && transient) {
      log.verbose(
        `re-adding transient properties: ${Object.keys(transient).join(", ")}`
      );
      Object.entries(transient).forEach(([key, val]) => {
        if (key in result)
          throw new InternalError(
            `Transient property ${key} already exists on model ${m.constructor.name}. should be impossible`
          );
        result[key as keyof M] = val;
      });
    }

    if (metadata) {
      // TODO move to couchdb
      log.silly(
        `Passing along ${this.flavour} persistence metadata for ${m.constructor.name} id ${id}: ${metadata}`
      );
      Object.defineProperty(result, PersistenceKeys.METADATA, {
        enumerable: false,
        configurable: true,
        writable: true,
        value: metadata,
      });
    }

    return result;
  }

  /**
   * @description Creates an index for a model
   * @summary This method is not implemented for Fabric and will throw an error
   * @template M - Type extending Model
   * @param {Constructor<M>} models - The model constructor
   * @return {Promise<void>} Promise that will throw an error
   */
  @debug()
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  protected index<M>(models: Constructor<M>): Promise<void> {
    throw new Error();
  }

  /**
   * @description Creates a single record
   * @summary Submits a transaction to create a record in the Fabric ledger
   * @param {string} tableName - The name of the table/collection
   * @param {string | number} id - The record identifier
   * @param {Record<string, any>} model - The record data
   * @param {Record<string, any>} transient - Transient data for the transaction
   * @return {Promise<Record<string, any>>} Promise resolving to the created record
   */
  @debug()
  @final()
  override async create<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: ContextualArgs<FabricClientContext>
  ): Promise<Record<string, any>> {
    const ctxArgs = [...(args as unknown as any[])];
    const transient = {}; //(ctxArgs.shift() as Record<string, any>) || {}; TODO: Verify
    const { log } = this.logCtx(
      ctxArgs as ContextualArgs<FabricClientContext>,
      this.create
    );
    const tableName = Model.tableName(clazz);
    log.verbose(`adding entry to ${tableName} table`);
    log.debug(`pk: ${id}`);
    const result = await this.submitTransaction(
      OperationKeys.CREATE,
      [this.serializer.serialize(model, clazz.name)],
      transient,
      undefined,
      clazz.name
    );
    return this.serializer.deserialize(this.decode(result));
  }

  /**
   * @description Reads a single record
   * @summary Evaluates a transaction to read a record from the Fabric ledger
   * @param {string} tableName - The name of the table/collection
   * @param {string | number} id - The record identifier
   * @return {Promise<Record<string, any>>} Promise resolving to the retrieved record
   */
  @debug()
  @final()
  async read<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    ...args: ContextualArgs<FabricClientContext>
  ): Promise<Record<string, any>> {
    const { log } = this.logCtx(args, this.readAll);
    const tableName = Model.tableName(clazz);

    log.verbose(`reading entry from ${tableName} table`);
    log.debug(`pk: ${id}`);
    const result = await this.evaluateTransaction(
      OperationKeys.READ,
      [id.toString()],
      undefined,
      undefined,
      clazz.name
    );
    return this.serializer.deserialize(this.decode(result));
  }

  override updatePrefix<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: MaybeContextualArg<FabricClientContext>
  ) {
    const tableName = Model.tableName(clazz);
    const { ctxArgs } = this.logCtx(args, this.updatePrefix);
    const record: Record<string, any> = {};
    record[CouchDBKeys.TABLE] = tableName;
    // record[CouchDBKeys.ID] = this.generateId(tableName, id);
    Object.assign(record, model);
    return [clazz, id, record, ...ctxArgs];
  }

  /**
   * @description Updates a single record
   * @summary Submits a transaction to update a record in the Fabric ledger
   * @param {string} tableName - The name of the table/collection
   * @param {string | number} id - The record identifier
   * @param {Record<string, any>} model - The updated record data
   * @param {Record<string, any>} transient - Transient data for the transaction
   * @return {Promise<Record<string, any>>} Promise resolving to the updated record
   */
  @debug()
  @final()
  async update<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: ContextualArgs<FabricClientContext>
  ): Promise<Record<string, any>> {
    const ctxArgs = [...(args as unknown as any[])];
    const transient = {}; //(ctxArgs.shift() as Record<string, any>) || {}; TODO: Verify
    const { log } = this.logCtx(
      ctxArgs as ContextualArgs<FabricClientContext>,
      this.updateAll
    );
    log.info(`CLIENT UPDATE class : ${typeof clazz}`);
    const tableName = Model.tableName(clazz);
    log.verbose(`updating entry to ${tableName} table`);
    log.debug(`pk: ${id}`);
    const result = await this.submitTransaction(
      OperationKeys.UPDATE,
      [this.serializer.serialize(model, clazz.name || clazz)], // TODO should be receving class but is receiving string
      transient,
      undefined,
      clazz.name
    );
    return this.serializer.deserialize(this.decode(result));
  }

  /**
   * @description Deletes a single record
   * @summary Submits a transaction to delete a record from the Fabric ledger
   * @param {string} tableName - The name of the table/collection
   * @param {string | number} id - The record identifier to delete
   * @return {Promise<Record<string, any>>} Promise resolving to the deleted record
   */
  @debug()
  @final()
  override async delete<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    ...args: ContextualArgs<FabricClientContext>
  ): Promise<Record<string, any>> {
    const { log } = this.logCtx(args, this.delete);
    const tableName = Model.tableName(clazz);
    log.verbose(`deleting entry from ${tableName} table`);
    log.debug(`pk: ${id}`);
    const result = await this.submitTransaction(
      OperationKeys.DELETE,
      [id.toString()],
      undefined,
      undefined,
      clazz.name
    );
    return this.serializer.deserialize(this.decode(result));
  }

  /**
   * @description Executes a raw query against the Fabric ledger
   * @summary Evaluates a transaction to perform a query using Mango Query syntax
   * @template V - The return type
   * @param {MangoQuery} rawInput - The Mango Query to execute
   * @param {boolean} process - Whether to process the result
   * @return {Promise<V>} Promise resolving to the query result
   * @mermaid
   * sequenceDiagram
   *   participant Client
   *   participant FabricAdapter
   *   participant Contract
   *   participant Chaincode
   *
   *   Client->>FabricAdapter: raw(rawInput, process)
   *   FabricAdapter->>FabricAdapter: JSON.stringify(rawInput)
   *   FabricAdapter->>FabricAdapter: evaluateTransaction("query", [input])
   *   FabricAdapter->>Contract: evaluate("query", proposalOptions)
   *   Contract->>Chaincode: invoke
   *   Chaincode-->>Contract: response
   *   Contract-->>FabricAdapter: result
   *   FabricAdapter->>FabricAdapter: JSON.parse(decode(result))
   *   FabricAdapter->>FabricAdapter: Process result based on type
   *   FabricAdapter-->>Client: processed result
   */
  @debug()
  override async raw<V>(
    rawInput: MangoQuery,
    ...args: ContextualArgs<FabricClientContext>
  ): Promise<V> {
    const ctxArgs = [...(args as unknown as any[])];
    if (typeof ctxArgs[0] === "boolean") {
      ctxArgs.shift();
    }
    let tableName: string | Constructor<any> | undefined;
    if (
      ctxArgs.length &&
      (typeof ctxArgs[0] === "string" || typeof ctxArgs[0] === "function")
    ) {
      tableName = ctxArgs.shift();
    }
    const { log } = this.logCtx(
      ctxArgs as ContextualArgs<FabricClientContext>,
      this.raw
    );
    log.info(`Performing raw  query on table`);
    log.debug(`processing raw input for query: ${JSON.stringify(rawInput)}`);
    let input: string;
    try {
      input = JSON.stringify(rawInput);
    } catch (e: any) {
      throw new SerializationError(
        `Failed to process raw input for query: ${e}`
      );
    }
    let transactionResult: any;
    try {
      if (typeof tableName !== "string")
        tableName = (tableName as Constructor<any> | undefined)?.name;
      transactionResult = await this.evaluateTransaction(
        "query",
        [input],
        undefined,
        undefined,
        tableName
      );
    } catch (e: unknown) {
      throw this.parseError(e as Error);
    }
    let result: any;
    try {
      result = JSON.parse(this.decode(transactionResult));
    } catch (e: any) {
      throw new SerializationError(`Failed to process result: ${e}`);
    }

    const parseRecord = (record: Record<any, any>) => {
      if (Model.isModel(record)) return Model.build(record);
      return record;
    };

    if (Array.isArray(result)) {
      if (!result.length) return result as V;
      const el = result[0];
      if (Model.isModel(el))
        // if the first one is a model, all are models
        return result.map((el) => Model.build(el)) as V;
      return result as V;
    }

    return parseRecord(result as any) as V;
  }

  /**
   * @description Gets or creates a gRPC client for the Fabric peer
   * @summary Returns a cached client or creates a new one if none exists
   * @return {Promise<Client>} Promise resolving to the gRPC client
   */
  override getClient(): Client {
    if (!this._client)
      this._client = FabricClientAdapter.getClient(this.config);
    return this._client;
  }

  /**
   * @description Gets a Gateway instance for the Fabric network
   * @summary Creates a new Gateway instance using the current client
   * @return {Promise<Gateway>} Promise resolving to the Gateway instance
   */
  protected async Gateway(): Promise<Gateway> {
    return FabricClientAdapter.getGateway(this.config, this.client);
  }

  private getContractName(className?: string) {
    if (!className) return undefined;
    return `${className}Contract`;
  }

  /**
   * @description Gets a Contract instance for the Fabric chaincode
   * @summary Creates a new Contract instance using the current Gateway
   * @return {Promise<Contrakt>} Promise resolving to the Contract instance
   */
  protected async Contract(contractName?: string): Promise<Contrakt> {
    return FabricClientAdapter.getContract(
      await this.Gateway(),
      this.config,
      contractName
    );
  }

  /**
   * @description Executes a transaction on the Fabric network
   * @summary Submits or evaluates a transaction on the Fabric chaincode
   * @param {string} api - The chaincode function to call
   * @param {boolean} submit - Whether to submit (true) or evaluate (false) the transaction
   * @param {any[]} [args] - Arguments to pass to the chaincode function
   * @param {Record<string, string>} [transientData] - Transient data for the transaction
   * @param {Array<string>} [endorsingOrganizations] - Organizations that must endorse the transaction
   * @return {Promise<Uint8Array>} Promise resolving to the transaction result
   * @mermaid
   * sequenceDiagram
   *   participant FabricAdapter
   *   participant Gateway
   *   participant Contract
   *   participant Chaincode
   *
   *   FabricAdapter->>Gateway: connect()
   *   FabricAdapter->>Contract: getContract()
   *   alt submit transaction
   *     FabricAdapter->>Contract: submit(api, proposalOptions)
   *   else evaluate transaction
   *     FabricAdapter->>Contract: evaluate(api, proposalOptions)
   *   end
   *   Contract->>Chaincode: invoke
   *   Chaincode-->>Contract: response
   *   Contract-->>FabricAdapter: result
   *   FabricAdapter->>Gateway: close()
   */
  protected async transaction(
    api: string,
    submit = true,
    args?: any[],
    transientData?: Record<string, string>,
    endorsingOrganizations?: Array<string>,
    className?: string
  ): Promise<Uint8Array> {
    const log = this.log.for(this.transaction);
    const gateway = await this.Gateway();
    try {
      const contract = await this.Contract(this.getContractName(className));
      log.verbose(
        `${submit ? "Submit" : "Evaluate"}ting transaction ${this.getContractName(className) || this.config.contractName}.${api}`
      );
      log.debug(`args: ${args?.map((a) => a.toString()).join("\n") || "none"}`);
      const method = submit ? contract.submit : contract.evaluate;

      endorsingOrganizations = endorsingOrganizations?.length
        ? endorsingOrganizations
        : undefined;
      const proposalOptions: ProposalOptions = {
        arguments: args || [],
        transientData: transientData,
        // ...(endorsingOrganizations && { endorsingOrganizations }) // mspId list
      };

      return await method.call(contract, api, proposalOptions);
    } catch (e: any) {
      if (e.code === 10) {
        throw new Error(`${e.details[0].message}`);
      }
      throw this.parseError(e);
    } finally {
      this.log.debug(`Closing ${this.config.mspId} gateway connection`);
      gateway.close();
    }
  }

  /**
   * @description Parses an error into a BaseError
   * @summary Converts any error into a standardized BaseError
   * @param {Error | string} err - The error to parse
   * @param {string} [reason] - Optional reason for the error
   * @return {BaseError} The parsed error
   */
  override parseError<E extends BaseError>(
    err: Error | string,
    reason?: string
  ): E {
    return FabricClientAdapter.parseError<E>(err, reason);
  }

  /**
   * @description Submits a transaction to the Fabric network
   * @summary Executes a transaction that modifies the ledger state
   * @param {string} api - The chaincode function to call
   * @param {any[]} [args] - Arguments to pass to the chaincode function
   * @param {Record<string, string>} [transientData] - Transient data for the transaction
   * @param {Array<string>} [endorsingOrganizations] - Organizations that must endorse the transaction
   * @return {Promise<Uint8Array>} Promise resolving to the transaction result
   */
  async submitTransaction(
    api: string,
    args?: any[],
    transientData?: Record<string, string>,
    endorsingOrganizations?: Array<string>,
    className?: string
  ): Promise<Uint8Array> {
    return this.transaction(
      api,
      true,
      args,
      transientData,
      endorsingOrganizations,
      className
    );
  }

  /**
   * @description Evaluates a transaction on the Fabric network
   * @summary Executes a transaction that does not modify the ledger state
   * @param {string} api - The chaincode function to call
   * @param {any[]} [args] - Arguments to pass to the chaincode function
   * @param {Record<string, string>} [transientData] - Transient data for the transaction
   * @param {Array<string>} [endorsingOrganizations] - Organizations that must endorse the transaction
   * @return {Promise<Uint8Array>} Promise resolving to the transaction result
   */
  async evaluateTransaction(
    api: string,
    args?: any[],
    transientData?: Record<string, string>,
    endorsingOrganizations?: Array<string>,
    className?: string
  ): Promise<Uint8Array> {
    return this.transaction(
      api,
      false,
      args,
      transientData,
      endorsingOrganizations,
      className
    );
  }

  /**
   * @description Closes the connection to the Fabric network
   * @summary Closes the gRPC client if it exists
   * @return {Promise<void>} Promise that resolves when the client is closed
   */
  async close(): Promise<void> {
    if (this.client) {
      this.log.verbose(`Closing ${this.config.mspId} gateway client`);
      this.client.close();
    }
  }

  /**
   * @description Gets a Contract instance from a Gateway
   * @summary Retrieves a chaincode contract from the specified network
   * @param {Gateway} gateway - The Gateway instance
   * @param {PeerConfig} config - The peer configuration
   * @return {Contrakt} The Contract instance
   */
  static getContract(
    gateway: Gateway,
    config: PeerConfig,
    contractName?: string
  ): Contrakt {
    const log = this.log.for(this.getContract);
    const network = this.getNetwork(gateway, config.channel);
    let contract: Contrakt;
    try {
      log.debug(
        `Retrieving chaincode ${config.chaincodeName} contract ${contractName || config.contractName} from network ${config.channel}`
      );
      contractName = contractName ? contractName : config.contractName;
      contract = network.getContract(config.chaincodeName, contractName);
    } catch (e: any) {
      throw this.parseError(e);
    }
    return contract;
  }

  /**
   * @description Gets a Network instance from a Gateway
   * @summary Connects to a specific channel on the Fabric network
   * @param {Gateway} gateway - The Gateway instance
   * @param {string} channelName - The name of the channel to connect to
   * @return {Network} The Network instance
   */
  static getNetwork(gateway: Gateway, channelName: string): Network {
    const log = Logging.for(this.getNetwork);
    let network: Network;
    try {
      log.debug(`Connecting to channel ${channelName}`);
      network = gateway.getNetwork(channelName);
    } catch (e: any) {
      throw this.parseError(e);
    }

    return network;
  }

  /**
   * @description Gets a Gateway instance for connecting to the Fabric network
   * @summary Creates a Gateway using the provided configuration and client
   * @param {PeerConfig} config - The peer configuration
   * @param {Client} [client] - Optional gRPC client, will be created if not provided
   * @return {Promise<Gateway>} Promise resolving to the Gateway instance
   */
  static async getGateway(config: PeerConfig, client?: Client) {
    return (await this.getConnection(
      client || (await this.getClient(config)),
      config
    )) as Gateway;
  }

  /**
   * @description Creates a gRPC client for connecting to a Fabric peer
   * @summary Initializes a client with TLS credentials for secure communication
   * @param {PeerConfig} config - The peer configuration
   * @return {Client} Promise resolving to the gRPC client
   */
  static getClient(config: PeerConfig): Client {
    const log = this.log.for(this.getClient);
    log.debug(`generating TLS credentials for msp ${config.mspId}`);
    const tlsCredentials = grpc.credentials.createSsl(
      typeof config.tlsCert === "string"
        ? Buffer.from(config.tlsCert)
        : config.tlsCert
    );
    log.debug(`generating Gateway Client for url ${config.peerEndpoint}`);
    return new Client(config.peerEndpoint, tlsCredentials);
  }

  /**
   * @description Establishes a connection to the Fabric network
   * @summary Creates a Gateway connection with identity and signer
   * @param {Client} client - The gRPC client
   * @param {PeerConfig} config - The peer configuration
   * @return {Promise<Gateway>} Promise resolving to the connected Gateway
   * @mermaid
   * sequenceDiagram
   *   participant Caller
   *   participant FabricAdapter
   *   participant Identity
   *   participant Signer
   *   participant Gateway
   *
   *   Caller->>FabricAdapter: getConnection(client, config)
   *   FabricAdapter->>Identity: getIdentity(mspId, certDirectoryPath)
   *   Identity-->>FabricAdapter: identity
   *   FabricAdapter->>Signer: getSigner(keyDirectoryPath)
   *   Signer-->>FabricAdapter: signer
   *   FabricAdapter->>FabricAdapter: Create ConnectOptions
   *   FabricAdapter->>Gateway: connect(options)
   *   Gateway-->>FabricAdapter: gateway
   *   FabricAdapter-->>Caller: gateway
   */
  static async getConnection(client: Client, config: PeerConfig) {
    const log = Logging.for(this.getConnection);
    log.debug(
      `Retrieving Peer Identity for ${config.mspId} under ${config.certCertOrDirectoryPath}`
    );
    const identity = await getIdentity(
      config.mspId,
      config.certCertOrDirectoryPath as any
    );
    log.debug(`Retrieving signer key from ${config.keyCertOrDirectoryPath}`);

    let signer: Signer,
      close = () => {};
    if (!config.hsm) {
      signer = await getSigner(config.keyCertOrDirectoryPath as any);
    } else {
      const hsm = new HSMSignerFactoryCustom(config.hsm.library);
      const identifier = hsm.getSKIFromCertificatePath(
        config.certCertOrDirectoryPath as any
      );
      const pkcs11Signer = hsm.newSigner({
        label: config.hsm.tokenLabel as string,
        pin: String(config.hsm.pin) as string,
        identifier: identifier,
        // userType: 1 /*CKU_USER */,
      });
      signer = pkcs11Signer.signer;

      close = pkcs11Signer.close;
    }

    const options = {
      client,
      identity: identity,
      signer: signer,
      // Default timeouts for different gRPC calls
      evaluateOptions: () => {
        return { deadline: Date.now() + 5000 }; // 5 seconds
      },
      endorseOptions: () => {
        return { deadline: Date.now() + 15000 }; // 15 seconds
      },
      submitOptions: () => {
        return { deadline: Date.now() + 5000 }; // 5 seconds
      },
      commitStatusOptions: () => {
        return { deadline: Date.now() + 60000 }; // 1 minute
      },
    } as ConnectOptions;

    log.debug(`Connecting to ${config.mspId}`);
    const gateway = connect(options);

    // TODO: replace?
    if (config.hsm) {
      gateway.close = new Proxy(gateway.close, {
        apply(target: () => void, thisArg: any, argArray: any[]): any {
          Reflect.apply(target, thisArg, argArray);
          close();
        },
      });
    }

    return gateway;
  }

  /**
   * @description Creates a new Dispatch instance for the Fabric client.
   * @summary This function is responsible for creating a new FabricClientDispatch instance that can be used to interact with the Fabric network.
   * @returns {Dispatch} A new Dispatch instance configured for the Fabric client.
   * @remarks The Dispatch instance is used to encapsulate the logic for interacting with the Fabric network, such as submitting transactions or querying data.
   * @example
   * const fabricDispatch = fabricClientAdapter.Dispatch();
   * fabricDispatch.submitTransaction('createProduct', { name: 'Product A', price: 100 });
   */
  override Dispatch(): FabricClientDispatch {
    return new FabricClientAdapter["_baseDispatch"]();
  }

  /**
   * @description Parses an error into a BaseError
   * @summary Converts any error into a standardized BaseError using the parent class implementation
   * @param {Error | string} err - The error to parse
   * @param {string} [reason] - Optional reason for the error
   * @return {BaseError} The parsed error
   */
  protected static override parseError<E extends BaseError>(
    err: Error | string,
    reason?: string
  ): E {
    // if (
    //   MISSING_PRIVATE_DATA_REGEX.test(
    //     typeof err === "string" ? err : err.message
    //   )
    // )
    //   return new UnauthorizedPrivateDataAccess(err) as E;
    const msg = typeof err === "string" ? err : err.message;
    if (msg.includes(NotFoundError.name)) return new NotFoundError(err) as E;
    if (msg.includes(ConflictError.name)) return new ConflictError(err) as E;
    if (msg.includes(BadRequestError.name))
      return new BadRequestError(err) as E;
    if (msg.includes(QueryError.name)) return new QueryError(err) as E;
    if (msg.includes(PagingError.name)) return new PagingError(err) as E;
    if (msg.includes(UnsupportedError.name))
      return new UnsupportedError(err) as E;
    if (msg.includes(MigrationError.name)) return new MigrationError(err) as E;
    if (msg.includes(ObserverError.name)) return new ObserverError(err) as E;
    if (msg.includes(AuthorizationError.name))
      return new AuthorizationError(err) as E;
    if (msg.includes(ForbiddenError.name)) return new ForbiddenError(err) as E;
    if (msg.includes(ConnectionError.name))
      return new ConnectionError(err) as E;
    if (msg.includes(SerializationError.name))
      return new SerializationError(err) as E;
    return new InternalError(err) as E;
  }
}

FabricClientAdapter.decoration();
Adapter.setCurrent(FabricFlavour);