[@ant] feat(ledger2): финальный реестр кошельков, WalletKind и упрощённый WalletOp

Super-story 1.1+1.2+1.3 эпика «L3 в ledger2» — атомарный релиз без коротких
итераций (ADR-009, ADR-010, ADR-002, ADR-003).

Реестр кошельков (cooptypes/ledger2/wallets.ts, contracts/lib/core/ledger2/wallets.hpp):
  • Финальный набор: 5 USER_SHARED + 6 COOPERATIVE = 11 кошельков
  • Введён WalletKind (USER_SHARED / COOPERATIVE) — обязателен для L3-разреза
  • USER_SHARED: w.reg.minshr, w.wal.share, w.wal.member, w.cap.blago, w.cap.gen
  • COOPERATIVE: w.reg.entry, w.wal.wthdrw, w.sov.infra, w.sov.delgte,
                 w.cap.loan, w.mkt.payout
  • Compile-time static_assert user_shared_naming_convention (base32 префикс w.)
  • Удалён w.wal.sharid (РИД-часть на legacy 80 — не существует ни у кого)
  • Унифицированы программы Благорост/Генератор: w.cap.blago и w.cap.gen
    взамен w.cap.bginv/w.cap.gncom/w.cap.bgrid

WalletOp cleanup (operations.hpp / operations.ts):
  • 5 значений: ISSUE / TRANSFER / BLOCK / UNBLOCK / BURN
  • WALLET_ONLY удалён — TRANSFER без бухпроводок маркируется
    (debit==null && credit==null) на уровне OPERATION_REGISTRY
  • REVOKE удалён — BURN покрывает оба сценария (штатное сжигание и mirror revert,
    различие через operation_code o.adj.rev)
  • static_asserts: zero_accounts_iff_both, burn_pattern_correct
  • Удалена операция o.mig.rid вместе с РИД-веткой migrate.cpp
  • migrate.cpp теперь жёстко проверяет legacy::accounts[80] == money-часть
    с явным сообщением «закрыть РИД до миграции» (страховка)

Capital — единые программные кошельки (ADR-009):
  • 5 операций o.cap.* переведены на w.cap.blago / w.cap.gen
  • Стандарты p.cap.invest, p.cap.prop, p.cap.rid обновлены под унифицированные имена
  • o.cap.invest: TRANSFER без бухпроводок (раньше WALLET_ONLY)
  • Константы BLAGOROST_INVEST/RID → BLAGOROST_FUND, GENERATOR_COMMIT → GENERATOR_FUND

Controller / cooptypes:
  • resolveWalletAccountId переведён на (debit==null && credit==null)
  • IRevert.mirror_wallet_op: 4=BURN (вместо 5=REVOKE)
  • Doc-комментарии обновлены под новые имена

Standards-site / desktop UI:
  • WalletOp type без WALLET_ONLY/с BURN
  • Граф/FocusBar: hasPosting и условия отображения переводов упрощены
  • WalletTransferDialog: resolveAccountId fallback по (debit==null && credit==null)

Boot тесты:
  • capital.test.ts: WALLET_GENERATOR_FUND / WALLET_BLAGOROST_FUND
  • ledger2-migrate.test.ts: SHARE_FUND_RID удалён из ассертов
  • walmove.test.ts: комментарии под новые имена

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
coopops
2026-05-05 06:27:38 +00:00
parent 244ff15e19
commit 44305a5b3f
27 changed files with 448 additions and 347 deletions
+28 -28
View File
@@ -69,17 +69,17 @@ let totalToCapitalConvertAmount = 0
let totalToProjectConvertAmount = 0
// Baseline ledger2-кошельков перед первым approvecmmt компонента-проекта.
// Нужен для инвариант-проверки «w.cap.gncom (ЦПП «Генератор» — коммит) проекта
// Нужен для инвариант-проверки «w.cap.gen (ЦПП «Генератор» — единый кошелёк программы) проекта
// закрылся в ноль» после полного цикла commits→voting→pushResult→signact2→
// convertsegm: approvecmmt(commit) кладёт на w.cap.gncom commit.amounts.
// convertsegm: approvecmmt(commit) кладёт на w.cap.gen commit.amounts.
// total_contribution, signact2(сегмента) забирает segment.available_for_program.
// Сумма выдач и приходов проекта обязана совпадать — иначе либо коммит попал
// на w.cap.gncom неправильной суммой, либо часть пайщиков не сконвертировала.
// на w.cap.gen неправильной суммой, либо часть пайщиков не сконвертировала.
//
// Имена кошельков — eosio::name (рефакт 2026-04-27). Старые numeric ID
// (10001/9002) больше не используются — wallets.id теперь string.
const WALLET_GENERATOR_COMMIT = 'w.cap.gncom'
const WALLET_BLAGOROST_RID = 'w.cap.bgrid'
// Имена кошельков — eosio::name. Финальный реестр (ADR-009): единые
// программные кошельки `w.cap.gen` и `w.cap.blago` вместо ранее декомпозированных.
const WALLET_GENERATOR_FUND = 'w.cap.gen'
const WALLET_BLAGOROST_FUND = 'w.cap.blago'
let generatorCommitBaselineRub = 0
let blagorostRidBaselineRub = 0
// Хранение хэшей долгов для каждого пользователя
@@ -656,21 +656,21 @@ describe('тест контракта CAPITAL', () => {
it('snapshot ledger2-кошельков перед первым approvecmmt компонента (baseline для инвариант-теста)', async () => {
// Берём baseline ровно перед первым approvecmmt componentProject:
// approvecmmt(tester1) ниже первым эмитит COMMIT_RID на w.cap.gncom
// (ЦПП «Генератор» — коммит). Финальный assert «w.cap.gncom проекта
// approvecmmt(tester1) ниже первым эмитит COMMIT_RID на w.cap.gen
// (ЦПП «Генератор» — единый кошелёк программы). Финальный assert «w.cap.gen проекта
// вернулся к baseline» сравнивает дельту по этому проекту, а не
// абсолютный остаток (на w.cap.gncom могут быть остатки property-
// абсолютный остаток (на w.cap.gen могут быть остатки property-
// операций или будущих компонентов в одном тесте).
const wallets = await blockchain.getTableRows('ledger2', 'voskhod', 'wallets', 500) as Array<{ id: string, available: string, blocked: string }>
const generator = wallets.find((w) => String(w.id) === WALLET_GENERATOR_COMMIT)
const blagorost = wallets.find((w) => String(w.id) === WALLET_BLAGOROST_RID)
const generator = wallets.find((w) => String(w.id) === WALLET_GENERATOR_FUND)
const blagorost = wallets.find((w) => String(w.id) === WALLET_BLAGOROST_FUND)
generatorCommitBaselineRub = generator
? parseFloat(generator.available.split(' ')[0]) + parseFloat(generator.blocked.split(' ')[0])
: 0
blagorostRidBaselineRub = blagorost
? parseFloat(blagorost.available.split(' ')[0]) + parseFloat(blagorost.blocked.split(' ')[0])
: 0
console.log(`📌 baseline ${WALLET_GENERATOR_COMMIT}=${generatorCommitBaselineRub} ${WALLET_BLAGOROST_RID}=${blagorostRidBaselineRub}`)
console.log(`📌 baseline ${WALLET_GENERATOR_FUND}=${generatorCommitBaselineRub} ${WALLET_BLAGOROST_FUND}=${blagorostRidBaselineRub}`)
})
it('добавить коммит создателя tester1 на 10 часов по 1000 RUB', async () => {
@@ -1803,7 +1803,7 @@ describe('тест контракта CAPITAL', () => {
console.log(`✅ Сегмент ${investor3} обработан`)
})
it('инвариант: w.cap.gncom (ЦПП «Генератор» — коммит) не уходит в дефицит после конвертации всех сегментов', async () => {
it('инвариант: w.cap.gen (ЦПП «Генератор» — единый кошелёк программы) не уходит в дефицит после конвертации всех сегментов', async () => {
// Σ COMMIT_RID(коммитов проекта) = Σ commit.amounts.total_contribution
// должна быть >= Σ ACCEPT_RID(сегментов) = Σ segment.available_for_program.
//
@@ -1812,42 +1812,42 @@ describe('тест контракта CAPITAL', () => {
// распределился по всем ролям. В сложных тестах (как этот) есть пограничные
// ситуации: investor3 со статусом 'skipped' → purgesegment без convertsegm,
// тестеры с is_contributor=0 не получают свою долю contributors_bonus.
// Их непокрытая часть остаётся на w.cap.gncom как избыток — это ожидаемо.
// Их непокрытая часть остаётся на w.cap.gen как избыток — это ожидаемо.
//
// **Жёсткий инвариант** — отсутствие дефицита (delta >= 0). Если он
// нарушен, значит approvecmmt кладёт на w.cap.gncom меньше, чем signact2
// нарушен, значит approvecmmt кладёт на w.cap.gen меньше, чем signact2
// забирает — это регрессия патча approvecmmt, и в проде на signact2
// последнего пайщика проекта возникает «недостаточно средств на кошельке
// w.cap.gncom». Защищает от ровно того бага, который ловили
// w.cap.gen». Защищает от ровно того бага, который ловили
// 2026-04-27 (delta_available вместо total_contribution).
const wallets = await blockchain.getTableRows('ledger2', 'voskhod', 'wallets', 500) as Array<{ id: string, available: string, blocked: string }>
const generator = wallets.find((w) => String(w.id) === WALLET_GENERATOR_COMMIT)
expect(generator, `wallet ${WALLET_GENERATOR_COMMIT} должен существовать`).toBeDefined()
const generator = wallets.find((w) => String(w.id) === WALLET_GENERATOR_FUND)
expect(generator, `wallet ${WALLET_GENERATOR_FUND} должен существовать`).toBeDefined()
const generatorTotalRub = parseFloat(generator!.available.split(' ')[0]) + parseFloat(generator!.blocked.split(' ')[0])
const generatorDelta = generatorTotalRub - generatorCommitBaselineRub
console.log(`🔍 ${WALLET_GENERATOR_COMMIT}: baseline=${generatorCommitBaselineRub} now=${generatorTotalRub} delta=${generatorDelta}`)
expect(generatorDelta, `${WALLET_GENERATOR_COMMIT} ушёл в дефицит (Σ COMMIT_RID < Σ ACCEPT_RID для проекта ${componentProject.project_hash.slice(0, 12)}…) — регрессия approvecmmt?`).toBeGreaterThanOrEqual(-0.0001)
console.log(`🔍 ${WALLET_GENERATOR_FUND}: baseline=${generatorCommitBaselineRub} now=${generatorTotalRub} delta=${generatorDelta}`)
expect(generatorDelta, `${WALLET_GENERATOR_FUND} ушёл в дефицит (Σ COMMIT_RID < Σ ACCEPT_RID для проекта ${componentProject.project_hash.slice(0, 12)}…) — регрессия approvecmmt?`).toBeGreaterThanOrEqual(-0.0001)
if (generatorDelta > 0.0001) {
console.log(`⚠️ ${WALLET_GENERATOR_COMMIT} остаток ${generatorDelta.toFixed(4)} RUB — это нераспределённые доли (skipped-сегменты, роли без is_contributor); ожидаемо для этого сценария.`)
console.log(`⚠️ ${WALLET_GENERATOR_FUND} остаток ${generatorDelta.toFixed(4)} RUB — это нераспределённые доли (skipped-сегменты, роли без is_contributor); ожидаемо для этого сценария.`)
}
// Дополнительно: w.cap.bgrid (BLAGOROST_RID) должен получить хоть какие-то
// Дополнительно: w.cap.blago (BLAGOROST_FUND) должен получить хоть какие-то
// средства (в проекте есть intellectual contribution, и хоть один сегмент
// дошёл до signact2). Точная сверка с total_contribution невозможна:
// signact2 параллельно вызывает REPAY (LOAN_ISSUED → SHARE_FUND_PAY)
// на debt_amount, который не идёт на w.cap.bgrid. Поэтому только sanity-чек,
// что путь ACCEPT_RID(w.cap.gncom → w.cap.bgrid) вообще работает.
// на debt_amount, который не идёт на w.cap.blago. Поэтому только sanity-чек,
// что путь ACCEPT_RID(w.cap.gen → w.cap.blago) вообще работает.
const project = await getProject(blockchain, 'voskhod', componentProject.project_hash)
const blagorost = wallets.find((w) => String(w.id) === WALLET_BLAGOROST_RID)
const blagorost = wallets.find((w) => String(w.id) === WALLET_BLAGOROST_FUND)
const blagorostTotalRub = blagorost
? parseFloat(blagorost.available.split(' ')[0]) + parseFloat(blagorost.blocked.split(' ')[0])
: 0
const blagorostDelta = blagorostTotalRub - blagorostRidBaselineRub
const expectedTotalContribution = parseFloat(String(project.fact.total_contribution).split(' ')[0])
console.log(`🔍 ${WALLET_BLAGOROST_RID} delta=${blagorostDelta}, project.fact.total_contribution=${expectedTotalContribution}`)
expect(blagorostDelta, `${WALLET_BLAGOROST_RID} не получил никаких средств — путь ACCEPT_RID не работает`).toBeGreaterThan(0)
console.log(`🔍 ${WALLET_BLAGOROST_FUND} delta=${blagorostDelta}, project.fact.total_contribution=${expectedTotalContribution}`)
expect(blagorostDelta, `${WALLET_BLAGOROST_FUND} не получил никаких средств — путь ACCEPT_RID не работает`).toBeGreaterThan(0)
})
it('удаляем компонент-проект', async () => {
@@ -13,13 +13,13 @@ import { LedgerAccountType } from './wallet/walletUtils'
//
// Семантика:
// TRANSIT_MIN_SHARE → Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND (w.reg.minshr)
// TRANSIT_BLAGOROST → Dr 51 / Cr 80, ISSUE BLAGOROST_INVEST (w.cap.bginv)
// TRANSIT_BLAGOROST → Dr 51 / Cr 80, ISSUE BLAGOROST_FUND (w.cap.blago)
// TRANSIT_SHARE → Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY (w.wal.share)
// TRANSIT_ENTRY → Dr 51 / Cr 86, ISSUE ENTRANCE_FEES (w.reg.entry)
// TRANSIT_COMMITMENT → Dr 08 / Cr 80, ISSUE GENERATOR_COMMIT (w.cap.gncom)
// TRANSIT_RID → Dr 04 / Cr 80, ISSUE SHARE_FUND_RID (w.wal.sharid)
// TRANSIT_COMMITMENT → Dr 08 / Cr 80, ISSUE GENERATOR_FUND (w.cap.gen)
//
// План счетов: 04, 08, 51, 58, 80, 86 (99 удалён как лишний транзит).
// РИД-перенос (w.wal.sharid + o.mig.rid) удалён в Story 1.1: legacy 80 без РИД-части (ADR-009).
// =============================================================================
const LEDGER = 'ledger'
@@ -35,7 +35,6 @@ const LEGACY_ACCOUNTS = {
const LEDGER2_WALLETS = {
SHARE_FUND_PAY: 'w.wal.share',
MIN_SHARE_FUND: 'w.reg.minshr',
SHARE_FUND_RID: 'w.wal.sharid',
ENTRANCE_FEES: 'w.reg.entry',
} as const
@@ -126,9 +125,9 @@ afterAll(() => {
console.log(`\n💾 TOTAL: ${(total / 1024).toFixed(2)} kb\n`)
})
describe('ledger2::migrate (пересмотр 2026-04-20: 6 TRANSIT_* без 99/CASH_MAIN)', () => {
describe('ledger2::migrate (пересмотр 2026-05-05: 3 TRANSIT_* без 99/CASH_MAIN/РИД)', () => {
// Типовой кейс «без РИД»: cash_legacy = entry_legacy + share_money.
// share_legacy = share_money (значит rid_share = 0 и TRANSIT_RID не сработает).
// share_legacy = share_money (legacy 80 без РИД-части — обязательное условие ADR-009).
const seedCash = 5_000 // → Dr 51 суммарно
const seedEntry = 1_500 // → Cr 86 (через TRANSIT_ENTRY)
const seedShare = seedCash - seedEntry // 3_500 → Cr 80 (распределится на MIN+SHARE, без РИД)
@@ -151,18 +150,16 @@ describe('ledger2::migrate (пересмотр 2026-04-20: 6 TRANSIT_* без 99
baselineShareAcc = parseAssetAmount((await getLedger2Account(LEDGER2_ACCOUNTS.SHARE_FUND))?.credit_balance)
baselineEntryAcc = parseAssetAmount((await getLedger2Account(LEDGER2_ACCOUNTS.TARGET_RECEIPTS))?.credit_balance)
// Baseline Σ wallets (w.wal.share + w.reg.minshr + w.reg.entry + w.wal.sharid): testnet-boot мог создать
// Baseline Σ wallets (w.wal.share + w.reg.minshr + w.reg.entry): testnet-boot мог создать
// начальные балансы для пайщиков. AC8 проверяет что seed добавил ровно
// seedCash сверху baseline.
const sharePayW = await getLedger2Wallet(LEDGER2_WALLETS.SHARE_FUND_PAY)
const minShareW = await getLedger2Wallet(LEDGER2_WALLETS.MIN_SHARE_FUND)
const entryW = await getLedger2Wallet(LEDGER2_WALLETS.ENTRANCE_FEES)
const ridW = await getLedger2Wallet(LEDGER2_WALLETS.SHARE_FUND_RID)
baselineWalletsTotal =
parseAssetAmount(sharePayW?.available) +
parseAssetAmount(minShareW?.available) +
parseAssetAmount(entryW?.available) +
parseAssetAmount(ridW?.available)
parseAssetAmount(entryW?.available)
await ledgerAdd(LEGACY_ACCOUNTS.BANK_ACCOUNT, rubAmount(seedCash), generateRandomSHA256(), 'ant')
await ledgerAdd(LEGACY_ACCOUNTS.SHARE_FUND, rubAmount(seedShare), generateRandomSHA256(), 'ant')
@@ -250,7 +247,7 @@ describe('ledger2::migrate (пересмотр 2026-04-20: 6 TRANSIT_* без 99
expect(legacyCashMain, 'wallet CASH_MAIN не должен существовать после migrate').toBeUndefined()
})
it('AC8: Σ wallets (w.wal.share + w.reg.minshr + w.reg.entry + w.wal.sharid) == baseline + seedCash (инвариант миграции)', async () => {
it('AC8: Σ wallets (w.wal.share + w.reg.minshr + w.reg.entry) == baseline + seedCash (инвариант миграции)', async () => {
if (migrateWasAlreadyDone) {
console.log('⏩ migrate был ранее — AC8 проверка Σ wallets пропущена (неизвестен baseline)')
return
@@ -258,17 +255,15 @@ describe('ledger2::migrate (пересмотр 2026-04-20: 6 TRANSIT_* без 99
const sharePayWallet = await getLedger2Wallet(LEDGER2_WALLETS.SHARE_FUND_PAY)
const minShareWallet = await getLedger2Wallet(LEDGER2_WALLETS.MIN_SHARE_FUND)
const entryWallet = await getLedger2Wallet(LEDGER2_WALLETS.ENTRANCE_FEES)
const ridWallet = await getLedger2Wallet(LEDGER2_WALLETS.SHARE_FUND_RID)
const totalWallets =
parseAssetAmount(sharePayWallet?.available) +
parseAssetAmount(minShareWallet?.available) +
parseAssetAmount(entryWallet?.available) +
parseAssetAmount(ridWallet?.available)
parseAssetAmount(entryWallet?.available)
// Инвариант: seed добавил ровно seedCash в Σ money-кошельков над baseline.
// Baseline снимался ДО seed (в первом it) — теперь baseline + seedCash.
// Благорост (w.cap.bginv) и Генератор (w.cap.gncom) в эту сумму не входят — они
// Благорост (w.cap.blago) и Генератор (w.cap.gen) в эту сумму не входят — они
// emplace-ятся отдельно из soviet::progwallets без участия в бух-проводках.
expect(totalWallets).toBeCloseTo(baselineWalletsTotal + seedCash, 4)
})
+2 -2
View File
@@ -9,7 +9,7 @@
* Проверяет:
* - mutation walmoveWallets отрабатывает успешно
* - balance кошельков `w.reg.minshr` и `w.wal.share` изменился на amount
* - account 80 (Cr SHARE_FUND) ≡ не изменился (WALLET_ONLY не трогает счета)
* - account 80 (Cr SHARE_FUND) ≡ не изменился (walmove не делает Dr/Cr inline-actions)
* - в getLedger2History появилась запись с operation_code='o.adj.walmove'
* - попытка перевода между кошельками разных счетов отбивается на backend
*/
@@ -104,7 +104,7 @@ describe('walmove (operation o.adj.walmove) — корректировка ме
expect(fromAfter).toBeCloseTo(fromBefore - moveAmount, 4)
expect(toAfter).toBeCloseTo(toBefore + moveAmount, 4)
// Главное свойство WALLET_ONLY: counterbalance бух.счёта не меняется.
// Главное свойство walmove (TRANSFER без Dr/Cr inline-actions, ADR-003): counterbalance бух.счёта не меняется.
expect(accountAfter).toBeCloseTo(accountBefore, 4)
// 5. Запись в реестре операций — parser отстаёт от head_block на 1-3с, ждём.
@@ -50,17 +50,17 @@ void capital::approvecmmt(eosio::name coopname, eosio::name master, checksum256
// Коммит РИД (Dr 08 / Cr 80) — возникает обязательство паевого взноса
// имуществом на полную стоимость одобренного коммита.
//
// На GENERATOR_COMMIT (10001) кладём ПОЛНУЮ стоимость коммита
// На GENERATOR_FUND (w.cap.gen) кладём ПОЛНУЮ стоимость коммита
// `commit.amounts.total_contribution` — это сумма всех начислений по
// коммиту, включая base/bonus создателей, base/bonus авторов, бонусы
// участников. Эта же сумма в сумме по всем коммитам проекта равна
// project.fact.total_contribution = sum(intellectual_cost) всех сегментов.
//
// Так signact2 (ACCEPT_RID) спокойно забирает с 10001 каждое
// Так signact2 (ACCEPT_RID) спокойно забирает с w.cap.gen каждое
// segment.available_for_program — независимо от того, как голосование
// перераспределило бонусы между сегментами: общая сумма не меняется.
// Инвариант: Σ COMMIT_RID(коммитов проекта) == Σ ACCEPT_RID(сегментов проекта)
// → GENERATOR_COMMIT закрывается в ноль на проекте, не на сегменте.
// → GENERATOR_FUND закрывается в ноль на проекте, не на сегменте.
//
// process_hash = project_hash (а не commit_hash), т.к. commit-entity
// удаляется ниже, а project_hash — долгоживущий якорь, под которым
@@ -54,8 +54,8 @@ void capital::signact2(eosio::name coopname, eosio::name chairman, checksum256 r
// 2) ACCEPT_RID (Dr 04 / Cr 08) — здесь, на полный накопленный
// `segment.available_for_program`. «Переносим с 08 на 04 когда РИД
// собран и подписан акт-2».
// Инвариант: Σ COMMIT_RID (по коммитам сегмента) == ACCEPT_RID → GENERATOR_COMMIT
// (10001) закрывается в ноль, 08-й счёт закрывается в ноль по этому сегменту.
// Инвариант: Σ COMMIT_RID (по коммитам сегмента) == ACCEPT_RID → GENERATOR_FUND
// (w.cap.gen) закрывается в ноль, 08-й счёт закрывается в ноль по этому сегменту.
if (segment.available_for_program.amount > 0) {
Wallet::add_blocked_funds(_capital, coopname, result -> username, segment.available_for_program, _source_program, memo);
@@ -1,11 +1,12 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Инвестиция в ЦПП «Благорост»» — перенос ранее внесённого паевого
# взноса с кошелька SHARE_FUND_PAY (w.wal.share) на инвестиционный кошелёк программы
# «Благорост» (w.cap.bginv) без бухгалтерских проводок.
# взноса с кошелька SHARE_FUND_PAY (w.wal.share) на единый кошелёк программы
# «Благорост» (w.cap.blago) без бухгалтерских проводок.
#
# Одноактовый процесс. В ledger2 — одна операция o.cap.invest c wallet_op =
# WALLET_ONLY: средства переходят между двумя аналитическими кошельками
# одного пайщика, не затрагивая бухгалтерские счета.
# TRANSFER без бухпроводок (debit_account_id == credit_account_id == 0):
# средства переходят между двумя аналитическими кошельками одного пайщика,
# не затрагивая бухгалтерские счета (оба связаны с Cr 80).
#
# Источники правды в коде:
# • cpp/capital/capital.hpp — actions
@@ -44,8 +45,8 @@ actions:
purpose: >
Закрывающее одношаговое действие: пайщик подписывает заявление об
инвестировании в «Благорост», контракт применяет ledger2-операцию
o.cap.invest и средства аналитически переходят с SHARE_FUND_PAY (w.wal.share)
на BLAGOROST_INVEST (w.cap.bginv).
o.cap.invest (TRANSFER без бухпроводок) и средства аналитически переходят
с SHARE_FUND_PAY (w.wal.share) на BLAGOROST_FUND (w.cap.blago).
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: capital::invest
@@ -57,7 +58,7 @@ states:
human: Инвестировано
description: >
Сумма перенесена с кошелька SHARE_FUND_PAY (w.wal.share) пайщика на
BLAGOROST_INVEST (w.cap.bginv). Бухгалтерия не затронута — это аналитический
BLAGOROST_FUND (w.cap.blago). Бухгалтерия не затронута — это аналитический
сдвиг внутри паевого фонда.
kind: final
@@ -85,15 +86,15 @@ scenario:
description: >
Пайщик подписывает заявление об инвестировании и вызывает
capital::createpinv. Контракт переводит указанную сумму с кошелька
SHARE_FUND_PAY (w.wal.share) на BLAGOROST_INVEST (w.cap.bginv) — операция
o.cap.invest, wallet_op WALLET_ONLY (без проводок). Запись в
реестре фиксирует факт инвестиции.
SHARE_FUND_PAY (w.wal.share) на BLAGOROST_FUND (w.cap.blago) — операция
o.cap.invest, wallet_op TRANSFER без бухпроводок (оба счёта 80).
Запись в реестре фиксирует факт инвестиции.
pre:
- Пайщик активен в кооперативе.
- Достаточный остаток на SHARE_FUND_PAY.
- Заявление подписано ЭЦП.
post:
- Сумма переведена w.wal.share → w.cap.bginv (аналитический сдвиг).
- Сумма переведена w.wal.share → w.cap.blago (аналитический сдвиг).
- В ledger2 применена операция o.cap.invest.
- Создана запись инвестиции в таблице.
@@ -111,9 +112,9 @@ documents:
operations:
- ledger_code: o.cap.invest
human_name: Инвестиция в ЦПП «Благорост» (перенос между кошельками)
wallet_op: WALLET_ONLY
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
wallet_to: w.cap.bginv # ЦПП «Благорост» — инвестиции деньгами
wallet_op: TRANSFER
wallet_from: w.wal.share # ЦК — паевая часть пайщика
wallet_to: w.cap.blago # ЦПП «Благорост» — единый кошелёк программы у пайщика
debit: null
credit: null
amount_ref: invest.quantity
@@ -4,7 +4,7 @@
#
# Шестиактовый процесс с двумя последовательными актами и одной операцией.
# В ledger2 — одна операция o.cap.actprp на закрывающем действии акта-2
# (ISSUE → w.cap.bgprop, Дт 51 / Кт 80).
# (ISSUE → w.cap.blago, Дт 51 / Кт 80).
#
# Источники правды в коде:
# • cpp/capital/capital.hpp — actions
@@ -70,7 +70,7 @@ actions:
role: closer
purpose: >
Председатель ставит вторую подпись на акте приёма-передачи — закрывающее
действие. Применяется ledger2-операция o.cap.actprp (ISSUE → w.cap.bgprop, Дт 51 / Кт 80).
действие. Применяется ledger2-операция o.cap.actprp (ISSUE → w.cap.blago, Дт 51 / Кт 80).
- name: capital::declinepgprp
human: Отклонить
actor: chairman
@@ -107,8 +107,8 @@ states:
human: Имущество принято
description: >
Председатель поставил вторую подпись на акте приёма-передачи: имущество учтено как паевой взнос на
кошельке «Благорост — имущественные паевые взносы» (w.cap.bgprop) с проводкой
Дт 51 / Кт 80.
кошельке «Благорост — единый кошелёк программы у пайщика» (w.cap.blago)
с проводкой Дт 51 / Кт 80.
kind: final
- name: removed
human: Отклонено
@@ -211,7 +211,7 @@ scenario:
description: >
Председатель ставит вторую подпись на акте приёма-передачи —
закрывающее действие. Применяется
ledger2-операция o.cap.actprp (ISSUE → w.cap.bgprop, Дт 51 / Кт 80) — имущество
ledger2-операция o.cap.actprp (ISSUE → w.cap.blago, Дт 51 / Кт 80) — имущество
учтено как паевой взнос.
alternatives:
@@ -255,14 +255,14 @@ operations:
human_name: Паевой взнос (имущественный) по программе «Благорост»
wallet_op: ISSUE
wallet_from: ''
wallet_to: w.cap.bgprop # ЦПП «Благорост» — имущественные паевые взносы
wallet_to: w.cap.blago # ЦПП «Благорост» — единый кошелёк программы у пайщика
debit: 51 # Расчётный счёт
credit: 80 # Паевой фонд (складочный капитал)
amount_ref: property.amount
triggered_by: capital::act2pgprp
description: >
Зачисление имущества (по сумме оценки) на кошелёк «Благорост —
имущественные паевые взносы» (w.cap.bgprop). Двойная запись Дт 51 / Кт 80 —
Зачисление имущества (по сумме оценки) на единый кошелёк программы
«Благорост» у пайщика (w.cap.blago). Двойная запись Дт 51 / Кт 80 —
кооператив принял имущество и оно учтено как часть паевого фонда.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
@@ -3,8 +3,8 @@
# участника проекта программы «Благорост» как имущественного паевого взноса.
#
# Многоэтапный процесс. Три ledger2-операции:
# • o.cap.commit — коммит РИД при одобрении (Дт 8 / Кт 80, кошелёк w.cap.gncom)
# • o.cap.accept — приём РИД в паевой фонд по акту-2 (TRANSFER w.cap.gncom → w.cap.bgrid, Дт 4 / Кт 8)
# • o.cap.commit — коммит РИД при одобрении (Дт 8 / Кт 80, кошелёк w.cap.gen)
# • o.cap.accept — приём РИД в паевой фонд по акту-2 (TRANSFER w.cap.gen → w.cap.blago, Дт 4 / Кт 8)
# • o.cap.repay — опционально, если у участника есть заём (см. p.cap.debt)
#
# Источники правды в коде:
@@ -59,8 +59,8 @@ actions:
role: progress
purpose: >
Мастер проекта одобряет коммит. На этом этапе применяется ledger2-
операция o.cap.commit — РИД зачисляется на «Генератор — принятый
коммит» (w.cap.gncom) с проводкой Дт 8 / Кт 80.
операция o.cap.commit — РИД зачисляется на единый кошелёк ЦПП
«Генератор» у пайщика (w.cap.gen) с проводкой Дт 8 / Кт 80.
- name: capital::declinecmmt
human: Отклонить коммит
@@ -142,8 +142,8 @@ states:
- name: commit_approved
human: Коммит одобрен
description: >
Мастер одобрил коммит. РИД зачислен на кошелёк «Генератор — принятый
коммит» (w.cap.gncom) с проводкой Дт 8 / Кт 80. Ожидается завершение
Мастер одобрил коммит. РИД зачислен на единый кошелёк ЦПП «Генератор»
у пайщика (w.cap.gen) с проводкой Дт 8 / Кт 80. Ожидается завершение
проекта и подача заявления о результате.
kind: normal
@@ -180,9 +180,10 @@ states:
human: РИД принят в паевой фонд
description: >
Председатель поставил вторую подпись на акте приёма-передачи: РИД
переведён с «Генератора» (w.cap.gncom) на «Благорост — принятые РИД»
(w.cap.bgrid), Дт 4 / Кт 8. Если у участника был заём проекта —
он закрылся (o.cap.repay). РИД считается имущественным паевым взносом.
переведён с единого кошелька ЦПП «Генератор» (w.cap.gen) на единый
кошелёк ЦПП «Благорост» у пайщика (w.cap.blago), Дт 4 / Кт 8. Если у
участника был заём проекта — он закрылся (o.cap.repay). РИД считается
имущественным паевым взносом.
kind: final
- name: removed
@@ -191,7 +192,7 @@ states:
Коммит отклонён мастером, либо результат отклонён советом. Запись
удалена, ledger2-операции по этому пути не создавались (o.cap.commit
сохраняется, если он уже сработал на этапе approvecmmt — учитывается
на w.cap.gncom до момента признания).
на w.cap.gen до момента признания).
kind: virtual
virtual: true
@@ -309,8 +310,8 @@ scenario:
action: capital::approvecmmt
description: >
Мастер проекта одобряет коммит. Применяется ledger2-операция
o.cap.commit — РИД зачисляется на «Генератор — принятый коммит»
(w.cap.gncom), Дт 8 / Кт 80.
o.cap.commit — РИД зачисляется на единый кошелёк ЦПП «Генератор»
у пайщика (w.cap.gen), Дт 8 / Кт 80.
pre:
- Коммит в статусе `created`.
post:
@@ -372,15 +373,15 @@ scenario:
description: >
Председатель ставит вторую подпись на акте приёма-передачи —
закрывающее действие. Применяются две ledger2-операции:
(1) o.cap.accept — РИД переходит с «Генератор — принятый коммит»
(w.cap.gncom) на «Благорост — принятые РИД» (w.cap.bgrid),
Дт 4 / Кт 8;
(1) o.cap.accept — РИД переходит с единого кошелька ЦПП «Генератор»
у пайщика (w.cap.gen) на единый кошелёк ЦПП «Благорост» у пайщика
(w.cap.blago), Дт 4 / Кт 8;
(2) опционально o.cap.repay — если у участника был заём проекта,
заём закрывается (w.cap.loan → w.wal.share, Дт 80 / Кт 58).
pre:
- Результат в статусе ACT1.
post:
- РИД в паевом фонде «Благорост — принятые РИД».
- РИД в едином кошельке ЦПП «Благорост» у пайщика (w.cap.blago).
- В ledger2 применены o.cap.accept (+ опц. o.cap.repay).
alternatives:
@@ -439,35 +440,36 @@ operations:
human_name: Коммит РИД по программе «Благорост»
wallet_op: ISSUE
wallet_from: ''
wallet_to: w.cap.gncom # ЦПП «Генератор» — принятый коммит (имущество)
wallet_to: w.cap.gen # ЦПП «Генератор» — единый кошелёк программы у пайщика
debit: 8 # Вложения во внеоборотные активы
credit: 80 # Паевой фонд (складочный капитал)
amount_ref: commit.delta_amount
triggered_by: capital::approvecmmt
description: >
Зачисление РИД-коммита на кошелёк «Генератор — принятый коммит» (w.cap.gncom)
Зачисление РИД-коммита на единый кошелёк ЦПП «Генератор» у пайщика (w.cap.gen)
при одобрении мастером. Двойная запись Дт 8 / Кт 80 — РИД учитывается
как вложение во внеоборотные активы и формирует паевой фонд.
- ledger_code: o.cap.accept
human_name: Приём РИД в паевой фонд
wallet_op: TRANSFER
wallet_from: w.cap.gncom # ЦПП «Генератор» — принятый коммит
wallet_to: w.cap.bgrid # ЦПП «Благорост» — принятые РИД
wallet_from: w.cap.gen # ЦПП «Генератор» — единый кошелёк программы у пайщика
wallet_to: w.cap.blago # ЦПП «Благорост» — единый кошелёк программы у пайщика
debit: 4 # Нематериальные активы
credit: 8 # Вложения во внеоборотные активы
amount_ref: result.amount
triggered_by: capital::signact2
description: >
Перевод одобренного коммита РИД из «Генератора» в «Благорост — принятые
РИД». Бухгалтерия закрывает Кт 8 (вложения) и записывает Дт 4
(нематериальные активы) — РИД зачислен в паевой фонд кооператива.
Перевод одобренного коммита РИД из единого кошелька ЦПП «Генератор»
у пайщика (w.cap.gen) в единый кошелёк ЦПП «Благорост» у пайщика
(w.cap.blago). Бухгалтерия закрывает Кт 8 (вложения) и записывает
Дт 4 (нематериальные активы) — РИД зачислен в паевой фонд кооператива.
- ledger_code: o.cap.repay
human_name: Возврат беспроцентного займа пайщика по акту-2
wallet_op: TRANSFER
wallet_from: w.cap.loan # Выданные пайщикам беспроцентные займы
wallet_to: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
wallet_to: w.wal.share # ЦК — паевая часть пайщика
debit: 80 # Паевой фонд (складочный капитал)
credit: 58 # Финансовые вложения
amount_ref: debt.amount
+7 -6
View File
@@ -175,8 +175,9 @@ public:
*
* Создаёт зеркальную проводку по операции `original_operation_id`:
* меняет местами Dr/Cr счета и (для wallet_op) wallet_from/wallet_to.
* Для исходного ISSUE используется новый `WalletOp::REVOKE` (изъятие
* с `wallet_from` без увеличения куда-либо).
* Для исходного ISSUE используется `WalletOp::BURN` (изъятие
* с `wallet_from` без увеличения куда-либо). Различие «штатное сжигание»
* vs «зеркало revert» делается через operation_code (`o.adj.rev`).
*
* Параметры зеркала готовит backend из своей БД (по записи оригинала
* в blockchain_actions/state) — контракт не имеет доступа к истории операций.
@@ -192,11 +193,11 @@ public:
* @param original_operation_code operation_code оригинала (для запрета o.mig.*)
* @param username username оригинала (для аналитики)
* @param amount сумма (как в оригинале)
* @param mirror_wallet_op тип wallet-операции зеркала (REVOKE/TRANSFER/WALLET_ONLY)
* @param mirror_wallet_op тип wallet-операции зеркала (TRANSFER/BURN)
* @param mirror_wallet_from кошелёк-источник зеркала
* @param mirror_wallet_to кошелёк-получатель зеркала (пустое имя для REVOKE)
* @param mirror_debit_account_id Dr-счёт зеркала (0 для WALLET_ONLY)
* @param mirror_credit_account_id Cr-счёт зеркала (0 для WALLET_ONLY)
* @param mirror_wallet_to кошелёк-получатель зеркала (пустое имя для BURN)
* @param mirror_debit_account_id Dr-счёт зеркала (0 если оригинал был без бухпроводок)
* @param mirror_credit_account_id Cr-счёт зеркала (0 если оригинал был без бухпроводок)
* @param process_hash уникальный хэш для зеркальной операции
* @param memo обязательное обоснование (≤ 255 символов)
*
@@ -84,13 +84,15 @@ void ledger2::revert(eosio::name coopname,
std::string{"revert: неизвестный mirror_wallet_to "} + mirror_wallet_to.to_string());
}
const bool wallet_only = (mirror_wallet_op == static_cast<uint8_t>(WalletOp::WALLET_ONLY));
if (wallet_only) {
eosio::check(mirror_debit_account_id == 0 && mirror_credit_account_id == 0,
"revert: для WALLET_ONLY mirror_debit_account_id и mirror_credit_account_id должны быть 0");
// ADR-003: «без бухпроводок» — пара (debit_account_id == 0, credit_account_id == 0).
// Соответственно zero-mirror = откат операции, у которой не было Dr/Cr (для walmove
// и других «без проводок»-сценариев).
const bool zero_mirror = (mirror_debit_account_id == 0 && mirror_credit_account_id == 0);
if (zero_mirror) {
// OK — откат без бухпроводок (zero-zero пара).
} else {
eosio::check(mirror_debit_account_id != 0 && mirror_credit_account_id != 0,
"revert: mirror_debit_account_id и mirror_credit_account_id обязательны для проводки с Dr/Cr");
"revert: смешанная пара mirror_debit/credit_account_id (один == 0, второй ≠ 0) запрещена");
eosio::check(mirror_debit_account_id != mirror_credit_account_id,
"revert: mirror_debit_account_id == mirror_credit_account_id (self-posting запрещён)");
eosio::check(ledger2_find_account_meta(mirror_debit_account_id) != nullptr,
@@ -109,7 +111,7 @@ void ledger2::revert(eosio::name coopname,
process_hash, memo)
).send();
if (wallet_only) {
if (zero_mirror) {
return;
}
@@ -2,9 +2,12 @@
* @brief Перевод между кошельками внутри одного бух.счёта (operation o.adj.walmove).
*
* Top-level action — председатель подписывает сам, никаких caller-контрактов.
* Делает один inline `walletop` с `op_code = WALLET_ONLY` (TRANSFER без Dr/Cr)
* так как корректировка между кошельками одного бух.счёта не меняет балансы
* самих счетов (`accounts2.balance`), только их аналитику.
* Делает один inline `walletop` с `op_code = TRANSFER` без бухпроводок
* корректировка между кошельками одного бух.счёта не меняет балансы самих
* счетов (`accounts2.balance`), только их аналитику. «Без бухпроводок» здесь
* выражается отсутствием inline-actions debit/credit на уровне самого walmove
* (ADR-003: семантика без проводок задаётся парой нулевых account_id записи
* OPERATION_REGISTRY; для walmove это технический action вне реестра).
*
* Связь wallet→account не хранится в LEDGER2_WALLET_REGISTRY (она выводится
* из OPERATION_REGISTRY по месту использования и может теоретически быть
@@ -57,12 +60,12 @@ void ledger2::walmove(eosio::name coopname,
eosio::check(memo.size() < 256, "walmove: memo не должен превышать 255 символов");
// -------- dispatch inline walletop --------
// WALLET_ONLY = перенос available между кошельками без Dr/Cr.
// На уровне walletop.cpp WALLET_ONLY обрабатывается тем же case, что и TRANSFER.
// TRANSFER без debit/credit inline-actions = перенос available между кошельками
// одного бухсчёта (ADR-003). Сам walletop.cpp делает только wallet-движение.
const auto self_perm = eosio::permission_level{get_self(), "active"_n};
eosio::action(self_perm, get_self(), "walletop"_n,
std::make_tuple(coopname,
static_cast<uint8_t>(WalletOp::WALLET_ONLY),
static_cast<uint8_t>(WalletOp::TRANSFER),
from_wallet, to_wallet, amount,
process_hash, memo)
).send();
@@ -59,9 +59,10 @@ void ledger2::apply(eosio::name coopname,
std::string{"Unknown operation code: "} + operation_code.to_string());
// -------- dispatch atomic inline actions --------
// Для штатных операций — «тройка» (walletop + debit + credit) с общим
// process_hash. Для WALLET_ONLY — только walletop (перенос средств между
// аналитическими разрезами одного бухсчёта, без debit/credit).
// Для записей с проводкой — «тройка» (walletop + debit + credit) с общим
// process_hash. Для записей без бухпроводки (оба account_id == 0,
// ADR-003) — только walletop: перенос средств между аналитическими
// разрезами одного бухсчёта, без debit/credit.
const auto self_perm = eosio::permission_level{get_self(), "active"_n};
eosio::action(self_perm, get_self(), "walletop"_n,
@@ -74,7 +75,7 @@ void ledger2::apply(eosio::name coopname,
memo)
).send();
if (entry->wallet_op == WalletOp::WALLET_ONLY) {
if (entry->debit_account_id == 0 && entry->credit_account_id == 0) {
return;
}
@@ -81,14 +81,14 @@ void ledger2::walletop(eosio::name coopname,
wallets.modify(it, payer, [&](auto& w) { w.available += amount; });
break;
}
case WalletOp::TRANSFER:
case WalletOp::WALLET_ONLY: {
// Оба типа — перенос available между кошельками. WALLET_ONLY отличается
// только отсутствием inline debit/credit (см. apply.cpp).
case WalletOp::TRANSFER: {
// Перенос available между кошельками. Опциональное отсутствие inline
// debit/credit определяется парой нулевых account_id на уровне записи
// OPERATION_REGISTRY (см. apply.cpp), а не отдельным walletop (ADR-003).
eosio::check(wallet_from.value != 0 && wallet_to.value != 0,
"walletop TRANSFER/WALLET_ONLY: требуются wallet_from и wallet_to");
"walletop TRANSFER: требуются wallet_from и wallet_to");
eosio::check(wallet_from != wallet_to,
"walletop TRANSFER/WALLET_ONLY: wallet_from == wallet_to");
"walletop TRANSFER: wallet_from == wallet_to");
auto from_it = wallets.find(wallet_from.value);
eosio::check(from_it != wallets.end() && from_it->available >= amount,
std::string{"walletop TRANSFER: недостаточно средств на кошельке "} +
@@ -125,14 +125,15 @@ void ledger2::walletop(eosio::name coopname,
});
break;
}
case WalletOp::REVOKE: {
// Зеркало ISSUE: убираем amount с wallet_from без увеличения куда-либо.
// Используется только из ledger2::revert при откате ISSUE-операций.
eosio::check(wallet_from.value != 0, "walletop REVOKE: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop REVOKE: wallet_to должен быть пустым");
case WalletOp::BURN: {
// Изъятие amount с wallet_from без увеличения куда-либо (ADR-003).
// Покрывает: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY;
// (b) зеркало ISSUE при ledger2::revert. Различие — через operation_code.
eosio::check(wallet_from.value != 0, "walletop BURN: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BURN: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->available >= amount,
std::string{"walletop REVOKE: недостаточно available на кошельке "} +
std::string{"walletop BURN: недостаточно available на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) { w.available -= amount; });
cleanup_if_empty(wallet_from);
@@ -13,7 +13,7 @@
* системы учёта, progwallet.blocked не проводится через 80-й счёт,
* поэтому любая попытка провести его через Dr 51 / Cr 80 вызовет
* двойной учёт на бухуровне. Вместо этого переносим только
* wallet-аналитику (w.cap.bginv BLAGOROST_INVEST, w.cap.gncom GENERATOR_COMMIT).
* wallet-аналитику (w.cap.blago BLAGOROST_FUND, w.cap.gen GENERATOR_FUND — ADR-009).
*
* Алгоритм на каждый кооператив:
*
@@ -28,25 +28,23 @@
*
* 3. Вычисление распределения:
* share_money = cash_legacy entry_legacy
* rid_share = share_legacy share_money
* min_total = clamp(Σ accepted-пайщиков по типу, 0, share_money)
* где для organization берётся coop.org_minimum,
* иначе — coop.minimum (см. compute_min_total_by_type).
* share_remain = share_money min_total
*
* Инварианты (eosio::check):
* cash_legacy >= entry_legacy
* share_legacy >= share_money
* cash_legacy >= entry_legacy
* share_legacy == share_money (legacy 80 без РИД-части — иначе abort)
*
* 4. Отправка 4 inline apply (ненулевые пропускаются):
* 4. Отправка 3 inline apply (ненулевые пропускаются):
* apply(migration::MIN_SHARE, min_total) → Dr 51 / Cr 80, MIN_SHARE_FUND (w.reg.minshr)
* apply(migration::SHARE, share_remain) → Dr 51 / Cr 80, SHARE_FUND_PAY (w.wal.share)
* apply(migration::ENTRY, entry_legacy) → Dr 51 / Cr 86, ENTRANCE_FEES (w.reg.entry)
* apply(migration::RID, rid_share) → Dr 04 / Cr 80, SHARE_FUND_RID (w.wal.sharid)
*
* 5. Прямой emplace в wallets2 для progwallets (БЕЗ бух-проводок):
* Σ progwallet[blagorost].blocked → wallets2[BLAGOROST_INVEST w.cap.bginv]
* Σ progwallet[generator].blocked → wallets2[GENERATOR_COMMIT w.cap.gncom]
* Σ progwallet[blagorost].blocked → wallets2[BLAGOROST_FUND w.cap.blago]
* Σ progwallet[generator].blocked → wallets2[GENERATOR_FUND w.cap.gen]
*
* Если запись wallets2 уже есть (после inline apply сработали) —
* aggregate через wallets.modify(available += sum). Иначе — emplace.
@@ -207,7 +205,7 @@ inline void emplace_wallet_only(eosio::name self_name,
} else {
// Если кошелёк уже создан (например, предыдущим inline apply TRANSIT_*) —
// просто доливаем сумму в available. Не должно случаться при чистой
// миграции (w.cap.bginv/w.cap.gncom не используются базовыми TRANSIT_*),
// миграции (w.cap.blago/w.cap.gen не используются базовыми TRANSIT_*),
// но безопасно.
wallets.modify(it, self_name, [&](auto& w) { w.available += amt; });
}
@@ -225,9 +223,9 @@ inline void emplace_wallet_only(eosio::name self_name,
* есть бумажный шлейф на 51, и т.п.). Вместо арифметической сшивки —
* заводим суммы вручную по тому, как кооператив должен выглядеть в ledger2.
*
* Имущественная часть на legacy 80 (РИД ~56.8M) сюда НЕ заводится
* отдельным w.wal.sharid: она остаётся осадком на legacy 80 до Phase 2
* (ADR-009), когда Благорост схлопнется в w.cap.blago и заберёт имущество.
* Имущественная часть на legacy 80 (РИД ~56.8M) сюда НЕ заводится:
* она остаётся осадком на legacy 80 до Phase 2 (ADR-009), когда Благорост
* схлопнется в w.cap.blago и заберёт имущество.
*
* Бух-баланс по этой таблице:
* Dr 51 = w.reg.minshr + w.wal.share + w.reg.entry = 31800 + 569900 + 12500 = 614200
@@ -235,7 +233,7 @@ inline void emplace_wallet_only(eosio::name self_name,
* Cr 86 = w.reg.entry = 12500
* Σ Dr = Σ Cr = 614200 ✓
*
* Σ Благороста на bginv (57 044 311) и Генератора на gncom (0) идут
* Σ Благороста на w.cap.blago (57 044 311) и Генератора на w.cap.gen (0) идут
* прямым emplace без бух-проводок (параллельный progwallets-учёт).
*/
inline void migrate_voskhod_facts(eosio::name self_name, const cooperative2& coop) {
@@ -257,19 +255,19 @@ inline void migrate_voskhod_facts(eosio::name self_name, const cooperative2& coo
// 3. w.reg.entry — Вступительные взносы (Dr 51 / Cr 86)
const eosio::asset W_REG_ENTRY = eosio::asset( 125'000'000LL, sym); // 12 500.0000 RUB
// 4. w.cap.bginv — ЦПП «Благорост» (emplace, БЕЗ проводок)
const eosio::asset W_CAP_BGINV = eosio::asset(570'443'110'000LL, sym); // 57 044 311.0000 RUB
// 4. w.cap.blago — ЦПП «Благорост» — единый кошелёк программы (emplace, БЕЗ проводок); ADR-009
const eosio::asset W_CAP_BLAGO = eosio::asset(570'443'110'000LL, sym); // 57 044 311.0000 RUB
// 5. w.cap.gncom — ЦПП «Генератор» (emplace, БЕЗ проводок); 0 → не создаётся
const eosio::asset W_CAP_GNCOM = eosio::asset( 0LL, sym); // 0.0000 RUB
// 5. w.cap.gen — ЦПП «Генератор» — единый кошелёк программы (emplace, БЕЗ проводок); 0 → не создаётся
const eosio::asset W_CAP_GEN = eosio::asset( 0LL, sym); // 0.0000 RUB
// ── применяем таблицу ───────────────────────────────────────────────
send_transit(self_name, coopname, operations::migration::MIN_SHARE, W_REG_MINSHR);
send_transit(self_name, coopname, operations::migration::SHARE, W_WAL_SHARE);
send_transit(self_name, coopname, operations::migration::ENTRY, W_REG_ENTRY);
emplace_wallet_only(self_name, coopname, ledger2_wallets::BLAGOROST_INVEST, W_CAP_BGINV);
emplace_wallet_only(self_name, coopname, ledger2_wallets::GENERATOR_COMMIT, W_CAP_GNCOM);
emplace_wallet_only(self_name, coopname, ledger2_wallets::BLAGOROST_FUND, W_CAP_BLAGO);
emplace_wallet_only(self_name, coopname, ledger2_wallets::GENERATOR_FUND, W_CAP_GEN);
}
/**
@@ -300,24 +298,23 @@ inline void migrate_one_coop(eosio::name self_name, const cooperative2& coop) {
return;
}
// ----- A. Бухгалтерский перенос legacy::accounts через 4 TRANSIT_* -----
// ----- A. Бухгалтерский перенос legacy::accounts через 3 TRANSIT_* -----
if (b.cash.amount > 0 || b.share.amount > 0 || b.entry.amount > 0) {
// Деньги на паевой фонд: всё на 51 сверх вступительных.
const eosio::asset share_money(
b.cash.amount >= b.entry.amount ? b.cash.amount - b.entry.amount : 0,
_root_govern_symbol
);
// Не-денежная часть 80: РИД Восхода (+56M) + всё, что было проведено
// как имущество через legacy::ledger::add на 80 без prior 51.
const eosio::asset rid_share(
b.share.amount >= share_money.amount ? b.share.amount - share_money.amount : 0,
_root_govern_symbol
);
eosio::check(b.cash.amount >= b.entry.amount,
std::string{"migrate: entry > cash на кооп "} + coopname.to_string());
eosio::check(b.share.amount >= share_money.amount,
std::string{"migrate: share_money > share_legacy на кооп "} + coopname.to_string());
// Инвариант: legacy::accounts[80] (паевой фонд) равен money-части (51 - 86).
// РИД-часть на legacy 80 не поддерживается этим путём (ADR-009: РИД-программа
// переезжает в w.cap.blago отдельно); если такой остаток есть — миграция
// отказывается, кооператив должен закрыть РИД-часть до миграции.
eosio::check(b.share.amount == share_money.amount,
std::string{"migrate: legacy 80 содержит РИД-часть на кооп "} + coopname.to_string() +
" — ledger2 не поддерживает РИД-перенос (ADR-009). Закрыть РИД до миграции.");
// Минимальный паевой: Σ по accepted-пайщикам с учётом типа
// (organization → coop.org_minimum, иначе → coop.minimum), не больше share_money.
@@ -334,14 +331,13 @@ inline void migrate_one_coop(eosio::name self_name, const cooperative2& coop) {
send_transit(self_name, coopname, operations::migration::MIN_SHARE, min_total);
send_transit(self_name, coopname, operations::migration::SHARE, share_remain);
send_transit(self_name, coopname, operations::migration::ENTRY, b.entry);
send_transit(self_name, coopname, operations::migration::RID, rid_share);
}
// ----- B. Программные кошельки — прямой emplace, без Dr/Cr -----
// progwallets.blocked НЕ входит в legacy::accounts[80], поэтому
// проводить их через apply(operations::migration::*) вызвало бы двойной учёт.
emplace_wallet_only(self_name, coopname, ledger2_wallets::BLAGOROST_INVEST, blagorost_invest);
emplace_wallet_only(self_name, coopname, ledger2_wallets::GENERATOR_COMMIT, generator_commit);
emplace_wallet_only(self_name, coopname, ledger2_wallets::BLAGOROST_FUND, blagorost_invest);
emplace_wallet_only(self_name, coopname, ledger2_wallets::GENERATOR_FUND, generator_commit);
}
} // namespace
@@ -24,11 +24,19 @@
* (`operations::registrator::`, `operations::capital::` и т.д.) — контракт
* считывается по месту вызова.
*
* Пересмотр 2026-04-20 (сохранён):
* Пересмотр 2026-05-05 (ADR-003, ADR-009):
* - План счетов: 04, 08, 51, 58, 80, 86.
* - Коммит РИД разделён на `o.cap.commit` (Dr 08/Cr 80) и `o.cap.accept`
* (Dr 04/Cr 08, TRANSFER GENERATOR_COMMIT → BLAGOROST_RID).
* - WALLET_ONLY: только wallet-движение, без Dr/Cr (для `o.cap.invest`).
* (Dr 04/Cr 08, TRANSFER GENERATOR_FUND → BLAGOROST_FUND).
* - `WalletOp::WALLET_ONLY` удалён (ADR-003): «без бухпроводок» определяется
* парой `(debit_account_id == 0, credit_account_id == 0)` на уровне записи.
* Для `o.cap.invest` теперь TRANSFER без проводок (оба account_id == 0).
* - Добавлен `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
* без `wallet_to`. На текущем этапе в `OPERATION_REGISTRY` не используется —
* зарезервирован под будущие операции штатного сжигания.
* - Единые программные кошельки: `BLAGOROST_FUND` (`w.cap.blago`) и
* `GENERATOR_FUND` (`w.cap.gen`) — заменили ранее декомпозированные
* `bginv/bgprop/bgrid/bgmem` и `gncom/gnmem` (ADR-009).
* - Миграционные операции — namespace `operations::migration`.
*
* Реестр — строго хардкод. Новая операция требует релиза контракта.
@@ -58,11 +66,11 @@ namespace operations {
// capital
namespace capital {
inline constexpr eosio::name IMPORT = "o.cap.import"_n; ///< Оффлайн-импорт пайщика Благорост (Dr 51 / Cr 80, ISSUE BLAGOROST_INVEST).
inline constexpr eosio::name INVEST = "o.cap.invest"_n; ///< Инвестиция в ЦПП Благорост (WALLET_ONLY TRANSFER 2001 → 9001).
inline constexpr eosio::name COMMIT_RID = "o.cap.commit"_n; ///< Коммит РИД (Dr 08 / Cr 80, ISSUE GENERATOR_COMMIT).
inline constexpr eosio::name ACCEPT_RID = "o.cap.accept"_n; ///< Приём РИД в НМА (Dr 04 / Cr 08, TRANSFER GENERATOR_COMMIT → BLAGOROST_RID).
inline constexpr eosio::name ACCEPT_PROPERTY = "o.cap.actprp"_n; ///< Акт-2 имущественный паевой взнос (Dr 51 / Cr 80, ISSUE BLAGOROST_PROPERTY).
inline constexpr eosio::name IMPORT = "o.cap.import"_n; ///< Оффлайн-импорт пайщика Благорост (Dr 51 / Cr 80, ISSUE BLAGOROST_FUND).
inline constexpr eosio::name INVEST = "o.cap.invest"_n; ///< Инвестиция в ЦПП Благорост (TRANSFER SHARE_FUND_PAY → BLAGOROST_FUND, без Dr/Cr).
inline constexpr eosio::name COMMIT_RID = "o.cap.commit"_n; ///< Коммит РИД (Dr 08 / Cr 80, ISSUE GENERATOR_FUND).
inline constexpr eosio::name ACCEPT_RID = "o.cap.accept"_n; ///< Приём РИД в НМА (Dr 04 / Cr 08, TRANSFER GENERATOR_FUND → BLAGOROST_FUND).
inline constexpr eosio::name ACCEPT_PROPERTY = "o.cap.actprp"_n; ///< Акт-2 имущественный паевой взнос (Dr 51 / Cr 80, ISSUE BLAGOROST_FUND).
inline constexpr eosio::name LEND = "o.cap.lend"_n; ///< Выдача беспроцентного займа пайщику (Dr 58 / Cr 51, ISSUE LOAN_ISSUED).
inline constexpr eosio::name REPAY = "o.cap.repay"_n; ///< Возврат займа пайщика по акту-2 (Dr 80 / Cr 58, TRANSFER LOAN_ISSUED → SHARE_FUND_PAY).
}
@@ -90,7 +98,6 @@ namespace operations {
inline constexpr eosio::name MIN_SHARE = "o.mig.minshr"_n; ///< Перенос: минимальный паевой взнос (Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND).
inline constexpr eosio::name SHARE = "o.mig.share"_n; ///< Перенос: остаток паевых деньгами (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
inline constexpr eosio::name ENTRY = "o.mig.entry"_n; ///< Перенос: вступительные (Dr 51 / Cr 86, ISSUE ENTRANCE_FEES).
inline constexpr eosio::name RID = "o.mig.rid"_n; ///< Перенос: РИД в НМА (Dr 04 / Cr 80, ISSUE SHARE_FUND_RID).
}
// adjustment (ручные корректировки председателя)
@@ -109,38 +116,41 @@ namespace operations {
} // namespace operations
/**
* @brief Элементарные операции по кошелькам.
* @brief Элементарные операции по кошелькам (ADR-003).
*
* `WALLET_ONLY` — новый тип (2026-04-20): TRANSFER между кошельками БЕЗ
* бухгалтерских проводок debit/credit. Используется, когда средства
* перемещаются между аналитическими разрезами одного счёта (пример:
* Цифровой Кошелёк 80 → инвестиционный Благорост 80 — в обоих случаях Cr 80,
* счёт не меняется, но wallet-аналитика — да).
* Семантика «без бухпроводок» — НЕ через отдельный walletop, а через пару
* `(debit_account_id == 0, credit_account_id == 0)` на уровне записи реестра.
* Compile-time правило `(debit==0) ⇔ (credit==0)` ловит смешанные пары.
*/
enum class WalletOp : uint8_t {
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr)
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
WALLET_ONLY = 4, ///< TRANSFER wallet_from wallet_to БЕЗ debit/credit inline-actions
REVOKE = 5, ///< изъятие amount с wallet_from без увеличения куда-либо (зеркало ISSUE для o.adj.rev)
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
BURN = 4, ///< изъятие amount с wallet_from, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
};
/**
* @brief Описание одной именованной операции.
*
* Для WALLET_ONLY поля `debit_account_id` / `credit_account_id` равны 0
* (compile-time валидацией ниже). Для ISSUE `wallet_from = eosio::name{}`,
* для BLOCK/UNBLOCK/REVOKE `wallet_to = eosio::name{}`.
* Семантика полей по `wallet_op`:
* - ISSUE: wallet_from = eosio::name{}, wallet_to = required.
* - TRANSFER: wallet_from = required, wallet_to = required (≠ from).
* - BLOCK / UNBLOCK / BURN: wallet_from = required, wallet_to = eosio::name{}.
*
* Семантика бух.проводки:
* - Без проводок: debit_account_id == 0 И credit_account_id == 0.
* - С проводкой: оба ≠ 0, ≠ друг друга, оба из LEDGER2_ACCOUNT_MAP.
* - Смешанная пара (один == 0, второй ≠ 0) запрещена compile-time.
*/
struct OperationRegistryEntry {
eosio::name code; ///< operation_code с префиксом `o.<contract>.<verb>`
eosio::name process_type; ///< тип процесса с префиксом `p.<contract>.<noun>`
WalletOp wallet_op;
eosio::name wallet_from; ///< пустое имя для ISSUE
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/REVOKE
uint64_t debit_account_id; ///< 0 для WALLET_ONLY
uint64_t credit_account_id; ///< 0 для WALLET_ONLY
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN
uint64_t debit_account_id; ///< 0 если без бухпроводки (тогда credit_account_id тоже == 0)
uint64_t credit_account_id; ///< 0 если без бухпроводки (тогда debit_account_id тоже == 0)
const char* human_name;
};
@@ -171,30 +181,30 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Возврат паевого взноса пайщику" },
// 5. Импорт пайщика Благорост (offline): Dr 51 / Cr 80, ISSUE BLAGOROST_INVEST
{ operations::capital::IMPORT, processes::capital::IMPORT, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::BLAGOROST_INVEST,
// 5. Импорт пайщика Благорост (offline): Dr 51 / Cr 80, ISSUE BLAGOROST_FUND (ADR-009: единый кошелёк программы)
{ operations::capital::IMPORT, processes::capital::IMPORT, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::BLAGOROST_FUND,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Паевой взнос по целевой потребительской программе «Благорост» (офлайн-импорт)" },
// 6. Инвестиция из Цифрового Кошелька в Благорост: WALLET_ONLY TRANSFER 2001 → 9001
{ operations::capital::INVEST, processes::capital::INVEST, WalletOp::WALLET_ONLY,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::BLAGOROST_INVEST,
// 6. Инвестиция из Цифрового Кошелька в Благорост: TRANSFER SHARE_FUND_PAY → BLAGOROST_FUND (без Dr/Cr — оба счёта 80)
{ 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_COMMIT
{ operations::capital::COMMIT_RID, processes::capital::RID, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::GENERATOR_COMMIT,
// 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, TRANSFER GENERATOR_COMMIT → BLAGOROST_RID
// 8. Приём РИД в НМА: Dr 04 / Cr 08, TRANSFER GENERATOR_FUND → BLAGOROST_FUND (ADR-009)
{ operations::capital::ACCEPT_RID, processes::capital::RID, WalletOp::TRANSFER,
ledger2_wallets::GENERATOR_COMMIT, ledger2_wallets::BLAGOROST_RID,
ledger2_wallets::GENERATOR_FUND, ledger2_wallets::BLAGOROST_FUND,
ledger2_accounts::INTANGIBLE_ASSETS, ledger2_accounts::NON_CURRENT_INVESTMENTS,
"Приём результата интеллектуальной деятельности в паевой фонд" },
// 9. Акт-2 имущественный паевой взнос: Dr 51 / Cr 80, ISSUE BLAGOROST_PROPERTY
{ operations::capital::ACCEPT_PROPERTY, processes::capital::PROPERTY, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::BLAGOROST_PROPERTY,
// 9. Акт-2 имущественный паевой взнос: Dr 51 / Cr 80, ISSUE BLAGOROST_FUND (ADR-009)
{ operations::capital::ACCEPT_PROPERTY, processes::capital::PROPERTY, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::BLAGOROST_FUND,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Паевой взнос (не денежный) по программе «Благорост»" },
@@ -242,11 +252,6 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
{ operations::migration::ENTRY, processes::migration::TRANSIT, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::ENTRANCE_FEES,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::TARGET_RECEIPTS,
"Транзитный перенос: вступительные взносы при миграции" },
// 18. Миграция: РИД в НМА: Dr 04 / Cr 80, ISSUE SHARE_FUND_RID
{ operations::migration::RID, processes::migration::TRANSIT, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::SHARE_FUND_RID,
ledger2_accounts::INTANGIBLE_ASSETS, ledger2_accounts::SHARE_FUND,
"Транзитный перенос: принятые РИД в паевой фонд при миграции" },
};
static constexpr size_t OPERATION_REGISTRY_SIZE = sizeof(OPERATION_REGISTRY) / sizeof(OPERATION_REGISTRY[0]);
@@ -255,13 +260,15 @@ static constexpr size_t OPERATION_REGISTRY_SIZE = sizeof(OPERATION_REGISTRY) / s
// Compile-time валидация реестра.
// =====================================================================
//
// Правила:
// Правила (ADR-003):
// 1. `code` уникален. `process_type` может повторяться.
// 2. Для операций с бухпроводками: `debit_account_id` ≠ `credit_account_id`.
// 3. Для TRANSFER и WALLET_ONLY: `wallet_from` ≠ `wallet_to`.
// 4. Для ISSUE: `wallet_from` == 0 и `wallet_to` ≠ 0.
// 5. Для WALLET_ONLY: оба счёта == 0 (без проводок).
// 6. Все id счетов/кошельков из registry существуют в справочниках.
// 2. `(debit_account_id == 0) ⇔ (credit_account_id == 0)` — без частичных проводок.
// 3. Для записей с проводками (оба ≠ 0): `debit_account_id` ≠ `credit_account_id`,
// оба существуют в `LEDGER2_ACCOUNT_MAP`.
// 4. Для TRANSFER: `wallet_from` ≠ `wallet_to`, оба ≠ 0.
// 5. Для ISSUE: `wallet_from` == 0 и `wallet_to` ≠ 0.
// 6. Для BLOCK / UNBLOCK / BURN: `wallet_from` ≠ 0, `wallet_to` == 0.
// 7. Все id кошельков из записей существуют в `LEDGER2_WALLET_REGISTRY`.
namespace ledger2_registry_detail {
constexpr bool operation_codes_unique() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
@@ -272,45 +279,61 @@ namespace ledger2_registry_detail {
return true;
}
// Правило 2: без частичных проводок. Либо оба == 0 (без бухпроводки), либо оба ≠ 0.
constexpr bool zero_accounts_iff_both() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
const bool dr_zero = (e.debit_account_id == 0);
const bool cr_zero = (e.credit_account_id == 0);
if (dr_zero != cr_zero) return false;
}
return true;
}
// Правило 3: при наличии проводки — debit ≠ credit.
constexpr bool dr_ne_cr_when_posting() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op == WalletOp::WALLET_ONLY) continue;
if (e.debit_account_id == 0 && e.credit_account_id == 0) continue; // без проводок
if (e.debit_account_id == e.credit_account_id) return false;
}
return true;
}
constexpr bool wallet_only_has_zero_accounts() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op != WalletOp::WALLET_ONLY) continue;
if (e.debit_account_id != 0 || e.credit_account_id != 0) return false;
}
return true;
}
// Правило 4: TRANSFER — wallet_from ≠ wallet_to, оба ≠ 0.
constexpr bool transfer_wallet_from_ne_to() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op == WalletOp::TRANSFER || e.wallet_op == WalletOp::WALLET_ONLY) {
if (e.wallet_from == e.wallet_to) return false;
if (e.wallet_from.value == 0 || e.wallet_to.value == 0) return false;
}
if (e.wallet_op != WalletOp::TRANSFER) continue;
if (e.wallet_from == e.wallet_to) return false;
if (e.wallet_from.value == 0 || e.wallet_to.value == 0) return false;
}
return true;
}
// Правило 6: BURN — wallet_from required, wallet_to == 0 (ADR-003).
constexpr bool burn_pattern_correct() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op != WalletOp::BURN) continue;
if (e.wallet_from.value == 0) return false;
if (e.wallet_to.value != 0) return false;
}
return true;
}
// Правило 3: оба account_id (если ≠ 0) существуют в LEDGER2_ACCOUNT_MAP.
constexpr bool accounts_exist_in_map() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op == WalletOp::WALLET_ONLY) continue;
if (e.debit_account_id == 0 && e.credit_account_id == 0) continue; // без проводок
if (ledger2_find_account_meta(e.debit_account_id) == nullptr) return false;
if (ledger2_find_account_meta(e.credit_account_id) == nullptr) return false;
}
return true;
}
// Правило 7: все ссылки на кошельки существуют в реестре.
constexpr bool wallets_exist_in_registry() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
@@ -323,12 +346,14 @@ namespace ledger2_registry_detail {
static_assert(ledger2_registry_detail::operation_codes_unique(),
"OPERATION_REGISTRY: duplicate operation_code detected");
static_assert(ledger2_registry_detail::zero_accounts_iff_both(),
"OPERATION_REGISTRY: смешанная пара debit/credit account_id (один == 0, второй ≠ 0)");
static_assert(ledger2_registry_detail::dr_ne_cr_when_posting(),
"OPERATION_REGISTRY: debit_account_id == credit_account_id (self-posting) на non-WALLET_ONLY записи");
static_assert(ledger2_registry_detail::wallet_only_has_zero_accounts(),
"OPERATION_REGISTRY: WALLET_ONLY запись с ненулевыми debit/credit");
"OPERATION_REGISTRY: debit_account_id == credit_account_id (self-posting) при наличии проводки");
static_assert(ledger2_registry_detail::transfer_wallet_from_ne_to(),
"OPERATION_REGISTRY: TRANSFER/WALLET_ONLY с wallet_from == wallet_to");
"OPERATION_REGISTRY: TRANSFER с wallet_from == wallet_to или одним из них == 0");
static_assert(ledger2_registry_detail::burn_pattern_correct(),
"OPERATION_REGISTRY: BURN требует wallet_from ≠ 0 и wallet_to == 0");
static_assert(ledger2_registry_detail::accounts_exist_in_map(),
"OPERATION_REGISTRY: ссылка на account id вне LEDGER2_ACCOUNT_MAP");
static_assert(ledger2_registry_detail::wallets_exist_in_registry(),
@@ -354,7 +379,7 @@ inline const OperationRegistryEntry* find_operation(eosio::name operation_code)
// debit/credit_account_id заполняются ДИНАМИЧЕСКИ при каждом вызове
// (определяются параметрами walmove/revert, не справочником). В
// OPERATION_REGISTRY их положить нельзя — сломаются static_assert
// (transfer_wallet_from_ne_to: 0==0, dr_ne_cr_when_posting и пр.).
// (transfer_wallet_from_ne_to, zero_accounts_iff_both, dr_ne_cr_when_posting и пр.).
//
// Здесь — только code + process_type + human_name для UI/audit
// (cooptypes mirror живёт в src/ledger2/operations.ts → addAdjustment).
@@ -7,7 +7,7 @@
#include <eosio/eosio.hpp>
/**
* @brief Стандарт кошельков ledger2 (пересмотр 2026-04-27).
* @brief Стандарт кошельков ledger2 (пересмотр 2026-05-05 — финальный реестр + WalletKind).
*
* Кошельки ledger2 — это аналитические разрезы бухгалтерских счетов:
* «куда/откуда движутся средства» на уровне ЦПП и фондов. Учёт остатков
@@ -24,82 +24,120 @@
* w.reg.* — Регистрация (минимальный паевой, вступительные)
* w.sov.* — Целевое финансирование (членские, делегатские)
* w.cap.* — Программы паевого фонда (Благорост, Генератор) и займы
* w.mkt.* — Маркетплейс (выплаты поставщикам, общий фонд «Стол Заказов»)
* w.led.* — Служебные (ручные корректировки)
* w.mkt.* — Маркетплейс (выплаты поставщикам)
*
* Sentinel `eosio::name{}` (пустое имя, value=0) — «кошелёк вне системы»
* для ISSUE (нет wallet_from) и REVOKE (нет wallet_to).
* для ISSUE (нет wallet_from) и для BURN/BLOCK/UNBLOCK (нет wallet_to).
*
* При первом ISSUE/TRANSFER кошелёк создаётся автоматически по записи
* из WALLET_REGISTRY. При обнулении available+blocked запись удаляется.
*
* Классификация `WalletKind` (ADR-002):
* USER_SHARED — обязателен L3-разрез по пайщику (`ledger2::userwallets`).
* COOPERATIVE — единый кооперативный баланс, без L3.
*
* Связь L2↔L3 (ADR-010): отдельный enum имён для L3 не вводится — таблица
* `userwallets` хранит `wallet_name`, ссылающийся на этот же реестр; запись
* L3 разрешена только для `kind == USER_SHARED`.
*
* @ingroup public_ledger2_consts
*/
struct ledger2_wallets {
// wallet — паевой фонд + возвраты
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< ЦПП «Цифровой Кошелёк» — паевые взносы деньгами (Cr 80)
static constexpr eosio::name SHARE_FUND_RID = "w.wal.sharid"_n; ///< Паевой фонд — РИД, принятые в НМА (Dr 04 / Cr 80)
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< Возвраты паевых взносов пайщикам (sink TRANSFER)
// wallet — паевой фонд + возвраты + ЦК
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< ЦК — паевая часть пайщика (USER_SHARED)
static constexpr eosio::name CK_MEMBER = "w.wal.member"_n; ///< ЦК — членская часть пайщика (USER_SHARED)
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< Возвраты паевых взносов пайщикам (sink TRANSFER, COOPERATIVE)
// registrator — минимальный паевой + вступительные
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос при регистрации (Cr 80)
static constexpr eosio::name ENTRANCE_FEES = "w.reg.entry"_n; ///< Вступительные взносы (Cr 86)
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос пайщика (USER_SHARED, без сверки соглашений)
static constexpr eosio::name ENTRANCE_FEES = "w.reg.entry"_n; ///< Вступительные взносы (Cr 86, COOPERATIVE)
// soviet — членские взносы
static constexpr eosio::name MEMBERSHIP_FEES = "w.sov.member"_n; ///< Членские взносы (платформенные) (Cr 86)
static constexpr eosio::name DELEGATE_FEES = "w.sov.delgte"_n; ///< Делегатские членские взносы (цель CONVERT_TO_AXN) (Cr 86)
// soviet — членские (инфраструктура) + делегатские
static constexpr eosio::name INFRA_FEES = "w.sov.infra"_n; ///< Членские взносы за инфраструктуру кооп. платформы (COOPERATIVE)
static constexpr eosio::name DELEGATE_FEES = "w.sov.delgte"_n; ///< Делегатские членские взносы (цель CONVERT_TO_AXN, COOPERATIVE)
// capital — программы Благорост, Генератор + займы
static constexpr eosio::name LOAN_ISSUED = "w.cap.loan"_n; ///< Выданные пайщикам беспроцентные займы (Dr 58 / Cr 51)
static constexpr eosio::name BLAGOROST_INVEST = "w.cap.bginv"_n; ///< Благорост — инвестиции деньгами (Cr 80)
static constexpr eosio::name BLAGOROST_RID = "w.cap.bgrid"_n; ///< Благорост — принятые РИД (Dr 04 / Cr 08 после accept)
static constexpr eosio::name BLAGOROST_PROPERTY = "w.cap.bgprop"_n; ///< Благорост — имущественные паевые взносы
static constexpr eosio::name BLAGOROST_MEMBERSHIP = "w.cap.bgmem"_n; ///< Благорост — членские взносы по программе (Cr 86)
static constexpr eosio::name GENERATOR_COMMIT = "w.cap.gncom"_n; ///< Генератор — паевой взнос имуществом в статусе «принятый коммит» (Dr 08 / Cr 80)
static constexpr eosio::name GENERATOR_MEMBERSHIP = "w.cap.gnmem"_n; ///< Генератор — членские взносы по программе (Cr 86)
// capital — единые программные кошельки + займы
static constexpr eosio::name LOAN_ISSUED = "w.cap.loan"_n; ///< Выданные пайщикам беспроцентные займы (COOPERATIVE; Dr 58 / Cr 51)
static constexpr eosio::name BLAGOROST_FUND = "w.cap.blago"_n; ///< Благорост — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
static constexpr eosio::name GENERATOR_FUND = "w.cap.gen"_n; ///< Генератор — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
// marketplace — выплаты + общий фонд
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink RECEIVE_CONFIRM)
static constexpr eosio::name MARKETPLACE_FUND = "w.mkt.fund"_n; ///< ЦПП «Стол Заказов» — общий кошелёк программы (резерв)
// ledger2 — служебные
static constexpr eosio::name MANUAL_ADJUST = "w.led.adjust"_n; ///< Ручные корректировки (резерв под ledger2::adjust)
// marketplace — выплаты
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink RECEIVE_CONFIRM, COOPERATIVE)
};
/**
* @brief Справочник кошелька: machine name → human-readable name.
* @brief Тип кошелька второго уровня (ADR-002).
*
* USER_SHARED — обязателен L3-разрез по пайщику (`ledger2::userwallets`),
* `walletop` требует параметр `username` (Эпик 3).
* COOPERATIVE — единый кооперативный баланс, без L3.
*
* `WalletKind` обязателен для каждой записи `LEDGER2_WALLET_REGISTRY`
* (поле без default — попытка добавить элемент без `kind` ломает сборку).
*/
enum class WalletKind : uint8_t {
USER_SHARED = 0,
COOPERATIVE = 1,
};
/**
* @brief Справочник кошелька: machine name → human-readable name + kind.
*
* `constexpr std::array` + `string_view` — без dynamic init в WASM.
*/
struct Ledger2WalletMeta {
eosio::name name;
std::string_view human_name;
WalletKind kind;
};
inline constexpr std::array<Ledger2WalletMeta, 17> LEDGER2_WALLET_REGISTRY = {{
{ ledger2_wallets::SHARE_FUND_PAY, "ЦПП «Цифровой Кошелёк» — паевые взносы деньгами" },
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос" },
{ ledger2_wallets::SHARE_FUND_RID, "Паевой фонд — принятые РИД" },
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы" },
{ ledger2_wallets::MEMBERSHIP_FEES, "Членские взносы (платформенные)" },
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы" },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам" },
{ ledger2_wallets::SUPPLIER_PAYMENTS, "Выплаты поставщикам" },
{ ledger2_wallets::LOAN_ISSUED, "Выданные пайщикам беспроцентные займы" },
{ ledger2_wallets::MANUAL_ADJUST, "Ручные корректировки" },
{ ledger2_wallets::BLAGOROST_INVEST, "ЦПП «Благорост» — инвестиции деньгами" },
{ ledger2_wallets::BLAGOROST_RID, "ЦПП «Благорост» — принятые РИД" },
{ ledger2_wallets::BLAGOROST_PROPERTY, "ЦПП «Благорост» — имущественные паевые взносы" },
{ ledger2_wallets::BLAGOROST_MEMBERSHIP, "ЦПП «Благорост» — членские взносы" },
{ ledger2_wallets::GENERATOR_COMMIT, "ЦПП «Генератор» — принятый коммит (имущество)" },
{ ledger2_wallets::GENERATOR_MEMBERSHIP, "ЦПП «Генератор» — членские взносы" },
{ ledger2_wallets::MARKETPLACE_FUND, "ЦПП «Стол Заказов» — общий кошелёк" },
inline constexpr std::array<Ledger2WalletMeta, 11> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (5) — L3-разрез по пайщику
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "ЦК — паевая часть пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::CK_MEMBER, "ЦК — членская часть пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
// COOPERATIVE (6) — единый кооперативный баланс, без L3
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам", WalletKind::COOPERATIVE },
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::LOAN_ISSUED, "Выданные пайщикам беспроцентные займы", WalletKind::COOPERATIVE },
{ ledger2_wallets::SUPPLIER_PAYMENTS, "Выплаты поставщикам", WalletKind::COOPERATIVE },
}};
static constexpr size_t LEDGER2_WALLET_REGISTRY_SIZE = LEDGER2_WALLET_REGISTRY.size();
// Compile-time проверка уникальности имён кошельков.
// =====================================================================
// Compile-time валидация реестра.
// =====================================================================
//
// Правила (ADR-002):
// 1. Имена уникальны.
// 2. Имена непустые (sentinel `eosio::name{}` запрещён).
// 3. USER_SHARED-имена соответствуют конвенции `w.<3-char-contract>.<verb>`
// (символ 0 = 'w', символы 1 и 5 = '.').
//
// Полнота поля `kind` обеспечена структурно: в `Ledger2WalletMeta` поле без
// значения по умолчанию — попытка добавить элемент реестра без явного
// `WalletKind` ломает сборку.
namespace ledger2_wallets_detail {
// base32 кодирование eosio::name (`.12345abcdefghijklmnopqrstuvwxyz`):
// '.' = 0
// '1'..'5' = 1..5
// 'a'..'z' = 6..31
// 64-битное value делится на 12 5-битных позиций (старшие биты — первый символ);
// 13-й символ (4 младших бита) для финального реестра не используется (имена ≤ 13 символов).
static constexpr uint8_t CHAR_DOT = 0; // '.'
static constexpr uint8_t CHAR_W = 28; // 'w' = 6 + ('w' - 'a') = 6 + 22
constexpr uint8_t decode_char_at(uint64_t value, size_t pos /* 0..11 */) {
// pos = 0 — старшие 5 бит (биты 59..63)
return static_cast<uint8_t>((value >> (59 - 5 * pos)) & 0x1F);
}
constexpr bool wallet_names_unique() {
for (size_t i = 0; i < LEDGER2_WALLET_REGISTRY_SIZE; ++i) {
for (size_t j = i + 1; j < LEDGER2_WALLET_REGISTRY_SIZE; ++j) {
@@ -115,18 +153,34 @@ namespace ledger2_wallets_detail {
}
return true;
}
// ADR-002: USER_SHARED-имена обязаны соответствовать конвенции `w.<contract>.<verb>`.
// Конкретно: символ 0 = 'w', символ 1 = '.', символ 5 = '.' (после 3-символьного contract).
constexpr bool user_shared_naming_convention() {
for (size_t i = 0; i < LEDGER2_WALLET_REGISTRY_SIZE; ++i) {
const auto& e = LEDGER2_WALLET_REGISTRY[i];
if (e.kind != WalletKind::USER_SHARED) continue;
const uint64_t v = e.name.value;
if (decode_char_at(v, 0) != CHAR_W) return false; // 'w'
if (decode_char_at(v, 1) != CHAR_DOT) return false; // '.'
if (decode_char_at(v, 5) != CHAR_DOT) return false; // '.' после 3-char contract
}
return true;
}
}
static_assert(ledger2_wallets_detail::wallet_names_unique(),
"LEDGER2_WALLET_REGISTRY: duplicate wallet name detected");
static_assert(ledger2_wallets_detail::wallet_names_nonempty(),
"LEDGER2_WALLET_REGISTRY: empty eosio::name (sentinel) не должно быть в реестре");
static_assert(ledger2_wallets_detail::user_shared_naming_convention(),
"LEDGER2_WALLET_REGISTRY: USER_SHARED-имя не соответствует конвенции `w.<3-char-contract>.<verb>`");
/**
* @brief Возвращает human-readable имя кошелька по его eosio::name.
*
* Возвращает пустой `string_view` для незарегистрированных имён и для пустого
* имени (sentinel — кошелёк вне системы при ISSUE/REVOKE).
* имени (sentinel — кошелёк вне системы при ISSUE/BURN).
*/
inline constexpr std::string_view ledger2_get_wallet_human_name(eosio::name wallet_name) {
for (size_t i = 0; i < LEDGER2_WALLET_REGISTRY_SIZE; ++i) {
@@ -144,3 +198,18 @@ inline constexpr bool ledger2_is_known_wallet(eosio::name wallet_name) {
}
return false;
}
/**
* @brief Возвращает `WalletKind` кошелька по его `eosio::name` (ADR-002, ADR-010).
*
* Для имени, отсутствующего в реестре — `eosio::check(false, ...)` (фейлит tx).
* Используется на runtime-write-путях, прежде всего в Эпике 3 при walletop с username
* (валидация подмножества L3 ⊆ L2 USER_SHARED). Здесь же — общий доступ к классификации.
*/
inline WalletKind ledger2_get_wallet_kind(eosio::name wallet_name) {
for (size_t i = 0; i < LEDGER2_WALLET_REGISTRY_SIZE; ++i) {
if (LEDGER2_WALLET_REGISTRY[i].name == wallet_name) return LEDGER2_WALLET_REGISTRY[i].kind;
}
eosio::check(false, "ledger2_get_wallet_kind: wallet_name отсутствует в LEDGER2_WALLET_REGISTRY");
return WalletKind::COOPERATIVE; // unreachable
}
@@ -353,7 +353,7 @@ function walletDisplayId(name: string | null | undefined): string {
</div>
<div
v-if="op.wallet_op !== 'WALLET_ONLY' && (!!op.wallet_from || !!op.wallet_to)"
v-if="!!op.wallet_from || !!op.wallet_to"
class="focus-bar__op-sub"
>
<div class="focus-bar__op-sub-label">Переводы</div>
@@ -420,8 +420,7 @@ function walletDisplayId(name: string | null | undefined): string {
</div>
<div
v-if="focusedOperation.wallet_op !== 'WALLET_ONLY'
&& (!!focusedOperation.wallet_from || !!focusedOperation.wallet_to)"
v-if="!!focusedOperation.wallet_from || !!focusedOperation.wallet_to"
class="focus-bar__op-sub"
>
<div class="focus-bar__op-sub-label">Переводы</div>
@@ -339,9 +339,7 @@ function buildLayout(standard: Standard): LayoutResult {
const ops = opsByAction.get(t.action) ?? [];
const hasDocs = (docsByAction.get(t.action) ?? []).length > 0;
const hasWalletMove = ops.length > 0;
const hasPosting = ops.some(
(op) => op.wallet_op !== 'WALLET_ONLY' && (op.debit != null || op.credit != null),
);
const hasPosting = ops.some((op) => op.debit != null || op.credit != null);
nodes.push({
id: actionId,
@@ -11,7 +11,7 @@
*/
// ── Типы-строки для wallet_op / relation / status / role ────────────────────
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'WALLET_ONLY';
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'BURN';
export type StandardLifecycle = 'proposed' | 'approved' | 'active' | 'deprecated';
@@ -35,10 +35,10 @@ export type Role =
// ── Элементарные ссылки в ledger2 ───────────────────────────────────────────
// Счета — числовые коды (51, 80, 86); кошельки — eosio::name-строки
// (w.wal.share, w.cap.bgrid). Имена подтягиваются из
// (w.wal.share, w.cap.blago). Имена подтягиваются из
// `data/registries.ts` (зеркало source-of-truth из cooptypes/ledger2).
export type AccountCode = number; // код счёта (51, 80, 86, …)
export type WalletId = string; // имя кошелька (w.wal.share, w.cap.bgrid, …)
export type WalletId = string; // имя кошелька (w.wal.share, w.cap.blago, …)
// ── Секции стандарта ────────────────────────────────────────────────────────
@@ -5,7 +5,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
* Кошельки пайщиков живут в контракте soviet — они сюда не попадают.
*
* `id` — eosio::name-идентификатор `w.<contract>.<waltype>` (например
* `w.wal.share`, `w.cap.bginv`). См. `cooptypes/ledger2/wallets.ts`.
* `w.wal.share`, `w.cap.blago`). См. `cooptypes/ledger2/wallets.ts`.
*/
@ObjectType('Ledger2Wallet')
export class Ledger2WalletDTO {
@@ -122,18 +122,20 @@ export class Ledger2Service {
private resolveWalletAccountId(walletName: string): number | null {
for (const op of Ledger2.LEDGER2_OPERATION_REGISTRY) {
if (op.kind === 'adjustment') continue;
if (op.wallet_op === 'WALLET_ONLY') continue; // у WALLET_ONLY нет debit/credit
// Записи без бухпроводок (debit == null && credit == null, ADR-003) — пропускаем
if (op.debit === null && op.credit === null) continue;
if (op.wallet_from === walletName && op.credit !== null) return op.credit;
if (op.wallet_to === walletName && op.credit !== null && op.wallet_op === 'ISSUE') return op.credit;
if (op.wallet_to === walletName && op.debit !== null) return op.debit;
if (op.wallet_from === walletName && op.debit !== null) return op.debit;
}
// Кошельки, которые встречаются только в WALLET_ONLY (например w.cap.bginv в o.cap.invest)
// — выводим из источника: WALLET_ONLY переносит между кошельками одного счёта,
// значит партнёрский кошелёк по WALLET_ONLY имеет тот же account_id, что и
// его пара в стандартной операции.
// Кошельки, встречающиеся только в TRANSFER без бухпроводок (например w.cap.blago
// в o.cap.invest) — выводим из источника: такой TRANSFER переносит между
// кошельками одного счёта, значит партнёрский кошелёк имеет тот же account_id,
// что и его пара в стандартной операции.
for (const op of Ledger2.LEDGER2_OPERATION_REGISTRY) {
if (op.wallet_op !== 'WALLET_ONLY' || op.kind === 'adjustment') continue;
if (op.kind === 'adjustment') continue;
if (op.debit !== null || op.credit !== null) continue; // только без проводок
if (op.wallet_from === walletName && op.wallet_to !== null) {
return this.resolveWalletAccountId(op.wallet_to);
}
@@ -5,7 +5,7 @@
* аналитические разрезы бухгалтерских счетов уровня кооператива.
*
* `id` — eosio::name-идентификатор `w.<contract>.<waltype>` (например
* `w.wal.share`, `w.cap.bginv`, `w.reg.entry`). Полный реестр — в
* `w.wal.share`, `w.cap.blago`, `w.reg.entry`). Полный реестр — в
* `cooptypes/ledger2/wallets.ts` и `contracts/cpp/lib/core/ledger2/wallets.hpp`.
*/
export interface Ledger2WalletDomainInterface {
@@ -88,7 +88,7 @@ export const PROCESS_HASH_LOCATOR: Readonly<Record<string, HashLocation[]>> = Ob
'p.cap.import': [{ code: 'capital', table: 'contributors', field: 'contributor_hash' }],
// p.cap.invest — wallet-only перенос w.wal.share → w.cap.bginv без Dr/Cr.
// p.cap.invest — TRANSFER w.wal.share → w.cap.blago без бухпроводок (ADR-003).
'p.cap.invest': [{ code: 'capital', table: 'contributors', field: 'contributor_hash' }],
// capital::pgproperties.property_hash — приём имущества в паевой фонд.
@@ -91,8 +91,11 @@ export interface IRevert {
username: IName
amount: IAsset
/**
* 0=ISSUE, 1=TRANSFER, 4=WALLET_ONLY, 5=REVOKE.
* 0=ISSUE, 1=TRANSFER, 4=BURN.
* 2=BLOCK / 3=UNBLOCK не разрешены — они симметричны сами себе.
* Для зеркала ISSUE используется BURN; различие «штатное сжигание» vs «mirror revert»
* фиксируется через operation_code (`o.adj.rev`).
* Для зеркала без бухпроводок mirror_debit_account_id и mirror_credit_account_id оба == 0.
*/
mirror_wallet_op: number
mirror_wallet_from: IName
+13 -18
View File
@@ -11,11 +11,11 @@
*
* Идентификаторы кошельков (`wallet_from`/`wallet_to`) — eosio::name с
* префиксом `w.<contract>.<waltype>` (см. `./wallets.ts`). Пустая строка
* (`""`) — sentinel «кошелёк вне системы» для ISSUE и REVOKE.
* (`""`) — sentinel «кошелёк вне системы» для ISSUE и BURN.
*/
import type { IName } from '../interfaces/ledger2'
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'WALLET_ONLY' | 'REVOKE'
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'BURN'
export interface OperationMeta {
/** Машинный идентификатор — eosio::name в контракте. */
@@ -28,17 +28,17 @@ export interface OperationMeta {
name: string
/**
* Тип операции по кошельку. Для `kind: 'adjustment'` — null, потому что
* реальный wallet_op зависит от исходной операции (WALMOVE = WALLET_ONLY всегда,
* REVERSAL = REVOKE/TRANSFER/WALLET_ONLY в зависимости от зеркала).
* реальный wallet_op зависит от исходной операции (WALMOVE = TRANSFER без Dr/Cr,
* REVERSAL = TRANSFER/BURN в зависимости от зеркала).
*/
wallet_op: WalletOp | null
/** Кошелёк-источник (null для ISSUE и для adjustment-операций). */
wallet_from: IName | null
/** Кошелёк-приёмник (null для BLOCK/UNBLOCK и для adjustment-операций). */
/** Кошелёк-приёмник (null для BLOCK/UNBLOCK/BURN и для adjustment-операций). */
wallet_to: IName | null
/** Код счёта Дт (null для WALLET_ONLY и для adjustment-операций). */
/** Код счёта Дт (null без бухпроводки, ADR-003: ⇔ credit == null). */
debit: number | null
/** Код счёта Кт (null для WALLET_ONLY и для adjustment-операций). */
/** Код счёта Кт (null без бухпроводки, ADR-003: ⇔ debit == null). */
credit: number | null
/** Человекочитаемое название для UI. */
human_name: string
@@ -78,29 +78,29 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
debit: 80, credit: 51,
human_name: 'Возврат паевого взноса пайщику' },
// capital
// capital (ADR-009: единые программные кошельки `w.cap.blago`/`w.cap.gen`)
{ code: 'o.cap.import', process_type: 'p.cap.import', contract: 'capital',
name: 'IMPORT', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.bginv',
name: 'IMPORT', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.blago',
debit: 51, credit: 80,
human_name: 'Паевой взнос по ЦПП «Благорост» (офлайн-импорт)' },
{ code: 'o.cap.invest', process_type: 'p.cap.invest', contract: 'capital',
name: 'INVEST', wallet_op: 'WALLET_ONLY', wallet_from: 'w.wal.share', wallet_to: 'w.cap.bginv',
name: 'INVEST', wallet_op: 'TRANSFER', wallet_from: 'w.wal.share', wallet_to: 'w.cap.blago',
debit: null, credit: null,
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.gncom',
name: 'COMMIT_RID', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.gen',
debit: 8, credit: 80,
human_name: 'Коммит РИД по программе «Благорост»' },
{ code: 'o.cap.accept', process_type: 'p.cap.rid', contract: 'capital',
name: 'ACCEPT_RID', wallet_op: 'TRANSFER', wallet_from: 'w.cap.gncom', wallet_to: 'w.cap.bgrid',
name: 'ACCEPT_RID', wallet_op: 'TRANSFER', wallet_from: 'w.cap.gen', wallet_to: 'w.cap.blago',
debit: 4, credit: 8,
human_name: 'Приём РИД в паевой фонд' },
{ code: 'o.cap.actprp', process_type: 'p.cap.prop', contract: 'capital',
name: 'ACCEPT_PROPERTY', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.bgprop',
name: 'ACCEPT_PROPERTY', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.blago',
debit: 51, credit: 80,
human_name: 'Паевой взнос (имущественный) по программе «Благорост»' },
@@ -147,11 +147,6 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
debit: 51, credit: 86,
human_name: 'Транзит: вступительные взносы' },
{ code: 'o.mig.rid', process_type: 'p.mig.trans', contract: 'migration',
name: 'RID', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.wal.sharid',
debit: 4, credit: 80,
human_name: 'Транзит: принятые РИД в паевой фонд' },
// adjustment (ручные корректировки председателя — динамические параметры,
// не идут через ledger2::apply; см. operations.hpp `OPERATION_ADJUSTMENT_REGISTRY`).
{ code: 'o.adj.walmove', process_type: 'p.adj.fix', contract: 'ledger2',
+35 -30
View File
@@ -9,59 +9,64 @@
* (`p.<contract>.<noun>`). Длина ≤ 13 base32-символов.
*
* `human_name` — отображаемое имя в UI (standards-site, desktop).
*
* Классификация `WalletKind` (ADR-002):
* USER_SHARED — обязателен L3-разрез по пайщику (`ledger2::userwallets`).
* COOPERATIVE — единый кооперативный баланс, без L3.
*/
import type { IName } from '../interfaces/ledger2'
export type WalletKind = 'USER_SHARED' | 'COOPERATIVE'
export interface WalletMeta {
/** Машинный идентификатор — eosio::name в контракте. */
name: IName
/** Человекочитаемое название для UI. */
human_name: string
/** Тип кошелька: USER_SHARED — L3-разрез по пайщику; COOPERATIVE — единый баланс. */
kind: WalletKind
}
export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
// Группа паевого фонда (Cr 80)
{ name: 'w.wal.share', human_name: 'ЦПП «Цифровой Кошелёк» — паевые взносы деньгами' },
{ name: 'w.reg.minshr', human_name: 'Минимальный паевой взнос' },
{ name: 'w.wal.sharid', human_name: 'Паевой фонд — принятые РИД' },
// USER_SHARED (5) — L3-разрез по пайщику
{ name: 'w.reg.minshr', human_name: 'Минимальный паевой взнос', kind: 'USER_SHARED' },
{ name: 'w.wal.share', human_name: 'ЦК — паевая часть пайщика', kind: 'USER_SHARED' },
{ name: 'w.wal.member', human_name: 'ЦК — членская часть пайщика', kind: 'USER_SHARED' },
{ name: 'w.cap.blago', human_name: 'ЦПП «Благорост» — единый кошелёк программы у пайщика', kind: 'USER_SHARED' },
{ name: 'w.cap.gen', human_name: 'ЦПП «Генератор» — единый кошелёк программы у пайщика', kind: 'USER_SHARED' },
// Целевое финансирование (Cr 86)
{ name: 'w.reg.entry', human_name: 'Вступительные взносы' },
{ name: 'w.sov.member', human_name: 'Членские взносы (платформенные)' },
{ name: 'w.sov.delgte', human_name: 'Делегатские членские взносы' },
// Выплаты / обязательства / финансовые вложения
{ name: 'w.wal.wthdrw', human_name: 'Возвраты паевых взносов пайщикам' },
{ name: 'w.mkt.payout', human_name: 'Выплаты поставщикам' },
{ name: 'w.cap.loan', human_name: 'Выданные пайщикам беспроцентные займы' },
// Служебные
{ name: 'w.led.adjust', human_name: 'Ручные корректировки' },
// ЦПП «Благорост»
{ name: 'w.cap.bginv', human_name: 'ЦПП «Благорост» — инвестиции деньгами' },
{ name: 'w.cap.bgrid', human_name: 'ЦПП «Благорост» — принятые РИД' },
{ name: 'w.cap.bgprop', human_name: 'ЦПП «Благорост» — имущественные паевые взносы' },
{ name: 'w.cap.bgmem', human_name: 'ЦПП «Благорост» — членские взносы' },
// ЦПП «Генератор»
{ name: 'w.cap.gncom', human_name: 'ЦПП «Генератор» — принятый коммит (имущество)' },
{ name: 'w.cap.gnmem', human_name: 'ЦПП «Генератор» — членские взносы' },
// ЦПП «Стол Заказов»
{ name: 'w.mkt.fund', human_name: 'ЦПП «Стол Заказов» — общий кошелёк' },
// COOPERATIVE (6) — единый кооперативный баланс, без L3
{ name: 'w.reg.entry', human_name: 'Вступительные взносы', kind: 'COOPERATIVE' },
{ name: 'w.wal.wthdrw', human_name: 'Возвраты паевых взносов пайщикам', kind: 'COOPERATIVE' },
{ name: 'w.sov.infra', human_name: 'Членские взносы за инфраструктуру кооп. платформы', kind: 'COOPERATIVE' },
{ name: 'w.sov.delgte', human_name: 'Делегатские членские взносы', kind: 'COOPERATIVE' },
{ name: 'w.cap.loan', human_name: 'Выданные пайщикам беспроцентные займы', kind: 'COOPERATIVE' },
{ name: 'w.mkt.payout', human_name: 'Выплаты поставщикам', kind: 'COOPERATIVE' },
] as const
const walletHumanByName = new Map<string, string>(
LEDGER2_WALLET_REGISTRY.map((w) => [w.name, w.human_name]),
)
const walletKindByName = new Map<string, WalletKind>(
LEDGER2_WALLET_REGISTRY.map((w) => [w.name, w.kind]),
)
/**
* Возвращает человекочитаемое имя кошелька по его eosio::name-идентификатору.
* Возвращает undefined для незарегистрированных имён и для пустого имени
* (sentinel "" — кошелёк-источник/сток вне системы при ISSUE/REVOKE).
* (sentinel "" — кошелёк-источник/сток вне системы при ISSUE/BURN).
*/
export function getWalletHumanName(name: IName | null | undefined): string | undefined {
if (!name) return undefined
return walletHumanByName.get(name)
}
/**
* Возвращает `WalletKind` кошелька по его eosio::name (ADR-002, ADR-010).
* Возвращает undefined для незарегистрированных имён.
*/
export function getWalletKind(name: IName | null | undefined): WalletKind | undefined {
if (!name) return undefined
return walletKindByName.get(name)
}
@@ -128,14 +128,17 @@ const form = reactive({
function resolveAccountId(walletName: string): number | null {
for (const op of Ledger2.LEDGER2_OPERATION_REGISTRY) {
if (op.kind === 'adjustment') continue
if (op.wallet_op === 'WALLET_ONLY') continue
if (op.debit === null && op.credit === null) continue
if (op.wallet_from === walletName && op.credit !== null) return op.credit
if (op.wallet_to === walletName && op.wallet_op === 'ISSUE' && op.credit !== null) return op.credit
if (op.wallet_to === walletName && op.debit !== null) return op.debit
if (op.wallet_from === walletName && op.debit !== null) return op.debit
}
// Fallback: TRANSFER без бухпроводок (debit==null && credit==null), напр. o.cap.invest —
// привязка кошелька к счёту резолвится через парный кошелёк операции.
for (const op of Ledger2.LEDGER2_OPERATION_REGISTRY) {
if (op.wallet_op !== 'WALLET_ONLY' || op.kind === 'adjustment') continue
if (op.kind === 'adjustment') continue
if (op.debit !== null || op.credit !== null) continue
if (op.wallet_from === walletName && op.wallet_to !== null) return resolveAccountId(op.wallet_to)
if (op.wallet_to === walletName && op.wallet_from !== null) return resolveAccountId(op.wallet_from)
}