# Программа `shine_payments`

Документ описывает текущее целевое поведение программы `shine_payments`.

Текущая целевая реализация программы:

- без Anchor;
- без использования вспомогательной зависимости из `programs/common`;
- на чистом `solana_program` с ручным разбором инструкций, PDA и состояний.

Назначение программы:

- хранить общие настройки экономической модели платежей и выплат;
- принимать покупку тикетов очереди выплат;
- хранить три очереди и отдельные ticket PDA;
- выдавать лимиты менеджерам на ручное добавление тикетов;
- выполнять пошаговые выплаты из inflow-вольта.

Если код и этот документ расходятся, нужно в том же изменении синхронизировать либо код, либо документ.

## 1. Program ID

Текущий program id:

- `c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW`

## 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`
- queue 3 seed prefix: `shine_payments_q3_ticket`
- второй seed: `ticket_index` в little-endian `u64`

### 3.3. Manager allowance PDA

- seed prefix: `shine_p_manager_allow`
- второй seed: `manager_wallet.as_ref()`

### 3.4. Правило создания PDA (защита от «минирования» адреса)

Все адреса PDA детерминированы (особенно тикеты: seed + последовательный `ticket_index`),
поэтому злоумышленник может заранее вычислить адрес будущего тикета и перевести на него
немного лампортов обычным system-переводом. Если бы создание шло строго через
`system_instruction::create_account`, такой «подсев» приводил бы к ошибке
«account already in use» и навсегда блокировал бы покупку/добавление тикета (griefing-DoS).

Поэтому `create_pda_account` создаёт аккаунт устойчиво к предзаполненному балансу:

- если на адресе нет лампортов — обычный `create_account` (быстрый путь);
- если лампорты уже есть — «создание поверх предзаполненного»: добор ренты переводом,
  затем `allocate` + `assign` под подписью PDA.

Признак «инициализируемого» аккаунта (`is_uninitialized_account`) — пустые данные и
владелец System Program; условие нулевого баланса намеренно не проверяется. Уже созданный
программой PDA имеет данные/владельца = program id и повторно инициализирован не будет.

## 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`
- `q3_tickets_total`
- `q3_tickets_paid`
- `q3_sum_total_usd_cents`
- `q3_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`
- `q3_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/Q3 для указанного менеджера;
- не добавляет тикеты сама.

## 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;
- `recipient_wallet` **не должен совпадать с адресом inflow-вольта** (см. §10.1).

### 9.2. Важная деталь денежного потока

В текущей реализации при покупке тикета лампорты переводятся:

- не во inflow vault;
- а напрямую в `dao_wallet`.

То есть:

- ticket фиксирует долг на будущую выплату;
- а inflow vault — отдельный источник средств для исполнения payouts.

Эта деталь обязательно должна быть осознана при переписи: либо её сохраняют как сознательную модель, либо отдельно меняют и тогда обновляют этот документ.

## 10. Инструкция `manager_add_ticket`

### Назначение

Дать менеджеру возможность вручную создать ticket в `Q1`, `Q2` или `Q3`.

### Авторизация

Signer должен совпадать с `manager_wallet` в `manager_allowance_pda`.

### Проверки

- `queue_id` только `1`, `2` или `3`;
- `payout_usd_cents > 0`;
- `recipient_wallet` не равен адресу inflow-вольта (см. §10.1);
- доступный allowance по очереди не меньше суммы тикета;
- ticket PDA ещё не существует.

### Эффект

- создаётся ticket;
- allowance уменьшается;
- агрегаты соответствующей очереди увеличиваются.

### 10.1. Запрет получателя, равного inflow-вольту (важно для безопасности)

`recipient_wallet` тикета не должен совпадать с адресом `inflow_vault` PDA.
Причина: в `step_payout` выплата получателю исполняется прямой манипуляцией
балансами (`transfer_from_vault`), где источником служит сам вольт. Если получатель
равен вольту, один и тот же аккаунт попадает в инструкцию дважды (в Solana дубли
делят общий `RefCell`), и второй mutable-займ лампортов завершается ошибкой. Такой
тикет невозможно ни оплатить, ни пропустить — он навсегда блокирует обслуживание
очереди (а если он в `Q1`, то и `Q2`/`Q3`) и замораживает средства вольта.

Поэтому равенство `recipient == inflow_vault` запрещено во всех точках, где
получатель задаётся: `buy_ticket*`, `manager_add_ticket`, `change_ticket_recipient`.
Дополнительно `transfer_from_vault` содержит защиту по умолчанию
`require!(vault.key != recipient.key)`.

## 11. Инструкция `step_payout`

### Назначение

Сделать один шаг исполнения очереди выплат.

### Выбор очереди

- если в `Q1` есть pending ticket, сначала обслуживается `Q1`;
- если `Q1` пустая, обслуживается `Q2`;
- если `Q1` и `Q2` пустые, обслуживается `Q3`.

### Что считается 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`
- для `Q3` = `3`

То есть DAO получает:

- `1x payout_usd` для Q1;
- `2x payout_usd` для Q2;
- `3x payout_usd` для Q3.

### Источник денег

Все три перевода идут из inflow vault PDA.

### Если pending нет

Если все три очереди пусты:

- весь доступный остаток inflow vault переводится в DAO wallet.

## 12. Инструкция `change_ticket_recipient`

### Назначение

Позволить текущему получателю тикета поменять адрес получения, пока ticket ещё не исполнен.

### Авторизация

Только текущий `ticket.recipient_wallet`.

### Ограничения

`new_recipient_wallet` не должен совпадать с адресом inflow-вольта (см. §10.1).

Нельзя менять получателя у следующего тикета на выплату в активной очереди.

Логика:

- если в `Q1` есть pending — следующий ticket определяется в `Q1`;
- иначе если в `Q2` есть pending — следующий ticket берётся в `Q2`;
- иначе следующий ticket берётся в `Q3`;
- если текущий ticket и есть этот ближайший ticket, смена recipient запрещена.

## 13. Pyth oracle и конвертация

Программа использует Pyth SOL/USD.

Проверки oracle:

- передан именно тот oracle account, что указан в settings;
- owner oracle-аккаунта совпадает с Pyth Solana Receiver program;
- feed id совпадает с ожидаемым `PYTH_SOL_USD_FEED_ID`;
- verification level должен быть `Full`;
- цена не старше `ORACLE_MAX_AGE_SECS`;
- доверительный интервал (`conf`) не должен быть шире `ORACLE_MAX_CONFIDENCE_PPM`;
- цена положительная и корректно переводима в ratio.

Реализация чтения:

- для декодирования price update используется официальный open-source `pyth-solana-receiver-sdk`;
- ручной парсинг по фиксированным offset-ам не используется.

Внутренние преобразования:

- `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/Q3;
- ту же приоритетность `Q1 -> Q2 -> Q3` в `step_payout`;
- ту же логику allowance менеджера;
- те же oracle-ограничения и округления;
- ту же текущую модель денежных потоков, если она не меняется отдельным решением.

Если при переписи вы решите поменять экономическую модель, сначала нужно обновить этот документ.
