[@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:
coopops
2026-05-09 12:12:10 +00:00
parent 88b285448a
commit a745e798a4
18 changed files with 456 additions and 1101 deletions
+1 -1
View File
@@ -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);
@@ -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);
@@ -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);
}
@@ -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-й). Перемещение
+33 -53
View File
@@ -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);
});
};
};
File diff suppressed because it is too large Load Diff
@@ -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
);
}
}
}
@@ -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,
@@ -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
View File
@@ -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 -1
View File
@@ -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"
}