Логин guard: корректный precheck, company приоритет, hp в trademarks; подробные ошибки UI
This commit is contained in:
parent
101fd2eaa4
commit
775b655aac
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.94
|
client.version=1.2.95
|
||||||
server.version=1.2.88
|
server.version=1.2.89
|
||||||
|
|||||||
87
scripts/devnet/test_login_guard_precheck.js
Normal file
87
scripts/devnet/test_login_guard_precheck.js
Normal file
@ -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 <login>');
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
import { authService, clearAuthMessages, state } from '../state.js';
|
import { authService, clearAuthMessages, state } from '../state.js';
|
||||||
import { toUserMessage } from '../services/ui-error-texts.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 };
|
export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false };
|
||||||
|
|
||||||
@ -90,12 +90,18 @@ export function render({ navigate }) {
|
|||||||
await authService.reconnect(state.entrySettings.shineServer);
|
await authService.reconnect(state.entrySettings.shineServer);
|
||||||
const isFree = await authService.ensureLoginFree(login);
|
const isFree = await authService.ensureLoginFree(login);
|
||||||
let className = '';
|
let className = '';
|
||||||
|
let precheckWarning = '';
|
||||||
if (isFree) {
|
if (isFree) {
|
||||||
|
try {
|
||||||
const precheck = await precheckLoginClassOnSolana({
|
const precheck = await precheckLoginClassOnSolana({
|
||||||
login,
|
login,
|
||||||
solanaEndpoint: state.entrySettings.solanaServer,
|
solanaEndpoint: state.entrySettings.solanaServer,
|
||||||
});
|
});
|
||||||
className = precheck.className;
|
className = precheck.className;
|
||||||
|
} catch (precheckError) {
|
||||||
|
className = 'free';
|
||||||
|
precheckWarning = formatSolanaErrorDetails(precheckError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lastCheckedLogin = login;
|
lastCheckedLogin = login;
|
||||||
lastCheckedFree = isFree;
|
lastCheckedFree = isFree;
|
||||||
@ -104,7 +110,9 @@ export function render({ navigate }) {
|
|||||||
statusText.textContent = 'Логин уже занят ❌';
|
statusText.textContent = 'Логин уже занят ❌';
|
||||||
statusText.className = 'is-unavailable';
|
statusText.className = 'is-unavailable';
|
||||||
} else if (className === 'free') {
|
} else if (className === 'free') {
|
||||||
statusText.textContent = 'Логин свободен ✅';
|
statusText.textContent = precheckWarning
|
||||||
|
? `Логин свободен ✅ (предпроверка Solana недоступна: ${precheckWarning})`
|
||||||
|
: 'Логин свободен ✅';
|
||||||
statusText.className = 'is-available';
|
statusText.className = 'is-available';
|
||||||
} else if (className === 'premium') {
|
} else if (className === 'premium') {
|
||||||
statusText.textContent = 'Логин свободен, но это премиум-логин (покупка через DAO) ❌';
|
statusText.textContent = 'Логин свободен, но это премиум-логин (покупка через DAO) ❌';
|
||||||
@ -119,7 +127,9 @@ export function render({ navigate }) {
|
|||||||
formError.style.display = 'none';
|
formError.style.display = 'none';
|
||||||
return isFree && className === 'free';
|
return isFree && className === 'free';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
statusText.textContent = toUserMessage(error, 'Не удалось проверить логин');
|
const base = toUserMessage(error, 'Не удалось проверить логин');
|
||||||
|
const details = formatSolanaErrorDetails(error);
|
||||||
|
statusText.textContent = `${base}. Детали: ${details}`;
|
||||||
statusText.className = 'is-unavailable';
|
statusText.className = 'is-unavailable';
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
getBalanceSol,
|
getBalanceSol,
|
||||||
getTopupSiteUrl,
|
getTopupSiteUrl,
|
||||||
} from '../services/solana-wallet-service.js';
|
} 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 };
|
export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false };
|
||||||
const MIN_REQUIRED_SOL = 0.01;
|
const MIN_REQUIRED_SOL = 0.01;
|
||||||
@ -192,7 +192,7 @@ export function render({ navigate }) {
|
|||||||
solanaEndpoint: state.entrySettings.solanaServer,
|
solanaEndpoint: state.entrySettings.solanaServer,
|
||||||
});
|
});
|
||||||
} catch (solanaError) {
|
} catch (solanaError) {
|
||||||
const solanaMsg = String(solanaError?.message || '');
|
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
||||||
// Пользователь уже зарегистрирован в Solana — продолжаем
|
// Пользователь уже зарегистрирован в Solana — продолжаем
|
||||||
if (!solanaMsg.includes('already') && !solanaMsg.includes('UserAlreadyExists')) {
|
if (!solanaMsg.includes('already') && !solanaMsg.includes('UserAlreadyExists')) {
|
||||||
throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`);
|
throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`);
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import {
|
|||||||
} from '../solana-programs.js';
|
} from '../solana-programs.js';
|
||||||
|
|
||||||
const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]);
|
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 ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
|
||||||
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
|
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
|
||||||
|
|
||||||
@ -197,24 +198,61 @@ function decodeU32FromB64(rawB64) {
|
|||||||
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true);
|
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 }) {
|
export async function precheckLoginClassOnSolana({ login, solanaEndpoint }) {
|
||||||
const solana = await loadSolanaLib();
|
const solana = await loadSolanaLib();
|
||||||
const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed');
|
const connection = new solana.Connection(String(solanaEndpoint || ''), 'confirmed');
|
||||||
const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID);
|
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({
|
const ix = new solana.TransactionInstruction({
|
||||||
programId: loginGuardProgram,
|
programId: loginGuardProgram,
|
||||||
keys: [{ pubkey: signer.publicKey, isSigner: true, isWritable: false }],
|
keys: [{ pubkey: payer, isSigner: true, isWritable: false }],
|
||||||
data: serializeClassifyLoginArgs(String(login || '').toLowerCase()),
|
data: serializeClassifyLoginArgs(String(login || '').toLowerCase()),
|
||||||
});
|
});
|
||||||
const tx = new solana.Transaction().add(ix);
|
const { blockhash } = await connection.getLatestBlockhash('confirmed');
|
||||||
tx.feePayer = signer.publicKey;
|
const v0Message = new solana.TransactionMessage({
|
||||||
tx.recentBlockhash = (await connection.getLatestBlockhash('confirmed')).blockhash;
|
payerKey: payer,
|
||||||
tx.sign(signer);
|
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) {
|
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;
|
const returnData = sim?.value?.returnData;
|
||||||
if (!returnData || returnData.programId !== SHINE_LOGIN_GUARD_PROGRAM_ID) {
|
if (!returnData || returnData.programId !== SHINE_LOGIN_GUARD_PROGRAM_ID) {
|
||||||
|
|||||||
@ -282,6 +282,7 @@ grubhub
|
|||||||
hcl
|
hcl
|
||||||
hikvision
|
hikvision
|
||||||
honeywell
|
honeywell
|
||||||
|
hp
|
||||||
hpe
|
hpe
|
||||||
infosys
|
infosys
|
||||||
ingrammicro
|
ingrammicro
|
||||||
|
|||||||
@ -32,13 +32,15 @@ fn classify(login: &str) -> u32 {
|
|||||||
let Some(normalized) = normalize_login(login) else {
|
let Some(normalized) = normalize_login(login) else {
|
||||||
return CLASS_PREMIUM;
|
return CLASS_PREMIUM;
|
||||||
};
|
};
|
||||||
|
// Сначала пытаемся классифицировать по словарям (в т.ч. trademark/company),
|
||||||
|
// и только если не нашли совпадений — применяем правило короткого логина.
|
||||||
|
if let Some(v) = classify_split(&normalized) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
if normalized.len() <= 7 {
|
if normalized.len() <= 7 {
|
||||||
return CLASS_PREMIUM;
|
return CLASS_PREMIUM;
|
||||||
}
|
}
|
||||||
match classify_split(&normalized) {
|
CLASS_FREE
|
||||||
Some(v) => v,
|
|
||||||
None => CLASS_FREE,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_login(login: &str) -> Option<String> {
|
fn normalize_login(login: &str) -> Option<String> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user