diff --git a/client/nginx/bitfeed.conf.template b/client/nginx/bitfeed.conf.template index bc6bf75..352cf1f 100644 --- a/client/nginx/bitfeed.conf.template +++ b/client/nginx/bitfeed.conf.template @@ -1,6 +1,7 @@ map $sent_http_content_type $expires { default off; text/css max; + text/json max; application/javascript max; } @@ -14,15 +15,10 @@ server { server_name client; - location = / { + location ~* \.(html)$ { add_header Cache-Control 'no-cache'; } - location / { - try_files $uri $uri/ =404; - expires $expires; - } - location /api { proxy_cache bitfeed; proxy_cache_revalidate on; @@ -42,6 +38,11 @@ server { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; } + + location / { + try_files $uri $uri/ /index.html; + expires $expires; + } } upstream wsmonobackend { diff --git a/client/package.json b/client/package.json index bd277f3..5319f61 100644 --- a/client/package.json +++ b/client/package.json @@ -4,7 +4,7 @@ "scripts": { "build": "rollup -c", "dev": "rollup -c -w", - "start": "sirv public" + "start": "sirv public --single" }, "dependencies": { "@babel/core": "^7.16.5", diff --git a/client/src/App.svelte b/client/src/App.svelte index f1f4537..98b0d7a 100644 --- a/client/src/App.svelte +++ b/client/src/App.svelte @@ -2,9 +2,12 @@ import TxViz from './components/TxViz.svelte' import analytics from './utils/analytics.js' import config from './config.js' - import { settings } from './stores.js' + import Router from './controllers/Router.js' + import {settings} from './stores.js' if (!$settings.noTrack && config.public) analytics.init() + + const router = new Router(window.location.pathname)
diff --git a/client/src/components/BlockInfo.svelte b/client/src/components/BlockInfo.svelte index 9fbd2df..08a4ef4 100644 --- a/client/src/components/BlockInfo.svelte +++ b/client/src/components/BlockInfo.svelte @@ -115,9 +115,9 @@ async function explorePrevBlock (e) { e.preventDefault() if (!$loading && block) { - $loading = true + loading.increment() await searchBlockHeight(block.height - 1) - $loading = false + loading.decrement() } } @@ -125,9 +125,9 @@ e.preventDefault() if (!$loading && block) { if (block.height + 1 < $latestBlockHeight) { - $loading = true + loading.increment() await searchBlockHeight(block.height + 1) - $loading = false + loading.decrement() } else { dispatch('quitExploring') } diff --git a/client/src/components/DonationOverlay.svelte b/client/src/components/DonationOverlay.svelte index 0dc913b..d82ab1e 100644 --- a/client/src/components/DonationOverlay.svelte +++ b/client/src/components/DonationOverlay.svelte @@ -17,7 +17,7 @@ import clipboardIcon from '../assets/icon/cil-clipboard.svg' import twitterIcon from '../assets/icon/cib-twitter.svg' import { fade, fly } from 'svelte/transition' import { durationFormat } from '../utils/format.js' -import { overlay, tiers } from '../stores.js' +import { overlay, tiers, urlPath } from '../stores.js' import QRCode from 'qrcode' let tab = 'form' // form | invoice | success @@ -164,6 +164,7 @@ $: { } $: { if ($overlay === 'donation') { + $urlPath = '/donate' startExpiryTimer() stopPollingInvoice() pollingEnabled = true @@ -416,9 +417,13 @@ async function copyInvoice () { } analytics.trackEvent('donations', 'invoice', 'copy') } + +function onClose () { + $urlPath = "/" +} - +
diff --git a/client/src/components/SearchBar.svelte b/client/src/components/SearchBar.svelte index 71f8061..68f9e79 100644 --- a/client/src/components/SearchBar.svelte +++ b/client/src/components/SearchBar.svelte @@ -54,7 +54,7 @@ async function searchSubmit (e) { e.preventDefault() if (matchedQuery && matchedQuery.query !== 'address') { - $loading++ + loading.increment() let searchErr switch(matchedQuery.query) { case 'txid': @@ -79,7 +79,7 @@ async function searchSubmit (e) { } if (searchErr == null) errorMessage = null else handleSearchError(searchErr) - $loading-- + loading.decrement() } else { errorMessage = 'enter a transaction id, block hash or block height' } diff --git a/client/src/components/TransactionOverlay.svelte b/client/src/components/TransactionOverlay.svelte index 379e57b..d2f57b6 100644 --- a/client/src/components/TransactionOverlay.svelte +++ b/client/src/components/TransactionOverlay.svelte @@ -3,7 +3,7 @@ import Overlay from '../components/Overlay.svelte' import Icon from './Icon.svelte' import BookmarkIcon from '../assets/icon/cil-bookmark.svg' import { longBtcFormat, numberFormat, feeRateFormat, dateFormat } from '../utils/format.js' -import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth, latestBlockHeight, highlightInOut, loading } from '../stores.js' +import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth, latestBlockHeight, highlightInOut, loading, urlPath } from '../stores.js' import { formatCurrency } from '../utils/fx.js' import { hlToHex, mixColor, teal, purple } from '../utils/color.js' import { SPKToAddress } from '../utils/encodings.js' @@ -14,6 +14,7 @@ import { fade } from 'svelte/transition' function onClose () { $detailTx = null $highlightInOut = null + $urlPath = "/" } function formatBTC (sats) { @@ -271,16 +272,16 @@ async function clickItem (item) { async function goToInput(e, input) { e.preventDefault() - $loading++ + loading.increment() await searchTx(input.prev_txid, null, input.prev_vout) - $loading-- + loading.decrement() } async function goToOutput(e, output) { e.preventDefault() - $loading++ + loading.increment() await searchTx(output.spend.txid, output.spend.vin) - $loading-- + loading.decrement() } diff --git a/client/src/controllers/Router.js b/client/src/controllers/Router.js new file mode 100644 index 0000000..cddbec9 --- /dev/null +++ b/client/src/controllers/Router.js @@ -0,0 +1,104 @@ +import { urlPath, settings, loading, detailTx, highlightInOut, explorerBlockData, overlay } from '../stores.js' +import { searchTx, searchBlockHash, searchBlockHeight } from '../utils/search.js' + +export default class Router { + constructor (initialPath = '/') { + this.path = initialPath + this.apply(initialPath) + urlPath.subscribe(val => { + if (val != null) { + this.pushHistory(val) + this.path = val + } + }) + window.addEventListener('popstate', e => { + if (e && e.state && e.state.path) { + this.path = e.state.path + this.apply(e.state.path) + } + }) + } + + pushHistory (path, replace = false) { + if (replace) { + window.history.replaceState({path}, "", path, window.history.state) + } else if (path !== this.path) { + window.history.pushState({path}, "", path) + } + } + + clearPath () { + urlPath.set("/") + } + + apply (path) { + const parts = path.split("/") + if (path === '/') { + detailTx.set(null) + highlightInOut.set(null) + urlPath.set("/") + explorerBlockData.set(null) + overlay.set(null) + } else { + switch (parts[1]) { + case 'block': + if (parts[2] === "height") { + try { + const height = parseInt(parts[3]) + this.goToBlockHeight(height) + } catch (err) { + // ?? + } + } else if (parts[2]) { + this.goToBlock(parts[2]) + } + break; + + case 'tx': + if (parts[2]) { + this.goToTransaction(parts[2]) + } + break; + + case 'donate': + overlay.set('donation') + break; + } + } + } + + async goToBlock (blockhash) { + loading.increment() + await searchBlockHash(blockhash) + loading.decrement() + } + + async goToBlockHeight (height) { + loading.increment() + await searchBlockHeight(height) + loading.decrement() + } + + async goToTransaction (q) { + loading.increment() + const parts = q.split(":") + let txid, input, output + if (parts.length) { + if (parts[0].length == 64) { + txid = parts[0] + output = parseInt(parts[1]) + if (isNaN(output)) output = null + } else if (parts[1].length == 64) { + txid = parts[1] + input = parseInt(parts[0]) + if (isNaN(input)) input = null + } else { + // invalid + } + } else { + txid = q + } + await searchTx(txid, input, output) + loading.decrement() + } +} diff --git a/client/src/controllers/TxController.js b/client/src/controllers/TxController.js index e495caf..1a8b67d 100644 --- a/client/src/controllers/TxController.js +++ b/client/src/controllers/TxController.js @@ -6,7 +6,7 @@ import BitcoinBlock from '../models/BitcoinBlock.js' import TxSprite from '../models/TxSprite.js' import { FastVertexArray } from '../utils/memory.js' import { searchTx, fetchSpends, addSpends } from '../utils/search.js' -import { overlay, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, detailTx, blockAreaSize, highlight, colorMode, blocksEnabled, latestBlockHeight, explorerBlockData, blockTransitionDirection, loading } from '../stores.js' +import { overlay, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, detailTx, blockAreaSize, highlight, colorMode, blocksEnabled, latestBlockHeight, explorerBlockData, blockTransitionDirection, loading, urlPath } from '../stores.js' import config from "../config.js" export default class TxController { @@ -322,6 +322,7 @@ export default class TxController { prevBlockScene.expire(2000) this.explorerBlockScene = null this.explorerBlock = null + urlPath.set("/") } if (this.blockScene && this.block) { blockTransitionDirection.set('right') @@ -397,6 +398,7 @@ export default class TxController { } else { const spendResult = await fetchSpends(selected.id) if (spendResult) selected = addSpends(selected, spendResult) + urlPath.set(`/tx/${selected.id}`) detailTx.set(selected) overlay.set('tx') } diff --git a/client/src/stores.js b/client/src/stores.js index e91b985..9e73683 100644 --- a/client/src/stores.js +++ b/client/src/stores.js @@ -170,3 +170,5 @@ export const highlightInOut = writable(null) export const loading = createCounter() export const explorerBlockData = writable(null) export const blockTransitionDirection = writable(null) + +export const urlPath = writable(null) diff --git a/client/src/utils/search.js b/client/src/utils/search.js index fd2fc96..7fbe593 100644 --- a/client/src/utils/search.js +++ b/client/src/utils/search.js @@ -1,7 +1,7 @@ import api from './api.js' import BitcoinTx from '../models/BitcoinTx.js' import BitcoinBlock from '../models/BitcoinBlock.js' -import { detailTx, selectedTx, currentBlock, explorerBlockData, overlay, highlightInOut } from '../stores.js' +import { detailTx, selectedTx, currentBlock, explorerBlockData, overlay, highlightInOut, urlPath } from '../stores.js' import { addressToSPK } from './encodings.js' // Quick heuristic matching to guess what kind of search a query is for @@ -198,6 +198,13 @@ function addSpends(tx, spends) { export {addSpends as addSpends} export async function searchTx (txid, input, output) { + if (input != null) { + urlPath.set(`/tx/${input}:${txid}`) + } else if (output != null) { + urlPath.set(`/tx/${txid}:${output}`) + } else { + urlPath.set(`/tx/${txid}`) + } try { let searchResult = await fetchTx(txid) const spendResult = await fetchSpends(txid) @@ -218,6 +225,7 @@ export async function searchTx (txid, input, output) { } export async function searchBlockHash (hash) { + urlPath.set(`/block/${hash}`) try { const searchResult = await fetchBlockByHash(hash) if (searchResult) { @@ -235,6 +243,7 @@ export async function searchBlockHash (hash) { } export async function searchBlockHeight (height) { + urlPath.set(`/block/height/${height}`) try { const searchResult = await fetchBlockByHeight(height) if (searchResult) { diff --git a/server/lib/spend_index.ex b/server/lib/spend_index.ex index 87db187..0b49bab 100644 --- a/server/lib/spend_index.ex +++ b/server/lib/spend_index.ex @@ -85,6 +85,7 @@ defmodule BitcoinStream.Index.Spend do @impl true def handle_cast({:block_disconnected, hash}, [dbref, indexed, done]) do + Logger.info("block disconnected: #{hash}"); if (indexed != nil and done) do block_disconnected(dbref, hash) end