Вынести экономику shine_users в отдельный PDA и добавить управление в UI
This commit is contained in:
parent
deef20c517
commit
7d2f50b6e1
@ -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();
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user