|
COOPENOMICS
v1
Кооперативная Экономика
|
Функции | |
| void | apps::cleanup (eosio::name package_id) |
Ручной cleanup TTL-просроченных superseded-записей пакета. Подробнее... | |
| void | apps::delclient (eosio::name catalog_operator, eosio::name client_coopname) |
| Отозвать кооператив-клиента каталога (FR8, D3). Подробнее... | |
| void | apps::expsub (eosio::name coopname, eosio::name subscriber, eosio::name package_id) |
Деактивировать подписку (active=false); row не удаляется (нужен для аудита истечения). Подробнее... | |
| void | apps::extendsub (eosio::name catalog_operator, eosio::name subscriber, eosio::name package_id, uint32_t period_seconds, eosio::checksum256 charge_intent_id) |
Идемпотентно продлить подписку через charge_intent_id (FR24, D4, D5). Подробнее... | |
| void | apps::reactivate (eosio::name coopname, eosio::name package_id, std::string version) |
| Реактивировать предыдущую версию пакета (FR43). Подробнее... | |
| void | apps::regclient (eosio::name catalog_operator, eosio::name client_coopname) |
| Зарегистрировать кооператив-клиента каталога (FR7, D3). Подробнее... | |
| void | apps::regcoop (eosio::name coopname, eosio::checksum256 chain_id, eosio::name subnet_label, eosio::public_key signing_key) |
| Зарегистрировать кооператив в каталоге. Подробнее... | |
| void | apps::regpackage (eosio::name coopname, eosio::name package_id, std::string package_name, eosio::name owner, std::vector< eosio::name > compatible_subnets) |
| Зарегистрировать новый пакет в каталоге. Подробнее... | |
| void | apps::regsub (eosio::name coopname, eosio::name subscriber, eosio::name package_id, eosio::checksum256 chain_id, eosio::name plan, eosio::time_point_sec start_at, eosio::time_point_sec end_at) |
| Зарегистрировать или продлить подписку кооператива на пакет. Подробнее... | |
| void | apps::setattempt (eosio::name catalog_operator, eosio::name subscriber, eosio::name package_id, uint8_t attempt) |
Установить attempt-счётчик для подписки (D4 retry-management). Подробнее... | |
| void | apps::setcoop (eosio::name coopname, std::optional< eosio::checksum256 > chain_id, std::optional< eosio::name > subnet_label, std::optional< eosio::public_key > signing_key, std::optional< bool > active) |
| Обновить параметры кооператива (ротация ключа, перенос подсети, деактивация). Подробнее... | |
| void | apps::setglobals (uint32_t min_payment_period_seconds, uint32_t free_trial_period_seconds, uint32_t lead_time_seconds, uint8_t retry_max) |
| Установить общесистемные параметры каталога (FR2, D2). Подробнее... | |
| void | apps::setpricing (eosio::name package_id, eosio::name plan, eosio::asset hourly_rate) |
Установить ставку плана plan для пакета package_id (FR1, D1). Подробнее... | |
| void | apps::setrelease (eosio::name coopname, eosio::name package_id, std::string version, Apps::scope_t scope, eosio::checksum256 tarball_sha256, eosio::name moderated_by, std::string meta) |
| Опубликовать новый release пакета. Подробнее... | |
| void | apps::transferpkg (eosio::name coopname, eosio::name package_id, eosio::name new_owner) |
| Передать владение пакетом другому пользователю (FR3). Подробнее... | |
| void | apps::withdraw (eosio::name coopname, eosio::name package_id, std::string version, std::string reason) |
| Отозвать релиз (CVE / нарушение / юридический отзыв). Подробнее... | |
| void apps::cleanup | ( | eosio::name | package_id | ) |
Ручной cleanup TTL-просроченных superseded-записей пакета.
Удаляет до CLEANUP_BUDGET_PER_CALL записей с superseded_at < now - RELEASE_RETENTION_SECS. Идемпотентен.
Используется когда setrelease для пакета давно не вызывался, и inline-cleanup в нём не подметает хвост. Удаляет до CLEANUP_BUDGET_PER_CALL записей с status=superseded и superseded_at < now - RELEASE_RETENTION_SECS.
Идемпотентно. Многократные вызовы безопасны: каждый вызов чистит свой batch до budget'а.
| void apps::delclient | ( | eosio::name | catalog_operator, |
| eosio::name | client_coopname | ||
| ) |
Отозвать кооператив-клиента каталога (FR8, D3).
Отозвать кооператив-клиента каталога (Story v2.4.1, FR8).
Удаляет row по PK; row нет → eosio_assert("client not found") (не идемпотентно сознательно — отличаем «уже отзывали» от «никогда не регистрировали»). Эффекты на CA-стороне: инвалидация membership-cache (D9 hybrid TTL) + добавление активных JWT в JwtRevokeList (FR15) живут off-chain.
require_auth(catalog_operator).Что делает действие:
catalog_operator @ active — require_auth(catalog_operator).client_coopname непустое.eosio_assert("client not found"). Идемпотентность сознательно НЕ соблюдается: повторный delclient должен вернуть явную ошибку, чтобы вышестоящие watcher'ы (cache-invalidation, JWT-revocation в CA) могли отличить «уже отзывали» от «никогда не было».Эффекты на TS-стороне (живут в apps-catalog, не на цепи):
client_coopname инвалидируется через hybrid-TTL (D9): критический путь забирает уже актуальный ответ из chain'а, read-API получает stale-ответ ≤ 60s.coopname=client_coopname помещаются в JwtRevokeList (FR15): пока не истечёт TTL токена — он отозван по jti.RAM payer возвращается catalog_operator. subs записи кооператива НЕ удаляются автоматически — это решение оператора, и история подписок остаётся в subs для аудита (флаг active=false выставляется отдельным expsub-вызовом).
| void apps::expsub | ( | eosio::name | coopname, |
| eosio::name | subscriber, | ||
| eosio::name | package_id | ||
| ) |
Деактивировать подписку (active=false); row не удаляется (нужен для аудита истечения).
Деактивировать подписку.
coopname @ active.Ставит active=false. Запись остаётся в таблице — она нужна для аудита истечения и для повторной активации (если кооператив снова подпишется, пойдёт через тот же row через regsub-upsert).
Cleanup физически просроченных записей (например, истёкших год назад и более не нужных в RAM) — задача отдельного периодического действия. Вне scope MVP.
coopname @ active. | void apps::extendsub | ( | eosio::name | catalog_operator, |
| eosio::name | subscriber, | ||
| eosio::name | package_id, | ||
| uint32_t | period_seconds, | ||
| eosio::checksum256 | charge_intent_id | ||
| ) |
Идемпотентно продлить подписку через charge_intent_id (FR24, D4, D5).
Идемпотентное продление подписки через charge_intent_id (D4, D5; story v2.5/v2.6, FR24).
Находит regsub по (subscriber, package_id); проверяет, что last_charge_intent_id != charge_intent_id. Иначе — eosio_assert("already extended") (это ok-сигнал для recovery-worker'а: дубль ack'а из кабинета или повтор после CA-crash). На успешном пути:
end_at += period_seconds (extend от текущей границы, не от now — important для непрерывности подписки),last_charge_intent_id = charge_intent_id,attempt = 0 (счётчик retry'ев сбрасывается на удачном charge'е),updated_at = now.Story v2.5.2 (TS write-port extendByChargeIntent) ожидает именно эту семантику; AlreadyExtendedError маппится с on-chain assert'а.
| catalog_operator | оператор каталога (платит RAM, подписывает). |
| subscriber | кооператив-подписчик. |
| package_id | пакет. |
| period_seconds | на сколько продлить (в секундах). Должен быть > 0. |
| charge_intent_id | UUIDv5 от (coopname, package_id, period_start_at). |
require_auth(catalog_operator).Что делает действие:
catalog_operator @ active.subscriber, package_id непустые, period_seconds > 0.regsub по (subscriber, package_id) через secondary index bycooppkg. Если нет — eosio_assert("subscription not found") (вызов до regsub — программная ошибка CA-стороны, а не штатный control-flow).last_charge_intent_id != charge_intent_id. Если уже этот intent был применён — eosio_assert("already extended"). Это и есть double-emit prevention уровня цепи (D5): UUIDv5-детерминированный id + on-chain unique-by-last проверка закрывают двойное продление при race'ах watcher-recovery или повторных webhook'ах из кабинета.regsub:end_at += period_seconds (extend ОТ текущей границы, не от now — это важно для непрерывности оплаченных интервалов; см. AR12).last_charge_intent_id = charge_intent_id.attempt = 0 (счётчик retry'ев сбрасывается на удачном charge'е).updated_at = now.RAM payer — catalog_operator (оператор оплачивает координацию; клиент-кооператив за regsub-row не платит).
| void apps::reactivate | ( | eosio::name | coopname, |
| eosio::name | package_id, | ||
| std::string | version | ||
| ) |
Реактивировать предыдущую версию пакета (FR43).
Находит release по (package_id, version); ставит ему status=active, superseded_at=0. Текущий active с тем же scope → superseded. Если scope=all — обновляет packages.last_active_version.
coopname @ active.Используется для отката broken release без full re-publish:
(package_id, version) со статусом superseded или withdrawn.scope → superseded.active, superseded_at=0.packages.last_active_version.Окно реактивации ограничено TTL retention (90 дней): за пределами окна superseded-записи уже удалены, и реактивация не возможна — нужен полноценный re-publish.
coopname @ active. | void apps::regclient | ( | eosio::name | catalog_operator, |
| eosio::name | client_coopname | ||
| ) |
Зарегистрировать кооператив-клиента каталога (FR7, D3).
Зарегистрировать кооператив-клиента каталога (Story v2.4.1).
clients-row {scope=catalog_operator, PK=client_coopname} с registered_at = now. Идемпотентность строгая: повторный вызов на ту же пару → eosio_assert("client already registered"). RAM payer — catalog_operator (ВОСХОД оплачивает onboarding).
| catalog_operator | оператор каталога, подписывает транзакцию и платит RAM (в MVP всегда voskhod). |
| client_coopname | кооператив, который подключается к каталогу. |
require_auth(catalog_operator).Что делает действие:
catalog_operator @ active — require_auth(catalog_operator). В MVP это всегда voskhod, но контракт не хардкодит имя — на проверке authority единственное допустимое подписавшее лицо это и есть scope записи.client_coopname непустое (eosio::name(0) — особое значение «отсутствие», не валидное имя).emplace. Row есть → eosio_assert("client already registered"). Это намеренный «строгий» upsert — повторный regclient без явного delclient должен ловить ошибки оператора (двойной вызов из-за UI-сбоя).RAM payer — catalog_operator. ВОСХОД оплачивает RAM записи как часть стоимости подключения кооператива; клиент за хранение clients-row не платит.
Что не делает (выносится в TS-сторону, Story v2.4.5):
coops-записи у client_coopname. Кооператив может быть подключён к каталогу до того, как оформлен в coops (signing_key, chain_id — это уровень subnet-операций). Каталог проверит это сам перед issue'ем JWT.audit_log_admin через regsub-watcher CA. | void apps::regcoop | ( | eosio::name | coopname, |
| eosio::checksum256 | chain_id, | ||
| eosio::name | subnet_label, | ||
| eosio::public_key | signing_key | ||
| ) |
Зарегистрировать кооператив в каталоге.
Регистрация кооператива в каталоге.
Этот action — часть процесса подключения коопа к каталогу приложений на Восходе (вне scope текущего MVP). Кооператив сам подписывает транзакцию (require_auth(coopname)), подтверждая, что владеет приватной частью signing_key.
| coopname | кооператив, который регистрируется. |
| chain_id | подсеть, к которой принадлежит коопе. |
| subnet_label | human-label подсети ("ru","by",...). |
| signing_key | subnet-signing-key (отдельный от @ active). |
coopname @ active. Идемпотентность: фейл, если уже зарегистрирован — для обновления использовать setcoop.Кооператив сам подписывает транзакцию своим @active-ключом и передаёт публичную часть signing_key (отдельный ключ для подписи signed-request'ов от DC-оркестратора в CA-auth).
Идемпотентность: если такой coopname уже зарегистрирован — фейл. Для обновления полей — setcoop.
coopname @ active. | void apps::regpackage | ( | eosio::name | coopname, |
| eosio::name | package_id, | ||
| std::string | package_name, | ||
| eosio::name | owner, | ||
| std::vector< eosio::name > | compatible_subnets | ||
| ) |
Зарегистрировать новый пакет в каталоге.
Регистрация нового пакета в каталоге.
| coopname | провайдер каталога (тот, кто подписал транзакцию). |
| package_id | кодовое имя пакета (Antelope name, ≤12 chars). |
| package_name | внешнее имя (npm/go/oci) — может содержать @scope/name. |
| owner | username владельца пакета. |
| compatible_subnets | human-labels подсетей, в которых пакет совместим. |
coopname @ active. Идемпотентность: фейл, если package_id уже зарегистрирован (нужен явный transferpkg/withdraw перед re-регистрацией).Контракт операции:
package_id. Если такой уже существует — фейлим («имя занято»).package_name — внешнее имя; не уникально (теоретически два пакета могут иметь одинаковое npm-имя, но разные package_id; на практике это ловится UI/правилами кабинета ВОСХОД).compatible_subnets — может быть пустой; в этом случае пакет считается совместимым со всеми подсетями (resolver на стороне CA обрабатывает пустой набор как «нет ограничений»).last_active_version инициализируется пустой строкой; заполнится первым setrelease со scope=all.coopname @ active. | void apps::regsub | ( | eosio::name | coopname, |
| eosio::name | subscriber, | ||
| eosio::name | package_id, | ||
| eosio::checksum256 | chain_id, | ||
| eosio::name | plan, | ||
| eosio::time_point_sec | start_at, | ||
| eosio::time_point_sec | end_at | ||
| ) |
Зарегистрировать или продлить подписку кооператива на пакет.
Регистрация или продление подписки кооператива на пакет.
Idempotent по (coopname, package_id):
active=true.plan, end_at = max(текущее, новое), active=true, chain_id (на случай переноса между подсетями). | coopname | провайдер каталога (тот, кто выписывает подписку). |
| subscriber | кооператив-подписчик. |
| chain_id | подсеть, в которой действует подписка. |
coopname @ active.(subscriber, package_id):Row нет → создаём с active=true, start_at, end_at, plan, chain_id.
plan, end_at = max(текущее, новое), active=true, chain_id (на случай миграции коопа между подсетями), start_at оставляем как был (это маркер «когда вообще началась подписка»).Это поведение специально сознательное: биллинг кабинета ВОСХОД при retry / повторной отправке не должен бояться дубликатов. История выписки подписок остаётся в blockchain trace'ах (Antelope action history), а не в таблице subs.
| coopname | провайдер каталога (тот, кто выписывает подписку). |
| subscriber | кооператив-подписчик. |
| chain_id | подсеть, в которой действует подписка. |
coopname @ active. Проверки chain_id против coops.chain_id НЕ делаем, потому что: 1) coops row может ещё не существовать при первой выдаче подписки; 2) подписка теоретически может быть выдана коопу из другой подсети (миграция). Лучше доверять биллингу. | void apps::setattempt | ( | eosio::name | catalog_operator, |
| eosio::name | subscriber, | ||
| eosio::name | package_id, | ||
| uint8_t | attempt | ||
| ) |
Установить attempt-счётчик для подписки (D4 retry-management).
Установить attempt-счётчик в regsub (D4 retry-management).
Простая запись uint8 в существующую regsub-row. Используется pricing-watcher'ом CA, когда ack=declined или charge провалился (типичный путь: setattempt(coopname, package_id, current+1)). Каскад с extendsub не нужен — последний сам сбросит attempt в 0 при успехе.
Альтернатива (on-chain incrattempt без передачи нового значения) могла бы избежать сравнения counter'ов на гонках, но усложняет контракт и редко полезна: pricing-watcher tick — единственный путь, и он сериализован per-(coopname, package_id) на стороне watcher-state в Postgres.
require_auth(catalog_operator).Pricing-watcher CA вызывает это действие, когда ack=declined или charge не дошёл, для отметки факта повторной попытки. Контракт не вычисляет current+1 сам — watcher держит state в Postgres (charge_intents.attempt) и передаёт нужное значение явно. Так проще: любая ошибочная гонка фиксируется на уровне Postgres-инкремента, а контракт остаётся stateless относительно retry-логики.
extendsub сам сбрасывает attempt=0 при удачном charge'е, поэтому pricing-watcher не должен дёргать setattempt(0) после успеха.attempt > globals.retry_max watcher эмитит SubscriptionExpiredError (story v2.6.9) и прекращает попытки; контракт это знание не дублирует — retry_max хранится в globals singleton и читается только off-chain. | void apps::setcoop | ( | eosio::name | coopname, |
| std::optional< eosio::checksum256 > | chain_id, | ||
| std::optional< eosio::name > | subnet_label, | ||
| std::optional< eosio::public_key > | signing_key, | ||
| std::optional< bool > | active | ||
| ) |
Обновить параметры кооператива (ротация ключа, перенос подсети, деактивация).
Обновление параметров кооператива.
Все параметры опциональны — обновляются только переданные. При смене signing_key обновляется key_rotated_at=now. Ранее выпущенные JWT (TTL 14 дней) остаются валидны; новые signed-request'ы должны идти от нового приват-ключа.
coopname @ active.Все параметры опциональны — обновляются только переданные. Сценарии использования:
setcoop(coopname, signing_key=<new>). Меняет signing_key и key_rotated_at=now.setcoop(coopname, active=false). CA-auth перестанет отвечать на запросы от этого коопа.setcoop(coopname, chain_id=<new>, subnet_label=<new>).setcoop(coopname, subnet_label=<new>).signing_key. Только новые signed-request'ы должны идти от нового приват-ключа. Это даёт zero-downtime ротации.coopname @ active. Если @active-ключ коопаккаунта скомпрометирован, эта операция не доступна злоумышленнику — сначала кооператив должен ротировать @active через eosio::updateauth (это отдельная операция, владелец-ключ). | void apps::setglobals | ( | uint32_t | min_payment_period_seconds, |
| uint32_t | free_trial_period_seconds, | ||
| uint32_t | lead_time_seconds, | ||
| uint8_t | retry_max | ||
| ) |
Установить общесистемные параметры каталога (FR2, D2).
Установка общесистемных параметров каталога (singleton-перезапись).
Полная замена singleton globals (scope = get_self()): row нет → создаём, есть → перезаписываем все четыре поля. RAM payer — get_self(). Story v2.1.1 — каркас без валидации диапазонов; валидация и snapshot предыдущего state'а добавляются в Story v2.1.2.
require_auth(get_self()).Story v2.1.1: каркас без валидации диапазонов. Что делает действие:
apps@active — require_auth(get_self()).globals singleton (scope = get_self(), PK у singleton нет): четыре поля заполняются переданными значениями.Что не делает (выносится в Story v2.1.2):
min_payment_period_seconds >= 60, free_trial_period_seconds <= 31 * 86400, retry_max <= 8 и т.п.RAM payer — get_self(). ВОСХОД как owner аккаунта apps оплачивает хранение singleton'а (одна запись, минимальный RAM).
| void apps::setpricing | ( | eosio::name | package_id, |
| eosio::name | plan, | ||
| eosio::asset | hourly_rate | ||
| ) |
Установить ставку плана plan для пакета package_id (FR1, D1).
Установка ставки плана для пакета (per-plan upsert) с валидацией (Story v2.1.2).
Per-plan upsert в таблице pricings (scope = package_id): row нет → emplace; row есть → modify с обновлённым hourly_rate и updated_at = текущее время блока. RAM payer — get_self() (apps-контракт; ВОСХОД как owner аккаунта оплачивает RAM координационной плоскости).
Story v2.1.1 — каркас без бизнес-валидации. Полная валидация (hourly_rate.amount > 0, символ против _root_govern_symbol, plan-whitelist, snapshot предыдущей ставки в audit_log_admin) добавляется в Story v2.1.2.
| package_id | scope таблицы pricings (Antelope name). |
| plan | имя плана, PK (в MVP всегда default). |
| hourly_rate | ставка за час (общая валюта экосистемы — RUB,4). |
require_auth(get_self()) — действие выполняется контрактом от своего имени по запросу из CA-admin через KE write-adapter (Story v2.1.3).Что делает действие:
apps@active — require_auth(get_self()).package_id и plan непустые (FR4 — сама длина ≤ 13 уже гарантирована типом eosio::name, но name(0) — особое значение «отсутствие», его явно отсекаем для удобной ошибки).hourly_rate.amount > 0 (FR1) — отрицательная или нулевая ставка не имеет экономического смысла и блокирует корректный расчёт charge.required в watcher'е.pricings (scope = package_id) находит запись по plan:emplace с переданными hourly_rate и updated_at = now.modify: обновляет hourly_rate и updated_at.Snapshot policy (FR2, journal-less invariant). Действие НИКОГДА не трогает таблицу subs: поле regsub.end_at существующих подписок остаётся как есть — перерасчёта прошлого нет. Текущий оплаченный период подписок зафиксирован в момент regsub/extendsub по тогдашней ставке, и ретро-изменения тарифа на него не влияют. Snapshot для audit-журнала живёт off-chain в audit_log_admin (журнал-less инвариант — на цепи нет log-таблиц, история восстанавливается из trace'ов Antelope).
Что намеренно не валидируется здесь:
hourly_rate.symbol против _root_govern_symbol (RUB,4). Cross-currency расчёт не предусмотрен MVP, но жёсткая привязка к одному символу даст ложноотрицательные ошибки при будущих multi-currency сценариях. Адаптер CA-стороны нормализует символ до отправки.plan. В MVP только default, но on-chain хранилище должно оставаться открытым для будущих tier-планов без миграции контракта.RAM payer — get_self() (apps-контракт). Фактически платит ВОСХОД как owner аккаунта apps; pricing-таблицы — часть координационной плоскости, оплачиваемой оператором каталога.
| void apps::setrelease | ( | eosio::name | coopname, |
| eosio::name | package_id, | ||
| std::string | version, | ||
| Apps::scope_t | scope, | ||
| eosio::checksum256 | tarball_sha256, | ||
| eosio::name | moderated_by, | ||
| std::string | meta | ||
| ) |
Опубликовать новый release пакета.
Опубликовать новый release пакета — атомарный transition.
Атомарно (FR8 «approve → release.ACTIVE»):
releases со status=active, superseded_at=0.active с тем же (package_id, scope) → superseded, superseded_at=now.scope.kind == "all" — обновляет packages.last_active_version.CLEANUP_BUDGET_PER_CALL самых старых superseded-записей с superseded_at < now - RELEASE_RETENTION_SECS.| scope | область видимости (см. scope_t). |
coopname @ active.Алгоритм (выполняется в одной транзакции):
kind + targets). Записывается superseded_at=now. Если кто-то уже superseded — пропускаем.status=active, superseded_at=0.scope.kind == "all"_n — обновить packages.last_active_version.bysuperseded-индексу для этого пакета и удалить до CLEANUP_BUDGET_PER_CALL записей с superseded_at < now - RELEASE_RETENTION_SECS.Это ядро «atomic moderation approve» (FR8): индекс CA + KE.releases — либо обе, либо ни одна. Со стороны blockchain'а — гарантия атомарности в пределах одной транзакции.
version запрещён: если releases уже содержит row для (package_id, version) — фейл. Версии публикуются монотонно вверх; для отката используется reactivate(version=старая).coopname @ active. | void apps::transferpkg | ( | eosio::name | coopname, |
| eosio::name | package_id, | ||
| eosio::name | new_owner | ||
| ) |
Передать владение пакетом другому пользователю (FR3).
Передача владения пакетом другому пользователю (FR3).
coopname @ active.Меняет owner на new_owner. Прав на сами релизы это не меняет — setrelease всё равно выполняется провайдером каталога (coopname), owner — это поле для аудита и UI «чей это пакет».
coopname @ active. | void apps::withdraw | ( | eosio::name | coopname, |
| eosio::name | package_id, | ||
| std::string | version, | ||
| std::string | reason | ||
| ) |
Отозвать релиз (CVE / нарушение / юридический отзыв).
Ставит status=withdrawn. Если это был active со scope=all — packages.last_active_version обнуляется ("") до следующего setrelease/reactivate.
coopname @ active.Семантика отзыва:
withdrawn и НЕ может быть установлен через CA-auth.last_active_version обнуляется ("") — UI должен показать «нет активной версии», пока оператор не выпустит новый setrelease или не реактивирует старую.last_active_version не трогается — она показывает версию для основной массы кооперативов.coopname @ active.