Переписать shine_payments и обновить тестовый UI с известным багом state

This commit is contained in:
AidarKC
2026-06-06 16:58:57 +04:00
parent c5ec32f87a
commit 89d06d317b
18 changed files with 1550 additions and 6578 deletions
@@ -66,7 +66,7 @@
<button id="updateCoefBtn">Обновить</button>
</div>
<div class="formula">Лимит покупки Q1 = max(limit_usd_cents - q1_sum_total_usd_cents, 0)</div>
<div class="formula">Шаг выплаты Q1 = ticket + dao(1x) + reward; Q2 = ticket + dao(2x) + reward</div>
<div class="formula">Шаг выплаты Q1 = ticket + dao(1x) + reward; Q2 = ticket + dao(2x) + reward; Q3 = ticket + dao(3x) + reward</div>
<div id="updateResult" class="muted"></div>
</div>
@@ -100,6 +100,12 @@
<div class="row"><button id="loadQ2Btn">Показать очередь 2</button></div>
<div id="queue2Table" class="muted"></div>
</div>
<div class="panel">
<h3>Очередь 3 (все билеты)</h3>
<div class="row"><button id="loadQ3Btn">Показать очередь 3</button></div>
<div id="queue3Table" class="muted"></div>
</div>
</div>
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
@@ -109,13 +115,16 @@
const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_v3_config",
coef: "shine_payments_v3_coef_limit",
queues: "shine_payments_v3_queues",
inflow: "shine_payments_v3_inflow_vault",
ticketQ1: "shine_payments_v3_q1_ticket",
ticketQ2: "shine_payments_v3_q2_ticket",
config: "shine_payments_config",
coef: "shine_payments_coef_limit",
queues: "shine_payments_queues",
inflow: "shine_payments_inflow_vault",
ticketQ1: "shine_payments_q1_ticket",
ticketQ2: "shine_payments_q2_ticket",
ticketQ3: "shine_payments_q3_ticket",
};
const IX = { init: 1, updateCoefLimit: 2 };
const USERS_IX = { initUsersEconomyConfig: 1, updateUsersEconomyConfig: 2 };
const USERS_SEEDS = {
economyConfig: "shine_users_v1_economy_config",
};
@@ -162,10 +171,8 @@
if (!Number.isFinite(v) || v <= 0) throw new Error("Некорректная сумма USD");
return BigInt(Math.round(v * 100));
}
async function ixDiscriminator(name) {
const msg = utf8("global:" + name);
const hash = await crypto.subtle.digest("SHA-256", msg);
return new Uint8Array(hash).slice(0, 8);
function ixData(tag, ...parts) {
return concat(new Uint8Array([tag]), ...parts);
}
function isUnauthorizedDao(msg) {
const s = String(msg || "").toLowerCase();
@@ -202,7 +209,11 @@
const q2Paid = readU64(data, o); o += 8;
const q2SumTotal = readU64(data, o); o += 8;
const q2SumPaid = readU64(data, o); o += 8;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid };
const q3Total = readU64(data, o); o += 8;
const q3Paid = readU64(data, o); o += 8;
const q3SumTotal = readU64(data, o); o += 8;
const q3SumPaid = readU64(data, o); o += 8;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid, q3Total, q3Paid, q3SumTotal, q3SumPaid };
}
function parseTicket(data) {
let o = 0;
@@ -263,7 +274,7 @@
return { usersEconomyConfigPda };
}
function ticketPda(queueId, index) {
const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2;
const seed = queueId === 1 ? SEEDS.ticketQ1 : (queueId === 2 ? SEEDS.ticketQ2 : SEEDS.ticketQ3);
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(seed), u64ToBytes(index)], PROGRAM_ID);
return pda;
}
@@ -320,6 +331,7 @@
<div>Доступно в inflow (сверх ренты): <b>${lamportsToSolStr(core.inflowAvailable)} SOL</b></div>
<div>Q1: total=${core.queues.q1Total}, paid=${core.queues.q1Paid}, sum_total=${centsToUsdStr(core.queues.q1SumTotal)} USD, sum_paid=${centsToUsdStr(core.queues.q1SumPaid)} USD</div>
<div>Q2: total=${core.queues.q2Total}, paid=${core.queues.q2Paid}, sum_total=${centsToUsdStr(core.queues.q2SumTotal)} USD, sum_paid=${centsToUsdStr(core.queues.q2SumPaid)} USD</div>
<div>Q3: total=${core.queues.q3Total}, paid=${core.queues.q3Paid}, sum_total=${centsToUsdStr(core.queues.q3SumTotal)} USD, sum_paid=${centsToUsdStr(core.queues.q3SumPaid)} USD</div>
`;
} catch (e) {
el.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
@@ -362,7 +374,6 @@
else if (!provider.isConnected) await provider.connect();
const pdas = derivePdas();
const disc = await ixDiscriminator("init");
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: pdas.configPda, isSigner: false, isWritable: true },
@@ -371,7 +382,7 @@
{ pubkey: pdas.inflowPda, isSigner: false, isWritable: true },
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data: disc });
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data: ixData(IX.init) });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Init выполнен. Tx: <code>${sig}</code></span>`;
await refreshAll();
@@ -397,8 +408,7 @@
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 data = concat(disc, u64ToBytes(coefPpm), u64ToBytes(limitUsdCents), u64ToBytes(rewardLamports));
const data = ixData(IX.updateCoefLimit, u64ToBytes(coefPpm), u64ToBytes(limitUsdCents), u64ToBytes(rewardLamports));
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: core.pdas.configPda, isSigner: false, isWritable: true },
@@ -427,13 +437,12 @@
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 ix = new solanaWeb3.TransactionInstruction({ programId: USERS_PROGRAM_ID, keys, data: ixData(USERS_IX.initUsersEconomyConfig) });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Users Economy init выполнен. Tx: <code>${sig}</code></span>`;
await refreshUsersEconomy();
@@ -455,9 +464,8 @@
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,
const data = ixData(
USERS_IX.updateUsersEconomyConfig,
u64ToBytes(registrationFeeLamports),
u64ToBytes(lamportsPerLimitStep),
u64ToBytes(startBonusLimit)
@@ -483,17 +491,17 @@
function currentDebtBeforeTicket(ticket, queues) {
if (ticket.isPaid) return 0n;
const paidSum = ticket.queueId === 1 ? queues.q1SumPaid : queues.q2SumPaid;
const paidSum = ticket.queueId === 1 ? queues.q1SumPaid : (ticket.queueId === 2 ? queues.q2SumPaid : queues.q3SumPaid);
return ticket.debtBefore > paidSum ? (ticket.debtBefore - paidSum) : 0n;
}
async function showQueue(queueId) {
const out = document.getElementById(queueId === 1 ? "queue1Table" : "queue2Table");
const out = document.getElementById(queueId === 1 ? "queue1Table" : (queueId === 2 ? "queue2Table" : "queue3Table"));
out.textContent = "Загрузка...";
try {
const core = await loadCore();
if (core.notInited) throw new Error("Сначала выполните init");
const total = queueId === 1 ? core.queues.q1Total : core.queues.q2Total;
const total = queueId === 1 ? core.queues.q1Total : (queueId === 2 ? core.queues.q2Total : core.queues.q3Total);
if (total === 0n) {
out.innerHTML = `<span class="muted">Очередь ${queueId} пока пустая.</span>`;
return;
@@ -555,6 +563,7 @@
document.getElementById("usersUpdateBtn").addEventListener("click", updateUsersEconomy);
document.getElementById("loadQ1Btn").addEventListener("click", () => showQueue(1));
document.getElementById("loadQ2Btn").addEventListener("click", () => showQueue(2));
document.getElementById("loadQ3Btn").addEventListener("click", () => showQueue(3));
refreshAll();
</script>
</body>
@@ -85,11 +85,12 @@
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_v3_config",
coef: "shine_payments_v3_coef_limit",
queues: "shine_payments_v3_queues",
ticketQ1: "shine_payments_v3_q1_ticket",
config: "shine_payments_config",
coef: "shine_payments_coef_limit",
queues: "shine_payments_queues",
ticketQ1: "shine_payments_q1_ticket",
};
const IX = { buyTicketUsd: 5, buyTicketSol: 6 };
const COEF_SCALE = 1_000_000n;
const LAMPORTS_PER_SOL = 1_000_000_000n;
@@ -177,10 +178,8 @@
return (cents * (10_000n - bp)) / 10_000n;
}
async function ixDiscriminator(name) {
const msg = utf8("global:" + name);
const hash = await crypto.subtle.digest("SHA-256", msg);
return new Uint8Array(hash).slice(0, 8);
function ixData(tag, ...parts) {
return concat(new Uint8Array([tag]), ...parts);
}
function parseConfig(data) {
@@ -209,7 +208,11 @@
const q2Paid = readU64(data, o); o += 8;
const q2SumTotal = readU64(data, o); o += 8;
const q2SumPaid = readU64(data, o); o += 8;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid };
const q3Total = readU64(data, o); o += 8;
const q3Paid = readU64(data, o); o += 8;
const q3SumTotal = readU64(data, o); o += 8;
const q3SumPaid = readU64(data, o); o += 8;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid, q3Total, q3Paid, q3SumTotal, q3SumPaid };
}
function getProvider() {
@@ -354,8 +357,7 @@
const nextIndex = queues.q1Total + 1n;
const [ticketPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.ticketQ1), u64ToBytes(nextIndex)], PROGRAM_ID);
const disc = await ixDiscriminator("buy_ticket_usd");
const data = concat(disc, u64ToBytes(usdCents), u64ToBytes(maxPayLamports), recipient.toBytes());
const data = ixData(IX.buyTicketUsd, u64ToBytes(usdCents), u64ToBytes(maxPayLamports), recipient.toBytes());
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: pdas.configPda, isSigner: false, isWritable: true },
@@ -394,8 +396,7 @@
const nextIndex = queues.q1Total + 1n;
const [ticketPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.ticketQ1), u64ToBytes(nextIndex)], PROGRAM_ID);
const disc = await ixDiscriminator("buy_ticket_sol");
const data = concat(disc, u64ToBytes(lamports), u64ToBytes(minUsdCents), recipient.toBytes());
const data = ixData(IX.buyTicketSol, u64ToBytes(lamports), u64ToBytes(minUsdCents), recipient.toBytes());
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: pdas.configPda, isSigner: false, isWritable: true },
@@ -65,6 +65,7 @@
<label>Кошелек менеджера: <input id="managerWallet" placeholder="Base58" /></label>
<label>Добавить лимит Q1 (USD): <input id="addQ1" value="100" /></label>
<label>Добавить лимит Q2 (USD): <input id="addQ2" value="50" /></label>
<label>Добавить лимит Q3 (USD): <input id="addQ3" value="25" /></label>
</div>
<div class="row">
<button id="grantBtn">Выдать лимиты</button>
@@ -87,9 +88,10 @@
const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_v3_config",
managerAllowance: "shine_p_v3_manager_allow",
config: "shine_payments_config",
managerAllowance: "shine_p_manager_allow",
};
const IX = { grantManagerLimits: 3 };
let walletPubkey = null;
let configCache = null;
document.getElementById("programId").textContent = PROGRAM_ID.toBase58();
@@ -124,10 +126,8 @@
if (!Number.isFinite(v) || v < 0) throw new Error("Некорректная сумма USD");
return BigInt(Math.round(v * 100));
}
async function ixDiscriminator(name) {
const msg = utf8("global:" + name);
const hash = await crypto.subtle.digest("SHA-256", msg);
return new Uint8Array(hash).slice(0, 8);
function ixData(tag, ...parts) {
return concat(new Uint8Array([tag]), ...parts);
}
function isUnauthorizedDao(msg) {
const s = String(msg || "").toLowerCase();
@@ -147,7 +147,8 @@
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const q1 = readU64(data, o); o += 8;
const q2 = readU64(data, o); o += 8;
return { version, manager, q1, q2 };
const q3 = readU64(data, o); o += 8;
return { version, manager, q1, q2, q3 };
}
function getProvider() {
@@ -216,11 +217,11 @@
const manager = new solanaWeb3.PublicKey(document.getElementById("managerWallet").value.trim());
const addQ1 = usdToCents(document.getElementById("addQ1").value.trim());
const addQ2 = usdToCents(document.getElementById("addQ2").value.trim());
if (addQ1 === 0n && addQ2 === 0n) throw new Error("Нужно указать сумму хотя бы для одной очереди.");
const addQ3 = usdToCents(document.getElementById("addQ3").value.trim());
if (addQ1 === 0n && addQ2 === 0n && addQ3 === 0n) throw new Error("Нужно указать сумму хотя бы для одной очереди.");
const allowancePda = deriveManagerAllowancePda(manager);
const disc = await ixDiscriminator("grant_manager_limits");
const data = concat(disc, manager.toBytes(), u64ToBytes(addQ1), u64ToBytes(addQ2));
const data = ixData(IX.grantManagerLimits, manager.toBytes(), u64ToBytes(addQ1), u64ToBytes(addQ2), u64ToBytes(addQ3));
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: configPda, isSigner: false, isWritable: true },
@@ -258,6 +259,7 @@
<div>PDA: <code>${allowancePda.toBase58()}</code></div>
<div>Доступно Q1: <b>${centsToUsdStr(st.q1)} USD</b></div>
<div>Доступно Q2: <b>${centsToUsdStr(st.q2)} USD</b></div>
<div>Доступно Q3: <b>${centsToUsdStr(st.q3)} USD</b></div>
`;
} catch (e) {
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
@@ -62,6 +62,7 @@
<select id="queueId">
<option value="1">Очередь 1</option>
<option value="2">Очередь 2</option>
<option value="3">Очередь 3</option>
</select>
</label>
<label>Получатель: <input id="recipient" placeholder="Base58 адрес" /></label>
@@ -80,11 +81,13 @@
const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
managerAllowance: "shine_p_v3_manager_allow",
queues: "shine_payments_v3_queues",
ticketQ1: "shine_payments_v3_q1_ticket",
ticketQ2: "shine_payments_v3_q2_ticket",
managerAllowance: "shine_p_manager_allow",
queues: "shine_payments_queues",
ticketQ1: "shine_payments_q1_ticket",
ticketQ2: "shine_payments_q2_ticket",
ticketQ3: "shine_payments_q3_ticket",
};
const IX = { managerAddTicket: 7 };
let walletPubkey = null;
let queuesCache = null;
document.getElementById("programId").textContent = PROGRAM_ID.toBase58();
@@ -119,10 +122,8 @@
if (!Number.isFinite(v) || v <= 0) throw new Error("Некорректная сумма USD");
return BigInt(Math.round(v * 100));
}
async function ixDiscriminator(name) {
const msg = utf8("global:" + name);
const hash = await crypto.subtle.digest("SHA-256", msg);
return new Uint8Array(hash).slice(0, 8);
function ixData(tag, ...parts) {
return concat(new Uint8Array([tag]), ...parts);
}
function isManagerErrors(msg) {
const s = String(msg || "").toLowerCase();
@@ -135,7 +136,8 @@
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const q1 = readU64(data, o); o += 8;
const q2 = readU64(data, o); o += 8;
return { version, manager, q1, q2 };
const q3 = readU64(data, o); o += 8;
return { version, manager, q1, q2, q3 };
}
function parseQueues(data) {
let o = 0;
@@ -148,7 +150,11 @@
const q2Paid = readU64(data, o); o += 8;
const q2SumTotal = readU64(data, o); o += 8;
const q2SumPaid = readU64(data, o); o += 8;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid };
const q3Total = readU64(data, o); o += 8;
const q3Paid = readU64(data, o); o += 8;
const q3SumTotal = readU64(data, o); o += 8;
const q3SumPaid = readU64(data, o); o += 8;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid, q3Total, q3Paid, q3SumTotal, q3SumPaid };
}
function getProvider() {
@@ -187,7 +193,7 @@
return pda;
}
function deriveTicketPda(queueId, index) {
const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2;
const seed = queueId === 1 ? SEEDS.ticketQ1 : (queueId === 2 ? SEEDS.ticketQ2 : SEEDS.ticketQ3);
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(seed), u64ToBytes(index)], PROGRAM_ID);
return pda;
}
@@ -223,6 +229,7 @@
<div>PDA лимитов: <code>${core.allowancePda.toBase58()}</code></div>
<div>Доступно Q1: <b>${centsToUsdStr(core.allowance.q1)} USD</b></div>
<div>Доступно Q2: <b>${centsToUsdStr(core.allowance.q2)} USD</b></div>
<div>Доступно Q3: <b>${centsToUsdStr(core.allowance.q3)} USD</b></div>
`;
} catch (e) {
el.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
@@ -241,15 +248,16 @@
if (!core.allowance) throw new Error("Для этого кошелька лимиты менеджера не выданы.");
const queueId = Number(document.getElementById("queueId").value);
if (queueId !== 1 && queueId !== 2) throw new Error("Очередь должна быть 1 или 2");
if (![1, 2, 3].includes(queueId)) throw new Error("Очередь должна быть 1, 2 или 3");
const recipient = new solanaWeb3.PublicKey(document.getElementById("recipient").value.trim());
const payout = usdToCents(document.getElementById("payoutUsd").value.trim());
const nextIndex = queueId === 1 ? (core.queues.q1Total + 1n) : (core.queues.q2Total + 1n);
const nextIndex = queueId === 1
? (core.queues.q1Total + 1n)
: (queueId === 2 ? (core.queues.q2Total + 1n) : (core.queues.q3Total + 1n));
const ticketPda = deriveTicketPda(queueId, nextIndex);
const disc = await ixDiscriminator("manager_add_ticket");
const data = concat(disc, new Uint8Array([queueId]), recipient.toBytes(), u64ToBytes(payout));
const data = ixData(IX.managerAddTicket, new Uint8Array([queueId]), recipient.toBytes(), u64ToBytes(payout));
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: core.allowancePda, isSigner: false, isWritable: true },
@@ -83,12 +83,14 @@
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_v3_config",
coef: "shine_payments_v3_coef_limit",
queues: "shine_payments_v3_queues",
ticketQ1: "shine_payments_v3_q1_ticket",
ticketQ2: "shine_payments_v3_q2_ticket",
config: "shine_payments_config",
coef: "shine_payments_coef_limit",
queues: "shine_payments_queues",
ticketQ1: "shine_payments_q1_ticket",
ticketQ2: "shine_payments_q2_ticket",
ticketQ3: "shine_payments_q3_ticket",
};
const IX = { changeTicketRecipient: 9, stepPayout: 8 };
const LAMPORTS_PER_SOL = 1_000_000_000n;
let walletPubkey = null;
@@ -133,10 +135,8 @@
function centsToUsdStr(c) {
return trimZeros((Number(c) / 100).toFixed(2));
}
async function ixDiscriminator(name) {
const msg = utf8("global:" + name);
const hash = await crypto.subtle.digest("SHA-256", msg);
return new Uint8Array(hash).slice(0, 8);
function ixData(tag, ...parts) {
return concat(new Uint8Array([tag]), ...parts);
}
function isNotEnoughForStep(msg) {
const s = String(msg || "").toLowerCase();
@@ -187,7 +187,11 @@
const q2Paid = readU64(data, o); o += 8;
const q2SumTotal = readU64(data, o); o += 8;
const q2SumPaid = readU64(data, o); o += 8;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid };
const q3Total = readU64(data, o); o += 8;
const q3Paid = readU64(data, o); o += 8;
const q3SumTotal = readU64(data, o); o += 8;
const q3SumPaid = readU64(data, o); o += 8;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid, q3Total, q3Paid, q3SumTotal, q3SumPaid };
}
function parseTicket(data) {
let o = 0;
@@ -232,7 +236,7 @@
return { configPda, coefPda, queuesPda };
}
function deriveTicketPda(queueId, index) {
const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2;
const seed = queueId === 1 ? SEEDS.ticketQ1 : (queueId === 2 ? SEEDS.ticketQ2 : SEEDS.ticketQ3);
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(seed), u64ToBytes(index)], PROGRAM_ID);
return pda;
}
@@ -262,14 +266,16 @@
function nextStepQueue(queues) {
const q1Pending = queues.q1Total - queues.q1Paid;
const q2Pending = queues.q2Total - queues.q2Paid;
const q3Pending = queues.q3Total - queues.q3Paid;
if (q1Pending > 0n) return 1;
if (q2Pending > 0n) return 2;
if (q3Pending > 0n) return 3;
return 0;
}
function nextPayoutTicket(queues) {
const queue = nextStepQueue(queues);
if (queue === 0) return null;
const index = queue === 1 ? (queues.q1Paid + 1n) : (queues.q2Paid + 1n);
const index = queue === 1 ? (queues.q1Paid + 1n) : (queue === 2 ? (queues.q2Paid + 1n) : (queues.q3Paid + 1n));
return { queue, index };
}
@@ -284,13 +290,13 @@
<div>Курс SOL/USD (Pyth): <b>${trimZeros((Number(core.pyth.num) / Number(core.pyth.den) / 100).toFixed(6))}</b>, возраст <b>${pythAge} сек</b></div>
<div>DAO: <code>${core.config.dao.toBase58()}</code></div>
<div class="muted">Сейчас это тестовый DAO-кошелек. В production здесь будет адрес реального DAO.</div>
<div>Обе очереди пусты/полностью выплачены.</div>
<div>Все очереди пусты/полностью выплачены.</div>
<div>На inflow vault доступно (сверх ренты): <b>${lamportsToSolStr(core.available)} SOL</b></div>
<div class="warn">При шаге эта сумма уйдет в DAO, награда не начисляется.</div>
`;
return;
}
const nextIndex = queue === 1 ? core.queues.q1Paid + 1n : core.queues.q2Paid + 1n;
const nextIndex = queue === 1 ? core.queues.q1Paid + 1n : (queue === 2 ? core.queues.q2Paid + 1n : core.queues.q3Paid + 1n);
const nextPda = deriveTicketPda(queue, nextIndex);
const nextAi = await connection.getAccountInfo(nextPda, "confirmed");
if (!nextAi) {
@@ -299,7 +305,7 @@
}
const next = parseTicket(nextAi.data);
const ticketLamports = usdCentsToLamportsCeil(next.payoutUsdCents, core.pyth);
const daoUsd = queue === 1 ? next.payoutUsdCents : (next.payoutUsdCents * 2n);
const daoUsd = next.payoutUsdCents * BigInt(queue);
const daoLamports = usdCentsToLamportsCeil(daoUsd, core.pyth);
const need = ticketLamports + daoLamports + core.coef.reward;
const missing = core.available >= need ? 0n : (need - core.available);
@@ -313,7 +319,7 @@
<div>DAO на этом шаге: <b>${centsToUsdStr(daoUsd)} USD</b> (~${lamportsToSolStr(daoLamports)} SOL)</div>
<div>Награда за шаг: <b>${lamportsToSolStr(core.coef.reward)} SOL</b></div>
<div>Нужно для шага: <b>${lamportsToSolStr(need)} SOL</b></div>
<div>Формула: <b>${queue === 1 ? "ticket + dao(1x) + reward" : "ticket + dao(2x) + reward"}</b></div>
<div>Формула: <b>ticket + dao(${queue}x) + reward</b></div>
<div>Доступно в inflow vault: <b>${lamportsToSolStr(core.available)} SOL</b></div>
<div>${missing === 0n
? '<span class="ok">Хватает для шага выплаты.</span>'
@@ -374,8 +380,7 @@
const newRecipient = new solanaWeb3.PublicKey(newRecipientRaw);
const core = cachedCore || await loadCoreState();
const disc = await ixDiscriminator("change_ticket_recipient");
const data = concat(disc, newRecipient.toBytes());
const data = ixData(IX.changeTicketRecipient, newRecipient.toBytes());
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: core.pdas.queuesPda, isSigner: false, isWritable: true },
@@ -404,7 +409,7 @@
if (idxRaw) {
const idx = BigInt(idxRaw);
for (const queue of [1, 2]) {
for (const queue of [1, 2, 3]) {
const pda = deriveTicketPda(queue, idx);
const ai = await connection.getAccountInfo(pda, "confirmed");
if (!ai) continue;
@@ -413,8 +418,8 @@
if (results.length === 0) throw new Error(`Тикет #${idx.toString()} не найден ни в одной очереди`);
} else if (walletRaw) {
const recipient = new solanaWeb3.PublicKey(walletRaw);
for (const queue of [1, 2]) {
const total = queue === 1 ? core.queues.q1Total : core.queues.q2Total;
for (const queue of [1, 2, 3]) {
const total = queue === 1 ? core.queues.q1Total : (queue === 2 ? core.queues.q2Total : core.queues.q3Total);
for (let i = 1n; i <= total; i++) {
const pda = deriveTicketPda(queue, i);
const ai = await connection.getAccountInfo(pda, "confirmed");
@@ -450,15 +455,14 @@
nextTicketPda = deriveTicketPda(1, core.queues.q1Paid + 1n);
recipient = walletPubkey;
} else {
const nextIndex = queue === 1 ? core.queues.q1Paid + 1n : core.queues.q2Paid + 1n;
const nextIndex = queue === 1 ? core.queues.q1Paid + 1n : (queue === 2 ? core.queues.q2Paid + 1n : core.queues.q3Paid + 1n);
nextTicketPda = deriveTicketPda(queue, nextIndex);
const ai = await connection.getAccountInfo(nextTicketPda, "confirmed");
if (!ai) throw new Error(`Следующий тикет #${nextIndex.toString()} для очереди ${queue} не найден`);
recipient = parseTicket(ai.data).recipient;
}
const disc = await ixDiscriminator("step_payout");
const data = concat(disc);
const data = ixData(IX.stepPayout);
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: core.pdas.configPda, isSigner: false, isWritable: true },