Skip to main content

Command Handlers

If Commands and Events describe what travels in the envelope, then a handler is what executes the command on the chaincode side. One command — one handler, and all business logic lives precisely in them. The transport takes on reception, signature and nonce verification, and hands the handler an already parsed command.

The base class and the execute method​

A handler inherits TransportCommandAsyncHandler<U, V, T> from @ts-core/common — the same base class as in ordinary @ts-core services, because the command model is one and the same for the blockchain and for a service. Here U is the request type (DTO), V is the response type, T is the command type. The developer implements a single method execute(request, holder), and the base wraps it: it runs the request through checkRequest, executes execute, passes the result through checkResponse and completes the command. An error thrown from execute is caught and returned to the client as an ExtendedError with a code — the very property thanks to which a domain error is not lost (see Commands and Events).

The binding to a command is set in the constructor — as the third argument of super(logger, transport, Command.NAME). A handler has no separate "readonly flag": a reading command is simply one whose execute makes neither putState nor dispatch.

import { Injectable } from '@nestjs/common';
import { Logger, Transport, TransportCommandAsyncHandler } from '@ts-core/common';
import { StubHolder } from '@hlf-core/transport-chaincode';
import { UserAddCommand, IUserAddDto } from '@project/common/hlf/transport';
import { User, UserRole } from '@project/common/hlf/user';
import { IUserStubHolder, UserGuard, rolesCheck } from '@project/module/core/guard';
import { UserService } from '../service';

@Injectable()
export class UserAddHandler extends TransportCommandAsyncHandler<IUserAddDto, User, UserAddCommand> {
constructor(logger: Logger, transport: Transport, private service: UserService) {
super(logger, transport, UserAddCommand.NAME);
}

@UserGuard()
protected async execute(params: IUserAddDto, @StubHolder() holder: IUserStubHolder): Promise<User> {
await rolesCheck(holder, UserRole.USER_MANAGER);
return this.service.add(holder, params);
}
}

State access: holder​

The second argument of execute is the holder, through which the handler works with the ledger. The subtlety is that the holder and the command are one and the same object: the command wrapper implements both the command interface and IStubHolder from @hlf-core/chaincode at once. To get it in the right parameter, the parameter is marked with the @StubHolder() decorator from @hlf-core/transport-chaincode.

Through holder.stub the state and the transaction context are accessible — getState / putState / hasState / removeState, the dispatch events, the user stub.user (with the fields id and publicKey, which the chaincode extracted from the signature) and, importantly, stub.transaction with its date and hash. It is these, not Date.now() and not random values, that are taken as the source of time and entropy — otherwise the result would diverge across different endorsing peers and the transaction would not pass validation. Domain code usually extends the holder for its own needs — adding the loaded user and its manager:

import { IStubHolder } from '@hlf-core/chaincode';
import { ITransportCommand } from '@ts-core/common';

export interface IUserStubHolder<U = any> extends IStubHolder, ITransportCommand<U> {
user?: User;
manager?: UserManager;
}

Guard: authorization before execute​

The framework has no ready-made auth guard, and this is done deliberately: the transport does not know the user domain model. Therefore authorization is written as a separate decorator of the execute method, which runs before the business logic and can reject the command. The canonical recipe is UserGuard: it loads the user from state by stub.user.id, checks the status (ACTIVE) and — crucially — compares the cryptoKey.value recorded in the ledger with the public key of the signature (stub.user.publicKey). Here the binding "key belongs to this user", which the transport by itself does not do, is closed (see Security).

export const UserGuard = (options?: IUserGuardOptions): any => {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
if (_.isNil(options)) {
options = { isNeedCheck: true };
}
let originalMethod = descriptor.value;
descriptor.value = async function (...args): Promise<any> {
let holder = await getUserStubHolder(this.logger, target, args);
await validateUserStubHolder(this.logger, holder, options);
return originalMethod.apply(this, args);
};
};
};

async function validateUserStubHolder<U>(logger: ILogger, item: IUserStubHolder<U>, options: IUserGuardOptions): Promise<void> {
if (_.isEmpty(options) || !options.isNeedCheck) {
return;
}
let { id } = item.stub.user;
let { user } = item;
if (_.isNil(user)) {
item.manager = new UserManager(logger, item.stub);
item.user = user = await item.manager.get(id, ['cryptoKey']);
}
if (_.isNil(user)) {
throw new UserNotFoundError(id);
}

let { status, cryptoKey } = user;
if (status !== UserStatus.ACTIVE) {
throw new UserStatusForbiddenError({ value: status, expected: UserStatus.ACTIVE });
}
if (_.isNil(cryptoKey)) {
throw new UserCryptoKeyNotFoundError(id);
}
if (cryptoKey.value !== item.stub.user.publicKey) {
throw new UserCryptoKeyInvalidError(id);
}
}

The option @UserGuard({ isNeedCheck: false }) skips the check — for reading commands such as UserGetCommand or CoinBalanceGetCommand, which need neither the user nor the key. And the role check is kept as a separate function rolesCheck(holder, ...roles) — it is called right inside execute where the right depends on the operation rather than on the mere fact of authorization. Next to it is rolesSomeOf(...) for the condition "at least one of the roles".

@Injectable()
export class CoinBalanceGetHandler extends TransportCommandAsyncHandler<ICoinBalanceGetDto, ICoinBalanceGetDtoResponse, CoinBalanceGetCommand> {
constructor(logger: Logger, transport: Transport, private service: CoinService) {
super(logger, transport, CoinBalanceGetCommand.NAME);
}

@UserGuard({ isNeedCheck: false })
protected async execute(params: ICoinBalanceGetDto, @StubHolder() holder: IUserStubHolder): Promise<ICoinBalanceGetDtoResponse> {
return this.service.balanceGet(holder, params);
}
}

Registration via NestJS​

There is no centralized list of handlers, and this is an important, easily missed point: a handler subscribes to the transport right in its constructor, so "registration" is simply its creation. In a NestJS project handlers are declared in the module's controllers, not in its providers — a provider is created lazily, on first access, and a command without it will simply not be heard, whereas Nest instantiates controllers right away.

@Module({
controllers: [CoinGetHandler, CoinAddHandler, CoinEmitHandler],
providers: [CoinService],
})
export class CoinModule {}

When a transaction arrives, TransportFabricChaincodeReceiver parses the envelope, verifies the signature and finds the handler by command name. If the name is not present — the command completes with a "no listener" error. Hence a convenient consequence: adding a new command does not require reinstalling the contract, it is enough to declare one more handler under its name — how exactly the contract is assembled and launched we cover in the Getting Started section.