Перейти к содержанию

Gerege Wallet

Частично · Слой 4 — Отраслевой продукт · Репозиторий: wallet-gerege-mn · wallet.gerege.mn · api.wallet.gerege.mn

Цифровой кошелёк гражданина — продукт, где вход выполняется по eID, видно остаток и можно делать переводы по IBAN. В качестве финансового ядра работает Apache Fineract.

Мобильные приложения (iOS SwiftUI, Android Compose) — основная поверхность; веб — вспомогательная консоль.

Архитектура денег — самое важное правило

Fineract — единственный источник истины о деньгах. Остатки, транзакции и книга проводок находятся там. PostgreSQL хранит ТОЛЬКО сопоставление «гражданин ↔ ID в Fineract», записи идемпотентности и пользовательские настройки.

Слой За что отвечает
Бэкенд Wallet (Go) Аутентификация, права, потоки, аудит
Apache Fineract 1.15 Счета, транзакции, остатки, двойная запись, книга проводок
PostgreSQL Только сопоставление и настройки — остатков здесь НЕТ НИКОГДА

Из этого следуют три правила:

  • Не кэшировать остаток. /accounts/balance читает напрямую из Fineract — условия для расхождения двух систем просто не возникают.
  • Не делать деньги float. Текстовая форма из JSON преобразуется сразу в int64 в минорных единицах (для ₮ деньги = ₮×100). Путь к ошибкам округления закрыт.
  • Каждая транзакция идемпотентна. Подробности ниже.

Почему готовая core banking система?

Финансовая книга проводок — предметная область, которую трудно сделать правильно и дорого сделать неправильно: двойная запись, сведение баланса, закрытие периодов, аудиторский след. Fineract решает это за годы работы в production. Мы добавляем сверху только слой идентичности и слой услуг.

Гражданин ↔ счёт ↔ IBAN

Каждому гражданину полагается ровно один клиент в Fineract, один сберегательный счёт и один IBAN. Ключом связи служит civil_id гражданина:

civil_id ──► externalId = PNOMN-<CIVIL_ID> ──► клиент + счёт в Fineract
                                              savings account ID
                                                      IBAN

Монгольский IBAN — 20 символов:

MN | kk | bbbb | aaaaaaaaaaaa
 2 |  2 |    4 |           12
 │    │     │      └─ номер счёта (Fineract savings ID, с ведущими нулями)
 │    │     └──────── код банка/учреждения (4 разряда)
 │    └────────────── контрольные разряды mod-97
 └─────────────────── код страны

12 разрядов номера счёта выводятся из ID в Fineract, поэтому дополнительная последовательность не нужна, а обратное сопоставление — чистая арифметика.

Ключ — civil_id, а НЕ внутренний user_id

Ранее externalId для Fineract выводился из user_id в PostgreSQL (новый UUID при каждом создании строки) — это исправлено. При той схеме пересоздание БД навсегда разрывало связь «гражданин ↔ счёт»: при повторном входе создавался новый счёт и новый IBAN, а прежний остаток оставался осиротевшим.

civil_id — пожизненный идентификатор (есть у каждого пользователя eID), поэтому выведенный из него ключ не зависит от базы. ПЕРЕД открытием счёта существующие клиент и счёт в Fineract разыскиваются по этому ключу и, если найдены, используются повторно — так что даже при утрате сопоставления гражданин получает обратно тот же IBAN.

Авторизация перевода

Перевод авторизуется JWT-сессией. Приложение обращается к /transfer/iban с access token, полученным при входе.

Защита от дублирования: каждый запрос несёт заголовок Idempotency-Key. Второй запрос с тем же ключом не создаёт новую транзакцию, а возвращает результат ПЕРВОГО — поэтому при обрыве сети и повторной отправке деньги не уйдут дважды. Гарантию в конечном счёте держит ограничение UNIQUE (user_id, idempotency_key) в базе.

Привязка подписи удалена

Раньше каждый перевод повторно хешировался в канонической форме GWT и сверялся с подписью eID PIN2 гражданина (WYSIWYS — «подписываешь то, что видишь»). К этому прилагались побайтово идентичные реализации в трёх портах (Go/Kotlin/Swift) и проверка golden fixtures в CI.

Всё это удалено продуктовым решением. Последствие: сторона, владеющая действительным access token, может двигать деньги — тогда как раньше даже украденный токен не позволял выполнить перевод без PIN2.

Вход по eID сохраняется — удалена только подпись ПЕРЕВОДА.

Поверхность API

Аутентификация приходит из базового слоя Gerege Platform; endpoint'ы кошелька находятся в этом репозитории.

Метод Путь Что делает
POST /api/v1/auth/initiate Отправляет eID push по номеру реестра
GET /api/v1/auth/status/{sid} Статус + токен + IBAN (здесь кошелёк и открывается)
GET /api/v1/accounts/balance Остаток (напрямую из Fineract)
GET /api/v1/accounts/transactions Выписка по счёту
GET /api/v1/accounts/lookup Проверка IBAN получателя
POST /api/v1/transfer/iban Перевод (требует Idempotency-Key)
GET/DELETE /api/v1/beneficiaries Сохранённые получатели
POST/DELETE /api/v1/devices/register Регистрация push-токена
POST /api/v1/pay/code/initiate Одноразовый платёжный QR-токен

Две формы ответа

  • Плоский JSON (без конверта) — для мобильных приложений. Кошелёк и мобильные auth-endpoint'ы используют эту форму.
  • Конверт {status, message, data} — для веб-BFF.

Добавляя новый endpoint для приложения, придерживайтесь плоской формы. Веб-BFF оборачивает плоский ответ в свой клиентский конверт и передаёт дальше.

Словарь статусов приложений

Терминальными приложения считают только CONFIRMED / REFUSED / TIMEOUT. Внутренние eID-наименования бэкенда (COMPLETE/EXPIRED/…) отображаются наружу в одном единственном месте.

Контракт задают приложения

Приложения для iOS и Android были созданы РАНЬШЕ бэкенда и ожидают приведённых выше форм. Если приложение и бэкенд расходятся, исправляют бэкенд.

Мобильные приложения

iOS Android
Технология SwiftUI Kotlin + Compose
Вход
Остаток / выписка
Перевод ⏳ экран пока не сделан
Оплата по QR (EMVCo)
Регистрация push

Приложения не обращаются к домену eID напрямую — весь трафик идёт через api.wallet.gerege.mn. Для входа гражданин вводит PIN в ответ на push, пришедший в приложение eID.

Безопасность

  • Row-Level Security. API подключается к БД ролью, которая НЕ является superuser (в production это проверяет boot guard), поэтому политики RLS действительно применяются. У каждой таблицы «на пользователя» своя политика. Доверенные серверные записи — открытие счёта, создание записи о переводе — выполняются отдельно под ролью service; гражданину права записи в эти таблицы не выдаются.
  • TLS для БД. У PostgreSQL настроен TLS с приватным CA, поэтому sslmode=verify-full имеет реальный смысл.
  • Где живут секреты. Все секреты — в /etc/gerege-wallet/*.env. Файл .env приложения ПЕРЕСОЗДАЁТСЯ при каждом деплое, так что правка вручную будет стёрта следующим деплоем.
  • Rate limit. /auth/* ~5 запросов/мин (cap тела 4 KiB), long-poll /auth/status — на отдельном, более свободном лимите, endpoint'ы, двигающие деньги, ~30/мин.
  • Идемпотентность. Каждый перевод требует Idempotency-Key — при обрыве сети и повторной отправке деньги не уйдут дважды. Поскольку привязки подписи больше нет, это ГЛАВНЫЙ механизм защиты от дублирования.
  • Изоляция Fineract. Слушает только на 127.0.0.1:8090 — пути извне нет.

Развёртывание

НЕ Docker — нативный systemd:

gerege-wallet.slice
├── gerege-wallet-fineract.service   # Fineract 1.15 (JAR, 127.0.0.1:8090)
├── gerege-wallet-api.service        # Go API (127.0.0.1:8080)
└── gerege-wallet-web.service        # Next.js BFF (127.0.0.1:3000)

PostgreSQL, Redis и nginx — сервисы хоста. nginx публикует wallet.gerege.mn и api.wallet.gerege.mn по TLS от Let's Encrypt.

CD: после слияния в main Deploy workflow запускается, как только CI становится зелёным. Вся сборка выполняется на раннере, и на сервер попадает только результат — там не нужен toolchain Go/Node, а окно простоя короткое. Деплой считается успешным лишь после того, как проверено не только /health, но и что защищённый путь отвечает корректно.

Текущее состояние

Возможность Состояние
Вход по eID (с RP-учётными данными) Работает
Кошелёк открывается автоматически + выдаётся IBAN Работает
Остаток / выписка Работает
Приветственный бонус новым кошелькам Работает
Перевод по IBAN (авторизация по JWT) Реализовано, тестов мало
Перевод / QR на Android В планах
App Store / TestFlight Готовится

Код банка в IBAN временный

Текущий код банка/учреждения — временное значение до получения реального кода от Банка Монголии. Выданный гражданину IBAN меняться не должен, поэтому после появления настоящих пользователей это значение менять не будут.

Подробная документация

Документы ARCHITECTURE, DEVELOPMENT, API_CONTRACT и SECURITY находятся в каталоге backend/docs/ репозитория wallet-gerege-mn (парами EN/MN). Инструкции по мобильным сборкам — в ios/README.md, по развёртыванию — в docs/DEPLOYMENT.md.

Связанные платформы: eID Mongolia · G-Sign · Gerege Platform · Gerege Verify