diff --git a/shine/AGENTS.md b/shine/AGENTS.md index f6e8869..a2dc824 100644 --- a/shine/AGENTS.md +++ b/shine/AGENTS.md @@ -23,3 +23,11 @@ 1. Обновить соответствующий документ в `doc/` в том же изменении. 2. Если документ сразу обновить нельзя, обязательно явно согласовать это с пользователем в чате и зафиксировать план обновления. + +## Rule: Git Push + +Для push в удаленный репозиторий использовать токен из переменной окружения: + +- `GITEA_TOKEN` + +Push выполнять через `http.extraHeader` (Authorization) без вывода токена в логи. diff --git a/shine/doc/SHINE_PAYMENTS_TEST_PLAN.md b/shine/doc/SHINE_PAYMENTS_TEST_PLAN.md new file mode 100644 index 0000000..ddf2b87 --- /dev/null +++ b/shine/doc/SHINE_PAYMENTS_TEST_PLAN.md @@ -0,0 +1,50 @@ +# SHINE Payments v2: тестовый план (Devnet) + +## Цель + +Проверить: + +1. создание PDA и инициализацию; +2. покупку обычных билетов (очередь 1); +3. выдачу DAO-лимитов менеджеру; +4. создание менеджерских билетов в очередь 1 и 2; +5. корректный приоритет выплат (очередь 1 > очередь 2); +6. перевод средств в DAO и награды вызывающему шаг выплат. + +## Сценарий А: один кошелек + +1. Открыть `admin_tools`, выполнить `init`. +2. В `buy_ticket` купить несколько билетов в очередь 1. +3. В `dao_tools` (тем же кошельком, если он DAO) выдать лимиты менеджеру. +4. В `manager_tools` создать билеты в очередь 1 и 2. +5. Пополнить inflow-вольт вручную. +6. В `track_ticket` запускать шаг выплат и смотреть, что: + - сначала платится очередь 1; + - после исчерпания очереди 1 платится очередь 2; + - если в процессе снова появился билет в очереди 1, приоритет возвращается к ней. + +## Сценарий Б: разные кошельки + +1. Кошелек DAO: выдает лимиты менеджерам. +2. Кошелек менеджера: добавляет тикеты через `manager_add_ticket`. +3. Кошельки покупателей: покупают обычные билеты. +4. Любой кошелек: вызывает `step_payout`. + +## Проверка результата + +1. У получателей тикетов растут балансы. +2. В DAO поступает симметричная сумма `X` при каждом шаге выплаты. +3. Вызывающий шаг выплат получает фиксированную награду. +4. Агрегаты очередей (`total/paid/sum_total/sum_paid`) изменяются ожидаемо. + +## Возврат средств после теста + +1. Перевести остатки SOL с тестовых кошельков на исходный кошелек. +2. При необходимости закрыть неиспользуемые program-аккаунты и вернуть ренту. +3. Проверить, что ключевые кошельки и devnet-параметры не содержат лишних средств/прав после завершения теста. + +## Ограничения текущего этапа + +1. DAO пока заменен обычным кошельком. +2. Финальный governance (голосование DAO) не подключен. +3. Расчеты пока в SOL/lamports, переход на USDT по курсу запланирован. diff --git a/shine/doc/SHINE_PAYMENTS_V2.md b/shine/doc/SHINE_PAYMENTS_V2.md index 9ab88dc..9eb7773 100644 --- a/shine/doc/SHINE_PAYMENTS_V2.md +++ b/shine/doc/SHINE_PAYMENTS_V2.md @@ -1,42 +1,50 @@ -# SHINE Payments v2 (краткое описание) +# SHINE Payments v2 ## Назначение -`shine_payments` v2 — контракт очереди выплат: +`shine_payments` v2 — контракт очереди выплат с двумя очередями: -1. Покупка билета в очередь 1: - - пользователь переводит SOL в DAO; - - получает тикет с суммой будущей выплаты `input * coef`. -2. Шаг выплат: - - из inflow-вольта платится следующий тикет; - - такая же сумма отправляется в DAO; - - фиксированная награда отправляется вызвавшему шаг. +1. обычная покупка билета (очередь 1); +2. менеджерское добавление билетов (очередь 1 и очередь 2 по лимитам от DAO); +3. пошаговые выплаты из inflow-вольта с приоритетом очереди 1. + +Сейчас тестовый этап в Devnet: расчеты в SOL/lamports. +Следующий этап — модель расчета в USDT по курсу. ## PDA 1. `config_pda` (`shine_payments_v2_config`) - `dao_wallet` - - `manager_wallet` + - `manager_wallet` (права на смену coef/limit) - `inflow_vault` - `call_reward_lamports` 2. `coef_limit_pda` (`shine_payments_v2_coef_limit`) - - `coef_ppm` (коэффициент в fixed-point, scale = 1_000_000) - - `limit_lamports` + - `coef_ppm` (fixed-point, scale = 1_000_000) + - `limit_lamports` (лимит долга очереди 1 для обычной покупки) 3. `queues_pda` (`shine_payments_v2_queues`) - очередь 1: `tickets_total`, `tickets_paid`, `sum_total`, `sum_paid` - очередь 2: `tickets_total`, `tickets_paid`, `sum_total`, `sum_paid` 4. `inflow_vault_pda` (`shine_payments_v2_inflow_vault`) - - PDA-вольт программы для выплат. + - входящий PDA-вольт программы для выплат. -5. `q1_ticket_pda` (`shine_payments_v2_q1_ticket + index_le_u64`) - - `is_paid` - - `recipient_wallet` - - `payout_lamports` - - `debt_before_lamports` - - `index` +5. `ticket_pda` + - очередь 1: `shine_payments_v2_q1_ticket + index_le_u64` + - очередь 2: `shine_payments_v2_q2_ticket + index_le_u64` + - поля тикета: + - `queue_id` + - `index` + - `is_paid` + - `recipient_wallet` + - `payout_lamports` + - `debt_before_lamports` + +6. `manager_allowance_pda` (`shine_payments_v2_manager_allowance + manager_pubkey`) + - `manager_wallet` + - `q1_available_lamports` + - `q2_available_lamports` ## Методы @@ -44,24 +52,36 @@ - вызывается один раз (кто угодно); - создает `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`. -2. `update_coef_limit` (только manager) - - меняет коэффициент и лимит. +2. `update_coef_limit` (только `manager_wallet` из config) + - меняет коэффициент и лимит покупки в очередь 1. -3. `buy_ticket` - - проверяет, что текущий долг очереди 1 меньше лимита; - - переводит входную сумму в DAO; - - создает тикет в очереди 1; - - увеличивает агрегаты очереди. +3. `grant_manager_limits` (только `dao_wallet` из config) + - DAO выдает/добавляет лимиты менеджеру: + - `add_q1_lamports` + - `add_q2_lamports` + - если PDA менеджера нет — создается; + - если есть — лимиты увеличиваются. -4. `step_payout` - - если есть тикеты в очереди 1: - - платит `X` получателю тикета, - - платит `X` в DAO, - - платит reward вызвавшему; - - помечает тикет выплаченным, обновляет агрегаты. - - если обе очереди пусты/полностью выплачены: - - переводит из inflow-вольта в DAO весь доступный остаток (сверх ренты), - - reward не платится. +4. `buy_ticket` + - обычная покупка билета в очередь 1; + - сумма покупки идет в DAO; + - тикет получает выплату `input * coef_ppm / 1_000_000`. + +5. `manager_add_ticket` + - менеджер добавляет тикет в очередь 1 или 2; + - без денежного перевода; + - списывает лимит менеджера по выбранной очереди. + +6. `step_payout` + - выбирает очередь по приоритету: + 1. сначала очередь 1; + 2. если в 1-й нет ожидания — очередь 2. + - шаг выплаты: + - `X` получателю тикета, + - `X` в DAO, + - `reward` вызывающему. + - если обе очереди пусты/выплачены: + - переводит весь доступный остаток inflow-вольта в DAO (без reward). ## Стартовые настройки @@ -70,4 +90,9 @@ - `START_COEF_PPM = 5_000_000` (коэффициент 5.0) - `START_LIMIT_LAMPORTS = 100 SOL` - `START_CALL_REWARD_LAMPORTS = 0.008 SOL` -- `DAO_WALLET`, `MANAGER_WALLET` +- `DAO_WALLET` +- `MANAGER_WALLET` + +## Тестовый режим + +Пока нет финального production-потока пополнения inflow из регистрации/экосистемы, inflow-вольт пополняется вручную в Devnet, после чего выполняются шаги выплат. diff --git a/shine/programs/shine_payments/src/lib.rs b/shine/programs/shine_payments/src/lib.rs index 869ca8d..fe08be5 100644 --- a/shine/programs/shine_payments/src/lib.rs +++ b/shine/programs/shine_payments/src/lib.rs @@ -121,6 +121,71 @@ pub mod shine_payments { Ok(()) } + pub fn grant_manager_limits( + ctx: Context, + args: GrantManagerLimitsArgs, + ) -> Result<()> { + let config = read_state::(&ctx.accounts.config_pda)?; + require_keys_eq!( + config.dao_wallet, + ctx.accounts.signer.key(), + PaymentsError::UnauthorizedDao + ); + require!( + args.add_q1_lamports > 0 || args.add_q2_lamports > 0, + PaymentsError::InvalidAmount + ); + + let (expected_pda, bump) = find_manager_allowance_pda(ctx.program_id, &args.manager_wallet); + require_keys_eq!( + expected_pda, + ctx.accounts.manager_allowance_pda.key(), + ErrCode::InvalidPdaAddress + ); + + let system_program_ai = ctx.accounts.system_program.to_account_info(); + let mut state = if ctx.accounts.manager_allowance_pda.owner == &Pubkey::default() { + let initial = ManagerAllowanceState { + version: 1, + manager_wallet: args.manager_wallet, + q1_available_lamports: 0, + q2_available_lamports: 0, + }; + create_state_with_seeds( + ctx.program_id, + &ctx.accounts.signer, + &system_program_ai, + &ctx.accounts.manager_allowance_pda, + &[ + settings::MANAGER_ALLOWANCE_SEED, + args.manager_wallet.as_ref(), + &[bump], + ], + settings::MANAGER_ALLOWANCE_SPACE, + &initial, + )?; + initial + } else { + read_state::(&ctx.accounts.manager_allowance_pda)? + }; + require_keys_eq!( + state.manager_wallet, + args.manager_wallet, + PaymentsError::InvalidManagerWallet + ); + + state.q1_available_lamports = state + .q1_available_lamports + .checked_add(args.add_q1_lamports) + .ok_or(error!(ErrCode::MathOverflow))?; + state.q2_available_lamports = state + .q2_available_lamports + .checked_add(args.add_q2_lamports) + .ok_or(error!(ErrCode::MathOverflow))?; + write_state(&ctx.accounts.manager_allowance_pda, &state)?; + Ok(()) + } + pub fn buy_ticket(ctx: Context, args: BuyTicketArgs) -> Result<()> { require!(args.amount_lamports > 0, PaymentsError::InvalidAmount); let config = read_state::(&ctx.accounts.config_pda)?; @@ -210,6 +275,125 @@ pub mod shine_payments { Ok(()) } + pub fn manager_add_ticket( + ctx: Context, + args: ManagerAddTicketArgs, + ) -> Result<()> { + require!(args.payout_lamports > 0, PaymentsError::InvalidPayoutAmount); + require!(args.queue_id == 1 || args.queue_id == 2, PaymentsError::InvalidTicketQueue); + + let (expected_manager_pda, _) = + find_manager_allowance_pda(ctx.program_id, ctx.accounts.signer.key); + require_keys_eq!( + expected_manager_pda, + ctx.accounts.manager_allowance_pda.key(), + ErrCode::InvalidPdaAddress + ); + let mut allowance = read_state::(&ctx.accounts.manager_allowance_pda)?; + require_keys_eq!( + allowance.manager_wallet, + ctx.accounts.signer.key(), + PaymentsError::InvalidManagerWallet + ); + + let mut queues = read_state::(&ctx.accounts.queues_pda)?; + let current_debt = if args.queue_id == 1 { + queues + .q1_sum_total + .checked_sub(queues.q1_sum_paid) + .ok_or(error!(ErrCode::MathOverflow))? + } else { + queues + .q2_sum_total + .checked_sub(queues.q2_sum_paid) + .ok_or(error!(ErrCode::MathOverflow))? + }; + + if args.queue_id == 1 { + require!( + allowance.q1_available_lamports >= args.payout_lamports, + PaymentsError::ManagerLimitExceeded + ); + } else { + require!( + allowance.q2_available_lamports >= args.payout_lamports, + PaymentsError::ManagerLimitExceeded + ); + } + + let ticket_index = if args.queue_id == 1 { + queues + .q1_tickets_total + .checked_add(1) + .ok_or(error!(ErrCode::MathOverflow))? + } else { + queues + .q2_tickets_total + .checked_add(1) + .ok_or(error!(ErrCode::MathOverflow))? + }; + let (expected_ticket_pda, ticket_bump) = + find_ticket_pda(ctx.program_id, args.queue_id, ticket_index); + require_keys_eq!( + expected_ticket_pda, + ctx.accounts.ticket_pda.key(), + ErrCode::InvalidPdaAddress + ); + require!( + ctx.accounts.ticket_pda.owner == &Pubkey::default(), + ErrCode::PdaAlreadyExists + ); + + let ticket = TicketState { + version: 1, + queue_id: args.queue_id, + index: ticket_index, + is_paid: false, + recipient_wallet: args.recipient_wallet, + payout_lamports: args.payout_lamports, + debt_before_lamports: current_debt, + }; + let seed_prefix = if args.queue_id == 1 { + settings::Q1_TICKET_SEED + } else { + settings::Q2_TICKET_SEED + }; + create_state_with_seeds( + ctx.program_id, + &ctx.accounts.signer, + &ctx.accounts.system_program.to_account_info(), + &ctx.accounts.ticket_pda, + &[seed_prefix, &ticket_index.to_le_bytes(), &[ticket_bump]], + settings::TICKET_SPACE, + &ticket, + )?; + + if args.queue_id == 1 { + allowance.q1_available_lamports = allowance + .q1_available_lamports + .checked_sub(args.payout_lamports) + .ok_or(error!(ErrCode::MathOverflow))?; + queues.q1_tickets_total = ticket_index; + queues.q1_sum_total = queues + .q1_sum_total + .checked_add(args.payout_lamports) + .ok_or(error!(ErrCode::MathOverflow))?; + } else { + allowance.q2_available_lamports = allowance + .q2_available_lamports + .checked_sub(args.payout_lamports) + .ok_or(error!(ErrCode::MathOverflow))?; + queues.q2_tickets_total = ticket_index; + queues.q2_sum_total = queues + .q2_sum_total + .checked_add(args.payout_lamports) + .ok_or(error!(ErrCode::MathOverflow))?; + } + write_state(&ctx.accounts.manager_allowance_pda, &allowance)?; + write_state(&ctx.accounts.queues_pda, &queues)?; + Ok(()) + } + pub fn step_payout(ctx: Context) -> Result<()> { let config = read_state::(&ctx.accounts.config_pda)?; let mut queues = read_state::(&ctx.accounts.queues_pda)?; @@ -235,10 +419,7 @@ pub mod shine_payments { .checked_sub(queues.q2_tickets_paid) .ok_or(error!(ErrCode::MathOverflow))?; - if q1_pending == 0 { - if q2_pending > 0 { - return Err(error!(PaymentsError::SecondQueueNotImplemented)); - } + if q1_pending == 0 && q2_pending == 0 { transfer_all_available_to_dao( &ctx.accounts.inflow_vault_pda, &ctx.accounts.dao_wallet, @@ -246,11 +427,19 @@ pub mod shine_payments { return Ok(()); } - let next_index = queues - .q1_tickets_paid - .checked_add(1) - .ok_or(error!(ErrCode::MathOverflow))?; - let (expected_ticket_pda, _) = find_ticket_pda(ctx.program_id, 1, next_index); + let target_queue = if q1_pending > 0 { 1 } else { 2 }; + let next_index = if target_queue == 1 { + queues + .q1_tickets_paid + .checked_add(1) + .ok_or(error!(ErrCode::MathOverflow))? + } else { + queues + .q2_tickets_paid + .checked_add(1) + .ok_or(error!(ErrCode::MathOverflow))? + }; + let (expected_ticket_pda, _) = find_ticket_pda(ctx.program_id, target_queue, next_index); require_keys_eq!( expected_ticket_pda, ctx.accounts.next_ticket_pda.key(), @@ -258,7 +447,10 @@ pub mod shine_payments { ); let mut ticket = read_state::(&ctx.accounts.next_ticket_pda)?; - require!(ticket.queue_id == 1, PaymentsError::InvalidTicketQueue); + require!( + ticket.queue_id == target_queue, + PaymentsError::InvalidTicketQueue + ); require!(ticket.index == next_index, PaymentsError::InvalidTicketIndex); require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid); require_keys_eq!( @@ -296,14 +488,25 @@ pub mod shine_payments { ticket.is_paid = true; write_state(&ctx.accounts.next_ticket_pda, &ticket)?; - queues.q1_tickets_paid = queues - .q1_tickets_paid - .checked_add(1) - .ok_or(error!(ErrCode::MathOverflow))?; - queues.q1_sum_paid = queues - .q1_sum_paid - .checked_add(ticket.payout_lamports) - .ok_or(error!(ErrCode::MathOverflow))?; + if target_queue == 1 { + queues.q1_tickets_paid = queues + .q1_tickets_paid + .checked_add(1) + .ok_or(error!(ErrCode::MathOverflow))?; + queues.q1_sum_paid = queues + .q1_sum_paid + .checked_add(ticket.payout_lamports) + .ok_or(error!(ErrCode::MathOverflow))?; + } else { + queues.q2_tickets_paid = queues + .q2_tickets_paid + .checked_add(1) + .ok_or(error!(ErrCode::MathOverflow))?; + queues.q2_sum_paid = queues + .q2_sum_paid + .checked_add(ticket.payout_lamports) + .ok_or(error!(ErrCode::MathOverflow))?; + } write_state(&ctx.accounts.queues_pda, &queues)?; Ok(()) @@ -343,6 +546,20 @@ pub struct UpdateCoefLimit<'info> { pub coef_limit_pda: AccountInfo<'info>, } +#[derive(Accounts)] +pub struct GrantManagerLimits<'info> { + /// CHECK: подписант DAO, проверяется атрибутом `signer` и сверкой с config.dao_wallet. + #[account(mut, signer)] + pub signer: AccountInfo<'info>, + /// CHECK: PDA конфига, читается и валидируется вручную. + #[account(mut)] + pub config_pda: AccountInfo<'info>, + /// CHECK: PDA лимитов менеджера, адрес проверяется вручную по manager pubkey. + #[account(mut)] + pub manager_allowance_pda: AccountInfo<'info>, + pub system_program: Program<'info, System>, +} + #[derive(Accounts)] pub struct BuyTicket<'info> { /// CHECK: подписант-покупатель, проверяется атрибутом `signer`. @@ -366,6 +583,23 @@ pub struct BuyTicket<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct ManagerAddTicket<'info> { + /// CHECK: подписант-менеджер, проверяется атрибутом `signer`. + #[account(mut, signer)] + pub signer: AccountInfo<'info>, + /// CHECK: PDA лимитов менеджера, адрес сверяется вручную по signer. + #[account(mut)] + pub manager_allowance_pda: AccountInfo<'info>, + /// CHECK: PDA очередей, читается и валидируется вручную. + #[account(mut)] + pub queues_pda: AccountInfo<'info>, + /// CHECK: PDA тикета, адрес и состояние (должен быть пустым) проверяются вручную. + #[account(mut)] + pub ticket_pda: AccountInfo<'info>, + pub system_program: Program<'info, System>, +} + #[derive(Accounts)] pub struct StepPayout<'info> { /// CHECK: подписант-вызвавший шаг выплат, проверяется атрибутом `signer`. @@ -397,12 +631,26 @@ pub struct UpdateCoefLimitArgs { pub limit_lamports: u64, } +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct GrantManagerLimitsArgs { + pub manager_wallet: Pubkey, + pub add_q1_lamports: u64, + pub add_q2_lamports: u64, +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct BuyTicketArgs { pub amount_lamports: u64, pub recipient_wallet: Pubkey, } +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct ManagerAddTicketArgs { + pub queue_id: u8, + pub recipient_wallet: Pubkey, + pub payout_lamports: u64, +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct ConfigState { pub version: u8, @@ -443,6 +691,14 @@ pub struct TicketState { pub debt_before_lamports: u64, } +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct ManagerAllowanceState { + pub version: u8, + pub manager_wallet: Pubkey, + pub q1_available_lamports: u64, + pub q2_available_lamports: u64, +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct VaultState { pub version: u8, @@ -460,6 +716,8 @@ pub enum PaymentsError { InvalidDaoWallet, #[msg("Управляющий кошелек не авторизован")] UnauthorizedManager, + #[msg("DAO кошелек не авторизован для этой операции")] + UnauthorizedDao, #[msg("Некорректный коэффициент")] InvalidCoefficient, #[msg("Некорректный лимит")] @@ -480,8 +738,10 @@ pub enum PaymentsError { InvalidTicketIndex, #[msg("Неверный тип очереди у тикета")] InvalidTicketQueue, - #[msg("Вторая очередь пока не реализована для выплат")] - SecondQueueNotImplemented, + #[msg("Неверный кошелек менеджера")] + InvalidManagerWallet, + #[msg("Лимит менеджера по выбранной очереди превышен")] + ManagerLimitExceeded, } fn ensure_expected_pdas(program_id: &Pubkey, accounts: &Init) -> Result<()> { @@ -522,6 +782,13 @@ fn find_ticket_pda(program_id: &Pubkey, queue_id: u8, index: u64) -> (Pubkey, u8 Pubkey::find_program_address(&[seed, &idx], program_id) } +fn find_manager_allowance_pda(program_id: &Pubkey, manager_wallet: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[settings::MANAGER_ALLOWANCE_SEED, manager_wallet.as_ref()], + program_id, + ) +} + fn create_and_store_state<'info, T: AnchorSerialize>( program_id: &Pubkey, payer: &AccountInfo<'info>, diff --git a/shine/programs/shine_payments/src/settings.rs b/shine/programs/shine_payments/src/settings.rs index e97aa92..6a03c88 100644 --- a/shine/programs/shine_payments/src/settings.rs +++ b/shine/programs/shine_payments/src/settings.rs @@ -4,12 +4,14 @@ pub const QUEUES_SEED: &[u8] = b"shine_payments_v2_queues"; pub const INFLOW_VAULT_SEED: &[u8] = b"shine_payments_v2_inflow_vault"; pub const Q1_TICKET_SEED: &[u8] = b"shine_payments_v2_q1_ticket"; pub const Q2_TICKET_SEED: &[u8] = b"shine_payments_v2_q2_ticket"; +pub const MANAGER_ALLOWANCE_SEED: &[u8] = b"shine_payments_v2_manager_allowance"; pub const CONFIG_SPACE: usize = 8 + 160; pub const COEF_LIMIT_SPACE: usize = 8 + 96; pub const QUEUES_SPACE: usize = 8 + 192; pub const INFLOW_VAULT_SPACE: usize = 8 + 32; pub const TICKET_SPACE: usize = 8 + 160; +pub const MANAGER_ALLOWANCE_SPACE: usize = 8 + 128; pub const COEF_SCALE_PPM: u64 = 1_000_000; pub const START_COEF_PPM: u64 = 5_000_000; // 5.0 diff --git a/shine/programs/shine_payments/web/admin_tools.html b/shine/programs/shine_payments/web/admin_tools.html index ab273dc..20a1ac6 100644 --- a/shine/programs/shine_payments/web/admin_tools.html +++ b/shine/programs/shine_payments/web/admin_tools.html @@ -46,16 +46,20 @@
-

Адреса и балансы

+

Адреса и агрегаты

Загрузка...

Очередь 1 (все билеты)

-
- -
-
+
+
+
+ +
+

Очередь 2 (все билеты)

+
+
@@ -69,8 +73,8 @@ queues: "shine_payments_v2_queues", inflow: "shine_payments_v2_inflow_vault", ticketQ1: "shine_payments_v2_q1_ticket", + ticketQ2: "shine_payments_v2_q2_ticket", }; - let walletPubkey = null; let cache = null; document.getElementById("programId").textContent = PROGRAM_ID.toBase58(); @@ -160,7 +164,6 @@ if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден"); return window.solana; } - async function connectWallet() { const provider = getProvider(); const r = await provider.connect(); @@ -168,7 +171,6 @@ document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58(); await refreshAll(); } - async function sendInstruction(ix) { const provider = getProvider(); if (!walletPubkey) await connectWallet(); @@ -189,9 +191,9 @@ const [inflowPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.inflow)], PROGRAM_ID); return { configPda, coefPda, queuesPda, inflowPda }; } - - function q1TicketPda(index) { - const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.ticketQ1), u64ToBytes(index)], PROGRAM_ID); + function ticketPda(queueId, index) { + const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2; + const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(seed), u64ToBytes(index)], PROGRAM_ID); return pda; } @@ -215,10 +217,7 @@ connection.getMinimumBalanceForRentExemption(inflowAi.data.length, "confirmed"), ]); cache = { - pdas, - config, - coef, - queues, + pdas, config, coef, queues, inflowLamports: BigInt(inflowAi.lamports), inflowAvailable: BigInt(Math.max(0, inflowAi.lamports - inflowRent)), daoBalance: BigInt(daoBal), @@ -240,7 +239,7 @@
DAO: ${core.config.dao.toBase58()}
Inflow vault: ${core.config.inflow.toBase58()}
Inflow vault — это входящий PDA-кошелек выплат программы.
-
Manager: ${core.config.manager.toBase58()}
+
Manager (для coef/limit): ${core.config.manager.toBase58()}
Награда за шаг: ${lamportsToSolStr(core.config.reward)} SOL
Коэффициент: ${coefText}, лимит: ${lamportsToSolStr(core.coef.limit)} SOL
Баланс DAO: ${lamportsToSolStr(core.daoBalance)} SOL
@@ -260,11 +259,9 @@ out.textContent = ""; try { const provider = getProvider(); - if (!walletPubkey) { - await connectWallet(); - } else if (!provider.isConnected) { - await provider.connect(); - } + if (!walletPubkey) await connectWallet(); + else if (!provider.isConnected) await provider.connect(); + const pdas = derivePdas(); const disc = await ixDiscriminator("init"); const keys = [ @@ -280,13 +277,7 @@ out.innerHTML = `Init выполнен. Tx: ${sig}`; await refreshAll(); } catch (e) { - const raw = String(e.message || e); - if (isUnauthorizedManager(raw)) { - const mgr = document.getElementById("managerAllowed").textContent; - out.innerHTML = `Вы подключены не под тем аккаунтом. Изменение доступно только управляющему кошельку: ${mgr}.`; - return; - } - out.innerHTML = `${raw}`; + out.innerHTML = `${String(e.message || e)}`; } } @@ -295,11 +286,8 @@ out.textContent = ""; try { const provider = getProvider(); - if (!walletPubkey) { - await connectWallet(); - } else if (!provider.isConnected) { - await provider.connect(); - } + if (!walletPubkey) await connectWallet(); + else if (!provider.isConnected) await provider.connect(); const core = await loadCore(); if (core.notInited) throw new Error("Сначала выполните init"); @@ -320,23 +308,30 @@ out.innerHTML = `Обновлено. Tx: ${sig}`; await refreshAll(); } catch (e) { - out.innerHTML = `${String(e.message || e)}`; + const raw = String(e.message || e); + if (isUnauthorizedManager(raw)) { + const mgr = document.getElementById("managerAllowed").textContent; + out.innerHTML = `Вы подключены не под тем аккаунтом. Изменение доступно только управляющему кошельку: ${mgr}.`; + return; + } + out.innerHTML = `${raw}`; } } - async function showQueue() { - const out = document.getElementById("queueTable"); + async function showQueue(queueId) { + const out = document.getElementById(queueId === 1 ? "queue1Table" : "queue2Table"); out.textContent = "Загрузка..."; try { const core = await loadCore(); if (core.notInited) throw new Error("Сначала выполните init"); - if (core.queues.q1Total === 0n) { - out.innerHTML = `Очередь 1 пока пустая.`; + const total = queueId === 1 ? core.queues.q1Total : core.queues.q2Total; + if (total === 0n) { + out.innerHTML = `Очередь ${queueId} пока пустая.`; return; } const rows = []; - for (let i = 1n; i <= core.queues.q1Total; i++) { - const pda = q1TicketPda(i); + for (let i = 1n; i <= total; i++) { + const pda = ticketPda(queueId, i); const ai = await connection.getAccountInfo(pda, "confirmed"); if (!ai) { rows.push(`${i.toString()}PDA не найден`); @@ -378,7 +373,8 @@ document.getElementById("refreshBtn").addEventListener("click", refreshAll); document.getElementById("initBtn").addEventListener("click", runInit); document.getElementById("updateCoefBtn").addEventListener("click", updateCoefLimit); - document.getElementById("loadQueueBtn").addEventListener("click", showQueue); + document.getElementById("loadQ1Btn").addEventListener("click", () => showQueue(1)); + document.getElementById("loadQ2Btn").addEventListener("click", () => showQueue(2)); refreshAll(); diff --git a/shine/programs/shine_payments/web/buy_ticket.html b/shine/programs/shine_payments/web/buy_ticket.html index 6394e6d..6097896 100644 --- a/shine/programs/shine_payments/web/buy_ticket.html +++ b/shine/programs/shine_payments/web/buy_ticket.html @@ -37,6 +37,7 @@

Покупка билета в 1-й очереди

+
Покупка создает билет только в 1-й очереди. Билеты 2-й очереди добавляются менеджерами с правами.
diff --git a/shine/programs/shine_payments/web/dao_tools.html b/shine/programs/shine_payments/web/dao_tools.html new file mode 100644 index 0000000..7141a3e --- /dev/null +++ b/shine/programs/shine_payments/web/dao_tools.html @@ -0,0 +1,253 @@ + + + + + + DAO-права менеджеров — Shine Payments Devnet + + + +

DAO: права менеджеров (Devnet)

+
Программа:
+ +
+
+ Пока реального DAO-голосования нет: роль DAO выполняет обычный кошелек.
+ Позже это заменяется на вызов из DAO-казначейства/голосования. +
+
+ +
+
+ + +
+
+
+
+ +
+

Выдать/добавить лимиты менеджеру

+
+ + + +
+
+ +
+
+
+ +
+

Текущие лимиты менеджера

+
+ +
+
+
+ + + + + diff --git a/shine/programs/shine_payments/web/index.html b/shine/programs/shine_payments/web/index.html index e0eb0da..ac93870 100644 --- a/shine/programs/shine_payments/web/index.html +++ b/shine/programs/shine_payments/web/index.html @@ -33,12 +33,37 @@

Отслеживание билета

-
Проверка позиции в очереди, статуса и шаг выплат.
+
Проверка позиции, статуса и шаг выплат по 1-й/2-й очереди.

Тех. инструменты

-
Init, просмотр всех билетов, коэффициент/лимит, адреса и балансы.
+
Init, просмотр всех билетов в обеих очередях, коэффициент/лимит, агрегаты.
+
+ + +

DAO-права менеджеров

+
Выдача лимитов менеджерам на добавление билетов в очередь 1/2.
+
+ + +

Инструменты менеджера

+
Показ лимитов менеджера и создание билетов в очередь 1/2.
+
+ + +

Логика работы

+
Кратко: как работают очереди, выплаты, лимиты и тестовый режим.
+
+ + +

Что ещё нужно до реального DAO

+
Ограничения тестовой версии и шаги к production.
+
+ + +

Сценарий тестирования

+
Пошаговая методика тестов и возврата средств после теста.
diff --git a/shine/programs/shine_payments/web/logic_overview.html b/shine/programs/shine_payments/web/logic_overview.html new file mode 100644 index 0000000..09bbde5 --- /dev/null +++ b/shine/programs/shine_payments/web/logic_overview.html @@ -0,0 +1,50 @@ + + + + + + Логика работы — Shine Payments Devnet + + + +

Логика работы Shine Payments (тестовый этап)

+
+

Сейчас система работает в Devnet. Все суммы на этом этапе в SOL/lamports.

+

В следующей версии расчёт будет считаться в USDT по курсу (переход на курсовую модель).

+
+ +
+

1. Очереди и билеты

+

Есть две очереди: очередь 1 и очередь 2. Каждый билет — отдельный PDA с полями: очередь, индекс, получатель, сумма выплаты, флаг выплачен/нет, сумма долга перед билетом.

+
+ +
+

2. Покупка билета

+

Обычная покупка создаёт билет только в очереди 1. Сумма покупки идёт в DAO, а сумма билета рассчитывается как input * coef.

+
+ +
+

3. Менеджерские билеты

+

DAO может выдать менеджеру лимиты на добавление билетов отдельно в очередь 1 и очередь 2. Менеджер создаёт билеты без денежного перевода, но с уменьшением своего доступного лимита.

+
+ +
+

4. Порядок выплат

+

Приоритет всегда у очереди 1. Если в очереди 1 нет невыплаченных билетов, идут выплаты очереди 2. Если в процессе выплат очереди 2 снова появляется билет в очереди 1, приоритет возвращается к очереди 1.

+

Если обе очереди пустые/выплачены — доступный остаток inflow-вольта переводится в DAO (без награды вызывающему).

+
+ +
+

5. Тестовый режим пополнения выплат

+

Пока регистрация/авто-поток пополнения не завершены, inflow-вольт для выплат пополняется вручную, после чего выполняются шаги выплат.

+
+ +
+ Подробная версия в документе репозитория: shine/doc/SHINE_PAYMENTS_V2.md. +
+ + diff --git a/shine/programs/shine_payments/web/manager_tools.html b/shine/programs/shine_payments/web/manager_tools.html new file mode 100644 index 0000000..510176c --- /dev/null +++ b/shine/programs/shine_payments/web/manager_tools.html @@ -0,0 +1,258 @@ + + + + + + Менеджерские билеты — Shine Payments Devnet + + + +

Менеджер: создание билетов (Devnet)

+
Программа:
+ +
+
+ + +
+
+
+ +
+

Лимиты менеджера

+
Загрузка...
+
+ +
+

Создать билет менеджером

+
+ + + +
+
+ +
+
+
+ + + + + diff --git a/shine/programs/shine_payments/web/roadmap_dao.html b/shine/programs/shine_payments/web/roadmap_dao.html new file mode 100644 index 0000000..6bf88e5 --- /dev/null +++ b/shine/programs/shine_payments/web/roadmap_dao.html @@ -0,0 +1,33 @@ + + + + + + Что ещё нужно до DAO — Shine Payments Devnet + + + +

Что ещё нужно до реального DAO

+ +
+

Сейчас роль DAO выполняет обычный кошелёк (тестовый режим Devnet).

+
+ +
+

Что нужно добавить в production:

+
    +
  1. Заменить обычный DAO-кошелёк на DAO-казначейство/голосование.
  2. +
  3. DAO-решения (выдача лимитов менеджерам, изменение параметров) проводить через голосование.
  4. +
  5. Переход с SOL-сумм на модель расчёта в USDT по курсу.
  6. +
  7. Ограничить тестовые ключи и закрыть доступ к приватным данным.
  8. +
+
+ +
+

Текущий дизайн уже совместим с DAO-заменой: достаточно сменить авторизацию вызовов на DAO-механику без изменения базовой структуры очередей и билетов.

+
+ + diff --git a/shine/programs/shine_payments/web/test_plan.html b/shine/programs/shine_payments/web/test_plan.html new file mode 100644 index 0000000..b2f8776 --- /dev/null +++ b/shine/programs/shine_payments/web/test_plan.html @@ -0,0 +1,51 @@ + + + + + + Сценарий тестирования — Shine Payments Devnet + + + +

Сценарий тестирования Shine Payments (Devnet)

+ +
+

Вариант А: один кошелёк

+
    +
  1. Открыть admin_tools, выполнить init.
  2. +
  3. Открыть buy_ticket, купить несколько билетов.
  4. +
  5. Открыть dao_tools, выдать лимиты менеджеру (тем же кошельком).
  6. +
  7. Открыть manager_tools, создать билеты в очередь 1 и очередь 2.
  8. +
  9. Пополнить inflow-вольт вручную.
  10. +
  11. Открыть track_ticket, выполнять шаги выплат до погашения очередей.
  12. +
  13. Проверить, что средства уходят получателям/DAO в ожидаемой пропорции.
  14. +
+
+ +
+

Вариант Б: несколько кошельков

+
    +
  1. Кошелёк 1: DAO (выдаёт лимиты менеджерам).
  2. +
  3. Кошелёк 2: менеджер (создаёт билеты в очередь 1/2).
  4. +
  5. Кошелёк 3+: покупатели (создают обычные билеты через покупку).
  6. +
  7. Любой кошелёк может запускать шаг выплат.
  8. +
+
+ +
+

Как вернуть средства после тестов

+
    +
  1. Довести выплаты до нужного состояния (или остановить на текущем шаге).
  2. +
  3. Сделать переводы с тестовых кошельков обратно на исходный кошелёк.
  4. +
  5. При необходимости закрыть неиспользуемые program/PDA-аккаунты и вернуть ренту (через CLI).
  6. +
+
+ +
+

Пока DAO-гovernance не подключена, ключевые действия DAO выполняются обычным тестовым кошельком. В production это заменяется голосованием DAO.

+
+ + diff --git a/shine/programs/shine_payments/web/track_ticket.html b/shine/programs/shine_payments/web/track_ticket.html index 1896cfa..4112a67 100644 --- a/shine/programs/shine_payments/web/track_ticket.html +++ b/shine/programs/shine_payments/web/track_ticket.html @@ -8,7 +8,7 @@ body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; } .panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; } - input { padding: 8px; min-width: 260px; } + input { padding: 8px; min-width: 240px; } button { padding: 8px 12px; cursor: pointer; } .muted { color: #666; } .ok { color: #0a7a3c; } @@ -32,7 +32,7 @@
-

Поиск билета

+

Поиск билетов

@@ -58,14 +58,12 @@ const SEEDS = { config: "shine_payments_v2_config", - coef: "shine_payments_v2_coef_limit", queues: "shine_payments_v2_queues", ticketQ1: "shine_payments_v2_q1_ticket", + ticketQ2: "shine_payments_v2_q2_ticket", }; - let walletPubkey = null; let cachedCore = null; - document.getElementById("programId").textContent = PROGRAM_ID.toBase58(); function utf8(s) { return new TextEncoder().encode(s); } @@ -91,8 +89,7 @@ return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, ""); } function lamportsToSolStr(l) { - const sol = Number(l) / 1_000_000_000; - return trimZeros(sol.toFixed(9)); + return trimZeros((Number(l) / 1_000_000_000).toFixed(9)); } async function ixDiscriminator(name) { const msg = utf8("global:" + name); @@ -142,7 +139,6 @@ if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден"); return window.solana; } - async function connectWallet() { const provider = getProvider(); const r = await provider.connect(); @@ -150,7 +146,6 @@ document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58(); await refreshAll(); } - async function sendInstruction(ix) { const provider = getProvider(); if (!walletPubkey) await connectWallet(); @@ -169,9 +164,9 @@ const [queuesPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID); return { configPda, queuesPda }; } - - function deriveQ1TicketPda(index) { - const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.ticketQ1), u64ToBytes(index)], PROGRAM_ID); + function deriveTicketPda(queueId, index) { + const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2; + const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(seed), u64ToBytes(index)], PROGRAM_ID); return pda; } @@ -192,14 +187,20 @@ return cachedCore; } + function nextStepQueue(queues) { + const q1Pending = queues.q1Total - queues.q1Paid; + const q2Pending = queues.q2Total - queues.q2Paid; + if (q1Pending > 0n) return 1; + if (q2Pending > 0n) return 2; + return 0; + } + async function refreshPayoutInfo() { const el = document.getElementById("payoutInfo"); try { const core = await loadCoreState(); - const q1Pending = core.queues.q1Total - core.queues.q1Paid; - const q2Pending = core.queues.q2Total - core.queues.q2Paid; - - if (q1Pending === 0n && q2Pending === 0n) { + const queue = nextStepQueue(core.queues); + if (queue === 0) { el.innerHTML = `
Обе очереди пусты/полностью выплачены.
На inflow vault доступно (сверх ренты): ${lamportsToSolStr(core.available)} SOL
@@ -207,22 +208,18 @@ `; return; } - if (q1Pending === 0n && q2Pending > 0n) { - el.innerHTML = `
Во 2-й очереди есть ожидание, но её выплаты пока не реализованы.
`; - return; - } - - const nextIndex = core.queues.q1Paid + 1n; - const nextPda = deriveQ1TicketPda(nextIndex); + const nextIndex = queue === 1 ? core.queues.q1Paid + 1n : core.queues.q2Paid + 1n; + const nextPda = deriveTicketPda(queue, nextIndex); const nextAi = await connection.getAccountInfo(nextPda, "confirmed"); if (!nextAi) { - el.innerHTML = `
Не найден следующий тикет #${nextIndex.toString()}
`; + el.innerHTML = `
Не найден следующий тикет #${nextIndex.toString()} для очереди ${queue}
`; return; } const next = parseTicket(nextAi.data); const need = next.payout * 2n + core.config.reward; const missing = core.available >= need ? 0n : (need - core.available); el.innerHTML = ` +
Следующий шаг выплат: очередь ${queue}
Следующий тикет: #${next.index.toString()}
Выплата по тикету: ${lamportsToSolStr(next.payout)} SOL
Нужно для шага (выплата + DAO + награда): ${lamportsToSolStr(need)} SOL
@@ -237,6 +234,34 @@ } } + function renderTicketCard(core, pda, t) { + const nextQ1 = core.queues.q1Paid + 1n; + const nextQ2 = core.queues.q2Paid + 1n; + const isCurrentQ1 = !t.isPaid && t.queueId === 1 && t.index === nextQ1; + const isCurrentQ2 = !t.isPaid && t.queueId === 2 && (core.queues.q1Total - core.queues.q1Paid) === 0n && t.index === nextQ2; + const inFront = t.queueId === 1 + ? (t.index > nextQ1 ? (t.index - nextQ1) : 0n) + : (t.index > nextQ2 ? (t.index - nextQ2) : 0n); + const sumPaid = t.queueId === 1 ? core.queues.q1SumPaid : core.queues.q2SumPaid; + const remainingToThis = t.debtBefore > sumPaid ? (t.debtBefore - sumPaid) : 0n; + const missingInsideCurrent = (isCurrentQ1 || isCurrentQ2) && core.available < t.payout ? (t.payout - core.available) : 0n; + return ` +
+
Тикет #${t.index.toString()} (очередь ${t.queueId}) (${t.isPaid ? "выплачен" : "ожидание"})
+
PDA: ${pda.toBase58()}
+
Получатель: ${t.recipient.toBase58()}
+
Сумма выплаты: ${lamportsToSolStr(t.payout)} SOL
+
Билетов перед ним сейчас: ${inFront.toString()}
+
До его выплаты по сумме в предыдущих билетах осталось: ${lamportsToSolStr(remainingToThis)} SOL
+ ${t.queueId === 2 && !t.isPaid ? `
Для 2-й очереди оценка не окончательная: 1-я очередь может увеличиваться.
` : ``} + ${(isCurrentQ1 || isCurrentQ2) ? `
Это текущий билет к выплате.
` : ``} + ${((isCurrentQ1 || isCurrentQ2) && missingInsideCurrent > 0n) + ? `
Для выплаты именно этого билета внутри его суммы не хватает: ${lamportsToSolStr(missingInsideCurrent)} SOL.
` + : ``} +
+ `; + } + async function findTickets() { const out = document.getElementById("ticketResult"); out.textContent = ""; @@ -244,53 +269,37 @@ const core = await loadCoreState(); const idxRaw = document.getElementById("ticketIndex").value.trim(); const walletRaw = document.getElementById("recipientWallet").value.trim(); - const results = []; + if (idxRaw) { const idx = BigInt(idxRaw); - const pda = deriveQ1TicketPda(idx); - const ai = await connection.getAccountInfo(pda, "confirmed"); - if (!ai) throw new Error(`Тикет #${idx.toString()} не найден`); - const t = parseTicket(ai.data); - results.push({ pda, t }); - } else if (walletRaw) { - const recipient = new solanaWeb3.PublicKey(walletRaw); - for (let i = 1n; i <= core.queues.q1Total; i++) { - const pda = deriveQ1TicketPda(i); + for (const queue of [1, 2]) { + const pda = deriveTicketPda(queue, idx); const ai = await connection.getAccountInfo(pda, "confirmed"); if (!ai) continue; - const t = parseTicket(ai.data); - if (t.recipient.toBase58() === recipient.toBase58()) { - results.push({ pda, t }); + results.push({ pda, t: parseTicket(ai.data) }); + } + if (results.length === 0) throw new Error(`Тикет #${idx.toString()} не найден ни в одной очереди`); + } else if (walletRaw) { + const recipient = new solanaWeb3.PublicKey(walletRaw); + for (const queue of [1, 2]) { + const total = queue === 1 ? core.queues.q1Total : core.queues.q2Total; + for (let i = 1n; i <= total; i++) { + const pda = deriveTicketPda(queue, i); + const ai = await connection.getAccountInfo(pda, "confirmed"); + if (!ai) continue; + const t = parseTicket(ai.data); + if (t.recipient.toBase58() === recipient.toBase58()) { + results.push({ pda, t }); + } } } if (results.length === 0) throw new Error("По этому кошельку тикеты не найдены"); } else { - throw new Error("Введите номер тикета или кошелек"); + throw new Error("Введите номер билета или кошелек получателя"); } - const nextIndex = core.queues.q1Paid + 1n; - const lines = results.map(({ pda, t }) => { - const isCurrent = !t.isPaid && t.index === nextIndex; - const inFront = t.index > nextIndex ? (t.index - nextIndex) : 0n; - const remainingToThis = t.debtBefore > core.queues.q1SumPaid ? (t.debtBefore - core.queues.q1SumPaid) : 0n; - const missingInsideCurrentTicket = isCurrent && core.available < t.payout ? (t.payout - core.available) : 0n; - return ` -
-
Тикет #${t.index.toString()} (${t.isPaid ? "выплачен" : "ожидание"})
-
PDA: ${pda.toBase58()}
-
Получатель: ${t.recipient.toBase58()}
-
Сумма выплаты: ${lamportsToSolStr(t.payout)} SOL
-
Билетов перед ним сейчас: ${inFront.toString()}
-
До его выплаты по сумме в предыдущих билетах осталось: ${lamportsToSolStr(remainingToThis)} SOL
- ${isCurrent ? `
Это текущий билет к выплате.
` : ``} - ${isCurrent && missingInsideCurrentTicket > 0n - ? `
Для выплаты именно этого билета внутри его суммы не хватает: ${lamportsToSolStr(missingInsideCurrentTicket)} SOL.
` - : ``} -
- `; - }); - out.innerHTML = lines.join(""); + out.innerHTML = results.map(({ pda, t }) => renderTicketCard(core, pda, t)).join(""); } catch (e) { out.innerHTML = `
${String(e.message || e)}
`; } @@ -307,19 +316,19 @@ await provider.connect(); } const core = cachedCore || await loadCoreState(); - const q1Pending = core.queues.q1Total - core.queues.q1Paid; + const queue = nextStepQueue(core.queues); let nextTicketPda; let recipient; - if (q1Pending > 0n) { - const nextIndex = core.queues.q1Paid + 1n; - nextTicketPda = deriveQ1TicketPda(nextIndex); - const ai = await connection.getAccountInfo(nextTicketPda, "confirmed"); - if (!ai) throw new Error(`Следующий тикет #${nextIndex.toString()} не найден`); - recipient = parseTicket(ai.data).recipient; - } else { - nextTicketPda = deriveQ1TicketPda(core.queues.q1Paid + 1n); + if (queue === 0) { + nextTicketPda = deriveTicketPda(1, core.queues.q1Paid + 1n); recipient = walletPubkey; + } else { + const nextIndex = queue === 1 ? core.queues.q1Paid + 1n : core.queues.q2Paid + 1n; + nextTicketPda = deriveTicketPda(queue, nextIndex); + const ai = await connection.getAccountInfo(nextTicketPda, "confirmed"); + if (!ai) throw new Error(`Следующий тикет #${nextIndex.toString()} для очереди ${queue} не найден`); + recipient = parseTicket(ai.data).recipient; } const disc = await ixDiscriminator("step_payout"); diff --git a/КОШЕЛЬКИ_DEVNET_ТЕСТ.md b/КОШЕЛЬКИ_DEVNET_ТЕСТ.md new file mode 100644 index 0000000..3d33840 --- /dev/null +++ b/КОШЕЛЬКИ_DEVNET_ТЕСТ.md @@ -0,0 +1,35 @@ +# Кошельки Devnet (тестовые) + +> Только для тестового этапа. Не использовать в production. + +## DAO (текущий) + +- public: `6bFc5Gz5qF172GQhK5HpDbWs8F6qcSxdHn5XqAstf1fY` +- private (base58): `3VYfYZZ3ugmgwisiQQAfcimX9T65AE9BmwmYVixAUj4jyneccSE9rzbC3g5twvH7ECZ8xgp7emJo3pR4yQqCwjGn` + +## Manager для coef/limit (текущий) + +- public: `4yzHKs2zFXpyqqCETe8KpAs4xhEo4QhJ2ybyTgRZphZv` +- private (json array): + +```json +[90,184,152,226,36,29,0,20,192,35,239,186,138,46,197,219,65,216,46,150,150,113,95,216,66,95,68,79,178,239,166,133,59,44,90,189,77,253,249,234,240,215,66,104,14,194,200,186,203,176,232,154,245,165,226,23,127,115,246,181,134,24,148,45] +``` + +## Тестовый кошелёк key1 + +- public: `HMww7YSVfwVm4i8sugqj7wyH26dqzHykzv3wzWwzEvPA` +- private (base58): `5pbFo9Zq1VsNheHwbEp6AZKa6R62CZHoGkJFZnugpMEtCmkQFjuUP7TgA5hSPqv4NABGmPP62qVnDPHmRqEAwvJc` + +## Тестовый кошелёк key2 + +- public: `E3ZDHbWv1qiFvDTmaRc9wjFCgbQw6UmKJLJYbaTNvjAh` +- private (base58): `5qm1GJGXB1fFJ3YsU5Y3XXgTiQfaimqBWk79oEveFASH9D2of3jqUoT7dumBvS449fW5j5Sw8MgAMH2QBMmFPdry` + +## Тестовый кошелёк phantomWallet + +- private (json array): + +```json +[221,119,143,125,90,136,155,115,191,198,210,85,228,111,251,118,168,138,27,60,249,62,247,24,121,228,139,112,218,69,55,143,215,21,229,69,219,1,74,36,10,239,63,163,48,240,58,208,237,251,209,37,17,202,215,77,13,165,178,18,141,21,193,64] +```