mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-12 19:20:46 +02:00
Merge pull request #22 from bitfeed-project/search-and-highlight
Search and highlight
This commit is contained in:
commit
83cb8cb2e7
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bitfeed-client",
|
||||
"version": "2.1.4",
|
||||
"version": "2.1.5",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
|
268
client/src/components/SearchTab.svelte
Normal file
268
client/src/components/SearchTab.svelte
Normal file
@ -0,0 +1,268 @@
|
||||
<script>
|
||||
import { tick } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { flip } from 'svelte/animate'
|
||||
import Icon from './Icon.svelte'
|
||||
import SearchIcon from '../assets/icon/cil-search.svg'
|
||||
import PlusIcon from '../assets/icon/cil-plus.svg'
|
||||
import CrossIcon from '../assets/icon/cil-x.svg'
|
||||
import AddressIcon from '../assets/icon/cil-wallet.svg'
|
||||
import TxIcon from '../assets/icon/cil-arrow-circle-right.svg'
|
||||
import { matchQuery } from '../utils/search.js'
|
||||
import { highlight, newHighlightQuery, highlightingFull } from '../stores.js'
|
||||
import { hcl } from 'd3-color'
|
||||
|
||||
const highlightColors = [
|
||||
{ h: 0.03, l: 0.35 },
|
||||
{ h: 0.40, l: 0.35 },
|
||||
{ h: 0.65, l: 0.35 },
|
||||
{ h: 0.85, l: 0.35 },
|
||||
{ h: 0.12, l: 0.35 },
|
||||
]
|
||||
const highlightHexColors = highlightColors.map(c => hclToHex(c))
|
||||
const usedColors = [false, false, false, false, false]
|
||||
|
||||
const queryIcons = {
|
||||
txid: TxIcon,
|
||||
address: AddressIcon
|
||||
}
|
||||
const queryType = {
|
||||
txid: 'transaction',
|
||||
address: 'address'
|
||||
}
|
||||
|
||||
export let tab
|
||||
|
||||
let query
|
||||
let matchedQuery
|
||||
let queryAddress
|
||||
let queryColor
|
||||
let queryColorHex
|
||||
let watchlist = []
|
||||
|
||||
init ()
|
||||
|
||||
setNextColor()
|
||||
|
||||
$: {
|
||||
if ($newHighlightQuery) {
|
||||
matchedQuery = matchQuery($newHighlightQuery)
|
||||
if (matchedQuery) {
|
||||
matchedQuery.color = queryColor
|
||||
matchedQuery.colorHex = queryColorHex
|
||||
add()
|
||||
query = null
|
||||
}
|
||||
$newHighlightQuery = null
|
||||
}
|
||||
}
|
||||
$: {
|
||||
$highlight = matchedQuery ? [ ...watchlist, matchedQuery ] : watchlist
|
||||
}
|
||||
$: {
|
||||
localStorage.setItem('highlight', JSON.stringify(watchlist))
|
||||
}
|
||||
$: {
|
||||
if (query) {
|
||||
matchedQuery = matchQuery(query.trim())
|
||||
if (matchedQuery) {
|
||||
matchedQuery.color = queryColor
|
||||
matchedQuery.colorHex = queryColorHex
|
||||
}
|
||||
} else matchedQuery = null
|
||||
}
|
||||
$: {
|
||||
$highlightingFull = watchlist.length >= 5
|
||||
}
|
||||
|
||||
function init () {
|
||||
const val = localStorage.getItem('highlight')
|
||||
if (val != null) {
|
||||
try {
|
||||
watchlist = JSON.parse(val)
|
||||
watchlist.forEach(q => {
|
||||
const i = highlightHexColors.findIndex(c => c === q.colorHex)
|
||||
if (i >= 0) usedColors[i] = q.colorHex
|
||||
else console.log('unknown color')
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('failed to parse cached highlight queries')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setNextColor () {
|
||||
const nextIndex = usedColors.findIndex(used => !used)
|
||||
if (nextIndex >= 0) {
|
||||
queryColor = highlightColors[nextIndex]
|
||||
queryColorHex = highlightHexColors[nextIndex]
|
||||
usedColors[nextIndex] = queryColorHex
|
||||
}
|
||||
}
|
||||
function clearUsedColor (hex) {
|
||||
const clearIndex = usedColors.findIndex(used => used === hex)
|
||||
usedColors[clearIndex] = false
|
||||
}
|
||||
|
||||
function hclToHex (color) {
|
||||
return hcl(color.h * 360, 78.225, color.l * 150).hex()
|
||||
}
|
||||
|
||||
async function add () {
|
||||
if (matchedQuery && !$highlightingFull) {
|
||||
watchlist.push({
|
||||
...matchedQuery
|
||||
})
|
||||
watchlist = watchlist
|
||||
query = null
|
||||
setNextColor()
|
||||
if (tab) {
|
||||
await tick()
|
||||
tab.updateContentHeight(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function remove (index) {
|
||||
const wasFull = $highlightingFull
|
||||
const removed = watchlist.splice(index,1)
|
||||
if (removed.length) {
|
||||
clearUsedColor(removed[0].colorHex)
|
||||
watchlist = watchlist
|
||||
if (tab) {
|
||||
await tick()
|
||||
tab.updateContentHeight(true)
|
||||
}
|
||||
if (wasFull) setNextColor()
|
||||
}
|
||||
}
|
||||
|
||||
function searchSubmit (e) {
|
||||
e.preventDefault()
|
||||
add()
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/scss">
|
||||
.search {
|
||||
width: 274px;
|
||||
|
||||
.watched, .input-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
--input-color: var(--bold-a);
|
||||
|
||||
.search-form {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.search-submit {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
color: var(--input-color);
|
||||
width: 100%;
|
||||
|
||||
&.disabled {
|
||||
color: var(--palette-e);
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.query {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
font-size: 24px;
|
||||
margin: 7px;
|
||||
transition: opacity 300ms, color 300ms, background 300ms;
|
||||
color: var(--input-color);
|
||||
|
||||
&.add-query {
|
||||
color: var(--light-good);
|
||||
}
|
||||
&.remove-query {
|
||||
color: var(--light-bad);
|
||||
}
|
||||
&.icon-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.icon-button {
|
||||
background: var(--palette-d);
|
||||
padding: 3px;
|
||||
border-radius: 5px;
|
||||
&:hover {
|
||||
background: var(--palette-e);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--palette-e);
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
border-bottom: solid 3px var(--input-color);
|
||||
|
||||
&.full {
|
||||
border-color: var(--palette-e);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="search tab-content">
|
||||
<div class="input-wrapper" class:full={$highlightingFull} style="--input-color: {queryColorHex};">
|
||||
{#if !$highlightingFull }
|
||||
<form class="search-form" action="" on:submit={searchSubmit}>
|
||||
<input class="search-input" type="text" bind:value={query} placeholder="Enter an address or txid...">
|
||||
<button type="submit" class="search-submit" />
|
||||
</form>
|
||||
{:else}
|
||||
<input class="search-input disabled" type="text" placeholder="Watchlist is full">
|
||||
{/if}
|
||||
<div class="input-icon add-query icon-button" class:disabled={matchedQuery == null || $highlightingFull} on:click={add} title="Add to watchlist">
|
||||
<Icon icon={PlusIcon}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="watchlist">
|
||||
{#each watchlist as watched, index (watched.colorHex)}
|
||||
<div
|
||||
class="watched"
|
||||
transition:fade={{ duration: 200 }}
|
||||
animate:flip={{ duration: 200 }}
|
||||
>
|
||||
<div class="input-icon query-type" style="color: {watched.colorHex};" title={queryType[watched.query]}>
|
||||
<Icon icon={queryIcons[watched.query]} />
|
||||
</div>
|
||||
<span class="query" style="color: {watched.colorHex};">{ watched.value }</span>
|
||||
<div class="input-icon remove-query icon-button" on:click={() => remove(index)} title="Remove from watchlist">
|
||||
<Icon icon={CrossIcon} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
@ -15,11 +15,15 @@ import atIcon from '../assets/icon/cil-at.svg'
|
||||
import gridIcon from '../assets/icon/grid-icon.svg'
|
||||
import peopleIcon from '../assets/icon/cil-people.svg'
|
||||
import giftIcon from '../assets/icon/cil-gift.svg'
|
||||
import searchIcon from '../assets/icon/cil-search.svg'
|
||||
import MempoolLegend from '../components/MempoolLegend.svelte'
|
||||
import ContactTab from '../components/ContactTab.svelte'
|
||||
import SearchTab from '../components/SearchTab.svelte'
|
||||
|
||||
import { sidebarToggle, overlay, currentBlock, blockVisible, haveSupporters } from '../stores.js'
|
||||
|
||||
let searchTabComponent
|
||||
|
||||
let blockHidden = false
|
||||
$: blockHidden = ($currentBlock && !$blockVisible)
|
||||
|
||||
@ -112,6 +116,14 @@ function showBlock () {
|
||||
<MempoolLegend />
|
||||
</div>
|
||||
</SidebarTab>
|
||||
<SidebarTab open={$sidebarToggle === 'search'} on:click={() => {settings('search')}} tooltip="Search & Highlight" bind:this={searchTabComponent}>
|
||||
<span slot="tab" title="Search & Highlight">
|
||||
<Icon icon={searchIcon} color="var(--bold-a)" />
|
||||
</span>
|
||||
<div slot="content">
|
||||
<SearchTab tab={searchTabComponent} />
|
||||
</div>
|
||||
</SidebarTab>
|
||||
<SidebarTab open={$sidebarToggle === 'settings'} on:click={() => {settings('settings')}} tooltip="Settings">
|
||||
<span slot="tab" title="Settings">
|
||||
<Icon icon={cogIcon} color="var(--bold-a)" />
|
||||
|
@ -9,7 +9,7 @@ let entered = false
|
||||
let contentElement
|
||||
let contentSlotElement
|
||||
|
||||
async function updateContentHeight (isOpen) {
|
||||
export async function updateContentHeight (isOpen) {
|
||||
if (contentElement && contentSlotElement) {
|
||||
if (isOpen) {
|
||||
contentElement.style.height = `${contentSlotElement.clientHeight}px`
|
||||
|
@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import Icon from './Icon.svelte'
|
||||
import BookmarkIcon from '../assets/icon/cil-bookmark.svg'
|
||||
import { longBtcFormat, integerFormat } from '../utils/format.js'
|
||||
import { exchangeRates, settings } from '../stores.js'
|
||||
import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull } from '../stores.js'
|
||||
import { formatCurrency } from '../utils/fx.js'
|
||||
|
||||
export let tx
|
||||
@ -32,6 +34,13 @@ $: {
|
||||
function formatBTC (sats) {
|
||||
return `₿ ${longBtcFormat.format(sats/100000000)}`
|
||||
}
|
||||
|
||||
function highlight () {
|
||||
if (!$highlightingFull && tx && tx.id) {
|
||||
$newHighlightQuery = tx.id
|
||||
$sidebarToggle = 'search'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/scss">
|
||||
@ -81,10 +90,32 @@ function formatBTC (sats) {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
float: right;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
transition: opacity 300ms, color 300ms, background 300ms;
|
||||
background: var(--palette-c);
|
||||
color: var(--bold-a);
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
&:hover {
|
||||
background: var(--palette-e);
|
||||
}
|
||||
&.disabled {
|
||||
color: var(--palette-e);
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="tx-info" class:above style="left: {clampedX}px; top: {clampedY}px">
|
||||
<div class="icon-button" class:disabled={$highlightingFull} on:click={highlight} title="Add to watchlist">
|
||||
<Icon icon={BookmarkIcon}/>
|
||||
</div>
|
||||
<p class="field hash">
|
||||
TxID: { tx.id }
|
||||
</p>
|
||||
|
@ -5,7 +5,7 @@ import BitcoinTx from '../models/BitcoinTx.js'
|
||||
import BitcoinBlock from '../models/BitcoinBlock.js'
|
||||
import TxSprite from '../models/TxSprite.js'
|
||||
import { FastVertexArray } from '../utils/memory.js'
|
||||
import { txQueueLength, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, blockAreaSize } from '../stores.js'
|
||||
import { txQueueLength, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, blockAreaSize, highlight } from '../stores.js'
|
||||
import config from "../config.js"
|
||||
|
||||
export default class TxController {
|
||||
@ -31,6 +31,11 @@ export default class TxController {
|
||||
this.queueTimeout = null
|
||||
this.queueLength = 0
|
||||
|
||||
highlight.subscribe(criteria => {
|
||||
this.highlightCriteria = criteria
|
||||
this.applyHighlighting()
|
||||
})
|
||||
|
||||
this.scheduleQueue(1000)
|
||||
}
|
||||
|
||||
@ -60,8 +65,16 @@ export default class TxController {
|
||||
this.redoLayout({ width, height })
|
||||
}
|
||||
|
||||
applyHighlighting () {
|
||||
this.poolScene.applyHighlighting(this.highlightCriteria)
|
||||
if (this.blockScene) {
|
||||
this.blockScene.applyHighlighting(this.highlightCriteria)
|
||||
}
|
||||
}
|
||||
|
||||
addTx (txData) {
|
||||
const tx = new BitcoinTx(txData, this.vertexArray)
|
||||
tx.applyHighlighting(this.highlightCriteria)
|
||||
if (!this.txs[tx.id] && !this.expiredTxs[tx.id]) {
|
||||
this.pendingTxs.push([tx, performance.now()])
|
||||
this.pendingTxs[tx.id] = tx
|
||||
@ -169,6 +182,7 @@ export default class TxController {
|
||||
block: block.id
|
||||
}, this.vertexArray)
|
||||
this.txs[tx.id] = tx
|
||||
this.txs[tx.id].applyHighlighting(this.highlightCriteria)
|
||||
this.blockScene.insert(tx, false)
|
||||
}
|
||||
this.expiredTxs[block.txns[i].id] = true
|
||||
|
@ -1,6 +1,15 @@
|
||||
import TxView from './TxView.js'
|
||||
import config from '../config.js'
|
||||
|
||||
const highlightColor = {
|
||||
h: 0.03,
|
||||
l: 0.35
|
||||
}
|
||||
const hoverColor = {
|
||||
h: 0.4,
|
||||
l: 0.42
|
||||
}
|
||||
|
||||
export default class BitcoinTx {
|
||||
constructor ({ version, id, value, vbytes, inputs, outputs, time, block }, vertexArray) {
|
||||
this.version = version
|
||||
@ -19,16 +28,7 @@ export default class BitcoinTx {
|
||||
}
|
||||
|
||||
this.time = time
|
||||
|
||||
// Highlight transactions to the static donation address
|
||||
// if (config.donationHash && this.outputs) {
|
||||
// this.outputs.forEach(output => {
|
||||
// if (output.script_pub_key.includes(config.donationHash)) {
|
||||
// console.log('donation!', this)
|
||||
// this.highlight = true
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
this.highlight = false
|
||||
|
||||
// is a coinbase transaction?
|
||||
if (this.inputs && this.inputs.length === 1 && this.inputs[0].prev_txid === "0000000000000000000000000000000000000000000000000000000000000000") {
|
||||
@ -70,11 +70,40 @@ export default class BitcoinTx {
|
||||
this.state = this.block ? 'block' : 'pool'
|
||||
}
|
||||
|
||||
hoverOn () {
|
||||
if (this.view) this.view.setHover(true)
|
||||
hoverOn (color = hoverColor) {
|
||||
if (this.view) this.view.setHover(true, color)
|
||||
}
|
||||
|
||||
hoverOff () {
|
||||
if (this.view) this.view.setHover(false)
|
||||
}
|
||||
|
||||
highlightOn (color = highlightColor) {
|
||||
if (this.view) this.view.setHighlight(true, color)
|
||||
this.highlight = true
|
||||
}
|
||||
|
||||
highlightOff () {
|
||||
if (this.view) this.view.setHighlight(false)
|
||||
this.highlight = false
|
||||
}
|
||||
|
||||
applyHighlighting (criteria) {
|
||||
let color
|
||||
this.highlight = false
|
||||
criteria.forEach(criterion => {
|
||||
if (criterion.txid === this.id) {
|
||||
this.highlight = true
|
||||
color = criterion.color
|
||||
} else if (criterion.address && criterion.scriptPubKey) {
|
||||
this.outputs.forEach(output => {
|
||||
if (output.script_pub_key === criterion.scriptPubKey) {
|
||||
this.highlight = true
|
||||
color = criterion.color
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
this.view.setHighlight(this.highlight, color || highlightColor)
|
||||
}
|
||||
}
|
||||
|
@ -322,4 +322,13 @@ export default class TxPoolScene {
|
||||
selectAt (position) {
|
||||
return null
|
||||
}
|
||||
|
||||
applyHighlighting (criteria) {
|
||||
Object.values(this.txs).forEach(tx => {
|
||||
tx.applyHighlighting(criteria)
|
||||
})
|
||||
Object.values(this.hiddenTxs).forEach(tx => {
|
||||
tx.applyHighlighting(criteria)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import TxSprite from './TxSprite.js'
|
||||
|
||||
const hoverTransitionTime = 300
|
||||
const highlightTransitionTime = 300
|
||||
|
||||
// converts from this class's update format to TxSprite's update format
|
||||
// now, id, value, position, size, color, alpha, duration, adjust
|
||||
@ -22,6 +22,9 @@ export default class TxView {
|
||||
this.vbytes = vbytes
|
||||
this.initialised = false
|
||||
this.vertexArray = vertexArray
|
||||
|
||||
this.hover = false
|
||||
this.highlight = false
|
||||
}
|
||||
|
||||
destroy () {
|
||||
@ -35,9 +38,9 @@ export default class TxView {
|
||||
display: defines the final appearance of the sprite
|
||||
position: { x, y }
|
||||
size: in pixels
|
||||
color: (in HCL space)
|
||||
h: hue
|
||||
l: lightness
|
||||
color:
|
||||
i: x coord in color texture
|
||||
j: y coord in color texture
|
||||
alpha: alpha transparency
|
||||
duration: of the tweening animation from the previous display state
|
||||
delay: for queued transitions, how long to wait after current transition
|
||||
@ -53,6 +56,22 @@ export default class TxView {
|
||||
toSpriteUpdate(display, duration, delay, start),
|
||||
this.vertexArray
|
||||
)
|
||||
// apply any pending modifications
|
||||
if (this.hover) {
|
||||
this.sprite.update({
|
||||
...this.highlightColor,
|
||||
duration: highlightTransitionTime,
|
||||
adjust: false,
|
||||
modify: true
|
||||
})
|
||||
} else if (this.highlight) {
|
||||
this.sprite.update({
|
||||
...this.highlightColor,
|
||||
duration: highlightTransitionTime,
|
||||
adjust: false,
|
||||
modify: true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.sprite.update(
|
||||
toSpriteUpdate(display, duration, delay, start, adjust)
|
||||
@ -60,19 +79,53 @@ export default class TxView {
|
||||
}
|
||||
}
|
||||
|
||||
setHover (hoverOn) {
|
||||
setHover (hoverOn, color) {
|
||||
if (hoverOn) {
|
||||
if (this.sprite) {
|
||||
this.sprite.update({
|
||||
h: 1.0,
|
||||
l: 0.4,
|
||||
duration: hoverTransitionTime,
|
||||
modify: true
|
||||
})
|
||||
this.hover = true
|
||||
this.hoverColor = color
|
||||
this.sprite.update({
|
||||
...this.hoverColor,
|
||||
duration: highlightTransitionTime,
|
||||
adjust: false,
|
||||
modify: true
|
||||
})
|
||||
} else {
|
||||
this.hover = false
|
||||
this.hoverColor = null
|
||||
if (this.highlight) {
|
||||
if (this.sprite) {
|
||||
this.sprite.update({
|
||||
...this.highlightColor,
|
||||
duration: highlightTransitionTime,
|
||||
adjust: false,
|
||||
modify: true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (this.sprite) this.sprite.resume(highlightTransitionTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setHighlight (highlightOn, color) {
|
||||
if (highlightOn) {
|
||||
this.highlight = true
|
||||
this.highlightColor = color
|
||||
if (!this.hover) {
|
||||
if (this.sprite) {
|
||||
this.sprite.update({
|
||||
...this.highlightColor,
|
||||
duration: highlightTransitionTime,
|
||||
adjust: false,
|
||||
modify: true
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.sprite) {
|
||||
this.sprite.resume(hoverTransitionTime)
|
||||
this.highlight = false
|
||||
this.highlightColor = null
|
||||
if (!this.hover) {
|
||||
if (this.sprite) this.sprite.resume(highlightTransitionTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,3 +119,7 @@ export const nativeAntialias = writable(false)
|
||||
const newVisitor = !localStorage.getItem('seen-welcome-msg')
|
||||
// export const overlay = writable(newVisitor ? 'about' : null)
|
||||
export const overlay = writable(null)
|
||||
|
||||
export const highlight = writable([])
|
||||
export const newHighlightQuery = writable(null)
|
||||
export const highlightingFull = writable(false)
|
||||
|
27
client/src/utils/encodings.js
Normal file
27
client/src/utils/encodings.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { Buffer } from 'buffer/'
|
||||
window.Buffer = Buffer
|
||||
import bech32 from 'bech32-buffer'
|
||||
import { base58_to_binary } from 'base58-js'
|
||||
|
||||
// Extract a raw script hash from an address
|
||||
export function addressToSPK (address) {
|
||||
if (address.startsWith('bc1')) {
|
||||
const result = bech32.BitcoinAddress.decode(address)
|
||||
let prefix
|
||||
if (result.scriptVersion == 1) prefix = '5120' // taproot (OP_PUSHNUM_1 OP_PUSHBYTES_32)
|
||||
else if (address.length == 62) prefix = '0020' // p2wsh (OP_0 OP_PUSHBYTES_32)
|
||||
else prefix = '0014' // p2wpkh (OP_0 OP_PUSHBYTES_20)
|
||||
return prefix + Buffer.from(result.data).toString('hex')
|
||||
} else {
|
||||
const result = base58_to_binary(address)
|
||||
let prefix, postfix
|
||||
if (address.charAt(0) === '1') {
|
||||
prefix = '76a914' // p2pkh (OP_DUP OP_HASH160 OP_PUSHBYTES_20)
|
||||
postfix = '88ac' // p2pkh (OP_EQUALVERIFY OP_CHECKSIG)
|
||||
} else {
|
||||
prefix = 'a914' // p2sh (OP_HASH160 OP_PUSHBYTES_20)
|
||||
postfix = '87' // p2sh (OP_EQUAL)
|
||||
}
|
||||
return prefix + Buffer.from(result).toString('hex').slice(2, -8) + postfix
|
||||
}
|
||||
}
|
81
client/src/utils/search.js
Normal file
81
client/src/utils/search.js
Normal file
@ -0,0 +1,81 @@
|
||||
import { addressToSPK } from './encodings.js'
|
||||
|
||||
// Quick heuristic matching to guess what kind of search a query is for
|
||||
// ***does not validate that a given address/txid/block is valid***
|
||||
export function matchQuery (query) {
|
||||
if (!query || !query.length) return
|
||||
|
||||
const q = query.toLowerCase()
|
||||
|
||||
// Looks like a block height?
|
||||
const asInt = parseInt(q)
|
||||
// Remember to update the bounds in
|
||||
if (!isNaN(asInt) && asInt >= 0 && `${asInt}` === q) {
|
||||
return null /*{
|
||||
query: 'blockheight',
|
||||
height: asInt,
|
||||
value: asInt
|
||||
}*/
|
||||
}
|
||||
|
||||
// Looks like a block hash?
|
||||
if (/^0{8}[a-f0-9]{56}$/.test(q)) {
|
||||
return null /* {
|
||||
query: 'blockhash',
|
||||
hash: query,
|
||||
value: query,
|
||||
}*/
|
||||
}
|
||||
|
||||
// Looks like a transaction id?
|
||||
if (/^[a-f0-9]{64}$/.test(q)) {
|
||||
return {
|
||||
query: 'txid',
|
||||
txid: q,
|
||||
value: q
|
||||
}
|
||||
}
|
||||
|
||||
// Looks like an address
|
||||
if ((q.length >= 26 && q.length <= 34) || q.length === 42 || q.length === 62) {
|
||||
// Looks like a legacy address
|
||||
if (/^[13]\w{24,33}$/.test(q)) {
|
||||
let addressType
|
||||
if (q[0] === '1') addressType = 'p2pkh'
|
||||
else if (q[0] === '3') addressType = 'p2sh'
|
||||
else return null
|
||||
|
||||
return {
|
||||
query: 'address',
|
||||
encoding: 'base58',
|
||||
addressType,
|
||||
address: query,
|
||||
value: query,
|
||||
scriptPubKey: addressToSPK(query)
|
||||
}
|
||||
}
|
||||
|
||||
// Looks like a bech32 address
|
||||
if (/^bc1\w{39}(\w{20})?$/.test(q)) {
|
||||
let addressType
|
||||
if (q.startsWith('bc1q')) {
|
||||
if (q.length === 42) addressType = 'p2wpkh'
|
||||
else if (q.length === 62) addressType = 'p2wsh'
|
||||
else return null
|
||||
} else if (q.startsWith('bc1p') && q.length === 62) {
|
||||
addressType = 'p2tr'
|
||||
} else return null
|
||||
|
||||
return {
|
||||
query: 'address',
|
||||
encoding: 'bech32',
|
||||
addressType,
|
||||
address: query,
|
||||
value: query,
|
||||
scriptPubKey: addressToSPK(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
Loading…
Reference in New Issue
Block a user