Синхронизировать изменения проекта

This commit is contained in:
AidarKC
2026-07-05 18:18:03 +04:00
parent 0240db59ee
commit 81bef1e1cc
36 changed files with 573 additions and 335 deletions
@@ -7,16 +7,15 @@ use solana_program::{
pubkey::Pubkey,
};
mod wordlist {
include!(concat!(env!("OUT_DIR"), "/generated_dictionary.rs"));
}
pub const INSTRUCTION_CLASSIFY_LOGIN: u8 = 1;
pub const CLASS_FREE: u32 = 0;
pub const CLASS_PREMIUM: u32 = 1;
pub const CLASS_TRADEMARK: u32 = 2;
pub const MAX_WORDS_PER_LOGIN: usize = 3;
pub const MAX_LOGIN_LEN: usize = 20;
// ВРЕМЕННАЯ ЗАГЛУШКА:
// до внедрения полноценной on-chain логики продажи/выдачи красивых имён
// пропускаем любой валидный логин длиной от 5 символов.
pub const TEMP_MIN_FREE_LOGIN_LEN: usize = 5;
solana_program::declare_id!("SHiGxGsXGioQYCYhchQ5R7KWoxN5UjFAFsucPf6sfnh");
@@ -75,13 +74,11 @@ pub fn build_classify_login_instruction_data(login: &str) -> Vec<u8> {
}
pub fn classify(login: &str) -> u32 {
// Временный режим: словарная premium/trademark классификация отключена.
let Some(normalized) = normalize_login(login) else {
return CLASS_PREMIUM;
};
if let Some(v) = classify_split(&normalized) {
return v;
}
if normalized.len() <= 7 {
if normalized.len() < TEMP_MIN_FREE_LOGIN_LEN {
return CLASS_PREMIUM;
}
CLASS_FREE
@@ -107,49 +104,6 @@ fn normalize_login(login: &str) -> Option<String> {
Some(out)
}
fn classify_split(login: &str) -> Option<u32> {
fn dfs(rest: &str, depth: usize, has_tm: bool) -> Option<u32> {
if rest.is_empty() {
if depth > 0 && depth <= MAX_WORDS_PER_LOGIN {
return Some(if has_tm { CLASS_TRADEMARK } else { CLASS_PREMIUM });
}
return None;
}
if depth >= MAX_WORDS_PER_LOGIN {
return None;
}
let max_piece = rest.len().min(MAX_LOGIN_LEN);
let mut premium_found = false;
for i in 1..=max_piece {
let candidate = &rest[..i];
let is_tm = is_trademark_word(candidate);
let is_pr = is_tm || is_premium_word(candidate);
if !is_pr {
continue;
}
match dfs(&rest[i..], depth + 1, has_tm || is_tm) {
Some(CLASS_TRADEMARK) => return Some(CLASS_TRADEMARK),
Some(CLASS_PREMIUM) => premium_found = true,
_ => {}
}
}
if premium_found {
Some(CLASS_PREMIUM)
} else {
None
}
}
dfs(login, 0, false)
}
fn is_premium_word(word: &str) -> bool {
wordlist::PREMIUM_WORDS.binary_search(&word).is_ok()
}
fn is_trademark_word(word: &str) -> bool {
wordlist::TRADEMARK_WORDS.binary_search(&word).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -165,12 +119,13 @@ mod tests {
}
#[test]
fn short_unknown_login_is_premium() {
assert_eq!(classify("abc123"), CLASS_PREMIUM);
fn login_shorter_than_temporary_threshold_is_premium() {
assert_eq!(classify("ab_1"), CLASS_PREMIUM);
}
#[test]
fn long_unknown_login_is_free() {
fn login_from_temporary_threshold_and_longer_is_free() {
assert_eq!(classify("abcde"), CLASS_FREE);
assert_eq!(classify("abcdefgh9"), CLASS_FREE);
}
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Тех. инструменты — Shine Payments Devnet</title>
<title>Тех. инструменты — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -43,7 +43,7 @@
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Техническая страница (Devnet)</h1>
<h1>Техническая страница (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -112,7 +112,7 @@
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const USERS_PROGRAM_ID = new solanaWeb3.PublicKey("SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_config",
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Покупка билета — Shine Payments Devnet</title>
<title>Покупка билета — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -40,7 +40,7 @@
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Покупка билета (Devnet)</h1>
<h1>Покупка билета (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -80,7 +80,7 @@
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const ORACLE_ACCOUNT = new solanaWeb3.PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE");
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DAO revoke vote — Shine Payments Devnet</title>
<title>DAO revoke vote — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -39,7 +39,7 @@
<body>
<div class="wrap">
<div style="margin-bottom: 12px;"><a class="back" href="./index.html">← На главную</a></div>
<h1>DAO: голосование на revoke/burn membership token (Devnet)</h1>
<h1>DAO: голосование на revoke/burn membership token (Mainnet)</h1>
<div class="muted">Governance program: <code id="govPid"></code></div>
<div class="panel">
@@ -97,8 +97,7 @@
import {
Connection,
PublicKey,
Transaction,
clusterApiUrl
Transaction
} from "https://esm.sh/@solana/web3.js@1.95.3";
import {
PROGRAM_VERSION_V3,
@@ -117,7 +116,7 @@
} from "https://esm.sh/@solana/spl-governance@0.3.28";
const GOVERNANCE_PROGRAM_ID = new PublicKey("GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw");
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const connection = new Connection("https://solana-rpc.publicnode.com", "confirmed");
document.getElementById("govPid").textContent = GOVERNANCE_PROGRAM_ID.toBase58();
let wallet = null;
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DAO-права менеджеров — Shine Payments Devnet</title>
<title>DAO-права менеджеров — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -39,7 +39,7 @@
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>DAO: права менеджеров (Devnet)</h1>
<h1>DAO: права менеджеров (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -85,7 +85,7 @@
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_config",
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Главная — Shine Payments Devnet</title>
<title>Главная — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -36,7 +36,7 @@
</head>
<body>
<div class="wrap">
<h1>Shine Payments Devnet</h1>
<h1>Shine Payments Mainnet</h1>
<div class="panel">
<div>Выберите страницу:</div>
</div>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Логика работы — Shine Payments Devnet</title>
<title>Логика работы — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -27,7 +27,7 @@
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Логика работы Shine Payments (тестовый этап)</h1>
<div class="panel">
<p>Система работает в <b>Devnet</b>. Экономика хранится в <b>USD-центах</b>, а реальные переводы происходят в SOL.</p>
<p>Система работает в <b>Mainnet</b>. Экономика хранится в <b>USD-центах</b>, а реальные переводы происходят в SOL.</p>
<p>Курс SOL/USD берётся из Pyth прямо в контракте при покупке и при шаге выплаты. Цена проверяется на актуальность (не старше 120 секунд).</p>
</div>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Менеджерские билеты — Shine Payments Devnet</title>
<title>Менеджерские билеты — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -33,13 +33,25 @@
.ok { color: var(--ok); }
.warn { color: var(--warn); }
.err { color: var(--err); white-space: pre-wrap; }
.err-box {
margin-top: 10px;
padding: 10px 12px;
border: 1px solid #4c2730;
border-radius: 8px;
background: #23151a;
color: var(--err);
white-space: pre-wrap;
line-height: 1.45;
font-family: monospace;
font-size: 13px;
}
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
</style>
</head>
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Менеджер: создание билетов (Devnet)</h1>
<h1>Менеджер: создание билетов (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -78,7 +90,7 @@
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
managerAllowance: "shine_p_manager_allow",
@@ -130,6 +142,55 @@
return s.includes("managerlimitexceeded") || s.includes("invalidmanagerwallet");
}
function safeJson(value) {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function formatSolanaErrorDetails(error) {
const parts = [];
const msg = String(error?.message || error || "").trim();
if (msg) parts.push(msg);
const logs = error?.logs || error?.transactionLogs || error?.simulationLogs || error?.data?.logs;
if (Array.isArray(logs) && logs.length) {
parts.push(`Logs:\n${logs.join("\n")}`);
}
const errObj = error?.value?.err || error?.err || error?.data?.err;
if (errObj) {
parts.push(`Err:\n${safeJson(errObj)}`);
}
if (error?.signature) {
parts.push(`Signature: ${error.signature}`);
}
return parts.length ? parts.join("\n\n") : "unknown";
}
async function attachSolanaLogs(error) {
if (!error) return error;
if (typeof error.getLogs === "function") {
try {
const logs = await error.getLogs(connection);
if (Array.isArray(logs) && logs.length) {
error.logs = logs;
error.transactionLogs = logs;
error.simulationLogs = logs;
}
} catch {
// Игнорируем: часть RPC не умеет отдавать логи для expired/confirm ошибок.
}
}
return error;
}
function parseManagerAllowance(data) {
let o = 0;
const version = data[o++];
@@ -183,8 +244,29 @@
tx.recentBlockhash = bh.blockhash;
const signed = await provider.signTransaction(tx);
const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: false });
await connection.confirmTransaction({ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight }, "confirmed");
return sig;
try {
await connection.confirmTransaction(
{ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight },
"confirmed",
);
return { signature: sig, confirmed: true };
} catch (confirmError) {
confirmError.signature = sig;
const txInfo = await connection.getTransaction(sig, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0,
});
if (txInfo?.meta?.logMessages?.length) {
confirmError.logs = txInfo.meta.logMessages;
confirmError.transactionLogs = txInfo.meta.logMessages;
}
await attachSolanaLogs(confirmError);
const postCheck = await Promise.resolve(typeof ix._postCheck === "function" ? ix._postCheck(sig) : null);
if (postCheck?.success) {
return { signature: sig, confirmed: false, postCheck };
}
throw confirmError;
}
}
function deriveManagerAllowancePda(managerWallet) {
@@ -272,8 +354,17 @@
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Билет создан. Tx: <code>${sig}</code></span>`;
ix._postCheck = async () => {
const ai = await connection.getAccountInfo(ticketPda, "confirmed");
if (!ai) return { success: false };
return { success: true, ticketPda: ticketPda.toBase58() };
};
const result = await sendInstruction(ix);
if (result.confirmed) {
out.innerHTML = `<span class="ok">Билет создан. Tx: <code>${result.signature}</code></span>`;
} else {
out.innerHTML = `<span class="warn">Транзакция подтверждалась с задержкой, но билет создан on-chain. Tx: <code>${result.signature}</code></span>`;
}
await refresh();
} catch (e) {
const raw = String(e.message || e);
@@ -281,7 +372,7 @@
out.innerHTML = `<span class="warn">Операция отклонена: лимит менеджера недостаточен или кошелек не имеет прав менеджера.</span>`;
return;
}
out.innerHTML = `<span class="err">${raw}</span>`;
out.innerHTML = `<span class="err">Ошибка создания билета:</span><div class="err-box">${formatSolanaErrorDetails(e)}</div>`;
}
}
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Что ещё нужно до DAO — Shine Payments Devnet</title>
<title>Что ещё нужно до DAO — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -25,7 +25,7 @@
<h1>Что ещё нужно до реального DAO</h1>
<div class="panel">
<p>Сейчас роль DAO выполняет обычный кошелёк (тестовый режим Devnet).</p>
<p>Сейчас роль DAO выполняет обычный кошелёк, а полное on-chain управление через DAO ещё не включено.</p>
</div>
<div class="panel">
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Сценарий тестирования — Shine Payments Devnet</title>
<title>Сценарий тестирования — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -22,7 +22,7 @@
</head>
<body>
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Сценарий тестирования Shine Payments (Devnet)</h1>
<h1>Сценарий тестирования Shine Payments (Mainnet)</h1>
<div class="panel">
<h3>Вариант А: один кошелёк</h3>
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Отслеживание билета — Shine Payments Devnet</title>
<title>Отслеживание билета — Shine Payments Mainnet</title>
<style>
:root {
color-scheme: dark;
@@ -42,7 +42,7 @@
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">← На главную</a></div>
<h1>Отслеживание билета (Devnet)</h1>
<h1>Отслеживание билета (Mainnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
@@ -78,7 +78,7 @@
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("SHiPmXbM9Fs9khzRUW3TGKsS2W84aqaXTxs3ZkajW9v");
const RPC_URL = "https://api.mainnet-beta.solana.com";
const RPC_URL = "https://solana-rpc.publicnode.com";
const ORACLE_ACCOUNT = new solanaWeb3.PublicKey("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE");
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");