mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-12 19:20:46 +02:00
424 lines
14 KiB
JavaScript
424 lines
14 KiB
JavaScript
import TxPoolScene from '../models/TxPoolScene.js'
|
|
import TxMondrianPoolScene from '../models/TxMondrianPoolScene.js'
|
|
import TxBlockScene from '../models/TxBlockScene.js'
|
|
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 { searchTx, fetchSpends, addSpends } from '../utils/search.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 {
|
|
constructor ({ width, height }) {
|
|
this.vertexArray = new FastVertexArray(2048, TxSprite.dataSize, txCount)
|
|
this.debugVertexArray = new FastVertexArray(1024, TxSprite.dataSize)
|
|
this.txs = {}
|
|
this.expiredTxs = {}
|
|
this.poolScene = new TxMondrianPoolScene({ width, height, controller: this, heightStore: mempoolScreenHeight })
|
|
this.blockAreaSize = (width <= 620) ? Math.min(window.innerWidth * 0.7, window.innerHeight / 2.75) : Math.min(window.innerWidth * 0.75, window.innerHeight / 2.5)
|
|
blockAreaSize.set(this.blockAreaSize)
|
|
this.blockScene = null
|
|
this.block = null
|
|
this.explorerBlockScene = null
|
|
this.explorerBlock = null
|
|
this.clearBlockTimeout = null
|
|
this.txDelay = 0 //config.txDelay
|
|
this.maxTxDelay = config.txDelay
|
|
this.knownBlocks = {}
|
|
|
|
this.selectedTx = null
|
|
this.selectionLocked = false
|
|
|
|
this.lastTxTime = 0
|
|
this.txDelay = 0
|
|
|
|
this.blocksEnabled = true
|
|
blocksEnabled.subscribe(enabled => {
|
|
this.blocksEnabled = enabled
|
|
})
|
|
detailTx.subscribe(tx => {
|
|
this.onDetailTxChanged(tx)
|
|
})
|
|
highlight.subscribe(criteria => {
|
|
this.highlightCriteria = criteria
|
|
this.applyHighlighting()
|
|
})
|
|
colorMode.subscribe(mode => {
|
|
this.setColorMode(mode)
|
|
})
|
|
explorerBlockData.subscribe(blockData => {
|
|
if (blockData) {
|
|
this.exploreBlock(blockData)
|
|
} else {
|
|
this.resumeLatest()
|
|
}
|
|
})
|
|
}
|
|
|
|
getVertexData () {
|
|
return this.vertexArray.getVertexData()
|
|
}
|
|
|
|
getDebugVertexData () {
|
|
return this.debugVertexArray.getVertexData()
|
|
}
|
|
|
|
getScenes () {
|
|
if (this.blockScene && this.explorerBlockScene) return [this.poolScene, this.blockScene, this.explorerBlockScene]
|
|
else if (this.blockScene) return [this.poolScene, this.blockScene]
|
|
else return [this.poolScene]
|
|
}
|
|
|
|
redoLayout ({ width, height }) {
|
|
this.poolScene.layoutAll({ width, height })
|
|
if (this.blockScene) {
|
|
this.blockScene.layoutAll({ width: this.blockAreaSize, height: this.blockAreaSize })
|
|
}
|
|
if (this.explorerBlockScene) {
|
|
this.explorerBlockScene.layoutAll({ width: this.blockAreaSize, height: this.blockAreaSize })
|
|
}
|
|
}
|
|
|
|
resize ({ width, height }) {
|
|
this.blockAreaSize = (width <= 620) ? Math.min(window.innerWidth * 0.7, window.innerHeight / 2.75) : Math.min(window.innerWidth * 0.75, window.innerHeight / 2.5)
|
|
blockAreaSize.set(this.blockAreaSize)
|
|
this.redoLayout({ width, height })
|
|
}
|
|
|
|
setColorMode (mode) {
|
|
this.colorMode = mode
|
|
this.poolScene.setColorMode(mode)
|
|
if (this.blockScene) {
|
|
this.blockScene.setColorMode(mode)
|
|
}
|
|
if (this.explorerBlockScene) {
|
|
this.explorerBlockScene.setColorMode(mode)
|
|
}
|
|
}
|
|
|
|
applyHighlighting () {
|
|
this.poolScene.applyHighlighting(this.highlightCriteria)
|
|
if (this.blockScene) {
|
|
this.blockScene.applyHighlighting(this.highlightCriteria)
|
|
}
|
|
if (this.explorerBlockScene) {
|
|
this.explorerBlockScene.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]) {
|
|
// smooth near-simultaneous arrivals over up to three seconds
|
|
const dx = performance.now() - this.lastTxTime
|
|
this.lastTxTime = performance.now()
|
|
if (dx <= 250) {
|
|
this.txDelay = Math.min(3000, this.txDelay + (Math.random() * 250))
|
|
} else {
|
|
this.txDelay = Math.max(0, this.txDelay - (dx-250))
|
|
}
|
|
this.txs[tx.id] = tx
|
|
tx.onEnterScene()
|
|
this.poolScene.insert(this.txs[tx.id], this.txDelay)
|
|
}
|
|
}
|
|
|
|
dropTx (txid) {
|
|
if (this.txs[txid] && this.poolScene.drop(txid)) {
|
|
this.txs[txid].view.update({
|
|
display: {
|
|
position: {
|
|
y: -100, //this.txs[txid].screenPosition.y - 100
|
|
},
|
|
// color: {
|
|
// alpha: 0
|
|
// }
|
|
},
|
|
delay: 0,
|
|
duration: 2000
|
|
})
|
|
setTimeout(() => {
|
|
this.destroyTx(txid)
|
|
}, 2000)
|
|
// this.poolScene.layoutAll()
|
|
}
|
|
}
|
|
|
|
simulateBlock () {
|
|
const time = Date.now() / 1000
|
|
console.log('sim time ', time)
|
|
this.addBlock({
|
|
version: 'fake',
|
|
id: Math.random(),
|
|
value: 10000,
|
|
prev_block: 'also_fake',
|
|
merkle_root: 'merkle',
|
|
timestamp: time,
|
|
bits: 'none',
|
|
txn_count: 20,
|
|
fees: 100,
|
|
txns: [{ version: 'fake', inflated: false, id: 'coinbase', value: 625000100, fee: 100, vbytes: 500, inputs:[{ prev_txid: '00000000000000000000000000000', prev_vout: 0, script_sig: '03e0170b04efb72c622f466f756e6472792055534120506f6f6c202364726f70676f6c642f0eb5059f0000000000000000',
|
|
sequence_no: 0, value: 625000100, script_pub_key: "76a9145e9b23809261178723055968d134a947f47e799f88ac" }], outputs: [{ prev_txid: '00000000000000000000000000000', prev_vout: 0, script_sig: '03e0170b04efb72c622f466f756e6472792055534120506f6f6c202364726f70676f6c642f0eb5059f0000000000000000',
|
|
sequence_no: 0, value: 625000100, script_pub_key: "76a9145e9b23809261178723055968d134a947f47e799f88ac" }], time: Date.now()
|
|
}, ...Object.keys(this.txs).filter(() => {
|
|
return (Math.random() < 0.5)
|
|
}).map(key => {
|
|
return {
|
|
...this.txs[key],
|
|
inputs: this.txs[key].inputs.map(input => { return {...input, script_pub_key: null, value: null }}),
|
|
}
|
|
})]
|
|
})
|
|
}
|
|
|
|
addBlock (blockData, realtime=true) {
|
|
// discard duplicate blocks
|
|
if (!blockData || !blockData.id || this.knownBlocks[blockData.id]) {
|
|
return
|
|
}
|
|
|
|
let block
|
|
block = new BitcoinBlock(blockData)
|
|
|
|
latestBlockHeight.set(block.height)
|
|
// this.knownBlocks[block.id] = true
|
|
if (this.clearBlockTimeout) clearTimeout(this.clearBlockTimeout)
|
|
|
|
this.expiredTxs = {}
|
|
|
|
if (!this.explorerBlockScene) this.clearBlock()
|
|
|
|
if (this.blocksEnabled) {
|
|
this.blockScene = new TxBlockScene({ width: this.blockAreaSize, height: this.blockAreaSize, blockId: block.id, controller: this, colorMode: this.colorMode })
|
|
let knownCount = 0
|
|
let unknownCount = 0
|
|
for (let i = 0; i < block.txns.length; i++) {
|
|
if (this.poolScene.remove(block.txns[i].id)) {
|
|
knownCount++
|
|
this.txs[block.txns[i].id].setData(block.txns[i])
|
|
this.txs[block.txns[i].id].setBlock(block)
|
|
this.blockScene.insert(this.txs[block.txns[i].id], 0, false)
|
|
} else {
|
|
unknownCount++
|
|
const tx = new BitcoinTx({
|
|
...block.txns[i],
|
|
block: block
|
|
}, this.vertexArray)
|
|
this.txs[tx.id] = tx
|
|
this.txs[tx.id].applyHighlighting(this.highlightCriteria)
|
|
this.blockScene.insert(tx, 0, false)
|
|
}
|
|
this.expiredTxs[block.txns[i].id] = true
|
|
}
|
|
console.log(`New block with ${knownCount} known transactions and ${unknownCount} unknown transactions`)
|
|
this.blockScene.initialLayout(!!this.explorerBlockScene)
|
|
setTimeout(() => { this.poolScene.scrollLock = false; this.poolScene.layoutAll() }, 4000)
|
|
|
|
blockVisible.set(true)
|
|
|
|
if (!this.explorerBlockScene) {
|
|
currentBlock.set(block)
|
|
}
|
|
} else {
|
|
this.poolScene.scrollLock = true
|
|
for (let i = 0; i < block.txns.length; i++) {
|
|
if (this.txs[block.txns[i].id] && this.txs[block.txns[i].id].view) {
|
|
this.txs[block.txns[i].id].view.update({
|
|
display: {
|
|
color: this.txs[block.txns[i].id].getColor('block', 'age').color
|
|
},
|
|
duration: 1000,
|
|
delay: 0,
|
|
jitter: 0,
|
|
})
|
|
}
|
|
this.expiredTxs[block.txns[i].id] = true
|
|
}
|
|
setTimeout(() => {
|
|
for (let i = 0; i < block.txns.length; i++) {
|
|
if (this.txs[block.txns[i].id] && this.txs[block.txns[i].id].view) {
|
|
this.txs[block.txns[i].id].view.update({
|
|
display: {
|
|
position: { y: window.innerHeight + 50, r: 10 },
|
|
color: { alpha: 0 }
|
|
},
|
|
duration: 3000,
|
|
delay: 0,
|
|
jitter: 1000,
|
|
})
|
|
}
|
|
this.expiredTxs[block.txns[i].id] = true
|
|
}
|
|
}, 1500)
|
|
setTimeout(() => {
|
|
for (let i = 0; i < block.txns.length; i++) {
|
|
this.poolScene.remove(block.txns[i].id)
|
|
}
|
|
this.poolScene.scrollLock = false
|
|
this.poolScene.layoutAll()
|
|
}, 5500)
|
|
|
|
currentBlock.set(block)
|
|
}
|
|
|
|
this.block = block
|
|
|
|
return block
|
|
}
|
|
|
|
exploreBlock (blockData) {
|
|
const block = blockData.isBlock ? blockData : new BitcoinBlock(blockData)
|
|
let enterFromRight = false
|
|
|
|
// clean up previous block
|
|
if (this.explorerBlock && this.explorerBlockScene) {
|
|
const prevBlock = this.explorerBlock
|
|
const prevBlockScene = this.explorerBlockScene
|
|
if (prevBlock.height < block.height) {
|
|
prevBlockScene.exitLeft()
|
|
enterFromRight = true
|
|
}
|
|
else prevBlockScene.exitRight()
|
|
prevBlockScene.expire(2000)
|
|
} else if (this.blockScene) {
|
|
this.blockScene.exitRight()
|
|
}
|
|
|
|
this.explorerBlock = block
|
|
|
|
if (this.blocksEnabled) {
|
|
this.explorerBlockScene = new TxBlockScene({ width: this.blockAreaSize, height: this.blockAreaSize, blockId: block.id, controller: this, colorMode: this.colorMode })
|
|
for (let i = 0; i < block.txns.length; i++) {
|
|
const tx = new BitcoinTx({
|
|
...block.txns[i],
|
|
block: block
|
|
}, this.vertexArray)
|
|
this.txs[tx.id] = tx
|
|
this.txs[tx.id].applyHighlighting(this.highlightCriteria)
|
|
this.explorerBlockScene.insert(tx, 0, false)
|
|
}
|
|
this.explorerBlockScene.prepareAll()
|
|
this.explorerBlockScene.layoutAll()
|
|
if (enterFromRight) {
|
|
blockTransitionDirection.set('right')
|
|
this.explorerBlockScene.enterRight()
|
|
} else {
|
|
blockTransitionDirection.set('left')
|
|
this.explorerBlockScene.enterLeft()
|
|
}
|
|
}
|
|
|
|
blockVisible.set(true)
|
|
currentBlock.set(block)
|
|
}
|
|
|
|
resumeLatest () {
|
|
if (this.explorerBlock && this.explorerBlockScene) {
|
|
const prevBlock = this.explorerBlock
|
|
const prevBlockScene = this.explorerBlockScene
|
|
prevBlockScene.exitLeft()
|
|
prevBlockScene.expire(2000)
|
|
this.explorerBlockScene = null
|
|
this.explorerBlock = null
|
|
urlPath.set("/")
|
|
}
|
|
if (this.blockScene && this.block) {
|
|
blockTransitionDirection.set('right')
|
|
this.blockScene.enterRight()
|
|
currentBlock.set(this.block)
|
|
}
|
|
}
|
|
|
|
hideBlock () {
|
|
if (this.blockScene && !this.explorerBlockScene) {
|
|
blockTransitionDirection.set(null)
|
|
this.blockScene.hide()
|
|
}
|
|
}
|
|
|
|
showBlock () {
|
|
if (this.blockScene && !this.explorerBlockScene) {
|
|
this.blockScene.show()
|
|
}
|
|
}
|
|
|
|
clearBlock () {
|
|
if (this.blockScene) {
|
|
this.blockScene.exitLeft()
|
|
this.blockScene.expire()
|
|
}
|
|
this.block = null
|
|
currentBlock.set(null)
|
|
}
|
|
|
|
destroyTx (id) {
|
|
this.getScenes().forEach(scene => {
|
|
scene.remove(id)
|
|
})
|
|
if (this.txs[id]) this.txs[id].destroy()
|
|
delete this.txs[id]
|
|
}
|
|
|
|
mouseMove (position) {
|
|
if (this.poolScene && !this.selectionLocked) {
|
|
let selected = this.poolScene.selectAt(position)
|
|
if (!selected && this.blockScene && !this.explorerBlock && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
|
|
if (!selected && this.explorerBlockScene && this.explorerBlock && !this.explorerBlockScene.hidden) selected = this.explorerBlockScene.selectAt(position)
|
|
|
|
if (selected !== this.selectedTx) {
|
|
if (this.selectedTx) this.selectedTx.hoverOff()
|
|
if (selected) selected.hoverOn()
|
|
}
|
|
this.selectedTx = selected
|
|
selectedTx.set(this.selectedTx)
|
|
}
|
|
}
|
|
|
|
async mouseClick (position) {
|
|
if (this.poolScene) {
|
|
let selected = this.poolScene.selectAt(position)
|
|
if (!selected && this.blockScene && !this.explorerBlock && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
|
|
if (!selected && this.explorerBlockScene && this.explorerBlock && !this.explorerBlockScene.hidden) selected = this.explorerBlockScene.selectAt(position)
|
|
|
|
let sameTx = true
|
|
if (selected !== this.selectedTx) {
|
|
sameTx = false
|
|
if (this.selectedTx) this.selectedTx.hoverOff()
|
|
if (selected) selected.hoverOn()
|
|
}
|
|
this.selectedTx = selected
|
|
selectedTx.set(selected)
|
|
if (sameTx && selected) {
|
|
if (!selected.is_inflated) {
|
|
loading.increment()
|
|
await searchTx(selected.id)
|
|
loading.decrement()
|
|
} else {
|
|
const spendResult = await fetchSpends(selected.id)
|
|
if (spendResult) selected = addSpends(selected, spendResult)
|
|
urlPath.set(`/tx/${selected.id}`)
|
|
detailTx.set(selected)
|
|
overlay.set('tx')
|
|
}
|
|
console.log(selected)
|
|
}
|
|
this.selectionLocked = !!selected && !(this.selectionLocked && sameTx)
|
|
}
|
|
}
|
|
|
|
onDetailTxChanged (tx) {
|
|
if (!tx) {
|
|
if (this.selectedTx) {
|
|
this.selectedTx.hoverOff()
|
|
this.selectedTx = null
|
|
}
|
|
selectedTx.set(null)
|
|
this.selectionLocked = false
|
|
}
|
|
|
|
selectedTx.set(null)
|
|
}
|
|
}
|