Журнал (двойная запись)
Каждое движение средств — сбалансированные проводки, а баланс вычисляется из журнала. Available и reserved, исторические балансы, идемпотентность и неизменяемость.
В V3 Custody деньги — это не число, которое перезаписывается, а журнал. Любое движение средств записывается двойной записью, а баланс вычисляется из этих записей. Поэтому на вопрос «сколько и почему» вы отвечаете запросом, а не расследованием.
Проводки, которые всегда сходятся
Каждая запись журнала (entry) состоит из проводок (postings), и их сумма равна нулю. Одна сторона трогает счёт вашего адреса, другая — встречную сторону движения (ончейн-контур или корректировку). Ноль в сумме — это гарантия, что средства не появляются и не исчезают, а только перемещаются.
Журнал доступен только на чтение и неизменяем:
GET /api/v1/vaults/{code}/ledger/entries— immutable-записи с проводками (сумма постингов = 0). Сортировка поpostedAt(сначала свежие), keyset-пагинация, фильтрыfrom/to/token. Обычный участник видит только записи, затрагивающие его адреса; администратор — весь Vault.
Баланс вычисляется, а не хранится
Баланс — это свёртка журнала, а не отдельно хранимое число: он обновляется атомарно с проводками, поэтому не может разойтись с ними. Каждый баланс делится на два состояния:
available— свободные средства, которые можно тратить.reserved— заблокировано под ожидающими переводами, но всё ещё принадлежит владельцу.
«Владеемый» баланс — это available + reserved.
GET /api/v1/vaults/{code}/balances— балансы Vault'а, пользователя или адреса; параметрыsummary,addressId,token. В ответе — агрегаты по токенам (totals).
Reserved: средства «в пути», но ваши
Когда создаётся перевод, сумма сначала резервируется — ещё до того, как транзакция уйдёт в сеть. Резерв защищает от двойной траты: те же деньги нельзя отправить дважды.
Дальше судьба резерва зависит от Policy Engine. Если перевод одобрен и подтверждён — резерв списывается. Если поставлен в очередь на подтверждение — сумма ждёт в reserved. Если отменён или отклонён — возвращается в available. Средства всё это время остаются вашими.
Баланс на любой момент прошлого
Поскольку журнал неизменяем, позицию можно восстановить на любую дату — из проводок, зафиксированных до этого момента:
GET /api/v1/vaults/{code}/balances/as-of?at=...— баланс на моментat, рассчитанный из проводок сpostedAt <= at. Прошлое не меняется задним числом. «Владеемый» баланс =available + reserved; обычный участник видит только свои адреса.
Это то, что нужно для выписок на конец периода и сверок: ответ на «сколько было на 31 марта» всегда одинаковый.
Идемпотентность: один запрос — один платёж
У перевода есть idempotencyKey. Повтор запроса с тем же ключом вернёт уже созданную операцию, а не создаст новую. Значит, ретраи при сетевых сбоях безопасны — двойного платежа не будет.
Ничего не переписывается — только добавляется
Записи журнала не редактируются и не удаляются. Ошибку исправляют новой операцией, связанной с исходной, — так история сохраняется целиком:
POST /api/v1/vaults/{code}/operation-relations— явная связь:refund,reversal,correction,replacement. Встречный платёж не считается возвратом автоматически — связь указывается явно. Частичный возврат задаётсяrelatedAmountRaw(сtokenId).
Плюс у каждой операции есть история статусов:
GET /api/v1/vaults/{code}/operations/{type}/{id}/status-history— хронология переходов статуса.
Лента операций и журнал — два взгляда
Журнал (проводки) — это бухгалтерская истина. Для человека же удобнее лента операций — бизнес-события:
GET /api/v1/vaults/{code}/history— ваша активность в Vault'е одной лентой (ваши переводы и депозиты на ваши адреса), по(createdAt, id)убыв., keyset. Фильтры:direction=in|out,from/to, статус (например, зависшие переводы).
Проводки отвечают на «как это отражено в учёте», лента — на «что произошло».
Суммы в журнале и балансах хранятся в минимальных единицах токена (raw), а показываются в десятичном виде по decimals сети. Поэтому в ответах встречаются и raw-, и десятичные значения сумм.