Вынести экономику shine_users в отдельный PDA и добавить управление в UI

This commit is contained in:
AidarKC 2026-05-16 18:18:07 +03:00
parent deef20c517
commit 7d2f50b6e1
5 changed files with 291 additions and 8 deletions

View File

@ -70,6 +70,20 @@
<div id="updateResult" class="muted"></div> <div id="updateResult" class="muted"></div>
</div> </div>
<div class="panel">
<h3>Shine Users: экономические параметры</h3>
<div class="muted">Право изменения: <code id="usersDaoAllowed">загрузка...</code></div>
<div id="usersEconomyState" class="muted">Загрузка...</div>
<div class="row">
<label>Комиссия регистрации (SOL): <input id="usersRegFeeInput" value="0.01" /></label>
<label>Цена шага лимита (SOL): <input id="usersLimitStepFeeInput" value="0.0001" /></label>
<label>Стартовый бонус лимита: <input id="usersBonusInput" value="100000" /></label>
<button id="usersUpdateBtn">Обновить</button>
<button id="usersInitBtn">Init Users Economy</button>
</div>
<div id="usersUpdateResult" 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>
@ -91,6 +105,7 @@
<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("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR"); const PROGRAM_ID = new solanaWeb3.PublicKey("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
const USERS_PROGRAM_ID = new solanaWeb3.PublicKey("FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm");
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 = {
@ -101,6 +116,9 @@
ticketQ1: "shine_payments_v3_q1_ticket", ticketQ1: "shine_payments_v3_q1_ticket",
ticketQ2: "shine_payments_v3_q2_ticket", ticketQ2: "shine_payments_v3_q2_ticket",
}; };
const USERS_SEEDS = {
economyConfig: "shine_users_v1_economy_config",
};
const MAX_REWARD_LAMPORTS = 10_000_000n; const MAX_REWARD_LAMPORTS = 10_000_000n;
let walletPubkey = null; let walletPubkey = null;
let cache = null; let cache = null;
@ -153,6 +171,10 @@
const s = String(msg || "").toLowerCase(); const s = String(msg || "").toLowerCase();
return s.includes("unauthorizeddao") || s.includes("0x1775"); return s.includes("unauthorizeddao") || s.includes("0x1775");
} }
function isUsersDaoUnauthorized(msg) {
const s = String(msg || "").toLowerCase();
return s.includes("invalidsigner") || s.includes("0x3ed");
}
function parseConfig(data) { function parseConfig(data) {
let o = 0; let o = 0;
@ -194,6 +216,14 @@
const debtBefore = readU64(data, o); o += 8; const debtBefore = readU64(data, o); o += 8;
return { version, queueId, index, isPaid, recipient, payout, debtBefore }; return { version, queueId, index, isPaid, recipient, payout, debtBefore };
} }
function parseUsersEconomyConfig(data) {
let o = 0;
const version = data[o++];
const registrationFeeLamports = readU64(data, o); o += 8;
const lamportsPerLimitStep = readU64(data, o); o += 8;
const startBonusLimit = readU64(data, o); o += 8;
return { version, registrationFeeLamports, lamportsPerLimitStep, startBonusLimit };
}
function getProvider() { function getProvider() {
if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден"); if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден");
@ -226,6 +256,13 @@
const [inflowPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.inflow)], PROGRAM_ID); const [inflowPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.inflow)], PROGRAM_ID);
return { configPda, coefPda, queuesPda, inflowPda }; return { configPda, coefPda, queuesPda, inflowPda };
} }
function deriveUsersPdas() {
const [usersEconomyConfigPda] = solanaWeb3.PublicKey.findProgramAddressSync(
[utf8(USERS_SEEDS.economyConfig)],
USERS_PROGRAM_ID
);
return { usersEconomyConfigPda };
}
function ticketPda(queueId, index) { function ticketPda(queueId, index) {
const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2; const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2;
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(seed), u64ToBytes(index)], PROGRAM_ID); const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(seed), u64ToBytes(index)], PROGRAM_ID);
@ -292,6 +329,32 @@
} }
} }
async function refreshUsersEconomy() {
const out = document.getElementById("usersEconomyState");
try {
const usersPdas = deriveUsersPdas();
const ai = await connection.getAccountInfo(usersPdas.usersEconomyConfigPda, "confirmed");
document.getElementById("usersDaoAllowed").textContent = "FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P";
if (!ai) {
out.innerHTML = `<span class="warn">PDA Users Economy еще не инициализирован.</span><div>PDA: <code>${usersPdas.usersEconomyConfigPda.toBase58()}</code></div>`;
return;
}
const c = parseUsersEconomyConfig(ai.data);
document.getElementById("usersRegFeeInput").value = lamportsToSolStr(c.registrationFeeLamports);
document.getElementById("usersLimitStepFeeInput").value = lamportsToSolStr(c.lamportsPerLimitStep);
document.getElementById("usersBonusInput").value = c.startBonusLimit.toString();
out.innerHTML = `
<div>Users program: <code>${USERS_PROGRAM_ID.toBase58()}</code></div>
<div>Economy config PDA: <code>${usersPdas.usersEconomyConfigPda.toBase58()}</code></div>
<div>registration_fee_lamports: <b>${c.registrationFeeLamports.toString()}</b> (~${lamportsToSolStr(c.registrationFeeLamports)} SOL)</div>
<div>lamports_per_limit_step: <b>${c.lamportsPerLimitStep.toString()}</b> (~${lamportsToSolStr(c.lamportsPerLimitStep)} SOL)</div>
<div>start_bonus_limit: <b>${c.startBonusLimit.toString()}</b></div>
`;
} catch (e) {
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
async function runInit() { async function runInit() {
const out = document.getElementById("initResult"); const out = document.getElementById("initResult");
out.textContent = ""; out.textContent = "";
@ -358,6 +421,68 @@
} }
} }
async function initUsersEconomy() {
const out = document.getElementById("usersUpdateResult");
out.textContent = "";
try {
const provider = getProvider();
if (!walletPubkey) await connectWallet();
else if (!provider.isConnected) await provider.connect();
const usersPdas = deriveUsersPdas();
const disc = await ixDiscriminator("init_users_economy_config");
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: usersPdas.usersEconomyConfigPda, isSigner: false, isWritable: true },
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: USERS_PROGRAM_ID, keys, data: disc });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Users Economy init выполнен. Tx: <code>${sig}</code></span>`;
await refreshUsersEconomy();
} catch (e) {
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
async function updateUsersEconomy() {
const out = document.getElementById("usersUpdateResult");
out.textContent = "";
try {
const provider = getProvider();
if (!walletPubkey) await connectWallet();
else if (!provider.isConnected) await provider.connect();
const usersPdas = deriveUsersPdas();
const registrationFeeLamports = solToLamports(document.getElementById("usersRegFeeInput").value.trim());
const lamportsPerLimitStep = solToLamports(document.getElementById("usersLimitStepFeeInput").value.trim());
const startBonusLimit = BigInt(document.getElementById("usersBonusInput").value.trim());
if (startBonusLimit < 0n) throw new Error("Стартовый бонус не может быть отрицательным");
const disc = await ixDiscriminator("update_users_economy_config");
const data = concat(
disc,
u64ToBytes(registrationFeeLamports),
u64ToBytes(lamportsPerLimitStep),
u64ToBytes(startBonusLimit)
);
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: usersPdas.usersEconomyConfigPda, isSigner: false, isWritable: true },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: USERS_PROGRAM_ID, keys, data });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Users Economy обновлен. Tx: <code>${sig}</code></span>`;
await refreshUsersEconomy();
} catch (e) {
const raw = String(e.message || e);
if (isUsersDaoUnauthorized(raw)) {
const dao = document.getElementById("usersDaoAllowed").textContent;
out.innerHTML = `<span class="warn">Изменение доступно только DAO-кошельку: <code>${dao}</code>.</span>`;
return;
}
out.innerHTML = `<span class="err">${raw}</span>`;
}
}
function currentDebtBeforeTicket(ticket, queues) { function currentDebtBeforeTicket(ticket, queues) {
if (ticket.isPaid) return 0n; if (ticket.isPaid) return 0n;
const paidSum = ticket.queueId === 1 ? queues.q1SumPaid : queues.q2SumPaid; const paidSum = ticket.queueId === 1 ? queues.q1SumPaid : queues.q2SumPaid;
@ -421,12 +546,15 @@
async function refreshAll() { async function refreshAll() {
await refreshBalances(); await refreshBalances();
await refreshUsersEconomy();
} }
document.getElementById("connectBtn").addEventListener("click", connectWallet); document.getElementById("connectBtn").addEventListener("click", connectWallet);
document.getElementById("refreshBtn").addEventListener("click", refreshAll); document.getElementById("refreshBtn").addEventListener("click", refreshAll);
document.getElementById("initBtn").addEventListener("click", runInit); document.getElementById("initBtn").addEventListener("click", runInit);
document.getElementById("updateCoefBtn").addEventListener("click", updateCoefLimit); document.getElementById("updateCoefBtn").addEventListener("click", updateCoefLimit);
document.getElementById("usersInitBtn").addEventListener("click", initUsersEconomy);
document.getElementById("usersUpdateBtn").addEventListener("click", updateUsersEconomy);
document.getElementById("loadQ1Btn").addEventListener("click", () => showQueue(1)); document.getElementById("loadQ1Btn").addEventListener("click", () => showQueue(1));
document.getElementById("loadQ2Btn").addEventListener("click", () => showQueue(2)); document.getElementById("loadQ2Btn").addEventListener("click", () => showQueue(2));
refreshAll(); refreshAll();

View File

@ -11,6 +11,17 @@ declare_id!("FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm");
pub mod shine { pub mod shine {
use super::*; use super::*;
pub fn init_users_economy_config(ctx: Context<InitUsersEconomyConfig>) -> Result<()> {
users::init_users_economy_config(ctx)
}
pub fn update_users_economy_config(
ctx: Context<UpdateUsersEconomyConfig>,
args: UpdateUsersEconomyConfigArgs,
) -> Result<()> {
users::update_users_economy_config(ctx, args)
}
pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> { pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> {
users::create_user_pda(ctx, args) users::create_user_pda(ctx, args)
} }

View File

@ -1,12 +1,16 @@
pub const USER_PDA_SEED_PREFIX: &str = "login="; pub const USER_PDA_SEED_PREFIX: &str = "login=";
pub const USERS_ECONOMY_CONFIG_SEED: &[u8] = b"shine_users_v1_economy_config";
// Увеличили размер PDA, чтобы оставить запас под будущие расширения формата // Увеличили размер PDA, чтобы оставить запас под будущие расширения формата
// (в частности, сценарии ротации root key с дополнительной подписью старого ключа). // (в частности, сценарии ротации root key с дополнительной подписью старого ключа).
pub const USER_PDA_SPACE: usize = 768; pub const USER_PDA_SPACE: usize = 768;
pub const USERS_ECONOMY_CONFIG_SPACE: usize = 8 + 96;
pub const DAO_AUTHORITY: &str = "FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P";
pub const REGISTRATION_FEE_RECEIVER: &str = "9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb"; pub const REGISTRATION_FEE_RECEIVER: &str = "9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb";
pub const REGISTRATION_FEE_LAMPORTS: u64 = 10_000_000; // 0.01 SOL pub const START_REGISTRATION_FEE_LAMPORTS: u64 = 10_000_000; // 0.01 SOL
pub const LIMIT_STEP: u64 = 10_000; pub const LIMIT_STEP: u64 = 10_000;
pub const LAMPORTS_PER_LIMIT_STEP: u64 = 100_000; // 0.0001 SOL за 10_000 лимита pub const START_LAMPORTS_PER_LIMIT_STEP: u64 = 100_000; // 0.0001 SOL за 10_000 лимита
pub const START_BONUS_LIMIT: u64 = 100_000; pub const START_BONUS_LIMIT: u64 = 100_000;

View File

@ -75,6 +75,14 @@ pub struct UserRecord {
pub signature: [u8; 64], pub signature: [u8; 64],
} }
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct UsersEconomyConfigState {
pub version: u8,
pub registration_fee_lamports: u64,
pub lamports_per_limit_step: u64,
pub start_bonus_limit: u64,
}
#[derive(Accounts)] #[derive(Accounts)]
pub struct CreateUserPda<'info> { pub struct CreateUserPda<'info> {
/// CHECK: подписант транзакции, валидируется Anchor как signer и mut. /// CHECK: подписант транзакции, валидируется Anchor как signer и mut.
@ -89,6 +97,8 @@ pub struct CreateUserPda<'info> {
pub fee_receiver: AccountInfo<'info>, pub fee_receiver: AccountInfo<'info>,
/// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции. /// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции.
pub instructions: AccountInfo<'info>, pub instructions: AccountInfo<'info>,
/// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную.
pub users_economy_config_pda: AccountInfo<'info>,
} }
#[derive(Accounts)] #[derive(Accounts)]
@ -105,6 +115,101 @@ pub struct UpdateUserPda<'info> {
pub fee_receiver: AccountInfo<'info>, pub fee_receiver: AccountInfo<'info>,
/// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции. /// CHECK: sysvar инструкций, нужен для проверки встроенной Ed25519Program инструкции.
pub instructions: AccountInfo<'info>, pub instructions: AccountInfo<'info>,
/// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную.
pub users_economy_config_pda: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct InitUsersEconomyConfig<'info> {
/// CHECK: подписант и плательщик, валидируется Anchor как signer и mut.
#[account(mut, signer)]
pub signer: AccountInfo<'info>,
/// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную.
#[account(mut)]
pub users_economy_config_pda: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateUsersEconomyConfig<'info> {
/// CHECK: подписант (должен быть DAO authority из settings).
#[account(mut, signer)]
pub signer: AccountInfo<'info>,
/// CHECK: PDA с экономическими настройками пользователей, адрес проверяется вручную.
#[account(mut)]
pub users_economy_config_pda: AccountInfo<'info>,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct UpdateUsersEconomyConfigArgs {
pub registration_fee_lamports: u64,
pub lamports_per_limit_step: u64,
pub start_bonus_limit: u64,
}
pub fn init_users_economy_config(ctx: Context<InitUsersEconomyConfig>) -> Result<()> {
let (expected_pda, bump) = find_users_economy_config_pda(ctx.program_id);
require_keys_eq!(
expected_pda,
ctx.accounts.users_economy_config_pda.key(),
ErrCode::InvalidPdaAddress
);
require!(
ctx.accounts.users_economy_config_pda.owner == &Pubkey::default(),
ErrCode::SystemAlreadyInitialized
);
let state = UsersEconomyConfigState {
version: 1,
registration_fee_lamports: settings::START_REGISTRATION_FEE_LAMPORTS,
lamports_per_limit_step: settings::START_LAMPORTS_PER_LIMIT_STEP,
start_bonus_limit: settings::START_BONUS_LIMIT,
};
let bytes = state
.try_to_vec()
.map_err(|_| error!(ErrCode::DeserializationError))?;
let seeds: &[&[u8]] = &[settings::USERS_ECONOMY_CONFIG_SEED, &[bump]];
create_pda(
&ctx.accounts.users_economy_config_pda,
&ctx.accounts.signer,
&ctx.accounts.system_program.to_account_info(),
ctx.program_id,
seeds,
settings::USERS_ECONOMY_CONFIG_SPACE as u64,
)?;
write_to_pda(&ctx.accounts.users_economy_config_pda, &bytes)?;
Ok(())
}
pub fn update_users_economy_config(
ctx: Context<UpdateUsersEconomyConfig>,
args: UpdateUsersEconomyConfigArgs,
) -> Result<()> {
let dao_authority =
Pubkey::from_str(settings::DAO_AUTHORITY).map_err(|_| error!(ErrCode::InvalidSigner))?;
require_keys_eq!(dao_authority, ctx.accounts.signer.key(), ErrCode::InvalidSigner);
let (expected_pda, _) = find_users_economy_config_pda(ctx.program_id);
require_keys_eq!(
expected_pda,
ctx.accounts.users_economy_config_pda.key(),
ErrCode::InvalidPdaAddress
);
require!(
ctx.accounts.users_economy_config_pda.owner == ctx.program_id,
ErrCode::InvalidPdaAddress
);
require!(args.lamports_per_limit_step > 0, ErrCode::InvalidRecordData);
let mut state = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?;
state.registration_fee_lamports = args.registration_fee_lamports;
state.lamports_per_limit_step = args.lamports_per_limit_step;
state.start_bonus_limit = args.start_bonus_limit;
let bytes = state
.try_to_vec()
.map_err(|_| error!(ErrCode::DeserializationError))?;
write_to_pda(&ctx.accounts.users_economy_config_pda, &bytes)?;
Ok(())
} }
pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> { pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) -> Result<()> {
@ -115,6 +220,7 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
args.additional_limit % settings::LIMIT_STEP == 0, args.additional_limit % settings::LIMIT_STEP == 0,
ErrCode::InvalidLimitIncrement ErrCode::InvalidLimitIncrement
); );
let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?;
let (expected_pda, bump) = find_user_pda(ctx.program_id, &args.login); let (expected_pda, bump) = find_user_pda(ctx.program_id, &args.login);
require_keys_eq!( require_keys_eq!(
@ -127,7 +233,8 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
ErrCode::UserAlreadyExists ErrCode::UserAlreadyExists
); );
let start_balance = settings::START_BONUS_LIMIT let start_balance = economy
.start_bonus_limit
.checked_add(args.additional_limit) .checked_add(args.additional_limit)
.ok_or(error!(ErrCode::MathOverflow))?; .ok_or(error!(ErrCode::MathOverflow))?;
@ -183,8 +290,9 @@ pub fn create_user_pda(ctx: Context<CreateUserPda>, args: CreateUserPdaArgs) ->
)?; )?;
write_to_pda(&ctx.accounts.user_pda, &padded)?; write_to_pda(&ctx.accounts.user_pda, &padded)?;
let total_fee = settings::REGISTRATION_FEE_LAMPORTS let total_fee = economy
.checked_add(limit_fee_lamports(args.additional_limit)?) .registration_fee_lamports
.checked_add(limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?)
.ok_or(error!(ErrCode::MathOverflow))?; .ok_or(error!(ErrCode::MathOverflow))?;
transfer_lamports( transfer_lamports(
&ctx.accounts.signer, &ctx.accounts.signer,
@ -204,6 +312,7 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
args.additional_limit % settings::LIMIT_STEP == 0, args.additional_limit % settings::LIMIT_STEP == 0,
ErrCode::InvalidLimitIncrement ErrCode::InvalidLimitIncrement
); );
let economy = read_users_economy_config(&ctx.accounts.users_economy_config_pda)?;
let (expected_pda, _) = find_user_pda(ctx.program_id, &args.login); let (expected_pda, _) = find_user_pda(ctx.program_id, &args.login);
require_keys_eq!( require_keys_eq!(
@ -295,7 +404,7 @@ pub fn update_user_pda(ctx: Context<UpdateUserPda>, args: UpdateUserPdaArgs) ->
let padded = pad_to_fixed_size(serialized, settings::USER_PDA_SPACE)?; let padded = pad_to_fixed_size(serialized, settings::USER_PDA_SPACE)?;
write_to_pda(&ctx.accounts.user_pda, &padded)?; write_to_pda(&ctx.accounts.user_pda, &padded)?;
let topup_fee = limit_fee_lamports(args.additional_limit)?; let topup_fee = limit_fee_lamports(args.additional_limit, economy.lamports_per_limit_step)?;
if topup_fee > 0 { if topup_fee > 0 {
transfer_lamports( transfer_lamports(
&ctx.accounts.signer, &ctx.accounts.signer,
@ -636,10 +745,10 @@ fn transfer_lamports<'info>(
Ok(()) Ok(())
} }
fn limit_fee_lamports(limit_delta: u64) -> Result<u64> { fn limit_fee_lamports(limit_delta: u64, lamports_per_limit_step: u64) -> Result<u64> {
let units = limit_delta / settings::LIMIT_STEP; let units = limit_delta / settings::LIMIT_STEP;
units units
.checked_mul(settings::LAMPORTS_PER_LIMIT_STEP) .checked_mul(lamports_per_limit_step)
.ok_or(error!(ErrCode::MathOverflow)) .ok_or(error!(ErrCode::MathOverflow))
} }
@ -650,6 +759,18 @@ fn find_user_pda(program_id: &Pubkey, login: &str) -> (Pubkey, u8) {
) )
} }
fn find_users_economy_config_pda(program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[settings::USERS_ECONOMY_CONFIG_SEED], program_id)
}
fn read_users_economy_config(pda: &AccountInfo) -> Result<UsersEconomyConfigState> {
let raw = safe_read_pda(pda);
require!(!raw.is_empty(), ErrCode::EmptyPdaData);
let mut slice: &[u8] = &raw;
UsersEconomyConfigState::deserialize(&mut slice)
.map_err(|_| error!(ErrCode::DeserializationError))
}
fn pad_to_fixed_size(mut bytes: Vec<u8>, target_size: usize) -> Result<Vec<u8>> { fn pad_to_fixed_size(mut bytes: Vec<u8>, target_size: usize) -> Result<Vec<u8>> {
require!(bytes.len() <= target_size, ErrCode::RecordTooLarge); require!(bytes.len() <= target_size, ErrCode::RecordTooLarge);
bytes.resize(target_size, 0); bytes.resize(target_size, 0);

View File

@ -21,6 +21,7 @@ const KEY_STATUS_CREATED = 0;
const LIMIT_STEP = 10_000n; const LIMIT_STEP = 10_000n;
const START_BONUS_LIMIT = 100_000n; const START_BONUS_LIMIT = 100_000n;
const FEE_RECEIVER = new PublicKey("9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb"); const FEE_RECEIVER = new PublicKey("9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb");
const USERS_ECONOMY_CONFIG_SEED = "shine_users_v1_economy_config";
type MutableFields = { type MutableFields = {
blockchainKey: PublicKey; blockchainKey: PublicKey;
@ -143,6 +144,22 @@ describe("shine_users e2e", () => {
[Buffer.from("login="), Buffer.from(login, "utf8")], [Buffer.from("login="), Buffer.from(login, "utf8")],
program.programId program.programId
); );
const [usersEconomyConfigPda] = PublicKey.findProgramAddressSync(
[Buffer.from(USERS_ECONOMY_CONFIG_SEED, "utf8")],
program.programId
);
const economyAi = await provider.connection.getAccountInfo(usersEconomyConfigPda);
if (!economyAi) {
await program.methods
.initUsersEconomyConfig()
.accounts({
signer: provider.wallet.publicKey,
usersEconomyConfigPda,
systemProgram: SystemProgram.programId,
})
.rpc();
}
const root = anchor.web3.Keypair.generate(); const root = anchor.web3.Keypair.generate();
const blockchainKey = anchor.web3.Keypair.generate().publicKey; const blockchainKey = anchor.web3.Keypair.generate().publicKey;
@ -207,6 +224,7 @@ describe("shine_users e2e", () => {
systemProgram: SystemProgram.programId, systemProgram: SystemProgram.programId,
feeReceiver: FEE_RECEIVER, feeReceiver: FEE_RECEIVER,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY, instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
usersEconomyConfigPda,
}) })
.instruction(); .instruction();
@ -275,6 +293,7 @@ describe("shine_users e2e", () => {
systemProgram: SystemProgram.programId, systemProgram: SystemProgram.programId,
feeReceiver: FEE_RECEIVER, feeReceiver: FEE_RECEIVER,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY, instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
usersEconomyConfigPda,
}) })
.instruction(); .instruction();