# Программа `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()` ## 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. ### 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`; - доступный allowance по очереди не меньше суммы тикета; - ticket PDA ещё не существует. ### Эффект - создаётся ticket; - allowance уменьшается; - агрегаты соответствующей очереди увеличиваются. ## 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`. ### Ограничения Нельзя менять получателя у следующего тикета на выплату в активной очереди. Логика: - если в `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-ограничения и округления; - ту же текущую модель денежных потоков, если она не меняется отдельным решением. Если при переписи вы решите поменять экономическую модель, сначала нужно обновить этот документ.