Классы TigerBeetle-аккаунтов¶
Архитектурный документ перечисляет все классы аккаунтов, которые Payment Manager создаёт в TigerBeetle, описывает их назначение, схему именования, TB-параметры (code/ledger/flags) и роль в денежных потоках. Это эталонное справочное место, на которое опираются остальные документы архитектуры: оно отвечает на вопрос «какие сущности существуют в леджере и как они между собой связаны».
1. Где источник истины¶
Все имена и коды аккаунтов определяются в одном месте — src/ledger/accounts.ts:
accountName(type, opts)— строит каноническое строковое имя аккаунта (используется как первичный ключ вpm.tb_account_map).accountId(name)— детерминированно вычисляетbigintTB-ID какuuidv5(name, TB_NS). Namespace UUID фиксирован навсегда — изменение инвалидирует все существующие TB-ID. Подробнее см. ./06-deterministic-ids.md.ACCOUNT_CODES— картаAccountType → TB code(используется TigerBeetle для категоризации аккаунтов).accountFlags(type)— выставляетdebits_must_not_exceed_creditsиhistoryв зависимости от класса.
Схема таблицы pm.tb_account_map, которая хранит соответствие
account_name → tb_account_id — в
../reference/database/06-tb-account-map.md.
2. Инвариант: единственный писатель TB¶
Write-доступ к TigerBeetle есть только у Payment Manager. Никакой другой сервис (Auth Center, Admin Panel, PSP-адаптеры, KYC-воркер, Notifications) не вызывает
createAccounts/createTransfersнапрямую. Admin Panel может читать TB (lookup), но любые изменения проходят черезPOST /intentsс HMAC.
Это критически важно, потому что TigerBeetle — единственный источник
истины по балансам. Допусти второго писателя — и инвариант
transit.balance = 0 (см. §3) станет недоказуемым.
3. Перечень классов аккаунтов¶
Всего в системе семь классов аккаунтов. Каждый имеет уникальный
TB code, свою схему имени и свой набор flags.
| Класс | Code | Префикс имени | debits_must_not_exceed_credits |
history |
|---|---|---|---|---|
USER_WALLET |
10 | user.<userId>.<currency> |
✅ | ✅ |
TRANSIT |
20 | system.transit.<channel>.<currency> |
❌ | ❌ |
REVENUE |
30 | system.revenue.<currency> |
❌ | ✅ |
NOSTRO |
40 | system.nostro.<provider>.<currency> |
❌ | ✅ |
MERCHANT_WALLET (+ MERCHANT_SETTLEMENT) |
50 / 60 | merchant.<merchantId>.<currency> / merchant.<merchantId>.settlement.<currency> |
✅ / ❌ | ✅ / ✅ |
AGENT_WALLET (+ AGENT_SETTLEMENT) |
90 / 100 | agent.<agentId>.<currency> / agent.<agentId>.settlement.<currency> |
✅ / ❌ | ✅ / ✅ |
SERVICE_ACCOUNT |
70 | service.<serviceUserId>.<currency> |
✅ | ✅ |
EQUITY |
80 | system.equity.<currency> |
❌ | ✅ |
Все аккаунты живут на ledger = 1 (для THB). Мультиледжерная схема
зарезервирована под другие валюты, но в проде на сегодня один.
3.1 USER_WALLET — кошелёк физлица¶
- Назначение: хранит баланс конкретного пользователя в конкретной валюте.
- Имя:
user.<userId>.<currency>(например,user.42.THB). - Flags:
debits_must_not_exceed_credits(нельзя уйти в минус) +history(полная история движений). - Кто создаёт: lazy — при первом платеже через
ensureAccountв context-builder. - Кто пишет: PM, шаги саги (
debit-source,credit-destination).
3.2 TRANSIT — клиринговый аккаунт канала¶
- Назначение: служит «перевалочным пунктом» при двухходовых
переводах. Деньги списываются с источника на TRANSIT, потом с TRANSIT
на получателя — обе операции линкованы как одна линкованная цепочка
в TigerBeetle (
TransferFlags.linked). - Имя:
system.transit.<channel>.<currency>.channel— это логический канал саги (INTERNAL_P2P,IPPS,MERCHANT,MERCHANT_INVOICE,SERVICE_TRANSFER,INTERNAL). Это не PSP (IPPS,QP,WISE— этоPspName, см. модулиpsp-router/иpsp-adapters/). - Flags: без
debits_must_not_exceed_credits, безhistory— TRANSIT обязан проходить через ноль, и его движения видны через саму историю transfers, отдельная account history не нужна. - Инвариант:
balance(transit) = credits_posted − debits_posted = 0после успешного завершения интента. Это главный финансовый инвариант системы: если на TRANSIT остались деньги — где-то посередине саги что-то застряло. TigerBeetle гарантирует это черезlinkedtransfers: либо оба перевода (source → transitиtransit → destination) применяются, либо ни один. - Кто создаёт: seed на старте PM (
seedAccounts) — каждый канал получает свой TRANSIT. - Кто пишет: PM, через
authorize()+settle()вsrc/ledger/transfers.ts.
3.3 REVENUE — собственный доход системы¶
- Назначение: аккумулирует все комиссии OneWallet (
APP_FEE). Каждый платный интент порождает дополнительный шагapply-fee, который зачисляет фи с TRANSIT на REVENUE. - Имя:
system.revenue.<currency>(одно на валюту). - Flags: только
history— REVENUE может расти бесконечно, и нам нужна детальная история начислений для бухгалтерии. - TB transfer code для фи:
TRANSFER_CODES.APP_FEE = 20(см.src/ledger/transfer-codes.ts).
3.4 NOSTRO — корреспондентский счёт у внешнего провайдера¶
- Назначение: «зеркало» нашего реального счёта у банка/PSP.
Когда пользователь топит баланс через IPPS, деньги физически
лежат на нашем счету в банке, и в TB это отражается как
NOSTRO → USER_WALLET. При выводе наоборот. - Имя:
system.nostro.<provider>.<currency>.provider— идентификатор партнёра, не путать с PspName. Сейчас сидируетсяipps(default) иbank. - Flags: только
history. Безdebits_must_not_exceed_credits— технически TB-аккаунт может уйти в минус относительно реального банка, потому что мы отражаем внешний поток с задержкой.
3.5 MERCHANT_WALLET / MERCHANT_SETTLEMENT — мерчант¶
- Назначение: отдельный класс аккаунтов для мерчантов (юрлиц, принимающих оплаты через инвойсы). См. [memory: project-multi-entity-accounts] — мерчант не «просто кошелёк пользователя», у него свой класс TB-аккаунта с отдельным code (50 / 60) и своей политикой ролей в context-builder.
- Имя:
merchant.<merchantId>.<currency>— операционный кошелёк, куда падают платежи от покупателей.merchant.<merchantId>.settlement.<currency>— settlement-аккаунт, с которого деньги уходят на банковский счёт мерчанта по расписанию.- Flags WALLET:
debits_must_not_exceed_credits+history. - Flags SETTLEMENT: только
history— settlement-аккаунт может быть проведён в минус для отражения payout-операций до их технической post-фазы.
3.6 AGENT_WALLET / AGENT_SETTLEMENT — агент¶
- Назначение: аналог merchant, но для агентов (cash-in/cash-out точки, например физлица или магазины, обслуживающие топ-апы налом). Это также отдельный класс, code 90 / 100. Отдельный класс нужен, чтобы лимиты, отчёты и комплаенс-правила для агентов применялись только к ним и не пересекались с мерчантами и физлицами.
- Имя:
agent.<agentId>.<currency>иagent.<agentId>.settlement.<currency>. - Flags: аналогично merchant.
3.7 SERVICE_ACCOUNT — служебный аккаунт mini-app¶
- Назначение: mini-app внутри OneWallet (например, оплата коммуналки или мобильной связи) держит собственный сервисный кошелёк, с которого списываются деньги пользователя в её пользу.
- Имя:
service.<serviceUserId>.<currency>. - Flags:
debits_must_not_exceed_credits+history.
3.8 EQUITY — equity-аккаунт системы¶
- Назначение: «затыкающий» аккаунт для внешних потоков, не
привязанных к конкретному пользовательскому переводу — например,
отражение комиссий PSP, корректировок, операционных списаний из
Admin Panel (
channel=ADMIN). - Имя:
system.equity.<currency>. - Flags: только
history. Безdebits_must_not_exceed_credits— EQUITY может уходить в минус, потому что трекает суммарный внешний flow.
4. Поток денег: P2P-перевод¶
Ниже — пример канонического INTERNAL_P2P платежа: пользователь A
переводит пользователю B 100 THB с комиссией системы 1 THB. Все
TB-transfers внутри одного интента линкованы (TransferFlags.linked)
и применяются атомарно — либо все, либо ни одного.
flowchart LR
A["user.A.THB<br/>(USER_WALLET)"]
T["system.transit.INTERNAL_P2P.THB<br/>(TRANSIT)"]
B["user.B.THB<br/>(USER_WALLET)"]
R["system.revenue.THB<br/>(REVENUE)"]
A -- "1. debit 101<br/>code=PAYMENT" --> T
T -- "2. credit 100<br/>code=PAYMENT" --> B
T -- "3. credit 1<br/>code=APP_FEE" --> R
classDef wallet fill:#e0f2fe,stroke:#0284c7
classDef transit fill:#fef3c7,stroke:#d97706,stroke-dasharray: 4 2
classDef revenue fill:#dcfce7,stroke:#16a34a
class A,B wallet
class T transit
class R revenue
После применения всех трёх transfers:
user.A.THB: −101 THB (101 списано).user.B.THB: +100 THB (получил перевод).system.revenue.THB: +1 THB (комиссия системы накопилась).system.transit.INTERNAL_P2P.THB:credits_posted = 101,debits_posted = 101→ balance = 0.
Если на TRANSIT после интента остался ненулевой баланс — это инцидент,
который ловится verifySystemAccounts() на старте PM и алёртами
operations-стека.
4.1 Two-phase: pending → post / void¶
Шаги выше используют двухфазную модель TigerBeetle:
- Authorize (
src/ledger/transfers.ts::authorize) — создаёт pending transfers сTransferFlags.pendingиtimeout=300s. Деньги замораживаются наdebits_pending/credits_pending. - Settle (
src/ledger/transfers.ts::settle) — пост этих pending черезTransferFlags.post_pending_transfer. Деньги мигрируют вdebits_posted/credits_posted. - Void (
src/ledger/transfers.ts::voidAll) — откатывает pending черезTransferFlags.void_pending_transfer(при ошибке саги).
Все post/void-ID детерминированы от pending-ID
(postId(pid) = pid ^ 1n, voidId(pid) = pid ^ 2n) — это позволяет
безопасно ретраить операции без риска создать дубликат. Подробнее
о схеме детерминированных ID см.
./06-deterministic-ids.md.
5. Лениво vs. seed на старте¶
| Класс | Когда создаётся |
|---|---|
USER_WALLET |
Лениво — при первом платеже (через ensureAccount) |
MERCHANT_* |
Лениво — при создании мерчанта в Auth Center |
AGENT_* |
Лениво — при онбординге агента |
SERVICE_ACCOUNT |
Лениво — при регистрации mini-app |
TRANSIT |
Seed — seedAccounts() на старте PM (по одному на канал) |
REVENUE |
Seed — один на валюту |
NOSTRO |
Seed — один на пару provider × currency |
EQUITY |
Seed — один на валюту |
seedAccounts(db) идемпотентен — повторный вызов не создаёт
дубликатов (использует pm.tb_account_map как guard и
обрабатывает CreateAccountStatus.exists от TB).
После seed выполняется verifySystemAccounts(db), который для каждого
системного аккаунта параллельно проверяет, что он есть и в TB
(через lookupAccounts), и в pm.tb_account_map. Расхождение
логируется на уровне error.
6. Где это используется¶
- Создание / резолв аккаунтов:
src/ledger/accounts.ts—createAccount,getAccountByName,getAccountByTbId. - Применение transfers саги:
src/ledger/transfers.ts—authorize,settle,voidAll. - Описание модуля ledger в целом: ../modules/ledger.md.
- Таблица
pm.tb_account_map(mappingaccount_name → uuid): ../reference/database/06-tb-account-map.md. - Резолв ролей (кто чей кошелёк по контексту интента) описан в
./03-intent-saga.md — Auth Center всегда
передаёт явное
fromAccountName/toAccountName, PM никогда не «угадывает» аккаунт по роли (см. memory:feedback-account-resolution).
7. Чек-лист для нового класса аккаунтов¶
Если когда-нибудь понадобится завести новый класс (например, escrow):
- Добавить литерал в
AccountType(src/shared/schema.ts). - Добавить ветку в
accountName()и решить схему имени. - Добавить
codeвACCOUNT_CODES(новое число, не пересекающееся с существующими). - Решить, какие flags нужны (обычно
history; для пользовательских кошельков — такжеdebits_must_not_exceed_credits). - Если аккаунт системный — добавить в
SYSTEM_ACCOUNTS, чтобы создавался seed-ом и проверялсяverifySystemAccounts. - Описать новый класс здесь, в этом документе.
- Обновить миграции и
pm.tb_account_map(если меняется лимит длиныaccount_nameили появляются новыеaccountType-значения).
См. также:
- ./03-intent-saga.md — как саги используют эти аккаунты.
- ./04-two-phase-channels.md — двухфазные каналы (MERCHANT_INVOICE) и их TRANSIT-аккаунты.
- ./06-deterministic-ids.md — формула
accountId(name) = uuidv5(name, TB_NS). - ../modules/ledger.md — модуль ledger целиком.
- ../reference/database/06-tb-account-map.md — таблица mapping.