diff --git a/VERSION.properties b/VERSION.properties index 4f1b0b27..b690bcaa 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.358 +client.version=1.2.359 server.version=1.2.341 diff --git a/shine-UI/js/pages/access-servers-view.js b/shine-UI/js/pages/access-servers-view.js index a07fc69c..ce7d7277 100644 --- a/shine-UI/js/pages/access-servers-view.js +++ b/shine-UI/js/pages/access-servers-view.js @@ -1,9 +1,10 @@ import { renderHeader } from '../components/header.js'; import { authService, state } from '../state.js'; -import { base64ToBytes } from '../services/crypto-utils.js'; +import { base64ToBytes, bytesToBase58, publicKeyB64FromPkcs8Ed25519 } from '../services/crypto-utils.js'; import { loadEncryptedUserSecrets } from '../services/key-vault.js'; import { resolveShineServerByServerLogin } from '../services/shine-server-resolver.js'; import { readShineUserPda, updateShineUserPdaOnSolana } from '../services/shine-user-pda-service.js'; +import { getTopupSiteUrl } from '../services/solana-wallet-service.js'; export const pageMeta = { id: 'access-servers-view', title: 'Серверы доступа' }; @@ -48,6 +49,27 @@ function shortenSignature(value) { return `${text.slice(0, 8)}...${text.slice(-8)}`; } +function isInsufficientFundsForRentError(error) { + const text = [ + error?.message, + error?.transactionMessage, + Array.isArray(error?.logs) ? error.logs.join('\n') : '', + Array.isArray(error?.transactionLogs) ? error.transactionLogs.join('\n') : '', + Array.isArray(error?.simulationLogs) ? error.simulationLogs.join('\n') : '', + ].filter(Boolean).join('\n').toLowerCase(); + return text.includes('insufficient funds') && text.includes('rent'); +} + +function clientAddressFromPublicB64(publicKeyB64) { + const bytes = base64ToBytes(publicKeyB64); + if (bytes.length !== 32) throw new Error('client public key должен быть 32 байта'); + return bytesToBase58(bytes); +} + +async function clientAddressFromPrivatePkcs8(privatePkcs8B64) { + return clientAddressFromPublicB64(await publicKeyB64FromPkcs8Ed25519(privatePkcs8B64)); +} + function createConfirmModal() { const root = document.getElementById('modal-root'); if (!(root instanceof HTMLElement)) return null; @@ -460,6 +482,18 @@ export function render({ navigate }) { )).join(''); }; + const showTopupRequiredStatus = (target, clientAddress) => { + const address = String(clientAddress || '').trim(); + const topupUrl = address ? getTopupSiteUrl(address) : '/devnet-topup'; + target.innerHTML = ` + + Не хватает SOL на client key для оплаты Solana rent/fee при обновлении user PDA. + + ${address ? `Кошелёк: ${escapeHtml(address)}` : ''} + Пополнить DEVNET кошелёк + `; + }; + const loadSuggestions = async () => { const prefix = normalizeLogin(addInput.value); if (suggestionsLoading || prefix.length < 2 || operationBusy) { @@ -508,6 +542,7 @@ export function render({ navigate }) { return { rootPrivatePkcs8B64: savedRoot, clientPrivatePkcs8B64: savedClient, + clientAddress: await clientAddressFromPrivatePkcs8(savedClient), }; } @@ -542,6 +577,7 @@ export function render({ navigate }) { return { rootPrivatePkcs8B64: keyBundle.rootPair.privatePkcs8B64, clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64, + clientAddress: clientAddressFromPublicB64(keyBundle.clientPair.publicKeyB64), }; }; @@ -558,9 +594,10 @@ export function render({ navigate }) { const normalizedList = uniqueLogins(nextLogins); setOperationBusy(true); target.textContent = String(inFlightText || 'Обновляем список серверов доступа...'); + let signingMaterial = null; try { const currentPda = await readShineUserPda({ login: sessionLogin, solanaEndpoint }); - const signingMaterial = await resolveAccessServerSigningMaterial(currentPda); + signingMaterial = await resolveAccessServerSigningMaterial(currentPda); const tx = await updateShineUserPdaOnSolana({ login: sessionLogin, solanaEndpoint, @@ -575,7 +612,11 @@ export function render({ navigate }) { suggestEl.innerHTML = ''; refreshAddButton(); } catch (error) { - target.textContent = error?.message || 'Не удалось обновить список серверов доступа.'; + if (isInsufficientFundsForRentError(error)) { + showTopupRequiredStatus(target, signingMaterial?.clientAddress); + } else { + target.textContent = error?.message || 'Не удалось обновить список серверов доступа.'; + } throw error; } finally { setOperationBusy(false);