diff --git a/static/index.html b/static/index.html index d7e4538..6e82cb8 100644 --- a/static/index.html +++ b/static/index.html @@ -31,7 +31,7 @@ octra_wallet - + @@ -83,7 +83,7 @@
- +
@@ -113,7 +113,14 @@
loading...
-
connecting...
+
+ + +
@@ -536,6 +543,6 @@ - + diff --git a/static/style.css b/static/style.css index 0b42f10..c7dd664 100644 --- a/static/style.css +++ b/static/style.css @@ -107,6 +107,67 @@ header .subtitle .mono { margin-left: 10px; } +.hdr-status-wrap { + position: relative; + flex-shrink: 0; +} + +header button.hdr-net-toggle { + border: none; + cursor: pointer; + font-family: Tahoma, arial, sans-serif; + letter-spacing: 1px; +} + +.hdr-network-menu { + position: absolute; + right: 0; + top: 100%; + margin-top: 2px; + min-width: 140px; + background: #fff; + border: 1px solid #D0D7E2; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + z-index: 50; +} + +.hdr-network-item { + display: block; + width: 100%; + border: none; + border-bottom: 1px solid #E5E9EF; + background: #fff; + padding: 8px 12px; + font-family: Tahoma, arial, sans-serif; + font-size: 11px; + letter-spacing: 1px; + text-align: left; + text-transform: lowercase; + color: #3B567F; + font-weight: bold; + cursor: pointer; +} + +.hdr-network-item:last-child { + border-bottom: none; +} + +.hdr-network-item:hover { + background: #E5E9EF; +} + +.hdr-network-item.hdr-network-item-current { + color: #B0B8C4; + background: #F6F7F9; + font-weight: normal; + cursor: default; + pointer-events: none; +} + +.hdr-network-item.hdr-network-item-current:hover { + background: #F6F7F9; +} + header .right { display: flex; align-items: center; diff --git a/static/wallet.js b/static/wallet.js index 6f73cbb..88f8969 100644 --- a/static/wallet.js +++ b/static/wallet.js @@ -109,7 +109,11 @@ var _cachedBal = null; var _encryptedBalanceRaw = 0; var _unclaimedCount = 0; var _pendingClaimIds = {}; -var _explorerUrl = 'https://octrascan.io'; +var RPC_MAINNET = 'http://46.101.86.250:8080'; +var RPC_DEVNET = 'https://devnet.octra.com/rpc'; +var EXPLORER_MAINNET = 'https://octrascan.io'; +var EXPLORER_DEVNET = 'https://devnet.octrascan.io'; +var _explorerUrl = EXPLORER_MAINNET; var _tokens = []; var _selectedToken = null; var _tokenSymbols = {}; @@ -118,7 +122,9 @@ var _tokensLoaded = false; var _tokTxGen = 0; var _compiledAbi = null; var _fees = {}; +var _fetchGen = 0; var _rpcHost = ''; +var _walletRpcNorm = ''; var _hasMasterSeed = false; @@ -760,6 +766,116 @@ function networkLabel(host) { return host; } +var _hdrNetworkMenuOpen = false; + +function normRpcUrl(u) { + try { + var x = new URL((u || '').trim()); + var p = x.pathname.replace(/\/+$/, ''); + return x.origin.toLowerCase() + p; + } catch (e) { + return (u || '').trim(); + } +} + +function syncHdrNetworkMenuSelection() { + var mainEl = $('hdr-net-mainnet'); + var devEl = $('hdr-net-devnet'); + if (!mainEl || !devEl) return; + mainEl.classList.remove('hdr-network-item-current'); + devEl.classList.remove('hdr-network-item-current'); + var cur = _walletRpcNorm; + if (!cur) return; + var nm = normRpcUrl(RPC_MAINNET); + var nd = normRpcUrl(RPC_DEVNET); + if (cur === nm) mainEl.classList.add('hdr-network-item-current'); + else if (cur === nd) devEl.classList.add('hdr-network-item-current'); +} + +function toggleHdrNetworkMenu(ev) { + if (ev) ev.stopPropagation(); + var m = $('hdr-network-menu'); + if (!m) return; + _hdrNetworkMenuOpen = !_hdrNetworkMenuOpen; + m.style.display = _hdrNetworkMenuOpen ? 'block' : 'none'; + if (_hdrNetworkMenuOpen) syncHdrNetworkMenuSelection(); +} + +function closeHdrNetworkMenu() { + var m = $('hdr-network-menu'); + if (m) m.style.display = 'none'; + _hdrNetworkMenuOpen = false; +} + +document.addEventListener('click', function() { + closeHdrNetworkMenu(); +}); + +function setHdrStatusConnecting() { + $('hdr-status-label').textContent = 'connecting...'; + $('hdr-status').className = 'right hdr-net-toggle'; +} + +function flushDashboardValues() { + _fetchGen++; + var ids = ['st-balance','st-enc-balance','st-nonce','st-staging', + 'send-bal','enc-pub-bal','enc-enc-bal','st-enc-bal-info','ct-bal']; + for (var i = 0; i < ids.length; i++) { + var el = $(ids[i]); + if (el) el.textContent = '-'; + } + if ($('btn-key-switch')) $('btn-key-switch').style.display = 'none'; + if ($('dash-txs')) $('dash-txs').innerHTML = '
loading...
'; + if ($('dash-more')) $('dash-more').innerHTML = ''; +} + +function beginNetworkSwitch() { + pauseRefreshTimer(); + setHdrStatusConnecting(); + flushDashboardValues(); +} + +async function switchNetworkFromHeader(rpcUrl, explorerUrl) { + closeHdrNetworkMenu(); + $('settings-rpc').value = rpcUrl; + $('settings-explorer').value = explorerUrl; + beginNetworkSwitch(); + try { + await doSaveSettings({ deferClientRefresh: true }); + await hdrNetworkAfterSave(); + } finally { + startRefreshTimer(); + } +} + +function applyHdrStatusOnline() { + $('hdr-status-label').textContent = _rpcHost ? 'online | ' + networkLabel(_rpcHost) : 'online'; + $('hdr-status').className = 'right hdr-net-toggle online'; +} + +async function hdrNetworkAfterSave() { + var onDash = document.querySelector('.nav-tabs a.active[data-view="dashboard"]'); + var ok = onDash + ? await loadDashboard({ skipHdr: true }) + : (await fetchBalance({ skipHdr: true })) !== null; + if (ok) applyHdrStatusOnline(); +} + +async function hdrNetworkPickMainnet() { + await switchNetworkFromHeader(RPC_MAINNET, EXPLORER_MAINNET); +} + +async function hdrNetworkPickDevnet() { + await switchNetworkFromHeader(RPC_DEVNET, EXPLORER_DEVNET); +} + +function hdrNetworkPickCustom() { + closeHdrNetworkMenu(); + switchView('settings'); + var el = $('settings-rpc'); + if (el) window.setTimeout(function() { el.focus(); el.select(); }, 0); +} + @@ -790,9 +906,12 @@ async function bgStealthScan() { } catch (e) {} } -async function fetchBalance() { +async function fetchBalance(opt) { + var gen = _fetchGen; + var skipHdr = opt && opt.skipHdr; try { var bal = await api('GET', '/balance'); + if (_fetchGen !== gen) return null; _cachedBal = bal; var pub = bal.public_balance || '0'; var enc = bal.encrypted_balance || '0'; @@ -813,12 +932,14 @@ async function fetchBalance() { if ($('st-enc-bal-info')) $('st-enc-bal-info').textContent = encCorrupt ? 'corrupted ciphertext' : fmtOct(enc); if ($('ct-bal')) $('ct-bal').textContent = fmtOct(pub); - $('hdr-status').textContent = _rpcHost ? 'online | ' + networkLabel(_rpcHost) : 'online'; - $('hdr-status').className = 'right online'; + if (!skipHdr) { + $('hdr-status-label').textContent = _rpcHost ? 'online | ' + networkLabel(_rpcHost) : 'online'; + $('hdr-status').className = 'right hdr-net-toggle online'; + } return bal; } catch (e) { - $('hdr-status').textContent = 'offline'; - $('hdr-status').className = 'right error'; + $('hdr-status-label').textContent = 'offline'; + $('hdr-status').className = 'right hdr-net-toggle error'; return null; } } @@ -1222,23 +1343,29 @@ function renderDashTxs(txs) { $('dash-more').innerHTML = '
view full history
'; } -async function loadDashboard() { - await fetchBalance(); +async function loadDashboard(opt) { + var gen = _fetchGen; + var skipHdr = opt && opt.skipHdr; + var bal = await fetchBalance({ skipHdr: skipHdr }); + if (_fetchGen !== gen || bal === null) return false; loadTokenSymbols(); try { var lim = dashTxLimit(); var hist = await api('GET', '/history?limit=' + lim + '&offset=0'); + if (_fetchGen !== gen) return false; var txs = hist.transactions || []; if (txs.length === 0) { $('dash-txs').innerHTML = '
no transactions yet
'; $('dash-more').innerHTML = ''; - return; + return true; } renderDashTxs(txs); - fetchMissingSymbols(txs).then(function() { renderDashTxs(txs); }); + fetchMissingSymbols(txs).then(function() { if (_fetchGen === gen) renderDashTxs(txs); }); + return true; } catch (e) { $('dash-txs').innerHTML = '
no transactions yet
'; $('dash-more').innerHTML = ''; + return true; } } @@ -2345,8 +2472,8 @@ async function revealPrivateKeys() { async function loadSettings() { try { var w = await api('GET', '/wallet'); - $('settings-rpc').value = w.rpc_url || 'http://46.101.86.250:8080'; - $('settings-explorer').value = w.explorer_url || 'https://octrascan.io'; + $('settings-rpc').value = w.rpc_url || RPC_MAINNET; + $('settings-explorer').value = w.explorer_url || EXPLORER_MAINNET; } catch (e) {} loadAccountList(); } @@ -2422,8 +2549,7 @@ function modalPrompt(title, label, opts) { $('modal-pin').style.display = 'block'; $('modal-pin-input').value = ''; $('pin-back-btn').style.display = ''; - var unlockBtn = $('modal-pin').querySelector('.action-btn'); - if (unlockBtn) unlockBtn.textContent = _modalPromptBtnText; + setModalUnlockState(_modalPromptBtnText, false); $('modal-pin-input').focus(); $('modal-overlay').style.display = 'flex'; } else { @@ -2530,7 +2656,9 @@ function showImportAnother() { $('modal-overlay').style.display = 'flex'; } -async function doSaveSettings() { +async function doSaveSettings(opts) { + opts = opts || {}; + var deferClientRefresh = !!opts.deferClientRefresh; clearResult('settings-result'); var rpc = $('settings-rpc').value.trim(); var explorer = $('settings-explorer').value.trim(); @@ -2539,6 +2667,7 @@ async function doSaveSettings() { var resp = await api('POST', '/settings', { rpc_url: rpc, explorer_url: explorer }); if (explorer) _explorerUrl = explorer.replace(/\/+$/, ''); try { _rpcHost = new URL(rpc).hostname; } catch(e) { _rpcHost = rpc; } + _walletRpcNorm = normRpcUrl(rpc); if (resp && resp.cache_cleared) { _cachedBal = null; _historyOffset = 0; @@ -2549,9 +2678,11 @@ async function doSaveSettings() { _unclaimedCount = 0; _tokenSymbols = {}; _tokenDecimals = {}; - fetchBalance(); - if (document.querySelector('.nav-tabs a.active[data-view="dashboard"]')) - loadDashboard(); + if (!deferClientRefresh) { + fetchBalance(); + if (document.querySelector('.nav-tabs a.active[data-view="dashboard"]')) + loadDashboard(); + } showResult('settings-result', true, 'saved ยท cache cleared'); } else { showResult('settings-result', true, 'saved'); @@ -2595,10 +2726,23 @@ function hideAllModalPanels() { $('modal-result').innerHTML = ''; } +function setModalUnlockState(label, busy) { + var btn = $('modal-unlock-btn'); + var inp = $('modal-pin-input'); + var back = $('pin-back-btn'); + if (btn) { + btn.textContent = label; + btn.disabled = !!busy; + } + if (inp) inp.disabled = !!busy; + if (back) back.disabled = !!busy; +} + function showPinEntry(showBack) { hideAllModalPanels(); $('modal-pin').style.display = 'block'; $('modal-pin-input').value = ''; + setModalUnlockState('unlock', false); var backBtn = $('pin-back-btn'); if (backBtn) backBtn.style.display = showBack ? '' : 'none'; $('modal-pin-input').focus(); @@ -2650,8 +2794,7 @@ function modalBackFromPin() { if (_modalPromptResolve) { var cb = _modalPromptResolve; _modalPromptResolve = null; - var unlockBtn = $('modal-pin').querySelector('.action-btn'); - if (unlockBtn) unlockBtn.textContent = 'unlock'; + setModalUnlockState('unlock', false); hideAllModalPanels(); $('modal-overlay').style.display = 'none'; cb(null); @@ -2730,14 +2873,14 @@ async function modalUnlock() { if (_modalPromptResolve) { var cb = _modalPromptResolve; _modalPromptResolve = null; - var unlockBtn = $('modal-pin').querySelector('.action-btn'); - if (unlockBtn) unlockBtn.textContent = 'unlock'; + setModalUnlockState('unlock', false); hideAllModalPanels(); $('modal-overlay').style.display = 'none'; cb(pin); return; } - $('modal-result').innerHTML = '
unlocking...
'; + setModalUnlockState('unlocking...', true); + var unlockFailed = false; try { var unlockBody = { pin: pin }; if (_selectedUnlockAddr) unlockBody.addr = _selectedUnlockAddr; @@ -2749,9 +2892,12 @@ async function modalUnlock() { await loadWalletInfo(); startRefreshTimer(); } catch (e) { + unlockFailed = true; $('modal-result').innerHTML = '
' + e.message + '
'; $('modal-pin-input').value = ''; - $('modal-pin-input').focus(); + } finally { + setModalUnlockState('unlock', false); + if (unlockFailed) $('modal-pin-input').focus(); } } @@ -2811,6 +2957,7 @@ async function loadWalletInfo() { _walletAddr = w.address || w.addr || ''; if (w.explorer_url) _explorerUrl = w.explorer_url.replace(/\/+$/, ''); if (w.rpc_url) try { _rpcHost = new URL(w.rpc_url).hostname; } catch(e) { _rpcHost = w.rpc_url; } + _walletRpcNorm = w.rpc_url ? normRpcUrl(w.rpc_url) : ''; _hasMasterSeed = !!w.has_master_seed; $('hdr-addr').innerHTML = '' + _walletAddr + ''; $('hdr-logout').style.display = ''; @@ -2819,23 +2966,24 @@ async function loadWalletInfo() { loadDashboard(); } catch (e) { $('hdr-addr').textContent = 'error loading wallet'; - $('hdr-status').textContent = 'error'; - $('hdr-status').className = 'right error'; + $('hdr-status-label').textContent = 'error'; + $('hdr-status').className = 'right hdr-net-toggle error'; } } async function doLogout() { try { await api('POST', '/wallet/lock', {}); } catch (e) {} - if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; } + pauseRefreshTimer(); _walletAddr = ''; _cachedBal = null; _encryptedBalanceRaw = 0; + _walletRpcNorm = ''; _hasMasterSeed = false; $('hdr-logout').style.display = 'none'; $('hdr-dev').style.display = 'none'; $('hdr-addr').textContent = 'locked'; - $('hdr-status').textContent = 'locked'; - $('hdr-status').className = 'right'; + $('hdr-status-label').textContent = 'locked'; + $('hdr-status').className = 'right hdr-net-toggle'; switchView('dashboard'); init(); } @@ -2852,6 +3000,10 @@ function startRefreshTimer() { }, 15000); } +function pauseRefreshTimer() { + if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; } +} + var _selectedUnlockAddr = ''; var _selectedUnlockFile = '';