src/main/generic/miner/BasePoolMiner.js
/**
* @abstract
* @deprecated
*/
class BasePoolMiner extends Miner {
/**
* @param {BasePoolMiner.Mode} mode
* @param {BaseChain} blockchain
* @param {Accounts} accounts
* @param {Mempool} mempool
* @param {Time} time
* @param {Address} address
* @param {number} deviceId
* @param {?object} deviceData
* @param {Uint8Array} [extraData=new Uint8Array(0)]
*/
constructor(mode, blockchain, accounts, mempool, time, address, deviceId, deviceData, extraData = new Uint8Array(0)) {
super(blockchain, accounts, mempool, time, address, extraData);
/** @type {Address} */
this._ourAddress = address;
/** @type {Uint8Array} */
this._ourExtraData = extraData;
/** @type {WebSocket} */
this._ws = null;
/** @type {number} */
this._deviceId = deviceId;
/** @type {object} */
this._deviceData = deviceData;
/** @type {BasePoolMiner.Mode} */
this.mode = mode;
/** @type {BasePoolMiner.ConnectionState} */
this.connectionState = BasePoolMiner.ConnectionState.CLOSED;
this._reconnectTimeout = null;
this._exponentialBackoffReconnect = BasePoolMiner.RECONNECT_TIMEOUT;
}
requestPayout() {
this._send({
message: 'payout',
});
}
_send(msg) {
if (this._ws) {
try {
this._ws.send(JSON.stringify(msg));
} catch (e) {
Log.w(BasePoolMiner, 'Error sending:', e.message || e);
}
}
}
connect(host, port) {
if (this._ws) throw new Error('Call disconnect() first');
this._host = host;
this._port = port;
const ws = this._ws = new WebSocket(`wss://${host}:${port}`);
this._ws.onopen = () => this._onOpen(ws);
this._ws.onerror = (e) => this._onError(ws, e);
this._ws.onmessage = (msg) => this._onMessage(ws, msg.data);
this._ws.onclose = (e) => this._onClose(ws, e);
this._changeConnectionState(BasePoolMiner.ConnectionState.CONNECTING);
}
_onOpen(ws) {
if (ws !== this._ws) {
ws.close();
} else {
this._register();
}
}
_register() {
this._send({
message: 'register',
mode: this.mode,
address: this._ourAddress.toUserFriendlyAddress(),
deviceId: this._deviceId,
deviceData: this._deviceData,
genesisHash: BufferUtils.toBase64(GenesisConfig.GENESIS_HASH.serialize())
});
}
_onError(ws, e) {
Log.d(BasePoolMiner, 'WebSocket connection errored', e.message || e);
if (ws === this._ws) {
this._timeoutReconnect();
}
try {
ws.close();
} catch (e2) {
Log.w(BasePoolMiner, e2.message || e2);
}
}
_onClose(ws, e) {
Log.d(BasePoolMiner, 'WebSocket connection closed', e.message || e);
if (ws === this._ws) {
this._changeConnectionState(BasePoolMiner.ConnectionState.CLOSED);
Log.w(BasePoolMiner, 'Disconnected from pool');
this._timeoutReconnect();
}
}
_timeoutReconnect() {
this.disconnect();
this._reconnectTimeout = setTimeout(() => {
if (!this._ws) {
this.connect(this._host, this._port);
}
}, this._exponentialBackoffReconnect);
this._exponentialBackoffReconnect = Math.min(this._exponentialBackoffReconnect * 2, BasePoolMiner.RECONNECT_TIMEOUT_MAX);
}
disconnect() {
this._turnPoolOff();
if (this._ws) {
this._changeConnectionState(BasePoolMiner.ConnectionState.CLOSED);
Log.w(BasePoolMiner, 'Disconnected from pool');
const ws = this._ws;
this._ws = null;
try {
ws.close();
} catch (e) {
Log.w(BasePoolMiner, e.message || e);
}
}
clearTimeout(this._reconnectTimeout);
}
_onMessage(ws, msgJson) {
if (ws !== this._ws) return;
try {
const msg = JSON.parse(msgJson);
if (msg && msg.message) {
switch (msg.message) {
case 'settings':
if (!msg.address || !msg.extraData) {
this._turnPoolOff();
this._ws.close();
} else {
this._onNewPoolSettings(Address.fromUserFriendlyAddress(msg.address), BufferUtils.fromBase64(msg.extraData), msg.targetCompact || BlockUtils.targetToCompact(new BigNumber(msg.target)), msg.nonce);
Log.d(BasePoolMiner, `Received settings from pool: address ${msg.address}, target ${msg.target}, extraData ${msg.extraData}`);
}
break;
case 'balance':
if (msg.balance === undefined || msg.confirmedBalance === undefined) {
this._turnPoolOff();
this._ws.close();
} else {
this._onBalance(msg.balance, msg.confirmedBalance, msg.payoutRequestActive);
Log.d(BasePoolMiner, `Received balance from pool: ${msg.balance} (${msg.confirmedBalance} confirmed), payout request active: ${msg.payoutRequestActive}`);
}
break;
case 'registered':
this._changeConnectionState(BasePoolMiner.ConnectionState.CONNECTED);
this._exponentialBackoffReconnect = BasePoolMiner.RECONNECT_TIMEOUT;
Log.i(BasePoolMiner, 'Connected to pool');
break;
case 'error':
Log.w(BasePoolMiner, 'Error from pool:', msg.reason);
break;
}
} else {
Log.w(BasePoolMiner, 'Received unknown message from pool server:', JSON.stringify(msg));
this._ws.close();
}
} catch (e) {
this._onError(ws, e);
}
}
/**
* @param {number} balance
* @param {number} confirmedBalance
* @param {boolean} payoutRequestActive
* @private
*/
_onBalance(balance, confirmedBalance, payoutRequestActive) {
const oldBalance = this.balance, oldConfirmedBalance = this.confirmedBalance;
this.balance = balance;
this.confirmedBalance = confirmedBalance;
this.payoutRequestActive = payoutRequestActive;
if (balance !== oldBalance || confirmedBalance !== oldConfirmedBalance) {
Log.i(BasePoolMiner, `Pool balance: ${Policy.lunasToCoins(balance)} NIM (confirmed ${Policy.lunasToCoins(confirmedBalance)} NIM)`);
}
if (balance !== oldBalance) {
this.fire('balance', balance);
}
if (confirmedBalance !== oldConfirmedBalance) {
this.fire('confirmed-balance', confirmedBalance);
}
}
_turnPoolOff() {
this.startConfigChanges();
super.address = this._ourAddress;
super.extraData = this._ourExtraData;
super.shareCompact = null;
this.finishConfigChanges();
}
/**
* @param {Address} address
* @param {Uint8Array} extraData
* @param {number} targetCompact
* @param {number} nonce
* @private
*/
_onNewPoolSettings(address, extraData, targetCompact, nonce) {
this.startConfigChanges();
super.address = address;
super.extraData = extraData;
super.shareCompact = targetCompact;
super.nonce = nonce;
this.finishConfigChanges();
}
_changeConnectionState(connectionState) {
this.connectionState = connectionState;
this.fire('connection-state', connectionState);
}
/**
* @returns {boolean}
*/
isConnected() {
return this.connectionState === BasePoolMiner.ConnectionState.CONNECTED;
}
/**
* @returns {boolean}
*/
isDisconnected() {
return this.connectionState === BasePoolMiner.ConnectionState.CLOSED;
}
/**
* @type {string}
*/
get host() {
return this._host;
}
/**
* @type {number}
*/
get port() {
return this._port;
}
/**
* @type {Address}
* @override
*/
get address() {
return this._ourAddress;
}
/**
* @type {Address}
* @override
*/
set address(address) {
this._ourAddress = address;
if (this.isConnected()) {
this.disconnect();
this.connect(this._host, this._port);
} else {
super.address = address;
}
}
/**
* @param {NetworkConfig} networkConfig
* @returns {number}
*/
static generateDeviceId(networkConfig) {
return Hash.blake2b([
BufferUtils.fromAscii('pool_device_id'),
networkConfig.keyPair.privateKey.serialize()
].reduce(BufferUtils.concatTypedArrays)).serialize().readUint32();
}
}
BasePoolMiner.PAYOUT_NONCE_PREFIX = 'POOL_PAYOUT';
BasePoolMiner.RECONNECT_TIMEOUT = 3000; // 3 seconds
BasePoolMiner.RECONNECT_TIMEOUT_MAX = 30000; // 30 seconds
/** @enum {number} */
BasePoolMiner.ConnectionState = {
CONNECTED: 0,
CONNECTING: 1,
CLOSED: 2
};
/** @enum {string} */
BasePoolMiner.Mode = {
NANO: 'nano',
SMART: 'smart'
};
Class.register(BasePoolMiner);