Добавить спецификации Solana программ и вынести формат PDA

This commit is contained in:
AidarKC
2026-06-04 22:17:17 +04:00
parent a9510a6d36
commit 6b0379bfdc
10 changed files with 1537 additions and 21 deletions
@@ -0,0 +1,378 @@
# Программа `shine_payments`
Документ описывает текущее целевое поведение программы `shine_payments`.
Назначение программы:
- хранить общие настройки экономической модели платежей и выплат;
- принимать покупку тикетов очереди выплат;
- хранить очереди и отдельные ticket PDA;
- выдавать лимиты менеджерам на ручное добавление тикетов;
- выполнять пошаговые выплаты из inflow-вольта.
Если код и этот документ расходятся, нужно в том же изменении синхронизировать либо код, либо документ.
## 1. Program ID
Текущий program id:
- `m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR`
## 2. Основная модель
`shine_payments` разделяет две вещи:
1. регистрацию долга/обязательств через тикеты;
2. фактическую выплату через отдельный inflow-вольт.
Это важно:
- покупка тикета не означает немедленную выплату из вольта;
- payout выполняется отдельной инструкцией `step_payout`;
- inflow-вольт должен быть заранее или отдельно пополнен.
## 3. PDA и seed-правила
### 3.1. Single PDA
- config: `shine_payments_config`
- coef/limit: `shine_payments_coef_limit`
- queues: `shine_payments_queues`
- inflow vault: `shine_payments_inflow_vault`
### 3.2. Ticket PDA
- queue 1 seed prefix: `shine_payments_q1_ticket`
- queue 2 seed prefix: `shine_payments_q2_ticket`
- второй seed: `ticket_index` в little-endian `u64`
### 3.3. Manager allowance PDA
- seed prefix: `shine_p_manager_allow`
- второй seed: `manager_wallet.as_ref()`
## 4. Состояния программы
### 4.1. `ConfigState`
Поля:
- `version: u8`
- `dao_wallet: Pubkey`
- `inflow_vault: Pubkey`
### 4.2. `CoefLimitState`
Поля:
- `version: u8`
- `coef_ppm: u64`
- `limit_usd_cents: u64`
- `call_reward_lamports: u64`
Смысл:
- `coef_ppm` — множитель payout в ppm;
- `limit_usd_cents` — лимит суммы очереди Q1;
- `call_reward_lamports` — награда вызывающему `step_payout`.
### 4.3. `QueuesState`
Поля агрегатов:
- `q1_tickets_total`
- `q1_tickets_paid`
- `q1_sum_total_usd_cents`
- `q1_sum_paid_usd_cents`
- `q2_tickets_total`
- `q2_tickets_paid`
- `q2_sum_total_usd_cents`
- `q2_sum_paid_usd_cents`
### 4.4. `TicketState`
Поля:
- `version: u8`
- `queue_id: u8`
- `index: u64`
- `is_paid: bool`
- `recipient_wallet: Pubkey`
- `payout_usd_cents: u64`
- `debt_before_usd_cents: u64`
### 4.5. `ManagerAllowanceState`
Поля:
- `version: u8`
- `manager_wallet: Pubkey`
- `q1_available_usd_cents: u64`
- `q2_available_usd_cents: u64`
### 4.6. `VaultState`
Поля:
- `version: u8`
Это техническая метка существования inflow PDA. Лампорты лежат на самом PDA-аккаунте.
## 5. Константы и экономика
Текущие базовые значения:
- `COEF_SCALE_PPM = 1_000_000`
- `START_COEF_PPM = 5_000_000` (5.0x)
- `START_LIMIT_USD_CENTS = 1_000_000` (10_000 USD)
- `START_CALL_REWARD_LAMPORTS = 8_000_000`
- `MAX_CALL_REWARD_LAMPORTS = 10_000_000`
- `ORACLE_MAX_AGE_SECS = 120`
- цена SOL/USD берётся из Pyth.
## 6. Инструкция `init`
### Назначение
Инициализировать все базовые PDA программы.
### Создаваемые PDA
- `config_pda`
- `coef_limit_pda`
- `queues_pda`
- `inflow_vault_pda`
### Кто может вызвать
Технически любой signer, если система ещё не инициализирована.
### Что записывается
- `dao_wallet` берётся из settings/deploy config;
- `inflow_vault` указывается как PDA самой программы;
- коэффициенты и лимиты пишутся стартовыми значениями;
- очереди стартуют с нулями.
## 7. Инструкция `update_coef_limit`
### Назначение
Изменить коэффициент выплат, лимит очереди Q1 и награду за шаг выплат.
### Авторизация
Только `dao_wallet` из `ConfigState`.
### Проверки
- signer = `config.dao_wallet`
- `coef_ppm > 0`
- `limit_usd_cents > 0`
- `call_reward_lamports <= MAX_CALL_REWARD_LAMPORTS`
## 8. Инструкция `grant_manager_limits`
### Назначение
Выдать менеджеру квоты на добавление тикетов вручную.
### Авторизация
Только DAO.
### Поведение
- создаёт `manager_allowance_pda`, если её ещё нет;
- увеличивает доступные лимиты Q1/Q2 для указанного менеджера;
- не добавляет тикеты сама.
## 9. Инструкции покупки тикета
Есть три входных варианта:
- `buy_ticket`
- `buy_ticket_usd`
- `buy_ticket_sol`
Все они в итоге сводятся к одному действию:
- рассчитывается `purchase_usd_cents`;
- создаётся ticket в очереди `Q1`;
- обновляются агрегаты очереди `Q1`.
### 9.1. Общие правила
- ticket создаётся только в `Q1`;
- очередь `Q1` временно блокируется, если достигнут её суммарный лимит;
- `payout_usd_cents = purchase_usd_cents * coef_ppm / COEF_SCALE_PPM`;
- `recipient_wallet` записывается в ticket.
### 9.2. Важная деталь денежного потока
В текущей реализации при покупке тикета лампорты переводятся:
- не во inflow vault;
- а напрямую в `dao_wallet`.
То есть:
- ticket фиксирует долг на будущую выплату;
- а inflow vault — отдельный источник средств для исполнения payouts.
Эта деталь обязательно должна быть осознана при переписи: либо её сохраняют как сознательную модель, либо отдельно меняют и тогда обновляют этот документ.
## 10. Инструкция `manager_add_ticket`
### Назначение
Дать менеджеру возможность вручную создать ticket в `Q1` или `Q2`.
### Авторизация
Signer должен совпадать с `manager_wallet` в `manager_allowance_pda`.
### Проверки
- `queue_id` только `1` или `2`;
- `payout_usd_cents > 0`;
- доступный allowance по очереди не меньше суммы тикета;
- ticket PDA ещё не существует.
### Эффект
- создаётся ticket;
- allowance уменьшается;
- агрегаты соответствующей очереди увеличиваются.
## 11. Инструкция `step_payout`
### Назначение
Сделать один шаг исполнения очереди выплат.
### Выбор очереди
- если в `Q1` есть pending ticket, сначала обслуживается `Q1`;
- если `Q1` пустая, обслуживается `Q2`.
### Что считается pending
```text
pending = tickets_total - tickets_paid
```
### Как выбирается ticket
Берётся следующий индекс:
```text
next_index = tickets_paid + 1
```
### Проверки
- `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda` валидны;
- `dao_wallet` совпадает с `config.dao_wallet`;
- `next_ticket_pda` совпадает с PDA следующего тикета;
- тикет ещё не оплачен;
- `ticket_recipient_wallet` совпадает с `ticket.recipient_wallet`;
- в inflow vault достаточно средств на весь шаг.
### Сколько переводится
Для target queue:
- получателю тикета переводится `ticket_lamports`;
- DAO переводится `dao_lamports`;
- вызвавшему шаг переводится `call_reward_lamports`.
`dao_multiplier`:
- для `Q1` = `1`
- для `Q2` = `2`
То есть DAO получает:
- `1x payout_usd` для Q1;
- `2x payout_usd` для Q2.
### Источник денег
Все три перевода идут из inflow vault PDA.
### Если pending нет
Если обе очереди пусты:
- весь доступный остаток inflow vault переводится в DAO wallet.
## 12. Инструкция `change_ticket_recipient`
### Назначение
Позволить текущему получателю тикета поменять адрес получения, пока ticket ещё не исполнен.
### Авторизация
Только текущий `ticket.recipient_wallet`.
### Ограничения
Нельзя менять получателя у следующего тикета на выплату в активной очереди.
Логика:
- если в `Q1` есть pending — следующий ticket определяется в `Q1`;
- иначе берётся следующий ticket в `Q2`;
- если текущий ticket и есть этот ближайший ticket, смена recipient запрещена.
## 13. Pyth oracle и конвертация
Программа использует Pyth SOL/USD.
Проверки oracle:
- передан именно тот oracle account, что указан в settings;
- feed id совпадает с ожидаемым;
- цена не старше `ORACLE_MAX_AGE_SECS`;
- цена положительная и корректно переводима в ratio.
Внутренние преобразования:
- `lamports -> usd_cents` делаются с округлением вниз;
- `usd_cents -> lamports` делаются с округлением вверх.
Это важно, чтобы не недоплачивать обязательства при payout.
## 14. Ошибки и классы отказа
Программа должна различать как минимум:
- неверный inflow vault;
- неверный DAO wallet;
- неавторизованный DAO;
- неавторизованный manager;
- неверный manager allowance PDA;
- неверный queue id;
- тикет уже выплачен;
- неверный recipient;
- нельзя сменить recipient ближайшего тикета;
- недостаточно средств inflow vault для step payout;
- queue temporarily paused из-за лимита;
- oracle account/feed/price invalid;
- slippage exceeded.
## 15. Что должно сохраниться при переписи без Anchor
Обязательно сохранить:
- те же PDA seed-правила;
- те же состояния и поля;
- ту же модель очередей Q1/Q2;
- ту же приоритетность `Q1` над `Q2` в `step_payout`;
- ту же логику allowance менеджера;
- те же oracle-ограничения и округления;
- ту же текущую модель денежных потоков, если она не меняется отдельным решением.
Если при переписи вы решите поменять экономическую модель, сначала нужно обновить этот документ.