Обновить логику Shine Payments, UI и задеплоить с новыми Program ID
This commit is contained in:
parent
7e8ca9157b
commit
ea25b908c1
@ -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"
|
||||||
|
|||||||
@ -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).
|
||||||
|
|
||||||
|
|||||||
@ -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))?;
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -5,66 +5,92 @@
|
|||||||
<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>
|
||||||
<h1>Техническая страница (Devnet)</h1>
|
<div class="wrap">
|
||||||
<div class="muted">Программа: <code id="programId"></code></div>
|
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
|
||||||
|
<h1>Техническая страница (Devnet)</h1>
|
||||||
|
<div class="muted">Программа: <code id="programId"></code></div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="connectBtn">Подключить кошелек</button>
|
<button id="connectBtn">Подключить кошелек</button>
|
||||||
<button id="refreshBtn">Обновить всё</button>
|
<button id="refreshBtn">Обновить всё</button>
|
||||||
<button id="initBtn">Init (один раз)</button>
|
<button id="initBtn">Init (один раз)</button>
|
||||||
|
</div>
|
||||||
|
<div id="walletInfo" class="muted"></div>
|
||||||
|
<div id="initResult" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="walletInfo" class="muted"></div>
|
|
||||||
<div id="initResult" class="muted"></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>
|
||||||
<button id="updateCoefBtn">Обновить</button>
|
<label>Награда шага (SOL, max 0.01): <input id="rewardInput" value="0.008" /></label>
|
||||||
|
<button id="updateCoefBtn">Обновить</button>
|
||||||
|
</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>
|
</div>
|
||||||
<div id="updateResult" class="muted"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Адреса и агрегаты</h3>
|
<h3>Адреса и агрегаты</h3>
|
||||||
<div id="balances" class="muted">Загрузка...</div>
|
<div id="balances" class="muted">Загрузка...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Очередь 1 (все билеты)</h3>
|
<h3>Очередь 1 (все билеты)</h3>
|
||||||
<div class="row"><button id="loadQ1Btn">Показать очередь 1</button></div>
|
<div class="row"><button id="loadQ1Btn">Показать очередь 1</button></div>
|
||||||
<div id="queue1Table" class="muted"></div>
|
<div id="queue1Table" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Очередь 2 (все билеты)</h3>
|
<h3>Очередь 2 (все билеты)</h3>
|
||||||
<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>
|
||||||
|
|||||||
@ -5,52 +5,74 @@
|
|||||||
<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>
|
||||||
<h1>Покупка билета (Devnet)</h1>
|
<div class="wrap">
|
||||||
<div class="muted">Программа: <code id="programId"></code></div>
|
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
|
||||||
|
<h1>Покупка билета (Devnet)</h1>
|
||||||
|
<div class="muted">Программа: <code id="programId"></code></div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="connectBtn">Подключить кошелек</button>
|
<button id="connectBtn">Подключить кошелек</button>
|
||||||
<button id="refreshBtn">Обновить состояние</button>
|
<button id="refreshBtn">Обновить состояние</button>
|
||||||
|
</div>
|
||||||
|
<div id="walletInfo" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="walletInfo" class="muted"></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>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Покупка билета в 1-й очереди</h3>
|
<h3>Покупка билета в 1-й очереди</h3>
|
||||||
<div class="muted">Покупка создает билет только в 1-й очереди. Билеты 2-й очереди добавляются менеджерами с правами.</div>
|
<div class="muted">Покупка создает билет только в 1-й очереди. Билеты 2-й очереди добавляются менеджерами с правами.</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Сумма (SOL): <input id="amountSol" value="0.1" /></label>
|
<label>Сумма (SOL): <input id="amountSol" value="0.1" /></label>
|
||||||
<label>Кошелек для выплаты: <input id="recipient" placeholder="по умолчанию ваш кошелек" /></label>
|
<label>Кошелек для выплаты: <input id="recipient" placeholder="по умолчанию ваш кошелек" /></label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="buyBtn">Купить билет</button>
|
||||||
|
</div>
|
||||||
|
<div id="buyResult" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<button id="buyBtn">Купить билет</button>
|
|
||||||
</div>
|
|
||||||
<div id="buyResult" class="muted"></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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,67 +5,89 @@
|
|||||||
<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>
|
||||||
<h1>DAO: права менеджеров (Devnet)</h1>
|
<div class="wrap">
|
||||||
<div class="muted">Программа: <code id="programId"></code></div>
|
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
|
||||||
|
<h1>DAO: права менеджеров (Devnet)</h1>
|
||||||
|
<div class="muted">Программа: <code id="programId"></code></div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="warn">
|
<div class="warn">
|
||||||
Пока реального DAO-голосования нет: роль DAO выполняет обычный кошелек.<br />
|
Пока реального DAO-голосования нет: роль DAO выполняет обычный кошелек.<br />
|
||||||
Позже это заменяется на вызов из DAO-казначейства/голосования.
|
Позже это заменяется на вызов из DAO-казначейства/голосования.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="connectBtn">Подключить кошелек</button>
|
<button id="connectBtn">Подключить кошелек</button>
|
||||||
<button id="refreshBtn">Обновить</button>
|
<button id="refreshBtn">Обновить</button>
|
||||||
|
</div>
|
||||||
|
<div id="walletInfo" class="muted"></div>
|
||||||
|
<div id="daoInfo" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="walletInfo" class="muted"></div>
|
|
||||||
<div id="daoInfo" class="muted"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Выдать/добавить лимиты менеджеру</h3>
|
<h3>Выдать/добавить лимиты менеджеру</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Кошелек менеджера: <input id="managerWallet" placeholder="Base58" /></label>
|
<label>Кошелек менеджера: <input id="managerWallet" placeholder="Base58" /></label>
|
||||||
<label>Добавить лимит Q1 (SOL): <input id="addQ1" value="1" /></label>
|
<label>Добавить лимит Q1 (SOL): <input id="addQ1" value="1" /></label>
|
||||||
<label>Добавить лимит Q2 (SOL): <input id="addQ2" value="0.5" /></label>
|
<label>Добавить лимит Q2 (SOL): <input id="addQ2" value="0.5" /></label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="grantBtn">Выдать лимиты</button>
|
||||||
|
</div>
|
||||||
|
<div id="grantResult" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<button id="grantBtn">Выдать лимиты</button>
|
|
||||||
</div>
|
|
||||||
<div id="grantResult" class="muted"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Текущие лимиты менеджера</h3>
|
<h3>Текущие лимиты менеджера</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="loadManagerBtn">Показать лимиты</button>
|
<button id="loadManagerBtn">Показать лимиты</button>
|
||||||
|
</div>
|
||||||
|
<div id="managerState" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="managerState" class="muted"></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());
|
||||||
|
|||||||
@ -5,65 +5,81 @@
|
|||||||
<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>
|
||||||
<h1>Shine Payments Devnet</h1>
|
<div class="wrap">
|
||||||
<div class="panel">
|
<h1>Shine Payments Devnet</h1>
|
||||||
<div>Выберите страницу:</div>
|
<div class="panel">
|
||||||
|
<div>Выберите страницу:</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="card" href="./buy_ticket.html">
|
||||||
|
<h3>Покупка билета</h3>
|
||||||
|
<div class="muted">Создание нового билета в 1-й очереди.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="./track_ticket.html">
|
||||||
|
<h3>Отслеживание билета</h3>
|
||||||
|
<div class="muted">Проверка позиции, статуса и шаг выплат по 1-й/2-й очереди.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="./admin_tools.html">
|
||||||
|
<h3>Тех. инструменты</h3>
|
||||||
|
<div class="muted">Init, просмотр всех билетов в обеих очередях, коэффициент/лимит/награда, агрегаты.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="./dao_tools.html">
|
||||||
|
<h3>DAO-права менеджеров</h3>
|
||||||
|
<div class="muted">Выдача лимитов менеджерам на добавление билетов в очередь 1/2.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="./manager_tools.html">
|
||||||
|
<h3>Инструменты менеджера</h3>
|
||||||
|
<div class="muted">Показ лимитов менеджера и создание билетов в очередь 1/2.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="./logic_overview.html">
|
||||||
|
<h3>Логика работы</h3>
|
||||||
|
<div class="muted">Кратко: как работают очереди, выплаты, лимиты и тестовый режим.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="./roadmap_dao.html">
|
||||||
|
<h3>Что ещё нужно до реального DAO</h3>
|
||||||
|
<div class="muted">Ограничения тестовой версии и шаги к production.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="card" href="./test_plan.html">
|
||||||
|
<h3>Сценарий тестирования</h3>
|
||||||
|
<div class="muted">Пошаговая методика тестов и возврата средств после теста.</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="card" href="./buy_ticket.html">
|
|
||||||
<h3>Покупка билета</h3>
|
|
||||||
<div class="muted">Создание нового билета в 1-й очереди.</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="card" href="./track_ticket.html">
|
|
||||||
<h3>Отслеживание билета</h3>
|
|
||||||
<div class="muted">Проверка позиции, статуса и шаг выплат по 1-й/2-й очереди.</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="card" href="./admin_tools.html">
|
|
||||||
<h3>Тех. инструменты</h3>
|
|
||||||
<div class="muted">Init, просмотр всех билетов в обеих очередях, коэффициент/лимит, агрегаты.</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="card" href="./dao_tools.html">
|
|
||||||
<h3>DAO-права менеджеров</h3>
|
|
||||||
<div class="muted">Выдача лимитов менеджерам на добавление билетов в очередь 1/2.</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="card" href="./manager_tools.html">
|
|
||||||
<h3>Инструменты менеджера</h3>
|
|
||||||
<div class="muted">Показ лимитов менеджера и создание билетов в очередь 1/2.</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="card" href="./logic_overview.html">
|
|
||||||
<h3>Логика работы</h3>
|
|
||||||
<div class="muted">Кратко: как работают очереди, выплаты, лимиты и тестовый режим.</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="card" href="./roadmap_dao.html">
|
|
||||||
<h3>Что ещё нужно до реального DAO</h3>
|
|
||||||
<div class="muted">Ограничения тестовой версии и шаги к production.</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="card" href="./test_plan.html">
|
|
||||||
<h3>Сценарий тестирования</h3>
|
|
||||||
<div class="muted">Пошаговая методика тестов и возврата средств после теста.</div>
|
|
||||||
</a>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -5,60 +5,82 @@
|
|||||||
<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>
|
||||||
<h1>Менеджер: создание билетов (Devnet)</h1>
|
<div class="wrap">
|
||||||
<div class="muted">Программа: <code id="programId"></code></div>
|
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
|
||||||
|
<h1>Менеджер: создание билетов (Devnet)</h1>
|
||||||
|
<div class="muted">Программа: <code id="programId"></code></div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="connectBtn">Подключить кошелек менеджера</button>
|
<button id="connectBtn">Подключить кошелек менеджера</button>
|
||||||
<button id="refreshBtn">Обновить</button>
|
<button id="refreshBtn">Обновить</button>
|
||||||
|
</div>
|
||||||
|
<div id="walletInfo" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="walletInfo" class="muted"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Лимиты менеджера</h3>
|
<h3>Лимиты менеджера</h3>
|
||||||
<div id="limitsInfo" class="muted">Загрузка...</div>
|
<div id="limitsInfo" class="muted">Загрузка...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Создать билет менеджером</h3>
|
<h3>Создать билет менеджером</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Очередь:
|
<label>Очередь:
|
||||||
<select id="queueId">
|
<select id="queueId">
|
||||||
<option value="1">Очередь 1</option>
|
<option value="1">Очередь 1</option>
|
||||||
<option value="2">Очередь 2</option>
|
<option value="2">Очередь 2</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Получатель: <input id="recipient" placeholder="Base58 адрес" /></label>
|
<label>Получатель: <input id="recipient" placeholder="Base58 адрес" /></label>
|
||||||
<label>Сумма выплаты (SOL): <input id="payoutSol" value="0.5" /></label>
|
<label>Сумма выплаты (SOL): <input id="payoutSol" value="0.5" /></label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="createBtn">Создать билет</button>
|
||||||
|
</div>
|
||||||
|
<div id="createResult" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<button id="createBtn">Создать билет</button>
|
|
||||||
</div>
|
|
||||||
<div id="createResult" class="muted"></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",
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -5,59 +5,82 @@
|
|||||||
<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>
|
||||||
<h1>Отслеживание билета (Devnet)</h1>
|
<div class="wrap">
|
||||||
<div class="muted">Программа: <code id="programId"></code></div>
|
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
|
||||||
|
<h1>Отслеживание билета (Devnet)</h1>
|
||||||
|
<div class="muted">Программа: <code id="programId"></code></div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="connectBtn">Подключить кошелек</button>
|
<button id="connectBtn">Подключить кошелек</button>
|
||||||
<button id="refreshBtn">Обновить</button>
|
<button id="refreshBtn">Обновить</button>
|
||||||
|
</div>
|
||||||
|
<div id="walletInfo" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="walletInfo" class="muted"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Поиск билетов</h3>
|
<h3>Поиск билетов</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Номер билета: <input id="ticketIndex" placeholder="например 1" /></label>
|
<label>Номер билета: <input id="ticketIndex" placeholder="например 1" /></label>
|
||||||
<label>или кошелек получателя: <input id="recipientWallet" placeholder="Base58 адрес" /></label>
|
<label>или кошелек получателя: <input id="recipientWallet" placeholder="Base58 адрес" /></label>
|
||||||
<button id="findBtn">Найти</button>
|
<button id="findBtn">Найти</button>
|
||||||
|
</div>
|
||||||
|
<div id="ticketResult" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="ticketResult" class="muted"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Состояние шага выплат</h3>
|
<h3>Состояние шага выплат</h3>
|
||||||
<div id="payoutInfo" class="muted">Загрузка...</div>
|
<div id="payoutInfo" class="muted">Загрузка...</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="stepBtn">Сделать шаг выплат</button>
|
<button id="stepBtn">Сделать шаг выплат</button>
|
||||||
|
</div>
|
||||||
|
<div id="stepResult" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="stepResult" class="muted"></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 },
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user