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);