@hlf-core/common
v3.2.8
Published
TypeScript library with core classes shared across hlf-core packages: Hyperledger Fabric state key building, user identifiers, crypto keys, transport commands and domain errors
Maintainers
Readme
English · Русский
@hlf-core/common
TypeScript library with the base classes of the hlf-core ecosystem: state keys, identifiers, transport commands
Full documentation: https://hlf.gnosisai.ru/docs/packages/common
Base classes and utilities shared by every package in the @hlf-core ecosystem: Hyperledger Fabric state key construction, user identifiers, cryptographic keys, transport commands, and domain errors.
The package depends neither on fabric-shim nor on the chaincode runtime, so it works identically in chaincode, backend, and frontend.
Table of Contents
Overview
@hlf-core/common is the lowest layer of the ecosystem. It holds what every other package needs and what is not tied to the runtime environment: the rules for building state keys, the user identifier schema, the cryptographic key description, the base transport commands, and the domain error framework.
Key Features
- Unified state key construction —
StateKeybuilds keys and search prefixes by the same rules, with a mandatory separator and segment validation - User identifiers —
UserUtilcreates and validates auidof the formuser_<time>_<hash> - Cryptographic keys —
CryptoKeydescribes a "value + algorithm" pair with validation - Transport commands —
HlfTransportCommandandHlfTransportCommandAsyncwith anisReadonlyflag to separate read requests from write requests - Operation initiator —
InitiatedDtoadds aninitiatorUidfield to any DTO - Domain errors — a base
ErrorwithErrorCodecodes, compatible withExtendedErrorfrom@ts-core/common
Installation
npm install @hlf-core/commonDependencies
{
"@ts-core/common": "~3.0.57"
}The package ships in two builds — ESM (./esm/public-api.js) and CommonJS (./cjs/public-api.js) — and the right one is selected automatically through the exports field.
Quick Start
import { StateKey, UserUtil } from '@hlf-core/common';
// key namespace for accounts
let key = new StateKey('→coin~account');
// exact record key
let uid = key.key('coin_user_X_0_RUB', 'user_00000000000000_abc');
// →coin~account:coin_user_X_0_RUB~user_00000000000000_abc
// prefix to enumerate every account of a coin
let prefix = key.prefix('coin_user_X_0_RUB');
// →coin~account:coin_user_X_0_RUB~
// user identifier
let userUid = UserUtil.createUid(new Date(), hash);
// user_01752065464752_9c6d51852f75...State Keys
How Fabric storage works
Hyperledger Fabric state is an ordered "key → value" dictionary. A record can be found in two ways: by its exact key, or with a range query (getStateByRange) that returns everything lexicographically falling between two strings. In practice, a range is almost always used as "everything that starts with the given string".
From this follows the main rule: secondary lookup is only possible on the left part of the key. That is why keys are composite — made of several segments, from the general to the specific:
→coin~account:coin_user_X_0_RUB~user_00000000000000_abc
└─ namespace ───┘└───── coin ────┘└─────── owner ───────┘Such a key serves two scenarios at once: reading a specific account by its full key and enumerating every account of a coin by prefix.
Why a prefix must end with a separator
A search prefix is the full key plus a separator, not "the key without its last segment". The difference is fundamental.
If you build a prefix without the trailing separator, the range query will capture neighboring records whose identifier starts the same way:
prefix: →coin~account:coin_user_X_0_RUB
matched by it:
→coin~account:coin_user_X_0_RUB~user_abc the intended record
→coin~account:coin_user_X_0_RUB2~user_xyz ANOTHER coin RUB2
→coin~account:coin_user_X_0_RUB.SERIES.1~user_xyz ANOTHER serial coinThe string coin_user_X_0_RUB is the beginning of the string coin_user_X_0_RUB2, and the store faithfully returns both. The bug stays invisible until the system has coins whose identifier is a prefix of another, and then it leads to operations on records that belong to someone else.
With a trailing separator this does not happen: at that position another coin has a 2 or a ., not a ~.
StateKey makes such a mistake impossible: the calling code has no way to obtain a prefix other than the prefix() method.
API Reference
StateKey
A state key builder. It is created once per namespace and then used for every key in that namespace.
import { StateKey } from '@hlf-core/common';
let key = new StateKey('→coin~account');Static properties
| Property | Value | Purpose |
|---|---|---|
| StateKey.SEGMENT | ~ | separator between key segments |
| StateKey.NAMESPACE | : | separator between the namespace and the segments |
key(...items: Array<UID>): string
Returns the exact record key. Segments are joined with the ~ separator.
key.key('coin_RUB', 'user_abc'); // →coin~account:coin_RUB~user_abc
key.key('coin_RUB'); // →coin~account:coin_RUB
key.key(); // →coin~account:Used wherever exactly one object is addressed: read, write, delete.
prefix(...items: Array<UID>): string
Returns the prefix for a range query — a key with a trailing separator.
key.prefix('coin_RUB'); // →coin~account:coin_RUB~
key.prefix('coin_RUB', 'user_abc'); // →coin~account:coin_RUB~user_abc~
key.prefix(); // →coin~account:A call with no arguments yields the prefix of the whole namespace — use it to enumerate every record of this kind.
isKey(item: UID): boolean
Checks whether a key belongs to this namespace.
key.isKey('→coin~account:coin_RUB~user_abc'); // true
key.isKey('→link~account~otc:coin_RUB~...'); // falseUseful when records of different kinds share a single range.
decompose(item: UID): Array<string>
Splits a key back into its segments. Returns null if the key belongs to a different namespace.
key.decompose('→coin~account:coin_RUB~user_abc'); // ['coin_RUB', 'user_abc']
key.decompose('→link~account~otc:coin_RUB~...'); // nullReplaces manual string parsing with split and indexOf.
Rules for key segments
Each segment is validated before being joined. A segment cannot be empty and cannot contain ~ or ::
key.key('a~b'); // StateKeySegmentInvalidError
key.key('a:b'); // StateKeySegmentInvalidError
key.key(''); // StateKeySegmentInvalidErrorThe reason: a separator inside a value shifts the boundary between segments, and the key silently drifts — a prefix search starts returning something other than what was expected. It is better to fail on write than to get an incorrect selection on read.
The namespace itself is not validated — it is set once by the developer and may contain any characters, including ~ (for example, →coin~account).
UserUtil
Building and validating user identifiers.
public static PREFIX = 'user';
public static UID_REG_EXP = /^user_[0-9]{14}_[0-9a-fA-F]{64}$/;createUid(created: Date, hash: string): string
Creates a uid of the form user_<time:14>_<hash:64>. The time is zero-padded to a fixed width, so identifiers are lexicographically ordered by creation date — a range query returns users in chronological order.
UserUtil.createUid(new Date(), stub.transaction.hash);
// user_01752065464752_9c6d51852f751fb8c402d8b676f82cbe...create<T extends IUser>(classType: ClassType<T>, created: Date, hash: string): T
Creates a user instance with uid and created filled in.
let user = UserUtil.create(User, stub.transaction.date, stub.transaction.hash);isUser(uid: UID): boolean
Checks whether an identifier matches the user format.
seed<T extends IUser>(classType: ClassType<T>, created?: Date, hash?: string): T
Creates a system user with a zero date and hash — a deterministic uid that is identical on every node of the network. Used when initializing a channel.
let seed = UserUtil.seed(User);
// user_00000000000000_0000000000000000000000000000000000000000000000000000000000000000IUser
The user interface, parameterized by status and role types.
export interface IUser<S = string, R = string> extends IUIDable {
uid: string;
created: Date;
roles?: Array<R>;
status?: S;
cryptoKey?: ICryptoKey;
description?: string;
}A specific project plugs in its own enumerations:
class User implements IUser<UserStatus, UserRole> { ... }CryptoKey
A description of a cryptographic key: value and algorithm.
export class CryptoKey implements ICryptoKey {
public static PREFIX = 'cryptoKey';
@IsString() value: string;
@IsString() algorithm: string;
}The fields are annotated with class-validator decorators, so the object is validated with the same tooling as the rest of the entities when written to state.
HlfTransportCommand
Transport commands with a "read-only" flag.
export class HlfTransportCommand<T> extends TransportCommand<T> {
constructor(name: string, request?: T, id?: string, public isReadonly?: boolean);
}
export class HlfTransportCommandAsync<U, V> extends TransportCommandAsync<U, V> {
constructor(name: string, request?: U, id?: string, public isReadonly?: boolean);
}isReadonly lets the transport distinguish requests that do not change state and handle them more cheaply — without sending a transaction for endorsement.
export class CoinGetCommand extends HlfTransportCommandAsync<ICoinGetDto, Coin> {
public static readonly NAME = 'CoinGet';
constructor(request: ICoinGetDto) {
super(CoinGetCommand.NAME, request, undefined, true); // read-only
}
}InitiatedDto
Adds an operation initiator to a DTO.
export interface IInitiatedDto {
initiatorUid?: string;
}
export class InitiatedDto implements IInitiatedDto {
@IsOptional()
@IsString()
public initiatorUid?: string;
}Used as a base for commands and events where you need to know on whose behalf an action was performed — separately from who signed the transaction.
export class CoinEmittedEventDto extends InitiatedDto {
public coinUid: string;
public value: string;
}Error
The package's domain error framework.
export class Error<T = void> extends ExtendedError<T, ErrorCode> {
public static instanceOf(item: any): item is Error;
constructor(code: ErrorCode, details: T, status?: number);
}An error carries a machine-readable code and details, while its message is the class name. This makes it possible to handle it by code on the other side of the transport, where the stack is no longer available.
Error Codes
| Code | Class | When it occurs |
|---|---|---|
| HLF_STATE_KEY_SEGMENT_INVALID | StateKeySegmentInvalidError | a key segment is empty or contains ~ or : |
try {
key.key(ticker);
}
catch (error) {
if (Error.instanceOf(error)) {
console.log(error.code); // HLF_STATE_KEY_SEGMENT_INVALID
console.log(error.details); // { value: 'RUB~2', expected: 'without "~" and ":"' }
}
}Usage Examples
Account keys
import { StateKey } from '@hlf-core/common';
export class CoinAccountUtil {
public static PREFIX = '→coin~account';
public static KEY = new StateKey(CoinAccountUtil.PREFIX);
// exact account key
public static createUid(coin: UID, owner: UID): string {
return CoinAccountUtil.KEY.key(coin, owner);
}
// prefix to enumerate every account of a coin
public static createPrefix(coin: UID): string {
return CoinAccountUtil.KEY.prefix(coin);
}
}The two modes are split across separate methods, so there is no way to confuse an exact key with a search prefix.
Secondary index
The task: find orders by coin and by the "coin + owner" pair. Fabric has no direct lookup by field, so a pointer key is stored next to the record.
let index = new StateKey('→otc~hold');
// when an order is created
await stub.putStateRaw(index.key(coinUid, ownerUid, otcUid), otcUid);
// all orders for a coin
let byCoin = await stub.getStateByRange(index.prefix(coinUid), ...);
// orders of a specific owner for this coin
let byOwner = await stub.getStateByRange(index.prefix(coinUid, ownerUid), ...);
// when an order is deleted
await stub.removeState(index.key(coinUid, ownerUid, otcUid));A single set of keys serves both slices thanks to the order of the segments: from the general to the specific.
Decomposing a key
let key = new StateKey('→coin~account');
let uid = key.key('coin_user_X_0_RUB', 'user_abc');
let items = key.decompose(uid); // ['coin_user_X_0_RUB', 'user_abc']
let [coinUid, ownerUid] = items;Segment order matters
Lookup is only possible on the left part, so the order determines which slices are available:
// key (coin, owner): you can search by coin and by the pair
key.prefix(coinUid); // all owners of this coin
key.prefix(coinUid, ownerUid); // a specific pair
// you cannot search by a single owner with this key —
// such a slice needs a second set of keys with the reverse order
let byOwner = new StateKey('→coin~owner');
await stub.putStateRaw(byOwner.key(ownerUid, coinUid), uid);Project Structure
src/
├── Error.ts domain errors and codes
├── public-api.ts the package's public API
├── state/
│ ├── StateKey.ts state key construction
│ └── index.ts
├── user/
│ ├── IUser.ts user interface
│ ├── UserUtil.ts user identifiers
│ └── index.ts
├── crypto/
│ ├── ICryptoKey.ts cryptographic key interface
│ ├── CryptoKey.ts implementation with validation
│ └── index.ts
└── transport/
├── HlfTransportCommand.ts commands with the isReadonly flag
├── IInitiatedDto.ts operation initiator
└── index.tsChangelog
3.2.6
- Added
StateKey— unified state key construction with a mandatory separator in prefixes and segment validation - Added
ErrorwithErrorCodecodes and the first error,StateKeySegmentInvalidError
The changes are additive only; the existing API is untouched. The key format produced by StateKey matches the one previously used across the ecosystem packages, so migrating existing code to StateKey requires no state migration.
License
ISC
Author
Renat Gubaev
- Email: [email protected]
- GitHub: @ManhattanDoctor
Links
Support
If you found a bug or have a suggestion for improvement, please open an issue in GitHub Issues.
