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 / нарушение / юридический отзыв). Подробнее...
 

Подробное описание

Функции

◆ cleanup()

void apps::cleanup ( eosio::name  package_id)

Ручной cleanup TTL-просроченных superseded-записей пакета.

Удаляет до CLEANUP_BUDGET_PER_CALL записей с superseded_at < now - RELEASE_RETENTION_SECS. Идемпотентен.

Заметки
Без авторизации: операция доброкачественная, удаляет только уже невидимые в продакшене записи. Любой может вызвать (и заплатить за CPU).

Используется когда setrelease для пакета давно не вызывался, и inline-cleanup в нём не подметает хвост. Удаляет до CLEANUP_BUDGET_PER_CALL записей с status=superseded и superseded_at < now - RELEASE_RETENTION_SECS.

Идемпотентно. Многократные вызовы безопасны: каждый вызов чистит свой batch до budget'а.

Заметки
БЕЗ авторизации: операция доброкачественная, удаляет только уже невидимые в продакшене записи, не меняет логику. Любой может вызвать (и заплатить за CPU). Это снижает оперативную нагрузку на оператора каталога.

◆ delclient()

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).

Что делает действие:

  1. Требует catalog_operator @ activerequire_auth(catalog_operator).
  2. Валидирует, что client_coopname непустое.
  3. Удаляет row по primary key. Если row нет → eosio_assert("client not found"). Идемпотентность сознательно НЕ соблюдается: повторный delclient должен вернуть явную ошибку, чтобы вышестоящие watcher'ы (cache-invalidation, JWT-revocation в CA) могли отличить «уже отзывали» от «никогда не было».

Эффекты на TS-стороне (живут в apps-catalog, не на цепи):

  • membership-cache для client_coopname инвалидируется через hybrid-TTL (D9): критический путь забирает уже актуальный ответ из chain'а, read-API получает stale-ответ ≤ 60s.
  • JWT с coopname=client_coopname помещаются в JwtRevokeList (FR15): пока не истечёт TTL токена — он отозван по jti.

RAM payer возвращается catalog_operator. subs записи кооператива НЕ удаляются автоматически — это решение оператора, и история подписок остаётся в subs для аудита (флаг active=false выставляется отдельным expsub-вызовом).

◆ 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.

◆ extendsub()

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_idUUIDv5 от (coopname, package_id, period_start_at).
Заметки
Авторизация: require_auth(catalog_operator).

Что делает действие:

  1. Требует подпись catalog_operator @ active.
  2. Валидирует входы: subscriber, package_id непустые, period_seconds > 0.
  3. Находит regsub по (subscriber, package_id) через secondary index bycooppkg. Если нет — eosio_assert("subscription not found") (вызов до regsub — программная ошибка CA-стороны, а не штатный control-flow).
  4. Сверяет 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'ах из кабинета.
  5. На успешном пути модифицирует 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 не платит).

◆ reactivate()

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:

  • Находит существующий release (package_id, version) со статусом superseded или withdrawn.
  • Текущий active с тем же scopesuperseded.
  • Найденный release → active, superseded_at=0.
  • Если scope=all — обновляется packages.last_active_version.

Окно реактивации ограничено TTL retention (90 дней): за пределами окна superseded-записи уже удалены, и реактивация не возможна — нужен полноценный re-publish.

Заметки
Авторизация: coopname @ active.

◆ regclient()

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).

Что делает действие:

  1. Требует подпись catalog_operator @ activerequire_auth(catalog_operator). В MVP это всегда voskhod, но контракт не хардкодит имя — на проверке authority единственное допустимое подписавшее лицо это и есть scope записи.
  2. Валидирует, что client_coopname непустое (eosio::name(0) — особое значение «отсутствие», не валидное имя).
  3. Идемпотентно с проверкой: row нет → 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.
  • Не выпускает события / webhook'и. Audit-trail off-chain в audit_log_admin через regsub-watcher CA.

◆ regcoop()

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_labelhuman-label подсети ("ru","by",...).
signing_keysubnet-signing-key (отдельный от @ active).
Заметки
Авторизация: coopname @ active. Идемпотентность: фейл, если уже зарегистрирован — для обновления использовать setcoop.

Кооператив сам подписывает транзакцию своим @active-ключом и передаёт публичную часть signing_key (отдельный ключ для подписи signed-request'ов от DC-оркестратора в CA-auth).

Идемпотентность: если такой coopname уже зарегистрирован — фейл. Для обновления полей — setcoop.

Заметки
Контекст: этот action — часть процесса подключения кооператива к каталогу приложений на Восходе (вне scope текущего MVP apps-catalog). Реальный flow: кабинет ВОСХОД проводит onboarding, генерирует/принимает signing keypair, подписывает и отправляет эту транзакцию.
Авторизация: coopname @ active.

◆ regpackage()

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.
ownerusername владельца пакета.
compatible_subnetshuman-labels подсетей, в которых пакет совместим.
Заметки
Авторизация: coopname @ active. Идемпотентность: фейл, если package_id уже зарегистрирован (нужен явный transferpkg/withdraw перед re-регистрацией).

Контракт операции:

  • PK = package_id. Если такой уже существует — фейлим («имя занято»).
  • package_name — внешнее имя; не уникально (теоретически два пакета могут иметь одинаковое npm-имя, но разные package_id; на практике это ловится UI/правилами кабинета ВОСХОД).
  • compatible_subnets — может быть пустой; в этом случае пакет считается совместимым со всеми подсетями (resolver на стороне CA обрабатывает пустой набор как «нет ограничений»).
  • last_active_version инициализируется пустой строкой; заполнится первым setrelease со scope=all.
Заметки
Авторизация: coopname @ active.

◆ regsub()

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):

  • row не существует → создаём с active=true.
  • row существует → обновляем plan, end_at = max(текущее, новое), active=true, chain_id (на случай переноса между подсетями).
    Аргументы
    coopnameпровайдер каталога (тот, кто выписывает подписку).
    subscriberкооператив-подписчик.
    chain_idподсеть, в которой действует подписка.
    Заметки
    Авторизация: coopname @ active.
    Idempotent по (subscriber, package_id):

Row нет → создаём с active=true, start_at, end_at, plan, chain_id.

  • Row есть → обновляем 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) подписка теоретически может быть выдана коопу из другой подсети (миграция). Лучше доверять биллингу.

◆ setattempt()

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.

◆ setcoop()

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>).
  • Обновление human-label: setcoop(coopname, subnet_label=<new>).
Заметки
Ранее выпущенные JWT (TTL 14 дней в CA-auth) остаются валидны после ротации signing_key. Только новые signed-request'ы должны идти от нового приват-ключа. Это даёт zero-downtime ротации.
Авторизация: coopname @ active. Если @active-ключ коопаккаунта скомпрометирован, эта операция не доступна злоумышленнику — сначала кооператив должен ротировать @active через eosio::updateauth (это отдельная операция, владелец-ключ).

◆ setglobals()

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: каркас без валидации диапазонов. Что делает действие:

  1. Требует apps@activerequire_auth(get_self()).
  2. Полная перезапись 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 и т.п.
  • Snapshot предыдущего state'а в audit_log_admin — off-chain (journal-less invariant).

RAM payer — get_self(). ВОСХОД как owner аккаунта apps оплачивает хранение singleton'а (одна запись, минимальный RAM).

◆ setpricing()

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_idscope таблицы pricings (Antelope name).
planимя плана, PK (в MVP всегда default).
hourly_rateставка за час (общая валюта экосистемы — RUB,4).
Заметки
Авторизация: require_auth(get_self()) — действие выполняется контрактом от своего имени по запросу из CA-admin через KE write-adapter (Story v2.1.3).

Что делает действие:

  1. Требует apps@activerequire_auth(get_self()).
  2. Валидирует входы: package_id и plan непустые (FR4 — сама длина ≤ 13 уже гарантирована типом eosio::name, но name(0) — особое значение «отсутствие», его явно отсекаем для удобной ошибки).
  3. Валидирует hourly_rate.amount > 0 (FR1) — отрицательная или нулевая ставка не имеет экономического смысла и блокирует корректный расчёт charge.required в watcher'е.
  4. В таблице pricings (scope = package_id) находит запись по plan:
    • row нет → emplace с переданными hourly_rate и updated_at = now.
    • row есть → 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-стороны нормализует символ до отправки.
  • Whitelist имён plan. В MVP только default, но on-chain хранилище должно оставаться открытым для будущих tier-планов без миграции контракта.

RAM payer — get_self() (apps-контракт). Фактически платит ВОСХОД как owner аккаунта apps; pricing-таблицы — часть координационной плоскости, оплачиваемой оператором каталога.

◆ setrelease()

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»):

  1. Создаёт row в releases со status=active, superseded_at=0.
  2. Любой существующий active с тем же (package_id, scope)superseded, superseded_at=now.
  3. Если scope.kind == "all" — обновляет packages.last_active_version.
  4. Inline-cleanup: удаляет до CLEANUP_BUDGET_PER_CALL самых старых superseded-записей с superseded_at < now - RELEASE_RETENTION_SECS.
Аргументы
scopeобласть видимости (см. scope_t).
Заметки
Авторизация: coopname @ active.

Алгоритм (выполняется в одной транзакции):

  1. Проверки: пакет существует; статус — известный; scope согласован (kind=all → targets пустой; kind=subnet/canary → targets непустой).
  2. Выполнить supersede предыдущего active с тем же scope (точное совпадение kind + targets). Записывается superseded_at=now. Если кто-то уже superseded — пропускаем.
  3. Создать новый row со status=active, superseded_at=0.
  4. Если scope.kind == "all"_n — обновить packages.last_active_version.
  5. Inline-cleanup: пройтись по bysuperseded-индексу для этого пакета и удалить до CLEANUP_BUDGET_PER_CALL записей с superseded_at < now - RELEASE_RETENTION_SECS.

Это ядро «atomic moderation approve» (FR8): индекс CA + KE.releases — либо обе, либо ни одна. Со стороны blockchain'а — гарантия атомарности в пределах одной транзакции.

Заметки
Re-publish одной и той же version запрещён: если releases уже содержит row для (package_id, version) — фейл. Версии публикуются монотонно вверх; для отката используется reactivate(version=старая).
Авторизация: coopname @ active.

◆ transferpkg()

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.

◆ withdraw()

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.
  • Запись остаётся в таблице — TTL на withdrawn не действует.
  • Если withdrawn-релиз был active со scope=all, last_active_version обнуляется ("") — UI должен показать «нет активной версии», пока оператор не выпустит новый setrelease или не реактивирует старую.
  • Если был active со scope=subnet/canary, last_active_version не трогается — она показывает версию для основной массы кооперативов.
Заметки
Авторизация: coopname @ active.