UI: показать пополнение при нехватке rent

This commit is contained in:
AidarKC
2026-07-29 12:51:11 +04:00
parent 2f73cdb0f9
commit ca8b6a33ba
2 changed files with 45 additions and 4 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
client.version=1.2.358 client.version=1.2.359
server.version=1.2.341 server.version=1.2.341
+44 -3
View File
@@ -1,9 +1,10 @@
import { renderHeader } from '../components/header.js'; import { renderHeader } from '../components/header.js';
import { authService, state } from '../state.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 { loadEncryptedUserSecrets } from '../services/key-vault.js';
import { resolveShineServerByServerLogin } from '../services/shine-server-resolver.js'; import { resolveShineServerByServerLogin } from '../services/shine-server-resolver.js';
import { readShineUserPda, updateShineUserPdaOnSolana } from '../services/shine-user-pda-service.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: 'Серверы доступа' }; export const pageMeta = { id: 'access-servers-view', title: 'Серверы доступа' };
@@ -48,6 +49,27 @@ function shortenSignature(value) {
return `${text.slice(0, 8)}...${text.slice(-8)}`; 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() { function createConfirmModal() {
const root = document.getElementById('modal-root'); const root = document.getElementById('modal-root');
if (!(root instanceof HTMLElement)) return null; if (!(root instanceof HTMLElement)) return null;
@@ -460,6 +482,18 @@ export function render({ navigate }) {
)).join(''); )).join('');
}; };
const showTopupRequiredStatus = (target, clientAddress) => {
const address = String(clientAddress || '').trim();
const topupUrl = address ? getTopupSiteUrl(address) : '/devnet-topup';
target.innerHTML = `
<span style="display:block; margin-bottom:0.55rem;">
Не хватает SOL на client key для оплаты Solana rent/fee при обновлении user PDA.
</span>
${address ? `<span style="display:block; overflow-wrap:anywhere; margin-bottom:0.55rem;">Кошелёк: ${escapeHtml(address)}</span>` : ''}
<a class="primary-btn" href="${escapeHtml(topupUrl)}" target="_blank" rel="noopener" style="display:inline-flex; text-decoration:none;">Пополнить DEVNET кошелёк</a>
`;
};
const loadSuggestions = async () => { const loadSuggestions = async () => {
const prefix = normalizeLogin(addInput.value); const prefix = normalizeLogin(addInput.value);
if (suggestionsLoading || prefix.length < 2 || operationBusy) { if (suggestionsLoading || prefix.length < 2 || operationBusy) {
@@ -508,6 +542,7 @@ export function render({ navigate }) {
return { return {
rootPrivatePkcs8B64: savedRoot, rootPrivatePkcs8B64: savedRoot,
clientPrivatePkcs8B64: savedClient, clientPrivatePkcs8B64: savedClient,
clientAddress: await clientAddressFromPrivatePkcs8(savedClient),
}; };
} }
@@ -542,6 +577,7 @@ export function render({ navigate }) {
return { return {
rootPrivatePkcs8B64: keyBundle.rootPair.privatePkcs8B64, rootPrivatePkcs8B64: keyBundle.rootPair.privatePkcs8B64,
clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64, clientPrivatePkcs8B64: keyBundle.clientPair.privatePkcs8B64,
clientAddress: clientAddressFromPublicB64(keyBundle.clientPair.publicKeyB64),
}; };
}; };
@@ -558,9 +594,10 @@ export function render({ navigate }) {
const normalizedList = uniqueLogins(nextLogins); const normalizedList = uniqueLogins(nextLogins);
setOperationBusy(true); setOperationBusy(true);
target.textContent = String(inFlightText || 'Обновляем список серверов доступа...'); target.textContent = String(inFlightText || 'Обновляем список серверов доступа...');
let signingMaterial = null;
try { try {
const currentPda = await readShineUserPda({ login: sessionLogin, solanaEndpoint }); const currentPda = await readShineUserPda({ login: sessionLogin, solanaEndpoint });
const signingMaterial = await resolveAccessServerSigningMaterial(currentPda); signingMaterial = await resolveAccessServerSigningMaterial(currentPda);
const tx = await updateShineUserPdaOnSolana({ const tx = await updateShineUserPdaOnSolana({
login: sessionLogin, login: sessionLogin,
solanaEndpoint, solanaEndpoint,
@@ -575,7 +612,11 @@ export function render({ navigate }) {
suggestEl.innerHTML = ''; suggestEl.innerHTML = '';
refreshAddButton(); refreshAddButton();
} catch (error) { } catch (error) {
target.textContent = error?.message || 'Не удалось обновить список серверов доступа.'; if (isInsufficientFundsForRentError(error)) {
showTopupRequiredStatus(target, signingMaterial?.clientAddress);
} else {
target.textContent = error?.message || 'Не удалось обновить список серверов доступа.';
}
throw error; throw error;
} finally { } finally {
setOperationBusy(false); setOperationBusy(false);