Исправить подключение и подпись в браузерном кошельке
This commit is contained in:
parent
23e61cc182
commit
684f3237cf
@ -30,6 +30,7 @@ const state = {
|
||||
devicesResolvedAtMs: 0,
|
||||
},
|
||||
currentWallet: null,
|
||||
pendingApproval: null,
|
||||
statusText: '',
|
||||
statusKind: 'info',
|
||||
};
|
||||
@ -69,6 +70,41 @@ function setStatus(message = '', kind = 'info') {
|
||||
state.statusKind = kind === 'error' ? 'error' : 'info';
|
||||
}
|
||||
|
||||
function makePendingApprovalSnapshot(payload = {}) {
|
||||
return {
|
||||
id: String(payload?.id || '').trim(),
|
||||
kind: String(payload?.kind || 'sign_transaction').trim() || 'sign_transaction',
|
||||
origin: String(payload?.origin || '').trim(),
|
||||
publicKeyBase58: String(payload?.publicKeyBase58 || '').trim(),
|
||||
comment: String(payload?.comment || '').trim(),
|
||||
createdAtMs: Number(payload?.createdAtMs || Date.now()),
|
||||
transactionSummary: payload?.transactionSummary && typeof payload.transactionSummary === 'object'
|
||||
? { ...payload.transactionSummary }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function clearPendingApproval({ rejectError = null } = {}) {
|
||||
if (!state.pendingApproval) return;
|
||||
const pending = state.pendingApproval;
|
||||
state.pendingApproval = null;
|
||||
if (pending.timeoutId) {
|
||||
clearTimeout(pending.timeoutId);
|
||||
}
|
||||
if (rejectError && pending.abortController) {
|
||||
try {
|
||||
pending.abortController.abort(rejectError);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
async function openSidePanelForSender(sender) {
|
||||
if (!chrome.sidePanel?.open || !sender?.tab?.id) return;
|
||||
try {
|
||||
await chrome.sidePanel.open({ tabId: sender.tab.id });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
if (state.pollTimer) {
|
||||
clearTimeout(state.pollTimer);
|
||||
@ -506,7 +542,7 @@ async function resolveSelectedHomeserverSession() {
|
||||
return selectedDevice;
|
||||
}
|
||||
|
||||
async function callWalletRpc(requestData, timeoutMs = 8000) {
|
||||
async function callWalletRpc(requestData, timeoutMs = 8000, abortSignal = null) {
|
||||
const selectedDevice = await resolveSelectedHomeserverSession();
|
||||
const resumed = await resumeActiveSession({ keepConnected: true });
|
||||
if (!resumed.ok) {
|
||||
@ -523,22 +559,46 @@ async function callWalletRpc(requestData, timeoutMs = 8000) {
|
||||
|
||||
try {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
let settled = false;
|
||||
let timeoutId = 0;
|
||||
let off = () => {};
|
||||
let removeAbortListener = () => {};
|
||||
const cleanup = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
off();
|
||||
removeAbortListener();
|
||||
};
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error('Таймаут ответа от ESP32.'));
|
||||
}, timeoutMs);
|
||||
const off = ensureApi().onEvent('IncomingCallSignal', (evt) => {
|
||||
off = ensureApi().onEvent('IncomingCallSignal', (evt) => {
|
||||
const eventPayload = evt?.payload || {};
|
||||
if (String(eventPayload?.callId || '') !== callId) return;
|
||||
if (Number(eventPayload?.type || 0) !== WALLET_RPC_RESPONSE_TYPE) return;
|
||||
clearTimeout(timeoutId);
|
||||
off();
|
||||
cleanup();
|
||||
try {
|
||||
resolve(JSON.parse(String(eventPayload?.data || '{}')));
|
||||
} catch {
|
||||
reject(new Error('ESP32 вернул некорректный JSON.'));
|
||||
}
|
||||
});
|
||||
if (abortSignal) {
|
||||
const onAbort = () => {
|
||||
cleanup();
|
||||
reject(abortSignal.reason instanceof Error ? abortSignal.reason : new Error('Ожидание подписи отменено.'));
|
||||
};
|
||||
if (abortSignal.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
abortSignal.addEventListener('abort', onAbort, { once: true });
|
||||
removeAbortListener = () => {
|
||||
abortSignal.removeEventListener('abort', onAbort);
|
||||
};
|
||||
}
|
||||
ensureApi().callSignalToSession({
|
||||
toLogin: state.activeSession.login,
|
||||
targetSessionId: selectedDevice.activeSessionId,
|
||||
@ -546,8 +606,7 @@ async function callWalletRpc(requestData, timeoutMs = 8000) {
|
||||
type: WALLET_RPC_REQUEST_TYPE,
|
||||
data: JSON.stringify(payload),
|
||||
}).catch((error) => {
|
||||
clearTimeout(timeoutId);
|
||||
off();
|
||||
cleanup();
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
@ -606,6 +665,45 @@ async function requestCurrentWallet() {
|
||||
return { ok: true, wallet: state.currentWallet };
|
||||
}
|
||||
|
||||
async function cancelPendingSiteApproval() {
|
||||
clearPendingApproval({
|
||||
rejectError: makeCodeError('User canceled request in extension.', 'USER_REJECTED'),
|
||||
});
|
||||
setStatus('Ожидание подписи отменено в расширении.', 'info');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function markPendingSiteApprovalResolved() {
|
||||
clearPendingApproval();
|
||||
}
|
||||
|
||||
async function beginSiteTransactionFlow(payload = {}, sender = null) {
|
||||
if (state.pendingApproval) {
|
||||
throw makeCodeError('Another signing request is already pending.', 'APPROVAL_ALREADY_PENDING');
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
const pending = makePendingApprovalSnapshot({
|
||||
...payload,
|
||||
kind: 'sign_transaction',
|
||||
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||
createdAtMs: Date.now(),
|
||||
});
|
||||
const timeoutId = setTimeout(() => {
|
||||
clearPendingApproval({
|
||||
rejectError: makeCodeError('Signing request timed out in extension.', 'USER_REJECTED'),
|
||||
});
|
||||
setStatus('Ожидание подписи истекло в расширении.', 'error');
|
||||
}, 120000);
|
||||
state.pendingApproval = {
|
||||
...pending,
|
||||
timeoutId,
|
||||
abortController,
|
||||
};
|
||||
setStatus(`Сайт ${pending.origin} запросил подпись. Подтвердите или отмените на доверенном устройстве.`, 'info');
|
||||
await openSidePanelForSender(sender);
|
||||
return pending;
|
||||
}
|
||||
|
||||
async function siteConnect({ origin, onlyIfTrusted = false } = {}) {
|
||||
const normalizedOrigin = normalizeOrigin(origin);
|
||||
if (!normalizedOrigin) {
|
||||
@ -636,7 +734,7 @@ async function siteDisconnect({ origin } = {}) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64, comment } = {}) {
|
||||
async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64, comment, transactionSummary } = {}, sender = null) {
|
||||
const normalizedOrigin = normalizeOrigin(origin);
|
||||
if (!normalizedOrigin) {
|
||||
throw makeCodeError('Site origin is missing.', 'BAD_ORIGIN');
|
||||
@ -649,8 +747,15 @@ async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64,
|
||||
if (!cleanPub || !cleanTx) {
|
||||
throw makeCodeError('Transaction payload is incomplete.', 'BAD_REQUEST');
|
||||
}
|
||||
const pending = await beginSiteTransactionFlow({
|
||||
origin: normalizedOrigin,
|
||||
publicKeyBase58: cleanPub,
|
||||
comment: String(comment || '').trim(),
|
||||
transactionSummary: transactionSummary || null,
|
||||
}, sender);
|
||||
const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`;
|
||||
try {
|
||||
const { response } = await callWalletRpc({
|
||||
v: 1,
|
||||
operation: 'sign_transaction',
|
||||
@ -658,7 +763,7 @@ async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64,
|
||||
publicKeyBase58: cleanPub,
|
||||
transactionBase64: cleanTx,
|
||||
comment: signComment,
|
||||
}, 120000);
|
||||
}, 120000, state.pendingApproval?.id === pending.id ? state.pendingApproval.abortController.signal : null);
|
||||
if (!response?.ok) {
|
||||
const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase();
|
||||
if (errorCode === 'REJECTED_BY_USER') {
|
||||
@ -666,12 +771,16 @@ async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64,
|
||||
}
|
||||
throw makeCodeError(`ESP32 rejected transaction signature: ${String(response?.error || 'unknown_error')}`, errorCode || 'RPC_REJECTED');
|
||||
}
|
||||
setStatus(`Подпись для ${normalizedOrigin} завершена.`, 'info');
|
||||
return {
|
||||
ok: true,
|
||||
publicKeyBase58: String(response?.publicKeyBase58 || cleanPub).trim(),
|
||||
signedTransactionBase64: String(response?.signedTransactionBase64 || '').trim(),
|
||||
signatureBase58: String(response?.signatureBase58 || '').trim(),
|
||||
};
|
||||
} finally {
|
||||
await markPendingSiteApprovalResolved();
|
||||
}
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
@ -688,6 +797,7 @@ function snapshot() {
|
||||
connectionOnline: !!state.activeSession,
|
||||
walletProfile: state.walletProfile ? { ...state.walletProfile } : null,
|
||||
currentWallet: state.currentWallet ? { ...state.currentWallet } : null,
|
||||
pendingApproval: state.pendingApproval ? makePendingApprovalSnapshot(state.pendingApproval) : null,
|
||||
signing: { ...state.signing },
|
||||
status: {
|
||||
text: state.statusText,
|
||||
@ -749,6 +859,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:cancelPendingSiteApproval') {
|
||||
const result = await cancelPendingSiteApproval();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:siteConnect') {
|
||||
const result = await siteConnect(message?.payload || {});
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
@ -760,7 +875,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:siteSignTransaction') {
|
||||
const result = await siteSignTransaction(message?.payload || {});
|
||||
const result = await siteSignTransaction(message?.payload || {}, _sender);
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const PAGE_REQUEST = 'shine-wallet-page-request';
|
||||
const PAGE_RESPONSE = 'shine-wallet-page-response';
|
||||
const PAGE_MESSAGE_TARGET_ORIGIN = '*';
|
||||
|
||||
function injectProviderBridge() {
|
||||
const root = document.head || document.documentElement;
|
||||
@ -20,14 +21,21 @@ function respondToPage(id, ok, result, error, code) {
|
||||
result: result || null,
|
||||
error: error ? String(error) : '',
|
||||
code: code ? String(code) : '',
|
||||
}, window.location.origin);
|
||||
}, PAGE_MESSAGE_TARGET_ORIGIN);
|
||||
}
|
||||
|
||||
function sendRuntimeMessage(type, payload = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ type, payload }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message || 'Runtime message failed'));
|
||||
const raw = String(chrome.runtime.lastError.message || 'Runtime message failed');
|
||||
if (/Extension context invalidated/i.test(raw)) {
|
||||
const error = new Error('Расширение было перезагружено или отключено. Обновите страницу и откройте кошелёк заново.');
|
||||
error.code = 'EXTENSION_CONTEXT_INVALIDATED';
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
reject(new Error(raw));
|
||||
return;
|
||||
}
|
||||
if (!response?.ok) {
|
||||
@ -71,6 +79,7 @@ window.addEventListener('message', (event) => {
|
||||
publicKeyBase58: String(params?.publicKeyBase58 || '').trim(),
|
||||
transactionBase64: String(params?.transactionBase64 || '').trim(),
|
||||
comment: String(params?.comment || '').trim(),
|
||||
transactionSummary: params?.transactionSummary || null,
|
||||
});
|
||||
respondToPage(id, true, response.result || null);
|
||||
return;
|
||||
|
||||
@ -198,6 +198,12 @@ select {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.device-row {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #243446;
|
||||
@ -205,6 +211,30 @@ select {
|
||||
background: #0d141d;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #243446;
|
||||
border-radius: 8px;
|
||||
background: #0d141d;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: #9aabbd;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #e8eef6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.detail-value.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
color: #bed5f5;
|
||||
}
|
||||
|
||||
.device-state {
|
||||
font-size: 12px;
|
||||
text-transform: lowercase;
|
||||
|
||||
@ -18,7 +18,6 @@
|
||||
</div>
|
||||
|
||||
<p id="server-login-info" class="muted small">Сервер SHiNE: —</p>
|
||||
<p id="server-address" class="muted small">Адрес: —</p>
|
||||
|
||||
<div id="connect-card" class="card">
|
||||
<div class="card-title">Подключение</div>
|
||||
@ -50,9 +49,6 @@
|
||||
<div id="session-card" class="card hidden">
|
||||
<div class="card-title">Подключено</div>
|
||||
<div class="summary-row"><span>Логин</span><strong id="session-login">—</strong></div>
|
||||
<div class="summary-row"><span>Session</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>clientKey</span><code id="client-key-short">—</code></div>
|
||||
<div class="actions">
|
||||
<button id="resume-btn" class="btn secondary" type="button">Проверить session</button>
|
||||
<button id="disconnect-btn" class="btn danger" type="button">Отключить</button>
|
||||
@ -72,6 +68,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pending-approval-card" class="card hidden">
|
||||
<div class="card-title">Ожидается подпись</div>
|
||||
<p id="pending-approval-subtitle" class="muted small">Сайт запросил подписание транзакции.</p>
|
||||
<div id="pending-approval-details" class="device-list"></div>
|
||||
<p class="muted small">Запрос уже отправлен на доверенное устройство. Здесь можно только отменить ожидание.</p>
|
||||
<div class="actions">
|
||||
<button id="cancel-pending-approval-btn" class="btn danger" type="button">Отменить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="wallet-result-card" class="card hidden">
|
||||
<div class="card-title">Полученный кошелёк</div>
|
||||
<div class="summary-row"><span>Тип</span><strong id="wallet-type">—</strong></div>
|
||||
|
||||
@ -2,7 +2,6 @@ import { formatPairingShortCode } from './js/lib/device-pairing.js';
|
||||
|
||||
const els = {
|
||||
serverLoginInfo: document.querySelector('#server-login-info'),
|
||||
serverAddress: document.querySelector('#server-address'),
|
||||
loginInput: document.querySelector('#login-input'),
|
||||
usePassword: document.querySelector('#use-password'),
|
||||
passwordField: document.querySelector('#password-field'),
|
||||
@ -17,9 +16,6 @@ const els = {
|
||||
status: document.querySelector('#status'),
|
||||
sessionCard: document.querySelector('#session-card'),
|
||||
sessionLogin: document.querySelector('#session-login'),
|
||||
sessionId: document.querySelector('#session-id'),
|
||||
sessionType: document.querySelector('#session-type'),
|
||||
clientKeyShort: document.querySelector('#client-key-short'),
|
||||
resumeBtn: document.querySelector('#resume-btn'),
|
||||
refreshDevicesBtn: document.querySelector('#refresh-devices-btn'),
|
||||
disconnectBtn: document.querySelector('#disconnect-btn'),
|
||||
@ -27,6 +23,10 @@ const els = {
|
||||
deviceSelect: document.querySelector('#device-select'),
|
||||
homeserverList: document.querySelector('#homeserver-list'),
|
||||
requestWalletBtn: document.querySelector('#request-wallet-btn'),
|
||||
pendingApprovalCard: document.querySelector('#pending-approval-card'),
|
||||
pendingApprovalSubtitle: document.querySelector('#pending-approval-subtitle'),
|
||||
pendingApprovalDetails: document.querySelector('#pending-approval-details'),
|
||||
cancelPendingApprovalBtn: document.querySelector('#cancel-pending-approval-btn'),
|
||||
walletResultCard: document.querySelector('#wallet-result-card'),
|
||||
walletType: document.querySelector('#wallet-type'),
|
||||
walletPubkey: document.querySelector('#wallet-pubkey'),
|
||||
@ -52,6 +52,7 @@ let state = {
|
||||
selectedDeviceName: '',
|
||||
},
|
||||
currentWallet: null,
|
||||
pendingApproval: null,
|
||||
status: {
|
||||
text: '',
|
||||
kind: 'info',
|
||||
@ -107,13 +108,49 @@ function renderHomeserverList(items = []) {
|
||||
});
|
||||
}
|
||||
|
||||
function renderPendingApproval(pendingApproval) {
|
||||
els.pendingApprovalDetails.innerHTML = '';
|
||||
if (!pendingApproval) return;
|
||||
const summary = pendingApproval.transactionSummary || {};
|
||||
const programs = Array.isArray(summary.programs) && summary.programs.length
|
||||
? summary.programs.join(', ')
|
||||
: 'не определены';
|
||||
const details = [
|
||||
{ label: 'Сайт', value: pendingApproval.origin || '—', mono: true },
|
||||
{ label: 'Кошелёк', value: pendingApproval.publicKeyBase58 || '—', mono: true },
|
||||
{ label: 'Комментарий', value: pendingApproval.comment || 'Транзакция запрошена сайтом' },
|
||||
{ label: 'Тип', value: summary.kind || 'legacy' },
|
||||
{ label: 'Инструкций', value: String(summary.instructionCount ?? 0) },
|
||||
{ label: 'Программы', value: programs, mono: true },
|
||||
];
|
||||
if (summary.feePayer) {
|
||||
details.push({ label: 'Fee payer', value: summary.feePayer, mono: true });
|
||||
}
|
||||
if (summary.recentBlockhash) {
|
||||
details.push({ label: 'Blockhash', value: summary.recentBlockhash, mono: true });
|
||||
}
|
||||
for (const item of details) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'detail-row';
|
||||
const label = document.createElement('div');
|
||||
label.className = 'detail-label';
|
||||
label.textContent = item.label;
|
||||
const value = document.createElement('div');
|
||||
value.className = `detail-value${item.mono ? ' mono' : ''}`;
|
||||
value.textContent = item.value;
|
||||
row.append(label, value);
|
||||
els.pendingApprovalDetails.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
function applyState(nextState) {
|
||||
state = nextState || state;
|
||||
const loginValue = String(state?.settings?.login || '');
|
||||
const resolvedServerLogin = String(state?.settings?.serverLogin || '').trim();
|
||||
const resolvedServerAddress = String(state?.settings?.serverHttp || '').trim();
|
||||
els.serverLoginInfo.textContent = resolvedServerLogin ? `Сервер SHiNE: ${resolvedServerLogin}` : 'Сервер SHiNE: —';
|
||||
els.serverAddress.textContent = resolvedServerAddress ? `Адрес: ${resolvedServerAddress}` : 'Адрес: —';
|
||||
els.serverLoginInfo.textContent = resolvedServerLogin && resolvedServerAddress
|
||||
? `Сервер SHiNE: ${resolvedServerLogin} (${resolvedServerAddress})`
|
||||
: 'Сервер SHiNE: —';
|
||||
if (document.activeElement !== els.loginInput) {
|
||||
els.loginInput.value = loginValue;
|
||||
}
|
||||
@ -125,16 +162,15 @@ function applyState(nextState) {
|
||||
const walletProfile = state?.walletProfile;
|
||||
const signing = state?.signing || {};
|
||||
const currentWallet = state?.currentWallet || null;
|
||||
const pendingApproval = state?.pendingApproval || null;
|
||||
|
||||
els.connectCard.classList.toggle('hidden', !!session);
|
||||
els.sessionCard.classList.toggle('hidden', !session);
|
||||
els.walletCard.classList.toggle('hidden', !session);
|
||||
els.pendingApprovalCard.classList.toggle('hidden', !pendingApproval);
|
||||
|
||||
if (session) {
|
||||
els.sessionLogin.textContent = session.login || '—';
|
||||
els.sessionId.textContent = session.sessionId || '—';
|
||||
els.sessionType.textContent = String(session.sessionType || 50) === '50' ? 'wallet' : String(session.sessionType || '—');
|
||||
els.clientKeyShort.textContent = shortKey(walletProfile?.publicKeys?.clientKeyBase58 || '');
|
||||
}
|
||||
|
||||
const homeservers = Array.isArray(walletProfile?.homeserverSessions) ? walletProfile.homeserverSessions : [];
|
||||
@ -149,6 +185,16 @@ function applyState(nextState) {
|
||||
renderHomeserverList(homeservers);
|
||||
els.requestWalletBtn.disabled = !session || !signing.selectedDeviceName;
|
||||
|
||||
if (pendingApproval) {
|
||||
els.pendingApprovalSubtitle.textContent = pendingApproval.origin
|
||||
? `Сайт ${pendingApproval.origin} запросил подписание транзакции.`
|
||||
: 'Сайт запросил подписание транзакции.';
|
||||
renderPendingApproval(pendingApproval);
|
||||
} else {
|
||||
els.pendingApprovalSubtitle.textContent = 'Сайт запросил подписание транзакции.';
|
||||
els.pendingApprovalDetails.innerHTML = '';
|
||||
}
|
||||
|
||||
if (currentWallet?.publicKeyBase58) {
|
||||
els.walletResultCard.classList.remove('hidden');
|
||||
els.walletType.textContent = currentWallet.type || '—';
|
||||
@ -313,6 +359,14 @@ async function copyWalletKey() {
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelPendingApproval() {
|
||||
try {
|
||||
await sendMessage('wallet:cancelPendingSiteApproval');
|
||||
} catch (error) {
|
||||
setStatus(error.message || 'Не удалось отменить ожидание подписи.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function startUiRefreshLoop() {
|
||||
stopUiRefreshLoop();
|
||||
refreshTimer = window.setInterval(() => {
|
||||
@ -347,6 +401,7 @@ function bindUi() {
|
||||
els.deviceSelect.addEventListener('change', () => { void updateDeviceSelection(); });
|
||||
els.requestWalletBtn.addEventListener('click', () => { void requestCurrentWallet(); });
|
||||
els.copyWalletBtn.addEventListener('click', () => { void copyWalletKey(); });
|
||||
els.cancelPendingApprovalBtn.addEventListener('click', () => { void cancelPendingApproval(); });
|
||||
}
|
||||
|
||||
async function init() {
|
||||
|
||||
@ -2,6 +2,7 @@ import { PublicKey, Transaction, VersionedTransaction } from './js/lib/vendor/so
|
||||
|
||||
const PAGE_REQUEST = 'shine-wallet-page-request';
|
||||
const PAGE_RESPONSE = 'shine-wallet-page-response';
|
||||
const PAGE_MESSAGE_TARGET_ORIGIN = '*';
|
||||
const STANDARD_REGISTER_EVENT = 'wallet-standard:register-wallet';
|
||||
const STANDARD_APP_READY_EVENT = 'wallet-standard:app-ready';
|
||||
const SOLANA_CHAINS = ['solana:mainnet', 'solana:devnet', 'solana:testnet'];
|
||||
@ -39,6 +40,45 @@ function createProviderError(message, code = '') {
|
||||
return error;
|
||||
}
|
||||
|
||||
function summarizeTransaction(transaction) {
|
||||
const summary = {
|
||||
kind: 'legacy',
|
||||
instructionCount: 0,
|
||||
accountCount: 0,
|
||||
feePayer: '',
|
||||
recentBlockhash: '',
|
||||
programs: [],
|
||||
};
|
||||
if (!transaction) return summary;
|
||||
|
||||
const isVersioned = typeof transaction?.version === 'number' || transaction instanceof VersionedTransaction;
|
||||
summary.kind = isVersioned ? `versioned:${String(transaction.version)}` : 'legacy';
|
||||
summary.feePayer = String(transaction?.feePayer?.toBase58?.() || '').trim();
|
||||
summary.recentBlockhash = String(transaction?.recentBlockhash || transaction?.message?.recentBlockhash || '').trim();
|
||||
|
||||
if (isVersioned) {
|
||||
const message = transaction?.message || {};
|
||||
const staticKeys = Array.isArray(message?.staticAccountKeys) ? message.staticAccountKeys : [];
|
||||
const instructions = Array.isArray(message?.compiledInstructions) ? message.compiledInstructions : [];
|
||||
summary.instructionCount = instructions.length;
|
||||
summary.accountCount = staticKeys.length;
|
||||
summary.programs = instructions
|
||||
.map((instruction) => staticKeys[instruction?.programIdIndex]?.toBase58?.() || '')
|
||||
.filter(Boolean)
|
||||
.slice(0, 5);
|
||||
return summary;
|
||||
}
|
||||
|
||||
const instructions = Array.isArray(transaction?.instructions) ? transaction.instructions : [];
|
||||
summary.instructionCount = instructions.length;
|
||||
summary.accountCount = Array.isArray(transaction?.signatures) ? transaction.signatures.length : 0;
|
||||
summary.programs = instructions
|
||||
.map((instruction) => instruction?.programId?.toBase58?.() || '')
|
||||
.filter(Boolean)
|
||||
.slice(0, 5);
|
||||
return summary;
|
||||
}
|
||||
|
||||
function createRequest(method, params = {}) {
|
||||
const id = `shine-wallet-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -59,7 +99,7 @@ function createRequest(method, params = {}) {
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
}, window.location.origin);
|
||||
}, PAGE_MESSAGE_TARGET_ORIGIN);
|
||||
});
|
||||
}
|
||||
|
||||
@ -122,12 +162,6 @@ class ShineProviderCore {
|
||||
|
||||
async connect(options = {}) {
|
||||
const onlyIfTrusted = !!options?.onlyIfTrusted || !!options?.silent;
|
||||
if (!onlyIfTrusted) {
|
||||
const confirmed = window.confirm(`Connect SHiNE Wallet to ${window.location.origin}?`);
|
||||
if (!confirmed) {
|
||||
throw createProviderError('User rejected wallet connection', 'USER_REJECTED');
|
||||
}
|
||||
}
|
||||
const result = await createRequest('connect', { onlyIfTrusted });
|
||||
const nextKey = new PublicKey(String(result?.publicKeyBase58 || '').trim());
|
||||
this.publicKey = nextKey;
|
||||
@ -157,10 +191,12 @@ class ShineProviderCore {
|
||||
await this.connect();
|
||||
}
|
||||
const transactionBase64 = serializeTransactionBase64(transaction);
|
||||
const transactionSummary = summarizeTransaction(transaction);
|
||||
const result = await createRequest('signTransaction', {
|
||||
publicKeyBase58: this.publicKeyBase58,
|
||||
transactionBase64,
|
||||
comment: String(comment || '').trim() || `Site ${window.location.origin} requested transaction signature`,
|
||||
transactionSummary,
|
||||
});
|
||||
return deserializeSignedTransaction(String(result?.signedTransactionBase64 || ''), transaction);
|
||||
}
|
||||
@ -169,10 +205,20 @@ class ShineProviderCore {
|
||||
if (!this.publicKey) {
|
||||
await this.connect();
|
||||
}
|
||||
const transactionSummary = {
|
||||
kind: 'raw-bytes',
|
||||
instructionCount: 0,
|
||||
accountCount: 0,
|
||||
feePayer: this.publicKeyBase58,
|
||||
recentBlockhash: '',
|
||||
programs: [],
|
||||
byteLength: Number(transactionBytes?.length || 0),
|
||||
};
|
||||
const result = await createRequest('signTransaction', {
|
||||
publicKeyBase58: this.publicKeyBase58,
|
||||
transactionBase64: bytesToBase64(transactionBytes),
|
||||
comment: String(comment || '').trim() || `Site ${window.location.origin} requested transaction signature`,
|
||||
transactionSummary,
|
||||
});
|
||||
return base64ToBytes(String(result?.signedTransactionBase64 || '').trim());
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
client.version=1.2.258
|
||||
server.version=1.2.243
|
||||
client.version=1.2.259
|
||||
server.version=1.2.244
|
||||
|
||||
@ -240,11 +240,17 @@
|
||||
return window.solana;
|
||||
}
|
||||
async function connectWallet() {
|
||||
const out = document.getElementById("walletInfo");
|
||||
try {
|
||||
const provider = getProvider();
|
||||
const r = await provider.connect();
|
||||
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
|
||||
document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||
out.textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
out.innerHTML = `<span class="err">${String(e?.message || e)}</span>`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async function sendInstruction(ix) {
|
||||
const provider = getProvider();
|
||||
|
||||
@ -221,11 +221,17 @@
|
||||
}
|
||||
|
||||
async function connectWallet() {
|
||||
const out = document.getElementById("walletInfo");
|
||||
try {
|
||||
const provider = getProvider();
|
||||
const r = await provider.connect();
|
||||
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
|
||||
document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||
out.textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||
await refreshState();
|
||||
} catch (e) {
|
||||
out.innerHTML = `<span class="err">${String(e?.message || e)}</span>`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendInstruction(ix) {
|
||||
|
||||
@ -156,11 +156,17 @@
|
||||
return window.solana;
|
||||
}
|
||||
async function connectWallet() {
|
||||
const out = document.getElementById("walletInfo");
|
||||
try {
|
||||
const provider = getProvider();
|
||||
const r = await provider.connect();
|
||||
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
|
||||
document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||
out.textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
out.innerHTML = `<span class="err">${String(e?.message || e)}</span>`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async function sendInstruction(ix) {
|
||||
const provider = getProvider();
|
||||
|
||||
@ -162,11 +162,17 @@
|
||||
return window.solana;
|
||||
}
|
||||
async function connectWallet() {
|
||||
const out = document.getElementById("walletInfo");
|
||||
try {
|
||||
const provider = getProvider();
|
||||
const r = await provider.connect();
|
||||
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
|
||||
document.getElementById("walletInfo").textContent = "Кошелек менеджера: " + walletPubkey.toBase58();
|
||||
out.textContent = "Кошелек менеджера: " + walletPubkey.toBase58();
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
out.innerHTML = `<span class="err">${String(e?.message || e)}</span>`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async function sendInstruction(ix) {
|
||||
const provider = getProvider();
|
||||
|
||||
@ -210,11 +210,17 @@
|
||||
return window.solana;
|
||||
}
|
||||
async function connectWallet() {
|
||||
const out = document.getElementById("walletInfo");
|
||||
try {
|
||||
const provider = getProvider();
|
||||
const r = await provider.connect();
|
||||
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
|
||||
document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||
out.textContent = "Кошелек: " + walletPubkey.toBase58();
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
out.innerHTML = `<span class="err">${String(e?.message || e)}</span>`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async function sendInstruction(ix) {
|
||||
const provider = getProvider();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user