ibctminer
Version:
```js const IntMiner = require('./src'); const Debug = require('./src/log')(); const fs = require('fs'); const COMP = '[SIPC]';
631 lines (549 loc) • 19.4 kB
JavaScript
module.exports = function (classes){
'use strict';
var
_ = classes.lodash,
q = classes.q;
var Server = classes.Base.$define('Server', {
construct : function (opts){
var self = this;
this.$super();
self.clients = {};
opts = opts || {};
self.opts = _.defaults(_.clone(opts), Server.defaults);
if (opts.rpc){
self.rpc = new classes.RPCServer(self.opts.rpc);
self.expose('mining.connections');
self.expose('mining.block');
self.expose('mining.wallet');
self.expose('mining.alert');
}
// classes.toobusy.maxLag(self.opts.settings.toobusy);
self.server = classes.net.createServer();
Server.debug('Created server');
self.server.on('connection', function serverConnection(socket){
self.newConnection(socket);
});
},
expose : function (name){
if (this.rpc) {
this.rpc.expose(name, Server.expose(this, name), this);
} else {
throw new Error('RPC is not enabled in the server');
}
},
/**
* Emits 'close' event when a connection was closed
*
* @param {Client} socket
*/
closeConnection: function (socket){
var id = socket.id;
socket.destroy();
if (typeof this.clients[id] !== 'undefined') {
delete this.clients[id];
}
this.emit('close', id);
Server.debug('(' + id + ') Closed connection ' + _.size(this.clients) + ' connections');
},
/**
* Emits 'busy' event when the server is on high load
* Emits 'connection' event when there's a new connection, passes the newly created socket as the first argument
*
* @param {Socket} socket
*/
newConnection : function (socket){
var
self = this,
closeSocket,
handleData;
// if (classes.toobusy()) {
if (false) {
socket.destroy();
Server.debug('Server is busy, ' + _.size(this.clients) + ' connections');
self.emit('busy');
} else {
socket = new classes.Client(socket, true);
closeSocket = (function (socket){
return function closeSocket(){
self.closeConnection(socket);
};
})(socket);
handleData = (function (socket){
return function handleData(buffer){
self.handleData(socket, buffer);
};
})(socket);
classes.Server.debug('(' + socket.id + ') New connection');
this.clients[socket.id] = socket;
socket.on('end', closeSocket);
socket.on('error', closeSocket);
socket.socket.on('data', handleData);
this.emit('connection', socket);
}
},
/**
*
* @param {Client} socket
* @param {Buffer} buffer
*/
handleData : function (socket, buffer){
var
self = this,
c = Server.getStratumCommands(buffer),
string = c.string,
cmds = c.cmds;
if (/ HTTP\/1\.1\n/i.test(string)) {
// getwork trying to access the stratum server, send HTTP header
socket.stratumHttpHeader(
this.opts.settings.hostname,
this.opts.settings.port
).done(function handleDataHttp(){
self.closeConnection(socket);
});
} else if (cmds.length) {
// We got data
self.$class.processCommands.call(self, socket, cmds);
}
},
/**
* Start the Stratum server, the RPC and any coind that are enabled
*
* @return {Q.promise}
*/
listen : function (){
var self = this, d = q.defer();
this.server.listen(this.opts.settings.port, this.opts.settings.host, function serverListen(){
d.resolve(Server.debug('Listening on port ' + self.opts.settings.port));
});
/*istanbul ignore else */
if (this.rpc) {
this.rpc.listen();
}
return d.promise;
},
close : function (){
Server.debug('Shutting down servers...');
this.server.close();
/*istanbul ignore else */
if (this.rpc) {
this.rpc.close();
}
},
/**
* Sends a Stratum result command directly to one socket
*
* @param {String} id UUID of the socket
* @param {String} type The type of command, as defined in server.commands
* @param {Array} array Parameters to send
*
* @return {Q.promise}
*/
sendToId : function (id, type, array){
var d = q.defer();
if (type && _.isFunction(this.$class.commands[type])) {
if (id && _.has(this.clients, id)) {
this.$class.commands[type].apply(this.clients[id], [id].concat(array)).done(
classes.curry.wrap(d.resolve, d),
classes.curry.wrap(d.reject, d)
);
} else {
d.reject(this.$class.debug('sendToId socket id not found "' + id + '"'));
}
} else {
d.reject(this.$class.debug('sendToId command doesnt exist "' + type + '"'));
}
return d.promise;
},
/**
* Send a mining method or result to all connected
* sockets
*
* Returns a promise, so when it's done sending, you can
* do:
*
* server.broadcast('notify', [...params]).then(function(){
* console.log('done');
* });
*
* @param {String} type
* @param {Array} data
* @returns {Q.promise}
*/
broadcast : function (type, data){
var self = this, d = q.defer(), total = 0;
if (typeof type === 'string' && _.isArray(data)) {
if (typeof self.$class.commands[type] === 'function' && self.$class.commands[type].broadcast === true) {
if (_.size(self.clients)) {
self.$class.debug('Brodcasting ' + type + ' with ', data);
q.try(function serverBroadcast(){
_.forEach(self.clients, function serverBroadcastEach(socket){
self.$class.commands[type].apply(socket, [null].concat(data));
total++;
});
return total;
}).done(
classes.curry.wrap(d.resolve, d),
classes.curry.wrap(d.reject, d)
);
} else {
d.reject(self.$class.debug('No clients connected'));
}
} else {
d.reject(self.$class.debug('Invalid broadcast type "' + type + '"'));
}
} else {
d.reject(self.$class.debug('Missing type and data array parameters'));
}
return d.promise;
}
}, {
/**
* Parse the incoming data for commands
*
* @param {Buffer} buffer
* @returns {{string: string, cmds: Array}}
*/
getStratumCommands: function (buffer){
var
string,
cmds = [];
if (Buffer.isBuffer(buffer) && buffer.length) {
string = buffer.toString().replace(/[\r\x00]/g, '');
cmds = _.filter(string.split('\n'), function serverCommandsFilter(item){ return !_.isEmpty(item) && !_.isNull(item); });
}
// Separate cleaned up raw string and commands array
return {string: string, cmds: cmds};
},
/**
* Process the Stratum commands and act on them
* Emits 'mining' event
*
* @param {Client} socket
* @param {Array} cmds
*/
processCommands : function (socket, cmds){
var
command,
method,
self = this,
onClient = self.$instanceOf(classes.Client),
onServer = !onClient && self.$instanceOf(classes.Server);
self.$class.debug('(' + socket.id + ') Received command ' + cmds);
_.forEach(cmds, function serverForEachCommand(cmd){
try {
command = JSON.parse(cmd);
// Is it a method Stratum call?
if (
// Deal with method calls only when on Server
onServer &&
typeof command['method'] !== 'undefined' &&
command.method.indexOf('mining.') !== -1
) {
method = command.method.split('mining.');
command['method'] = method[1];
if (method.length === 2 && typeof self.$class.commands[method[1]] === 'function') {
// We don't want client sockets messing around with broadcast functions!
if (self.$class.commands[method[1]].broadcast !== true && method[1] !== 'error') {
// only set lastActivity for real mining activity
socket.setLastActivity();
var
d = q.defer(),
accept, reject;
// Resolved, call the method and send data to socket
accept = self.$class.bindCommand(socket, method[1], command.id);
// Rejected, send error to socket
reject = self.$class.bindCommand(socket, 'error', command.id);
d.promise.spread(accept, reject);
self.emit('mining', command, d, socket);
} else {
throw new Error('(' + socket.id + ') Client trying to reach a broadcast function "' + method[1] + '"');
}
} else {
self.$class.commands.error.call(socket, command.id, self.$class.errors.METHOD_NOT_FOUND);
throw new Error('Method not found "' + command.method + '"');
}
} else if ((onClient || socket.byServer === true) && (_.has(command, 'result') || _.has(command, 'method'))) {
// Result commands ONLY when 'self' is an instance of Client
// Since (unfortunately) stratum expects every command to be given in order
// we need to keep track on what we asked to the server, so we can
// act accordingly. This call is either a result from a previous command or a method call (broadcast)
socket.fullfill(command);
} else {
throw new Error('Stratum request without method or result field');
}
} catch (e) {
if (self.emit)
self.emit('mining.error', self.$class.debug(e), socket);
}
});
},
/**
* Wraps the callback and predefine the ID of the current stratum call
*
* @param {Client} socket
* @param {String} type
* @param {String} id
*
* @returns {Function} curryed function
*/
bindCommand : function (socket, type, id){
return classes.curry.predefine(this.commands[type], [id], socket);
},
rejected : function (msg){
return q.reject(this.$class.debug(msg));
},
expose : function (base, name){
return function serverExposedFunction(args, connection, callback){
var d = q.defer();
classes.RPCServer.debug('Method "' + name + '": ' + args);
d.promise.then(function serverExposedResolve(res){
res = [].concat(res);
classes.RPCServer.debug('Resolve "' + name + '": ' + res);
callback.call(base, null, [res[0]]);
}, function serverExposedReject(err){
classes.RPCServer.debug('Reject "' + name + '": ' + err);
callback.call(base, ([].concat(err))[0]);
});
base.emit('rpc', name, args, connection, d);
};
},
invalidArgs: function(id, name, expected, args) {
var count = _.filter(args, function(i){ return typeof i !== 'undefined'; }).length - 1;
if ((id === null || id === undefined) || count !== expected) {
return classes.Server.rejected(
(id === null || id === undefined) ?
'No ID provided' :
'Wrong number of arguments in "' + name + '", expected ' + expected + ' but got ' + count
);
}
return true;
},
commands : {
/**
* Return subscription parameters to the new client
*
* @param id
* @param {String} difficulty
* @param {String} subscription
* @param {String} extranonce1
* @param {Number} extranonce2_size
*
* @returns {Q.promise}
*/
subscribe : function (id, difficulty, subscription, extranonce1, extranonce2_size){
var ret;
if ((ret = classes.Server.invalidArgs(id, 'subscribe', 4, arguments)) !== true){
return ret;
}
this.subscription = subscription;
return this.stratumSend({
id : id,
result: [
[
['mining.set_difficulty', difficulty],
['mining.notify', subscription]
],
extranonce1,
extranonce2_size
],
error : null
}, true, 'subscribe');
},
/**
* Send if submitted share is valid
*
* @param {Number} id ID of the call
* @param {Boolean} accepted
* @returns {Q.promise}
*/
submit : function (id, accepted){
var ret;
if ((ret = classes.Server.invalidArgs(id, 'submit', 1, arguments)) !== true){
return ret;
}
return this.stratumSend({
id : id,
result: !!accepted,
error : null
}, false, 'submit');
},
/**
* Send an error
*
* @param {Number} id
* @param {Array|String} error
* @returns {Q.promise}
*/
error : function (id, error){
var ret;
if ((ret = classes.Server.invalidArgs(id, 'error', 1, arguments)) !== true){
return ret;
}
this.$class.debug('Stratum error: ' + error);
return this.stratumSend({
id : id,
error : error,
result: null
}, true, 'error');
},
/**
* Authorize the client (or not). Must be subscribed
*
* @param {Number} id
* @param {Boolean} authorized
*
* @returns {Q.promise}
*/
authorize : function (id, authorized){
var ret;
if ((ret = classes.Server.invalidArgs(id, 'authorize', 1, arguments)) !== true){
return ret;
}
if (!this.subscription) {
return Server.commands.error.call(this, id, Server.errors.NOT_SUBSCRIBED);
}
this.authorized = !!authorized;
return this.stratumSend({
id : id,
result: this.authorized,
error : null
}, true, 'authorize');
},
/**
* Miner is asking for pool transparency
*
* @param {String} id txlist_jobid
* @param {*} merkles
*/
get_transactions: function (id, merkles){
var ret;
if ((ret = classes.Server.invalidArgs(id, 'get_transactions', 1, arguments)) !== true){
return ret;
}
return this.stratumSend({
id : id,
result: [].concat(merkles),
error : null
}, false, 'get_transactions');
},
/**
* Notify of a new job
*
* @param {Number} id
* @param {*} job_id
* @param {String} previous_hash
* @param {String} coinbase1
* @param {String} coinbase2
* @param {Array} branches
* @param {String} block_version
* @param {String} nbit
* @param {String} ntime
* @param {Boolean} clean
*
* @returns {Q.promise}
*/
notify : function (id, job_id, previous_hash, coinbase1, coinbase2, branches, block_version, nbit, ntime, clean){
var ret;
if ((ret = classes.Server.invalidArgs(false, 'notify', 9, arguments)) !== true){
return ret;
}
return this.stratumSend({
id : null,
method: 'mining.notify',
params: [job_id, previous_hash, coinbase1, coinbase2, branches, block_version, nbit, ntime, clean]
}, true, 'notify');
},
/**
* Set the difficulty
*
* @param {Number} id
* @param {Number} value
* @returns {Q.promise}
*/
set_difficulty : function (id, value){
var ret;
if ((ret = classes.Server.invalidArgs(false, 'set_difficulty', 1, arguments)) !== true){
return ret;
}
return this.stratumSend({
id : null,
method: 'mining.set_difficulty',
params: [value]
}, true, 'set_difficulty');
}
},
errors : {
'FEE_REQUIRED' : [-10, 'Fee required', null],
'SERVICE_NOT_FOUND' : [-2, 'Service not found', null],
'METHOD_NOT_FOUND' : [-3, 'Method not found', null],
'UNKNOWN' : [-20, 'Unknown error', null],
'STALE_WORK' : [-21, 'Stale work', null],
'DUPLICATE_SHARE' : [-22, 'Duplicate share', null],
'HIGH_HASH' : [-23, 'Low difficulty share', null],
'UNAUTHORIZED_WORKER': [-24, 'Unauthorized worker', null],
'NOT_SUBSCRIBED' : [-25, 'Not subscribed', null]
},
defaults : {
/**
* RPC to listen interface for this server
*/
rpc : {
/**
* Bind to address
*
* @type {String}
*/
host: 'localhost',
/**
* RPC port
*
* @type {Number}
*/
port: 1337,
/**
* RPC password, this needs to be a SHA256 hash, defaults to 'password'
* To create a hash out of your password, launch node.js and write
*
* require('crypto').createHash('sha256').update('password').digest('hex');
*
* @type {String}
*/
password: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8',
/**
* Mode to listen. By default listen only on TCP, but you may use 'http' or 'both' (deal
* with HTTP and TCP at same time)
*/
mode: 'tcp'
},
/**
* The server settings itself
*/
settings: {
/**
* Address to set the X-Stratum header if someone connects using HTTP
* @type {String}
*/
hostname: 'localhost',
/**
* Max server lag before considering the server "too busy" and drop new connections
* @type {Number}
*/
toobusy : 70,
/**
* Bind to address, use 0.0.0.0 for external access
* @type {string}
*/
host : 'localhost',
/**
* Port for the stratum TCP server to listen on
* @type {Number}
*/
port : 3333
}
}
});
Server.commands.notify.broadcast = true;
Server.commands.set_difficulty.broadcast = true;
Server.commands.error.serverOnly = true;
return Server;
};