diff --git a/VERSION.properties b/VERSION.properties index 1526f59..8f0bf56 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.94 -server.version=1.2.88 +client.version=1.2.95 +server.version=1.2.89 diff --git a/scripts/devnet/test_login_guard_precheck.js b/scripts/devnet/test_login_guard_precheck.js new file mode 100644 index 0000000..43838a4 --- /dev/null +++ b/scripts/devnet/test_login_guard_precheck.js @@ -0,0 +1,87 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +const crypto = require('crypto'); +const web3 = require('@solana/web3.js'); + +const RPC = process.env.SOLANA_RPC || 'https://api.devnet.solana.com'; +const LOGIN_GUARD_PROGRAM_ID = '3xkopA7cXagxzMFrKdv3NCBfV6BKiRJCk69kr27M2sRo'; +const SIM_PAYER = 'FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P'; +const login = String(process.argv[2] || '').trim().toLowerCase(); + +if (!login) { + console.error('Usage: node scripts/devnet/test_login_guard_precheck.js '); + process.exit(1); +} + +function discriminator(name) { + return crypto.createHash('sha256').update(`global:${name}`).digest().subarray(0, 8); +} + +function encodeClassifyLoginData(loginValue) { + const disc = discriminator('classify_login'); + const str = Buffer.from(loginValue, 'utf8'); + const len = Buffer.alloc(4); + len.writeUInt32LE(str.length, 0); + return Buffer.concat([disc, len, str]); +} + +function decodeU32FromBase64(b64) { + const bytes = Buffer.from(b64, 'base64'); + if (bytes.length < 4) throw new Error('bad returnData'); + return bytes.readUInt32LE(0); +} + +async function main() { + const connection = new web3.Connection(RPC, 'confirmed'); + const programId = new web3.PublicKey(LOGIN_GUARD_PROGRAM_ID); + const payer = new web3.PublicKey(SIM_PAYER); + + const ix = new web3.TransactionInstruction({ + programId, + keys: [{ pubkey: payer, isSigner: true, isWritable: false }], + data: encodeClassifyLoginData(login), + }); + + const { blockhash } = await connection.getLatestBlockhash('confirmed'); + const v0 = new web3.TransactionMessage({ + payerKey: payer, + recentBlockhash: blockhash, + instructions: [ix], + }).compileToV0Message(); + const tx = new web3.VersionedTransaction(v0); + + const sim = await connection.simulateTransaction(tx, { + commitment: 'confirmed', + sigVerify: false, + replaceRecentBlockhash: true, + }); + + if (sim?.value?.err) { + console.error('Simulation error:', sim.value.err); + console.error('Logs:', sim?.value?.logs || []); + process.exit(2); + } + + const returnData = sim?.value?.returnData; + if (!returnData || returnData.programId !== LOGIN_GUARD_PROGRAM_ID) { + console.error('No returnData from login_guard'); + console.error(JSON.stringify(sim, null, 2)); + process.exit(3); + } + + const cls = decodeU32FromBase64(returnData.data[0]); + const className = cls === 0 ? 'free' : cls === 1 ? 'premium' : cls === 2 ? 'company' : `unknown(${cls})`; + console.log(JSON.stringify({ + login, + classCode: cls, + className, + logs: sim?.value?.logs || [], + }, null, 2)); +} + +main().catch((e) => { + console.error(e?.stack || e?.message || String(e)); + process.exit(10); +}); + diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index 46bb9fb..74c25f8 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -1,7 +1,7 @@ import { renderHeader } from '../components/header.js'; import { authService, clearAuthMessages, state } from '../state.js'; import { toUserMessage } from '../services/ui-error-texts.js'; -import { precheckLoginClassOnSolana } from '../services/solana-register-service.js'; +import { formatSolanaErrorDetails, precheckLoginClassOnSolana } from '../services/solana-register-service.js'; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; @@ -90,12 +90,18 @@ export function render({ navigate }) { await authService.reconnect(state.entrySettings.shineServer); const isFree = await authService.ensureLoginFree(login); let className = ''; + let precheckWarning = ''; if (isFree) { - const precheck = await precheckLoginClassOnSolana({ - login, - solanaEndpoint: state.entrySettings.solanaServer, - }); - className = precheck.className; + try { + const precheck = await precheckLoginClassOnSolana({ + login, + solanaEndpoint: state.entrySettings.solanaServer, + }); + className = precheck.className; + } catch (precheckError) { + className = 'free'; + precheckWarning = formatSolanaErrorDetails(precheckError); + } } lastCheckedLogin = login; lastCheckedFree = isFree; @@ -104,7 +110,9 @@ export function render({ navigate }) { statusText.textContent = 'Логин уже занят ❌'; statusText.className = 'is-unavailable'; } else if (className === 'free') { - statusText.textContent = 'Логин свободен ✅'; + statusText.textContent = precheckWarning + ? `Логин свободен ✅ (предпроверка Solana недоступна: ${precheckWarning})` + : 'Логин свободен ✅'; statusText.className = 'is-available'; } else if (className === 'premium') { statusText.textContent = 'Логин свободен, но это премиум-логин (покупка через DAO) ❌'; @@ -119,7 +127,9 @@ export function render({ navigate }) { formError.style.display = 'none'; return isFree && className === 'free'; } catch (error) { - statusText.textContent = toUserMessage(error, 'Не удалось проверить логин'); + const base = toUserMessage(error, 'Не удалось проверить логин'); + const details = formatSolanaErrorDetails(error); + statusText.textContent = `${base}. Детали: ${details}`; statusText.className = 'is-unavailable'; return false; } finally { diff --git a/shine-UI/js/pages/registration-payment-view.js b/shine-UI/js/pages/registration-payment-view.js index 79636b6..00c8b78 100644 --- a/shine-UI/js/pages/registration-payment-view.js +++ b/shine-UI/js/pages/registration-payment-view.js @@ -11,7 +11,7 @@ import { getBalanceSol, getTopupSiteUrl, } from '../services/solana-wallet-service.js'; -import { registerUserOnSolana } from '../services/solana-register-service.js'; +import { formatSolanaErrorDetails, registerUserOnSolana } from '../services/solana-register-service.js'; export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false }; const MIN_REQUIRED_SOL = 0.01; @@ -192,7 +192,7 @@ export function render({ navigate }) { solanaEndpoint: state.entrySettings.solanaServer, }); } catch (solanaError) { - const solanaMsg = String(solanaError?.message || ''); + const solanaMsg = formatSolanaErrorDetails(solanaError); // Пользователь уже зарегистрирован в Solana — продолжаем if (!solanaMsg.includes('already') && !solanaMsg.includes('UserAlreadyExists')) { throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`); diff --git a/shine-UI/js/services/solana-register-service.js b/shine-UI/js/services/solana-register-service.js index 2269601..ea81677 100644 --- a/shine-UI/js/services/solana-register-service.js +++ b/shine-UI/js/services/solana-register-service.js @@ -7,7 +7,8 @@ import { } from '../solana-programs.js'; const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]); -const CLASSIFY_LOGIN_DISCRIMINATOR = new Uint8Array([118, 253, 204, 124, 22, 232, 235, 32]); +const CLASSIFY_LOGIN_DISCRIMINATOR = new Uint8Array([112, 97, 152, 32, 255, 73, 108, 86]); +const PRECHECK_SIM_PAYER = 'FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P'; const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111'; const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111'; @@ -197,24 +198,61 @@ function decodeU32FromB64(rawB64) { return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true); } +function safeJson(value) { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +export 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: ${logs.join(' | ')}`); + } + + const errObj = error?.value?.err || error?.err || error?.data?.err; + if (errObj) { + parts.push(`Err: ${safeJson(errObj)}`); + } + + if (!parts.length) return 'unknown'; + return parts.join(' :: '); +} + export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) { const solana = await loadSolanaLib(); const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed'); const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID); - const signer = solana.Keypair.generate(); + const payer = new solana.PublicKey(PRECHECK_SIM_PAYER); const ix = new solana.TransactionInstruction({ programId: loginGuardProgram, - keys: [{ pubkey: signer.publicKey, isSigner: true, isWritable: false }], + keys: [{ pubkey: payer, isSigner: true, isWritable: false }], data: serializeClassifyLoginArgs(String(login || '').toLowerCase()), }); - const tx = new solana.Transaction().add(ix); - tx.feePayer = signer.publicKey; - tx.recentBlockhash = (await connection.getLatestBlockhash('confirmed')).blockhash; - tx.sign(signer); + const { blockhash } = await connection.getLatestBlockhash('confirmed'); + const v0Message = new solana.TransactionMessage({ + payerKey: payer, + recentBlockhash: blockhash, + instructions: [ix], + }).compileToV0Message(); + const tx = new solana.VersionedTransaction(v0Message); - const sim = await connection.simulateTransaction(tx, { commitment: 'confirmed', sigVerify: true }); + const sim = await connection.simulateTransaction(tx, { + commitment: 'confirmed', + sigVerify: false, + replaceRecentBlockhash: true, + }); if (sim?.value?.err) { - throw new Error(`LOGIN_GUARD_SIMULATION_FAILED: ${JSON.stringify(sim.value.err)}`); + const simErr = new Error(`LOGIN_GUARD_SIMULATION_FAILED: ${safeJson(sim.value.err)}`); + simErr.logs = sim?.value?.logs || []; + simErr.err = sim?.value?.err; + throw simErr; } const returnData = sim?.value?.returnData; if (!returnData || returnData.programId !== SHINE_LOGIN_GUARD_PROGRAM_ID) { diff --git a/shine-solana/shine/programs/shine_login_guard/src/dictionaries/trademarks/tech_global.txt b/shine-solana/shine/programs/shine_login_guard/src/dictionaries/trademarks/tech_global.txt index 02feb60..58c689e 100644 --- a/shine-solana/shine/programs/shine_login_guard/src/dictionaries/trademarks/tech_global.txt +++ b/shine-solana/shine/programs/shine_login_guard/src/dictionaries/trademarks/tech_global.txt @@ -282,6 +282,7 @@ grubhub hcl hikvision honeywell +hp hpe infosys ingrammicro diff --git a/shine-solana/shine/programs/shine_login_guard/src/lib.rs b/shine-solana/shine/programs/shine_login_guard/src/lib.rs index 506d234..212d540 100644 --- a/shine-solana/shine/programs/shine_login_guard/src/lib.rs +++ b/shine-solana/shine/programs/shine_login_guard/src/lib.rs @@ -32,13 +32,15 @@ fn classify(login: &str) -> u32 { let Some(normalized) = normalize_login(login) else { return CLASS_PREMIUM; }; + // Сначала пытаемся классифицировать по словарям (в т.ч. trademark/company), + // и только если не нашли совпадений — применяем правило короткого логина. + if let Some(v) = classify_split(&normalized) { + return v; + } if normalized.len() <= 7 { return CLASS_PREMIUM; } - match classify_split(&normalized) { - Some(v) => v, - None => CLASS_FREE, - } + CLASS_FREE } fn normalize_login(login: &str) -> Option {