Сборка и запуск chaincode
«Быстрый старт» показывает скелет — контракт и клиент в несколько строк. Здесь мы разбираем, как это выглядит в боевом chaincode: на NestJS, с модулями, режимами запуска и метаданными, которыми контракт сам о себе рассказывает.
Точка входа и DI вне HTTP
Chaincode это не веб-сервер, но DI-контейнер NestJS нам всё равно удобен — модули, провайдеры,
внедрение зависимостей. Поэтому Nest поднимается в standalone-режиме, без HTTP, через
createApplicationContext. Есть один обязательный и легко упускаемый шаг: сразу после создания
контекста нужно отдать его ModuleRef в APPLICATION_INJECTOR — иначе часть инфраструктуры
@ts-core, которая достаёт зависимости по требованию, не найдёт контейнер.
async function bootstrap() {
let settings = new AppSettings();
let logger = (settings.logger = new DefaultLogger(settings.loggerLevel));
let application = await NestFactory.createApplicationContext(AppModule.forRoot(settings), { logger });
APPLICATION_INJECTOR(application.get(ModuleRef));
let chaincode = application.get(Chaincode);
if (settings.chaincodeMode === ChaincodeMode.INTERNAL) {
Shim.start(chaincode);
return;
}
let server = Shim.server(chaincode, settings.chaincodeServerOptions);
server.start();
}
bootstrap();
Модуль и регистрация
Модуль приложения собирает всё воедино: подключает транспорт через TransportFabricChaincodeModule.forRoot(settings)
и доменные модули, а обработчики команд объявляет в controllers — именно контроллерами, а не
провайдерами, потому что обработчик подписывается на транспорт в конструкторе и должен быть создан
сразу (подробнее в «Обработчиках команд»).
public static forRoot(settings: AppSettings): DynamicModule {
return {
module: AppModule,
imports: [
LoggerModule.forRoot(settings),
TransportFabricChaincodeModule.forRoot(settings),
UserModule, CoinModule, SeedModule, // доменные модули
],
providers: [{ provide: AppSettings, useValue: settings }, Chaincode],
controllers: [MetadataGetHandler, NonceGetHandler],
};
}
Сам контракт — это Chaincode, наследник TransportFabricSeededChaincode, который на Init
идемпотентно инициализирует реестр через сидер (см. «Seed и инициализацию»).
Два режима запуска
Тот же код контракта запускается двумя способами, и выбор определяется настройкой chaincodeMode.
В режиме INTERNAL контракт поднимается привычным Shim.start — как процесс, которым управляет сам
peer. Иначе это chaincode-as-a-service: контракт работает отдельным сервисом и слушает peer по
адресу, который задан в chaincodeServerOptions (его ccid и address обычно приходят из
окружения):
public get chaincodeMode(): ChaincodeMode {
return this.getValue('CHAINCODE_MODE', ChaincodeMode.INTERNAL);
}
public get chaincodeServerOptions(): ChaincodeServerOpts {
return { ccid: this.getValue('CORE_CHAINCODE_ID'), address: this.getValue('CORE_CHAINCODE_ADDRESS'), tlsProps: null };
}
Второй режим удобнее в разработке и при частых обновлениях логики: контракт можно перезапускать, не трогая peer. Но знать о развилке нужно заранее, потому что от неё зависит и сборка образа, и то, как контракт регистрируется в сети.
Метаданные контракта
Клиенту полезно узнать о контракте, не заглядывая в его исходники — включён ли batch, в каком он
режиме. Для этого есть readonly-команда ChaincodeMetadataGetCommand, и обработчик к ней возвращает
IChaincodeMetadata:
@Injectable()
export class MetadataGetHandler extends TransportCommandAsyncHandler<void, IChaincodeMetadata, ChaincodeMetadataGetCommand> {
constructor(logger: Logger, transport: Transport, private settings: AppSettings, private chaincode: Chaincode) {
super(logger, transport, ChaincodeMetadataGetCommand.NAME);
}
protected async execute(): Promise<IChaincodeMetadata> {
return { name: this.chaincode.name, mode: this.settings.chaincodeMode, batch: this.settings.batch };
}
}
По этим данным клиент и выбирает транспорт — обычный или пакетный, о чём речь в разделе «Batch-режим».