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);
Source