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.