Source

client/FabricClientAdapter.ts

import "../shared/overrides";
import {
  CouchDBKeys,
  type MangoQuery,
  type ViewResponse,
} from "@decaf-ts/for-couchdb";
import { Client } from "@grpc/grpc-js";
import * as grpc from "@grpc/grpc-js";
import {
  Model,
  type ModelConstructor,
  type Serializer,
} from "@decaf-ts/decorator-validation";
import { debug, final, Logging } from "@decaf-ts/logging";
import {
  type PeerConfig,
  type SegregatedModel,
  type MspDetails,
} from "../shared/types";
import {
  connect,
  type ConnectOptions,
  Gateway,
  Network,
  ProposalOptions,
  Contract as Contrakt,
  type Signer,
  GatewayError,
  EndorseError,
} from "@hyperledger/fabric-gateway";
import { Gateway as LegacyGateway, Wallets } from "fabric-network";
import type { Endorser } from "fabric-common";
import {
  getIdentity,
  getSigner,
  getFirstDirFileNameContent,
  readFile as readFsFile,
} from "./fabric-fs";
import {
  BaseError,
  InternalError,
  OperationKeys,
  SerializationError,
  BulkCrudOperationKeys,
  NotFoundError,
  ConflictError,
  BadRequestError,
  type PrimaryKeyType,
  ValidationError,
} from "@decaf-ts/db-decorators";
import {
  Context,
  Adapter,
  type AdapterFlags,
  AuthorizationError,
  ConnectionError,
  ForbiddenError,
  MigrationError,
  ObserverError,
  PagingError,
  PersistenceKeys,
  QueryError,
  Repository,
  UnsupportedError,
  Statement,
  type PreparedStatement,
  Paginator,
  MaybeContextualArg,
  ContextualArgs,
  type PreparedModel,
  AllOperationKeys,
} from "@decaf-ts/core";
import { FabricFlavour } from "../shared/constants";
import { ClientSerializer } from "../shared/ClientSerializer";
import { FabricClientDispatch } from "./FabricClientDispatch";
// import { HSMSignerFactoryCustom } from "./fabric-hsm";
import { type Constructor } from "@decaf-ts/decoration";
import { FabricClientStatement } from "./FabricClientStatement";
import { FabricClientPaginator } from "./FabricClientPaginator";
import { FabricClientRepository } from "./FabricClientRepository";
import {
  EndorsementError,
  EndorsementPolicyError,
  MvccReadConflictError,
  PhantomReadConflictError,
  TransactionTimeoutError,
} from "../shared/errors";
import { FabricClientFlags } from "./types";
import { DefaultFabricClientFlags } from "./constants";
import fs from "fs";
import { CryptoUtils } from "./crypto";
import { extractIds } from "./ids/id-extraction";
import { Identity } from "../shared/index";

type LegacyPeerTarget = {
  mspId: string;
  peerEndpoint: string;
  peerHostAlias?: string;
  tlsCert?: string | Buffer;
};

type LegacyPeerWithName = LegacyPeerTarget & { name: string };

/**
 * @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 Adapter<
  PeerConfig,
  Client,
  MangoQuery,
  Context<FabricClientFlags>
> {
  /**
   * @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(
      Object.assign({}, DefaultFabricClientFlags, config),
      FabricFlavour,
      alias
    );
  }

  override Statement<M extends Model>(
    overrides?: Partial<AdapterFlags>
  ): Statement<M, FabricClientAdapter, any, MangoQuery> {
    return new FabricClientStatement(this, overrides);
  }

  Paginator<M extends Model>(
    query: PreparedStatement<any> | MangoQuery,
    size: number,
    clazz: Constructor<M>
  ): Paginator<M, any, MangoQuery> {
    return new FabricClientPaginator(this, query, size, clazz);
  }

  protected override async flags<M extends Model>(
    operation: OperationKeys | string,
    model: Constructor<M> | Constructor<M>[] | undefined,
    flags: Partial<FabricClientFlags>,
    ...args: any[]
  ): Promise<FabricClientFlags> {
    const mergedFlags = Object.assign({}, this.config, flags);
    const f = Object.assign(
      await super.flags(operation, model, mergedFlags, ...args)
    );
    return f;
  }

  override async context<M extends Model>(
    operation: ((...args: any[]) => any) | AllOperationKeys,
    overrides: Partial<FabricClientFlags>,
    model: Constructor<M> | Constructor<M>[],
    ...args: MaybeContextualArg<Context<any>>
  ): Promise<Context<FabricClientFlags>> {
    const log = this.log.for(this.context);
    log.silly(
      `creating new context for ${operation} operation on ${model ? (Array.isArray(model) ? model.map((m) => Model.tableName(m)) : Model.tableName(model)) : "no"} table ${overrides && Object.keys(overrides) ? Object.keys(overrides).length : "no"} with flag overrides`
    );
    let ctx = args.pop();
    if (typeof ctx !== "undefined" && !(ctx instanceof Context)) {
      args.push(ctx);
      ctx = undefined;
    }

    overrides = ctx
      ? Object.assign({}, ctx.toOverrides(), overrides)
      : overrides;
    const flags = await this.flags(
      typeof operation === "string" ? operation : operation.name,
      model,
      overrides as Partial<FabricClientFlags>,
      ...[...args, ctx].filter(Boolean)
    );

    if (ctx) {
      if (!(ctx instanceof this.Context)) {
        const newCtx = new this.Context().accumulate({
          ...ctx["cache"],
          ...flags,
          parentContext: ctx,
        }) as any;
        ctx.accumulate({
          childContexts: [
            ...(ctx.getOrUndefined("childContexts") || []),
            newCtx,
          ],
        });
        return newCtx;
      }
      const currentOp = ctx.getOrUndefined("operation");
      const currentModel = ctx.getOrUndefined("affectedTables");
      if (
        !currentOp ||
        currentOp !== operation ||
        (model && model !== currentModel)
      ) {
        const newCtx = new this.Context().accumulate({
          ...ctx["cache"],
          ...flags,
          parentContext: ctx,
        }) as any;

        ctx.accumulate({
          childContexts: [
            ...(ctx.getOrUndefined("childContexts") || []),
            newCtx,
          ],
        });
        return newCtx;
      }
      return ctx.accumulate(flags) as any;
    }

    return new this.Context().accumulate({
      ...flags,
    }) as any;
  }

  /**
   * @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, Context<FabricClientFlags>>
    >,
  >(): Constructor<R> {
    return FabricClientRepository as unknown as Constructor<R>;
  }

  protected createPrefix<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: MaybeContextualArg<Context<FabricClientFlags>>
  ): [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;
    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 createAllPrefix<M extends Model>(
    clazz: Constructor<M>,
    ids: string[] | number[],
    models: Record<string, any>[],
    ...args: MaybeContextualArg<Context<FabricClientFlags>>
  ) {
    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;
      Object.assign(record, models[count]);
      return record;
    });
    return [clazz, ids, records, ...ctxArgs];
  }

  protected updateAllPrefix<M extends Model>(
    clazz: Constructor<M>,
    ids: PrimaryKeyType[],
    models: Record<string, any>[],
    ...args: MaybeContextualArg<Context<FabricClientFlags>>
  ) {
    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(() => {
      const record: Record<string, any> = {};
      record[CouchDBKeys.TABLE] = tableName;
      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<Context<FabricClientFlags>>
  ): Promise<Record<string, any>[]> {
    if (ids.length !== models.length)
      throw new InternalError("Ids and models must have the same length");
    //HERE!
    const ctxArgs = [...(args as unknown as any[])];
    const transient = ctxArgs.shift() as Record<string, any>;
    const { log, ctx } = this.logCtx(
      ctxArgs as ContextualArgs<Context<FabricClientFlags>>,
      this.createAll
    );
    const tableName = Model.tableName(clazz);

    log.info(`adding ${ids.length} entries to ${tableName} table`);
    log.verbose(`pks: ${ids}`);
    const hasTransient = transient && Object.keys(transient).length > 0;
    const needsFullPayload =
      hasTransient || this.shouldForceGatewayHydration(ctx);
    const transientPayload = hasTransient ? { [tableName]: transient } : {};

    const result = await this.submitTransaction(
      ctx,
      BulkCrudOperationKeys.CREATE_ALL,
      [
        JSON.stringify(
          models.map((m) => this.serializer.serialize(m, clazz.name))
        ),
      ],
      transientPayload as any,
      this.getEndorsingOrganizations(ctx),
      clazz.name
    );

    let res: Record<string, any>[];
    try {
      res = JSON.parse(this.decode(result)).map((r: any) => JSON.parse(r));
    } catch (e: unknown) {
      throw new SerializationError(e as Error);
    }

    if (
      this.shouldRefreshAfterWrite(
        clazz,
        ctx,
        needsFullPayload,
        res[0][Model.pk(clazz) as string] || ids[0]
      )
    ) {
      return this.readAll(
        clazz,
        extractIds(
          clazz,
          models.map((m, i) =>
            Model.merge(Object.assign({}, m as any, transient[i] || {}), res[i])
          ),
          ids
        ),
        ctx
      );
    }

    return res;
  }

  /**
   * @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<Context<FabricClientFlags>>
  ): Promise<Record<string, any>[]> {
    const { log, ctx } = 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(
      ctx,
      BulkCrudOperationKeys.READ_ALL,
      [JSON.stringify(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 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<Context<FabricClientFlags>>
  ): 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, ctx } = this.logCtx(
      ctxArgs as ContextualArgs<Context<FabricClientFlags>>,
      this.updateAll
    );
    const tableName = Model.tableName(clazz);
    log.info(`updating ${ids.length} entries to ${tableName} table`);
    log.verbose(`pks: ${ids}`);
    const hasTransient = transient && Object.keys(transient).length > 0;
    const needsFullPayload =
      hasTransient || this.shouldForceGatewayHydration(ctx);
    const transientPayload = hasTransient ? { [tableName]: transient } : {};

    const result = await this.submitTransaction(
      ctx,
      BulkCrudOperationKeys.UPDATE_ALL,
      [
        JSON.stringify(
          models.map((m) => this.serializer.serialize(m, clazz.name))
        ),
      ],
      transientPayload as any,
      this.getEndorsingOrganizations(ctx),
      clazz.name
    );

    let res: any;
    try {
      res = JSON.parse(this.decode(result)).map((r: any) => JSON.parse(r));
    } catch (e: unknown) {
      throw new SerializationError(e as Error);
    }

    if (this.shouldRefreshAfterWrite(clazz, ctx, needsFullPayload, ids[0])) {
      return this.readAll(
        clazz,
        extractIds(
          clazz,
          models.map((m, i) =>
            Model.merge(Object.assign({}, m as any, transient[i] || {}), res[i])
          ),
          ids
        ),
        ctx
      );
    }
    return res;
  }

  /**
   * @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<Context<FabricClientFlags>>
  ): Promise<Record<string, any>[]> {
    const { log, ctx, ctxArgs } = this.logCtx(args, this.deleteAll);
    const tableName = Model.tableName(clazz);

    const needsFullPayload =
      Model.isTransient(clazz) || this.shouldForceGatewayHydration(ctx);
    let result: any;
    const shouldHydrate = this.shouldRefreshAfterWrite(
      clazz,
      ctx,
      needsFullPayload,
      ids[0]
    );
    if (shouldHydrate) {
      result = await this.readAll(clazz, ids, ...ctxArgs);
    }

    log.info(`deleting ${ids.length} entries to ${tableName} table`);
    log.verbose(`pks: ${ids}`);
    const res = await this.submitTransaction(
      ctx,
      BulkCrudOperationKeys.DELETE_ALL,
      [JSON.stringify(ids)],
      undefined,
      this.getEndorsingOrganizations(ctx),
      clazz.name
    );
    try {
      return shouldHydrate
        ? result
        : JSON.parse(this.decode(res)).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<Context<FabricClientFlags>>
  ): SegregatedModel<M> & PreparedModel {
    const { log, ctx } = 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],
      });
    }

    const mirrorMeta = Model.mirroredAt(model);
    if (mirrorMeta) {
      const mirrorMsp = mirrorMeta.mspId;
      if (!mirrorMsp) throw new InternalError(`No mirror MSP could be found`);
      let msps = this.getEndorsingOrganizations(ctx) || [];
      msps = Array.isArray(msps) ? msps : [msps];
      const merged = [...new Set([...msps, mirrorMsp])];
      ctx.accumulate({
        endorsingOrgs: merged,
        endorsingOrganizations: merged,
        legacy: true,
      });
    }

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

  override revert<M extends Model>(
    obj: Record<string, any>,
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    transient?: Record<string, any>,
    ...args: ContextualArgs<Context<FabricClientFlags>>
  ): M {
    const { log, ctx } = this.logCtx(args, this.revert);
    if (
      transient &&
      this.shouldRebuildWithTransient(
        ctx,
        ctx.getOrUndefined("operation") as string | undefined
      )
    ) {
      log.verbose(
        `re-adding transient properties: ${Object.keys(transient).join(", ")}`
      );
      Object.entries(transient as Record<string, any>)
        .filter(([, v]) => typeof v !== "undefined")
        .forEach(([key, val]) => {
          if (key in obj && (obj as any)[key] !== undefined)
            log.warn(
              `overwriting existing ${key}. if this is not a default value, this may pose a problem`
            );
          (obj as M)[key as keyof M] = val;
        });
    }

    const result = new (clazz as Constructor<M>)(obj);

    return result;
  }

  private shouldRebuildWithTransient(
    ctx: Context<FabricClientFlags>,
    operation?: string
  ): boolean {
    if (!ctx) return false;
    if (ctx.getOrUndefined("rebuildWithTransient")) return true;
    const childRebuild =
      typeof (ctx as any).getFromChildren === "function"
        ? (ctx as any).getFromChildren("rebuildWithTransient")
        : undefined;
    if (childRebuild) return true;
    const resolvedOp = this.resolveOperation(ctx, operation);
    if (!resolvedOp) return false;
    const op = resolvedOp.toString().toLowerCase();
    return (
      op.includes("read") ||
      op.includes("find") ||
      op.includes("query") ||
      op.includes("statement") ||
      op.includes("page")
    );
  }

  private resolveOperation(
    ctx: Context<FabricClientFlags>,
    operation?: string
  ): string | undefined {
    if (operation) return operation;
    if (typeof (ctx as any).getFromChildren === "function") {
      return (ctx as any).getFromChildren("operation");
    }
    return undefined;
  }

  private shouldRefreshAfterWrite<M extends Model>(
    clazz: Constructor<M>,
    ctx: Context<FabricClientFlags>,
    hasTransient: boolean,
    id?: PrimaryKeyType
  ): boolean {
    if (!hasTransient) return false;
    const pk = Model.pk(clazz);
    const composed = Model.composed(clazz, pk);
    const generated = Model.generated(clazz, pk);
    const hasId = id !== undefined && id !== null;
    if (!hasId && composed) return true;
    if (!hasId && generated) {
      ctx.logger.warn(
        `Cannot refresh record with private generated primary key`
      );
      return false;
    }
    return hasId;
  }

  private getEndorsingOrganizations(
    ctx: Context<FabricClientFlags>
  ): string[] | undefined {
    const direct =
      ctx.getOrUndefined("endorsingOrgs") ||
      (ctx.getOrUndefined("endorsingOrgs") as string[] | undefined);
    if (direct && direct.length) return direct;
    return (
      (ctx.getFromChildren("endorsingOrgs") as string[] | undefined) ||
      (ctx.getFromChildren("endorsingOrgs") as string[] | undefined)
    );
  }

  private shouldForceGatewayHydration(
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    ctx: Context<FabricClientFlags>
  ): boolean {
    return !!this.config.allowGatewayOverride;
  }

  /**
   * @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>,
    transient: Record<string, any> = {},
    ...args: ContextualArgs<Context<FabricClientFlags>>
  ): Promise<Record<string, any>> {
    const ctxArgs = [...(args as unknown as any[])];
    const { log, ctx } = this.logCtx(
      ctxArgs as ContextualArgs<Context<FabricClientFlags>>,
      this.create
    );
    const tableName = Model.tableName(clazz);
    log.verbose(`adding entry to ${tableName} table`);
    log.debug(`pk: ${id}`);
    const hasTransient = transient && Object.keys(transient).length > 0;
    const needsFullPayload =
      hasTransient || this.shouldForceGatewayHydration(ctx);
    const transientPayload = hasTransient ? { [tableName]: transient } : {};
    const result = await this.submitTransaction(
      ctx,
      OperationKeys.CREATE,
      [this.serializer.serialize(model, clazz.name)],
      transientPayload as any,
      this.getEndorsingOrganizations(ctx),
      clazz.name
    );
    const deserialized = this.serializer.deserialize(this.decode(result));
    if (this.shouldRefreshAfterWrite(clazz, ctx, needsFullPayload, id)) {
      return this.read(
        clazz,
        extractIds(
          clazz,
          Model.merge(
            Object.assign({}, model, transient || {}),
            deserialized,
            clazz
          ),
          id
        ),
        ctx
      );
    }
    return deserialized;
  }

  @debug()
  @final()
  async healthcheck<M extends Model>(
    clazz: Constructor<M>,
    ...args: ContextualArgs<Context<FabricClientFlags>>
  ): Promise<Record<string, any>> {
    const { log, ctx } = this.logCtx(args, this.healthcheck);
    const tableName = Model.tableName(clazz);

    log.verbose(`reading entry from ${tableName} table`);
    const result = await this.evaluateTransaction(
      ctx,
      "healthcheck",
      [],
      undefined,
      undefined,
      clazz.name
    );
    return JSON.parse(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<Context<FabricClientFlags>>
  ): Promise<Record<string, any>> {
    const { log, ctx } = this.logCtx(args, this.read);
    const tableName = Model.tableName(clazz);

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

  updatePrefix<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: MaybeContextualArg<Context<FabricClientFlags>>
  ) {
    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>,
    transient: Record<string, any> = {},
    ...args: ContextualArgs<Context<FabricClientFlags>>
  ): Promise<Record<string, any>> {
    const ctxArgs = [...(args as unknown as any[])];
    const { log, ctx } = this.logCtx(
      ctxArgs as ContextualArgs<Context<FabricClientFlags>>,
      this.update
    );
    log.info(`CLIENT UPDATE class : ${typeof clazz}`);
    const tableName = Model.tableName(clazz);
    log.verbose(`updating entry to ${tableName} table`);
    log.debug(`pk: ${id}`);
    const hasTransient = transient && Object.keys(transient).length > 0;
    const needsFullPayload =
      hasTransient || this.shouldForceGatewayHydration(ctx);
    const transientPayload = hasTransient ? { [tableName]: transient } : {};
    const result = await this.submitTransaction(
      ctx,
      OperationKeys.UPDATE,
      [this.serializer.serialize(model, clazz.name || clazz)], // TODO should be receving class but is receiving string
      transientPayload as any,
      this.getEndorsingOrganizations(ctx),
      clazz.name
    );
    const deserialized = this.serializer.deserialize(this.decode(result));
    if (this.shouldRefreshAfterWrite(clazz, ctx, needsFullPayload, id)) {
      return this.read(
        clazz,
        extractIds(
          clazz,
          Model.merge(
            Object.assign({}, model, transient || {}),
            deserialized,
            clazz
          ),
          id
        ),
        ctx
      );
    }
    return deserialized;
  }

  /**
   * @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<Context<FabricClientFlags>>
  ): Promise<Record<string, any>> {
    const { log, ctx } = this.logCtx(args, this.delete);
    const tableName = Model.tableName(clazz);
    const needsFullPayload =
      Model.isTransient(clazz) || this.shouldForceGatewayHydration(ctx);
    let result: any;
    const shouldHydrate = this.shouldRefreshAfterWrite(
      clazz,
      ctx,
      needsFullPayload,
      id
    );
    if (shouldHydrate) {
      result = await this.read(clazz, id, ctx);
    }

    log.verbose(`deleting entry from ${tableName} table`);
    log.debug(`pk: ${id}`);
    const res = await this.submitTransaction(
      ctx,
      OperationKeys.DELETE,
      [id.toString()],
      undefined,
      this.getEndorsingOrganizations(ctx),
      clazz.name
    );
    return shouldHydrate
      ? result
      : this.serializer.deserialize(this.decode(res));
  }

  /**
   * @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()
  async raw<V, D extends boolean>(
    rawInput: MangoQuery,
    docsOnly: D = true as D,
    clazz: ModelConstructor<any>,
    ...args: ContextualArgs<Context<FabricClientFlags>>
  ): Promise<V> {
    const { log, ctx } = this.logCtx(args, this.raw);
    const tableName = clazz.name;
    log.info(`Performing raw statement on table ${Model.tableName(clazz)}`);
    let transactionResult: any;
    try {
      transactionResult = await this.evaluateTransaction(
        ctx,
        "raw",
        [JSON.stringify(rawInput), docsOnly],
        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 Executes a CouchDB view query against the Fabric chaincode
   * @summary Evaluates a transaction to query a design document view
   * @template R - The view response type
   * @param {string} ddoc - Design document name
   * @param {string} viewName - View name
   * @param {Record<string, any>} options - View query options
   * @param {...ContextualArgs<Context<FabricClientFlags>>} args - Optional contextual arguments
   * @return {Promise<ViewResponse<R>>} The view response
   */
  @debug()
  async view<R>(
    ddoc: string,
    viewName: string,
    options: Record<string, any>,
    ...args: ContextualArgs<Context<FabricClientFlags>>
  ): Promise<ViewResponse<R>> {
    const { log, ctx } = this.logCtx(args, this.view);
    log.info(`Querying view ${ddoc}/${viewName}`);
    let transactionResult: any;
    try {
      transactionResult = await this.evaluateTransaction(
        ctx,
        "view",
        [ddoc, viewName, JSON.stringify(options)],
        undefined,
        undefined,
        undefined
      );
    } catch (e: unknown) {
      throw this.parseError(e as Error);
    }
    let result: ViewResponse<R>;
    try {
      result = JSON.parse(this.decode(transactionResult));
    } catch (e: any) {
      throw new SerializationError(`Failed to process view result: ${e}`);
    }
    return result;
  }

  /**
   * @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(ctx: Context<FabricClientFlags>): Promise<Gateway> {
    return FabricClientAdapter.getGateway(ctx, 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(
    ctx: Context<FabricClientFlags>,
    contractName?: string
  ): Promise<Contrakt> {
    return FabricClientAdapter.getContract(
      await this.Gateway(ctx),
      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(
    ctx: Context<FabricClientFlags>,
    api: string,
    submit = true,
    args?: any[],
    transientData: Record<string, string> = {},
    endorsingOrganizations?: string[],
    className?: string
  ): Promise<Uint8Array> {
    const log = this.log.for(this.transaction);
    const gateway = await this.Gateway(ctx);
    try {
      const contract = await this.Contract(
        ctx,
        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: Object.entries(transientData).reduce(
          (acc, [key, val]) => {
            acc[key] = JSON.stringify(val);
            return acc;
          },
          {} as typeof transientData
        ),
        endorsingOrganizations: ctx.getOrUndefined("allowManualEndorsingOrgs")
          ? endorsingOrganizations || undefined
          : undefined, // mspId list
      };

      return await method.call(contract, api, proposalOptions);
    } catch (e: any) {
      throw this.parseError(e);
    } finally {
      this.log.debug(`Closing ${this.config.mspId} gateway connection`);
      gateway.close();
    }
  }

  private shouldUseLegacyGateway(ctx: Context<FabricClientFlags>): boolean {
    return !!ctx.getOrUndefined("legacy") && !!this.config.allowGatewayOverride;
  }

  private prepareLegacyArgs(args?: any[]): string[] {
    return (args || []).map((arg) =>
      typeof arg === "string" ? arg : JSON.stringify(arg)
    );
  }

  private buildLegacyTransient(
    transientData?: Record<string, string>
  ): Record<string, Buffer> | undefined {
    if (!transientData) return undefined;
    const entries = Object.entries(transientData);
    if (!entries.length) return undefined;
    const map: Record<string, Buffer> = {};
    for (const [key, value] of entries) {
      map[key] = Buffer.from(JSON.stringify(value));
    }
    return map;
  }

  private resolveLegacyMspCount(): number {
    const configured = this.config.legacyMspCount ?? 1;
    const parsed = Number(configured);
    if (!Number.isFinite(parsed) || parsed < 1) return 1;
    return Math.floor(parsed);
  }

  private pickLegacyCandidates(
    candidates: MspDetails[],
    limit: number
  ): MspDetails[] {
    if (!candidates.length || limit <= 0) return [];
    const available = [...candidates];
    const target = Math.min(limit, available.length);
    const picked: MspDetails[] = [];
    while (picked.length < target && available.length) {
      const idx = Math.floor(Math.random() * available.length);
      const [selection] = available.splice(idx, 1);
      if (selection) {
        picked.push(selection);
      }
    }
    return picked;
  }

  private buildLegacyPeerConfigs(
    ctx: Context<FabricClientFlags>
  ): LegacyPeerTarget[] {
    const peers: LegacyPeerTarget[] = [
      {
        mspId: this.config.mspId,
        peerEndpoint: this.config.peerEndpoint,
        peerHostAlias: this.config.peerHostAlias,
        tlsCert: this.config.tlsCert,
      },
    ];

    let endorsingOrgs = this.getEndorsingOrganizations(ctx) || [];
    endorsingOrgs = Array.isArray(endorsingOrgs)
      ? endorsingOrgs
      : [endorsingOrgs];
    const endorsers =
      endorsingOrgs.filter((org): org is string => Boolean(org)) || [];
    const extras = endorsers.filter((org) => org !== this.config.mspId);
    if (!extras.length) return peers;

    const map = this.config.mspMap;
    const legacyCount = this.resolveLegacyMspCount();
    for (const msp of extras) {
      const candidates = map?.[msp];
      if (!candidates?.length) {
        throw new UnsupportedError(
          `No peer mapping available for MSP ${msp}. Provide it via config.mspMap`
        );
      }
      const selections = this.pickLegacyCandidates(candidates, legacyCount);
      if (!selections.length) {
        throw new UnsupportedError(
          `No valid peer mapping available for MSP ${msp}. Provide it via config.mspMap`
        );
      }
      for (const choice of selections) {
        if (!choice.endpoint) {
          throw new UnsupportedError(
            `Invalid peer mapping for MSP ${msp}: missing endpoint`
          );
        }
        peers.push({
          mspId: msp,
          peerEndpoint: choice.endpoint,
          peerHostAlias: choice.alias,
          tlsCert: choice.tlsCert || this.config.tlsCert,
        });
      }
    }

    return peers;
  }

  private async submitLegacyWithExplicitEndorsers(
    ctx: Context<FabricClientFlags>,
    fcn: string,
    args: string[],
    transientMap: Record<string, Buffer> | undefined,
    peerConfigs: LegacyPeerTarget[],
    className?: string
  ): Promise<Uint8Array> {
    const log = this.log.for(this.submitLegacyWithExplicitEndorsers);
    const peers = this.normalizeLegacyPeers(peerConfigs);
    const identityMaterial = await this.resolveLegacyIdentityMaterial();
    const wallet = await Wallets.newInMemoryWallet();
    const identityLabel = `${this.config.mspId}-legacy`;
    await wallet.put(identityLabel, {
      credentials: {
        certificate: identityMaterial.certificate,
        privateKey: identityMaterial.privateKey,
      },
      mspId: this.config.mspId,
      type: "X.509",
    } as Identity);

    const connectionProfile = await this.buildLegacyConnectionProfile(peers);
    const gateway = new LegacyGateway();

    try {
      await gateway.connect(connectionProfile, {
        identity: identityLabel,
        wallet,
        discovery: {
          enabled: true,
          asLocalhost: this.shouldTreatPeersAsLocalhost(peers),
        },
        tlsInfo: {
          certificate: identityMaterial.certificate,
          key: identityMaterial.privateKey,
        },
      });

      const network = await gateway.getNetwork(this.config.channel);
      const contract = network.getContract(
        this.config.chaincodeName,
        this.getContractName(className)
      );
      const transaction = contract.createTransaction(fcn);

      if (transientMap) {
        transaction.setTransient(transientMap);
      }

      const endorsers = peers
        .map((peer) => network.getChannel().getEndorser(peer.name))
        .filter((endorser): endorser is Endorser => Boolean(endorser));
      if (endorsers.length) {
        transaction.setEndorsingPeers(endorsers);
      }

      log.verbose(
        `Legacy submitting ${this.getContractName(className) || this.config.contractName}.${fcn} via peers ${peers.map((p) => p.peerEndpoint).join(", ")}`
      );

      const result = await transaction.submit(...args);
      return Uint8Array.from(result);
    } catch (e: any) {
      throw this.parseError(e);
    } finally {
      gateway.disconnect();
    }
  }

  private normalizeLegacyPeers(
    peers: LegacyPeerTarget[]
  ): LegacyPeerWithName[] {
    const deduped = new Map<string, LegacyPeerWithName>();
    const addPeer = (peer: LegacyPeerTarget) => {
      const key = `${peer.peerEndpoint}|${peer.peerHostAlias || ""}`;
      if (deduped.has(key)) return;
      const name = `peer-${peer.mspId}-${deduped.size}`;
      deduped.set(key, {
        ...peer,
        name,
      });
    };

    peers.forEach(addPeer);
    addPeer({
      mspId: this.config.mspId,
      peerEndpoint: this.config.peerEndpoint,
      peerHostAlias: this.config.peerHostAlias,
    });

    return Array.from(deduped.values());
  }

  private async resolveLegacyIdentityMaterial(): Promise<{
    certificate: string;
    privateKey: string;
  }> {
    const certificate = await this.readPemInput(
      this.config.certCertOrDirectoryPath
    );
    const privateKey = await this.readPemInput(
      this.config.keyCertOrDirectoryPath
    );
    return { certificate, privateKey };
  }

  private async buildLegacyConnectionProfile(peers: LegacyPeerWithName[]) {
    const peerEntries: Record<string, any> = {};
    const channelPeers: Record<string, any> = {};
    const orgs: Record<string, any> = {};
    for (const peer of peers) {
      const tlsPem = await this.readPemInput(
        peer.tlsCert || this.config.tlsCert
      );
      const hostname =
        peer.peerHostAlias || this.extractHost(peer.peerEndpoint);
      peerEntries[peer.name] = {
        url: this.ensureGrpcUrl(peer.peerEndpoint),
        tlsCACerts: { pem: tlsPem },
        grpcOptions: {
          "ssl-target-name-override": hostname,
          hostnameOverride: hostname,
        },
      };
      channelPeers[peer.name] = {
        endorsingPeer: true,
        chaincodeQuery: true,
        ledgerQuery: true,
        eventSource: true,
      };
      orgs[peer.mspId] = orgs[peer.mspId] || {
        mspid: peer.mspId,
        peers: [],
      };
      orgs[peer.mspId].peers.push(peer.name);
    }

    return {
      name: "legacy-manual",
      version: "1.0.0",
      client: {
        organization: this.config.mspId,
      },
      organizations: orgs,
      peers: peerEntries,
      orderers: {},
      channels: {
        [this.config.channel]: {
          peers: channelPeers,
        },
      },
    };
  }

  private shouldTreatPeersAsLocalhost(peers: LegacyPeerWithName[]): boolean {
    return peers.every((peer) => this.isLocalEndpoint(peer.peerEndpoint));
  }

  private isLocalEndpoint(endpoint: string): boolean {
    const host = this.extractHost(endpoint).toLowerCase();
    return host === "localhost" || host === "127.0.0.1";
  }

  private extractHost(endpoint: string): string {
    const sanitized = endpoint.replace(/^grpcs?:\/\//, "");
    return sanitized.split(":")[0];
  }

  private ensureGrpcUrl(endpoint: string): string {
    if (/^grpcs?:\/\//i.test(endpoint)) return endpoint;
    return `grpcs://${endpoint}`;
  }

  private async readPemInput(source?: string | Buffer): Promise<string> {
    if (!source) throw new InternalError("Missing certificate or key material");
    if (Buffer.isBuffer(source)) return source.toString("utf8");
    const trimmed = source.trim();
    if (/-----BEGIN [A-Z ]+-----/.test(trimmed)) return trimmed;
    const stats = await fs.promises.stat(source).catch(() => undefined);
    if (stats?.isDirectory()) {
      return await getFirstDirFileNameContent(source);
    }
    return (await readFsFile(source)).toString();
  }

  /**
   * @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): E {
    return FabricClientAdapter.parseError<E>(err);
  }

  /**
   * @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(
    ctx: Context<FabricClientFlags>,
    api: string,
    args?: any[],
    transientData?: Record<string, string>,
    endorsingOrganizations?: Array<string>,
    className?: string
  ): Promise<Uint8Array> {
    if (this.shouldUseLegacyGateway(ctx)) {
      const legacyArgs = this.prepareLegacyArgs(args);
      const transientMap = this.buildLegacyTransient(transientData);
      const peerConfigs = this.buildLegacyPeerConfigs(ctx);
      return this.submitLegacyWithExplicitEndorsers(
        ctx,
        api,
        legacyArgs,
        transientMap,
        peerConfigs,
        className
      );
    }
    return this.transaction(
      ctx,
      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(
    ctx: Context<FabricClientFlags>,
    api: string,
    args?: any[],
    transientData?: Record<string, string>,
    endorsingOrganizations?: Array<string>,
    className?: string
  ): Promise<Uint8Array> {
    return this.transaction(
      ctx,
      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(
    ctx: Context<FabricClientFlags>,
    config: PeerConfig,
    client?: Client
  ) {
    return (await this.getConnection(
      client || (await this.getClient(config)),
      config,
      ctx
    )) 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}`);
    let pathOrCert: string | Buffer = config.tlsCert as string | Buffer;

    if (typeof pathOrCert === "string") {
      if (
        pathOrCert.match(
          /-----BEGIN (CERTIFICATE|KEY|PRIVATE KEY)-----.+?-----END \1-----$/gms
        )
      ) {
        pathOrCert = Buffer.from(pathOrCert, "utf8");
      } else {
        try {
          pathOrCert = Buffer.from(fs.readFileSync(pathOrCert, "utf8"));
        } catch (e: unknown) {
          throw new InternalError(
            `Failed to read the tls certificate from ${pathOrCert}: ${e}`
          );
        }
      }
    }

    const tlsCredentials = grpc.credentials.createSsl(pathOrCert);
    log.debug(`generating Gateway Client for url ${config.peerEndpoint}`);
    return new Client(config.peerEndpoint, tlsCredentials, {
      "grpc.max_receive_message_length": (config.sizeLimit || 15) * 1024 * 1024,
      "grpc.max_send_message_length": (config.sizeLimit || 15) * 1024 * 1024,
    });
  }

  /**
   * @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,
    ctx: Context<FabricClientFlags>
  ) {
    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
    );
    try {
      log.debug(
        `preparing transaction signer for ${CryptoUtils.fabricIdFromCertificate(identity.credentials.toString())}`
      );
    } catch (e: unknown) {
      log.error(`Failed to extract Fabric ID from certificate`, e as Error);
    }

    let signer: Signer;
    const 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;
      throw new UnsupportedError("HSM NOT IMPLEMENTED");
    }

    const options = {
      client,
      identity: identity,
      signer: signer,
      // Default timeouts for different gRPC calls
      evaluateOptions: () => {
        return { deadline: Date.now() + 1000 * ctx.get("evaluateTimeout") }; // defaults to 5 seconds
      },
      endorseOptions: () => {
        return { deadline: Date.now() + 1000 * ctx.get("endorseTimeout") }; // defaults to 15 seconds
      },
      submitOptions: () => {
        return { deadline: Date.now() + 1000 * ctx.get("submitTimeout") }; // defaults to 5 seconds
      },
      commitStatusOptions: () => {
        return { deadline: Date.now() + 1000 * ctx.get("commitTimeout") }; // defaults to 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 parseError<E extends BaseError>(
    err: Error | string | GatewayError
  ): E {
    // if (
    //   MISSING_PRIVATE_DATA_REGEX.test(
    //     typeof err === "string" ? err : err.message
    //   )
    // )
    //   return new UnauthorizedPrivateDataAccess(err) as E;

    let msg = typeof err === "string" ? err : err.message;
    if (err instanceof GatewayError && err.details.length && err.code === 10) {
      msg = `${err.details[0].message}`;
    }

    if (
      err instanceof EndorseError &&
      err.details.length &&
      err.code === 10 &&
      err.details[0].message?.includes(UnsupportedError.name)
    ) {
      msg = `${err.details[0].message}`;
    }

    if (msg.includes("MVCC_READ_CONFLICT"))
      return new MvccReadConflictError(err) as E;

    if (msg.includes("DEADLINE_EXCEEDED"))
      return new TransactionTimeoutError(err) as E;

    if (msg.includes("ENDORSEMENT_POLICY_FAILURE"))
      return new EndorsementPolicyError(err) as E;

    if (msg.includes("PHANTOM_READ_CONFLICT"))
      return new PhantomReadConflictError(err) as E;

    if (err instanceof Error && (err as any).code) {
      switch ((err as any).code) {
        case 9:
          return new EndorsementError(err) as E;
      }
    }

    if (msg.includes(ValidationError.name))
      return new ValidationError(err) as E;
    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);