CFA-Core Service
Основной сервис управления жизненным циклом ЦФА
CFA-Core Service
Статус: Реализован. Основной сервис управления жизненным циклом ЦФА с полной интеграцией с блокчейном.
CFA-Core — это "мозг" платформы, отвечающий за исполнение бизнес-логики, связанной с цифровыми финансовыми активами, в строгом соответствии с 259-ФЗ.
Основные функции
- Управление выпусками (Issuance): Регистрация решения о выпуске, контроль лимитов эмиссии.
- Жизненный цикл токенов: Mint (выпуск), Burn (погашение), Transfer (перевод).
- Смарт-контракты: Генерация параметров и деплой смарт-контрактов для каждого выпуска.
- Синхронизация с DLT: Гарантия того, что состояние в блокчейне соответствует юридическим требованиям.
- Интеграция с реестром: Ведение реестра владельцев (совместно с Ledger-DB).
Технологический стек
- Фреймворк: NestJS
- База данных: PostgreSQL
- Блокчейн: Hyperledger Besu (через Besu Connector)
- Язык контрактов: Solidity
Protobuf Сервис
service CfaRegistryService {
rpc IssueCfaRub(IssueCfaRubRequest) returns (IssueCfaRubResponse);
rpc RedeemCfaRub(RedeemCfaRubRequest) returns (RedeemCfaRubResponse);
rpc IssueInvestmentCfa(IssueInvestmentCfaRequest) returns (IssueInvestmentCfaResponse);
rpc RedeemInvestmentCfa(RedeemInvestmentCfaRequest) returns (RedeemInvestmentCfaResponse);
rpc CheckIntegrity(CheckIntegrityRequest) returns (CheckIntegrityResponse);
}Статусы операций
CREATED— операция созданаKYC_REQUIRED— требуется проверка KYCCOMPLIANCE_CHECK— проверка AML/санкцийAWAITING_PAYMENT— ожидание платежаPROCESSING— обработкаONCHAIN_SUBMITTED— отправлена в DLTFINALIZED— завершена в DLTREJECTED— отклоненаFAILED— ошибка
Взаимодействие
Входящие события
FiatDepositedEvent(из Payments-RU) → Инициация выпуска CFA-RUB.TradeExecutedEvent(из Market Service) → Перевод прав требования в DLT.
Исходящие события
CfaIssuedEvent→ Сигнал для Ledger-DB обновить балансы.CfaRedeemedEvent→ Сигнал о завершении жизненного цикла актива.
Безопасность
Все операции по изменению состояния в DLT проходят через проверку KYC и соответствие лимитам, запрашиваемым у Identity Service.
API (gRPC Connect)
CfaRegistryService
RegisterInstrument
Регистрация нового инструмента (выпуска ЦФА).
message RegisterInstrumentRequest {
RequestContext context = 1;
string symbol = 2;
string name = 3;
InstrumentType type = 4;
uint32 decimals = 5;
string issuer_id = 6;
string smart_contract_address = 7;
string currency_code = 8;
}
message RegisterInstrumentResponse {
string instrument_id = 1;
}Пример:
const instrument = await cfaCoreService.registerInstrument(
{ requestId, correlationId, idempotencyKey },
{
symbol: 'GAZP-BOND-2024',
name: 'Облигации ПАО "Газпром" 2024',
type: InstrumentType.CFA_BOND,
decimals: 2,
issuerId: 'issuer-456',
currencyCode: 'RUB',
},
);IssueCfaRub
Выпуск CFA-RUB (денежных требований).
message IssueCfaRubRequest {
RequestContext context = 1;
string user_id = 2;
Money amount = 3;
string payment_id = 4;
}
message IssueCfaRubResponse {
CfaOperation operation = 1;
}RedeemCfaRub
Погашение CFA-RUB.
message RedeemCfaRubRequest {
RequestContext context = 1;
string user_id = 2;
Money amount = 3;
string payout_id = 4;
}
message RedeemCfaRubResponse {
CfaOperation operation = 1;
}IssueInvestmentCfa
Выпуск инвестиционных ЦФА.
message IssueInvestmentCfaRequest {
RequestContext context = 1;
string instrument_id = 2;
string investor_id = 3;
MonetaryAmount amount = 4;
string bookbuilding_id = 5;
}
message IssueInvestmentCfaResponse {
CfaOperation operation = 1;
}TransferCfa
Перевод ЦФА между пользователями.
message TransferCfaRequest {
RequestContext context = 1;
string instrument_id = 2;
string from_user_id = 3;
string to_user_id = 4;
MonetaryAmount amount = 5;
string trade_id = 6;
}
message TransferCfaResponse {
CfaOperation operation = 1;
}ListHolders
Получение списка держателей ЦФА.
message ListHoldersRequest {
RequestContext context = 1;
string instrument_id = 2;
PaginationRequest pagination = 3;
}
message ListHoldersResponse {
repeated HolderBalance holders = 1;
PaginationResponse pagination = 2;
}CheckIntegrity
Проверка целостности реестра.
message CheckIntegrityRequest {
RequestContext context = 1;
uint64 block_number = 2;
string block_hash = 3;
HashAlgorithm algorithm = 4;
}
message CheckIntegrityResponse {
bool valid = 1;
string computed_hash = 2;
}События Kafka
Исходящие (maniton.cfa.events.v1)
CfaIssuedEvent
message CfaIssuedEvent {
RequestContext context = 1;
string operation_id = 2;
string instrument_id = 3;
string holder_id = 4;
MonetaryAmount amount = 5;
string tx_hash = 6;
google.protobuf.Timestamp issued_at = 7;
}CfaRedeemedEvent
message CfaRedeemedEvent {
RequestContext context = 1;
string operation_id = 2;
string instrument_id = 3;
string holder_id = 4;
MonetaryAmount amount = 5;
string tx_hash = 6;
google.protobuf.Timestamp redeemed_at = 7;
}CfaTransferredEvent
message CfaTransferredEvent {
RequestContext context = 1;
string operation_id = 2;
string instrument_id = 3;
string from_user_id = 4;
string to_user_id = 5;
MonetaryAmount amount = 6;
string tx_hash = 7;
google.protobuf.Timestamp transferred_at = 8;
}InstrumentRegisteredEvent
message InstrumentRegisteredEvent {
RequestContext context = 1;
string instrument_id = 2;
string symbol = 3;
string name = 4;
InstrumentType type = 5;
string issuer_id = 6;
google.protobuf.Timestamp registered_at = 7;
}Входящие
maniton.payments.events.v1:FiatDepositedEventmaniton.market.events.v1:TradeExecutedEvent
Use Cases
IssueCfaRubUseCase
Выпуск CFA-RUB после поступления фиатных средств.
async execute(request: IssueCfaRubRequest): Promise<IssueCfaRubResponse> {
const { context, userId, amount, paymentId } = request;
// 1. Проверка идемпотентности
const existing = await this.operationRepository.findByIdempotencyKey(
context.idempotencyKey
);
if (existing) {
return { operation: existing };
}
// 2. Проверка лимитов
const limitsCheck = await this.identityClient.checkLimits(
context,
userId,
LimitType.DEPOSIT,
amount,
OperationType.ISSUANCE
);
if (!limitsCheck.allowed) {
const rejectionReason = limitsCheck.reason?.includes('KYC')
? RejectionReasonCode.KYC_REQUIRED
: RejectionReasonCode.LIMIT_EXCEEDED;
const operation = new CfaOperation(
crypto.randomUUID(),
OperationType.ISSUANCE,
OperationStatus.REJECTED,
userId,
null,
amount.amount.value,
amount.currencyCode,
context.idempotencyKey,
paymentId
);
operation.rejectionReason = rejectionReason;
operation.rejectionMessage = limitsCheck.reason;
await this.operationRepository.save(operation);
await this.auditPublisher.log({
resource: AuditResourceType.CFA_ISSUANCE,
action: CFA_AUDIT_ACTIONS.ISSUANCE_REJECTED,
userId,
reason: limitsCheck.reason,
});
return { operation };
}
// 3. Создание операции
const operation = new CfaOperation(
crypto.randomUUID(),
OperationType.ISSUANCE,
OperationStatus.PROCESSING,
userId,
null,
amount.amount.value,
amount.currencyCode,
context.idempotencyKey,
paymentId
);
await this.operationRepository.save(operation);
// 4. Генерация транзакции
const txPayload = this.transactionBuilder.buildMintTx({
to: '0x' + userId.replace(/-/g, '').substring(0, 40),
value: amount.amount.value,
});
// 5. Подпись транзакции
const signedTx = await this.web3Signer.sign({
payload: txPayload,
chainId: this.configService.get('besu.chainId'),
});
// 6. Отправка в блокчейн
const txHash = await this.besuCommand.submitTransaction({
context,
payload: signedTx,
});
// 7. Обновление статуса
operation.status = OperationStatus.ONCHAIN_SUBMITTED;
operation.onchainTxHash = txHash;
await this.operationRepository.save(operation);
// 8. Публикация события
await this.eventPublisher.publish({
type: 'IssuanceStarted',
payload: operation,
});
return { operation };
}FinalizeOperationUseCase
Финализация операции после подтверждения транзакции в блокчейне.
async execute(txHash: string): Promise<void> {
// 1. Получение receipt
const receipt = await this.besuCommand.getTransactionReceipt({
context: { requestId, correlationId, idempotencyKey },
txHash,
});
// 2. Получение операции
const operation = await this.operationRepository.findByTxHash(txHash);
if (!operation) {
throw new Error('Operation not found');
}
// 3. Проверка статуса транзакции
if (receipt.status === BesuTxStatus.REVERTED) {
operation.status = OperationStatus.REJECTED;
operation.rejectionReason = RejectionReasonCode.INTEGRATION_ERROR;
operation.rejectionMessage = 'Transaction reverted';
await this.operationRepository.save(operation);
await this.eventPublisher.publish({
type: 'IssuanceFailed',
payload: operation,
});
return;
}
// 4. Финализация операции
operation.status = OperationStatus.FINALIZED;
operation.finalizedAt = new Date();
await this.operationRepository.save(operation);
// 5. Публикация события
await this.eventPublisher.publish({
type: 'CfaIssued',
payload: {
operationId: operation.operationId,
instrumentId: 'CFA-RUB',
holderId: operation.userId,
amount: operation.amount,
txHash,
issuedAt: new Date(),
},
});
// 6. Аудит
await this.auditPublisher.log({
resource: AuditResourceType.CFA_ISSUANCE,
action: CFA_AUDIT_ACTIONS.ISSUANCE_FINALIZED,
userId: operation.userId,
operationId: operation.operationId,
txHash,
});
}Мониторинг
Метрики
import { Counter, Histogram, Gauge } from 'prom-client';
export const cfaIssuancesTotal = new Counter({
name: 'cfa_issuances_total',
help: 'Total number of CFA issuances',
labelNames: ['type', 'status'],
});
export const cfaRedemptionsTotal = new Counter({
name: 'cfa_redemptions_total',
help: 'Total number of CFA redemptions',
labelNames: ['type', 'status'],
});
export const cfaTransfersTotal = new Counter({
name: 'cfa_transfers_total',
help: 'Total number of CFA transfers',
labelNames: ['instrument_id'],
});
export const blockchainTransactionsTotal = new Counter({
name: 'blockchain_transactions_total',
help: 'Total number of blockchain transactions',
labelNames: ['status'],
});
export const issuanceDuration = new Histogram({
name: 'issuance_duration_seconds',
help: 'Time spent on CFA issuance',
buckets: [1, 5, 10, 30, 60, 120, 300],
});Логи
this.logger.log('CFA issued', {
operationId: operation.operationId,
userId: operation.userId,
instrumentId: 'CFA-RUB',
amount: operation.amount,
txHash: operation.onchainTxHash,
});
this.logger.log('CFA transferred', {
operationId: operation.operationId,
instrumentId: operation.instrumentId,
fromUserId: operation.fromUserId,
toUserId: operation.toUserId,
amount: operation.amount,
txHash: operation.onchainTxHash,
});Интеграции
Besu Connector
Взаимодействие с блокчейном через Besu Connector.
interface BesuCommand {
submitTransaction(payload: TransactionPayload): Promise<string>;
getTransactionReceipt(txHash: string): Promise<TransactionReceipt>;
getBlock(number: number): Promise<Block>;
verifyBlockHash(number: number, hash: string): Promise<boolean>;
}Identity Service
Проверка KYC и лимитов перед операциями.
interface IdentityService {
getUserProfile(context: RequestContext, userId: string): Promise<UserProfile>;
checkLimits(
context: RequestContext,
userId: string,
limitType: LimitType,
amount: Money,
operationType: OperationType,
): Promise<{ allowed: boolean; reason?: string }>;
checkSanctions(
context: RequestContext,
userId: string,
payload: SubmitKycPayload,
): Promise<SanctionsCheck>;
}Ledger Service
Операционный учет операций.
interface LedgerService {
createOperation(request: CreateOperationRequest): Promise<CreateOperationResponse>;
getOperation(operationId: string): Promise<GetOperationResponse>;
createHold(request: CreateHoldRequest): Promise<CreateHoldResponse>;
consumeHold(holdId: string): Promise<ConsumeHoldResponse>;
}Troubleshooting
Проблема: Транзакция не проходит в блокчейне
Диагностика:
# Проверка статуса транзакции
curl -X POST http://besu-connector:3004/getTransactionReceipt \
-H "Content-Type: application/json" \
-d '{"tx_hash": "0x..."}'
# Проверка блоков
curl -X POST http://besu-connector:3004/getBlock \
-H "Content-Type: application/json" \
-d '{"block_number": 12345}'
# Проверка газа
curl -X POST http://besu-connector:3004/estimateGas \
-H "Content-Type: application/json" \
-d '{"to": "0x...", "data": "0x..."}'Решение:
- Проверьте, что газ достаточен
- Проверьте, что nonce верный
- Проверьте, что контракт существует
Проблема: Выпуск не проходит
Диагностика:
# Проверка лимитов
curl http://auth-service:3001/identity/limits/$USER_ID
# Проверка KYC
curl http://auth-service:3001/identity/users/$USER_ID
# Проверка операции
curl http://cfa-core-service:3002/operations/$OPERATION_IDРешение:
- Проверьте KYC статус
- Проверьте лимиты
- Проверьте санкционные списки