diff --git a/client/package-lock.json b/client/package-lock.json index 4fbacb9..364275e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "bitfeed-client", - "version": "2.1.5", + "version": "2.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bitfeed-client", - "version": "2.1.5", + "version": "2.2.1", "dependencies": { "@babel/core": "^7.16.5", "@babel/preset-env": "^7.16.5", @@ -21,6 +21,7 @@ "d3-color": "^3.0.1", "d3-interpolate": "^3.0.1", "dotenv": "^10.0.0", + "hash.js": "^1.1.7", "locale-currency": "0.0.2", "qrcode": "^1.5.0", "rollup": "^2.62.0", @@ -2834,6 +2835,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3161,6 +3171,11 @@ "node": ">=4" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -6383,6 +6398,15 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6628,6 +6652,11 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", diff --git a/client/package.json b/client/package.json index 1ad13d6..3bdda8f 100644 --- a/client/package.json +++ b/client/package.json @@ -20,6 +20,7 @@ "d3-color": "^3.0.1", "d3-interpolate": "^3.0.1", "dotenv": "^10.0.0", + "hash.js": "^1.1.7", "locale-currency": "0.0.2", "qrcode": "^1.5.0", "rollup": "^2.62.0", diff --git a/client/src/components/TransactionOverlay.svelte b/client/src/components/TransactionOverlay.svelte index ca2f270..6de94f4 100644 --- a/client/src/components/TransactionOverlay.svelte +++ b/client/src/components/TransactionOverlay.svelte @@ -6,6 +6,7 @@ import { longBtcFormat, numberFormat, feeRateFormat } from '../utils/format.js' import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth } from '../stores.js' import { formatCurrency } from '../utils/fx.js' import { hlToHex, mixColor, teal, purple } from '../utils/color.js' +import { SPKToAddress } from '../utils/encodings.js' function onClose () { $detailTx = null @@ -50,6 +51,29 @@ $: { } } +function expandAddresses(items) { + return items.map(item => { + let address = 'unknown' + if (item.script_pub_key) { + address = SPKToAddress(item.script_pub_key) || "" + } + return { + ...item, + address + } + }) +} + +let inputs = [] +let outputs = [] +$: { + if ($detailTx && $detailTx.inputs) { + inputs = expandAddresses($detailTx.inputs) + } else inputs = [] + if ($detailTx && $detailTx.outputs) { + outputs = expandAddresses($detailTx.outputs) + } else outputs = [] +} let sankeyLines let sankeyHeight @@ -230,10 +254,8 @@ function getMiterOffset (weight, dy, dx) { } .flow-diagram { - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; + display: grid; + grid-template-columns: minmax(0px, 1fr) 380px minmax(0px, 1fr); .header { height: 60px; @@ -244,8 +266,7 @@ function getMiterOffset (weight, dy, dx) { } .column { - flex-grow: 1; - flex-shrink: 1; + width: 100%; margin: 0; .entry { @@ -257,29 +278,42 @@ function getMiterOffset (weight, dy, dx) { border-top: solid 1px var(--grey); box-sizing: border-box; text-align: right; + width: 100%; + overflow: hidden; &:last-child { border-bottom: solid 1px var(--grey); } .amount, .address { margin: 0; - } - .amount { font-family: monospace; font-size: 1.1em; + } + .amount { white-space: nowrap; } + .address { + width: 100%; + text-align: right; + span { + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + } + .truncatable { + max-width: calc(100% - 4em); + } + } } &.inputs { .entry { align-items: flex-start; + .address { + text-align: left; + } } } - &.diagram { - flex-grow: 0; - flex-shrink: 0; - } } .sankey { @@ -301,23 +335,11 @@ function getMiterOffset (weight, dy, dx) { @media (max-width: 460px) { .flow-diagram { - flex-direction: column; - align-items: center; - font-size: 0.8em; + display: block; .column { width: 100%; - - &.diagram { - order: 1; - } - &.inputs { - order: 2 - } - &.outputs { - margin-top: 30px; - order: 3 - } + margin: 30px 0; } } } @@ -356,12 +378,12 @@ function getMiterOffset (weight, dy, dx) {

Inputs & Outputs

-
+

{$detailTx.inputs.length} input{$detailTx.inputs.length > 1 ? 's' : ''}

- {#each $detailTx.inputs as input} + {#each inputs as input}
-

address

+

{input.address.slice(0,-6)}{input.address.slice(-6)}

{ formatBTC(input.value) }

{/each} @@ -399,9 +421,9 @@ function getMiterOffset (weight, dy, dx) {

fee

{ formatBTC($detailTx.fee) }

- {#each $detailTx.outputs as output} + {#each outputs as output}
-

address

+

{output.address.slice(0,-6)}{output.address.slice(-6)}

{ formatBTC(output.value) }

{/each} diff --git a/client/src/controllers/TxController.js b/client/src/controllers/TxController.js index 78fea2c..81c6e75 100644 --- a/client/src/controllers/TxController.js +++ b/client/src/controllers/TxController.js @@ -205,9 +205,10 @@ export default class TxController { } this.selectedTx = selected selectedTx.set(this.selectedTx) - console.log(this.selectedTx) - detailTx.set(this.selectedTx) - if (this.selectedTx) overlay.set('tx') + if (sameTx && this.selectedTx) { + detailTx.set(this.selectedTx) + overlay.set('tx') + } this.selectionLocked = !!this.selectedTx && !(this.selectionLocked && sameTx) } } diff --git a/client/src/utils/encodings.js b/client/src/utils/encodings.js index 0b089fc..a9e3f8a 100644 --- a/client/src/utils/encodings.js +++ b/client/src/utils/encodings.js @@ -1,7 +1,8 @@ import { Buffer } from 'buffer/' window.Buffer = Buffer import bech32 from 'bech32-buffer' -import { base58_to_binary } from 'base58-js' +import { base58_to_binary, binary_to_base58 } from 'base58-js' +import { sha256 } from 'hash.js' // Extract a raw script hash from an address export function addressToSPK (address) { @@ -25,3 +26,36 @@ export function addressToSPK (address) { return prefix + Buffer.from(result).toString('hex').slice(2, -8) + postfix } } + +// Extract an address from a raw scriptpubkey +export function SPKToAddress (spk) { + if (spk.startsWith('5120')) { + // taproot + return (new bech32.BitcoinAddress('bc', 1, hexToUintArray(spk.slice(4)))).encode() + } else if (spk.startsWith('0020') || spk.startsWith('0014')) { + // p2wsh or p2wpkh + return (new bech32.BitcoinAddress('bc', 0, hexToUintArray(spk.slice(4)))).encode() + } else if (spk.startsWith('76a914')) { + // p2pkh + const payload = "00" + spk.slice(6, -4) + const checksum = hash(hash(payload)).slice(0, 8) + return binary_to_base58(hexToUintArray(payload + checksum)) + } else if (spk.startsWith('a914')) { + // p2sh + const payload = "05" + spk.slice(4, -2) + const checksum = hash(hash(payload)).slice(0, 8) + return binary_to_base58(hexToUintArray(payload + checksum)) + } +} + +function hexToUintArray(hex) { + let a = new Uint8Array(hex.length / 2) + for (let i = 0; i < a.length; i++) { + a[i] = parseInt(hex.substr(2 * i, 2), 16) + } + return a +} + +function hash (hex) { + return sha256().update(hexToUintArray(hex)).digest('hex') +}