Merge pull request #42 from bitfeed-project/address-types

Address types
This commit is contained in:
Mononaut 2022-04-28 23:32:28 +01:00 committed by GitHub
commit 30b7c8d42f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 154 additions and 38 deletions

View File

@ -4,15 +4,26 @@ import { fade } from 'svelte/transition'
import { flip } from 'svelte/animate' import { flip } from 'svelte/animate'
import Icon from './Icon.svelte' import Icon from './Icon.svelte'
import SearchIcon from '../assets/icon/cil-search.svg' import SearchIcon from '../assets/icon/cil-search.svg'
import CrossIcon from '../assets/icon/cil-x.svg' import CrossIcon from '../assets/icon/cil-x-circle.svg'
import AddressIcon from '../assets/icon/cil-wallet.svg' import AddressIcon from '../assets/icon/cil-wallet.svg'
import TxIcon from '../assets/icon/cil-arrow-circle-right.svg' import TxIcon from '../assets/icon/cil-arrow-circle-right.svg'
import BlockIcon from '../assets/icon/grid-icon.svg'
import { fly } from 'svelte/transition' import { fly } from 'svelte/transition'
import { matchQuery, searchTx, searchBlock } from '../utils/search.js' import { matchQuery, searchTx, searchBlock } from '../utils/search.js'
import { selectedTx, detailTx, overlay, loading } from '../stores.js' import { selectedTx, detailTx, overlay, loading } from '../stores.js'
const queryIcons = {
txid: TxIcon,
input: TxIcon,
output: TxIcon,
// address: AddressIcon,
blockhash: BlockIcon,
blockheight: BlockIcon,
}
let query let query
let matchedQuery let matchedQuery
let errorMessage
$: { $: {
if (query) { if (query) {
@ -20,27 +31,48 @@ $: {
} else { } else {
matchedQuery = null matchedQuery = null
} }
errorMessage = null
}
function clearInput () {
query = null
}
function handleSearchError (err) {
switch (err) {
case '404':
if (matchedQuery && matchedQuery.label) {
errorMessage = `${matchedQuery.label} not found`
}
break;
default:
errorMessage = 'server error'
}
} }
async function searchSubmit (e) { async function searchSubmit (e) {
e.preventDefault() e.preventDefault()
if (matchedQuery) { if (matchedQuery && matchedQuery.query !== 'address') {
$loading++ $loading++
let searchErr
switch(matchedQuery.query) { switch(matchedQuery.query) {
case 'txid': case 'txid':
await searchTx(matchedQuery.txid) searchErr = await searchTx(matchedQuery.txid)
break; break;
case 'input': case 'input':
await searchTx(matchedQuery.txid, matchedQuery.input, null) searchErr = await searchTx(matchedQuery.txid, matchedQuery.input, null)
break; break;
case 'output': case 'output':
await searchTx(matchedQuery.txid, null, matchedQuery.output) searchErr = await searchTx(matchedQuery.txid, null, matchedQuery.output)
break; break;
} }
if (searchErr != null) handleSearchError(searchErr)
$loading-- $loading--
} else {
errorMessage = 'enter a transaction id, block hash or block height'
} }
return false return false
@ -58,6 +90,22 @@ async function searchSubmit (e) {
max-width: 600px; max-width: 600px;
margin: 0 1em; margin: 0 1em;
.clear-button {
position: absolute;
right: 0;
bottom: .4em;
margin: 0;
color: var(--palette-bad);
font-size: 1.2em;
cursor: pointer;
opacity: 1;
transition: opacity 300ms;
&.disabled {
opacity: 0;
}
}
.input-icon { .input-icon {
font-size: 24px; font-size: 24px;
margin: 0 10px; margin: 0 10px;
@ -115,10 +163,28 @@ async function searchSubmit (e) {
transition: width 300ms; transition: width 300ms;
} }
} }
.error-msg {
position: absolute;
left: 0;
top: 100%;
margin: 0;
font-size: 0.9em;
color: var(--palette-bad);
}
.input-icon.query-type {
position: absolute;
left: 0;
bottom: .4em;
margin: 0;
color: var(--palette-x);
font-size: 1.2em;
}
} }
&:hover, &:active, &:focus { .search-input:active, .search-input:focus {
.underline.active { & ~ .underline.active {
width: 100%; width: 100%;
} }
} }
@ -130,6 +196,8 @@ async function searchSubmit (e) {
margin: 0; margin: 0;
color: var(--input-color); color: var(--input-color);
width: 100%; width: 100%;
padding-left: 1.5em;
padding-right: 1.5em;
&.disabled { &.disabled {
color: var(--palette-e); color: var(--palette-e);
@ -143,9 +211,20 @@ async function searchSubmit (e) {
<div class="input-wrapper" transition:fly={{ y: -25 }}> <div class="input-wrapper" transition:fly={{ y: -25 }}>
<form class="search-form" action="" on:submit={searchSubmit}> <form class="search-form" action="" on:submit={searchSubmit}>
<input class="search-input" type="text" bind:value={query} placeholder="Enter a txid"> <input class="search-input" type="text" bind:value={query} placeholder="Enter a txid">
<div class="clear-button" class:disabled={query == null || query === ''} on:click={clearInput} title="Clear">
<Icon icon={CrossIcon}/>
</div>
<div class="underline" /> <div class="underline" />
<div class="underline active" /> <div class="underline active" />
<button type="submit" class="search-submit" /> <button type="submit" class="search-submit" />
{#if matchedQuery && matchedQuery.query && queryIcons[matchedQuery.query]}
<div class="input-icon query-type" transition:fade={{ duration: 300 }} title={matchedQuery.label}>
<Icon icon={queryIcons[matchedQuery.query]} />
</div>
{/if}
{#if errorMessage }
<p class="error-msg" transition:fade={{ duration: 300 }}>{ errorMessage }</p>
{/if}
</form> </form>
<div class="input-icon search icon-button" on:click={searchSubmit} title="Search"> <div class="input-icon search icon-button" on:click={searchSubmit} title="Search">
<Icon icon={SearchIcon}/> <Icon icon={SearchIcon}/>

View File

@ -42,6 +42,7 @@ setNextColor()
$: { $: {
if ($newHighlightQuery) { if ($newHighlightQuery) {
matchedQuery = matchQuery($newHighlightQuery) matchedQuery = matchQuery($newHighlightQuery)
if (matchedQuery && (matchedQuery.query === 'blockhash' || matchedQuery.query === 'blockheight')) matchedQuery = null
if (matchedQuery) { if (matchedQuery) {
matchedQuery.colorIndex = queryColorIndex matchedQuery.colorIndex = queryColorIndex
matchedQuery.color = highlightColors[queryColorIndex] matchedQuery.color = highlightColors[queryColorIndex]
@ -61,6 +62,7 @@ $: {
$: { $: {
if (query) { if (query) {
matchedQuery = matchQuery(query.trim()) matchedQuery = matchQuery(query.trim())
if (matchedQuery && (matchedQuery.query === 'blockhash' || matchedQuery.query === 'blockheight')) matchedQuery = null
if (matchedQuery) { if (matchedQuery) {
matchedQuery.colorIndex = queryColorIndex matchedQuery.colorIndex = queryColorIndex
matchedQuery.color = highlightColors[queryColorIndex] matchedQuery.color = highlightColors[queryColorIndex]
@ -113,7 +115,7 @@ function clearUsedColor (colorIndex) {
} }
async function add () { async function add () {
if (matchedQuery && !$highlightingFull) { if (matchedQuery && matchedQuery.query !== 'blockhash' && matchedQuery.query !== 'blockheight' && !$highlightingFull) {
watchlist.push({ watchlist.push({
...matchedQuery ...matchedQuery
}) })

View File

@ -11,6 +11,7 @@ import { searchTx } from '../utils/search.js'
function onClose () { function onClose () {
$detailTx = null $detailTx = null
$highlightInOut = null
} }
function formatBTC (sats) { function formatBTC (sats) {
@ -69,9 +70,12 @@ function expandAddresses(items, truncate) {
if (item.script_pub_key) { if (item.script_pub_key) {
address = SPKToAddress(item.script_pub_key) || "unknown" address = SPKToAddress(item.script_pub_key) || "unknown"
if (address === 'OP_RETURN') { if (address === 'OP_RETURN') {
title = item.script_pub_key.substring(2).match(/../g).reduce((parsed, hexChar) => { title = item.script_pub_key.substring(4).match(/../g).reduce((parsed, hexChar) => {
return parsed + String.fromCharCode(parseInt(hexChar, 16)) return parsed + String.fromCharCode(parseInt(hexChar, 16))
}, "") }, "")
} else if (address === 'P2PK') {
if (item.script_pub_key.length === 70) title = 'compressed pubkey: ' + item.script_pub_key.substring(2,68)
else title = 'pubkey: ' + item.script_pub_key.substring(2,132)
} }
} }
return { return {
@ -113,7 +117,7 @@ $: {
} }
} else inputs = [] } else inputs = []
if ($detailTx && $detailTx.outputs) { if ($detailTx && $detailTx.outputs) {
if ($detailTx.isCoinbase || !$detailTx.is_inflated || $detailTx.fee == null) { if ($detailTx.isCoinbase || !$detailTx.is_inflated || !$detailTx.fee) {
outputs = expandAddresses($detailTx.outputs, truncate) outputs = expandAddresses($detailTx.outputs, truncate)
} else { } else {
outputs = [{address: 'fee', value: $detailTx.fee, fee: true}, ...expandAddresses($detailTx.outputs, truncate)] outputs = [{address: 'fee', value: $detailTx.fee, fee: true}, ...expandAddresses($detailTx.outputs, truncate)]
@ -580,7 +584,7 @@ async function clickItem (item) {
<p class="header">{$detailTx.inputs.length} input{$detailTx.inputs.length > 1 ? 's' : ''}</p> <p class="header">{$detailTx.inputs.length} input{$detailTx.inputs.length > 1 ? 's' : ''}</p>
{#each inputs as input} {#each inputs as input}
<div class="entry clickable" on:click={() => clickItem(input)}> <div class="entry clickable" on:click={() => clickItem(input)}>
<p class="address" title={input.address}><span class="truncatable">{input.address.slice(0,-6)}</span><span class="suffix">{input.address.slice(-6)}</span></p> <p class="address" title={input.title || input.address}><span class="truncatable">{input.address.slice(0,-6)}</span><span class="suffix">{input.address.slice(-6)}</span></p>
<p class="amount">{ input.value == null ? '???' : formatBTC(input.value) }</p> <p class="amount">{ input.value == null ? '???' : formatBTC(input.value) }</p>
</div> </div>
{/each} {/each}
@ -613,7 +617,7 @@ async function clickItem (item) {
{/if} {/if}
</div> </div>
<div class="column outputs"> <div class="column outputs">
<p class="header">{$detailTx.outputs.length} output{$detailTx.outputs.length > 1 ? 's' : ''} {#if $detailTx.fee != null}+ fee{/if}</p> <p class="header">{$detailTx.outputs.length} output{$detailTx.outputs.length > 1 ? 's' : ''} {#if $detailTx.fee}+ fee{/if}</p>
{#each outputs as output} {#each outputs as output}
<div class="entry" class:clickable={output.rest || output.spend} class:unspent={!output.spend && !output.fee} class:highlight={highlight.out != null && highlight.out === output.index} on:click={() => clickItem(output)}> <div class="entry" class:clickable={output.rest || output.spend} class:unspent={!output.spend && !output.fee} class:highlight={highlight.out != null && highlight.out === output.index} on:click={() => clickItem(output)}>
<p class="address" title={output.title || output.address}><span class="truncatable">{output.address.slice(0,-6)}</span><span class="suffix">{output.address.slice(-6)}</span></p> <p class="address" title={output.title || output.address}><span class="truncatable">{output.address.slice(0,-6)}</span><span class="suffix">{output.address.slice(-6)}</span></p>

View File

@ -6,6 +6,7 @@
box-sizing: border-box; box-sizing: border-box;
border-radius: 50%; border-radius: 50%;
border: solid 2px var(--palette-x); border: solid 2px var(--palette-x);
pointer-events: none;
.sizer { .sizer {
width: 100%; width: 100%;

View File

@ -106,7 +106,6 @@ export default class TxController {
dropTx (txid) { dropTx (txid) {
if (this.txs[txid] && this.poolScene.drop(txid)) { if (this.txs[txid] && this.poolScene.drop(txid)) {
console.log('dropping tx', txid)
this.txs[txid].view.update({ this.txs[txid].view.update({
display: { display: {
position: { position: {
@ -123,8 +122,6 @@ export default class TxController {
this.destroyTx(txid) this.destroyTx(txid)
}, 2000) }, 2000)
// this.poolScene.layoutAll() // this.poolScene.layoutAll()
} else {
console.log('dropped unknown tx', txid)
} }
} }

View File

@ -48,6 +48,24 @@ export function SPKToAddress (spk) {
} else if (spk.startsWith('6a')) { } else if (spk.startsWith('6a')) {
// OP_RETURN // OP_RETURN
return 'OP_RETURN' return 'OP_RETURN'
} else if (spk.length == 134 && spk.startsWith('41') && spk.endsWith('ac')) {
// uncompressed p2pk
return 'P2PK'
} else if (spk.length == 70 && spk.startsWith('21') && spk.endsWith('ac')) {
// compressed p2pk
return 'P2PK'
} else if (spk.endsWith('51ae') && spk.startsWith('51')) {
// possible p2ms (raw multisig)
return '1-of-1 P2MS'
} else if (spk.endsWith('52ae')) {
// possible p2ms (raw multisig)
if (spk.startsWith(51)) return '1-of-2 P2MS'
if (spk.startsWith(52)) return '2-of-2 P2MS'
} else if (spk.endsWith('53ae')) {
// possible p2ms (raw multisig)
if (spk.startsWith(51)) return '1-of-3 P2MS'
if (spk.startsWith(52)) return '2-of-3 P2MS'
if (spk.startsWith(53)) return '3-of-3 P2MS'
} }
} }

View File

@ -14,20 +14,22 @@ function matchQuery (query) {
const asInt = parseInt(q) const asInt = parseInt(q)
// Remember to update the bounds in // Remember to update the bounds in
if (!isNaN(asInt) && asInt >= 0 && `${asInt}` === q) { if (!isNaN(asInt) && asInt >= 0 && `${asInt}` === q) {
return null /*{ return {
query: 'blockheight', query: 'blockheight',
label: 'block height',
height: asInt, height: asInt,
value: asInt value: asInt
}*/ }
} }
// Looks like a block hash? // Looks like a block hash?
if (/^0{8}[a-f0-9]{56}$/.test(q)) { if (/^0{8}[a-f0-9]{56}$/.test(q)) {
return null /* { return {
query: 'blockhash', query: 'blockhash',
label: 'block hash',
hash: query, hash: query,
value: query, value: query,
}*/ }
} }
// Looks like a transaction input? // Looks like a transaction input?
@ -35,6 +37,7 @@ function matchQuery (query) {
const parts = q.split(':') const parts = q.split(':')
return { return {
query: 'input', query: 'input',
label: 'transaction input',
txid: parts[1], txid: parts[1],
output: parts[0], output: parts[0],
value: q value: q
@ -46,6 +49,7 @@ function matchQuery (query) {
const parts = q.split(':') const parts = q.split(':')
return { return {
query: 'output', query: 'output',
label: 'transaction output',
txid: parts[0], txid: parts[0],
output: parts[1], output: parts[1],
value: q value: q
@ -56,6 +60,7 @@ function matchQuery (query) {
if (/^[a-f0-9]{64}$/.test(q)) { if (/^[a-f0-9]{64}$/.test(q)) {
return { return {
query: 'txid', query: 'txid',
label: 'transaction',
txid: q, txid: q,
value: q value: q
} }
@ -72,6 +77,7 @@ function matchQuery (query) {
return { return {
query: 'address', query: 'address',
label: 'address',
encoding: 'base58', encoding: 'base58',
addressType, addressType,
address: query, address: query,
@ -93,6 +99,7 @@ function matchQuery (query) {
return { return {
query: 'address', query: 'address',
label: 'address',
encoding: 'bech32', encoding: 'bech32',
addressType, addressType,
address: query, address: query,
@ -108,30 +115,37 @@ export {matchQuery as matchQuery}
async function fetchTx (txid) { async function fetchTx (txid) {
if (!txid) return if (!txid) return
try { const response = await fetch(`${api.uri}/api/tx/${txid}`, {
const response = await fetch(`${api.uri}/api/tx/${txid}`, { method: 'GET'
method: 'GET' })
}) if (!response) throw new Error('null response')
if (response.status == 200) {
const result = await response.json() const result = await response.json()
const txData = result.tx const txData = result.tx
txData.block = { height: result.blockheight, hash: result.blockhash, time: result.time * 1000 } if (result.blockheight != null && result.blockhash != null) {
txData.block = { height: result.blockheight, hash: result.blockhash, time: result.time * 1000 }
}
return new BitcoinTx(txData, null, (txData.inputs && txData.inputs[0] && txData.inputs[0].prev_txid === "0000000000000000000000000000000000000000000000000000000000000000")) return new BitcoinTx(txData, null, (txData.inputs && txData.inputs[0] && txData.inputs[0].prev_txid === "0000000000000000000000000000000000000000000000000000000000000000"))
} catch (err) { } else {
console.log("failed to fetch tx ", txid) throw new Error(response.status)
return null
} }
} }
export async function searchTx(txid, input, output) { export async function searchTx(txid, input, output) {
const searchResult = await fetchTx(txid) try {
if (searchResult) { const searchResult = await fetchTx(txid)
selectedTx.set(searchResult) if (searchResult) {
detailTx.set(searchResult) selectedTx.set(searchResult)
overlay.set('tx') detailTx.set(searchResult)
if (input != null || output != null) highlightInOut.set({txid, input, output}) overlay.set('tx')
return true if (input != null || output != null) highlightInOut.set({txid, input, output})
} else { return null
return false } else {
return '500'
}
} catch (err) {
console.log('error fetching tx ', err)
return err.message
} }
} }

View File

@ -55,8 +55,9 @@ defmodule BitcoinStream.Router do
end end
defp get_tx(txid) do defp get_tx(txid) do
with {:ok, 200, %{"hex" => hex, "blockhash" => blockhash}} <- RPC.request(:rpc, "getrawtransaction", [txid, true]), with {:ok, 200, verbosetx} <- RPC.request(:rpc, "getrawtransaction", [txid, true]),
{:ok, 200, %{"height" => height, "time" => time}} <- RPC.request(:rpc, "getblockheader", [blockhash, true]), %{"hex" => hex, "blockhash" => blockhash} <- Map.merge(%{"blockhash" => nil}, verbosetx),
{:ok, 200, %{"height" => height, "time" => time}} <- (if blockhash != nil do RPC.request(:rpc, "getblockheader", [blockhash, true]) else {:ok, 200, %{"height" => nil, "time" => nil}} end),
rawtx <- Base.decode16!(hex, case: :lower), rawtx <- Base.decode16!(hex, case: :lower),
{:ok, txn } <- BitcoinTx.decode(rawtx), {:ok, txn } <- BitcoinTx.decode(rawtx),
inflated_txn <- BitcoinTx.inflate(txn, false), inflated_txn <- BitcoinTx.inflate(txn, false),