[@ant] fix(controller+capital): event-driven regshare + delete-row aware реестр кошельков
- controller: listener delta::ledger2::userwallets[w.cap.blago] синхронизирует regshare без ожидания scheduler-тика 1440 мин - typeorm-ledger2-state.repository: getWallets/getAccounts берут самую свежую row на ключ и при present=false обнуляют суммы (не выкидывают), чтобы удалённый L2-кошелёк остался раскрываемым в реестре с историей - capital: inline regshare после apprvappndx + whitelist auth в regshare - programs.hpp: is_participant_of_cpp_by_program_id через wallet::users.programs[], get_program_wallet → has_program_wallet - cooptypes/operations: human_name «Коммит РИД по программе Генератор», убрано «(перенос между кошельками)»
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
cleos -u http://172.17.0.1:8888 "$@"
|
||||
cleos -u http://172.17.0.1:8908 "$@"
|
||||
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
* @note Авторизация требуется от аккаунта кооператива: @p coopname (@c active)
|
||||
*/
|
||||
void capital::regshare(eosio::name coopname, checksum256 project_hash, eosio::name username, eosio::asset user_shares) {
|
||||
require_auth(coopname);
|
||||
// Прямой вызов от кооператива (планировщик контроллера) ИЛИ inline-вызов
|
||||
// от системного контракта (например, capital::apprvappndx — авторегистрация
|
||||
// доли при допуске участника к проекту).
|
||||
if (!has_auth(coopname)) {
|
||||
check_auth_and_get_payer_or_fail(contracts_whitelist);
|
||||
}
|
||||
|
||||
// Проверяем существование проекта
|
||||
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@ void capital::createpinv(name coopname, name username, checksum256 invest_hash,
|
||||
auto contributor = Capital::Contributors::get_active_contributor_or_fail(coopname, username);
|
||||
|
||||
// Проверяем наличие кошелька в программе благороста (_capital_wallet)
|
||||
auto capital_wallet = Capital::Wallets::get_program_capital_wallet(coopname, username);
|
||||
eosio::check(capital_wallet.has_value(), "У пайщика нет кошелька в программе благороста");
|
||||
eosio::check(Capital::Wallets::has_program_capital_wallet(coopname, username),
|
||||
"У пайщика нет кошелька в программе благороста");
|
||||
|
||||
std::string memo = Capital::Memo::get_program_invest_memo(contributor -> id);
|
||||
|
||||
|
||||
+16
@@ -41,6 +41,22 @@ void capital::apprvappndx(eosio::name coopname, eosio::name username, checksum25
|
||||
// Фиксируем документ в реестре как принятый
|
||||
Soviet::make_complete_document(_capital, coopname, appendix->username, Names::Capital::APPROVE_APPENDIX, appendix_hash, approved_document);
|
||||
|
||||
// Авторегистрация доли по программе «Благорост» при допуске к проекту:
|
||||
// если у пайщика уже есть баланс w.cap.blago — сразу заводим сегмент с
|
||||
// user_shares = available + blocked. Без баланса regshare не вызываем —
|
||||
// сегмент создастся при ближайшем тике scheduler'а, как только появится
|
||||
// первая инвестиция в программу.
|
||||
const auto user_shares =
|
||||
Capital::Core::get_capital_program_user_share_balance(coopname, appendix->username);
|
||||
if (user_shares.amount > 0) {
|
||||
eosio::action(
|
||||
eosio::permission_level{_capital, "active"_n},
|
||||
_capital,
|
||||
"regshare"_n,
|
||||
std::make_tuple(coopname, appendix->project_hash, appendix->username, user_shares)
|
||||
).send();
|
||||
}
|
||||
|
||||
// Удаляем приложение
|
||||
Capital::Appendix::delete_appendix(coopname, appendix->id);
|
||||
}
|
||||
+2
-3
@@ -100,8 +100,7 @@ void capital::regcontrib(eosio::name coopname, eosio::name username, checksum256
|
||||
}
|
||||
|
||||
// Открываем кошельки для пайщика если необходимо
|
||||
auto program_wallet = get_program_wallet(coopname, username, _source_program);
|
||||
if (!program_wallet.has_value()) {
|
||||
if (!has_program_wallet(coopname, username, _source_program)) {
|
||||
Action::send<openprogwall_interface>(
|
||||
_soviet,
|
||||
Names::External::OPEN_PROGRAM_WALLET,
|
||||
@@ -113,7 +112,7 @@ void capital::regcontrib(eosio::name coopname, eosio::name username, checksum256
|
||||
);
|
||||
}
|
||||
|
||||
if (!get_program_wallet(coopname, username, _capital_program).has_value()) {
|
||||
if (!has_program_wallet(coopname, username, _capital_program)) {
|
||||
Action::send<openprogwall_interface>(
|
||||
_soviet,
|
||||
Names::External::OPEN_PROGRAM_WALLET,
|
||||
|
||||
@@ -26,20 +26,16 @@ namespace Capital {
|
||||
|
||||
namespace Wallets {
|
||||
|
||||
inline std::optional<progwallet> get_program_capital_wallet(eosio::name coopname, eosio::name username) {
|
||||
|
||||
/**
|
||||
* Открыт ли у пайщика программный кошелёк ЦПП «Благорост»
|
||||
* (есть подписанное программное соглашение в `wallet::users.programs[]`).
|
||||
* Балансы читаются отдельно через ledger2::userwallets.
|
||||
*/
|
||||
inline bool has_program_capital_wallet(eosio::name coopname, eosio::name username) {
|
||||
auto program_id = get_program_id(_capital_program);
|
||||
|
||||
auto program = get_program_or_fail(coopname, program_id);
|
||||
|
||||
auto capital_wallet = get_program_wallet(coopname, username, _capital_program);
|
||||
|
||||
if (!capital_wallet.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return capital_wallet.value();
|
||||
|
||||
// Программа должна существовать в реестре кооператива.
|
||||
get_program_or_fail(coopname, program_id);
|
||||
return has_program_wallet(coopname, username, _capital_program);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -196,12 +196,12 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
{ operations::capital::INVEST, processes::capital::INVEST, WalletOp::TRANSFER,
|
||||
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::BLAGOROST_FUND,
|
||||
0, 0,
|
||||
"Инвестиция в ЦПП «Благорост» (перенос между кошельками паевого фонда)" },
|
||||
"Инвестиция в ЦПП «Благорост»" },
|
||||
|
||||
// 7. Коммит РИД: Dr 08 / Cr 80, ISSUE GENERATOR_FUND (ADR-009: единый кошелёк программы Генератор)
|
||||
{ operations::capital::COMMIT_RID, processes::capital::RID, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::GENERATOR_FUND,
|
||||
ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::SHARE_FUND,
|
||||
"Коммит результата интеллектуальной деятельности по программе «Благорост»" },
|
||||
"Коммит результата интеллектуальной деятельности по программе «Генератор»" },
|
||||
|
||||
// 8. Приём РИД в НМА: Dr 04 / Cr 08, NONE — кошелёк остаётся на GENERATOR_FUND.
|
||||
// Семантика: подписан акт-2, РИД принят как НМА (закрылся 08-й). Перемещение
|
||||
|
||||
@@ -9,8 +9,16 @@
|
||||
using namespace eosio;
|
||||
|
||||
/**
|
||||
* Логика программ пайщика и кошелька (совет).
|
||||
* Таблицы programs / progwallets / agreements — из domain/index.hpp (подключать раньше).
|
||||
* Логика программ пайщика и кошелька.
|
||||
*
|
||||
* Источник правды по «участник ЦПП X» = `wallet::users[username].programs[]`
|
||||
* (Эпик 2). signagree добавляет запись в этот вектор, revokeagree — удаляет;
|
||||
* status="declined" в агриментах при ревоке не нужен — записи просто нет.
|
||||
*
|
||||
* Таблицы из domain/index.hpp:
|
||||
* - soviet::programs — реестр программ кооператива.
|
||||
* - soviet::coagreements — мапа program_type → program_id (per-coop).
|
||||
* - wallet::users — подписанные программные соглашения пайщика.
|
||||
*/
|
||||
|
||||
program get_program_or_fail(eosio::name coopname, uint64_t program_id) {
|
||||
@@ -22,57 +30,33 @@ program get_program_or_fail(eosio::name coopname, uint64_t program_id) {
|
||||
return program(*program_itr);
|
||||
}
|
||||
|
||||
inline bool has_signed_program_agreement(eosio::name coopname, eosio::name username, uint64_t program_id) {
|
||||
WalletTables::users_index users(_wallet, coopname.value);
|
||||
auto user_it = users.find(username.value);
|
||||
if (user_it == users.end()) return false;
|
||||
for (const auto &p : user_it->programs) {
|
||||
if (p.program_id == program_id) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_participant_of_cpp_by_program_id(eosio::name coopname, eosio::name username, uint64_t program_id) {
|
||||
progwallets_index progwallets(_soviet, coopname.value);
|
||||
|
||||
auto wallets_by_username_and_program = progwallets.template get_index<"byuserprog"_n>();
|
||||
auto username_and_program_index = combine_ids(username.value, program_id);
|
||||
auto wallet = wallets_by_username_and_program.find(username_and_program_index);
|
||||
|
||||
if (wallet == wallets_by_username_and_program.end())
|
||||
return false;
|
||||
|
||||
auto program_row = get_program_or_fail(coopname, program_id);
|
||||
|
||||
agreements2_index agreements(_soviet, coopname.value);
|
||||
auto agreement_row = agreements.find(wallet->agreement_id);
|
||||
|
||||
if (agreement_row->status == "declined"_n)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
return has_signed_program_agreement(coopname, username, program_id);
|
||||
}
|
||||
|
||||
bool is_valid_participant_of_program_by_type(eosio::name coopname, eosio::name username,
|
||||
eosio::name program_type) {
|
||||
programs_index programs(_soviet, coopname.value);
|
||||
progwallets_index progwallets(_soviet, coopname.value);
|
||||
|
||||
coagreements_index coagreements(_soviet, coopname.value);
|
||||
auto coagreement_row = coagreements.find(program_type.value);
|
||||
|
||||
if (coagreement_row == coagreements.end())
|
||||
return false;
|
||||
|
||||
programs_index programs(_soviet, coopname.value);
|
||||
auto exist = programs.find(coagreement_row->program_id);
|
||||
|
||||
if (exist == programs.end())
|
||||
return false;
|
||||
|
||||
auto wallets_by_username_and_program = progwallets.template get_index<"byuserprog"_n>();
|
||||
auto username_and_program_index = combine_ids(username.value, exist->id);
|
||||
auto wallet = wallets_by_username_and_program.find(username_and_program_index);
|
||||
|
||||
if (wallet == wallets_by_username_and_program.end())
|
||||
return false;
|
||||
|
||||
agreements2_index agreements(_soviet, coopname.value);
|
||||
auto agreement_row = agreements.find(wallet->agreement_id);
|
||||
|
||||
if (agreement_row->status == "declined"_n)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
return has_signed_program_agreement(coopname, username, exist->id);
|
||||
}
|
||||
|
||||
struct ProgramInfo {
|
||||
@@ -102,18 +86,14 @@ inline uint64_t get_draft_id(const eosio::name &type) {
|
||||
return it->second.draft_id;
|
||||
}
|
||||
|
||||
std::optional<progwallet> get_program_wallet(eosio::name coopname, eosio::name username, eosio::name type) {
|
||||
auto program_id = get_program_id(type);
|
||||
|
||||
progwallets_index progwallets(_soviet, coopname.value);
|
||||
|
||||
auto balances_by_username_and_program = progwallets.template get_index<"byuserprog"_n>();
|
||||
auto username_and_program_index = combine_ids(username.value, program_id);
|
||||
auto wallet = balances_by_username_and_program.find(username_and_program_index);
|
||||
|
||||
if (wallet == balances_by_username_and_program.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return *wallet;
|
||||
/**
|
||||
* Открыт ли у пайщика программный кошелёк по типу программы.
|
||||
*
|
||||
* Источник — `wallet::users[username].programs[]`: пайщик считается «открывшим»
|
||||
* программу, если у него есть подписанное программное соглашение нужного
|
||||
* `program_id`. Балансы — отдельно через ledger2::userwallets, эта функция
|
||||
* только про факт открытия.
|
||||
*/
|
||||
bool has_program_wallet(eosio::name coopname, eosio::name username, eosio::name type) {
|
||||
return has_signed_program_agreement(coopname, username, get_program_id(type));
|
||||
}
|
||||
|
||||
@@ -14,19 +14,23 @@
|
||||
void soviet::openprogwall(name coopname, name username, name program_type, uint64_t agreement_id) {
|
||||
name payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
|
||||
|
||||
auto program_wallet = get_program_wallet(coopname, username, program_type);
|
||||
|
||||
if (!program_wallet.has_value()) {
|
||||
auto program_id = get_program_id(program_type);
|
||||
|
||||
// Идемпотентность по самой таблице progwallets (легаси-учёт): не плодим
|
||||
// дубликаты, если запись уже существует. Проверка по «byuserprog» оставлена
|
||||
// здесь локально — фазаут progwallets ещё не завершён, общий помощник в
|
||||
// lib/core/programs.hpp переехал на wallet::users (другая семантика).
|
||||
progwallets_index progwallets(_soviet, coopname.value);
|
||||
auto by_user_program = progwallets.template get_index<"byuserprog"_n>();
|
||||
auto wallet_idx_it = by_user_program.find(combine_ids(username.value, program_id));
|
||||
if (wallet_idx_it != by_user_program.end()) return;
|
||||
|
||||
if (agreement_id > 0) {
|
||||
agreements2_index agreements(_soviet, coopname.value);
|
||||
auto agreement = agreements.find(agreement_id);
|
||||
check(agreement != agreements.end(), "Соглашение не найдено");
|
||||
check(agreement -> username == username, "Неверный пользователь соглашения и кошелька");
|
||||
};
|
||||
|
||||
progwallets_index progwallets(_soviet, coopname.value);
|
||||
check(agreement->username == username, "Неверный пользователь соглашения и кошелька");
|
||||
}
|
||||
|
||||
//TODO: make payer coopname
|
||||
progwallets.emplace(_soviet, [&](auto &b) {
|
||||
@@ -39,6 +43,4 @@ void soviet::openprogwall(name coopname, name username, name program_type, uint6
|
||||
b.blocked = asset(0, _root_govern_symbol);
|
||||
b.membership_contribution = asset(0, _root_govern_symbol);
|
||||
});
|
||||
|
||||
};
|
||||
};
|
||||
+166
-920
File diff suppressed because it is too large
Load Diff
+54
@@ -0,0 +1,54 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Ledger2Contract } from 'cooptypes';
|
||||
import config from '~/config/config';
|
||||
import type { IDelta } from '~/types/common';
|
||||
import { ProgramShareRegistrationService } from '../services/program-share-registration.service';
|
||||
|
||||
const BLAGOROST_WALLET_NAME = 'w.cap.blago';
|
||||
|
||||
/**
|
||||
* Слушатель дельт `ledger2::userwallets`. При изменении баланса пайщика на
|
||||
* программном кошельке «Благорост» (`w.cap.blago`) запускает точечную
|
||||
* синхронизацию долей по всем pending|active проектам пайщика. Это закрывает
|
||||
* окно между поллингом scheduler'а (по умолчанию 1440 мин) и быстрыми
|
||||
* пользовательскими сценариями: invest → commit → result.
|
||||
*
|
||||
* `present=false` (запись стёрта при обнулении баланса, Эпик 3 §6) пока не
|
||||
* обрабатываем: scheduler в итоге сверит и обнулит долю; обработка отдельной
|
||||
* action regshare с user_shares=0 — отдельная задача.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ProgramShareRegistrationOnUserWalletDeltaListener {
|
||||
private readonly logger = new Logger(ProgramShareRegistrationOnUserWalletDeltaListener.name);
|
||||
|
||||
constructor(
|
||||
private readonly programShareRegistrationService: ProgramShareRegistrationService
|
||||
) {}
|
||||
|
||||
@OnEvent(`delta::${Ledger2Contract.contractName.production}::${Ledger2Contract.Tables.UserWallets.tableName}`)
|
||||
async handleUserWalletDelta(delta: IDelta): Promise<void> {
|
||||
if (!delta.present) return;
|
||||
if (delta.scope !== config.coopname) return;
|
||||
|
||||
const value = delta.value as Ledger2Contract.Tables.UserWallets.IUserWallet | undefined;
|
||||
if (!value) return;
|
||||
if (String(value.wallet_name) !== BLAGOROST_WALLET_NAME) return;
|
||||
if (!value.username) return;
|
||||
|
||||
const username = String(value.username);
|
||||
try {
|
||||
await this.programShareRegistrationService.syncProgramSharesForUser(
|
||||
delta.scope,
|
||||
username
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const stack = error instanceof Error ? error.stack : undefined;
|
||||
this.logger.warn(
|
||||
`regshare event-driven sync не выполнен: coop=${delta.scope} user=${username}: ${message}`,
|
||||
stack
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
-25
@@ -13,7 +13,9 @@ import {
|
||||
} from '../../domain/repositories/project.repository';
|
||||
import { ContributorStatus } from '../../domain/enums/contributor-status.enum';
|
||||
import { ProjectStatus } from '../../domain/enums/project-status.enum';
|
||||
import { WALLET_BLOCKCHAIN_PORT, type WalletBlockchainPort } from '~/domain/wallet/ports/wallet-blockchain.port';
|
||||
import type { ContributorDomainEntity } from '../../domain/entities/contributor.entity';
|
||||
import type { ProjectDomainEntity } from '../../domain/entities/project.entity';
|
||||
import { WALLET_DOMAIN_PORT, type WalletDomainPort } from '~/domain/wallet/ports/wallet-domain.port';
|
||||
import { getProgramId, ProgramType } from '~/domain/wallet/enums/program-type.enum';
|
||||
import { AssetUtils } from '~/shared/utils/asset.utils';
|
||||
import { HttpApiError } from '~/utils/httpApiError';
|
||||
@@ -40,15 +42,19 @@ export class ProgramShareRegistrationService {
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
@Inject(CAPITAL_BLOCKCHAIN_PORT)
|
||||
private readonly capitalBlockchainPort: CapitalBlockchainPort,
|
||||
@Inject(WALLET_BLOCKCHAIN_PORT)
|
||||
private readonly walletBlockchainPort: WalletBlockchainPort
|
||||
@Inject(WALLET_DOMAIN_PORT)
|
||||
private readonly walletDomainPort: WalletDomainPort
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Обход участников в статусах active/import, проектов pending/active; при изменении user_shares относительно capital_contributor_shares — regshare.
|
||||
*/
|
||||
async syncProgramSharesForCoop(coopname: string): Promise<void> {
|
||||
const programId = getProgramId(ProgramType.BLAGOROST);
|
||||
const projects = await this.findActiveProjects(coopname);
|
||||
if (projects.length === 0) {
|
||||
this.logger.debug(`Синхронизация regshare: нет проектов в статусах pending/active для ${coopname}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contributors = (await this.contributorRepository.findAll()).filter(
|
||||
(c) =>
|
||||
@@ -56,27 +62,53 @@ export class ProgramShareRegistrationService {
|
||||
(c.status === ContributorStatus.ACTIVE || c.status === ContributorStatus.IMPORT)
|
||||
);
|
||||
|
||||
const projects = (await this.projectRepository.findAll()).filter(
|
||||
for (const contributor of contributors) {
|
||||
await this.syncContributor(coopname, contributor, projects);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Точечная синхронизация regshare для одного пайщика — вызывается из listener'а
|
||||
* на дельты `ledger2::userwallets[w.cap.blago]`. Не пишет лог, если у пайщика
|
||||
* нет ни одного pending/active проекта.
|
||||
*/
|
||||
async syncProgramSharesForUser(coopname: string, username: string): Promise<void> {
|
||||
const projects = await this.findActiveProjects(coopname);
|
||||
if (projects.length === 0) return;
|
||||
|
||||
const contributor = (await this.contributorRepository.findAll()).find(
|
||||
(c) =>
|
||||
c.coopname === coopname &&
|
||||
c.username === username &&
|
||||
(c.status === ContributorStatus.ACTIVE || c.status === ContributorStatus.IMPORT)
|
||||
);
|
||||
if (!contributor) return;
|
||||
|
||||
await this.syncContributor(coopname, contributor, projects);
|
||||
}
|
||||
|
||||
private async findActiveProjects(coopname: string): Promise<ProjectDomainEntity[]> {
|
||||
return (await this.projectRepository.findAll()).filter(
|
||||
(p) =>
|
||||
p.coopname === coopname &&
|
||||
(p.status === ProjectStatus.PENDING || p.status === ProjectStatus.ACTIVE)
|
||||
);
|
||||
|
||||
if (projects.length === 0) {
|
||||
this.logger.debug(`Синхронизация regshare: нет проектов в статусах pending/active для ${coopname}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const contributor of contributors) {
|
||||
const wallet = await this.walletBlockchainPort.getProgramWallet(
|
||||
private async syncContributor(
|
||||
coopname: string,
|
||||
contributor: ContributorDomainEntity,
|
||||
projects: ProjectDomainEntity[]
|
||||
): Promise<void> {
|
||||
const programId = getProgramId(ProgramType.BLAGOROST);
|
||||
|
||||
const wallet = await this.walletDomainPort.getProgramWallet({
|
||||
coopname,
|
||||
contributor.username,
|
||||
programId
|
||||
);
|
||||
username: contributor.username,
|
||||
program_id: programId,
|
||||
});
|
||||
|
||||
if (!wallet) {
|
||||
continue;
|
||||
}
|
||||
if (!wallet || !wallet.available || !wallet.blocked) return;
|
||||
|
||||
let targetShares: string;
|
||||
try {
|
||||
@@ -86,13 +118,11 @@ export class ProgramShareRegistrationService {
|
||||
this.logger.warn(
|
||||
`Синхронизация regshare: не удалось сложить балансы кошелька ${contributor.username}: ${message}`
|
||||
);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
const targetParsed = AssetUtils.parseAsset(targetShares);
|
||||
if (!targetParsed.symbol) {
|
||||
continue;
|
||||
}
|
||||
if (!targetParsed.symbol) return;
|
||||
|
||||
for (const project of projects) {
|
||||
const segment = await this.capitalBlockchainPort.getSegmentByProjectUser(
|
||||
@@ -104,9 +134,7 @@ export class ProgramShareRegistrationService {
|
||||
const registeredStr =
|
||||
segment?.capital_contributor_shares ?? AssetUtils.formatAsset(0, targetParsed.symbol);
|
||||
|
||||
if (this.sameAssetAmount(registeredStr, targetShares)) {
|
||||
continue;
|
||||
}
|
||||
if (this.sameAssetAmount(registeredStr, targetShares)) continue;
|
||||
|
||||
try {
|
||||
await this.capitalBlockchainPort.registerShare({
|
||||
@@ -129,7 +157,6 @@ export class ProgramShareRegistrationService {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sameAssetAmount(a: string, b: string): boolean {
|
||||
const pa = AssetUtils.parseAsset(a);
|
||||
|
||||
@@ -300,6 +300,7 @@ import { GitHubService } from './infrastructure/services/github.service';
|
||||
import { GitHubSyncSchedulerService } from './infrastructure/services/github-sync-scheduler.service';
|
||||
import { ProgramShareRegistrationSchedulerService } from './infrastructure/services/program-share-registration-scheduler.service';
|
||||
import { ProgramShareRegistrationService } from './application/services/program-share-registration.service';
|
||||
import { ProgramShareRegistrationOnUserWalletDeltaListener } from './application/listeners/program-share-registration-on-user-wallet-delta.listener';
|
||||
import { CapitalDevelopmentRepositoryGitSyncService } from './application/services/capital-development-repository-git-sync.service';
|
||||
import { CapitalGithubExtensionLifecycleListener } from './application/listeners/capital-github-extension-lifecycle.listener';
|
||||
import { GitCommitMarkersSyncService } from './application/services/git-commit-markers-sync.service';
|
||||
@@ -824,6 +825,7 @@ IssueIdGenerationService,
|
||||
GitHubSyncSchedulerService,
|
||||
ProgramShareRegistrationService,
|
||||
ProgramShareRegistrationSchedulerService,
|
||||
ProgramShareRegistrationOnUserWalletDeltaListener,
|
||||
GitCommitMarkersSyncService,
|
||||
{
|
||||
provide: ISSUE_LINKED_GIT_COMMIT_REPOSITORY,
|
||||
|
||||
+37
-9
@@ -20,6 +20,20 @@ import type {
|
||||
|
||||
const LEDGER2_CODE = Ledger2Contract.contractName.production;
|
||||
|
||||
/**
|
||||
* Сохранить symbol/precision исходного актива, но занулить amount.
|
||||
* Используется при present=false — запись удалена в чейне (`cleanup_l2_if_empty`
|
||||
* / `cleanup_l3_if_empty`), но в реестре оставляем строку с нулями, чтобы
|
||||
* пользователь мог раскрыть и посмотреть историю движений.
|
||||
*/
|
||||
function zeroAssetLike(s: unknown): string {
|
||||
const str = typeof s === 'string' ? s : '';
|
||||
const [num = '0.0000', symbol = 'RUB'] = str.trim().split(/\s+/);
|
||||
const dotIdx = num.indexOf('.');
|
||||
const decimals = dotIdx === -1 ? 4 : num.length - dotIdx - 1;
|
||||
return `${(0).toFixed(decimals)} ${symbol}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Чтение состояния ledger2 из Postgres-таблиц блокчейн-синка.
|
||||
*
|
||||
@@ -41,41 +55,55 @@ export class TypeOrmLedger2StateRepository implements Ledger2StatePort {
|
||||
) {}
|
||||
|
||||
async getAccounts(coopname: string): Promise<Ledger2AccountDomainInterface[]> {
|
||||
// Берём самую свежую row на ключ (DISTINCT ON по block_num DESC) вместе
|
||||
// с present-флагом. Удалённые в чейне записи (present=false) **не**
|
||||
// отбрасываем: оставляем строку в реестре с обнулёнными суммами и
|
||||
// именем/id из last_snapshot — у такой записи может быть история
|
||||
// (debit/credit), которую пользователь хочет раскрыть.
|
||||
const rows = await this.deltaRepo.manager.query(
|
||||
`SELECT DISTINCT ON (primary_key) value
|
||||
`SELECT DISTINCT ON (primary_key) value, present
|
||||
FROM blockchain_deltas
|
||||
WHERE code = $1 AND "table" = '${Ledger2Contract.Tables.Accounts.tableName}' AND scope = $2
|
||||
ORDER BY primary_key, block_num DESC, created_at DESC`,
|
||||
[LEDGER2_CODE, coopname],
|
||||
);
|
||||
return (rows as Array<{ value: Record<string, unknown> }>).map((row) => {
|
||||
return (rows as Array<{ value: Record<string, unknown>; present: boolean }>).map((row) => {
|
||||
const v = row.value ?? {};
|
||||
const live = row.present === true;
|
||||
return {
|
||||
id: parseInt(String(v.id ?? 0), 10),
|
||||
name: String(v.name ?? ''),
|
||||
balance: String(v.balance ?? '0.0000 RUB'),
|
||||
debitBalance: String(v.debit_balance ?? '0.0000 RUB'),
|
||||
creditBalance: String(v.credit_balance ?? '0.0000 RUB'),
|
||||
balance: live ? String(v.balance ?? '0.0000 RUB') : zeroAssetLike(v.balance),
|
||||
debitBalance: live ? String(v.debit_balance ?? '0.0000 RUB') : zeroAssetLike(v.debit_balance),
|
||||
creditBalance: live ? String(v.credit_balance ?? '0.0000 RUB') : zeroAssetLike(v.credit_balance),
|
||||
accountType: typeof v.account_type === 'number' ? v.account_type : Number(v.account_type ?? 0),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getWallets(coopname: string): Promise<Ledger2WalletDomainInterface[]> {
|
||||
// Контракт удаляет L2-запись (`cleanup_l2_if_empty`) при обнулении баланса.
|
||||
// Parser2 кладёт delete как row с present=false и value=last_snapshot.
|
||||
// В реестре кошельков такую запись **оставляем** (с available=0/blocked=0
|
||||
// и id/name из snapshot), чтобы пользователь мог раскрыть и посмотреть
|
||||
// историю движений по кошельку. WHERE present=true тут НЕЛЬЗЯ: фильтр
|
||||
// выкинет delete-row, и DISTINCT ON выберет предыдущую present=true со
|
||||
// старым снимком — кошелёк продолжал бы светиться с прежним балансом.
|
||||
const rows = await this.deltaRepo.manager.query(
|
||||
`SELECT DISTINCT ON (primary_key) value
|
||||
`SELECT DISTINCT ON (primary_key) value, present
|
||||
FROM blockchain_deltas
|
||||
WHERE code = $1 AND "table" = '${Ledger2Contract.Tables.Wallets.tableName}' AND scope = $2
|
||||
ORDER BY primary_key, block_num DESC, created_at DESC`,
|
||||
[LEDGER2_CODE, coopname],
|
||||
);
|
||||
return (rows as Array<{ value: Record<string, unknown> }>).map((row) => {
|
||||
return (rows as Array<{ value: Record<string, unknown>; present: boolean }>).map((row) => {
|
||||
const v = row.value ?? {};
|
||||
const live = row.present === true;
|
||||
return {
|
||||
id: String(v.id ?? ''),
|
||||
name: String(v.name ?? ''),
|
||||
available: String(v.available ?? '0.0000 RUB'),
|
||||
blocked: String(v.blocked ?? '0.0000 RUB'),
|
||||
available: live ? String(v.available ?? '0.0000 RUB') : zeroAssetLike(v.available),
|
||||
blocked: live ? String(v.blocked ?? '0.0000 RUB') : zeroAssetLike(v.blocked),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,12 +97,12 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
|
||||
{ code: 'o.cap.invest', process_type: 'p.cap.invest', contract: 'capital',
|
||||
name: 'INVEST', wallet_op: 'TRANSFER', wallet_from: 'w.wal.share', wallet_to: 'w.cap.blago',
|
||||
debit: null, credit: null,
|
||||
human_name: 'Инвестиция в ЦПП «Благорост» (перенос между кошельками)' },
|
||||
human_name: 'Инвестиция в ЦПП «Благорост»' },
|
||||
|
||||
{ code: 'o.cap.commit', process_type: 'p.cap.rid', contract: 'capital',
|
||||
name: 'COMMIT_RID', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.gen',
|
||||
debit: 8, credit: 80,
|
||||
human_name: 'Коммит РИД по программе «Благорост»' },
|
||||
human_name: 'Коммит РИД по программе «Генератор»' },
|
||||
|
||||
{ code: 'o.cap.accept', process_type: 'p.cap.rid', contract: 'capital',
|
||||
name: 'ACCEPT_RID', wallet_op: 'NONE', wallet_from: null, wallet_to: null,
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "browser-harness",
|
||||
"name": "@coopenomics/docs-harness",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "browser-harness",
|
||||
"name": "@coopenomics/docs-harness",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"local": "046_create_drafts.ts",
|
||||
"local": "049_migrate_phase2_progwallets.ts",
|
||||
"test": "046_create_drafts.ts",
|
||||
"prod": "046_create_drafts.ts"
|
||||
}
|
||||
Reference in New Issue
Block a user