Аппрувалы
Жизненный цикл заявки на подтверждение — снапшот подписантов на момент создания, m-of-n, veto и возврат резерва.
Когда Policy Engine возвращает queue, действие не выполняется сразу — оно превращается в заявку на подтверждение и ждёт нужного числа одобрений. На этой странице — как заявка живёт от создания до разрешения. О том, кто и почему ставит действие в очередь, — на странице Policy Engine.
Откуда берётся заявка
Действие, меняющее состояние (перевод, удаление адреса, изменение контрагента), возвращает один из двух ответов:
200— политика разрешила (allow), действие выполнено сразу.202— политика поставила в очередь (queue), создана заявка. В ответе — её идентификатор.
Заявка несёт снапшот решения на момент создания: какая политика решила (decisionPolicyId), кто вправе подтверждать и какой порог. Поэтому поздние правки политики не меняют уже созданные заявки.
Жизненный цикл
У заявки один из четырёх статусов:
PENDING— ждёт голосов. Для перевода сумма всё это время держится вreserved.APPROVED— набран порог, целевое действие применено.REJECTED— вето одного из одобряющих или устаревание (stale, см. ниже).CANCELLED— инициатор отменил заявку (для перевода — через/cancel).
Для перевода при отклонении или отмене зарезервированная сумма возвращается в available.
Как голосуют (m-of-n)
Голос подаёт член approver-группы заявки. Инициатор не голосует за собственную заявку (если это не разрешено политикой явно), и у каждого — один голос.
POST /api/v1/vaults/{code}/approval-requests/{id}/approvals—approveилиrejectпо заявке. Финальныйapprove, добравший порог, применяет целевое действие в этом же запросе. В ответе — финальный статус иresolvedResultId(илиapplyError, если применить не удалось).POST /api/v1/vaults/{code}/transfers/{id}/approvals— отдельная поверхность для переводов (статусawaiting_approval): те же правила, средства освобождаются при отклонении.
reject — это вето: одного отклонения достаточно, чтобы заявка стала REJECTED. Порог набирается только из approve.
Снапшот и защита от гонок
Два механизма делают исход предсказуемым:
- Снапшот подписантов. Заявка помнит одобряющих и порог такими, какими они были в момент создания. Изменили политику после — уже открытые заявки это не затронет.
- Optimistic-lock. Финальный
approveприменяет действие атомарно. Если целевой объект успел измениться с момента создания заявки, она помечаетсяREJECTED(stale), а не применяет устаревшее действие. Так исключены гонки между параллельными изменениями.
Композитные заявки
Несколько связанных действий можно оформить как одну заявку с единым флоу одобрения:
POST /api/v1/vaults/{code}/approval-requests/composite— цепочка шагов как единая сущность. Каждый шаг гейтится своей политикой; голосapproveзасчитывается тем шагам, где голосующий уполномочен; заявка становитсяAPPROVED, когда каждый шаг набрал свой порог.reject— вето всей цепочки. Если все шаги разрешены сразу, цепочка применяется без заявки (applied: true).
Где смотреть заявки
GET /api/v1/vaults/{code}/approval-requests— заявки, видимые вам (как инициатору, одобряющему или админу). Фильтры:status(PENDING|APPROVED|REJECTED|CANCELLED),action,mine=true(я инициатор),pendingMine=true(ждут моего голоса),from/to,resolvedFrom/resolvedTo,initiatorId,approverId,decisionPolicyId.GET /api/v1/vaults/{code}/approval-requests/{id}— детали одной заявки.
pendingMine=true отвечает на вопрос «что ждёт именно моего голоса» — удобно для дашборда одобряющего.