@hlf-core/transport
v3.2.11
Published
TypeScript library for Hyperledger Fabric client transport: command sending, block and event parsing, batch delivery and chaincode error handling
Maintainers
Readme
English · Русский
@hlf-core/transport
TypeScript client transport library for Hyperledger Fabric: sending commands, parsing blocks, and receiving events
Full documentation: https://hlf.gnosisai.ru/docs/packages/transport
The client side of the transport: connecting to the network, sending commands to chaincode, receiving responses, and parsing blocks and events. It implements the @ts-core/common command model on top of fabric-network, so application code works with the transport the same way regardless of whether it talks to a blockchain or to an ordinary service.
The paired package on the chaincode side is @hlf-core/transport-chaincode; the shared protocol is @hlf-core/transport-common.
Table of Contents
- Overview
- Installation
- Quick Start
- How It Works
- API Documentation
- Usage Examples
- Important Notes
- Project Structure
- Related Packages
Overview
The transport extends TransportImpl from @ts-core/common, meaning it provides the familiar send and sendListen methods for commands. The difference from an ordinary transport is that delivery goes through Hyperledger Fabric: a command is packed into an envelope, sent via a single contract method, and — depending on the isReadonly flag — executed either as a read-only query or as a transaction with endorsement.
The reverse direction works differently. Chaincode cannot "respond" outside of a transaction, so events arrive not through the transport but from blocks: the transport subscribes to the network's blocks and parses their contents.
Key Features
- Sending commands — with automatic selection between a query and a transaction based on the
isReadonlyflag - Connection with reconnection — configurable delay and number of attempts
- Chaincode error parsing — the domain error is extracted from the Fabric response and restored as an
ExtendedErrorwith its code and details - Block parsing — transactions, events, validation codes, and the link between events and the transactions that produced them
- Batch mode — commands are accumulated by the chaincode and executed in a single transaction, with responses parsed from the block
- Transport factory — reuse of connections by identifier
Installation
npm install @hlf-core/transportDependencies
{
"@hlf-core/api": "~3.1.4",
"@hlf-core/transport-common": "~3.0.15"
}@ts-core/common, fabric-network, and fabric-common are pulled in transitively (through @hlf-core/api).
Quick Start
import { TransportFabric } from '@hlf-core/transport';
let transport = new TransportFabric(logger, {
fabricIdentity: 'user',
fabricNetworkName: 'mychannel',
fabricChaincodeName: 'mychaincode',
fabricConnectionSettingsPath: './connection.json',
fabricIdentityPrivateKey: privateKey,
fabricIdentityCertificate: certificate,
reconnectDelay: 1000,
reconnectMaxAttempts: 5,
isExitApplicationOnDisconnect: false
});
await transport.connect();
// command with a reply
let user = await transport.sendListen(new UserGetCommand(uid));
// command without waiting for a reply
transport.send(new UserAddCommand({ name: 'Ivan', publicKey }));How It Works
Connection and Reconnection
connect() applies default values and starts the connection loop:
if (!_.isNumber(this.settings.reconnectDelay)) { this.settings.reconnectDelay = DateUtil.MILISECONDS_SECOND; }
if (!_.isNumber(this.settings.reconnectMaxAttempts)) { this.settings.reconnectMaxAttempts = 0; }
if (!_.isBoolean(this.settings.isExitApplicationOnDisconnect)) { this.settings.isExitApplicationOnDisconnect = true; }Attempts are repeated with a reconnectDelay delay until their count exceeds reconnectMaxAttempts. By default there are no retries: reconnectMaxAttempts is zero, so the very first failure leads to a disconnect.
On a successful connection the transport subscribes to the network's blocks and to the contract's events, after which the connect() promise resolves.
Important:
isExitApplicationOnDisconnectis enabled by default, and on a disconnect the transport terminates the process viaprocess.exit(1). This is convenient for services managed by an orchestrator that will restart the container, but unexpected in other scenarios. See the Important Notes section for details.
Sending a Command
protected async commandRequestExecute(command, options, isNeedReply) {
let payload = this.createRequestPayload(command, options, isNeedReply);
TransportFabricRequestPayload.clear(payload);
let response = await this.transactionSend(this.api.contract.createTransaction(TRANSPORT_FABRIC_METHOD), command, payload);
if (isNeedReply && this.isCommandAsync(command)) {
this.responseMessageReceived(command.id, response);
}
}The steps: assemble the envelope, strip default values out of it, send it via the single contract method, and parse the response.
The sending method is chosen based on the command's flag:
let method = payload.isReadonly ? transaction.evaluate : transaction.submit;evaluate executes the command on a peer without creating a transaction — nothing ends up in the ledger. submit goes through the full endorsement and commit cycle.
The flag is taken from the command itself, and both names are supported — the historical isQuery and the current isReadonly:
protected isCommandReadonly<U>(command: ITransportCommand<U>): boolean {
if (ObjectUtil.hasOwnProperty(command, 'isQuery')) { return command['isQuery'] === true; }
if (ObjectUtil.hasOwnProperty(command, 'isReadonly')) { return command['isReadonly'] === true; }
return false;
}Chaincode Errors
Fabric returns a chaincode error wrapped in its own structure and prefixed with text. The transport parses it and restores the original domain error:
protected parseChaincodeError(error: any): ExtendedError {
let message = error.message.replace('error in simulation: transaction returned with failure:', '').trim();
if (!ObjectUtil.isJSON(message)) { return null; }
let response = TransformUtil.toClass(TransportFabricResponsePayload, TransformUtil.toJSON(message));
if (!ExtendedError.instanceOf(response.response)) { return null; }
let item = ExtendedError.create(response.response);
item.stack = null;
return item;
}Thanks to this, application code catches the same error with the same code that was thrown inside the chaincode, rather than a string of transaction text. The stack is nulled out — it would refer to the client and would be misleading.
The error source is searched for in sequence in response, responses[0].response, endorsements[0], and in the error itself when the status is 500 — the shape of the response depends on the stage at which Fabric aborted execution.
Blocks and Events
The transport is one-way: commands flow only from the client to the chaincode. An attempt to send an event or a response in the reverse direction is explicitly forbidden:
protected eventRequestExecute<U>(event: ITransportEvent<U>, options?: void): Promise<void> {
throw new ExtendedError(`Method doesn't supported`);
}Events arrive from blocks. TransportFabricBlockParser parses a block into a structure of transactions and events, filling in validation codes from the block's metadata along the way and linking events to transactions by hash:
public static checkEventsTransaction(events, transactions): void {
let map = new Map();
transactions.forEach(item => map.set(item.hash, item));
for (let event of events) {
let item = map.get(event.transactionHash);
event.requestId = item.requestId;
event.transactionValidationCode = item.validationCode;
}
}This makes it possible to learn from an event which command produced it (requestId) and whether the transaction was accepted by the network (transactionValidationCode) — the event is present in the block even for a rejected transaction.
Transport events are distinguished from the rest by name: everything published under transportFabricEvent is parsed as a "transaction hash → list of events" map, and everything else as a single chaincode event.
Empty hooks are provided for subclasses:
protected async blockEventCallback(event: IFabricBlock): Promise<void> { }
protected async contractEventCallback(event: ContractEvent): Promise<void> { }Batch Mode
TransportFabricBatch changes the delivery scheme for state-changing commands. The command is sent, but the response is not awaited immediately: the chaincode accumulates such commands and executes them in a single transaction. The responses come back later — from the block.
protected async commandRequestExecute(command, options, isNeedReply) {
if (this.isCommandReadonly(command)) {
return super.commandRequestExecute(command, options, isNeedReply); // readonly — as usual
}
let payload = this.createRequestPayload(command, options, isNeedReply);
TransportFabricRequestPayload.clear(payload);
return this.transactionSend(...); // the reply will come from the block
}When a block arrives, the transport looks in it for a successful batch transaction and parses its response as a "hash → response" map, after which it resolves the promises of the waiting commands:
let payload = TransformUtil.toClass(TransportFabricResponsePayload, batch.response);
for (let hash in payload.response) {
let item = TransformUtil.toClass(TransportFabricResponsePayload, payload.response[hash]);
this.responseMessageReceived(item.id, TransformUtil.fromClassBuffer(item));
}Read commands do not end up in a batch — they are still executed immediately via evaluate.
API Documentation
TransportFabric
class TransportFabric<T extends ITransportFabricConnectionSettings> extends TransportImpl<T, ITransportFabricCommandOptions>| Method | Purpose |
|---|---|
| connect(): Promise<void> | connect to the network with reconnection; a repeated call returns the same promise |
| disconnect(error?): void | unsubscribe from blocks and events, tear down the connection |
| destroy(): void | destroy the transport; internally calls disconnect() |
| isConnected: boolean | connection state |
| api: FabricApiClient | access to the Fabric client for direct operations |
Commands are sent via the inherited send and sendListen from TransportImpl.
Methods to override in subclasses:
| Method | Purpose |
|---|---|
| blockEventCallback(block) | called for every network block |
| contractEventCallback(event) | called for every contract event |
| isCommandReadonly(command) | determines the sending method |
| parseError(error) / parseChaincodeError(error) | parse Fabric errors |
ITransportFabricConnectionSettings
export interface ITransportFabricConnectionSettings extends IFabricConnectionSettings, ITransportSettings {
reconnectDelay?: number;
reconnectMaxAttempts?: number;
isExitApplicationOnDisconnect?: boolean;
}| Parameter | Default | Purpose |
|---|---|---|
| reconnectDelay | 1000 ms | delay between connection attempts |
| reconnectMaxAttempts | 0 | maximum number of retries; zero means no retries |
| isExitApplicationOnDisconnect | true | terminate the process on a disconnect |
The remaining connection parameters (identity, channel, chaincode name, certificates) are inherited from IFabricConnectionSettings of the @hlf-core/api package.
TransportFabricBlockParser
class TransportFabricBlockParser<U extends ITransportFabricTransaction, V extends ITransportFabricEvent, T extends ITransportFabricBlock<U, V>>| Method | Purpose |
|---|---|
| parse(block): Promise<T> | parse a block into transactions and events |
| parseTransaction(data): U | parse a single transaction with its validation code |
| checkEventsTransaction(events, transactions) | link events to transactions by hash |
| isTransactionValid(item) | the validation code equals VALID |
| isTransactionError(item) | the transaction's response is an error |
| isTransactionSucceed(item) | the transaction is valid and contains no error |
Only transport-related content is parsed: a transaction is taken into account if the transportFabricExecute method was called with exactly two arguments.
The structure of a parsed block:
export interface ITransportFabricBlock<U, V> {
hash: string;
date: Date;
number: number;
events: Array<V>;
transactions: Array<U>;
}
export interface ITransportFabricTransaction<U = any, V = any> {
hash: string;
date: Date;
channel: string;
requestId: string;
chaincode: ITransportFabricTransactionChaincode;
validationCode: FabricTransactionValidationCode;
request: ITransportFabricRequestPayload<U>;
response: ITransportFabricResponsePayload<V>;
}An event carries a reference to the transaction that produced it:
export interface ITransportFabricEvent<T = any> extends ITransportEvent<T> {
date: Date;
channel: string;
requestId: string;
chaincode: string;
transactionHash: string;
transactionValidationCode: FabricTransactionValidationCode;
}TransportFabricBatch
class TransportFabricBatch<T extends ITransportFabricConnectionSettings> extends TransportFabric<T>A subclass that delivers state-changing commands in batches. The sending interface does not change — application code still uses send and sendListen, but the response to a state-changing command arrives after the chaincode has executed the batch and it has made it into a block.
TransportFabricBlockParserBatch
class TransportFabricBlockParserBatch extends TransportFabricBlockParser<ITransportFabricTransactionBatch, ITransportFabricEvent, ITransportFabricBlockBatch>Parses a block containing a batch transaction. In addition to the ordinary parsing, it unpacks the batch: for each hash in the response it loads the original transaction and its block via qscc to reconstruct the full picture.
export interface ITransportFabricTransactionBatch<U = any, V = any> extends ITransportFabricTransaction<U, V> {
blockReceived: number;
}The blockReceived field is the number of the block in which the command was accepted, as opposed to the block in which it was executed as part of the batch.
| Method | Purpose |
|---|---|
| getBatchTransaction(block) | find the batch transaction in a block |
| getSucceedBatchTransaction(block) | find a successful batch transaction |
Factories
class TransportFabricFactory extends LoggerWrapper {
public async get(uid: string): Promise<TransportFabric>;
}Stores transports by identifier and reuses them: if a transport has already been created but is not connected, get will connect it. Settings are taken from TransportFabricConnectionSettingsFactory, a subclass of the settings factory from @hlf-core/api.
Useful when an application works with several channels or identities at the same time.
Usage Examples
Sending a Command with a Response
let transport = new TransportFabric(logger, settings);
await transport.connect();
let user = await transport.sendListen(new UserGetCommand(uid));Signing a Command
await transport.sendListen(new UserAddCommand(dto), {
userId: user.uid,
signature: { value, algorithm, publicKey, nonce }
});The command options are extended with the userId and signature fields — the chaincode will verify the signature and the nonce before execution. The current nonce value is requested with the NonceGetCommand command from @hlf-core/transport-common.
Processing Blocks
class MyTransport extends TransportFabric {
protected async blockEventCallback(block: IFabricBlock): Promise<void> {
let parser = new TransportFabricBlockParser();
let item = await parser.parse(block);
for (let transaction of item.transactions) {
if (!TransportFabricBlockParser.isTransactionSucceed(transaction)) {
continue;
}
console.log(transaction.request.name, transaction.requestId);
}
for (let event of item.events) {
console.log(event.name, event.requestId, event.transactionValidationCode);
}
}
}Multiple Connections
let factory = new TransportFabricFactory(logger, settingsFactory);
let first = await factory.get('channel-one');
let second = await factory.get('channel-two');Important Notes
Process termination on disconnect. isExitApplicationOnDisconnect is enabled by default, so disconnect() calls process.exit(1). Note that destroy() internally calls disconnect() — destroying the transport with this setting enabled will terminate the application. If this behavior is undesirable, set the parameter explicitly:
new TransportFabric(logger, { ...settings, isExitApplicationOnDisconnect: false });Connection retries are disabled by default. reconnectMaxAttempts is zero, which means the very first failed attempt leads to a disconnect — and, with the setting above enabled, to process termination as well. For services that start at the same time as the network, the value is worth setting explicitly.
The first block after connecting is ignored in batch mode. TransportFabricBatch skips the first block that arrives so as not to reprocess what was already processed before reconnection. If that block contained responses to pending commands, they will not be delivered and the commands will time out.
The transport is one-way. eventRequestExecute and commandResponseExecute throw an exception: an event or a response cannot be sent toward the chaincode. The reverse direction is only through blocks.
Command options are modified when sending. getCommandOptions calls clearDefaultOptions on the passed options object, i.e. strips it of default values. Do not reuse the same options object for several commands expecting it to remain unchanged.
Project Structure
src/
├── TransportFabric.ts client-side transport
├── ITransportFabricConnectionSettings.ts connection settings
├── public-api.ts package public API
├── block/
│ ├── TransportFabricBlockParser.ts parsing of blocks, transactions and events
│ ├── ITransportFabricBlock.ts structure of a parsed block
│ ├── ITransportFabricTransaction.ts transaction structure
│ ├── ITransportFabricTransactionChaincode.ts chaincode transaction data
│ └── ITransportFabricEvent.ts event structure
├── batch/
│ ├── TransportFabricBatch.ts transport with batch delivery
│ └── block/
│ ├── TransportFabricBlockParserBatch.ts parsing of a block with a batch
│ ├── ITransportFabricBlockBatch.ts structure of a block with a batch
│ └── ITransportFabricTransactionBatch.ts transaction with the receiving block number
└── factory/
├── TransportFabricFactory.ts transport reuse
└── TransportFabricConnectionSettingsFactory.ts settings factoryRelated Packages
| Package | Role | |---|---| | @hlf-core/transport-common | shared protocol: request and response envelopes, options with a signature | | @hlf-core/transport-chaincode | chaincode side: receiving commands, verifying signatures | | @hlf-core/api | Fabric client: connection, contracts, queries to qscc |
License
ISC
Author
Renat Gubaev
- Email: [email protected]
- GitHub: @ManhattanDoctor
Links
Support
If you have found a bug or have a suggestion for an improvement, please create an issue in GitHub Issues.
