fix(controller/install): bundle adduser+createBoard в одну tx
Cause. install.interactor.ts шлёт adduser×N и createBoard двумя отдельными
tx через BlockchainService. На production-нодах с producer-схемой это
работает (lag реплицирования минимальный), но на dev-loop'е partner-
coopback соединён со своим nodeos, который p2p-репликой подтягивает
блоки от producer'а — после accept'а adduser в local state ещё нет
soviet::participants[username] к моменту push'а createBoard. Контракт
soviet::createboard падает «Один из аккаунтов не найден в реестре
пайщиков».
Fix. Объединил все adduser-action'ы и createBoard-action в одну tx
через новый метод BlockchainPort.installSoviet(). Обе action'ы теперь
атомарны в одном блоке — soviet::addpartcpnt (inline action от
adduser) обновляет participants и createBoard видит запись сразу же
в том же блоке.
Поток в install.interactor.ts перестроен в два шага:
1. Цикл по soviet: createUser в БД + setupNotificationSubscriber +
сбор addUserActions[] и members[] (без on-chain активности).
2. installSoviet(addUserActions, createBoardData) — одна tx.
Catch при ошибке on-chain (как и раньше) откатывает users из БД.
Зачем. Закрывает блокер сценария 08-chairman-install-on-partner-dev:
без этого финальный экран wizard'а «Установка завершена» недостижим
на dev-loop'е (Эпик 0 не закрывается).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -136,14 +136,16 @@ export class InstallInteractor {
|
||||
const users = [] as UserDomainEntity[];
|
||||
const members = [] as any;
|
||||
const sovietExt = [] as any;
|
||||
const addUserActions: RegistratorContract.Actions.AddUser.IAddUser[] = [];
|
||||
const soviet = data.soviet;
|
||||
|
||||
try {
|
||||
// Шаг 1: создаём users в БД и собираем on-chain action'ы (без отправки)
|
||||
for (const member of soviet) {
|
||||
const username = generateUsername();
|
||||
sovietExt.push({ ...member, username });
|
||||
|
||||
const addUser: RegistratorContract.Actions.AddUser.IAddUser = {
|
||||
addUserActions.push({
|
||||
coopname: config.coopname,
|
||||
referer: '',
|
||||
username,
|
||||
@@ -154,9 +156,7 @@ export class InstallInteractor {
|
||||
spread_initial: false,
|
||||
meta: '',
|
||||
registration_hash: sha256(username),
|
||||
};
|
||||
|
||||
await this.blockchainPort.addUser(addUser);
|
||||
});
|
||||
|
||||
const createUser: CreateUserInputDomainInterface = {
|
||||
email: member.individual_data.email,
|
||||
@@ -168,20 +168,17 @@ export class InstallInteractor {
|
||||
};
|
||||
|
||||
const user = await this.createUser(createUser);
|
||||
// Обновляем статус пользователя на "зарегистрирован"
|
||||
await this.userDomainService.updateUserByUsername(username, {
|
||||
status: userStatus['4_Registered'],
|
||||
is_registered: true,
|
||||
});
|
||||
|
||||
// Настраиваем подписчика уведомлений для члена совета
|
||||
try {
|
||||
await this.accountDomainService.setupNotificationSubscriber(username, 'члена совета');
|
||||
} catch (error: any) {
|
||||
logger.error(`Ошибка настройки подписчика NOVU для члена совета ${username}: ${error.message}`, error.stack);
|
||||
}
|
||||
|
||||
// Добавляем в массив членов для отправки в блокчейн
|
||||
members.push({
|
||||
username: username,
|
||||
is_voting: true,
|
||||
@@ -192,8 +189,9 @@ export class InstallInteractor {
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
// Создаём доску совета
|
||||
await this.blockchainPort.createBoard({
|
||||
// Шаг 2: bundle adduser×N + createBoard ОДНОЙ tx (исключает race
|
||||
// между отдельными tx когда coopback соединён с не-producer nodeos)
|
||||
await this.blockchainPort.installSoviet(addUserActions, {
|
||||
coopname: config.coopname,
|
||||
username: config.coopname,
|
||||
type: 'soviet',
|
||||
|
||||
@@ -42,6 +42,10 @@ export interface BlockchainPort {
|
||||
// System installation methods
|
||||
addUser(data: RegistratorContract.Actions.AddUser.IAddUser): Promise<void>;
|
||||
createBoard(data: SovietContract.Actions.Boards.CreateBoard.ICreateboard): Promise<void>;
|
||||
installSoviet(
|
||||
addUsers: RegistratorContract.Actions.AddUser.IAddUser[],
|
||||
createBoardData: SovietContract.Actions.Boards.CreateBoard.ICreateboard
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export const BLOCKCHAIN_PORT = Symbol('BlockchainPort');
|
||||
|
||||
@@ -319,4 +319,40 @@ export class BlockchainService implements BlockchainPort {
|
||||
|
||||
await this.transact(actions);
|
||||
}
|
||||
|
||||
// Шлёт adduser×N + createBoard ОДНОЙ транзакцией. Раздельные tx
|
||||
// ловят race на не-producer-нодах (partner-coopback соединён со своим
|
||||
// nodeos, который p2p-репликой подтягивает блоки от producer'а — между
|
||||
// accept'ом adduser и push'ем createBoard нет гарантии что
|
||||
// soviet::participants[username] уже виден в local state). Bundle
|
||||
// делает обе action'ы атомарными в одном блоке.
|
||||
public async installSoviet(
|
||||
addUsers: RegistratorContract.Actions.AddUser.IAddUser[],
|
||||
createBoardData: SovietContract.Actions.Boards.CreateBoard.ICreateboard
|
||||
): Promise<void> {
|
||||
const wif = await this.vaultDomainService.getWif(config.coopname);
|
||||
if (!wif) throw new Error(`Не найден приватный ключ для кооператива ${config.coopname}`);
|
||||
|
||||
this.initialize(config.coopname, wif);
|
||||
|
||||
const actions: any[] = [];
|
||||
|
||||
for (const data of addUsers) {
|
||||
actions.push({
|
||||
account: RegistratorContract.contractName.production,
|
||||
name: RegistratorContract.Actions.AddUser.actionName,
|
||||
authorization: [{ actor: config.coopname, permission: 'active' }],
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
account: SovietContract.contractName.production,
|
||||
name: SovietContract.Actions.Boards.CreateBoard.actionName,
|
||||
authorization: [{ actor: config.coopname, permission: 'active' }],
|
||||
data: createBoardData,
|
||||
});
|
||||
|
||||
await this.transact(actions);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user