fix(ledger2,wallet): возврат паевого взноса — BURN_BLOCKED из заблокированной суммы

Завершение возврата паевого (o.wal.wthcpl) переключено с TRANSFER
(SHARE_FUND_PAY → WITHDRAWALS_SINK) на новый WalletOp::BURN_BLOCKED:
заблокированная при createwthd сумма сжигается на кошельке пайщика,
получателя на цепи нет — деньги уходят из системы банковским переводом.

Чинит assertion 'walletop TRANSFER: недостаточно L3-средств' на
completewthd: после REQUEST_WITHDRAW (BLOCK) сумма лежит в L3.blocked,
а TRANSFER проверял L3.available.

Изменения:
- ledger2: WalletOp::BURN_BLOCKED=6 + case в walletop.cpp (списание из
  L2/L3 blocked). OPERATION_REGISTRY: o.wal.wthcpl с BURN_BLOCKED, без
  wallet_to. burn_pattern_correct обобщён на оба BURN-варианта.
- cooptypes: WalletOp += 'BURN_BLOCKED'; o.wal.wthcpl wallet_to=null.
- wallets.hpp: WITHDRAWALS_SINK (w.wal.wthdrw) помечен DEPRECATED —
  остаётся в реестре для исторических L2-балансов.
- standard p.wal.wthdrw: приведён к коду — одностадийный flow
  (pending → authorized → completed), без approvewthd.
- wallet::approvewthd удалён как dead code: action не в whitelist
  callback'ов, из createwthd создаётся повестка сразу с callback=authwthd.
  Удалены: .cpp, declaration в wallet.hpp, include в wallet.cpp, struct
  и action в abi_json/wallet.json.
- sov.authpkg.standard.yaml: упоминание approvewthd → authwthd.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
coopops
2026-05-21 10:24:44 +00:00
parent bf3f9ca5ac
commit 33a414c7e7
10 changed files with 139 additions and 140 deletions
@@ -1,5 +1,5 @@
/**
* @brief Атомарная операция по кошельку (issue/transfer/block/unblock/burn).
* @brief Атомарная операция по кошельку (issue/transfer/block/unblock/burn/burn_blocked).
*
* Внутренний action ledger2 — вызывается только через inline из apply().
* Auth: только сам ledger2 (require_auth(get_self())).
@@ -56,7 +56,10 @@ void ledger2::walletop(eosio::name coopname,
// op_code = 5 (NONE) намеренно не допускается: NONE-операции — это только
// бухпроводка без кошелькового движения, apply.cpp не диспатчит для них walletop.
// Прямой вызов с op_code=5 был бы no-op и сбил бы инвариант parity.
eosio::check(op_code <= 4, "walletop: неизвестный op_code");
// Допустимы 0..4 (ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN) и 6 (BURN_BLOCKED).
eosio::check(op_code <= static_cast<uint8_t>(WalletOp::BURN_BLOCKED) &&
op_code != static_cast<uint8_t>(WalletOp::NONE),
"walletop: неизвестный op_code");
wallets2_index wallets(get_self(), coopname.value);
userwallets_index user_wallets(get_self(), coopname.value);
@@ -169,7 +172,7 @@ void ledger2::walletop(eosio::name coopname,
// миграций 048/049.
//
// walletop по построению применяет одно и то же `amount` к L2 и L3 (см.
// ниже case'ы ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN), а sender-guard на
// ниже case'ы ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN/BURN_BLOCKED), а sender-guard на
// строке 46-47 запрещает обход. Поэтому инвариант сохраняется по
// конструкции; полную сверку выполняет бэкенд («стол бухгалтера»),
// вне транзакционного hot path.
@@ -298,6 +301,36 @@ void ledger2::walletop(eosio::name coopname,
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.available -= amount; });
}
cleanup_l2_if_empty(wallet_from);
cleanup_l3_if_empty(wallet_from);
break;
}
case WalletOp::NONE: {
// Недостижимо: проверка op_code != NONE стоит на входе action; этот case
// нужен только чтобы покрыть enum в switch (-Wswitch).
eosio::check(false, "walletop NONE: запрещённый op_code");
break;
}
case WalletOp::BURN_BLOCKED: {
eosio::check(wallet_from.value != 0, "walletop BURN_BLOCKED: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BURN_BLOCKED: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop BURN_BLOCKED: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) { w.blocked -= amount; });
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop BURN_BLOCKED: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.blocked -= amount; });
}
cleanup_l2_if_empty(wallet_from);
cleanup_l3_if_empty(wallet_from);
break;
@@ -61,7 +61,7 @@ namespace operations {
// wallet
namespace wallet {
inline constexpr eosio::name COMPLETE_DEPOSIT = "o.wal.depcpl"_n; ///< Завершение внесения паевого взноса (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → WITHDRAWALS_SINK).
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY — деньги уходят из системы, без wallet_to).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
}
@@ -130,12 +130,13 @@ namespace operations {
* Compile-time правило `(debit==0) ⇔ (credit==0)` ловит смешанные пары.
*/
enum class WalletOp : uint8_t {
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).
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
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->available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
BURN_BLOCKED = 6, ///< изъятие amount из wallet_from->blocked, без wallet_to. Используется при завершении возврата паевого (`o.wal.wthcpl`): средства предварительно заблокированы через BLOCK (REQUEST_WITHDRAW), на завершении сжигаются — выходят из системы (получателя на цепи нет). На L3 синхронно: from_uw->blocked -= amount.
};
/**
@@ -144,7 +145,7 @@ enum class WalletOp : uint8_t {
* Семантика полей по `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{}.
* - BLOCK / UNBLOCK / BURN / BURN_BLOCKED: wallet_from = required, wallet_to = eosio::name{}.
*
* Семантика бух.проводки:
* - Без проводок: debit_account_id == 0 И credit_account_id == 0.
@@ -156,7 +157,7 @@ struct OperationRegistryEntry {
eosio::name process_type; ///< тип процесса с префиксом `p.<contract>.<noun>`
WalletOp wallet_op;
eosio::name wallet_from; ///< пустое имя для ISSUE
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN/BURN_BLOCKED
uint64_t debit_account_id; ///< 0 если без бухпроводки (тогда credit_account_id тоже == 0)
uint64_t credit_account_id; ///< 0 если без бухпроводки (тогда debit_account_id тоже == 0)
const char* human_name;
@@ -183,9 +184,12 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Внесение пайщиком паевого взноса" },
// 4. Возврат паевого взноса: Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → WITHDRAWALS_SINK
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::WITHDRAWALS_SINK,
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY.
// Сжигание из заблокированной суммы пайщика (BLOCK был на REQUEST_WITHDRAW):
// деньги уходят из системы (банковский перевод пайщику), получателя на цепи нет.
// Бухгалтерия: паевой фонд уменьшается (Дт 80), расчётный счёт уменьшается (Кт 51).
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN_BLOCKED,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Возврат паевого взноса пайщику" },
@@ -336,7 +340,7 @@ static constexpr size_t OPERATION_REGISTRY_SIZE = sizeof(OPERATION_REGISTRY) / s
// оба существуют в `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.
// 6. Для BLOCK / UNBLOCK / BURN / BURN_BLOCKED: `wallet_from` ≠ 0, `wallet_to` == 0.
// 7. Все id кошельков из записей существуют в `LEDGER2_WALLET_REGISTRY`.
namespace ledger2_registry_detail {
constexpr bool operation_codes_unique() {
@@ -380,11 +384,11 @@ namespace ledger2_registry_detail {
return true;
}
// Правило 6: BURN — wallet_from required, wallet_to == 0 (ADR-003).
// Правило 6: BURN / BURN_BLOCKED — 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_op != WalletOp::BURN && e.wallet_op != WalletOp::BURN_BLOCKED) continue;
if (e.wallet_from.value == 0) return false;
if (e.wallet_to.value != 0) return false;
}
@@ -436,7 +440,7 @@ static_assert(ledger2_registry_detail::dr_ne_cr_when_posting(),
static_assert(ledger2_registry_detail::transfer_wallet_from_ne_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");
"OPERATION_REGISTRY: BURN / BURN_BLOCKED требует wallet_from ≠ 0 и wallet_to == 0");
static_assert(ledger2_registry_detail::none_pattern_correct(),
"OPERATION_REGISTRY: NONE требует wallet_from == 0, wallet_to == 0 и обе проводки заполненными");
static_assert(ledger2_registry_detail::accounts_exist_in_map(),
@@ -46,7 +46,7 @@ struct ledger2_wallets {
// 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)
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: после переключения o.wal.wthcpl с TRANSFER на BURN_BLOCKED кошелёк больше не получает новых средств. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
// registrator — минимальный паевой + вступительные
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос пайщика (USER_SHARED, без сверки соглашений)
@@ -109,7 +109,7 @@ inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
// ломала convertsegm у пайщиков, чья доля выросла через CRPS.
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы", WalletKind::COOPERATIVE },
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", WalletKind::COOPERATIVE },
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::SOV_EXPENSES, "Хозяйственные расходы из числа целевого финансирования", WalletKind::COOPERATIVE },
@@ -10,7 +10,7 @@
# ↓
# authorized=true → exec → callback в исходный контракт (confirmreg /
# authmeet / approvereg / authpgprp / authrslt
# / authcontrib / approvewthd / ... — конкретный
# / authcontrib / authwthd / ... — конкретный
# handler заранее зашит в decision.type).
#
# Документ повестки приходит из контракта-инициатора (без registry_id —
@@ -2,13 +2,15 @@
# Стандарт «Возврат паевого взноса» — деньгами, через целевую потребительскую
# программу «Цифровой Кошелёк» (SHARE_FUND_PAY).
#
# Пятиактовый процесс с авторизацией советом. Запись заявки живёт в таблице
# `withdraws` от создания до выплаты или отказа. На закрывающем действии
# completewthd срабатывает одна операция o.wal.wthcpl (TRANSFER w.wal.share → w.wal.wthdrw).
# Четырёхактовый процесс с одной авторизацией советом. Запись заявки живёт в
# таблице `withdraws` от создания до выплаты или отказа. На закрывающем
# действии completewthd срабатывает одна операция o.wal.wthcpl — сжигание
# заблокированной суммы (BURN_BLOCKED w.wal.share): деньги уходят из системы
# пайщику банковским переводом, получателя на цепи нет.
#
# Источники правды в коде:
# • cpp/wallet/wallet.hpp — actions
# • cpp/wallet/src/withdraw/{createwthd,authwthd,approvewthd,completewthd,declinewthd}.cpp
# • cpp/wallet/src/withdraw/{createwthd,authwthd,completewthd,declinewthd}.cpp
# • cpp/lib/domain/table_wallet_withdraws.hpp — entity withdraws
# • cpp/lib/core/ledger2/operations.hpp — o.wal.wthcpl
# • cpp/lib/core/ledger2/processes.hpp — processes::wallet::WITHDRAW
@@ -22,8 +24,8 @@ slug: withdraw
status: proposed
contract: wallet
summary: >
Пайщик получает обратно ранее внесённый паевой взнос деньгами. Процесс
авторизуется советом и закрывается выплатой через Gateway.
Пайщик получает обратно ранее внесённый паевой взнос деньгами. Заявку
авторизует совет, кассир подтверждает выплату через Gateway.
purpose: >
«Возврат паевого взноса» — пайщик получает обратно ранее внесённые
деньги. Заявку рассматривает совет, после авторизации выплата
@@ -43,21 +45,14 @@ actions:
purpose: >
Пайщик создаёт заявку на возврат паевого взноса. Контракт блокирует
сумму на кошельке SHARE_FUND_PAY (статус available → blocked) и
создаёт повестку в совете о возврате.
- name: wallet::approvewthd
human: Одобрить решением совета
actor: soviet
role: progress
purpose: >
Совет добавляет к заявке документ-решение об одобрении возврата.
Заявка получает подписанное решение, но платёж ещё не отправлен.
создаёт повестку в совете о возврате с callback'ом авторизации.
- name: wallet::authwthd
human: Авторизовать выплату
actor: soviet
role: progress
purpose: >
Совет авторизует выплату: контракт меняет статус заявки на
`authorized` и отправляет в Gateway исходящий платёж с
Совет авторизует выплату единым решением: контракт переводит заявку
в статус `authorized` и отправляет в Gateway исходящий платёж с
callback'ами completewthd / declinewthd.
- name: wallet::completewthd
human: Подтвердить выплату
@@ -65,16 +60,16 @@ actions:
role: closer
purpose: >
Gateway подтвердил списание средств в пользу пайщика. Это закрывающее
действие: применяется операция o.wal.wthcpl (TRANSFER w.wal.share → w.wal.wthdrw),
разблокированная сумма уходит со счёта 80 на счёт 51, запись заявки
удаляется.
действие: применяется операция o.wal.wthcpl (BURN_BLOCKED w.wal.share)
— заблокированная сумма сжигается, в бухгалтерии проходит обратная
проводка Дт 80 / Кт 51. Запись заявки удаляется.
- name: wallet::declinewthd
human: Отклонить
actor: soviet
role: reject
purpose: >
Отказ на любом из этапов до выплаты. Сумма разблокируется на кошельке
пайщика, запись заявки удаляется, операция в ledger2 не создаётся.
пайщика (UNBLOCK через o.wal.wthdec), запись заявки удаляется.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: wallet::withdraw
@@ -88,12 +83,6 @@ states:
Заявка создана, сумма заблокирована на кошельке пайщика (available → blocked),
повестка отправлена в совет.
kind: normal
- name: approved
human: Решение совета подписано
description: >
Совет добавил к заявке документ с положительным решением, но
исходящий платёж в Gateway ещё не отправлен.
kind: normal
- name: authorized
human: Выплата отправлена
description: >
@@ -103,15 +92,15 @@ states:
- name: completed
human: Возврат выплачен
description: >
Gateway подтвердил списание, средства переведены с кошелька пайщика
на системный кошелёк выплат (w.wal.share → w.wal.wthdrw), в бухгалтерии прошла
обратная проводка Дт 80 / Кт 51. Запись заявки удалена.
Gateway подтвердил списание, заблокированная сумма сжигается на кошельке
пайщика (BURN_BLOCKED w.wal.share). В бухгалтерии прошла обратная
проводка Дт 80 / Кт 51. Запись заявки удалена.
kind: final
- name: removed
human: Отклонено
description: >
Возврат не состоялся: сумма разблокирована, запись удалена,
операция в ledger2 не создавалась.
Возврат не состоялся: сумма разблокирована (UNBLOCK через o.wal.wthdec),
запись удалена.
kind: virtual
virtual: true
@@ -120,6 +109,9 @@ transitions:
to: pending
action: wallet::createwthd
actor: contributor
ledger_code: o.wal.wthreq
operations:
- o.wal.wthreq
guards:
- Пайщик имеет статус active.
- Сумма возврата ≤ доступного остатка на SHARE_FUND_PAY (w.wal.share).
@@ -127,18 +119,11 @@ transitions:
- Заявление подписано ЭЦП пайщика.
- from: pending
to: approved
action: wallet::approvewthd
actor: soviet
guards:
- Решение совета по повестке возврата подписано.
- from: approved
to: authorized
action: wallet::authwthd
actor: soviet
guards:
- Документ авторизации совета подписан.
- Решение совета по повестке возврата подписано и авторизовано.
- from: authorized
to: completed
@@ -154,20 +139,19 @@ transitions:
to: removed
action: wallet::declinewthd
actor: soviet
ledger_code: o.wal.wthdec
operations:
- o.wal.wthdec
guards:
- Совет отклонил возврат либо платёж не прошёл.
- from: approved
to: removed
action: wallet::declinewthd
actor: soviet
guards:
- Совет отклонил возврат либо платёж не прошёл.
- Совет отклонил возврат.
- from: authorized
to: removed
action: wallet::declinewthd
actor: gateway_operator
ledger_code: o.wal.wthdec
operations:
- o.wal.wthdec
guards:
- Платёж не прошёл / отклонён Gateway.
@@ -182,7 +166,7 @@ scenario:
Пайщик оформляет и подписывает заявление о возврате паевого взноса.
Контракт создаёт запись в таблице withdraws со статусом `pending`,
блокирует сумму на кошельке SHARE_FUND_PAY и направляет в совет
повестку о возврате.
повестку о возврате с callback'ом авторизации.
pre:
- Пайщик — активный участник кооператива.
- Сумма возврата ≤ доступного остатка.
@@ -192,46 +176,34 @@ scenario:
- Совет получил повестку о возврате.
- step: 2
title: Одобрение советом
actor: soviet
action: wallet::approvewthd
description: >
Совет принимает положительное решение по повестке возврата и
добавляет к заявке документ-решение. Статус заявки → `approved`.
pre:
- Заявка в статусе `pending`.
- Решение совета подписано.
post:
- Статус заявки `approved`, документ-решение сохранён.
- step: 3
title: Авторизация выплаты
title: Авторизация выплаты советом
actor: soviet
action: wallet::authwthd
description: >
Совет подписывает авторизацию выплаты — контракт инициирует
исходящий платёж в Gateway с callback'ами completewthd / declinewthd.
Совет принимает положительное решение по повестке возврата и
авторизует выплату единым шагом. Контракт инициирует исходящий
платёж в Gateway с callback'ами completewthd / declinewthd.
Статус заявки → `authorized`.
pre:
- Заявка в статусе `approved`.
- Документ авторизации совета подписан.
- Заявка в статусе `pending`.
- Решение совета подписано и авторизовано председателем.
post:
- Статус заявки `authorized`.
- В Gateway создан исходящий платёж с callback'ами.
- step: 4
- step: 3
title: Подтверждение выплаты
actor: gateway_operator
action: wallet::completewthd
description: >
Gateway подтверждает списание средств в пользу пайщика. Контракт
применяет ledger2-операцию o.wal.wthcpl (TRANSFER w.wal.share → w.wal.wthdrw с
применяет ledger2-операцию o.wal.wthcpl (BURN_BLOCKED w.wal.share с
обратной проводкой Дт 80 / Кт 51) и удаляет запись заявки.
pre:
- Заявка в статусе `authorized`.
- Gateway подтвердил списание.
post:
- Сумма переведена с SHARE_FUND_PAY (w.wal.share) на WITHDRAWALS (w.wal.wthdrw).
- Заблокированная сумма сожжена с SHARE_FUND_PAY (w.wal.share).
- В ledger2 применена операция o.wal.wthcpl.
- Запись заявки удалена.
@@ -242,8 +214,8 @@ scenario:
actor: soviet
description: >
Совет принял отрицательное решение либо Gateway отклонил платёж.
Сумма разблокирована на кошельке пайщика, запись удалена, операция
в ledger2 не создаётся.
Сумма разблокирована на кошельке пайщика (UNBLOCK через o.wal.wthdec),
запись удалена.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
documents:
@@ -254,12 +226,6 @@ documents:
stored_in: withdraws.statement
- step: 2
title: Решение совета о возврате паевого взноса (одобрение)
registry_id: 901
signed_by: [soviet]
stored_in: withdraws.approved_statement
- step: 3
title: Решение совета о возврате паевого взноса (авторизация на выплату)
registry_id: 901
signed_by: [soviet]
@@ -267,18 +233,46 @@ documents:
# ── Секция 6. Операции ──────────────────────────────────────────────────────
operations:
- ledger_code: o.wal.wthreq
human_name: Блокировка паевого под запрос на возврат
wallet_op: BLOCK
wallet_from: w.wal.share
wallet_to: ~
debit: ~
credit: ~
amount_ref: withdraw.quantity
triggered_by: wallet::createwthd
description: >
На создании заявки контракт блокирует сумму на кошельке пайщика
(SHARE_FUND_PAY): available → blocked. Бухгалтерская проводка не
создаётся — движение внутри одного бухсчёта 80.
- ledger_code: o.wal.wthdec
human_name: Разблокировка паевого после отклонения запроса на возврат
wallet_op: UNBLOCK
wallet_from: w.wal.share
wallet_to: ~
debit: ~
credit: ~
amount_ref: withdraw.quantity
triggered_by: wallet::declinewthd
description: >
Зеркало REQUEST_WITHDRAW: blocked → available на кошельке пайщика.
Бухгалтерская проводка не создаётся.
- ledger_code: o.wal.wthcpl
human_name: Возврат паевого взноса пайщику
wallet_op: TRANSFER
wallet_op: BURN_BLOCKED
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
wallet_to: w.wal.wthdrw # Возвраты паевых взносов пайщикам
wallet_to: ~ # деньги уходят из системы, получателя на цепи нет
debit: 80 # Паевой фонд (складочный капитал)
credit: 51 # Расчётный счёт
amount_ref: withdraw.quantity
triggered_by: wallet::completewthd
description: >
Обратная операция к o.wal.depcpl: средства уходят с кошелька пайщика
«ЦПП Цифровой Кошелёк» (w.wal.share) на системный кошелёк выплат (w.wal.wthdrw).
Обратная операция к o.wal.depcpl: заблокированная сумма сжигается с
кошелька пайщика «ЦПП Цифровой Кошелёк» (w.wal.share) — деньги уходят
из системы банковским переводом пайщику. На цепи получателя нет.
В бухгалтерии прошла проводка Дт 80 / Кт 51 — паевой фонд уменьшился,
деньги ушли с расчётного счёта пайщику.
@@ -1,30 +0,0 @@
void wallet::approvewthd(eosio::name coopname, checksum256 withdraw_hash, document2 approved_statement) {
require_auth(_soviet);
auto exist_withdraw = Wallet::get_withdraw(coopname, withdraw_hash);
eosio::check(exist_withdraw.has_value(), "Объект возврата паевого взноса не найден");
Wallet::withdraws_index withdraws(_wallet, coopname.value);
auto withdraw = withdraws.find(exist_withdraw -> id);
withdraws.modify(withdraw, _wallet, [&](auto &w){
w.status = "approved"_n;
w.approved_statement = approved_statement;
});
//отправляем в совет
action(permission_level{ _wallet, "active"_n}, _soviet, "createagenda"_n,
std::make_tuple(
coopname,
withdraw -> username,
get_valid_soviet_action("withdraw"_n),
withdraw_hash,
_wallet,
"authwthd"_n,
"declinewthd"_n,
withdraw -> statement,
std::string("")
)
).send();
}
@@ -6,7 +6,6 @@
#include "src/deposit/createdpst.cpp"
#include "src/deposit/declinedpst.cpp"
#include "src/withdraw/approvewthd.cpp"
#include "src/withdraw/authwthd.cpp"
#include "src/withdraw/completewthd.cpp"
#include "src/withdraw/createwthd.cpp"
@@ -28,7 +28,6 @@ public:
[[eosio::action]] void completewthd(COMPLETEWTHD_SIGNATURE);
[[eosio::action]] void declinewthd(DECLINEWTHD_SIGNATURE);
[[eosio::action]] void authwthd(AUTHWTHD_SIGNATURE);
[[eosio::action]] void approvewthd(eosio::name coopname, checksum256 withdraw_hash, document2 approved_statement);
// программные соглашения (Эпик 2 компонента 48; ADR-008)
[[eosio::action]] void signagree(eosio::name coopname, eosio::name username, uint64_t program_id, document2 document, uint64_t draft_id);
@@ -1 +1 @@
{"account_name":"wallet","abi":{"version":"eosio::abi/1.2","types":[],"structs":[{"name":"approvewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"approved_statement","type":"document"}]},{"name":"authwthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"}]},{"name":"completedpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"deposit_hash","type":"checksum256"}]},{"name":"completewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"memo","type":"string"}]},{"name":"createdpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"quantity","type":"asset"}]},{"name":"createwthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"quantity","type":"asset"},{"name":"statement","type":"document"}]},{"name":"declinedpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"reason","type":"string"}]},{"name":"declinewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"reason","type":"string"}]},{"name":"deposit","base":"","fields":[{"name":"id","type":"uint64"},{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"quantity","type":"asset"},{"name":"status","type":"name"},{"name":"created_at","type":"time_point_sec"}]},{"name":"document","base":"","fields":[{"name":"hash","type":"checksum256"},{"name":"public_key","type":"public_key"},{"name":"signature","type":"signature"},{"name":"meta","type":"string"}]},{"name":"migrate","base":"","fields":[]},{"name":"withdraw","base":"","fields":[{"name":"id","type":"uint64"},{"name":"username","type":"name"},{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"status","type":"name"},{"name":"quantity","type":"asset"},{"name":"statement","type":"document"},{"name":"approved_statement","type":"document"},{"name":"authorization","type":"document"},{"name":"created_at","type":"time_point_sec"}]}],"actions":[{"name":"approvewthd","type":"approvewthd","ricardian_contract":""},{"name":"authwthd","type":"authwthd","ricardian_contract":""},{"name":"completedpst","type":"completedpst","ricardian_contract":""},{"name":"completewthd","type":"completewthd","ricardian_contract":""},{"name":"createdpst","type":"createdpst","ricardian_contract":""},{"name":"createwthd","type":"createwthd","ricardian_contract":""},{"name":"declinedpst","type":"declinedpst","ricardian_contract":""},{"name":"declinewthd","type":"declinewthd","ricardian_contract":""},{"name":"migrate","type":"migrate","ricardian_contract":""}],"tables":[{"name":"deposits","index_type":"i64","key_names":[],"key_types":[],"type":"deposit"},{"name":"withdraws","index_type":"i64","key_names":[],"key_types":[],"type":"withdraw"}],"ricardian_clauses":[],"error_messages":[],"abi_extensions":[],"variants":[],"action_results":[]}}
{"account_name":"wallet","abi":{"version":"eosio::abi/1.2","types":[],"structs":[{"name":"authwthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"}]},{"name":"completedpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"deposit_hash","type":"checksum256"}]},{"name":"completewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"memo","type":"string"}]},{"name":"createdpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"quantity","type":"asset"}]},{"name":"createwthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"quantity","type":"asset"},{"name":"statement","type":"document"}]},{"name":"declinedpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"reason","type":"string"}]},{"name":"declinewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"reason","type":"string"}]},{"name":"deposit","base":"","fields":[{"name":"id","type":"uint64"},{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"quantity","type":"asset"},{"name":"status","type":"name"},{"name":"created_at","type":"time_point_sec"}]},{"name":"document","base":"","fields":[{"name":"hash","type":"checksum256"},{"name":"public_key","type":"public_key"},{"name":"signature","type":"signature"},{"name":"meta","type":"string"}]},{"name":"migrate","base":"","fields":[]},{"name":"withdraw","base":"","fields":[{"name":"id","type":"uint64"},{"name":"username","type":"name"},{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"status","type":"name"},{"name":"quantity","type":"asset"},{"name":"statement","type":"document"},{"name":"approved_statement","type":"document"},{"name":"authorization","type":"document"},{"name":"created_at","type":"time_point_sec"}]}],"actions":[{"name":"authwthd","type":"authwthd","ricardian_contract":""},{"name":"completedpst","type":"completedpst","ricardian_contract":""},{"name":"completewthd","type":"completewthd","ricardian_contract":""},{"name":"createdpst","type":"createdpst","ricardian_contract":""},{"name":"createwthd","type":"createwthd","ricardian_contract":""},{"name":"declinedpst","type":"declinedpst","ricardian_contract":""},{"name":"declinewthd","type":"declinewthd","ricardian_contract":""},{"name":"migrate","type":"migrate","ricardian_contract":""}],"tables":[{"name":"deposits","index_type":"i64","key_names":[],"key_types":[],"type":"deposit"},{"name":"withdraws","index_type":"i64","key_names":[],"key_types":[],"type":"withdraw"}],"ricardian_clauses":[],"error_messages":[],"abi_extensions":[],"variants":[],"action_results":[]}}
@@ -11,11 +11,11 @@
*
* Идентификаторы кошельков (`wallet_from`/`wallet_to`) — eosio::name с
* префиксом `w.<contract>.<waltype>` (см. `./wallets.ts`). Пустая строка
* (`""`) — sentinel «кошелёк вне системы» для ISSUE и BURN.
* (`""`) — sentinel «кошелёк вне системы» для ISSUE, BURN и BURN_BLOCKED.
*/
import type { IName } from '../interfaces/ledger2'
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'BURN' | 'NONE'
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'BURN' | 'BURN_BLOCKED' | 'NONE'
export interface OperationMeta {
/** Машинный идентификатор — eosio::name в контракте. */
@@ -34,7 +34,7 @@ export interface OperationMeta {
wallet_op: WalletOp | null
/** Кошелёк-источник (null для ISSUE и для adjustment-операций). */
wallet_from: IName | null
/** Кошелёк-приёмник (null для BLOCK/UNBLOCK/BURN и для adjustment-операций). */
/** Кошелёк-приёмник (null для BLOCK/UNBLOCK/BURN/BURN_BLOCKED и для adjustment-операций). */
wallet_to: IName | null
/** Код счёта Дт (null без бухпроводки, ADR-003: ⇔ credit == null). */
debit: number | null
@@ -84,7 +84,7 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
human_name: 'Разблокировка паевого после отклонения запроса на возврат' },
{ code: 'o.wal.wthcpl', process_type: 'p.wal.wthdrw', contract: 'wallet',
name: 'COMPLETE_WITHDRAW', wallet_op: 'TRANSFER', wallet_from: 'w.wal.share', wallet_to: 'w.wal.wthdrw',
name: 'COMPLETE_WITHDRAW', wallet_op: 'BURN_BLOCKED', wallet_from: 'w.wal.share', wallet_to: null,
debit: 80, credit: 51,
human_name: 'Возврат паевого взноса пайщику' },