diff --git a/SHiNE-browser-plugin-wallet/background.js b/SHiNE-browser-plugin-wallet/background.js
index dd23775..e54cb75 100644
--- a/SHiNE-browser-plugin-wallet/background.js
+++ b/SHiNE-browser-plugin-wallet/background.js
@@ -30,7 +30,9 @@ const state = {
devicesResolvedAtMs: 0,
},
currentWallet: null,
- pendingApproval: null,
+ pendingApprovals: [],
+ siteApprovalChain: Promise.resolve(),
+ sessionAttachInProgress: false,
statusText: '',
statusKind: 'info',
};
@@ -71,23 +73,37 @@ function setStatus(message = '', kind = 'info') {
}
function makePendingApprovalSnapshot(payload = {}) {
+ const pendingId = String(payload?.id || '').trim();
+ const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : [];
+ const queueIndex = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === pendingId);
+ const queueLength = pendingApprovals.length;
return {
- id: String(payload?.id || '').trim(),
+ id: pendingId,
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()),
+ status: String(payload?.status || 'queued').trim() || 'queued',
+ queuePosition: queueIndex >= 0 ? queueIndex + 1 : 1,
+ queueLength: queueLength || 1,
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;
+function getCurrentPendingApproval() {
+ return Array.isArray(state.pendingApprovals) && state.pendingApprovals.length
+ ? state.pendingApprovals[0]
+ : null;
+}
+
+function removePendingApproval(pendingId, { rejectError = null } = {}) {
+ const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : [];
+ const index = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === String(pendingId || '').trim());
+ if (index < 0) return;
+ const [pending] = pendingApprovals.splice(index, 1);
if (pending.timeoutId) {
clearTimeout(pending.timeoutId);
}
@@ -139,7 +155,10 @@ async function loadStateFromStorage() {
login: String(settings?.login || '').trim(),
connectedOrigins: Array.isArray(settings?.connectedOrigins) ? settings.connectedOrigins.map((item) => normalizeOrigin(item)).filter(Boolean) : [],
};
- state.activeSession = await loadSessionMaterial();
+ const storedSession = await loadSessionMaterial();
+ if (storedSession || !state.sessionAttachInProgress) {
+ state.activeSession = storedSession;
+ }
state.walletProfile = state.activeSession?.walletProfile || null;
state.signing = {
selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''),
@@ -353,18 +372,30 @@ async function attachApprovedSession(payload) {
throw new Error('Получен неполный session-only payload');
}
- await clearSessionMaterial();
- state.activeSession = sessionRecord;
- await hydrateWalletProfile(login);
- await saveActiveSessionRecord();
- await persistSettings({
- login: sessionRecord.login,
- serverLogin: sessionRecord.serverLogin,
- serverHttp: sessionRecord.serverHttp,
- serverUrl: sessionRecord.serverUrl,
- });
- state.connectionOnline = false;
- state.currentWallet = null;
+ state.sessionAttachInProgress = true;
+ try {
+ state.activeSession = sessionRecord;
+ state.walletProfile = null;
+ state.currentWallet = null;
+ state.signing = {
+ ...state.signing,
+ selectedDeviceName: '',
+ devicesResolvedAtMs: 0,
+ };
+ await saveActiveSessionRecord();
+ await hydrateWalletProfile(login);
+ await saveActiveSessionRecord();
+ await persistSettings({
+ login: sessionRecord.login,
+ serverLogin: sessionRecord.serverLogin,
+ serverHttp: sessionRecord.serverHttp,
+ serverUrl: sessionRecord.serverUrl,
+ });
+ state.connectionOnline = false;
+ state.currentWallet = null;
+ } finally {
+ state.sessionAttachInProgress = false;
+ }
}
async function pollPairingStatus() {
@@ -666,41 +697,56 @@ async function requestCurrentWallet() {
}
async function cancelPendingSiteApproval() {
- clearPendingApproval({
+ const pending = getCurrentPendingApproval();
+ if (!pending) {
+ setStatus('Сейчас нет активного ожидания подписи.', 'info');
+ return { ok: true };
+ }
+ removePendingApproval(pending.id, {
rejectError: makeCodeError('User canceled request in extension.', 'USER_REJECTED'),
});
setStatus('Ожидание подписи отменено в расширении.', 'info');
return { ok: true };
}
-async function markPendingSiteApprovalResolved() {
- clearPendingApproval();
+async function markPendingSiteApprovalResolved(pendingId) {
+ removePendingApproval(pendingId);
}
-async function beginSiteTransactionFlow(payload = {}, sender = null) {
- if (state.pendingApproval) {
- throw makeCodeError('Another signing request is already pending.', 'APPROVAL_ALREADY_PENDING');
- }
+function enqueueSiteApproval(work) {
+ const run = state.siteApprovalChain.then(work, work);
+ state.siteApprovalChain = run.catch(() => {});
+ return run;
+}
+
+async function activatePendingApproval(pending, sender = null) {
const abortController = new AbortController();
+ pending.status = 'active';
+ pending.abortController = abortController;
+ pending.timeoutId = setTimeout(() => {
+ removePendingApproval(pending.id, {
+ rejectError: makeCodeError('Signing request timed out in extension.', 'USER_REJECTED'),
+ });
+ setStatus('Ожидание подписи истекло в расширении.', 'error');
+ }, 120000);
+ setStatus(`Сайт ${pending.origin} запросил подпись. Подтвердите или отмените на доверенном устройстве.`, 'info');
+ await openSidePanelForSender(sender);
+ return pending;
+}
+
+function beginSiteTransactionFlow(payload = {}) {
const pending = makePendingApprovalSnapshot({
...payload,
kind: 'sign_transaction',
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
createdAtMs: Date.now(),
+ status: 'queued',
});
- const timeoutId = setTimeout(() => {
- clearPendingApproval({
- rejectError: makeCodeError('Signing request timed out in extension.', 'USER_REJECTED'),
- });
- setStatus('Ожидание подписи истекло в расширении.', 'error');
- }, 120000);
- state.pendingApproval = {
+ state.pendingApprovals.push({
...pending,
- timeoutId,
- abortController,
- };
- setStatus(`Сайт ${pending.origin} запросил подпись. Подтвердите или отмените на доверенном устройстве.`, 'info');
- await openSidePanelForSender(sender);
+ timeoutId: 0,
+ abortController: null,
+ });
return pending;
}
@@ -747,40 +793,44 @@ async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64,
if (!cleanPub || !cleanTx) {
throw makeCodeError('Transaction payload is incomplete.', 'BAD_REQUEST');
}
- const pending = await beginSiteTransactionFlow({
+ const pending = 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',
- requestId,
- publicKeyBase58: cleanPub,
- transactionBase64: cleanTx,
- comment: signComment,
- }, 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') {
- throw makeCodeError('User rejected transaction signature on ESP32.', 'USER_REJECTED');
+ });
+ return enqueueSiteApproval(async () => {
+ await activatePendingApproval(getCurrentPendingApproval() || pending, sender);
+ const activePending = getCurrentPendingApproval() || pending;
+ 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',
+ requestId,
+ publicKeyBase58: cleanPub,
+ transactionBase64: cleanTx,
+ comment: signComment,
+ }, 120000, activePending.abortController?.signal || null);
+ if (!response?.ok) {
+ const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase();
+ if (errorCode === 'REJECTED_BY_USER') {
+ throw makeCodeError('User rejected transaction signature on ESP32.', 'USER_REJECTED');
+ }
+ throw makeCodeError(`ESP32 rejected transaction signature: ${String(response?.error || 'unknown_error')}`, errorCode || 'RPC_REJECTED');
}
- 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(activePending.id);
}
- 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() {
@@ -797,7 +847,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,
+ pendingApproval: getCurrentPendingApproval() ? makePendingApprovalSnapshot(getCurrentPendingApproval()) : null,
signing: { ...state.signing },
status: {
text: state.statusText,
diff --git a/SHiNE-browser-plugin-wallet/popup.js b/SHiNE-browser-plugin-wallet/popup.js
index e7f7d60..10b7572 100644
--- a/SHiNE-browser-plugin-wallet/popup.js
+++ b/SHiNE-browser-plugin-wallet/popup.js
@@ -118,6 +118,7 @@ function renderPendingApproval(pendingApproval) {
const details = [
{ label: 'Сайт', value: pendingApproval.origin || '—', mono: true },
{ label: 'Кошелёк', value: pendingApproval.publicKeyBase58 || '—', mono: true },
+ { label: 'Очередь', value: `${pendingApproval.queuePosition || 1} из ${pendingApproval.queueLength || 1}` },
{ label: 'Комментарий', value: pendingApproval.comment || 'Транзакция запрошена сайтом' },
{ label: 'Тип', value: summary.kind || 'legacy' },
{ label: 'Инструкций', value: String(summary.instructionCount ?? 0) },
@@ -186,9 +187,12 @@ function applyState(nextState) {
els.requestWalletBtn.disabled = !session || !signing.selectedDeviceName;
if (pendingApproval) {
+ const queueSuffix = (pendingApproval.queueLength || 1) > 1
+ ? ` В очереди ${pendingApproval.queueLength} транзакции.`
+ : '';
els.pendingApprovalSubtitle.textContent = pendingApproval.origin
- ? `Сайт ${pendingApproval.origin} запросил подписание транзакции.`
- : 'Сайт запросил подписание транзакции.';
+ ? `Сайт ${pendingApproval.origin} запросил подписание транзакции.${queueSuffix}`
+ : `Сайт запросил подписание транзакции.${queueSuffix}`;
renderPendingApproval(pendingApproval);
} else {
els.pendingApprovalSubtitle.textContent = 'Сайт запросил подписание транзакции.';
diff --git a/SHiNE-browser-plugin-wallet/provider-bridge.js b/SHiNE-browser-plugin-wallet/provider-bridge.js
index d6ad649..d632234 100644
--- a/SHiNE-browser-plugin-wallet/provider-bridge.js
+++ b/SHiNE-browser-plugin-wallet/provider-bridge.js
@@ -308,6 +308,15 @@ class ShineSolanaProvider {
return this.core.signTransaction(transaction);
}
+ async signAllTransactions(transactions = []) {
+ const list = Array.isArray(transactions) ? transactions : [];
+ const outputs = [];
+ for (const transaction of list) {
+ outputs.push(await this.core.signTransaction(transaction));
+ }
+ return outputs;
+ }
+
async request(args = {}) {
const method = String(args?.method || '');
const params = args?.params;
@@ -321,6 +330,12 @@ class ShineSolanaProvider {
const tx = Array.isArray(params) ? params[0] : params?.transaction || params;
return this.signTransaction(tx);
}
+ if (method === 'signAllTransactions') {
+ const transactions = Array.isArray(params)
+ ? params
+ : Array.isArray(params?.transactions) ? params.transactions : [];
+ return this.signAllTransactions(transactions);
+ }
throw createProviderError(`Unsupported request method: ${method}`, 'UNSUPPORTED_METHOD');
}
}
diff --git a/VERSION.properties b/VERSION.properties
index be130be..893c286 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.259
-server.version=1.2.244
+client.version=1.2.260
+server.version=1.2.245
diff --git a/shine-UI/index.html b/shine-UI/index.html
index 7fd4f01..635e6bb 100644
--- a/shine-UI/index.html
+++ b/shine-UI/index.html
@@ -5,7 +5,9 @@