Wallet plugin: офлайн wallet-session и выбор homeserver\n\nСделано:\n- wallet plugin сохраняет PDA-профиль и остаётся офлайн до действия;\n- добавлен каркас выбора ключа подписи и homeserver-устройства;\n- добавлен ручной refresh trusted devices через ListSessions;\n- на регистрации показан первый сервер SHiNE и его адрес;\n- обновлены pending notes для ручной проверки.\n\nЕщё не проверено / не доделано:\n- end-to-end ручная проверка plugin после этих правок не завершена;\n- signaling запроса подписи и ответ подписи ещё не реализованы;\n- локальный browser plugin нужно отдельно reload в Chrome/Opera.
This commit is contained in:
parent
f8a76bcd7f
commit
2225c2d173
@ -0,0 +1,17 @@
|
||||
# Wallet plugin: PDA-ключи и выбор homeserver
|
||||
|
||||
- краткое описание:
|
||||
`SHiNE-browser-plugin-wallet` после session-only подключения сохраняет `login`, публичные `root/device/blockchain` ключи из PDA и список опубликованных `homeserver`-сессий. Постоянное подключение не удерживается: plugin остаётся офлайн, а список trusted devices обновляет по запросу.
|
||||
|
||||
- что проверять:
|
||||
1. После успешного pairing plugin показывает сохранённую wallet-session без автоматического постоянного подключения.
|
||||
2. В карточке wallet-session виден сокращённый `deviceKey`.
|
||||
3. Кнопка `Обновить устройства` подтягивает homeserver-сессии из PDA и показывает их список со статусом `online/offline/unknown`.
|
||||
4. В селекте ключа подписи доступен `deviceKey (ed25519, ...)`.
|
||||
5. Кнопка `Запросить подпись` не падает и честно сообщает, что signaling подписи ещё не доделан.
|
||||
|
||||
- ожидаемый результат:
|
||||
кошелёк хранит PDA-метаданные локально, не висит всё время онлайн и показывает каркас выбора ключа и устройства перед будущим этапом signaling подписи.
|
||||
|
||||
- статус:
|
||||
pending
|
||||
@ -5,6 +5,7 @@ import {
|
||||
DEFAULT_SHINE_SERVER_LOGIN,
|
||||
buildHttpBase,
|
||||
normalizeServerLogin,
|
||||
readWalletProfileByLogin,
|
||||
resolveShineServerByServerLogin,
|
||||
} from './js/lib/shine-server-resolver.js';
|
||||
|
||||
@ -24,6 +25,12 @@ const state = {
|
||||
pollTimer: 0,
|
||||
activeSession: null,
|
||||
connectionOnline: false,
|
||||
walletProfile: null,
|
||||
signing: {
|
||||
selectedKeyId: 'device',
|
||||
selectedDeviceName: '',
|
||||
devicesResolvedAtMs: 0,
|
||||
},
|
||||
statusText: '',
|
||||
statusKind: 'info',
|
||||
};
|
||||
@ -80,6 +87,12 @@ async function loadStateFromStorage() {
|
||||
login: String(settings?.login || '').trim(),
|
||||
};
|
||||
state.activeSession = await loadSessionMaterial();
|
||||
state.walletProfile = state.activeSession?.walletProfile || null;
|
||||
state.signing = {
|
||||
selectedKeyId: String(state.activeSession?.selectedKeyId || 'device'),
|
||||
selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''),
|
||||
devicesResolvedAtMs: Number(state.activeSession?.devicesResolvedAtMs || 0),
|
||||
};
|
||||
}
|
||||
|
||||
async function persistSettings(nextSettings = {}) {
|
||||
@ -93,7 +106,89 @@ async function persistSettings(nextSettings = {}) {
|
||||
return state.settings;
|
||||
}
|
||||
|
||||
async function resumeActiveSession() {
|
||||
async function saveActiveSessionRecord() {
|
||||
if (!state.activeSession) return;
|
||||
const nextRecord = {
|
||||
...state.activeSession,
|
||||
walletProfile: state.walletProfile,
|
||||
selectedKeyId: state.signing.selectedKeyId,
|
||||
selectedDeviceName: state.signing.selectedDeviceName,
|
||||
devicesResolvedAtMs: state.signing.devicesResolvedAtMs,
|
||||
};
|
||||
state.activeSession = nextRecord;
|
||||
await saveSessionMaterial(nextRecord);
|
||||
}
|
||||
|
||||
function shortKey(value = '', size = 10) {
|
||||
const raw = String(value || '').trim();
|
||||
return raw ? raw.slice(0, size) : '';
|
||||
}
|
||||
|
||||
function buildSigningKeyOptions(walletProfile) {
|
||||
const deviceKey = String(walletProfile?.publicKeys?.deviceKeyBase58 || '').trim();
|
||||
if (!deviceKey) return [];
|
||||
return [{
|
||||
id: 'device',
|
||||
label: `deviceKey (ed25519, ${shortKey(deviceKey)})`,
|
||||
keyType: 'ed25519',
|
||||
publicKeyBase58: deviceKey,
|
||||
}];
|
||||
}
|
||||
|
||||
function mergeHomeserverStatuses(publishedHomeservers = [], serverSessions = []) {
|
||||
const published = Array.isArray(publishedHomeservers) ? publishedHomeservers : [];
|
||||
const homeserverSessions = Array.isArray(serverSessions)
|
||||
? serverSessions.filter((item) => Number(item?.sessionType || 0) === 100)
|
||||
: [];
|
||||
const onlineHomeservers = homeserverSessions.filter((item) => !!item?.onlineOnThisServer);
|
||||
|
||||
return published.map((item) => {
|
||||
let onlineState = 'unknown';
|
||||
if (published.length === 1) {
|
||||
onlineState = onlineHomeservers.length > 0 ? 'online' : 'offline';
|
||||
} else if (onlineHomeservers.length === 0) {
|
||||
onlineState = 'offline';
|
||||
} else if (onlineHomeservers.length === published.length) {
|
||||
onlineState = 'online';
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
onlineState,
|
||||
onlineLabel: onlineState === 'online' ? 'online' : onlineState === 'offline' ? 'offline' : 'unknown',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function hydrateWalletProfile(login) {
|
||||
const cleanLogin = String(login || state.activeSession?.login || state.settings.login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Нет логина для чтения PDA кошелька.');
|
||||
const profile = await readWalletProfileByLogin(cleanLogin);
|
||||
const signingKeyOptions = buildSigningKeyOptions(profile);
|
||||
const selectedKeyId = signingKeyOptions.some((item) => item.id === state.signing.selectedKeyId)
|
||||
? state.signing.selectedKeyId
|
||||
: (signingKeyOptions[0]?.id || '');
|
||||
const selectedDeviceName = state.signing.selectedDeviceName
|
||||
|| String(profile?.homeserverSessions?.[0]?.sessionName || '');
|
||||
|
||||
state.walletProfile = {
|
||||
...profile,
|
||||
signingKeyOptions,
|
||||
homeserverSessions: Array.isArray(profile.homeserverSessions) ? profile.homeserverSessions.map((item) => ({
|
||||
...item,
|
||||
onlineState: 'unknown',
|
||||
onlineLabel: 'unknown',
|
||||
})) : [],
|
||||
};
|
||||
state.signing = {
|
||||
...state.signing,
|
||||
selectedKeyId,
|
||||
selectedDeviceName,
|
||||
};
|
||||
await saveActiveSessionRecord();
|
||||
return state.walletProfile;
|
||||
}
|
||||
|
||||
async function resumeActiveSession({ keepConnected = false } = {}) {
|
||||
const sessionRecord = await loadSessionMaterial();
|
||||
state.activeSession = sessionRecord;
|
||||
if (!sessionRecord) {
|
||||
@ -110,8 +205,14 @@ async function resumeActiveSession() {
|
||||
login: String(sessionRecord?.login || state.settings.login || '').trim(),
|
||||
});
|
||||
const resumed = await ensureApi().resumeSession(sessionRecord);
|
||||
state.connectionOnline = true;
|
||||
setStatus(`Wallet-session активна для @${resumed.login}.`, 'info');
|
||||
state.connectionOnline = !!keepConnected;
|
||||
if (!keepConnected) {
|
||||
ensureApi().close();
|
||||
state.api = null;
|
||||
setStatus(`Wallet-session сохранена для @${resumed.login}. Подключение будет открываться только по действию.`, 'info');
|
||||
} else {
|
||||
setStatus(`Wallet-session активна для @${resumed.login}.`, 'info');
|
||||
}
|
||||
return { ok: true, connected: true, login: resumed.login, sessionId: resumed.sessionId };
|
||||
} catch (error) {
|
||||
state.connectionOnline = false;
|
||||
@ -142,13 +243,14 @@ async function attachApprovedSession(payload) {
|
||||
}
|
||||
|
||||
await clearSessionMaterial();
|
||||
await saveSessionMaterial(sessionRecord);
|
||||
state.activeSession = sessionRecord;
|
||||
await hydrateWalletProfile(login);
|
||||
await saveActiveSessionRecord();
|
||||
await persistSettings({
|
||||
login: sessionRecord.login,
|
||||
serverUrl: sessionRecord.serverUrl,
|
||||
});
|
||||
await resumeActiveSession();
|
||||
state.connectionOnline = false;
|
||||
}
|
||||
|
||||
async function pollPairingStatus() {
|
||||
@ -166,7 +268,7 @@ async function pollPairingStatus() {
|
||||
const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, state.requesterMaterial);
|
||||
await attachApprovedSession(decoded);
|
||||
clearPairingState();
|
||||
setStatus('Wallet-session создана и сохранена.', 'info');
|
||||
setStatus('Wallet-session создана и сохранена. Кошелёк остаётся офлайн до запроса подписи.', 'info');
|
||||
return;
|
||||
}
|
||||
if (stateValue === 'rejected') {
|
||||
@ -251,13 +353,87 @@ async function cancelPairing() {
|
||||
}
|
||||
|
||||
async function disconnectSession() {
|
||||
ensureApi().close();
|
||||
state.api = null;
|
||||
await clearSessionMaterial();
|
||||
state.activeSession = null;
|
||||
state.connectionOnline = false;
|
||||
state.walletProfile = null;
|
||||
state.signing = {
|
||||
selectedKeyId: 'device',
|
||||
selectedDeviceName: '',
|
||||
devicesResolvedAtMs: 0,
|
||||
};
|
||||
setStatus('Сохранённая wallet-session удалена из plugin.', 'info');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function refreshWalletDevices() {
|
||||
if (!state.activeSession?.login) {
|
||||
throw new Error('Сначала подключите wallet-session.');
|
||||
}
|
||||
await hydrateWalletProfile(state.activeSession.login);
|
||||
const resumed = await resumeActiveSession({ keepConnected: true });
|
||||
if (!resumed.ok) {
|
||||
throw new Error(resumed.error || 'Не удалось открыть wallet-session.');
|
||||
}
|
||||
try {
|
||||
const sessions = await ensureApi().listSessions();
|
||||
state.walletProfile = {
|
||||
...state.walletProfile,
|
||||
homeserverSessions: mergeHomeserverStatuses(state.walletProfile?.homeserverSessions, sessions),
|
||||
};
|
||||
state.signing.devicesResolvedAtMs = Date.now();
|
||||
if (!state.signing.selectedDeviceName && state.walletProfile.homeserverSessions[0]?.sessionName) {
|
||||
state.signing.selectedDeviceName = state.walletProfile.homeserverSessions[0].sessionName;
|
||||
}
|
||||
await saveActiveSessionRecord();
|
||||
setStatus('Список доверенных homeserver-устройств обновлён.', 'info');
|
||||
return {
|
||||
ok: true,
|
||||
devices: state.walletProfile.homeserverSessions,
|
||||
};
|
||||
} finally {
|
||||
ensureApi().close();
|
||||
state.api = null;
|
||||
state.connectionOnline = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSigningSelection({ selectedKeyId, selectedDeviceName } = {}) {
|
||||
state.signing = {
|
||||
...state.signing,
|
||||
selectedKeyId: String(selectedKeyId || state.signing.selectedKeyId || ''),
|
||||
selectedDeviceName: String(selectedDeviceName || state.signing.selectedDeviceName || ''),
|
||||
};
|
||||
await saveActiveSessionRecord();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function prepareSignSignal() {
|
||||
if (!state.activeSession?.login) {
|
||||
throw new Error('Сначала подключите wallet-session.');
|
||||
}
|
||||
if (!state.signing.selectedKeyId) {
|
||||
throw new Error('Не выбран ключ подписи.');
|
||||
}
|
||||
if (!state.signing.selectedDeviceName) {
|
||||
throw new Error('Не выбрано устройство homeserver.');
|
||||
}
|
||||
const selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName);
|
||||
if (!selectedDevice) {
|
||||
throw new Error('Выбранное устройство не найдено в PDA аккаунта.');
|
||||
}
|
||||
setStatus(
|
||||
`Каркас готов: запрос подписи должен идти через ${selectedDevice.sessionName}. Сам signaling подписи ещё не доделан.`,
|
||||
'info',
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
pending: true,
|
||||
};
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
return {
|
||||
settings: { ...state.settings },
|
||||
@ -270,6 +446,8 @@ function snapshot() {
|
||||
},
|
||||
session: state.activeSession ? { ...state.activeSession } : null,
|
||||
connectionOnline: state.connectionOnline,
|
||||
walletProfile: state.walletProfile ? { ...state.walletProfile } : null,
|
||||
signing: { ...state.signing },
|
||||
status: {
|
||||
text: state.statusText,
|
||||
kind: state.statusKind,
|
||||
@ -305,6 +483,21 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:refreshWalletDevices') {
|
||||
const result = await refreshWalletDevices();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:updateSigningSelection') {
|
||||
const result = await updateSigningSelection(message?.payload || {});
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:prepareSignSignal') {
|
||||
const result = await prepareSignSignal();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:disconnectSession') {
|
||||
const result = await disconnectSession();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
@ -318,6 +511,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
return true;
|
||||
});
|
||||
|
||||
void loadStateFromStorage().then(() => resumeActiveSession()).catch((error) => {
|
||||
void loadStateFromStorage().then(async () => {
|
||||
if (state.activeSession?.login) {
|
||||
await hydrateWalletProfile(state.activeSession.login).catch(() => {});
|
||||
setStatus(`Wallet-session сохранена для @${state.activeSession.login}. Подключение будет открываться только по действию.`, 'info');
|
||||
}
|
||||
}).catch((error) => {
|
||||
setStatus(error?.message || 'Не удалось инициализировать wallet plugin.', 'error');
|
||||
});
|
||||
|
||||
@ -69,6 +69,12 @@ export class ShineApiClient {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async listSessions() {
|
||||
const response = await this.ws.request('ListSessions', {});
|
||||
if (response.status !== 200) throw opError('ListSessions', response);
|
||||
return Array.isArray(response?.payload?.sessions) ? response.payload.sessions : [];
|
||||
}
|
||||
|
||||
async resumeSession(sessionRecord) {
|
||||
const login = String(sessionRecord?.login || '').trim();
|
||||
const sessionId = String(sessionRecord?.sessionId || '').trim();
|
||||
|
||||
@ -69,21 +69,32 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
||||
let isServer = false;
|
||||
let serverAddress = '';
|
||||
let accessServers = [];
|
||||
let rootKey32 = null;
|
||||
let deviceKey32 = null;
|
||||
let blockchainKey32 = null;
|
||||
let blockchainName = '';
|
||||
let homeserverSessions = [];
|
||||
|
||||
for (let i = 0; i < blocksCount; i += 1) {
|
||||
const blockType = readU8(bytes, cursorRef);
|
||||
cursorRef.value += 1; // block_version
|
||||
|
||||
if (blockType === 1 || blockType === 2) {
|
||||
cursorRef.value += 32;
|
||||
const key32 = readBytes(bytes, cursorRef, 32);
|
||||
if (blockType === 1) rootKey32 = key32;
|
||||
if (blockType === 2) deviceKey32 = key32;
|
||||
continue;
|
||||
}
|
||||
if (blockType === 3) {
|
||||
const count = readU8(bytes, cursorRef);
|
||||
for (let j = 0; j < count; j += 1) {
|
||||
cursorRef.value += 1;
|
||||
readStrU8(bytes, cursorRef);
|
||||
cursorRef.value += 32;
|
||||
const currentBlockchainName = readStrU8(bytes, cursorRef);
|
||||
const currentBlockchainKey32 = readBytes(bytes, cursorRef, 32);
|
||||
if (!blockchainKey32) {
|
||||
blockchainKey32 = currentBlockchainKey32;
|
||||
blockchainName = currentBlockchainName;
|
||||
}
|
||||
cursorRef.value += 8 + 8 + 4 + 32 + 64;
|
||||
const arPresent = readU8(bytes, cursorRef);
|
||||
if (arPresent === 1) readStrU8(bytes, cursorRef);
|
||||
@ -111,9 +122,19 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
||||
cursorRef.value += 1;
|
||||
const sessionsCount = readU8(bytes, cursorRef);
|
||||
for (let j = 0; j < sessionsCount; j += 1) {
|
||||
cursorRef.value += 1 + 1;
|
||||
readStrU8(bytes, cursorRef);
|
||||
cursorRef.value += 32;
|
||||
const sessionType = readU8(bytes, cursorRef);
|
||||
const sessionVersion = readU8(bytes, cursorRef);
|
||||
const sessionName = readStrU8(bytes, cursorRef);
|
||||
const sessionPubKey32 = readBytes(bytes, cursorRef, 32);
|
||||
if (sessionType === 100) {
|
||||
homeserverSessions.push({
|
||||
sessionType,
|
||||
sessionVersion,
|
||||
sessionName,
|
||||
sessionPubKeyBase58: new PublicKey(sessionPubKey32).toBase58(),
|
||||
sessionPubKeyB64: `ed25519/${btoa(String.fromCharCode(...sessionPubKey32))}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@ -128,6 +149,13 @@ function parseServerFieldsFromUserPda(dataBytes) {
|
||||
isServer,
|
||||
serverAddress: normalizeHostLike(serverAddress),
|
||||
accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean),
|
||||
publicKeys: {
|
||||
rootKeyBase58: rootKey32 ? new PublicKey(rootKey32).toBase58() : '',
|
||||
deviceKeyBase58: deviceKey32 ? new PublicKey(deviceKey32).toBase58() : '',
|
||||
blockchainKeyBase58: blockchainKey32 ? new PublicKey(blockchainKey32).toBase58() : '',
|
||||
blockchainName,
|
||||
},
|
||||
homeserverSessions,
|
||||
};
|
||||
}
|
||||
|
||||
@ -176,6 +204,17 @@ export async function resolveShineServerByServerLogin(serverLogin, solanaEndpoin
|
||||
};
|
||||
}
|
||||
|
||||
export async function readWalletProfileByLogin(login, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) {
|
||||
const cleanLogin = normalizeServerLogin(login);
|
||||
const parsed = await fetchUserPda(cleanLogin, solanaEndpoint);
|
||||
return {
|
||||
login: cleanLogin,
|
||||
accessServers: parsed.accessServers,
|
||||
publicKeys: parsed.publicKeys,
|
||||
homeserverSessions: parsed.homeserverSessions,
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
DEFAULT_SHINE_SERVER_ADDRESS,
|
||||
DEFAULT_SHINE_SERVER_LOGIN,
|
||||
|
||||
@ -88,7 +88,8 @@ body {
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
input[type="password"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #314459;
|
||||
@ -178,3 +179,33 @@ input[type="password"] {
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.device-row {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #243446;
|
||||
border-radius: 8px;
|
||||
background: #0d141d;
|
||||
}
|
||||
|
||||
.device-state {
|
||||
font-size: 12px;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.device-state-online {
|
||||
color: #b7f5ce;
|
||||
}
|
||||
|
||||
.device-state-offline {
|
||||
color: #ffc4cf;
|
||||
}
|
||||
|
||||
.device-state-unknown {
|
||||
color: #f8e2a0;
|
||||
}
|
||||
|
||||
@ -28,12 +28,36 @@
|
||||
<div class="summary-row"><span>Логин</span><strong id="session-login">—</strong></div>
|
||||
<div class="summary-row"><span>Session ID</span><code id="session-id">—</code></div>
|
||||
<div class="summary-row"><span>Тип</span><strong id="session-type">wallet</strong></div>
|
||||
<div class="summary-row"><span>deviceKey</span><code id="device-key-short">—</code></div>
|
||||
<div class="actions">
|
||||
<button id="resume-btn" class="btn secondary" type="button">Переподключить</button>
|
||||
<button id="resume-btn" class="btn secondary" type="button">Проверить session</button>
|
||||
<button id="refresh-devices-btn" class="btn secondary" type="button">Обновить устройства</button>
|
||||
<button id="disconnect-btn" class="btn danger" type="button">Отключить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="signing-card" class="card hidden">
|
||||
<div class="card-title">Подготовка подписи</div>
|
||||
<label class="field">
|
||||
<span>Ключ подписи</span>
|
||||
<select id="sign-key-select"></select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Устройство homeserver</span>
|
||||
<select id="device-select"></select>
|
||||
</label>
|
||||
<div id="homeserver-list" class="device-list"></div>
|
||||
<p class="muted small">
|
||||
Для выбора доступны homeserver-сессии, опубликованные в PDA аккаунта. Online-статус определяется без постоянного удержания соединения.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<button id="prepare-sign-btn" class="btn primary" type="button">Запросить подпись</button>
|
||||
</div>
|
||||
<p class="muted small">
|
||||
Сам signaling подтверждения подписи ещё не доделан. Сейчас доступен только каркас выбора ключа и устройства.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Войти через другое устройство</div>
|
||||
<label class="field">
|
||||
|
||||
@ -16,8 +16,15 @@ const els = {
|
||||
sessionLogin: document.querySelector('#session-login'),
|
||||
sessionId: document.querySelector('#session-id'),
|
||||
sessionType: document.querySelector('#session-type'),
|
||||
deviceKeyShort: document.querySelector('#device-key-short'),
|
||||
resumeBtn: document.querySelector('#resume-btn'),
|
||||
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
|
||||
disconnectBtn: document.querySelector('#disconnect-btn'),
|
||||
signingCard: document.querySelector('#signing-card'),
|
||||
signKeySelect: document.querySelector('#sign-key-select'),
|
||||
deviceSelect: document.querySelector('#device-select'),
|
||||
homeserverList: document.querySelector('#homeserver-list'),
|
||||
prepareSignBtn: document.querySelector('#prepare-sign-btn'),
|
||||
connectionPill: document.querySelector('#connection-pill'),
|
||||
};
|
||||
|
||||
@ -62,6 +69,34 @@ function formatRemaining(ms) {
|
||||
return `${minutes} мин ${seconds} сек`;
|
||||
}
|
||||
|
||||
function shortKey(value, size = 10) {
|
||||
const raw = String(value || '').trim();
|
||||
return raw ? `${raw.slice(0, size)}...` : '—';
|
||||
}
|
||||
|
||||
function renderHomeserverList(items = []) {
|
||||
els.homeserverList.innerHTML = '';
|
||||
if (!items.length) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'muted small';
|
||||
empty.textContent = 'В PDA пока нет опубликованных homeserver-сессий.';
|
||||
els.homeserverList.append(empty);
|
||||
return;
|
||||
}
|
||||
items.forEach((item) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'summary-row device-row';
|
||||
const label = document.createElement('span');
|
||||
label.textContent = `${item.sessionName} (${shortKey(item.sessionPubKeyBase58, 8)})`;
|
||||
const badge = document.createElement('strong');
|
||||
const stateValue = String(item.onlineState || 'unknown');
|
||||
badge.textContent = stateValue;
|
||||
badge.className = `device-state device-state-${stateValue}`;
|
||||
row.append(label, badge);
|
||||
els.homeserverList.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
function applyState(nextState) {
|
||||
state = nextState || state;
|
||||
const serverValue = String(state?.settings?.serverLogin || 'shineupme');
|
||||
@ -78,18 +113,46 @@ function applyState(nextState) {
|
||||
setStatus(state?.status?.text || '', state?.status?.kind || 'info');
|
||||
|
||||
const session = state?.session;
|
||||
const walletProfile = state?.walletProfile;
|
||||
const signing = state?.signing || {};
|
||||
if (session) {
|
||||
els.sessionCard.classList.remove('hidden');
|
||||
els.sessionLogin.textContent = session.login || '—';
|
||||
els.sessionId.textContent = session.sessionId || '—';
|
||||
els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—');
|
||||
els.deviceKeyShort.textContent = shortKey(walletProfile?.publicKeys?.deviceKeyBase58 || '');
|
||||
els.signingCard.classList.remove('hidden');
|
||||
} else {
|
||||
els.sessionCard.classList.add('hidden');
|
||||
els.sessionLogin.textContent = '—';
|
||||
els.sessionId.textContent = '—';
|
||||
els.sessionType.textContent = 'wallet';
|
||||
els.deviceKeyShort.textContent = '—';
|
||||
els.signingCard.classList.add('hidden');
|
||||
}
|
||||
|
||||
const signKeyOptions = Array.isArray(walletProfile?.signingKeyOptions) ? walletProfile.signingKeyOptions : [];
|
||||
els.signKeySelect.innerHTML = '';
|
||||
signKeyOptions.forEach((item) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.id;
|
||||
option.textContent = item.label;
|
||||
option.selected = item.id === signing.selectedKeyId;
|
||||
els.signKeySelect.append(option);
|
||||
});
|
||||
|
||||
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
|
||||
els.deviceSelect.innerHTML = '';
|
||||
homeservers.forEach((item) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.sessionName;
|
||||
option.textContent = `${item.sessionName} [${item.onlineState || 'unknown'}]`;
|
||||
option.selected = item.sessionName === signing.selectedDeviceName;
|
||||
els.deviceSelect.append(option);
|
||||
});
|
||||
renderHomeserverList(homeservers);
|
||||
els.prepareSignBtn.disabled = !session || !signing.selectedKeyId || !signing.selectedDeviceName;
|
||||
|
||||
const pairing = state?.pairing || {};
|
||||
if (pairing.active) {
|
||||
els.pairingCard.classList.remove('hidden');
|
||||
@ -201,6 +264,35 @@ async function disconnectSession() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshDevices() {
|
||||
setStatus('Обновляем trusted homeserver-устройства...', 'info');
|
||||
try {
|
||||
await sendMessage('wallet:refreshWalletDevices');
|
||||
} catch (error) {
|
||||
setStatus(error.message || 'Не удалось обновить список устройств.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSigningSelection() {
|
||||
try {
|
||||
await sendMessage('wallet:updateSigningSelection', {
|
||||
selectedKeyId: String(els.signKeySelect.value || '').trim(),
|
||||
selectedDeviceName: String(els.deviceSelect.value || '').trim(),
|
||||
});
|
||||
} catch (error) {
|
||||
setStatus(error.message || 'Не удалось обновить выбор для подписи.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareSignSignal() {
|
||||
setStatus('Готовим каркас запроса подписи...', 'info');
|
||||
try {
|
||||
await sendMessage('wallet:prepareSignSignal');
|
||||
} catch (error) {
|
||||
setStatus(error.message || 'Не удалось подготовить запрос подписи.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function startUiRefreshLoop() {
|
||||
stopUiRefreshLoop();
|
||||
refreshTimer = window.setInterval(() => {
|
||||
@ -229,7 +321,11 @@ function bindUi() {
|
||||
els.startBtn.addEventListener('click', () => { void startPairing(); });
|
||||
els.cancelBtn.addEventListener('click', () => { void cancelPairing(); });
|
||||
els.resumeBtn.addEventListener('click', () => { void resumeSession(); });
|
||||
els.refreshDevicesBtn.addEventListener('click', () => { void refreshDevices(); });
|
||||
els.disconnectBtn.addEventListener('click', () => { void disconnectSession(); });
|
||||
els.signKeySelect.addEventListener('change', () => { void updateSigningSelection(); });
|
||||
els.deviceSelect.addEventListener('change', () => { void updateSigningSelection(); });
|
||||
els.prepareSignBtn.addEventListener('click', () => { void prepareSignSignal(); });
|
||||
}
|
||||
|
||||
async function init() {
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
client.version=1.2.206
|
||||
server.version=1.2.195
|
||||
client.version=1.2.207
|
||||
server.version=1.2.196
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<link rel="manifest" href="./manifest.webmanifest" />
|
||||
<title>Shine UI Demo</title>
|
||||
<script>
|
||||
window.__SHINE_BUILD_HASH__ = '20260616124000';
|
||||
window.__SHINE_BUILD_HASH__ = '20260616130000';
|
||||
window.__SHINE_CLIENT_VERSION__ = '1.2.8';
|
||||
</script>
|
||||
<script>
|
||||
|
||||
@ -41,6 +41,16 @@ export function render({ navigate }) {
|
||||
statusText.className = 'meta-muted';
|
||||
statusText.textContent = 'Проверка логина: не выполнена';
|
||||
|
||||
const serverNotice = document.createElement('div');
|
||||
serverNotice.className = 'card stack';
|
||||
serverNotice.innerHTML = `
|
||||
<p class="field-label">Первый сервер SHiNE</p>
|
||||
<p class="meta-muted">Сейчас вашим первым и основным сервером будет серверный аккаунт <strong>${state.entrySettings.shineServerLogin || 'shineupme'}</strong>.</p>
|
||||
<p class="meta-muted">Текущий адрес этого сервера: <strong>${state.entrySettings.shineServerHttp || 'https://shineup.me'}</strong>.</p>
|
||||
<p class="meta-muted">При регистрации этот сервер будет записан в вашу PDA как первый сервер доступа.</p>
|
||||
<p class="meta-muted">При желании его можно изменить в настройках до регистрации или позже. В будущем будет поддерживаться мультисерверная модель, но сейчас используется один основной сервер.</p>
|
||||
`;
|
||||
|
||||
const formError = document.createElement('p');
|
||||
formError.className = 'status-line is-unavailable';
|
||||
formError.style.display = 'none';
|
||||
@ -201,7 +211,7 @@ export function render({ navigate }) {
|
||||
`;
|
||||
form.children[0].append(loginInput);
|
||||
form.children[1].append(passwordInput);
|
||||
form.append(checkButton, statusText, advanced, formError);
|
||||
form.append(serverNotice, checkButton, statusText, advanced, formError);
|
||||
actions.innerHTML = '';
|
||||
actions.append(backButton, nextButton);
|
||||
backButton.disabled = false;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user