diff --git a/VERSION.properties b/VERSION.properties
index e1e5b1b..151884f 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.244
-server.version=1.2.229
+client.version=1.2.245
+server.version=1.2.230
diff --git a/shine-UI/js/services/shine-user-pda-service.js b/shine-UI/js/services/shine-user-pda-service.js
index 3ead3b8..33acae7 100644
--- a/shine-UI/js/services/shine-user-pda-service.js
+++ b/shine-UI/js/services/shine-user-pda-service.js
@@ -623,6 +623,38 @@ export async function readShineUserPda({ login, solanaEndpoint }) {
};
}
+export async function readShineUserPdaByAddress({ pdaAddress, solanaEndpoint }) {
+ const address = String(pdaAddress || '').trim();
+ const endpoint = String(solanaEndpoint || '').trim();
+ if (!address) throw new Error('Не указан адрес PDA');
+ if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
+ const solana = await loadSolanaLib();
+ const connection = new solana.Connection(endpoint, 'confirmed');
+ const accountAddress = new solana.PublicKey(address);
+ const accountInfo = await connection.getAccountInfo(accountAddress, 'confirmed');
+ if (!accountInfo?.data) throw new Error(`PDA не найдена: ${address}`);
+ return {
+ ...parseShineUserPda(accountInfo.data),
+ userPda: accountAddress.toBase58(),
+ pdaAddress: accountAddress.toBase58(),
+ endpoint,
+ };
+}
+
+export async function readShineUserPdaByRef({ value, solanaEndpoint }) {
+ const ref = String(value || '').trim();
+ if (!ref) throw new Error('Не указан логин или адрес PDA');
+ try {
+ const bytes = base58ToBytes(ref);
+ if (bytes.length === 32) {
+ return await readShineUserPdaByAddress({ pdaAddress: ref, solanaEndpoint });
+ }
+ } catch {
+ // Если это не адрес, читаем как логин.
+ }
+ return readShineUserPda({ login: ref, solanaEndpoint });
+}
+
export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
const parsed = await readShineUserPda({ login, solanaEndpoint });
const bch = parsed.blockchain;
diff --git a/shine-UI/server-ui.html b/shine-UI/server-ui.html
index 9df708b..ca37ce6 100644
--- a/shine-UI/server-ui.html
+++ b/shine-UI/server-ui.html
@@ -14,21 +14,28 @@
Как это работает
diff --git a/shine-UI/server-ui/create-server-pda.html b/shine-UI/server-ui/create-server-pda.html
index 40cc3c3..ecffab0 100644
--- a/shine-UI/server-ui/create-server-pda.html
+++ b/shine-UI/server-ui/create-server-pda.html
@@ -38,6 +38,7 @@
Регистрация серверного аккаунта
diff --git a/shine-UI/server-ui/js/read-pda-page.js b/shine-UI/server-ui/js/read-pda-page.js
new file mode 100644
index 0000000..7cc8fb7
--- /dev/null
+++ b/shine-UI/server-ui/js/read-pda-page.js
@@ -0,0 +1,242 @@
+import { readShineUserPdaByRef } from '../../js/services/shine-user-pda-service.js';
+import { bytesToBase58 } from '../../js/services/crypto-utils.js';
+import { $, clearStatus, formatBigInt, formatTimestamp, setStatus } from './server-ui-shared.js';
+
+function hex(bytes) {
+ const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
+ return Array.from(data).map((x) => x.toString(16).padStart(2, '0')).join('');
+}
+
+function base58(bytes) {
+ const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
+ return bytesToBase58(data);
+}
+
+function renderRows(rows) {
+ const container = $('summaryRows');
+ container.innerHTML = '';
+ for (const row of rows) {
+ const el = document.createElement('div');
+ el.className = 'field-row';
+ const label = document.createElement('div');
+ label.className = 'field-label';
+ label.textContent = row.label;
+ const value = document.createElement('div');
+ value.className = `field-value${row.muted ? ' muted' : ''}`;
+ value.textContent = row.value;
+ el.append(label, value);
+ container.appendChild(el);
+ }
+}
+
+function renderMiniGrid(items) {
+ const wrap = document.createElement('div');
+ wrap.className = 'mini-grid';
+ for (const item of items) {
+ const node = document.createElement('div');
+ node.className = 'mini-item';
+ const label = document.createElement('div');
+ label.className = 'mini-label';
+ label.textContent = item.label;
+ const value = document.createElement('div');
+ value.className = 'mini-value';
+ value.textContent = item.value;
+ node.append(label, value);
+ wrap.appendChild(node);
+ }
+ return wrap;
+}
+
+function renderBlock(title, subtitle, items) {
+ const card = document.createElement('div');
+ card.className = 'block-card';
+ const ttl = document.createElement('div');
+ ttl.className = 'block-title';
+ ttl.textContent = title;
+ card.appendChild(ttl);
+ if (subtitle) {
+ const sub = document.createElement('div');
+ sub.className = 'block-subtitle';
+ sub.textContent = subtitle;
+ card.appendChild(sub);
+ }
+ card.appendChild(renderMiniGrid(items));
+ return card;
+}
+
+function renderArrayLine(values, emptyLabel = '—') {
+ const arr = Array.isArray(values) ? values : [];
+ return arr.length ? arr.join(', ') : emptyLabel;
+}
+
+function renderSessionList(sessions) {
+ const list = document.createElement('div');
+ list.className = 'details-wrap';
+ const details = document.createElement('details');
+ details.open = true;
+ const summary = document.createElement('summary');
+ summary.textContent = `Сессии (${Array.isArray(sessions) ? sessions.length : 0})`;
+ const body = document.createElement('div');
+ body.className = 'details-body';
+ if (!Array.isArray(sessions) || sessions.length === 0) {
+ body.innerHTML = '
Сессий нет.
';
+ } else {
+ const grid = document.createElement('div');
+ grid.className = 'block-list';
+ sessions.forEach((session, idx) => {
+ grid.appendChild(renderBlock(
+ `Session #${idx + 1}`,
+ `type=${session.sessionType}, version=${session.sessionVersion}`,
+ [
+ { label: 'Имя', value: session.sessionName || '—' },
+ { label: 'PubKey', value: base58(session.sessionPubKey32) },
+ ],
+ ));
+ });
+ body.appendChild(grid);
+ }
+ details.append(summary, body);
+ list.appendChild(details);
+ return list;
+}
+
+function renderParsed(parsed) {
+ $('summaryCard').style.display = 'block';
+ $('blocksCard').style.display = 'block';
+ $('rawCard').style.display = 'block';
+
+ renderRows([
+ { label: 'PDA адрес', value: parsed.pdaAddress },
+ { label: 'Логин', value: parsed.login },
+ { label: 'Статус', value: parsed.isServer ? 'server' : 'not server' },
+ { label: 'recordNumber', value: String(parsed.recordNumber) },
+ { label: 'createdAtMs', value: `${parsed.createdAtMs} · ${formatTimestamp(parsed.createdAtMs)}` },
+ { label: 'updatedAtMs', value: `${parsed.updatedAtMs} · ${formatTimestamp(parsed.updatedAtMs)}` },
+ { label: 'recordLen', value: String(parsed.recordLen) },
+ { label: 'prevRecordHash32', value: hex(parsed.prevRecordHash) },
+ { label: 'signature64', value: base58(parsed.signature) },
+ { label: 'unsignedBytesLen', value: String(parsed.unsignedBytes?.length || 0) },
+ ]);
+
+ const badgeLine = $('badgeLine');
+ badgeLine.innerHTML = '';
+ const badges = [
+ { label: `server=${parsed.isServer ? '1' : '0'}`, kind: parsed.isServer ? 'ok' : 'warn' },
+ { label: `trusted=${Number(parsed.trustedCount || 0)}`, kind: Number(parsed.trustedCount || 0) > 0 ? 'ok' : 'warn' },
+ { label: `sessions=${Array.isArray(parsed.sessions) ? parsed.sessions.length : 0}`, kind: 'info' },
+ { label: `access=${Array.isArray(parsed.accessServers) ? parsed.accessServers.length : 0}`, kind: 'info' },
+ { label: `sync=${Array.isArray(parsed.syncServers) ? parsed.syncServers.length : 0}`, kind: 'info' },
+ ];
+ badges.forEach((item) => {
+ const badge = document.createElement('span');
+ badge.className = `badge ${item.kind}`;
+ badge.textContent = item.label;
+ badgeLine.appendChild(badge);
+ });
+
+ const blocks = $('blocksList');
+ blocks.innerHTML = '';
+ blocks.appendChild(renderBlock('RecoveryKeyBlock', 'block_type=0', [
+ { label: 'recoveryKey32', value: base58(parsed.recoveryKey) },
+ ]));
+ blocks.appendChild(renderBlock('RootKeyBlock', 'block_type=1', [
+ { label: 'rootKey32', value: base58(parsed.rootKey) },
+ ]));
+ blocks.appendChild(renderBlock('ClientKeyBlock', 'block_type=2', [
+ { label: 'clientKey32', value: base58(parsed.clientKey) },
+ ]));
+ blocks.appendChild(renderBlock('BlockchainRegistryBlock', 'block_type=3', [
+ { label: 'blockchainType', value: String(parsed.blockchain?.blockchainType ?? 0) },
+ { label: 'blockchainName', value: parsed.blockchain?.blockchainName || '—' },
+ { label: 'blockchainPublicKey32', value: base58(parsed.blockchain?.blockchainPublicKey) },
+ { label: 'paidLimitBytes', value: formatBigInt(parsed.blockchain?.paidLimitBytes || 0n) },
+ { label: 'usedBytes', value: formatBigInt(parsed.blockchain?.usedBytes || 0n) },
+ { label: 'lastBlockNumber', value: String(parsed.blockchain?.lastBlockNumber ?? 0) },
+ { label: 'lastBlockHash32', value: hex(parsed.blockchain?.lastBlockHash) },
+ { label: 'lastBlockSignature64', value: base58(parsed.blockchain?.lastBlockSignature) },
+ { label: 'arweaveTxId', value: parsed.blockchain?.arweaveTxId || '—' },
+ ]));
+ blocks.appendChild(renderBlock('ServerProfileBlock', 'block_type=30', [
+ { label: 'isServer', value: parsed.isServer ? '1' : '0' },
+ { label: 'addressFormatType', value: String(parsed.addressFormatType ?? 0) },
+ { label: 'addressFormatVersion', value: String(parsed.addressFormatVersion ?? 0) },
+ { label: 'serverAddress', value: parsed.serverAddress || '—' },
+ { label: 'syncServersCount', value: String(parsed.syncServers?.length || 0) },
+ { label: 'syncServers', value: renderArrayLine(parsed.syncServers) },
+ ]));
+ blocks.appendChild(renderBlock('AccessServersBlock', 'block_type=40', [
+ { label: 'accessServersCount', value: String(parsed.accessServers?.length || 0) },
+ { label: 'accessServers', value: renderArrayLine(parsed.accessServers) },
+ ]));
+ blocks.appendChild(renderBlock('SessionsBlock', 'block_type=50', [
+ { label: 'sessionsMode', value: String(parsed.sessionsMode ?? 0) },
+ { label: 'sessionsCount', value: String(parsed.sessions?.length || 0) },
+ { label: 'sessions', value: parsed.sessions?.length ? 'ниже' : '—' },
+ ]));
+ blocks.appendChild(renderBlock('TrustedStateBlock', 'block_type=70', [
+ { label: 'trustedCount', value: String(parsed.trustedCount ?? 0) },
+ ]));
+ blocks.appendChild(renderSessionList(parsed.sessions));
+
+ $('rawJson').textContent = JSON.stringify({
+ pdaAddress: parsed.pdaAddress,
+ login: parsed.login,
+ isServer: parsed.isServer,
+ recordNumber: parsed.recordNumber,
+ createdAtMs: parsed.createdAtMs.toString(),
+ updatedAtMs: parsed.updatedAtMs.toString(),
+ recordLen: parsed.recordLen,
+ trustedCount: parsed.trustedCount,
+ addressFormatType: parsed.addressFormatType,
+ addressFormatVersion: parsed.addressFormatVersion,
+ serverAddress: parsed.serverAddress,
+ syncServers: parsed.syncServers,
+ accessServers: parsed.accessServers,
+ sessionsMode: parsed.sessionsMode,
+ sessions: parsed.sessions.map((s) => ({
+ sessionType: s.sessionType,
+ sessionVersion: s.sessionVersion,
+ sessionName: s.sessionName,
+ sessionPubKey32: base58(s.sessionPubKey32),
+ })),
+ recoveryKey: base58(parsed.recoveryKey),
+ rootKey: base58(parsed.rootKey),
+ clientKey: base58(parsed.clientKey),
+ blockchain: {
+ blockchainType: parsed.blockchain.blockchainType,
+ blockchainName: parsed.blockchain.blockchainName,
+ blockchainPublicKey: base58(parsed.blockchain.blockchainPublicKey),
+ paidLimitBytes: parsed.blockchain.paidLimitBytes.toString(),
+ usedBytes: parsed.blockchain.usedBytes.toString(),
+ lastBlockNumber: parsed.blockchain.lastBlockNumber,
+ lastBlockHash: hex(parsed.blockchain.lastBlockHash),
+ lastBlockSignature: base58(parsed.blockchain.lastBlockSignature),
+ arweaveTxId: parsed.blockchain.arweaveTxId || '',
+ },
+ }, null, 2);
+}
+
+$('btnLoad').addEventListener('click', async () => {
+ clearStatus($('status'));
+ $('btnLoad').disabled = true;
+ $('summaryCard').style.display = 'none';
+ $('blocksCard').style.display = 'none';
+ $('rawCard').style.display = 'none';
+ try {
+ const endpoint = String($('endpoint').value || '').trim();
+ const ref = String($('ref').value || '').trim();
+ if (!endpoint) throw new Error('Укажите Solana endpoint');
+ if (!ref) throw new Error('Укажите логин или адрес PDA');
+
+ setStatus($('status'), 'Чтение PDA...', 'info');
+ const parsed = await readShineUserPdaByRef({ value: ref, solanaEndpoint: endpoint });
+ renderParsed(parsed);
+ setStatus($('status'), 'PDA загружена.', 'success');
+ } catch (error) {
+ setStatus($('status'), error?.message || String(error), 'error');
+ } finally {
+ $('btnLoad').disabled = false;
+ }
+});
+
+document.body.dataset.ready = '1';
diff --git a/shine-UI/server-ui/read-pda.html b/shine-UI/server-ui/read-pda.html
new file mode 100644
index 0000000..08fcb27
--- /dev/null
+++ b/shine-UI/server-ui/read-pda.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+
Просмотр PDA — SHiNE Server Admin
+
+
+
+
+
+
+
+
Просмотр PDA
+
Читает user_pda или server PDA по логину либо по адресу и показывает все поля
+
+
+
Параметры Solana
+
+
+
+
+
+
+
+
Источник PDA
+
+
+
+
Если введён base58-адрес длиной 32 байта, он читается напрямую. Иначе значение трактуется как логин.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shine-UI/server-ui/update-server-pda.html b/shine-UI/server-ui/update-server-pda.html
index b4e9e5f..6b695ba 100644
--- a/shine-UI/server-ui/update-server-pda.html
+++ b/shine-UI/server-ui/update-server-pda.html
@@ -46,6 +46,7 @@
Обновление PDA сервера