Обновить логику Shine Payments, UI и задеплоить с новыми Program ID

This commit is contained in:
AidarKC 2026-05-11 14:34:20 +03:00
parent 7e8ca9157b
commit ea25b908c1
14 changed files with 600 additions and 359 deletions

View File

@ -6,12 +6,12 @@ resolution = true
skip-lint = false skip-lint = false
[programs.devnet] [programs.devnet]
shine_payments = "4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE" shine_payments = "m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR"
shine_users = "8Z3HQizFRhyVu5cNBwWNBXZHTpu89VMkn7Wuk1oCtkeJ" shine_users = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm"
[programs.localnet] [programs.localnet]
shine_payments = "4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE" shine_payments = "m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR"
shine_users = "5dFcWDNp42Xn9Vv4oDMJzM4obBJ8hvDuAtPX54fT5L3t" shine_users = "FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm"
[registry] [registry]
url = "https://api.apr.dev" url = "https://api.apr.dev"

View File

@ -15,13 +15,13 @@
1. `config_pda` (`shine_payments_v2_config`) 1. `config_pda` (`shine_payments_v2_config`)
- `dao_wallet` - `dao_wallet`
- `manager_wallet` (права на смену coef/limit) - `manager_wallet` (сервисный параметр, для будущих сценариев)
- `inflow_vault` - `inflow_vault`
- `call_reward_lamports`
2. `coef_limit_pda` (`shine_payments_v2_coef_limit`) 2. `coef_limit_pda` (`shine_payments_v2_coef_limit`)
- `coef_ppm` (fixed-point, scale = 1_000_000) - `coef_ppm` (fixed-point, scale = 1_000_000)
- `limit_lamports` (лимит долга очереди 1 для обычной покупки) - `limit_lamports` (лимит суммарной исторической суммы очереди 1 для обычной покупки)
- `call_reward_lamports` (награда за шаг выплат, максимум 0.01 SOL)
3. `queues_pda` (`shine_payments_v2_queues`) 3. `queues_pda` (`shine_payments_v2_queues`)
- очередь 1: `tickets_total`, `tickets_paid`, `sum_total`, `sum_paid` - очередь 1: `tickets_total`, `tickets_paid`, `sum_total`, `sum_paid`
@ -52,8 +52,9 @@
- вызывается один раз (кто угодно); - вызывается один раз (кто угодно);
- создает `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`. - создает `config_pda`, `coef_limit_pda`, `queues_pda`, `inflow_vault_pda`.
2. `update_coef_limit` (только `manager_wallet` из config) 2. `update_coef_limit` (только `dao_wallet` из config)
- меняет коэффициент и лимит покупки в очередь 1. - меняет коэффициент, лимит покупки в очередь 1 и награду шага выплат;
- ограничение награды: не более `0.01 SOL`.
3. `grant_manager_limits` (только `dao_wallet` из config) 3. `grant_manager_limits` (только `dao_wallet` из config)
- DAO выдает/добавляет лимиты менеджеру: - DAO выдает/добавляет лимиты менеджеру:
@ -65,7 +66,8 @@
4. `buy_ticket` 4. `buy_ticket`
- обычная покупка билета в очередь 1; - обычная покупка билета в очередь 1;
- сумма покупки идет в DAO; - сумма покупки идет в DAO;
- тикет получает выплату `input * coef_ppm / 1_000_000`. - тикет получает выплату `input * coef_ppm / 1_000_000`;
- проверка лимита выполняется по `q1_sum_total` (исторически накопленная сумма, без вычета уже выплаченного).
5. `manager_add_ticket` 5. `manager_add_ticket`
- менеджер добавляет тикет в очередь 1 или 2; - менеджер добавляет тикет в очередь 1 или 2;
@ -79,7 +81,7 @@
- шаг выплаты: - шаг выплаты:
- `X` получателю тикета, - `X` получателю тикета,
- `X` в DAO, - `X` в DAO,
- `reward` вызывающему. - `reward` вызывающему (из `coef_limit_pda`).
- если обе очереди пусты/выплачены: - если обе очереди пусты/выплачены:
- переводит весь доступный остаток inflow-вольта в DAO (без reward). - переводит весь доступный остаток inflow-вольта в DAO (без reward).

View File

@ -5,7 +5,7 @@ use std::str::FromStr;
pub mod settings; pub mod settings;
declare_id!("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE"); declare_id!("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
#[program] #[program]
pub mod shine_payments { pub mod shine_payments {
@ -42,7 +42,6 @@ pub mod shine_payments {
dao_wallet, dao_wallet,
manager_wallet, manager_wallet,
inflow_vault: ctx.accounts.inflow_vault_pda.key(), inflow_vault: ctx.accounts.inflow_vault_pda.key(),
call_reward_lamports: settings::START_CALL_REWARD_LAMPORTS,
}; };
create_and_store_state( create_and_store_state(
ctx.program_id, ctx.program_id,
@ -58,6 +57,7 @@ pub mod shine_payments {
version: 1, version: 1,
coef_ppm: settings::START_COEF_PPM, coef_ppm: settings::START_COEF_PPM,
limit_lamports: settings::START_LIMIT_LAMPORTS, limit_lamports: settings::START_LIMIT_LAMPORTS,
call_reward_lamports: settings::START_CALL_REWARD_LAMPORTS,
}; };
create_and_store_state( create_and_store_state(
ctx.program_id, ctx.program_id,
@ -104,19 +104,27 @@ pub mod shine_payments {
Ok(()) Ok(())
} }
pub fn update_coef_limit(ctx: Context<UpdateCoefLimit>, args: UpdateCoefLimitArgs) -> Result<()> { pub fn update_coef_limit(
ctx: Context<UpdateCoefLimit>,
args: UpdateCoefLimitArgs,
) -> Result<()> {
let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?; let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?;
require_keys_eq!( require_keys_eq!(
config.manager_wallet, config.dao_wallet,
ctx.accounts.signer.key(), ctx.accounts.signer.key(),
PaymentsError::UnauthorizedManager PaymentsError::UnauthorizedDao
); );
require!(args.coef_ppm > 0, PaymentsError::InvalidCoefficient); require!(args.coef_ppm > 0, PaymentsError::InvalidCoefficient);
require!(args.limit_lamports > 0, PaymentsError::InvalidLimit); require!(args.limit_lamports > 0, PaymentsError::InvalidLimit);
require!(
args.call_reward_lamports <= settings::MAX_CALL_REWARD_LAMPORTS,
PaymentsError::InvalidCallReward
);
let mut coef_limit = read_state::<CoefLimitState>(&ctx.accounts.coef_limit_pda)?; let mut coef_limit = read_state::<CoefLimitState>(&ctx.accounts.coef_limit_pda)?;
coef_limit.coef_ppm = args.coef_ppm; coef_limit.coef_ppm = args.coef_ppm;
coef_limit.limit_lamports = args.limit_lamports; coef_limit.limit_lamports = args.limit_lamports;
coef_limit.call_reward_lamports = args.call_reward_lamports;
write_state(&ctx.accounts.coef_limit_pda, &coef_limit)?; write_state(&ctx.accounts.coef_limit_pda, &coef_limit)?;
Ok(()) Ok(())
} }
@ -198,12 +206,9 @@ pub mod shine_payments {
PaymentsError::InvalidDaoWallet PaymentsError::InvalidDaoWallet
); );
let current_debt = queues let queue1_sum_total_before = queues.q1_sum_total;
.q1_sum_total
.checked_sub(queues.q1_sum_paid)
.ok_or(error!(ErrCode::MathOverflow))?;
require!( require!(
current_debt < coef_limit.limit_lamports, queue1_sum_total_before < coef_limit.limit_lamports,
PaymentsError::QueueTemporarilyPaused PaymentsError::QueueTemporarilyPaused
); );
@ -250,7 +255,7 @@ pub mod shine_payments {
is_paid: false, is_paid: false,
recipient_wallet: args.recipient_wallet, recipient_wallet: args.recipient_wallet,
payout_lamports, payout_lamports,
debt_before_lamports: current_debt, debt_before_lamports: queue1_sum_total_before,
}; };
create_state_with_seeds( create_state_with_seeds(
ctx.program_id, ctx.program_id,
@ -280,7 +285,10 @@ pub mod shine_payments {
args: ManagerAddTicketArgs, args: ManagerAddTicketArgs,
) -> Result<()> { ) -> Result<()> {
require!(args.payout_lamports > 0, PaymentsError::InvalidPayoutAmount); require!(args.payout_lamports > 0, PaymentsError::InvalidPayoutAmount);
require!(args.queue_id == 1 || args.queue_id == 2, PaymentsError::InvalidTicketQueue); require!(
args.queue_id == 1 || args.queue_id == 2,
PaymentsError::InvalidTicketQueue
);
let (expected_manager_pda, _) = let (expected_manager_pda, _) =
find_manager_allowance_pda(ctx.program_id, ctx.accounts.signer.key); find_manager_allowance_pda(ctx.program_id, ctx.accounts.signer.key);
@ -289,7 +297,8 @@ pub mod shine_payments {
ctx.accounts.manager_allowance_pda.key(), ctx.accounts.manager_allowance_pda.key(),
ErrCode::InvalidPdaAddress ErrCode::InvalidPdaAddress
); );
let mut allowance = read_state::<ManagerAllowanceState>(&ctx.accounts.manager_allowance_pda)?; let mut allowance =
read_state::<ManagerAllowanceState>(&ctx.accounts.manager_allowance_pda)?;
require_keys_eq!( require_keys_eq!(
allowance.manager_wallet, allowance.manager_wallet,
ctx.accounts.signer.key(), ctx.accounts.signer.key(),
@ -297,16 +306,10 @@ pub mod shine_payments {
); );
let mut queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?; let mut queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?;
let current_debt = if args.queue_id == 1 { let debt_before_total = if args.queue_id == 1 {
queues queues.q1_sum_total
.q1_sum_total
.checked_sub(queues.q1_sum_paid)
.ok_or(error!(ErrCode::MathOverflow))?
} else { } else {
queues queues.q2_sum_total
.q2_sum_total
.checked_sub(queues.q2_sum_paid)
.ok_or(error!(ErrCode::MathOverflow))?
}; };
if args.queue_id == 1 { if args.queue_id == 1 {
@ -351,7 +354,7 @@ pub mod shine_payments {
is_paid: false, is_paid: false,
recipient_wallet: args.recipient_wallet, recipient_wallet: args.recipient_wallet,
payout_lamports: args.payout_lamports, payout_lamports: args.payout_lamports,
debt_before_lamports: current_debt, debt_before_lamports: debt_before_total,
}; };
let seed_prefix = if args.queue_id == 1 { let seed_prefix = if args.queue_id == 1 {
settings::Q1_TICKET_SEED settings::Q1_TICKET_SEED
@ -396,6 +399,7 @@ pub mod shine_payments {
pub fn step_payout(ctx: Context<StepPayout>) -> Result<()> { pub fn step_payout(ctx: Context<StepPayout>) -> Result<()> {
let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?; let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?;
let coef_limit = read_state::<CoefLimitState>(&ctx.accounts.coef_limit_pda)?;
let mut queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?; let mut queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?;
let _vault_state = read_state::<VaultState>(&ctx.accounts.inflow_vault_pda)?; let _vault_state = read_state::<VaultState>(&ctx.accounts.inflow_vault_pda)?;
@ -451,7 +455,10 @@ pub mod shine_payments {
ticket.queue_id == target_queue, ticket.queue_id == target_queue,
PaymentsError::InvalidTicketQueue PaymentsError::InvalidTicketQueue
); );
require!(ticket.index == next_index, PaymentsError::InvalidTicketIndex); require!(
ticket.index == next_index,
PaymentsError::InvalidTicketIndex
);
require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid); require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid);
require_keys_eq!( require_keys_eq!(
ctx.accounts.ticket_recipient_wallet.key(), ctx.accounts.ticket_recipient_wallet.key(),
@ -462,7 +469,7 @@ pub mod shine_payments {
let needed = ticket let needed = ticket
.payout_lamports .payout_lamports
.checked_add(ticket.payout_lamports) .checked_add(ticket.payout_lamports)
.and_then(|v| v.checked_add(config.call_reward_lamports)) .and_then(|v| v.checked_add(coef_limit.call_reward_lamports))
.ok_or(error!(ErrCode::MathOverflow))?; .ok_or(error!(ErrCode::MathOverflow))?;
require!( require!(
available_vault_lamports(&ctx.accounts.inflow_vault_pda)? >= needed, available_vault_lamports(&ctx.accounts.inflow_vault_pda)? >= needed,
@ -482,7 +489,7 @@ pub mod shine_payments {
transfer_from_vault( transfer_from_vault(
&ctx.accounts.inflow_vault_pda, &ctx.accounts.inflow_vault_pda,
&ctx.accounts.signer, &ctx.accounts.signer,
config.call_reward_lamports, coef_limit.call_reward_lamports,
)?; )?;
ticket.is_paid = true; ticket.is_paid = true;
@ -535,7 +542,7 @@ pub struct Init<'info> {
#[derive(Accounts)] #[derive(Accounts)]
pub struct UpdateCoefLimit<'info> { pub struct UpdateCoefLimit<'info> {
/// CHECK: подписант-менеджер, проверяется атрибутом `signer` и сверкой адреса в коде. /// CHECK: подписант-DAO, проверяется атрибутом `signer` и сверкой адреса в коде.
#[account(mut, signer)] #[account(mut, signer)]
pub signer: AccountInfo<'info>, pub signer: AccountInfo<'info>,
/// CHECK: PDA конфига, читается и валидируется вручную. /// CHECK: PDA конфига, читается и валидируется вручную.
@ -611,6 +618,9 @@ pub struct StepPayout<'info> {
/// CHECK: PDA очередей, читается и валидируется вручную. /// CHECK: PDA очередей, читается и валидируется вручную.
#[account(mut)] #[account(mut)]
pub queues_pda: AccountInfo<'info>, pub queues_pda: AccountInfo<'info>,
/// CHECK: PDA коэффициента/лимита/награды, читается и валидируется вручную.
#[account(mut)]
pub coef_limit_pda: AccountInfo<'info>,
/// CHECK: PDA inflow-вольта, адрес сверяется с конфигом вручную. /// CHECK: PDA inflow-вольта, адрес сверяется с конфигом вручную.
#[account(mut)] #[account(mut)]
pub inflow_vault_pda: AccountInfo<'info>, pub inflow_vault_pda: AccountInfo<'info>,
@ -629,6 +639,7 @@ pub struct StepPayout<'info> {
pub struct UpdateCoefLimitArgs { pub struct UpdateCoefLimitArgs {
pub coef_ppm: u64, pub coef_ppm: u64,
pub limit_lamports: u64, pub limit_lamports: u64,
pub call_reward_lamports: u64,
} }
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
@ -657,7 +668,6 @@ pub struct ConfigState {
pub dao_wallet: Pubkey, pub dao_wallet: Pubkey,
pub manager_wallet: Pubkey, pub manager_wallet: Pubkey,
pub inflow_vault: Pubkey, pub inflow_vault: Pubkey,
pub call_reward_lamports: u64,
} }
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
@ -665,6 +675,7 @@ pub struct CoefLimitState {
pub version: u8, pub version: u8,
pub coef_ppm: u64, pub coef_ppm: u64,
pub limit_lamports: u64, pub limit_lamports: u64,
pub call_reward_lamports: u64,
} }
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
@ -722,6 +733,8 @@ pub enum PaymentsError {
InvalidCoefficient, InvalidCoefficient,
#[msg("Некорректный лимит")] #[msg("Некорректный лимит")]
InvalidLimit, InvalidLimit,
#[msg("Некорректная награда за шаг выплаты")]
InvalidCallReward,
#[msg("Некорректная сумма")] #[msg("Некорректная сумма")]
InvalidAmount, InvalidAmount,
#[msg("Очередь временно приостановлена: достигнут лимит")] #[msg("Очередь временно приостановлена: достигнут лимит")]
@ -749,7 +762,11 @@ fn ensure_expected_pdas(program_id: &Pubkey, accounts: &Init) -> Result<()> {
let (coef, _) = find_single_pda(program_id, settings::COEF_LIMIT_SEED); let (coef, _) = find_single_pda(program_id, settings::COEF_LIMIT_SEED);
let (queues, _) = find_single_pda(program_id, settings::QUEUES_SEED); let (queues, _) = find_single_pda(program_id, settings::QUEUES_SEED);
let (inflow, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED); let (inflow, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED);
require_keys_eq!(config, accounts.config_pda.key(), ErrCode::InvalidPdaAddress); require_keys_eq!(
config,
accounts.config_pda.key(),
ErrCode::InvalidPdaAddress
);
require_keys_eq!( require_keys_eq!(
coef, coef,
accounts.coef_limit_pda.key(), accounts.coef_limit_pda.key(),
@ -819,14 +836,7 @@ fn create_state_with_seeds<'info, T: AnchorSerialize>(
space: usize, space: usize,
state: &T, state: &T,
) -> Result<()> { ) -> Result<()> {
create_pda( create_pda(pda, payer, system_program, program_id, seeds, space as u64)?;
pda,
payer,
system_program,
program_id,
seeds,
space as u64,
)?;
write_state(pda, state) write_state(pda, state)
} }
@ -856,7 +866,10 @@ fn transfer_from_vault(vault: &AccountInfo, recipient: &AccountInfo, amount: u64
} }
let mut vault_lamports = vault.try_borrow_mut_lamports()?; let mut vault_lamports = vault.try_borrow_mut_lamports()?;
let mut recipient_lamports = recipient.try_borrow_mut_lamports()?; let mut recipient_lamports = recipient.try_borrow_mut_lamports()?;
require!(**vault_lamports >= amount, PaymentsError::NotEnoughInflowForStep); require!(
**vault_lamports >= amount,
PaymentsError::NotEnoughInflowForStep
);
**vault_lamports = vault_lamports **vault_lamports = vault_lamports
.checked_sub(amount) .checked_sub(amount)
.ok_or(error!(ErrCode::MathOverflow))?; .ok_or(error!(ErrCode::MathOverflow))?;

View File

@ -4,7 +4,7 @@ pub const QUEUES_SEED: &[u8] = b"shine_payments_v2_queues";
pub const INFLOW_VAULT_SEED: &[u8] = b"shine_payments_v2_inflow_vault"; 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 Q1_TICKET_SEED: &[u8] = b"shine_payments_v2_q1_ticket";
pub const Q2_TICKET_SEED: &[u8] = b"shine_payments_v2_q2_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 MANAGER_ALLOWANCE_SEED: &[u8] = b"shine_p_v2_manager_allow";
pub const CONFIG_SPACE: usize = 8 + 160; pub const CONFIG_SPACE: usize = 8 + 160;
pub const COEF_LIMIT_SPACE: usize = 8 + 96; pub const COEF_LIMIT_SPACE: usize = 8 + 96;
@ -17,6 +17,7 @@ pub const COEF_SCALE_PPM: u64 = 1_000_000;
pub const START_COEF_PPM: u64 = 5_000_000; // 5.0 pub const START_COEF_PPM: u64 = 5_000_000; // 5.0
pub const START_LIMIT_LAMPORTS: u64 = 100 * 1_000_000_000; // 100 SOL pub const START_LIMIT_LAMPORTS: u64 = 100 * 1_000_000_000; // 100 SOL
pub const START_CALL_REWARD_LAMPORTS: u64 = 8_000_000; // 0.008 SOL pub const START_CALL_REWARD_LAMPORTS: u64 = 8_000_000; // 0.008 SOL
pub const MAX_CALL_REWARD_LAMPORTS: u64 = 10_000_000; // 0.01 SOL
pub const DAO_WALLET: &str = "6bFc5Gz5qF172GQhK5HpDbWs8F6qcSxdHn5XqAstf1fY"; pub const DAO_WALLET: &str = "9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb";
pub const MANAGER_WALLET: &str = "4yzHKs2zFXpyqqCETe8KpAs4xhEo4QhJ2ybyTgRZphZv"; pub const MANAGER_WALLET: &str = "4yzHKs2zFXpyqqCETe8KpAs4xhEo4QhJ2ybyTgRZphZv";

View File

@ -5,22 +5,44 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Тех. инструменты — Shine Payments Devnet</title> <title>Тех. инструменты — Shine Payments Devnet</title>
<style> <style>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; } :root {
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--ok: #55d48a;
--warn: #ffbf5e;
--err: #ff7d7d;
--btn: #273247;
--btn-hover: #32415c;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
.topbar { margin-bottom: 12px; }
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
.wrap { width: 100%; max-width: 1850px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); width: 100%; }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; } .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
input { padding: 8px; min-width: 180px; } input { padding: 9px 10px; min-width: 170px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
button { padding: 8px 12px; cursor: pointer; } button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
.muted { color: #666; } button:hover { background: var(--btn-hover); }
.ok { color: #0a7a3c; } .muted { color: var(--muted); }
.warn { color: #9f5f00; } .ok { color: var(--ok); }
.err { color: #b30000; white-space: pre-wrap; } .warn { color: var(--warn); }
.paid { color: #0a7a3c; font-weight: 700; } .err { color: var(--err); white-space: pre-wrap; }
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; } .paid { color: var(--ok); font-weight: 700; }
.formula { font-family: monospace; color: #c9d7f0; }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
table { border-collapse: collapse; width: 100%; } table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 6px; text-align: left; font-size: 14px; } th, td { border: 1px solid var(--line); padding: 6px; text-align: left; font-size: 14px; vertical-align: top; }
</style> </style>
</head> </head>
<body> <body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">На главную</a></div>
<h1>Техническая страница (Devnet)</h1> <h1>Техническая страница (Devnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div> <div class="muted">Программа: <code id="programId"></code></div>
@ -35,13 +57,16 @@
</div> </div>
<div class="panel"> <div class="panel">
<h3>Коэффициент и лимит</h3> <h3>Коэффициент, лимит и награда шага выплат</h3>
<div class="muted">Право изменения: <code id="managerAllowed">загрузка...</code></div> <div class="muted">Право изменения: <code id="daoAllowed">загрузка...</code></div>
<div class="row"> <div class="row">
<label>Коэффициент (например 5): <input id="coefInput" value="5" /></label> <label>Коэффициент (например 5): <input id="coefInput" value="5" /></label>
<label>Лимит (SOL): <input id="limitInput" value="100" /></label> <label>Лимит (SOL): <input id="limitInput" value="100" /></label>
<label>Награда шага (SOL, max 0.01): <input id="rewardInput" value="0.008" /></label>
<button id="updateCoefBtn">Обновить</button> <button id="updateCoefBtn">Обновить</button>
</div> </div>
<div class="formula">Лимит покупки Q1 = max(limit_lamports - q1_sum_total, 0)</div>
<div class="formula">Шаг выплаты = payout + payout + reward</div>
<div id="updateResult" class="muted"></div> <div id="updateResult" class="muted"></div>
</div> </div>
@ -61,10 +86,11 @@
<div class="row"><button id="loadQ2Btn">Показать очередь 2</button></div> <div class="row"><button id="loadQ2Btn">Показать очередь 2</button></div>
<div id="queue2Table" class="muted"></div> <div id="queue2Table" class="muted"></div>
</div> </div>
</div>
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script> <script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script> <script>
const PROGRAM_ID = new solanaWeb3.PublicKey("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE"); const PROGRAM_ID = new solanaWeb3.PublicKey("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
const RPC_URL = "https://api.devnet.solana.com"; const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed"); const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = { const SEEDS = {
@ -75,6 +101,7 @@
ticketQ1: "shine_payments_v2_q1_ticket", ticketQ1: "shine_payments_v2_q1_ticket",
ticketQ2: "shine_payments_v2_q2_ticket", ticketQ2: "shine_payments_v2_q2_ticket",
}; };
const MAX_REWARD_LAMPORTS = 10_000_000n;
let walletPubkey = null; let walletPubkey = null;
let cache = null; let cache = null;
document.getElementById("programId").textContent = PROGRAM_ID.toBase58(); document.getElementById("programId").textContent = PROGRAM_ID.toBase58();
@ -114,9 +141,9 @@
const hash = await crypto.subtle.digest("SHA-256", msg); const hash = await crypto.subtle.digest("SHA-256", msg);
return new Uint8Array(hash).slice(0, 8); return new Uint8Array(hash).slice(0, 8);
} }
function isUnauthorizedManager(msg) { function isUnauthorizedDao(msg) {
const s = String(msg || "").toLowerCase(); const s = String(msg || "").toLowerCase();
return s.includes("unauthorizedmanager") || s.includes("0x1774"); return s.includes("unauthorizeddao") || s.includes("0x1775");
} }
function parseConfig(data) { function parseConfig(data) {
@ -125,15 +152,15 @@
const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32; const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32; const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32; const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const reward = readU64(data, o); o += 8; return { version, dao, manager, inflow };
return { version, dao, manager, inflow, reward };
} }
function parseCoef(data) { function parseCoef(data) {
let o = 0; let o = 0;
const version = data[o++]; const version = data[o++];
const coefPpm = readU64(data, o); o += 8; const coefPpm = readU64(data, o); o += 8;
const limit = readU64(data, o); o += 8; const limit = readU64(data, o); o += 8;
return { version, coefPpm, limit }; const reward = readU64(data, o); o += 8;
return { version, coefPpm, limit, reward };
} }
function parseQueues(data) { function parseQueues(data) {
let o = 0; let o = 0;
@ -234,14 +261,16 @@
return; return;
} }
const coefText = trimZeros((Number(core.coef.coefPpm) / 1_000_000).toFixed(6)); const coefText = trimZeros((Number(core.coef.coefPpm) / 1_000_000).toFixed(6));
document.getElementById("managerAllowed").textContent = core.config.manager.toBase58(); const limitRemain = core.coef.limit > core.queues.q1SumTotal ? (core.coef.limit - core.queues.q1SumTotal) : 0n;
document.getElementById("daoAllowed").textContent = core.config.dao.toBase58();
el.innerHTML = ` el.innerHTML = `
<div>DAO: <code>${core.config.dao.toBase58()}</code></div> <div>DAO: <code>${core.config.dao.toBase58()}</code></div>
<div>Inflow vault: <code>${core.config.inflow.toBase58()}</code></div> <div>Inflow vault: <code>${core.config.inflow.toBase58()}</code></div>
<div class="muted">Inflow vault — это входящий PDA-кошелек выплат программы.</div> <div class="muted">Inflow vault — входящий PDA-кошелек выплат программы.</div>
<div>Manager (для coef/limit): <code>${core.config.manager.toBase58()}</code></div> <div>Manager (для справки): <code>${core.config.manager.toBase58()}</code></div>
<div>Награда за шаг: <b>${lamportsToSolStr(core.config.reward)} SOL</b></div> <div>Награда за шаг: <b>${lamportsToSolStr(core.coef.reward)} SOL</b></div>
<div>Коэффициент: <b>${coefText}</b>, лимит: <b>${lamportsToSolStr(core.coef.limit)} SOL</b></div> <div>Коэффициент: <b>${coefText}</b>, лимит: <b>${lamportsToSolStr(core.coef.limit)} SOL</b></div>
<div>Осталось лимита для покупки Q1: <b>${lamportsToSolStr(limitRemain)} SOL</b></div>
<div>Баланс DAO: <b>${lamportsToSolStr(core.daoBalance)} SOL</b></div> <div>Баланс DAO: <b>${lamportsToSolStr(core.daoBalance)} SOL</b></div>
<div>Баланс inflow vault: <b>${lamportsToSolStr(core.inflowLamports)} SOL</b></div> <div>Баланс inflow vault: <b>${lamportsToSolStr(core.inflowLamports)} SOL</b></div>
<div>Доступно в inflow (сверх ренты): <b>${lamportsToSolStr(core.inflowAvailable)} SOL</b></div> <div>Доступно в inflow (сверх ренты): <b>${lamportsToSolStr(core.inflowAvailable)} SOL</b></div>
@ -250,7 +279,7 @@
`; `;
} catch (e) { } catch (e) {
el.innerHTML = `<span class="err">${String(e.message || e)}</span>`; el.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
document.getElementById("managerAllowed").textContent = "не определен"; document.getElementById("daoAllowed").textContent = "не определен";
} }
} }
@ -295,9 +324,11 @@
if (!Number.isFinite(coef) || coef <= 0) throw new Error("Некорректный коэффициент"); if (!Number.isFinite(coef) || coef <= 0) throw new Error("Некорректный коэффициент");
const coefPpm = BigInt(Math.round(coef * 1_000_000)); const coefPpm = BigInt(Math.round(coef * 1_000_000));
const limitLamports = solToLamports(document.getElementById("limitInput").value.trim()); const limitLamports = solToLamports(document.getElementById("limitInput").value.trim());
const rewardLamports = solToLamports(document.getElementById("rewardInput").value.trim());
if (rewardLamports > MAX_REWARD_LAMPORTS) throw new Error("Награда не должна быть больше 0.01 SOL");
const disc = await ixDiscriminator("update_coef_limit"); const disc = await ixDiscriminator("update_coef_limit");
const data = concat(disc, u64ToBytes(coefPpm), u64ToBytes(limitLamports)); const data = concat(disc, u64ToBytes(coefPpm), u64ToBytes(limitLamports), u64ToBytes(rewardLamports));
const keys = [ const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true }, { pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: core.pdas.configPda, isSigner: false, isWritable: true }, { pubkey: core.pdas.configPda, isSigner: false, isWritable: true },
@ -309,15 +340,21 @@
await refreshAll(); await refreshAll();
} catch (e) { } catch (e) {
const raw = String(e.message || e); const raw = String(e.message || e);
if (isUnauthorizedManager(raw)) { if (isUnauthorizedDao(raw)) {
const mgr = document.getElementById("managerAllowed").textContent; const dao = document.getElementById("daoAllowed").textContent;
out.innerHTML = `<span class="warn">Вы подключены не под тем аккаунтом. Изменение доступно только управляющему кошельку: <code>${mgr}</code>.</span>`; out.innerHTML = `<span class="warn">Вы подключены не под тем аккаунтом. Изменение доступно только DAO-кошельку: <code>${dao}</code>.</span>`;
return; return;
} }
out.innerHTML = `<span class="err">${raw}</span>`; out.innerHTML = `<span class="err">${raw}</span>`;
} }
} }
function currentDebtBeforeTicket(ticket, queues) {
if (ticket.isPaid) return 0n;
const paidSum = ticket.queueId === 1 ? queues.q1SumPaid : queues.q2SumPaid;
return ticket.debtBefore > paidSum ? (ticket.debtBefore - paidSum) : 0n;
}
async function showQueue(queueId) { async function showQueue(queueId) {
const out = document.getElementById(queueId === 1 ? "queue1Table" : "queue2Table"); const out = document.getElementById(queueId === 1 ? "queue1Table" : "queue2Table");
out.textContent = "Загрузка..."; out.textContent = "Загрузка...";
@ -334,19 +371,20 @@
const pda = ticketPda(queueId, i); const pda = ticketPda(queueId, i);
const ai = await connection.getAccountInfo(pda, "confirmed"); const ai = await connection.getAccountInfo(pda, "confirmed");
if (!ai) { if (!ai) {
rows.push(`<tr><td>${i.toString()}</td><td colspan="6" class="err">PDA не найден</td></tr>`); rows.push(`<tr><td>${i.toString()}</td><td>${queueId}</td><td colspan="6" class="err">PDA не найден</td></tr>`);
continue; continue;
} }
const t = parseTicket(ai.data); const t = parseTicket(ai.data);
rows.push(` rows.push(`
<tr> <tr>
<td>${t.index.toString()}</td> <td>${t.index.toString()}</td>
<td>${t.isPaid ? '<span class="paid">да</span>' : "нет"}</td> <td>${t.queueId}</td>
<td><code>${pda.toBase58()}</code></td> <td>${t.isPaid ? '<span class="paid">выплачен</span>' : "ожидание"}</td>
<td><code>${t.recipient.toBase58()}</code></td> <td><code>${t.recipient.toBase58()}</code></td>
<td>${lamportsToSolStr(t.payout)} SOL</td> <td>${lamportsToSolStr(t.payout)} SOL</td>
<td>${lamportsToSolStr(t.debtBefore)} SOL</td> <td>${lamportsToSolStr(t.debtBefore)} SOL</td>
<td>${t.queueId}</td> <td>${lamportsToSolStr(currentDebtBeforeTicket(t, core.queues))} SOL</td>
<td><code>${pda.toBase58()}</code></td>
</tr> </tr>
`); `);
} }
@ -354,7 +392,14 @@
<table> <table>
<thead> <thead>
<tr> <tr>
<th>#</th><th>Выплачен</th><th>PDA</th><th>Получатель</th><th>Сумма</th><th>Debt Before</th><th>Очередь</th> <th>#</th>
<th>Очередь</th>
<th>Статус</th>
<th>Получатель</th>
<th>Сумма выплаты</th>
<th>Очередь до него (от старта)</th>
<th>Очередь до него (актуально)</th>
<th>PDA</th>
</tr> </tr>
</thead> </thead>
<tbody>${rows.join("")}</tbody> <tbody>${rows.join("")}</tbody>

View File

@ -5,20 +5,41 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Покупка билета — Shine Payments Devnet</title> <title>Покупка билета — Shine Payments Devnet</title>
<style> <style>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; } :root {
h1 { margin-bottom: 8px; } color-scheme: dark;
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } --bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--ok: #55d48a;
--warn: #ffbf5e;
--err: #ff7d7d;
--btn: #273247;
--btn-hover: #32415c;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
.topbar { margin-bottom: 12px; }
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
.wrap { width: 100%; max-width: 1700px; }
h1 { margin: 8px 0; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); width: 100%; }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; } .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
input { padding: 8px; min-width: 220px; } input { padding: 9px 10px; min-width: 260px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
button { padding: 8px 12px; cursor: pointer; } button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
.muted { color: #666; } button:hover { background: var(--btn-hover); }
.ok { color: #0a7a3c; } .muted { color: var(--muted); }
.warn { color: #9f5f00; } .ok { color: var(--ok); }
.err { color: #b30000; white-space: pre-wrap; } .warn { color: var(--warn); }
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; } .err { color: var(--err); white-space: pre-wrap; }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
</style> </style>
</head> </head>
<body> <body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">На главную</a></div>
<h1>Покупка билета (Devnet)</h1> <h1>Покупка билета (Devnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div> <div class="muted">Программа: <code id="programId"></code></div>
@ -31,7 +52,7 @@
</div> </div>
<div class="panel"> <div class="panel">
<h3>Текущее состояние</h3> <h3>Текущее состояние (очередь 1)</h3>
<div id="stateInfo" class="muted">Загрузка...</div> <div id="stateInfo" class="muted">Загрузка...</div>
</div> </div>
@ -47,10 +68,11 @@
</div> </div>
<div id="buyResult" class="muted"></div> <div id="buyResult" class="muted"></div>
</div> </div>
</div>
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script> <script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script> <script>
const PROGRAM_ID = new solanaWeb3.PublicKey("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE"); const PROGRAM_ID = new solanaWeb3.PublicKey("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
const RPC_URL = "https://api.devnet.solana.com"; const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed"); const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
@ -89,8 +111,7 @@
return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, ""); return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, "");
} }
function lamportsToSolStr(l) { function lamportsToSolStr(l) {
const sol = Number(l) / 1_000_000_000; return trimZeros((Number(l) / 1_000_000_000).toFixed(9));
return trimZeros(sol.toFixed(9));
} }
function solToLamports(solStr) { function solToLamports(solStr) {
const v = Number(solStr); const v = Number(solStr);
@ -110,15 +131,15 @@
const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)).toBase58(); o += 32; const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)).toBase58(); o += 32;
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)).toBase58(); o += 32; const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)).toBase58(); o += 32;
const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)).toBase58(); o += 32; const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)).toBase58(); o += 32;
const reward = readU64(data, o); o += 8; return { version, dao, manager, inflow };
return { version, dao, manager, inflow, reward };
} }
function parseCoef(data) { function parseCoef(data) {
let o = 0; let o = 0;
const version = data[o++]; const version = data[o++];
const coefPpm = readU64(data, o); o += 8; const coefPpm = readU64(data, o); o += 8;
const limit = readU64(data, o); o += 8; const limit = readU64(data, o); o += 8;
return { version, coefPpm, limit }; const reward = readU64(data, o); o += 8;
return { version, coefPpm, limit, reward };
} }
function parseQueues(data) { function parseQueues(data) {
let o = 0; let o = 0;
@ -160,7 +181,7 @@
return sig; return sig;
} }
async function derivePdas() { function derivePdas() {
const [configPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.config)], PROGRAM_ID); const [configPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.config)], PROGRAM_ID);
const [coefPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.coef)], PROGRAM_ID); const [coefPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.coef)], PROGRAM_ID);
const [queuesPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID); const [queuesPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID);
@ -168,7 +189,7 @@
} }
async function loadCoreState() { async function loadCoreState() {
const pdas = await derivePdas(); const pdas = derivePdas();
const [cfgAi, coefAi, queuesAi] = await Promise.all([ const [cfgAi, coefAi, queuesAi] = await Promise.all([
connection.getAccountInfo(pdas.configPda, "confirmed"), connection.getAccountInfo(pdas.configPda, "confirmed"),
connection.getAccountInfo(pdas.coefPda, "confirmed"), connection.getAccountInfo(pdas.coefPda, "confirmed"),
@ -185,21 +206,31 @@
const el = document.getElementById("stateInfo"); const el = document.getElementById("stateInfo");
try { try {
const { config, coef, queues } = await loadCoreState(); const { config, coef, queues } = await loadCoreState();
const currentDebt = queues.q1SumTotal - queues.q1SumPaid;
const pendingTickets = queues.q1Total - queues.q1Paid;
const remaining = coef.limit > currentDebt ? coef.limit - currentDebt : 0n;
const coefText = trimZeros((Number(coef.coefPpm) / Number(COEF_SCALE)).toFixed(6)); const coefText = trimZeros((Number(coef.coefPpm) / Number(COEF_SCALE)).toFixed(6));
const paused = currentDebt >= coef.limit;
const totalBefore = queues.q1Total;
const totalBeforeSum = queues.q1SumTotal;
const pendingBeforeCount = queues.q1Total - queues.q1Paid;
const pendingBeforeSum = queues.q1SumTotal - queues.q1SumPaid;
const nextTicketIndex = queues.q1Total + 1n;
const remainingByTotal = coef.limit > queues.q1SumTotal ? (coef.limit - queues.q1SumTotal) : 0n;
const paused = queues.q1SumTotal >= coef.limit;
el.innerHTML = ` el.innerHTML = `
<div>DAO: <code>${config.dao}</code></div> <div>DAO: <code>${config.dao}</code></div>
<div>Inflow vault: <code>${config.inflow}</code></div> <div>Inflow vault: <code>${config.inflow}</code></div>
<div class="muted">Inflow vault — это входящий PDA-кошелек выплат программы.</div> <div class="muted">Inflow vault — входящий PDA-кошелек выплат программы.</div>
<div>Коэффициент: <b>${coefText}</b></div> <div>Коэффициент: <b>${coefText}</b></div>
<div>Лимит очереди (1): <b>${lamportsToSolStr(coef.limit)} SOL</b></div> <div>Лимит очереди 1: <b>${lamportsToSolStr(coef.limit)} SOL</b></div>
<div>Текущий долг очереди (1): <b>${lamportsToSolStr(currentDebt)} SOL</b></div> <div>Награда за шаг выплат: <b>${lamportsToSolStr(coef.reward)} SOL</b></div>
<div>Билетов в ожидании до вас сейчас: <b>${pendingTickets.toString()}</b></div> <div>До вас всего билетов (исторически): <b>${totalBefore.toString()}</b></div>
<div>Осталось до изменения коэффициента/лимита: <b>${lamportsToSolStr(remaining)} SOL</b></div> <div>До вас всего сумма билетов (исторически): <b>${lamportsToSolStr(totalBeforeSum)} SOL</b></div>
<div class="${paused ? "warn" : "ok"}">${paused ? "Покупка временно приостановлена: очередь заполнена." : "Покупка доступна."}</div> <div>Из них сейчас не выплачено билетов: <b>${pendingBeforeCount.toString()}</b></div>
<div>Из них сейчас не выплачено по сумме: <b>${lamportsToSolStr(pendingBeforeSum)} SOL</b></div>
<div>Ваш билет будет номером: <b>${nextTicketIndex.toString()}</b></div>
<div>Осталось актуального лимита до изменения коэффициента: <b>${lamportsToSolStr(remainingByTotal)} SOL</b></div>
<div class="${paused ? "warn" : "ok"}">${paused ? "Покупка временно приостановлена: лимит заполнен." : "Покупка доступна."}</div>
`; `;
} catch (e) { } catch (e) {
el.innerHTML = `<div class="err">${String(e.message || e)}</div>`; el.innerHTML = `<div class="err">${String(e.message || e)}</div>`;
@ -217,8 +248,7 @@
await provider.connect(); await provider.connect();
} }
const { pdas, config, coef, queues } = await loadCoreState(); const { pdas, config, coef, queues } = await loadCoreState();
const currentDebt = queues.q1SumTotal - queues.q1SumPaid; if (queues.q1SumTotal >= coef.limit) {
if (currentDebt >= coef.limit) {
out.innerHTML = `<span class="warn">Пока временно приостановлено: очередь заполнена. После изменения коэффициента/лимита покупка снова заработает.</span>`; out.innerHTML = `<span class="warn">Пока временно приостановлено: очередь заполнена. После изменения коэффициента/лимита покупка снова заработает.</span>`;
return; return;
} }

View File

@ -5,19 +5,40 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DAO-права менеджеров — Shine Payments Devnet</title> <title>DAO-права менеджеров — Shine Payments Devnet</title>
<style> <style>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; } :root {
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--ok: #55d48a;
--warn: #ffbf5e;
--err: #ff7d7d;
--btn: #273247;
--btn-hover: #32415c;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
.topbar { margin-bottom: 12px; }
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
.wrap { width: 100%; max-width: 1800px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); width: 100%; }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; } .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
input { padding: 8px; min-width: 240px; } input { padding: 9px 10px; min-width: 220px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
button { padding: 8px 12px; cursor: pointer; } button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
.muted { color: #666; } button:hover { background: var(--btn-hover); }
.ok { color: #0a7a3c; } .muted { color: var(--muted); }
.warn { color: #9f5f00; } .ok { color: var(--ok); }
.err { color: #b30000; white-space: pre-wrap; } .warn { color: var(--warn); }
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; } .err { color: var(--err); white-space: pre-wrap; }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
</style> </style>
</head> </head>
<body> <body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">На главную</a></div>
<h1>DAO: права менеджеров (Devnet)</h1> <h1>DAO: права менеджеров (Devnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div> <div class="muted">Программа: <code id="programId"></code></div>
@ -57,15 +78,16 @@
</div> </div>
<div id="managerState" class="muted"></div> <div id="managerState" class="muted"></div>
</div> </div>
</div>
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script> <script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script> <script>
const PROGRAM_ID = new solanaWeb3.PublicKey("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE"); const PROGRAM_ID = new solanaWeb3.PublicKey("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
const RPC_URL = "https://api.devnet.solana.com"; const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed"); const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = { const SEEDS = {
config: "shine_payments_v2_config", config: "shine_payments_v2_config",
managerAllowance: "shine_payments_v2_manager_allowance", managerAllowance: "shine_p_v2_manager_allow",
}; };
let walletPubkey = null; let walletPubkey = null;
let configCache = null; let configCache = null;
@ -117,8 +139,7 @@
const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32; const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32; const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32; const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const reward = readU64(data, o); o += 8; return { version, dao, manager, inflow };
return { version, dao, manager, inflow, reward };
} }
function parseManagerAllowance(data) { function parseManagerAllowance(data) {
let o = 0; let o = 0;
@ -190,8 +211,8 @@
const provider = getProvider(); const provider = getProvider();
if (!walletPubkey) await connectWallet(); if (!walletPubkey) await connectWallet();
else if (!provider.isConnected) await provider.connect(); else if (!provider.isConnected) await provider.connect();
const { configPda, config } = configCache || await loadConfig();
const { configPda } = configCache || await loadConfig();
const manager = new solanaWeb3.PublicKey(document.getElementById("managerWallet").value.trim()); const manager = new solanaWeb3.PublicKey(document.getElementById("managerWallet").value.trim());
const addQ1 = solToLamports(document.getElementById("addQ1").value.trim()); const addQ1 = solToLamports(document.getElementById("addQ1").value.trim());
const addQ2 = solToLamports(document.getElementById("addQ2").value.trim()); const addQ2 = solToLamports(document.getElementById("addQ2").value.trim());

View File

@ -5,22 +5,37 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Главная — Shine Payments Devnet</title> <title>Главная — Shine Payments Devnet</title>
<style> <style>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; } :root {
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--hover: #1f2634;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
.wrap { width: 100%; max-width: 1800px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); }
a.card { a.card {
display: block; display: block;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
border: 1px solid #ddd; border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
padding: 14px; padding: 14px;
margin-bottom: 12px; margin-bottom: 12px;
background: var(--panel);
} }
a.card:hover { background: #fafafa; } a.card:hover { background: var(--hover); }
.muted { color: #666; } .muted { color: var(--muted); }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
</style> </style>
</head> </head>
<body> <body>
<div class="wrap">
<h1>Shine Payments Devnet</h1> <h1>Shine Payments Devnet</h1>
<div class="panel"> <div class="panel">
<div>Выберите страницу:</div> <div>Выберите страницу:</div>
@ -38,7 +53,7 @@
<a class="card" href="./admin_tools.html"> <a class="card" href="./admin_tools.html">
<h3>Тех. инструменты</h3> <h3>Тех. инструменты</h3>
<div class="muted">Init, просмотр всех билетов в обеих очередях, коэффициент/лимит, агрегаты.</div> <div class="muted">Init, просмотр всех билетов в обеих очередях, коэффициент/лимит/награда, агрегаты.</div>
</a> </a>
<a class="card" href="./dao_tools.html"> <a class="card" href="./dao_tools.html">
@ -65,5 +80,6 @@
<h3>Сценарий тестирования</h3> <h3>Сценарий тестирования</h3>
<div class="muted">Пошаговая методика тестов и возврата средств после теста.</div> <div class="muted">Пошаговая методика тестов и возврата средств после теста.</div>
</a> </a>
</div>
</body> </body>
</html> </html>

View File

@ -5,12 +5,26 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Логика работы — Shine Payments Devnet</title> <title>Логика работы — Shine Payments Devnet</title>
<style> <style>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; line-height: 1.45; } :root {
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } color-scheme: dark;
.muted { color: #666; } --bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; max-width: 1800px; line-height: 1.45; background: var(--bg); color: var(--text); }
.topbar { margin-bottom: 12px; }
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); }
.muted { color: var(--muted); }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
</style> </style>
</head> </head>
<body> <body>
<div class="topbar"><a class="back" href="./index.html">На главную</a></div>
<h1>Логика работы Shine Payments (тестовый этап)</h1> <h1>Логика работы Shine Payments (тестовый этап)</h1>
<div class="panel"> <div class="panel">
<p>Сейчас система работает в <b>Devnet</b>. Все суммы на этом этапе в SOL/lamports.</p> <p>Сейчас система работает в <b>Devnet</b>. Все суммы на этом этапе в SOL/lamports.</p>

View File

@ -5,19 +5,40 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Менеджерские билеты — Shine Payments Devnet</title> <title>Менеджерские билеты — Shine Payments Devnet</title>
<style> <style>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; } :root {
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--ok: #55d48a;
--warn: #ffbf5e;
--err: #ff7d7d;
--btn: #273247;
--btn-hover: #32415c;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
.topbar { margin-bottom: 12px; }
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
.wrap { width: 100%; max-width: 1800px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); width: 100%; }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; } .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
input, select { padding: 8px; min-width: 200px; } input, select { padding: 9px 10px; min-width: 190px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
button { padding: 8px 12px; cursor: pointer; } button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
.muted { color: #666; } button:hover { background: var(--btn-hover); }
.ok { color: #0a7a3c; } .muted { color: var(--muted); }
.warn { color: #9f5f00; } .ok { color: var(--ok); }
.err { color: #b30000; white-space: pre-wrap; } .warn { color: var(--warn); }
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; } .err { color: var(--err); white-space: pre-wrap; }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
</style> </style>
</head> </head>
<body> <body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">На главную</a></div>
<h1>Менеджер: создание билетов (Devnet)</h1> <h1>Менеджер: создание билетов (Devnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div> <div class="muted">Программа: <code id="programId"></code></div>
@ -51,14 +72,15 @@
</div> </div>
<div id="createResult" class="muted"></div> <div id="createResult" class="muted"></div>
</div> </div>
</div>
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script> <script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script> <script>
const PROGRAM_ID = new solanaWeb3.PublicKey("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE"); const PROGRAM_ID = new solanaWeb3.PublicKey("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
const RPC_URL = "https://api.devnet.solana.com"; const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed"); const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = { const SEEDS = {
managerAllowance: "shine_payments_v2_manager_allowance", managerAllowance: "shine_p_v2_manager_allow",
queues: "shine_payments_v2_queues", queues: "shine_payments_v2_queues",
ticketQ1: "shine_payments_v2_q1_ticket", ticketQ1: "shine_payments_v2_q1_ticket",
ticketQ2: "shine_payments_v2_q2_ticket", ticketQ2: "shine_payments_v2_q2_ticket",

View File

@ -5,11 +5,23 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Что ещё нужно до DAO — Shine Payments Devnet</title> <title>Что ещё нужно до DAO — Shine Payments Devnet</title>
<style> <style>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; line-height: 1.45; } :root {
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; max-width: 1800px; line-height: 1.45; background: var(--bg); color: var(--text); }
.topbar { margin-bottom: 12px; }
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); }
</style> </style>
</head> </head>
<body> <body>
<div class="topbar"><a class="back" href="./index.html">На главную</a></div>
<h1>Что ещё нужно до реального DAO</h1> <h1>Что ещё нужно до реального DAO</h1>
<div class="panel"> <div class="panel">

View File

@ -5,11 +5,23 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Сценарий тестирования — Shine Payments Devnet</title> <title>Сценарий тестирования — Shine Payments Devnet</title>
<style> <style>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; line-height: 1.45; } :root {
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; max-width: 1800px; line-height: 1.45; background: var(--bg); color: var(--text); }
.topbar { margin-bottom: 12px; }
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); }
</style> </style>
</head> </head>
<body> <body>
<div class="topbar"><a class="back" href="./index.html">На главную</a></div>
<h1>Сценарий тестирования Shine Payments (Devnet)</h1> <h1>Сценарий тестирования Shine Payments (Devnet)</h1>
<div class="panel"> <div class="panel">
@ -30,7 +42,7 @@
<ol> <ol>
<li>Кошелёк 1: DAO (выдаёт лимиты менеджерам).</li> <li>Кошелёк 1: DAO (выдаёт лимиты менеджерам).</li>
<li>Кошелёк 2: менеджер (создаёт билеты в очередь 1/2).</li> <li>Кошелёк 2: менеджер (создаёт билеты в очередь 1/2).</li>
<li>Кошелёк 3+: покупатели (создают обычные билеты через покупку).</li> <li>Кошельки 3+: покупатели (создают обычные билеты через покупку).</li>
<li>Любой кошелёк может запускать шаг выплат.</li> <li>Любой кошелёк может запускать шаг выплат.</li>
</ol> </ol>
</div> </div>
@ -45,7 +57,7 @@
</div> </div>
<div class="panel"> <div class="panel">
<p>Пока DAO-гovernance не подключена, ключевые действия DAO выполняются обычным тестовым кошельком. В production это заменяется голосованием DAO.</p> <p>Пока DAO-governance не подключена, ключевые действия DAO выполняются обычным тестовым кошельком. В production это заменяется голосованием DAO.</p>
</div> </div>
</body> </body>
</html> </html>

View File

@ -5,21 +5,42 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Отслеживание билета — Shine Payments Devnet</title> <title>Отслеживание билета — Shine Payments Devnet</title>
<style> <style>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; } :root {
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; } color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--ok: #55d48a;
--warn: #ffbf5e;
--err: #ff7d7d;
--btn: #273247;
--btn-hover: #32415c;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
.topbar { margin-bottom: 12px; }
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
.wrap { width: 100%; max-width: 1850px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); width: 100%; }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; } .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
input { padding: 8px; min-width: 240px; } input { padding: 9px 10px; min-width: 240px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
button { padding: 8px 12px; cursor: pointer; } button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
.muted { color: #666; } button:hover { background: var(--btn-hover); }
.ok { color: #0a7a3c; } .muted { color: var(--muted); }
.warn { color: #9f5f00; } .ok { color: var(--ok); }
.err { color: #b30000; white-space: pre-wrap; } .warn { color: var(--warn); }
.paid { color: #0a7a3c; font-weight: 700; } .err { color: var(--err); white-space: pre-wrap; }
.waiting { color: #666; } .paid { color: var(--ok); font-weight: 700; }
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; } .waiting { color: var(--muted); }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
</style> </style>
</head> </head>
<body> <body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">На главную</a></div>
<h1>Отслеживание билета (Devnet)</h1> <h1>Отслеживание билета (Devnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div> <div class="muted">Программа: <code id="programId"></code></div>
@ -49,15 +70,17 @@
</div> </div>
<div id="stepResult" class="muted"></div> <div id="stepResult" class="muted"></div>
</div> </div>
</div>
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script> <script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script> <script>
const PROGRAM_ID = new solanaWeb3.PublicKey("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE"); const PROGRAM_ID = new solanaWeb3.PublicKey("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
const RPC_URL = "https://api.devnet.solana.com"; const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed"); const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = { const SEEDS = {
config: "shine_payments_v2_config", config: "shine_payments_v2_config",
coef: "shine_payments_v2_coef_limit",
queues: "shine_payments_v2_queues", queues: "shine_payments_v2_queues",
ticketQ1: "shine_payments_v2_q1_ticket", ticketQ1: "shine_payments_v2_q1_ticket",
ticketQ2: "shine_payments_v2_q2_ticket", ticketQ2: "shine_payments_v2_q2_ticket",
@ -107,8 +130,15 @@
const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32; const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32; const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32; const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
return { version, dao, manager, inflow };
}
function parseCoef(data) {
let o = 0;
const version = data[o++];
const coefPpm = readU64(data, o); o += 8;
const limit = readU64(data, o); o += 8;
const reward = readU64(data, o); o += 8; const reward = readU64(data, o); o += 8;
return { version, dao, manager, inflow, reward }; return { version, coefPpm, limit, reward };
} }
function parseQueues(data) { function parseQueues(data) {
let o = 0; let o = 0;
@ -161,8 +191,9 @@
function deriveCorePdas() { function deriveCorePdas() {
const [configPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.config)], PROGRAM_ID); const [configPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.config)], PROGRAM_ID);
const [coefPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.coef)], PROGRAM_ID);
const [queuesPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID); const [queuesPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID);
return { configPda, queuesPda }; return { configPda, coefPda, queuesPda };
} }
function deriveTicketPda(queueId, index) { function deriveTicketPda(queueId, index) {
const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2; const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2;
@ -172,18 +203,20 @@
async function loadCoreState() { async function loadCoreState() {
const pdas = deriveCorePdas(); const pdas = deriveCorePdas();
const [cfgAi, qAi] = await Promise.all([ const [cfgAi, coefAi, qAi] = await Promise.all([
connection.getAccountInfo(pdas.configPda, "confirmed"), connection.getAccountInfo(pdas.configPda, "confirmed"),
connection.getAccountInfo(pdas.coefPda, "confirmed"),
connection.getAccountInfo(pdas.queuesPda, "confirmed"), connection.getAccountInfo(pdas.queuesPda, "confirmed"),
]); ]);
if (!cfgAi || !qAi) throw new Error("PDA не инициализированы. Запустите init на странице админки."); if (!cfgAi || !coefAi || !qAi) throw new Error("PDA не инициализированы. Запустите init на странице админки.");
const config = parseConfig(cfgAi.data); const config = parseConfig(cfgAi.data);
const coef = parseCoef(coefAi.data);
const queues = parseQueues(qAi.data); const queues = parseQueues(qAi.data);
const inflowAi = await connection.getAccountInfo(config.inflow, "confirmed"); const inflowAi = await connection.getAccountInfo(config.inflow, "confirmed");
if (!inflowAi) throw new Error("Inflow vault отсутствует"); if (!inflowAi) throw new Error("Inflow vault отсутствует");
const rentMin = await connection.getMinimumBalanceForRentExemption(inflowAi.data.length, "confirmed"); const rentMin = await connection.getMinimumBalanceForRentExemption(inflowAi.data.length, "confirmed");
const available = BigInt(Math.max(0, inflowAi.lamports - rentMin)); const available = BigInt(Math.max(0, inflowAi.lamports - rentMin));
cachedCore = { pdas, config, queues, inflowAi, available }; cachedCore = { pdas, config, coef, queues, inflowAi, available };
return cachedCore; return cachedCore;
} }
@ -216,12 +249,13 @@
return; return;
} }
const next = parseTicket(nextAi.data); const next = parseTicket(nextAi.data);
const need = next.payout * 2n + core.config.reward; const need = next.payout * 2n + core.coef.reward;
const missing = core.available >= need ? 0n : (need - core.available); const missing = core.available >= need ? 0n : (need - core.available);
el.innerHTML = ` el.innerHTML = `
<div>Следующий шаг выплат: <b>очередь ${queue}</b></div> <div>Следующий шаг выплат: <b>очередь ${queue}</b></div>
<div>Следующий тикет: <b>#${next.index.toString()}</b></div> <div>Следующий тикет: <b>#${next.index.toString()}</b></div>
<div>Выплата по тикету: <b>${lamportsToSolStr(next.payout)} SOL</b></div> <div>Выплата по тикету: <b>${lamportsToSolStr(next.payout)} SOL</b></div>
<div>Награда за шаг: <b>${lamportsToSolStr(core.coef.reward)} SOL</b></div>
<div>Нужно для шага (выплата + DAO + награда): <b>${lamportsToSolStr(need)} SOL</b></div> <div>Нужно для шага (выплата + DAO + награда): <b>${lamportsToSolStr(need)} SOL</b></div>
<div>Доступно в inflow vault: <b>${lamportsToSolStr(core.available)} SOL</b></div> <div>Доступно в inflow vault: <b>${lamportsToSolStr(core.available)} SOL</b></div>
<div>${missing === 0n <div>${missing === 0n
@ -234,29 +268,49 @@
} }
} }
function ticketAnalytics(core, t) {
const q1PendingCount = core.queues.q1Total - core.queues.q1Paid;
const q1PendingSum = core.queues.q1SumTotal - core.queues.q1SumPaid;
const initialCountBefore = t.index > 0n ? (t.index - 1n) : 0n;
const initialSumBefore = t.debtBefore;
let currentCountBefore = 0n;
let currentSumBefore = 0n;
if (!t.isPaid) {
if (t.queueId === 1) {
currentCountBefore = initialCountBefore > core.queues.q1Paid ? (initialCountBefore - core.queues.q1Paid) : 0n;
currentSumBefore = initialSumBefore > core.queues.q1SumPaid ? (initialSumBefore - core.queues.q1SumPaid) : 0n;
} else {
const ownCountRemain = initialCountBefore > core.queues.q2Paid ? (initialCountBefore - core.queues.q2Paid) : 0n;
const ownSumRemain = initialSumBefore > core.queues.q2SumPaid ? (initialSumBefore - core.queues.q2SumPaid) : 0n;
currentCountBefore = q1PendingCount + ownCountRemain;
currentSumBefore = q1PendingSum + ownSumRemain;
}
}
return { initialCountBefore, initialSumBefore, currentCountBefore, currentSumBefore };
}
function renderTicketCard(core, pda, t) { function renderTicketCard(core, pda, t) {
const q1Pending = core.queues.q1Total - core.queues.q1Paid;
const nextQ1 = core.queues.q1Paid + 1n; const nextQ1 = core.queues.q1Paid + 1n;
const nextQ2 = core.queues.q2Paid + 1n; const nextQ2 = core.queues.q2Paid + 1n;
const isCurrentQ1 = !t.isPaid && t.queueId === 1 && t.index === nextQ1; 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 isCurrentQ2 = !t.isPaid && t.queueId === 2 && q1Pending === 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; const missingInsideCurrent = (isCurrentQ1 || isCurrentQ2) && core.available < t.payout ? (t.payout - core.available) : 0n;
const a = ticketAnalytics(core, t);
return ` return `
<div class="panel"> <div class="panel">
<div>Тикет #<b>${t.index.toString()}</b> (очередь <b>${t.queueId}</b>) (<span class="${t.isPaid ? "paid" : "waiting"}">${t.isPaid ? "выплачен" : "ожидание"}</span>)</div> <div>Тикет #<b>${t.index.toString()}</b> (очередь <b>${t.queueId}</b>) (<span class="${t.isPaid ? "paid" : "waiting"}">${t.isPaid ? "выплачен" : "ожидание"}</span>)</div>
<div>PDA: <code>${pda.toBase58()}</code></div> <div>PDA: <code>${pda.toBase58()}</code></div>
<div>Получатель: <code>${t.recipient.toBase58()}</code></div> <div>Получатель: <code>${t.recipient.toBase58()}</code></div>
<div>Сумма выплаты: <b>${lamportsToSolStr(t.payout)} SOL</b></div> <div>Сумма выплаты: <b>${lamportsToSolStr(t.payout)} SOL</b></div>
<div>Билетов перед ним сейчас: <b>${inFront.toString()}</b></div> <div>Изначально перед билетом было: <b>${a.initialCountBefore.toString()}</b> билетов на <b>${lamportsToSolStr(a.initialSumBefore)} SOL</b></div>
<div>До его выплаты по сумме в предыдущих билетах осталось: <b>${lamportsToSolStr(remainingToThis)} SOL</b></div> <div>Сейчас перед билетом осталось: <b>${a.currentCountBefore.toString()}</b> билетов на <b>${lamportsToSolStr(a.currentSumBefore)} SOL</b></div>
${t.queueId === 2 && !t.isPaid ? `<div class="warn">Для 2-й очереди оценка не окончательная: 1-я очередь может увеличиваться.</div>` : ``} ${t.queueId === 2 && !t.isPaid ? `<div class="warn">Для 2-й очереди оценка не окончательная: 1-я очередь может увеличиваться.</div>` : ``}
${(isCurrentQ1 || isCurrentQ2) ? `<div class="warn">Это текущий билет к выплате.</div>` : ``} ${(isCurrentQ1 || isCurrentQ2) ? `<div class="warn">Это текущий билет к выплате.</div>` : ``}
${((isCurrentQ1 || isCurrentQ2) && missingInsideCurrent > 0n) ${((isCurrentQ1 || isCurrentQ2) && missingInsideCurrent > 0n)
? `<div class="warn">Для выплаты именно этого билета внутри его суммы не хватает: <b>${lamportsToSolStr(missingInsideCurrent)} SOL</b>.</div>` ? `<div class="warn">Чтобы выплатить именно этот билет, внутри его суммы пока не хватает: <b>${lamportsToSolStr(missingInsideCurrent)} SOL</b>.</div>`
: ``} : ``}
</div> </div>
`; `;
@ -337,6 +391,7 @@
{ pubkey: walletPubkey, isSigner: true, isWritable: true }, { pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: core.pdas.configPda, isSigner: false, isWritable: true }, { pubkey: core.pdas.configPda, isSigner: false, isWritable: true },
{ pubkey: core.pdas.queuesPda, isSigner: false, isWritable: true }, { pubkey: core.pdas.queuesPda, isSigner: false, isWritable: true },
{ pubkey: core.pdas.coefPda, isSigner: false, isWritable: true },
{ pubkey: core.config.inflow, isSigner: false, isWritable: true }, { pubkey: core.config.inflow, isSigner: false, isWritable: true },
{ pubkey: nextTicketPda, isSigner: false, isWritable: true }, { pubkey: nextTicketPda, isSigner: false, isWritable: true },
{ pubkey: recipient, isSigner: false, isWritable: true }, { pubkey: recipient, isSigner: false, isWritable: true },

View File

@ -1,13 +1,11 @@
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
pub mod users;
pub mod settings; pub mod settings;
pub mod users;
use users::*; use users::*;
declare_id!("FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm");
declare_id!("8Z3HQizFRhyVu5cNBwWNBXZHTpu89VMkn7Wuk1oCtkeJ");
#[program] #[program]
pub mod shine { pub mod shine {