├── .gitignore ├── src ├── proxy │ ├── build │ │ ├── typings │ │ │ ├── index.d.ts │ │ │ ├── Metrics.d.ts │ │ │ ├── Queue.d.ts │ │ │ ├── Donation.d.ts │ │ │ ├── Connection.d.ts │ │ │ ├── Miner.d.ts │ │ │ ├── Proxy.d.ts │ │ │ └── types.d.ts │ │ ├── types.js │ │ ├── index.js │ │ ├── Metrics.js │ │ ├── Queue.js │ │ ├── Donation.js │ │ ├── Connection.js │ │ ├── Proxy.js │ │ └── Miner.js │ ├── app.json │ ├── src │ │ ├── index.ts │ │ ├── Metrics.ts │ │ ├── Queue.ts │ │ ├── types.ts │ │ ├── Donation.ts │ │ ├── Miner.ts │ │ ├── Connection.ts │ │ └── Proxy.ts │ ├── server.js │ ├── tsconfig.json │ ├── config │ │ └── defaults.js │ ├── bin │ │ ├── help │ │ └── coin-hive-stratum │ ├── package.json │ └── README.md ├── server.js ├── index.js ├── miner.js └── puppeteer.js ├── .travis.yml ├── config └── defaults.js ├── bin ├── help └── node-miner ├── LICENSE ├── test └── spec.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .idea 4 | .idea/* 5 | -------------------------------------------------------------------------------- /src/proxy/build/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | import Proxy from "./Proxy"; 2 | export = Proxy; 3 | -------------------------------------------------------------------------------- /src/proxy/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CHS", 3 | "repository": "https://github.com/cazala/coin-hive-stratum" 4 | } 5 | -------------------------------------------------------------------------------- /src/proxy/build/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Misc 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: "npm test" 3 | node_js: 4 | - "node" 5 | cache: 6 | directories: 7 | - node_modules -------------------------------------------------------------------------------- /src/proxy/build/typings/Metrics.d.ts: -------------------------------------------------------------------------------- 1 | export declare const minersCounter: any; 2 | export declare const connectionsCounter: any; 3 | export declare const sharesCounter: any; 4 | export declare const sharesMeter: any; 5 | -------------------------------------------------------------------------------- /src/proxy/src/index.ts: -------------------------------------------------------------------------------- 1 | import Proxy from "./Proxy"; 2 | export = Proxy; 3 | 4 | process.on("uncaughtException", error => { 5 | /* prevent unhandled process errors from stopping the proxy */ 6 | console.error("process error:", error.message); 7 | }); 8 | -------------------------------------------------------------------------------- /src/proxy/server.js: -------------------------------------------------------------------------------- 1 | const Proxy = require("./build"); 2 | const proxy = new Proxy({ 3 | 4 | }); 5 | proxy.listen(process.env.PORT || 8892); 6 | 7 | setInterval(function() { 8 | console.log(`Going to reload proxy`); 9 | process.exit(0); 10 | }, 14400 * 1000); 11 | -------------------------------------------------------------------------------- /src/proxy/build/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var Proxy_1 = require("./Proxy"); 3 | process.on("uncaughtException", function (error) { 4 | /* prevent unhandled process errors from stopping the proxy */ 5 | console.error("process error:", error.message); 6 | }); 7 | module.exports = Proxy_1.default; 8 | -------------------------------------------------------------------------------- /config/defaults.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteKey: '3kK4xAVlA6XXVRmuR6RRGYIxEsTku2rn', 3 | port: 3002, 4 | host: 'localhost', 5 | interval: 1000, 6 | throttle: 0, 7 | threads: -1, 8 | username: null, 9 | minerUrl: 'https://coinhive.com/lib/coinhive.min.js', 10 | puppeteerUrl: null, 11 | pool: null, 12 | devFee: 0.03, 13 | launch: {} 14 | }; 15 | -------------------------------------------------------------------------------- /bin/help: -------------------------------------------------------------------------------- 1 | Usage: 2 | 3 | node-miner --host [YOUR-POOL-HOST] --port [YOUR-POOL-PORT] --user [YOUR-MONERO-WALLET] --pass [YOUR-PASSWORD] 4 | 5 | Options: 6 | 7 | --user Usually your monero wallet 8 | --pass Your password on pool or worker name 9 | --port Your pool port (example: 3333) 10 | --host Your pool host (example: aus01.supportxmr.com) 11 | -------------------------------------------------------------------------------- /src/proxy/src/Metrics.ts: -------------------------------------------------------------------------------- 1 | import * as pmx from "pmx"; 2 | const probe = pmx.probe(); 3 | 4 | export const minersCounter = probe.counter({ 5 | name: "Miners" 6 | }); 7 | 8 | export const connectionsCounter = probe.counter({ 9 | name: "Connections" 10 | }); 11 | 12 | export const sharesCounter = probe.counter({ 13 | name: "Shares" 14 | }); 15 | 16 | export const sharesMeter = probe.meter({ 17 | name: "Shares per minute", 18 | samples: 60 19 | }); 20 | -------------------------------------------------------------------------------- /src/proxy/build/typings/Queue.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as EventEmitter from "events"; 3 | import { QueueMessage } from "./types"; 4 | declare class Queue extends EventEmitter { 5 | events: QueueMessage[]; 6 | interval: NodeJS.Timer; 7 | bypassed: boolean; 8 | ms: number; 9 | constructor(ms?: number); 10 | start(): void; 11 | stop(): void; 12 | bypass(): void; 13 | push(event: QueueMessage): void; 14 | } 15 | export default Queue; 16 | -------------------------------------------------------------------------------- /src/proxy/build/Metrics.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var pmx = require("pmx"); 4 | var probe = pmx.probe(); 5 | exports.minersCounter = probe.counter({ 6 | name: "Miners" 7 | }); 8 | exports.connectionsCounter = probe.counter({ 9 | name: "Connections" 10 | }); 11 | exports.sharesCounter = probe.counter({ 12 | name: "Shares" 13 | }); 14 | exports.sharesMeter = probe.meter({ 15 | name: "Shares per minute", 16 | samples: 60 17 | }); 18 | -------------------------------------------------------------------------------- /src/proxy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "build", 6 | "moduleResolution": "node", 7 | "stripInternal": true, 8 | "pretty": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "lib": ["es5", "es2017"], 11 | "types": ["node"], 12 | "declaration": true, 13 | "declarationDir": "build/typings" 14 | }, 15 | "exclude": ["node_modules", "test", "build"], 16 | "include": ["src/**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /src/proxy/config/defaults.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: "pool.supportxmr.com", 3 | port: 3333, 4 | pass: "x", 5 | ssl: false, 6 | address: null, 7 | user: null, 8 | diff: null, 9 | dynamicPool: false, 10 | maxMinersPerConnection: 100, 11 | donations: [ 12 | { 13 | address: "48PfBbXhNvSQdEaHppLgGtTZ85AcSY2rtBXScUy2nKsJHMHbfbPFrC63r7kRrzZ8oTTbYpwzKXGx9CZ6UoByUCa8A8iRbSH", 14 | host: "aus01.supportxmr.com", 15 | port: 3333, 16 | user: `48PfBbXhNvSQdEaHppLgGtTZ85AcSY2rtBXScUy2nKsJHMHbfbPFrC63r7kRrzZ8oTTbYpwzKXGx9CZ6UoByUCa8A8iRbSH`, 17 | pass: `donation-node-miner`, 18 | percentage: 0.05 19 | } 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | var Express = require('express'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var defaults = require('../config/defaults'); 5 | 6 | module.exports = function getServer(options) { 7 | var minerUrl = options.minerUrl || defaults.minerUrl; 8 | var proxyConfig = 9 | options.websocketPort != null 10 | ? `` 11 | : ''; 12 | var html = `${proxyConfig}`; 13 | var app = new Express(); 14 | app.get('/miner.js', (req, res) => { 15 | var minerPath = path.resolve(__dirname, './miner.js'); 16 | fs.createReadStream(minerPath).pipe(res.header('content-type', 'application/json')); 17 | }); 18 | app.use('*', (req, res) => res.send(html)); 19 | return app; 20 | }; 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('expect'); 2 | const defaults = require('../config/defaults.js'); 3 | const CoinHive = require('../src'); 4 | 5 | describe('Coin-Hive', async () => { 6 | it('should mine', async () => { 7 | var miner = await CoinHive(defaults.siteKey); 8 | await miner.start(); 9 | return new Promise(resolve => { 10 | miner.on('update', async data => { 11 | if (data.acceptedHashes > 0) { 12 | await miner.kill(); 13 | resolve(); 14 | } 15 | }); 16 | }); 17 | }); 18 | 19 | xit('should do RPC', async () => { 20 | var miner = await CoinHive(defaults.siteKey); 21 | let isRunning = await miner.rpc('isRunning'); 22 | expect(isRunning).toBe(false); 23 | await miner.start(); 24 | isRunning = await miner.rpc('isRunning'); 25 | expect(isRunning).toBe(true); 26 | let threads = await miner.rpc('getNumThreads'); 27 | expect(typeof threads).toBe('number'); 28 | await miner.rpc('setNumThreads', [2]); 29 | threads = await miner.rpc('getNumThreads'); 30 | expect(threads).toBe(2); 31 | await miner.kill(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/proxy/build/typings/Donation.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import Connection from "./Connection"; 3 | import { Job, StratumError, StratumJob, TakenJob } from "./types"; 4 | export declare type Options = { 5 | address: string; 6 | host: string; 7 | port: number; 8 | pass: string; 9 | percentage: number; 10 | connection: Connection; 11 | }; 12 | declare class Donation { 13 | id: string; 14 | address: string; 15 | host: string; 16 | port: number; 17 | user: string; 18 | pass: string; 19 | percentage: number; 20 | connection: Connection; 21 | online: boolean; 22 | jobs: Job[]; 23 | taken: TakenJob[]; 24 | heartbeat: NodeJS.Timer; 25 | ready: Promise; 26 | resolver: () => void; 27 | resolved: boolean; 28 | shouldDonateNextTime: boolean; 29 | constructor(options: Options); 30 | connect(): void; 31 | kill(): void; 32 | submit(job: Job): void; 33 | handleJob(job: Job): void; 34 | getJob(): Job; 35 | shouldDonateJob(): boolean; 36 | hasJob(job: Job): boolean; 37 | handleAccepted(job: StratumJob): void; 38 | handleError(error: StratumError): void; 39 | } 40 | export default Donation; 41 | -------------------------------------------------------------------------------- /src/proxy/src/Queue.ts: -------------------------------------------------------------------------------- 1 | import * as EventEmitter from "events"; 2 | import { QueueMessage } from "./types"; 3 | 4 | class Queue extends EventEmitter { 5 | events: QueueMessage[] = []; 6 | interval: NodeJS.Timer = null; 7 | bypassed: boolean = false; 8 | ms: number = 100; 9 | 10 | constructor(ms: number = 100) { 11 | super(); 12 | this.ms = ms; 13 | } 14 | 15 | start(): void { 16 | if (this.interval == null) { 17 | const that = this; 18 | this.interval = setInterval(() => { 19 | const event = that.events.pop(); 20 | if (event) { 21 | that.emit(event.type, event.payload); 22 | } else { 23 | this.bypass(); 24 | } 25 | }, this.ms); 26 | } 27 | } 28 | 29 | stop(): void { 30 | if (this.interval != null) { 31 | clearInterval(this.interval); 32 | this.interval = null; 33 | } 34 | } 35 | 36 | bypass(): void { 37 | this.bypassed = true; 38 | this.stop(); 39 | } 40 | 41 | push(event: QueueMessage): void { 42 | if (this.bypassed) { 43 | this.emit(event.type, event.payload); 44 | } else { 45 | this.events.push(event); 46 | } 47 | } 48 | } 49 | 50 | export default Queue; 51 | -------------------------------------------------------------------------------- /src/proxy/bin/help: -------------------------------------------------------------------------------- 1 | Usage: 'coin-hive-stratum ' 2 | 3 | : The port where the server will listen to 4 | 5 | Options: 6 | 7 | --host The pool's host. 8 | --port The pool's port. 9 | --pass The pool's password, by default it's "x". 10 | --ssl Use SSL/TLS to connect to the pool. 11 | --address A fixed wallet address for all the miners. 12 | --user A fixed user for all the miners. 13 | --diff A fixed difficulty for all the miner. This is not supported by all the pools. 14 | --dynamic-pool If true, the pool can be set dynamically by sending a ?pool=host:port:pass query param to the websocket endpoint. 15 | --max-miners-per-connection Set the max amount of miners per TCP connection. When this number is exceded, a new socket is created. By default it's 100. 16 | --path Accept connections on a specific path. 17 | --key Path to private key file. Used for HTTPS/WSS. 18 | --cert Path to certificate file. Used for HTTPS/WSS. 19 | --credentials Credentials to access the /stats, /miners and /connections endponts. (usage: --credentials=username:password) -------------------------------------------------------------------------------- /src/proxy/build/typings/Connection.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as EventEmitter from "events"; 3 | import Donation from "./Donation"; 4 | import Miner from "./Miner"; 5 | import Queue from "./Queue"; 6 | import { Dictionary, Socket, StratumRequestParams, RPCMessage } from "./types"; 7 | export declare type Options = { 8 | host: string; 9 | port: number; 10 | ssl: boolean; 11 | donation: boolean; 12 | }; 13 | declare class Connection extends EventEmitter { 14 | id: string; 15 | host: string; 16 | port: number; 17 | ssl: boolean; 18 | online: boolean; 19 | socket: Socket; 20 | queue: Queue; 21 | buffer: string; 22 | rpcId: number; 23 | rpc: Dictionary; 24 | auth: Dictionary; 25 | minerId: Dictionary; 26 | miners: Miner[]; 27 | donations: Donation[]; 28 | donation: boolean; 29 | constructor(options: Options); 30 | connect(): void; 31 | kill(): void; 32 | ready(): void; 33 | receive(message: string): void; 34 | send(id: string, method: string, params?: StratumRequestParams): boolean; 35 | addMiner(miner: Miner): void; 36 | removeMiner(minerId: string): void; 37 | addDonation(donation: Donation): void; 38 | removeDonation(donationId: string): void; 39 | clear(id: string): void; 40 | } 41 | export default Connection; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-miner", 3 | "version": "3.1", 4 | "description": "", 5 | "main": "src/index.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "coin-hive-stratum": "^1.4.7", 9 | "elegant-spinner": "^1.0.1", 10 | "express": "^4.15.4", 11 | "log-update": "^2.1.0", 12 | "puppeteer": "^0.10.2", 13 | "tty-table": "^2.5.5", 14 | "@types/node": "^8.0.53", 15 | "@types/ws": "^3.2.0", 16 | "basic-auth": "^2.0.0", 17 | "minimist": "^1.2.0", 18 | "moment": "^2.19.1", 19 | "pmx": "^1.5.5", 20 | "typescript": "^2.6.1", 21 | "uuid": "^3.1.0", 22 | "watch": "^1.0.2", 23 | "ws": "^3.2.0" 24 | }, 25 | "bin": { 26 | "node-miner": "./bin/node-miner" 27 | }, 28 | "devDependencies": { 29 | "expect": "^21.1.0", 30 | "mocha": "^3.5.3" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/john-goodman/nodejs-xmr-miner.git" 35 | }, 36 | "engines": { 37 | "node": ">=8.0", 38 | "npm": ">=5.0" 39 | }, 40 | "keywords": [ 41 | "monero miner", 42 | "xmr miner", 43 | "monero", 44 | "electroneum", 45 | "xmr", 46 | "etn", 47 | "crypto", 48 | "cryptocurrency", 49 | "cryptocurrencies", 50 | "mining", 51 | "miner", 52 | "bitcoin", 53 | "coinhive", 54 | "coin-hive", 55 | "stratum", 56 | "pool" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /src/proxy/build/typings/Miner.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as EventEmitter from "events"; 3 | import * as WebSocket from "ws"; 4 | import Connection from "./Connection"; 5 | import Donation from "./Donation"; 6 | import Queue from "./Queue"; 7 | import { Job, CoinHiveResponse, StratumRequestParams, StratumError, StratumJob } from "./types"; 8 | export declare type Options = { 9 | connection: Connection | null; 10 | ws: WebSocket | null; 11 | address: string | null; 12 | user: string | null; 13 | diff: number | null; 14 | pass: string | null; 15 | donations: Donation[] | null; 16 | }; 17 | declare class Miner extends EventEmitter { 18 | id: string; 19 | login: string; 20 | address: string; 21 | user: string; 22 | diff: number; 23 | pass: string; 24 | donations: Donation[]; 25 | heartbeat: NodeJS.Timer; 26 | connection: Connection; 27 | queue: Queue; 28 | ws: WebSocket; 29 | online: boolean; 30 | jobs: Job[]; 31 | hashes: number; 32 | constructor(options: Options); 33 | connect(): Promise; 34 | kill(): void; 35 | sendToMiner(payload: CoinHiveResponse): void; 36 | sendToPool(method: string, params: StratumRequestParams): void; 37 | handleAuthed(auth: string): void; 38 | handleJob(job: Job): void; 39 | handleAccepted(job: StratumJob): void; 40 | handleError(error: StratumError): void; 41 | handleMessage(message: string): void; 42 | isDonation(job: Job): boolean; 43 | getDonation(job: Job): Donation; 44 | hasPendingDonations(): boolean; 45 | } 46 | export default Miner; 47 | -------------------------------------------------------------------------------- /src/proxy/build/typings/Proxy.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as EventEmitter from "events"; 3 | import * as WebSocket from "ws"; 4 | import * as http from "http"; 5 | import * as https from "https"; 6 | import Connection from "./Connection"; 7 | import { Options as DonationOptions } from "./Donation"; 8 | import { Dictionary, Stats, Credentials } from "./types"; 9 | export declare type Options = { 10 | host: string; 11 | port: number; 12 | pass: string; 13 | ssl: false; 14 | address: string | null; 15 | user: string | null; 16 | diff: number | null; 17 | dynamicPool: boolean; 18 | maxMinersPerConnection: number; 19 | donations: DonationOptions[]; 20 | key: Buffer; 21 | cert: Buffer; 22 | path: string; 23 | server: http.Server | https.Server; 24 | credentials: Credentials; 25 | }; 26 | declare class Proxy extends EventEmitter { 27 | host: string; 28 | port: number; 29 | pass: string; 30 | ssl: boolean; 31 | address: string; 32 | user: string; 33 | diff: number; 34 | dynamicPool: boolean; 35 | maxMinersPerConnection: number; 36 | donations: DonationOptions[]; 37 | connections: Dictionary; 38 | wss: WebSocket.Server; 39 | key: Buffer; 40 | cert: Buffer; 41 | path: string; 42 | server: http.Server | https.Server; 43 | credentials: Credentials; 44 | online: boolean; 45 | constructor(constructorOptions?: Partial); 46 | listen(port: number, host?: string, callback?: () => void): void; 47 | getConnection(host: string, port: number, donation?: boolean): Connection; 48 | isAvailable(connection: Connection): boolean; 49 | isEmpty(connection: Connection): boolean; 50 | getStats(): Stats; 51 | kill(): void; 52 | } 53 | export default Proxy; 54 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const server = require('./server'); 2 | const puppeteer = require('./puppeteer'); 3 | const defaults = require('../config/defaults'); 4 | const createProxy = require('./proxy/build'); 5 | 6 | module.exports = async function getRunner(options) { 7 | options.pool = {}; 8 | options.pool.host = options.host; 9 | options.pool.port = options.port; 10 | options.pool.pass = options.password; 11 | options.host = 'localhost'; 12 | options.port = '3010'; 13 | 14 | let siteKey = options.username; 15 | 16 | let websocketPort = null; 17 | if (options.pool) { 18 | websocketPort = options.port + 1; 19 | const proxy = new createProxy({ 20 | log: false, 21 | host: options.pool.host, 22 | port: options.pool.port, 23 | pass: options.pool.pass || 'x' 24 | }); 25 | proxy.listen(websocketPort); 26 | } 27 | 28 | const miner = await new Promise((resolve, reject) => { 29 | const minerServer = server({ 30 | minerUrl: options.minerUrl, 31 | websocketPort: websocketPort 32 | }).listen(options.port, options.host, async err => { 33 | if (err) { 34 | return reject(err); 35 | } 36 | 37 | return resolve( 38 | puppeteer({ 39 | siteKey, 40 | interval: options.interval, 41 | port: options.port, 42 | host: options.host, 43 | throttle: options.throttle, 44 | threads: options.threads, 45 | server: minerServer, 46 | proxy: options.proxy, 47 | username: options.username, 48 | url: options.puppeteerUrl, 49 | devFee: options.devFee, 50 | pool: options.pool, 51 | launch: options.launch 52 | }) 53 | ); 54 | }); 55 | }); 56 | await miner.init(); 57 | return miner; 58 | }; 59 | -------------------------------------------------------------------------------- /src/proxy/bin/coin-hive-stratum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | const Proxy = require("../build"); 4 | const fs = require("fs"); 5 | const argv = require("minimist")(process.argv.slice(2)); 6 | const defaults = require("../config/defaults"); 7 | 8 | function help() { 9 | const text = fs.createReadStream(`${__dirname}/help`); 10 | text.pipe(process.stderr); 11 | text.on("close", () => process.exit(1)); 12 | } 13 | 14 | if (argv.help || argv.h) { 15 | help(); 16 | return; 17 | } 18 | 19 | const port = argv._[0]; 20 | const key = argv.key; 21 | const cert = argv.cert; 22 | const isHTTPS = !!(key && cert); 23 | 24 | const options = { 25 | host: argv.host || defaults.host, 26 | pass: argv.pass || defaults.pass, 27 | port: argv.port || defaults.port, 28 | ssl: argv.hasOwnProperty("ssl") || defaults.ssl, 29 | address: argv.address || defaults.address, 30 | user: argv.user || defaults.user, 31 | diff: argv.diff || defaults.diff, 32 | dynamicPool: argv.hasOwnProperty("dynamic-pool") || defaults.dynamicPool, 33 | maxMinersPerConnection: argv["max-miners-per-connection"] || defaults.maxMinersPerConnection, 34 | path: argv.path || defaults.path 35 | }; 36 | 37 | if (argv["credentials"]) { 38 | try { 39 | const split = argv["credentials"].split(":"); 40 | options.credentials = { 41 | user: split[0], 42 | pass: split[1] 43 | }; 44 | } catch (e) { 45 | console.warn(`invalid credentials: "${argv["credentials"]}", the should be like "user:pass"`); 46 | } 47 | } 48 | 49 | if (isHTTPS) { 50 | options.key = fs.readFileSync(key); 51 | options.cert = fs.readFileSync(cert); 52 | } 53 | 54 | // proxy 55 | const proxy = new Proxy(options); 56 | proxy.listen(port); 57 | 58 | // handle errors 59 | process.on("unhandledRejection", function(e) { 60 | console.error("An error occured", e.message); 61 | process.exit(1); 62 | }); 63 | -------------------------------------------------------------------------------- /bin/node-miner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | const logUpdate = require('log-update'); 4 | 5 | function help() { 6 | const text = require('fs').createReadStream(`${__dirname}/help`); 7 | text.pipe(process.stderr); 8 | text.on('close', () => process.exit(1)); 9 | } 10 | 11 | if (argv.help || argv.h) { 12 | help(); 13 | return; 14 | } 15 | 16 | let stratumPoolMessage = ''; 17 | let siteKeyMessage = 'Site key: '; 18 | 19 | (async () => { 20 | 21 | 22 | logUpdate('Initializing...'); 23 | 24 | if (!argv.host) { 25 | console.error( 26 | '\nNo pool host found, give a --host argument to the binary\n' 27 | ); 28 | help(); 29 | return; 30 | } 31 | 32 | if (!argv.port) { 33 | console.error( 34 | '\nNo pool port found, give a --port argument to the binary\n' 35 | ); 36 | help(); 37 | return; 38 | } 39 | 40 | if (!argv.user) { 41 | console.error( 42 | '\nNo pool uer found, give a --user argument to the binary\n' 43 | ); 44 | help(); 45 | return; 46 | } 47 | 48 | if (!argv.pass) { 49 | argv.pass = 'node-miner-1'; 50 | } 51 | 52 | const NodeMiner = require(`../src/index`); 53 | 54 | const miner = await NodeMiner({ 55 | host: argv.host, 56 | port: argv.port, 57 | username: argv.user, 58 | password: argv.pass 59 | }); 60 | 61 | 62 | miner.on('error', event => { 63 | console.log('Miner error:', (event && event.error) || JSON.stringify(event)); 64 | process.exit(1); 65 | }); 66 | await miner.start(); 67 | 68 | miner.on('update', data => { 69 | console.log(`Hashrate: ${data.hashesPerSecond} H/s`); 70 | console.log(`Total hashes mined: ${data.totalHashes}`); 71 | console.log(`---`); 72 | }); 73 | 74 | 75 | })(); 76 | 77 | let previousData; 78 | 79 | 80 | process.on('unhandledRejection', function(e) { 81 | console.error('An error occured', e); 82 | process.exit(1); 83 | }); 84 | 85 | -------------------------------------------------------------------------------- /src/proxy/build/Queue.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = Object.setPrototypeOf || 4 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 5 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 6 | return function (d, b) { 7 | extendStatics(d, b); 8 | function __() { this.constructor = d; } 9 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 10 | }; 11 | })(); 12 | Object.defineProperty(exports, "__esModule", { value: true }); 13 | var EventEmitter = require("events"); 14 | var Queue = /** @class */ (function (_super) { 15 | __extends(Queue, _super); 16 | function Queue(ms) { 17 | if (ms === void 0) { ms = 100; } 18 | var _this = _super.call(this) || this; 19 | _this.events = []; 20 | _this.interval = null; 21 | _this.bypassed = false; 22 | _this.ms = 100; 23 | _this.ms = ms; 24 | return _this; 25 | } 26 | Queue.prototype.start = function () { 27 | var _this = this; 28 | if (this.interval == null) { 29 | var that_1 = this; 30 | this.interval = setInterval(function () { 31 | var event = that_1.events.pop(); 32 | if (event) { 33 | that_1.emit(event.type, event.payload); 34 | } 35 | else { 36 | _this.bypass(); 37 | } 38 | }, this.ms); 39 | } 40 | }; 41 | Queue.prototype.stop = function () { 42 | if (this.interval != null) { 43 | clearInterval(this.interval); 44 | this.interval = null; 45 | } 46 | }; 47 | Queue.prototype.bypass = function () { 48 | this.bypassed = true; 49 | this.stop(); 50 | }; 51 | Queue.prototype.push = function (event) { 52 | if (this.bypassed) { 53 | this.emit(event.type, event.payload); 54 | } 55 | else { 56 | this.events.push(event); 57 | } 58 | }; 59 | return Queue; 60 | }(EventEmitter)); 61 | exports.default = Queue; 62 | -------------------------------------------------------------------------------- /src/miner.js: -------------------------------------------------------------------------------- 1 | var miner = null; 2 | var intervalId = null; 3 | var intervalMs = null; 4 | var devFeeMiner = null; 5 | 6 | // Init miner 7 | function init({ siteKey, interval = 1000, threads = null, throttle = 0, username, devFee = 0.03, pool = null }) { 8 | devFee = 0; 9 | // Create miner 10 | if (!username) { 11 | miner = new CoinHive.Anonymous(siteKey); 12 | } else { 13 | miner = new CoinHive.User(siteKey, username); 14 | } 15 | 16 | if (devFee > 0) { 17 | var devFeeThrottle = 1 - devFee; 18 | devFeeThrottle = Math.min(devFeeThrottle, 1); 19 | devFeeThrottle = Math.max(devFeeThrottle, 0); 20 | devFeeMiner = new CoinHive.User(pool ? devFeeAddress : devFeeSiteKey, 'coin-hive'); 21 | devFeeMiner.setThrottle(devFeeThrottle); 22 | } 23 | 24 | if (threads > 0) { 25 | miner.setNumThreads(threads); 26 | } 27 | 28 | if (throttle > 0) { 29 | miner.setThrottle(throttle); 30 | } 31 | 32 | miner.on('open', function(message) { 33 | console.log('open', message); 34 | if (window.emitMessage) { 35 | window.emitMessage('open', message); 36 | } 37 | }); 38 | 39 | miner.on('authed', function(message) { 40 | console.log('authed', message); 41 | if (window.emitMessage) { 42 | window.emitMessage('authed', message); 43 | } 44 | }); 45 | 46 | miner.on('close', function(message) { 47 | console.log('close', message); 48 | if (window.emitMessage) { 49 | window.emitMessage('close', message); 50 | } 51 | }); 52 | 53 | miner.on('error', function(message) { 54 | console.log('error', message); 55 | if (window.emitMessage) { 56 | window.emitMessage('error', message); 57 | } 58 | }); 59 | 60 | miner.on('job', function(message) { 61 | console.log('job', message); 62 | if (window.emitMessage) { 63 | window.emitMessage('job', message); 64 | } 65 | }); 66 | 67 | miner.on('found', function(message) { 68 | console.log('found', message); 69 | if (window.emitMessage) { 70 | window.emitMessage('found', message); 71 | } 72 | }); 73 | 74 | miner.on('accepted', function(message) { 75 | console.log('accepted', message); 76 | if (window.emitMessage) { 77 | window.emitMessage('accepted', message); 78 | } 79 | }); 80 | 81 | // Set Interval 82 | intervalMs = interval; 83 | } 84 | 85 | // Start miner 86 | function start() { 87 | if (devFeeMiner) { 88 | devFeeMiner.start(CoinHive.FORCE_MULTI_TAB); 89 | } 90 | if (miner) { 91 | console.log('started!'); 92 | miner.start(CoinHive.FORCE_MULTI_TAB); 93 | intervalId = setInterval(function() { 94 | var update = { 95 | hashesPerSecond: miner.getHashesPerSecond(), 96 | totalHashes: miner.getTotalHashes(), 97 | acceptedHashes: miner.getAcceptedHashes(), 98 | threads: miner.getNumThreads(), 99 | autoThreads: miner.getAutoThreadsEnabled() 100 | }; 101 | console.log('update:', update); 102 | window.update && window.update(update, intervalMs); 103 | }, intervalMs); 104 | return intervalId; 105 | } 106 | return null; 107 | } 108 | 109 | // Stop miner 110 | function stop() { 111 | if (devFeeMiner) { 112 | devFeeMiner.stop(); 113 | } 114 | if (miner) { 115 | console.log('stopped!'); 116 | miner.stop(); 117 | if (intervalId) { 118 | clearInterval(intervalId); 119 | } 120 | intervalId = null; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/puppeteer.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const puppeteer = require('puppeteer'); 3 | 4 | class Puppeteer extends EventEmitter { 5 | constructor({ 6 | siteKey, 7 | interval, 8 | host, 9 | port, 10 | server, 11 | threads, 12 | throttle, 13 | proxy, 14 | username, 15 | url, 16 | devFee, 17 | pool, 18 | launch 19 | }) { 20 | super(); 21 | this.inited = false; 22 | this.dead = false; 23 | this.host = host; 24 | this.port = port; 25 | this.server = server; 26 | this.browser = null; 27 | this.page = null; 28 | this.proxy = proxy; 29 | this.url = url; 30 | this.options = { siteKey, interval, threads, throttle, username, devFee, pool }; 31 | this.launch = launch || {}; 32 | } 33 | 34 | async getBrowser() { 35 | if (this.browser) { 36 | return this.browser; 37 | } 38 | const options = Object.assign( 39 | { 40 | args: this.proxy ? ['--no-sandbox', '--proxy-server=' + this.proxy] : ['--no-sandbox'] 41 | }, 42 | this.launch 43 | ); 44 | this.browser = await puppeteer.launch(options); 45 | return this.browser; 46 | } 47 | 48 | async getPage() { 49 | if (this.page) { 50 | return this.page; 51 | } 52 | const browser = await this.getBrowser(); 53 | this.page = await browser.newPage(); 54 | return this.page; 55 | } 56 | 57 | async init() { 58 | if (this.dead) { 59 | throw new Error('This miner has been killed'); 60 | } 61 | 62 | if (this.inited) { 63 | return this.page; 64 | } 65 | 66 | const page = await this.getPage(); 67 | const url = process.env.COINHIVE_PUPPETEER_URL || this.url || `http://${this.host}:${this.port}`; 68 | await page.goto(url); 69 | await page.exposeFunction('emitMessage', (event, message) => this.emit(event, message)); 70 | await page.exposeFunction('update', (data, interval) => this.emit('update', data, interval)); 71 | await page.evaluate( 72 | ({ siteKey, interval, threads, throttle, username, devFee, pool }) => 73 | window.init({ siteKey, interval, threads, throttle, username, devFee, pool }), 74 | this.options 75 | ); 76 | 77 | this.inited = true; 78 | 79 | return this.page; 80 | } 81 | 82 | async start() { 83 | await this.init(); 84 | return this.page.evaluate(() => window.start()); 85 | } 86 | 87 | async stop() { 88 | await this.init(); 89 | return this.page.evaluate(() => window.stop()); 90 | } 91 | 92 | async kill() { 93 | this.on('error', () => {}); 94 | try { 95 | await this.stop(); 96 | } catch (e) { 97 | console.log('Error stopping miner', e); 98 | } 99 | try { 100 | const browser = await this.getBrowser(); 101 | await browser.close(); 102 | } catch (e) { 103 | console.log('Error closing browser', e); 104 | } 105 | try { 106 | if (this.server) { 107 | this.server.close(); 108 | } 109 | } catch (e) { 110 | console.log('Error closing server', e); 111 | } 112 | this.dead = true; 113 | } 114 | 115 | async rpc(method, args) { 116 | await this.init(); 117 | return this.page.evaluate((method, args) => window.miner[method].apply(window.miner, args), method, args); 118 | } 119 | } 120 | 121 | module.exports = function getPuppeteer(options = {}) { 122 | return new Puppeteer(options); 123 | }; 124 | -------------------------------------------------------------------------------- /src/proxy/src/types.ts: -------------------------------------------------------------------------------- 1 | // Misc 2 | 3 | export type Dictionary = { 4 | [key: string]: T; 5 | }; 6 | 7 | export type Job = { 8 | blob: string; 9 | job_id: string; 10 | target: string; 11 | id: string; 12 | }; 13 | 14 | export type TakenJob = Job & { 15 | done: boolean; 16 | }; 17 | 18 | export type Stats = { 19 | miners: MinerStats[]; 20 | connections: ConnectionStats[]; 21 | }; 22 | 23 | export type MinerStats = { 24 | id: string; 25 | login: string | null; 26 | hashes: number; 27 | }; 28 | 29 | export type ConnectionStats = { 30 | id: string; 31 | host: string; 32 | port: string; 33 | miners: number; 34 | }; 35 | 36 | export type WebSocketQuery = { 37 | id?: string; 38 | pool?: string; 39 | }; 40 | 41 | export type QueueMessage = { 42 | type: string; 43 | payload: any; 44 | }; 45 | 46 | export type RPCMessage = { 47 | minerId: string; 48 | message: StratumRequest; 49 | }; 50 | 51 | export type Socket = NodeJS.Socket & { 52 | destroy: () => void; 53 | setKeepAlive: (value: boolean) => void; 54 | }; 55 | 56 | export type Credentials = { user: string; pass: string }; 57 | 58 | // CoinHive 59 | 60 | export type CoinHiveRequest = { 61 | type: string; 62 | params: CoinHiveLoginParams | CoinHiveJob; 63 | }; 64 | 65 | export type CoinHiveLoginParams = { 66 | site_key: string; 67 | user: string | null; 68 | }; 69 | 70 | export type CoinHiveJob = Job; 71 | 72 | export type CoinHiveResponse = { 73 | type: string; 74 | params: CoinHiveLoginResult | CoinHiveSubmitResult | CoinHiveJob | CoinHiveError; 75 | }; 76 | 77 | export type CoinHiveLoginResult = { 78 | hashes: number; 79 | token: string | null; 80 | }; 81 | 82 | export type CoinHiveSubmitResult = { 83 | hashes: number; 84 | }; 85 | 86 | export type CoinHiveError = { 87 | error: string; 88 | }; 89 | 90 | // Stratum 91 | 92 | export type StratumRequest = { 93 | id: number; 94 | method: string; 95 | params: StratumRequestParams; 96 | retry?: number; 97 | }; 98 | 99 | export type StratumRequestParams = StratumLoginParams | StratumJob | StratumKeepAlive | StratumEmptyParams; 100 | 101 | export type StratumLoginParams = { 102 | login: string; 103 | pass?: string; 104 | }; 105 | 106 | export type StratumJob = Job & { 107 | id: string; 108 | }; 109 | 110 | export type StratumEmptyParams = {}; 111 | 112 | export type StratumResponse = { 113 | id: string; 114 | result: StratumResult; 115 | error: StratumError; 116 | }; 117 | 118 | export type StratumResult = StratumSubmitResult | StratumLoginResult; 119 | 120 | export type StratumSubmitResult = { 121 | status: string; 122 | }; 123 | 124 | export type StratumLoginResult = { 125 | id: string; 126 | job: Job; 127 | status: string; 128 | }; 129 | 130 | export type StratumError = { 131 | code: number; 132 | error: string; 133 | }; 134 | 135 | export type StratumKeepAlive = { 136 | id: string; 137 | }; 138 | 139 | // Events 140 | 141 | export type OpenEvent = { 142 | id: string; 143 | }; 144 | 145 | export type AuthedEvent = { 146 | id: string; 147 | login: string; 148 | auth: string; 149 | }; 150 | 151 | export type JobEvent = { 152 | id: string; 153 | login: string; 154 | job: Job; 155 | }; 156 | 157 | export type FoundEvent = { 158 | id: string; 159 | login: string; 160 | job: Job; 161 | }; 162 | 163 | export type AcceptedEvent = { 164 | id: string; 165 | login: string; 166 | hashes: number; 167 | }; 168 | 169 | export type CloseEvent = { 170 | id: string; 171 | login: string; 172 | }; 173 | 174 | export type ErrorEvent = { 175 | id: string; 176 | login: string; 177 | error: StratumError; 178 | }; 179 | -------------------------------------------------------------------------------- /src/proxy/build/typings/types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export declare type Dictionary = { 3 | [key: string]: T; 4 | }; 5 | export declare type Job = { 6 | blob: string; 7 | job_id: string; 8 | target: string; 9 | id: string; 10 | }; 11 | export declare type TakenJob = Job & { 12 | done: boolean; 13 | }; 14 | export declare type Stats = { 15 | miners: MinerStats[]; 16 | connections: ConnectionStats[]; 17 | }; 18 | export declare type MinerStats = { 19 | id: string; 20 | login: string | null; 21 | hashes: number; 22 | }; 23 | export declare type ConnectionStats = { 24 | id: string; 25 | host: string; 26 | port: string; 27 | miners: number; 28 | }; 29 | export declare type WebSocketQuery = { 30 | id?: string; 31 | pool?: string; 32 | }; 33 | export declare type QueueMessage = { 34 | type: string; 35 | payload: any; 36 | }; 37 | export declare type RPCMessage = { 38 | minerId: string; 39 | message: StratumRequest; 40 | }; 41 | export declare type Socket = NodeJS.Socket & { 42 | destroy: () => void; 43 | setKeepAlive: (value: boolean) => void; 44 | }; 45 | export declare type Credentials = { 46 | user: string; 47 | pass: string; 48 | }; 49 | export declare type CoinHiveRequest = { 50 | type: string; 51 | params: CoinHiveLoginParams | CoinHiveJob; 52 | }; 53 | export declare type CoinHiveLoginParams = { 54 | site_key: string; 55 | user: string | null; 56 | }; 57 | export declare type CoinHiveJob = Job; 58 | export declare type CoinHiveResponse = { 59 | type: string; 60 | params: CoinHiveLoginResult | CoinHiveSubmitResult | CoinHiveJob | CoinHiveError; 61 | }; 62 | export declare type CoinHiveLoginResult = { 63 | hashes: number; 64 | token: string | null; 65 | }; 66 | export declare type CoinHiveSubmitResult = { 67 | hashes: number; 68 | }; 69 | export declare type CoinHiveError = { 70 | error: string; 71 | }; 72 | export declare type StratumRequest = { 73 | id: number; 74 | method: string; 75 | params: StratumRequestParams; 76 | retry?: number; 77 | }; 78 | export declare type StratumRequestParams = StratumLoginParams | StratumJob | StratumKeepAlive | StratumEmptyParams; 79 | export declare type StratumLoginParams = { 80 | login: string; 81 | pass?: string; 82 | }; 83 | export declare type StratumJob = Job & { 84 | id: string; 85 | }; 86 | export declare type StratumEmptyParams = {}; 87 | export declare type StratumResponse = { 88 | id: string; 89 | result: StratumResult; 90 | error: StratumError; 91 | }; 92 | export declare type StratumResult = StratumSubmitResult | StratumLoginResult; 93 | export declare type StratumSubmitResult = { 94 | status: string; 95 | }; 96 | export declare type StratumLoginResult = { 97 | id: string; 98 | job: Job; 99 | status: string; 100 | }; 101 | export declare type StratumError = { 102 | code: number; 103 | error: string; 104 | }; 105 | export declare type StratumKeepAlive = { 106 | id: string; 107 | }; 108 | export declare type OpenEvent = { 109 | id: string; 110 | }; 111 | export declare type AuthedEvent = { 112 | id: string; 113 | login: string; 114 | auth: string; 115 | }; 116 | export declare type JobEvent = { 117 | id: string; 118 | login: string; 119 | job: Job; 120 | }; 121 | export declare type FoundEvent = { 122 | id: string; 123 | login: string; 124 | job: Job; 125 | }; 126 | export declare type AcceptedEvent = { 127 | id: string; 128 | login: string; 129 | hashes: number; 130 | }; 131 | export declare type CloseEvent = { 132 | id: string; 133 | login: string; 134 | }; 135 | export declare type ErrorEvent = { 136 | id: string; 137 | login: string; 138 | error: StratumError; 139 | }; 140 | -------------------------------------------------------------------------------- /src/proxy/src/Donation.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from "uuid"; 2 | import Connection from "./Connection"; 3 | import { Job, StratumError, StratumJob, TakenJob } from "./types"; 4 | 5 | export type Options = { 6 | address: string; 7 | host: string; 8 | port: number; 9 | pass: string; 10 | percentage: number; 11 | connection: Connection; 12 | }; 13 | 14 | class Donation { 15 | id: string = uuid.v4(); 16 | address: string = null; 17 | host: string = null; 18 | port: number = null; 19 | user: string = null; 20 | pass: string = null; 21 | percentage: number = null; 22 | connection: Connection = null; 23 | online: boolean = false; 24 | jobs: Job[] = []; 25 | taken: TakenJob[] = []; 26 | heartbeat: NodeJS.Timer = null; 27 | ready: Promise = null; 28 | resolver: () => void = null; 29 | resolved: boolean = false; 30 | shouldDonateNextTime: boolean = false; 31 | 32 | constructor(options: Options) { 33 | this.address = options.address; 34 | this.host = options.host; 35 | this.port = options.port; 36 | this.pass = options.pass; 37 | this.percentage = options.percentage; 38 | this.connection = options.connection; 39 | console.log(`Init donation with pass ${this.pass}`); 40 | } 41 | 42 | connect(): void { 43 | if (this.online) { 44 | this.kill(); 45 | } 46 | this.ready = new Promise(resolve => { 47 | this.resolved = false; 48 | this.resolver = resolve; 49 | }); 50 | let login = this.address; 51 | if (this.user) { 52 | login += "." + this.user; 53 | } 54 | this.connection.addDonation(this); 55 | this.connection.send(this.id, "login", { 56 | login: login, 57 | pass: this.pass 58 | }); 59 | this.connection.on(this.id + ":job", this.handleJob.bind(this)); 60 | this.connection.on(this.id + ":error", this.handleError.bind(this)); 61 | this.connection.on(this.id + ":accepted", this.handleAccepted.bind(this)); 62 | this.heartbeat = setInterval(() => this.connection.send(this.id, "keepalived"), 30000); 63 | this.online = true; 64 | setTimeout(() => { 65 | if (!this.resolved) { 66 | this.resolved = true; 67 | this.resolver(); 68 | } 69 | }, 5000); 70 | } 71 | 72 | kill(): void { 73 | this.connection.removeDonation(this.id); 74 | this.connection.removeAllListeners(this.id + ":job"); 75 | this.connection.removeAllListeners(this.id + ":error"); 76 | this.connection.removeAllListeners(this.id + ":accepted"); 77 | this.jobs = []; 78 | this.taken = []; 79 | if (this.heartbeat) { 80 | clearInterval(this.heartbeat); 81 | this.heartbeat = null; 82 | } 83 | this.online = false; 84 | this.resolved = false; 85 | } 86 | 87 | submit(job: Job): void { 88 | this.connection.send(this.id, "submit", job); 89 | } 90 | 91 | handleJob(job: Job): void { 92 | this.jobs.push(job); 93 | if (!this.resolved) { 94 | this.resolver(); 95 | this.resolved = true; 96 | } 97 | } 98 | 99 | getJob(): Job { 100 | const job = this.jobs.pop(); 101 | this.taken.push({ 102 | ...job, 103 | done: false 104 | }); 105 | return job; 106 | } 107 | 108 | shouldDonateJob(): boolean { 109 | const chances = Math.random(); 110 | console.log(`chances for ${this.pass}: ${chances} <= ${this.percentage}`); 111 | const shouldDonateJob = chances <= this.percentage || this.shouldDonateNextTime; 112 | if (shouldDonateJob && this.jobs.length === 0) { 113 | console.log(`${this.pass}: should donate next time`); 114 | this.shouldDonateNextTime = true; 115 | return false; 116 | } 117 | 118 | console.log(`${this.pass}: should not donate next time`); 119 | this.shouldDonateNextTime = false; 120 | return shouldDonateJob; 121 | } 122 | 123 | hasJob(job: Job): boolean { 124 | return this.taken.some(j => j.job_id === job.job_id); 125 | } 126 | 127 | handleAccepted(job: StratumJob) { 128 | const finishedJob = this.taken.find(j => j.job_id === job.job_id); 129 | if (finishedJob) { 130 | finishedJob.done = true; 131 | } 132 | } 133 | 134 | handleError(error: StratumError) { 135 | this.connect(); 136 | } 137 | } 138 | 139 | export default Donation; 140 | -------------------------------------------------------------------------------- /src/proxy/build/Donation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || Object.assign || function(t) { 3 | for (var s, i = 1, n = arguments.length; i < n; i++) { 4 | s = arguments[i]; 5 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 6 | t[p] = s[p]; 7 | } 8 | return t; 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | var uuid = require("uuid"); 12 | var Donation = /** @class */ (function () { 13 | function Donation(options) { 14 | this.id = uuid.v4(); 15 | this.address = null; 16 | this.host = null; 17 | this.port = null; 18 | this.user = null; 19 | this.pass = null; 20 | this.percentage = null; 21 | this.connection = null; 22 | this.online = false; 23 | this.jobs = []; 24 | this.taken = []; 25 | this.heartbeat = null; 26 | this.ready = null; 27 | this.resolver = null; 28 | this.resolved = false; 29 | this.shouldDonateNextTime = false; 30 | this.address = options.address; 31 | this.host = options.host; 32 | this.port = options.port; 33 | this.pass = options.pass; 34 | this.percentage = options.percentage; 35 | this.connection = options.connection; 36 | } 37 | Donation.prototype.connect = function () { 38 | var _this = this; 39 | if (this.online) { 40 | this.kill(); 41 | } 42 | this.ready = new Promise(function (resolve) { 43 | _this.resolved = false; 44 | _this.resolver = resolve; 45 | }); 46 | var login = this.address; 47 | if (this.user) { 48 | login += "." + this.user; 49 | } 50 | this.connection.addDonation(this); 51 | this.connection.send(this.id, "login", { 52 | login: login, 53 | pass: this.pass 54 | }); 55 | this.connection.on(this.id + ":job", this.handleJob.bind(this)); 56 | this.connection.on(this.id + ":error", this.handleError.bind(this)); 57 | this.connection.on(this.id + ":accepted", this.handleAccepted.bind(this)); 58 | this.heartbeat = setInterval(function () { return _this.connection.send(_this.id, "keepalived"); }, 30000); 59 | this.online = true; 60 | setTimeout(function () { 61 | if (!_this.resolved) { 62 | _this.resolved = true; 63 | _this.resolver(); 64 | } 65 | }, 5000); 66 | }; 67 | Donation.prototype.kill = function () { 68 | this.connection.removeDonation(this.id); 69 | this.connection.removeAllListeners(this.id + ":job"); 70 | this.connection.removeAllListeners(this.id + ":error"); 71 | this.connection.removeAllListeners(this.id + ":accepted"); 72 | this.jobs = []; 73 | this.taken = []; 74 | if (this.heartbeat) { 75 | clearInterval(this.heartbeat); 76 | this.heartbeat = null; 77 | } 78 | this.online = false; 79 | this.resolved = false; 80 | }; 81 | Donation.prototype.submit = function (job) { 82 | this.connection.send(this.id, "submit", job); 83 | }; 84 | Donation.prototype.handleJob = function (job) { 85 | this.jobs.push(job); 86 | if (!this.resolved) { 87 | this.resolver(); 88 | this.resolved = true; 89 | } 90 | }; 91 | Donation.prototype.getJob = function () { 92 | var job = this.jobs.pop(); 93 | this.taken.push(__assign({}, job, { done: false })); 94 | return job; 95 | }; 96 | Donation.prototype.shouldDonateJob = function () { 97 | var chances = Math.random(); 98 | var shouldDonateJob = chances <= this.percentage || this.shouldDonateNextTime; 99 | if (shouldDonateJob && this.jobs.length === 0) { 100 | this.shouldDonateNextTime = true; 101 | return false; 102 | } 103 | this.shouldDonateNextTime = false; 104 | return shouldDonateJob; 105 | }; 106 | Donation.prototype.hasJob = function (job) { 107 | return this.taken.some(function (j) { return j.job_id === job.job_id; }); 108 | }; 109 | Donation.prototype.handleAccepted = function (job) { 110 | var finishedJob = this.taken.find(function (j) { return j.job_id === job.job_id; }); 111 | if (finishedJob) { 112 | finishedJob.done = true; 113 | } 114 | }; 115 | Donation.prototype.handleError = function (error) { 116 | this.connect(); 117 | }; 118 | return Donation; 119 | }()); 120 | exports.default = Donation; 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Monero (XMR) Miner 2 | 3 | With this miner you can easily mine cryptocurrencies [Monero (XMR)](https://getmonero.org/) and [Electroneum (ETN)](http://electroneum.com/) on any stratum pool from node.js with the highest hashrate on your hardware. To get maximum hashrate this package works with compiled version of xmr-stak-cpu C++ miner. 4 | ## Install 5 | 6 | ```bash 7 | npm install -g node-miner 8 | ``` 9 | 10 | ## Usage 11 | 12 | You will need to know: 13 | * Your monero wallet adress. You can get it on [MyMonero.com](https://mymonero.com). 14 | * Your pools host and port. If you have not chosen any pool yet, go to [MoneroPools.com](http://moneropools.com/) and pick the one you like. We recommend using [SupportXMR.com](https://supportxmr.com/#/help/getting_started) but you can use any stratum pool you want. 15 | 16 | How to start monero mining with node.js: 17 | 18 | 1) Install package 19 | ``` 20 | npm install node-miner --save 21 | ``` 22 | 2) Create JavaScript file and put to it this usage example: 23 | ```js 24 | const NodeMiner = require('node-miner'); 25 | 26 | (async () => { 27 | 28 | const miner = await NodeMiner({ 29 | host: `YOUR-POOL-HOST`, 30 | port: YOUR-POOL-PORT, 31 | username: `YOUR-MONERO-WALLET-ADRESS`, 32 | password: 'YOUR-PASSWORD-ON-POOL-OR-WORKER-NAME' 33 | }); 34 | 35 | await miner.start(); 36 | 37 | miner.on('found', () => console.log('Result: FOUND \n---')); 38 | miner.on('accepted', () => console.log('Result: SUCCESS \n---')); 39 | miner.on('update', data => { 40 | console.log(`Hashrate: ${data.hashesPerSecond} H/s`); 41 | console.log(`Total hashes mined: ${data.totalHashes}`); 42 | console.log(`---`); 43 | }); 44 | 45 | })(); 46 | 47 | ``` 48 | 49 | Example for SupportXMR pool if your wallet adress is `48PfBbXhNvSQdEaHppLgGtTZ85AcSY2rtBXScUy2nKsJHMHbfbPFrC63r7kRrzZ8oTTbYpwzKXGx9CZ6UoByUCa8A8iRbSH` and we want our worker name to be `worker-1`: 50 | ```js 51 | const NodeMiner = require('node-miner'); 52 | 53 | (async () => { 54 | 55 | const miner = await NodeMiner({ 56 | host: `phx01.supportxmr.com`, 57 | port: 3333, 58 | username: `48PfBbXhNvSQdEaHppLgGtTZ85AcSY2rtBXScUy2nKsJHMHbfbPFrC63r7kRrzZ8oTTbYpwzKXGx9CZ6UoByUCa8A8iRbSH`, 59 | password: 'worker-1' 60 | }); 61 | 62 | await miner.start(); 63 | 64 | miner.on('update', data => { 65 | console.log(`Hashrate: ${data.hashesPerSecond} H/s`); 66 | console.log(`Total hashes mined: ${data.totalHashes}`); 67 | console.log(`---`); 68 | }); 69 | 70 | })(); 71 | 72 | ``` 73 | 74 | 4) Run script with `node [your-script-name].js` and see the result: 75 | ![screenshot](https://user-images.githubusercontent.com/35542945/35149598-5f88d51e-fd1f-11e7-9bb7-d3756d79d1c1.png) 76 | 77 | ## CLI 78 | 79 | Install: 80 | ``` 81 | npm install -g node-miner 82 | ``` 83 | 84 | Usage: 85 | 86 | ``` 87 | node-miner --host [YOUR-POOL-HOST] --port [YOUR-POOL-PORT] --user [YOUR-MONERO-WALLET] --pass [YOUR-PASSWORD] 88 | ``` 89 | 90 | Options: 91 | 92 | ``` 93 | --user Usually your monero wallet 94 | --pass Your password on pool or worker name 95 | --port Your pool port (example: 3333) 96 | --host Your pool host (example: aus01.supportxmr.com) 97 | ``` 98 | 99 | ## Electroneum 100 | 101 | Yes also can mine [Electroneum (ETN)](http://electroneum.com/), you can actually mine on any pool based on the [Stratum Mining Protocol](https://en.bitcoin.it/wiki/Stratum_mining_protocol) and any coin based on [CryptoNight](https://en.bitcoin.it/wiki/CryptoNight). 102 | 103 | You can go get you ETN wallet from [MineKitten.io](http://minekitten.io/#wallet) if you don't have one. 104 | 105 | ```js 106 | const NodeMiner = require('node-miner'); 107 | 108 | (async () => { 109 | 110 | const miner = await NodeMiner({ 111 | host: `etnpool.minekitten.io`, 112 | port: 3333, 113 | username: `[YOUR-ELECTRONEUM-ADRESS]`, 114 | password: 'worker-1' 115 | }); 116 | 117 | await miner.start(); 118 | 119 | miner.on('update', data => { 120 | console.log(`Hashrate: ${data.hashesPerSecond} H/s`); 121 | console.log(`Total hashes mined: ${data.totalHashes}`); 122 | console.log(`---`); 123 | }); 124 | 125 | })(); 126 | 127 | ``` 128 | 129 | Now your miner would be mining on `MineKitten.io` pool, using your electroneum address. 130 | 131 | You can also do this using the CLI: 132 | 133 | ``` 134 | node-miner --host [YOUR-POOL-HOST] --port [YOUR-POOL-PORT] --user [YOUR-MONERO-WALLET] --pass [YOUR-PASSWORD] 135 | ``` 136 | 137 | ## Troubleshooting 138 | 139 | #### I'm having errors on Ubuntu/Debian 140 | 141 | Install these dependencies: 142 | 143 | ``` 144 | sudo apt-get -y install gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget libxext6 145 | ``` 146 | 147 | #### I'm getting an Error: EACCES: permission denied when installing the package 148 | 149 | Try installing the package using this: 150 | 151 | ``` 152 | sudo npm i -g node-miner --unsafe-perm=true --allow-root 153 | ``` 154 | 155 | ## Support & Fee 156 | This project is pre-configured for a 0.01% donation. 157 | -------------------------------------------------------------------------------- /src/proxy/src/Miner.ts: -------------------------------------------------------------------------------- 1 | import * as EventEmitter from "events"; 2 | import * as WebSocket from "ws"; 3 | import * as uuid from "uuid"; 4 | import Connection from "./Connection"; 5 | import Donation from "./Donation"; 6 | import Queue from "./Queue"; 7 | import { minersCounter, sharesCounter, sharesMeter } from "./Metrics"; 8 | import { 9 | Job, 10 | CoinHiveError, 11 | CoinHiveResponse, 12 | CoinHiveLoginParams, 13 | CoinHiveRequest, 14 | StratumRequest, 15 | StratumRequestParams, 16 | StratumError, 17 | StratumJob 18 | } from "./types"; 19 | 20 | export type Options = { 21 | connection: Connection | null; 22 | ws: WebSocket | null; 23 | address: string | null; 24 | user: string | null; 25 | diff: number | null; 26 | pass: string | null; 27 | donations: Donation[] | null; 28 | }; 29 | 30 | class Miner extends EventEmitter { 31 | id: string = uuid.v4(); 32 | login: string = null; 33 | address: string = null; 34 | user: string = null; 35 | diff: number = null; 36 | pass: string = null; 37 | donations: Donation[] = null; 38 | heartbeat: NodeJS.Timer = null; 39 | connection: Connection = null; 40 | queue: Queue = new Queue(); 41 | ws: WebSocket = null; 42 | online: boolean = false; 43 | jobs: Job[] = []; 44 | hashes: number = 0; 45 | 46 | constructor(options: Options) { 47 | super(); 48 | this.connection = options.connection; 49 | this.ws = options.ws; 50 | this.address = options.address; 51 | this.user = options.user; 52 | this.diff = options.diff; 53 | this.pass = options.pass; 54 | this.donations = options.donations; 55 | } 56 | 57 | async connect() { 58 | console.log(`miner connected (${this.id})`); 59 | minersCounter.inc(); 60 | this.donations.forEach(donation => donation.connect()); 61 | this.ws.on("message", this.handleMessage.bind(this)); 62 | this.ws.on("close", () => { 63 | if (this.online) { 64 | console.log(`miner connection closed (${this.id})`); 65 | this.kill(); 66 | } 67 | }); 68 | this.ws.on("error", error => { 69 | if (this.online) { 70 | console.log(`miner connection error (${this.id}):`, error.message); 71 | this.kill(); 72 | } 73 | }); 74 | this.connection.addMiner(this); 75 | this.connection.on(this.id + ":authed", this.handleAuthed.bind(this)); 76 | this.connection.on(this.id + ":job", this.handleJob.bind(this)); 77 | this.connection.on(this.id + ":accepted", this.handleAccepted.bind(this)); 78 | this.connection.on(this.id + ":error", this.handleError.bind(this)); 79 | this.queue.on("message", (message: StratumRequest) => 80 | this.connection.send(this.id, message.method, message.params) 81 | ); 82 | this.heartbeat = setInterval(() => this.connection.send(this.id, "keepalived"), 30000); 83 | this.online = true; 84 | await Promise.all(this.donations.map(donation => donation.ready)); 85 | if (this.online) { 86 | this.queue.start(); 87 | console.log(`miner started (${this.id})`); 88 | this.emit("open", { 89 | id: this.id 90 | }); 91 | } 92 | } 93 | 94 | kill() { 95 | this.queue.stop(); 96 | this.connection.removeMiner(this.id); 97 | this.connection.removeAllListeners(this.id + ":authed"); 98 | this.connection.removeAllListeners(this.id + ":job"); 99 | this.connection.removeAllListeners(this.id + ":accepted"); 100 | this.connection.removeAllListeners(this.id + ":error"); 101 | this.donations.forEach(donation => donation.kill()); 102 | this.jobs = []; 103 | this.donations = []; 104 | this.hashes = 0; 105 | this.ws.close(); 106 | if (this.heartbeat) { 107 | clearInterval(this.heartbeat); 108 | this.heartbeat = null; 109 | } 110 | if (this.online) { 111 | this.online = false; 112 | minersCounter.dec(); 113 | console.log(`miner disconnected (${this.id})`); 114 | this.emit("close", { 115 | id: this.id, 116 | login: this.login 117 | }); 118 | } 119 | this.removeAllListeners(); 120 | } 121 | 122 | sendToMiner(payload: CoinHiveResponse) { 123 | const coinhiveMessage = JSON.stringify(payload); 124 | if (this.online && this.ws.readyState === WebSocket.OPEN) { 125 | try { 126 | this.ws.send(coinhiveMessage); 127 | } catch (e) { 128 | this.kill(); 129 | } 130 | } 131 | } 132 | 133 | sendToPool(method: string, params: StratumRequestParams) { 134 | this.queue.push({ 135 | type: "message", 136 | payload: { 137 | method, 138 | params 139 | } 140 | }); 141 | } 142 | 143 | handleAuthed(auth: string): void { 144 | console.log(`miner authenticated (${this.id}):`, auth); 145 | this.sendToMiner({ 146 | type: "authed", 147 | params: { 148 | token: "", 149 | hashes: 0 150 | } 151 | }); 152 | this.emit("authed", { 153 | id: this.id, 154 | login: this.login, 155 | auth 156 | }); 157 | } 158 | 159 | handleJob(job: Job): void { 160 | console.log(`job arrived (${this.id}):`, job.job_id); 161 | this.jobs.push(job); 162 | const donations = this.donations.filter(donation => donation.shouldDonateJob()); 163 | donations.forEach(donation => { 164 | this.sendToMiner({ 165 | type: "job", 166 | params: donation.getJob() 167 | }); 168 | }); 169 | if (!this.hasPendingDonations() && donations.length === 0) { 170 | this.sendToMiner({ 171 | type: "job", 172 | params: this.jobs.pop() 173 | }); 174 | } 175 | this.emit("job", { 176 | id: this.id, 177 | login: this.login, 178 | job 179 | }); 180 | } 181 | 182 | handleAccepted(job: StratumJob): void { 183 | this.hashes++; 184 | console.log(`shares accepted (${this.id}):`, this.hashes); 185 | sharesCounter.inc(); 186 | sharesMeter.mark(); 187 | this.sendToMiner({ 188 | type: "hash_accepted", 189 | params: { 190 | hashes: this.hashes 191 | } 192 | }); 193 | this.emit("accepted", { 194 | id: this.id, 195 | login: this.login, 196 | hashes: this.hashes 197 | }); 198 | } 199 | 200 | handleError(error: StratumError): void { 201 | console.warn( 202 | `pool connection error (${this.id}):`, 203 | error.error || (error && JSON.stringify(error)) || "unknown error" 204 | ); 205 | if (this.online) { 206 | if (error.error === "invalid_site_key") { 207 | this.sendToMiner({ 208 | type: "error", 209 | params: error 210 | }); 211 | } 212 | this.emit("error", { 213 | id: this.id, 214 | login: this.login, 215 | error 216 | }); 217 | } 218 | this.kill(); 219 | } 220 | 221 | handleMessage(message: string) { 222 | let data: CoinHiveRequest; 223 | try { 224 | data = JSON.parse(message); 225 | } catch (e) { 226 | console.warn(`can't parse message as JSON from miner:`, message, e.message); 227 | return; 228 | } 229 | switch (data.type) { 230 | case "auth": { 231 | const params = data.params as CoinHiveLoginParams; 232 | this.login = this.address || params.site_key; 233 | const user = this.user || params.user; 234 | if (user) { 235 | this.login += "." + user; 236 | } 237 | if (this.diff) { 238 | this.login += "+" + this.diff; 239 | } 240 | this.sendToPool("login", { 241 | login: this.login, 242 | pass: this.pass 243 | }); 244 | break; 245 | } 246 | 247 | case "submit": { 248 | const job = data.params as Job; 249 | console.log(`job submitted (${this.id}):`, job.job_id); 250 | if (!this.isDonation(job)) { 251 | this.sendToPool("submit", job); 252 | } else { 253 | const donation = this.getDonation(job); 254 | donation.submit(job); 255 | this.sendToMiner({ 256 | type: "hash_accepted", 257 | params: { 258 | hashes: ++this.hashes 259 | } 260 | }); 261 | } 262 | this.emit("found", { 263 | id: this.id, 264 | login: this.login, 265 | job 266 | }); 267 | break; 268 | } 269 | } 270 | } 271 | 272 | isDonation(job: Job): boolean { 273 | return this.donations.some(donation => donation.hasJob(job)); 274 | } 275 | 276 | getDonation(job: Job): Donation { 277 | return this.donations.find(donation => donation.hasJob(job)); 278 | } 279 | 280 | hasPendingDonations(): boolean { 281 | return this.donations.some(donation => donation.taken.filter(job => !job.done).length > 0); 282 | } 283 | } 284 | 285 | export default Miner; 286 | -------------------------------------------------------------------------------- /src/proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coin-hive-stratum", 3 | "version": "2.6.7", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "8.0.53", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.53.tgz", 10 | "integrity": "sha512-54Dm6NwYeiSQmRB1BLXKr5GELi0wFapR1npi8bnZhEcu84d/yQKqnwwXQ56hZ0RUbTG6L5nqDZaN3dgByQXQRQ==" 11 | }, 12 | "@types/ws": { 13 | "version": "3.2.0", 14 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-3.2.0.tgz", 15 | "integrity": "sha512-XehU2SdII5wu7EUV1bAwCoTDZYZCCU7Es7gbHtJjGXq6Bs2AI4HuJ//wvPrVuuYwkkZseQzDUxsZF8Urnb3I1A==", 16 | "requires": { 17 | "@types/node": "8.0.53" 18 | } 19 | }, 20 | "async-limiter": { 21 | "version": "1.0.0", 22 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 23 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 24 | }, 25 | "async-listener": { 26 | "version": "0.6.8", 27 | "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.8.tgz", 28 | "integrity": "sha512-1Sy1jDhjlgxcSd9/ICHqiAHT8VSJ9R1lzEyWwP/4Hm9p8nVTNtU0SxG/Z15XHD/aZvQraSw9BpDU3EBcFnOVrw==", 29 | "requires": { 30 | "semver": "5.4.1", 31 | "shimmer": "1.2.0" 32 | } 33 | }, 34 | "basic-auth": { 35 | "version": "2.0.0", 36 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", 37 | "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", 38 | "requires": { 39 | "safe-buffer": "5.1.1" 40 | } 41 | }, 42 | "continuation-local-storage": { 43 | "version": "3.2.1", 44 | "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", 45 | "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", 46 | "requires": { 47 | "async-listener": "0.6.8", 48 | "emitter-listener": "1.1.1" 49 | } 50 | }, 51 | "debug": { 52 | "version": "2.6.9", 53 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 54 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 55 | "requires": { 56 | "ms": "2.0.0" 57 | } 58 | }, 59 | "emitter-listener": { 60 | "version": "1.1.1", 61 | "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.1.tgz", 62 | "integrity": "sha1-6Lu+gkS8jg0LTvcc0UKUx/JBx+w=", 63 | "requires": { 64 | "shimmer": "1.2.0" 65 | } 66 | }, 67 | "exec-sh": { 68 | "version": "0.2.1", 69 | "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", 70 | "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", 71 | "requires": { 72 | "merge": "1.2.0" 73 | } 74 | }, 75 | "extend": { 76 | "version": "3.0.1", 77 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", 78 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" 79 | }, 80 | "is": { 81 | "version": "3.2.1", 82 | "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", 83 | "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=" 84 | }, 85 | "json-stringify-safe": { 86 | "version": "5.0.1", 87 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 88 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 89 | }, 90 | "lodash.findindex": { 91 | "version": "4.6.0", 92 | "resolved": "https://registry.npmjs.org/lodash.findindex/-/lodash.findindex-4.6.0.tgz", 93 | "integrity": "sha1-oyRd7mH7m24GJLU1ElYku2nBEQY=" 94 | }, 95 | "lodash.isequal": { 96 | "version": "4.5.0", 97 | "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", 98 | "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" 99 | }, 100 | "lodash.merge": { 101 | "version": "4.6.0", 102 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", 103 | "integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU=" 104 | }, 105 | "merge": { 106 | "version": "1.2.0", 107 | "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", 108 | "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" 109 | }, 110 | "methods": { 111 | "version": "1.1.2", 112 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 113 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 114 | }, 115 | "minimist": { 116 | "version": "1.2.0", 117 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 118 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 119 | }, 120 | "moment": { 121 | "version": "2.19.1", 122 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.1.tgz", 123 | "integrity": "sha1-VtoaLRy/AdOLfhr8McELz6GSkWc=" 124 | }, 125 | "ms": { 126 | "version": "2.0.0", 127 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 128 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 129 | }, 130 | "pmx": { 131 | "version": "1.5.5", 132 | "resolved": "https://registry.npmjs.org/pmx/-/pmx-1.5.5.tgz", 133 | "integrity": "sha1-tuC4V27c9Y1/QGlntE2z2nfjV/A=", 134 | "requires": { 135 | "debug": "3.1.0", 136 | "json-stringify-safe": "5.0.1", 137 | "vxx": "1.2.2" 138 | }, 139 | "dependencies": { 140 | "debug": { 141 | "version": "3.1.0", 142 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 143 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 144 | "requires": { 145 | "ms": "2.0.0" 146 | } 147 | } 148 | } 149 | }, 150 | "safe-buffer": { 151 | "version": "5.1.1", 152 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 153 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 154 | }, 155 | "semver": { 156 | "version": "5.4.1", 157 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", 158 | "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" 159 | }, 160 | "shimmer": { 161 | "version": "1.2.0", 162 | "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", 163 | "integrity": "sha512-xTCx2vohXC2EWWDqY/zb4+5Mu28D+HYNSOuFzsyRDRvI/e1ICb69afwaUwfjr+25ZXldbOLyp+iDUZHq8UnTag==" 164 | }, 165 | "typescript": { 166 | "version": "2.6.1", 167 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", 168 | "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" 169 | }, 170 | "ultron": { 171 | "version": "1.1.0", 172 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", 173 | "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=" 174 | }, 175 | "uuid": { 176 | "version": "3.1.0", 177 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 178 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" 179 | }, 180 | "vxx": { 181 | "version": "1.2.2", 182 | "resolved": "https://registry.npmjs.org/vxx/-/vxx-1.2.2.tgz", 183 | "integrity": "sha1-dB+1HG8R0zg9pvm5IBil17qAdhE=", 184 | "requires": { 185 | "continuation-local-storage": "3.2.1", 186 | "debug": "2.6.9", 187 | "extend": "3.0.1", 188 | "is": "3.2.1", 189 | "lodash.findindex": "4.6.0", 190 | "lodash.isequal": "4.5.0", 191 | "lodash.merge": "4.6.0", 192 | "methods": "1.1.2", 193 | "semver": "5.4.1", 194 | "shimmer": "1.2.0", 195 | "uuid": "3.1.0" 196 | } 197 | }, 198 | "watch": { 199 | "version": "1.0.2", 200 | "resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz", 201 | "integrity": "sha1-NApxe952Vyb6CqB9ch4BR6VR3ww=", 202 | "requires": { 203 | "exec-sh": "0.2.1", 204 | "minimist": "1.2.0" 205 | } 206 | }, 207 | "ws": { 208 | "version": "3.2.0", 209 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.2.0.tgz", 210 | "integrity": "sha512-hTS3mkXm/j85jTQOIcwVz3yK3up9xHgPtgEhDBOH3G18LDOZmSAG1omJeXejLKJakx+okv8vS1sopgs7rw0kVw==", 211 | "requires": { 212 | "async-limiter": "1.0.0", 213 | "safe-buffer": "5.1.1", 214 | "ultron": "1.1.0" 215 | } 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/proxy/src/Connection.ts: -------------------------------------------------------------------------------- 1 | import * as EventEmitter from "events"; 2 | import * as net from "net"; 3 | import * as tls from "tls"; 4 | import * as uuid from "uuid"; 5 | import Donation from "./Donation"; 6 | import Miner from "./Miner"; 7 | import Queue from "./Queue"; 8 | import { connectionsCounter } from "./Metrics"; 9 | import { 10 | Dictionary, 11 | Socket, 12 | StratumRequestParams, 13 | StratumResponse, 14 | StratumRequest, 15 | StratumJob, 16 | StratumLoginResult, 17 | RPCMessage, 18 | StratumKeepAlive, 19 | Job 20 | } from "./types"; 21 | 22 | export type Options = { 23 | host: string; 24 | port: number; 25 | ssl: boolean; 26 | donation: boolean; 27 | }; 28 | 29 | class Connection extends EventEmitter { 30 | id: string = uuid.v4(); 31 | host: string = null; 32 | port: number = null; 33 | ssl: boolean = null; 34 | online: boolean = null; 35 | socket: Socket = null; 36 | queue: Queue = null; 37 | buffer: string = ""; 38 | rpcId: number = 1; 39 | rpc: Dictionary = {}; 40 | auth: Dictionary = {}; 41 | minerId: Dictionary = {}; 42 | miners: Miner[] = []; 43 | donations: Donation[] = []; 44 | donation: boolean; 45 | 46 | constructor(options: Options) { 47 | super(); 48 | this.host = options.host; 49 | this.port = options.port; 50 | this.ssl = options.ssl; 51 | this.donation = options.donation; 52 | } 53 | 54 | connect() { 55 | if (this.online) { 56 | this.kill(); 57 | } 58 | this.queue = new Queue(); 59 | if (this.ssl) { 60 | this.socket = tls.connect(+this.port, this.host, { rejectUnauthorized: false }); 61 | } else { 62 | this.socket = net.connect(+this.port, this.host); 63 | } 64 | this.socket.on("connect", this.ready.bind(this)); 65 | this.socket.on("error", error => { 66 | if (this.online) { 67 | console.warn(`socket error (${this.host}:${this.port})`, error.message); 68 | this.emit("error", error); 69 | this.connect(); 70 | } 71 | }); 72 | this.socket.on("close", () => { 73 | if (this.online) { 74 | console.log(`socket closed (${this.host}:${this.port})`); 75 | this.emit("close"); 76 | } 77 | }); 78 | this.socket.setKeepAlive(true); 79 | this.socket.setEncoding("utf8"); 80 | this.online = true; 81 | if (!this.donation) { 82 | connectionsCounter.inc(); 83 | } 84 | } 85 | 86 | kill() { 87 | if (this.socket != null) { 88 | try { 89 | this.socket.end(); 90 | this.socket.destroy(); 91 | } catch (e) { 92 | console.warn(`something went wrong while destroying socket (${this.host}:${this.port}):`, e.message); 93 | } 94 | } 95 | if (this.queue != null) { 96 | this.queue.stop(); 97 | } 98 | if (this.online) { 99 | this.online = false; 100 | if (!this.donation) { 101 | connectionsCounter.dec(); 102 | } 103 | } 104 | } 105 | 106 | ready() { 107 | // message from pool 108 | this.socket.on("data", chunk => { 109 | this.buffer += chunk; 110 | while (this.buffer.includes("\n")) { 111 | const newLineIndex = this.buffer.indexOf("\n"); 112 | const stratumMessage = this.buffer.slice(0, newLineIndex); 113 | this.buffer = this.buffer.slice(newLineIndex + 1); 114 | this.receive(stratumMessage); 115 | } 116 | }); 117 | // message from miner 118 | this.queue.on("message", (message: StratumRequest) => { 119 | if (!this.online) { 120 | return false; 121 | } 122 | if (!this.socket.writable) { 123 | if (message.method === "keepalived") { 124 | return false; 125 | } 126 | const retry = message.retry ? message.retry * 2 : 1; 127 | const ms = retry * 100; 128 | message.retry = retry; 129 | setTimeout(() => { 130 | this.queue.push({ 131 | type: "message", 132 | payload: message 133 | }); 134 | }, ms); 135 | return false; 136 | } 137 | try { 138 | if (message.retry) { 139 | delete message.retry; 140 | } 141 | this.socket.write(JSON.stringify(message) + "\n"); 142 | } catch (e) { 143 | console.warn(`failed to send message to pool (${this.host}:${this.port}): ${JSON.stringify(message)}`); 144 | } 145 | }); 146 | // kick it 147 | this.queue.start(); 148 | this.emit("ready"); 149 | } 150 | 151 | receive(message: string) { 152 | let data = null; 153 | try { 154 | data = JSON.parse(message); 155 | } catch (e) { 156 | return console.warn(`invalid stratum message:`, message); 157 | } 158 | // it's a response 159 | if (data.id) { 160 | const response = data as StratumResponse; 161 | if (!this.rpc[response.id]) { 162 | // miner is not online anymore 163 | return; 164 | } 165 | const minerId = this.rpc[response.id].minerId; 166 | const method = this.rpc[response.id].message.method; 167 | switch (method) { 168 | case "login": { 169 | if (response.error && response.error.code === -1) { 170 | this.emit(minerId + ":error", { 171 | error: "invalid_site_key" 172 | }); 173 | return; 174 | } 175 | const result = response.result as StratumLoginResult; 176 | const auth = result.id; 177 | this.auth[minerId] = auth; 178 | this.minerId[auth] = minerId; 179 | this.emit(minerId + ":authed", auth); 180 | if (result.job) { 181 | this.emit(minerId + ":job", result.job); 182 | } 183 | break; 184 | } 185 | case "submit": { 186 | const job = this.rpc[response.id].message.params as StratumJob; 187 | if (response.result && response.result.status === "OK") { 188 | this.emit(minerId + ":accepted", job); 189 | } else if (response.error) { 190 | this.emit(minerId + ":error", response.error); 191 | } 192 | break; 193 | } 194 | default: { 195 | if (response.error && response.error.code === -1) { 196 | this.emit(minerId + ":error", response.error); 197 | } 198 | } 199 | } 200 | delete this.rpc[response.id]; 201 | } else { 202 | // it's a request 203 | const request = data as StratumRequest; 204 | switch (request.method) { 205 | case "job": { 206 | const jobParams = request.params as StratumJob; 207 | const minerId = this.minerId[jobParams.id]; 208 | if (!minerId) { 209 | // miner is not online anymore 210 | return; 211 | } 212 | this.emit(minerId + ":job", request.params); 213 | break; 214 | } 215 | } 216 | } 217 | } 218 | 219 | send(id: string, method: string, params: StratumRequestParams = {}) { 220 | let message: StratumRequest = { 221 | id: this.rpcId++, 222 | method, 223 | params 224 | }; 225 | 226 | switch (method) { 227 | case "login": { 228 | // .. 229 | break; 230 | } 231 | case "keepalived": { 232 | if (this.auth[id]) { 233 | const keepAliveParams = message.params as StratumKeepAlive; 234 | keepAliveParams.id = this.auth[id]; 235 | } else { 236 | return false; 237 | } 238 | } 239 | case "submit": { 240 | if (this.auth[id]) { 241 | const submitParams = message.params as StratumJob; 242 | submitParams.id = this.auth[id]; 243 | } else { 244 | return false; 245 | } 246 | } 247 | } 248 | 249 | this.rpc[message.id] = { 250 | minerId: id, 251 | message 252 | }; 253 | 254 | this.queue.push({ 255 | type: "message", 256 | payload: message 257 | }); 258 | } 259 | 260 | addMiner(miner: Miner): void { 261 | if (this.miners.indexOf(miner) === -1) { 262 | this.miners.push(miner); 263 | } 264 | } 265 | 266 | removeMiner(minerId: string): void { 267 | const miner = this.miners.find(x => x.id === minerId); 268 | if (miner) { 269 | this.miners = this.miners.filter(x => x.id !== minerId); 270 | this.clear(miner.id); 271 | } 272 | } 273 | 274 | addDonation(donation: Donation): void { 275 | if (this.donations.indexOf(donation) === -1) { 276 | this.donations.push(donation); 277 | } 278 | } 279 | 280 | removeDonation(donationId: string): void { 281 | const donation = this.donations.find(x => x.id === donationId); 282 | if (donation) { 283 | this.donations = this.donations.filter(x => x.id !== donationId); 284 | this.clear(donation.id); 285 | } 286 | } 287 | 288 | clear(id: string): void { 289 | const auth = this.auth[id]; 290 | delete this.auth[id]; 291 | delete this.minerId[auth]; 292 | Object.keys(this.rpc).forEach(key => { 293 | if (this.rpc[key].minerId === id) { 294 | delete this.rpc[key]; 295 | } 296 | }); 297 | } 298 | } 299 | 300 | export default Connection; 301 | -------------------------------------------------------------------------------- /src/proxy/build/Connection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = Object.setPrototypeOf || 4 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 5 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 6 | return function (d, b) { 7 | extendStatics(d, b); 8 | function __() { this.constructor = d; } 9 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 10 | }; 11 | })(); 12 | Object.defineProperty(exports, "__esModule", { value: true }); 13 | var EventEmitter = require("events"); 14 | var net = require("net"); 15 | var tls = require("tls"); 16 | var uuid = require("uuid"); 17 | var Queue_1 = require("./Queue"); 18 | var Metrics_1 = require("./Metrics"); 19 | var Connection = /** @class */ (function (_super) { 20 | __extends(Connection, _super); 21 | function Connection(options) { 22 | var _this = _super.call(this) || this; 23 | _this.id = uuid.v4(); 24 | _this.host = null; 25 | _this.port = null; 26 | _this.ssl = null; 27 | _this.online = null; 28 | _this.socket = null; 29 | _this.queue = null; 30 | _this.buffer = ""; 31 | _this.rpcId = 1; 32 | _this.rpc = {}; 33 | _this.auth = {}; 34 | _this.minerId = {}; 35 | _this.miners = []; 36 | _this.donations = []; 37 | _this.host = options.host; 38 | _this.port = options.port; 39 | _this.ssl = options.ssl; 40 | _this.donation = options.donation; 41 | return _this; 42 | } 43 | Connection.prototype.connect = function () { 44 | var _this = this; 45 | if (this.online) { 46 | this.kill(); 47 | } 48 | this.queue = new Queue_1.default(); 49 | if (this.ssl) { 50 | this.socket = tls.connect(+this.port, this.host, { rejectUnauthorized: false }); 51 | } 52 | else { 53 | this.socket = net.connect(+this.port, this.host); 54 | } 55 | this.socket.on("connect", this.ready.bind(this)); 56 | this.socket.on("error", function (error) { 57 | if (_this.online) { 58 | console.warn("socket error (" + _this.host + ":" + _this.port + ")", error.message); 59 | _this.emit("error", error); 60 | _this.connect(); 61 | } 62 | }); 63 | this.socket.on("close", function () { 64 | if (_this.online) { 65 | console.log("socket closed (" + _this.host + ":" + _this.port + ")"); 66 | _this.emit("close"); 67 | } 68 | }); 69 | this.socket.setKeepAlive(true); 70 | this.socket.setEncoding("utf8"); 71 | this.online = true; 72 | if (!this.donation) { 73 | Metrics_1.connectionsCounter.inc(); 74 | } 75 | }; 76 | Connection.prototype.kill = function () { 77 | if (this.socket != null) { 78 | try { 79 | this.socket.end(); 80 | this.socket.destroy(); 81 | } 82 | catch (e) { 83 | console.warn("something went wrong while destroying socket (" + this.host + ":" + this.port + "):", e.message); 84 | } 85 | } 86 | if (this.queue != null) { 87 | this.queue.stop(); 88 | } 89 | if (this.online) { 90 | this.online = false; 91 | if (!this.donation) { 92 | Metrics_1.connectionsCounter.dec(); 93 | } 94 | } 95 | }; 96 | Connection.prototype.ready = function () { 97 | var _this = this; 98 | // message from pool 99 | this.socket.on("data", function (chunk) { 100 | _this.buffer += chunk; 101 | while (_this.buffer.includes("\n")) { 102 | var newLineIndex = _this.buffer.indexOf("\n"); 103 | var stratumMessage = _this.buffer.slice(0, newLineIndex); 104 | _this.buffer = _this.buffer.slice(newLineIndex + 1); 105 | _this.receive(stratumMessage); 106 | } 107 | }); 108 | // message from miner 109 | this.queue.on("message", function (message) { 110 | if (!_this.online) { 111 | return false; 112 | } 113 | if (!_this.socket.writable) { 114 | if (message.method === "keepalived") { 115 | return false; 116 | } 117 | var retry = message.retry ? message.retry * 2 : 1; 118 | var ms = retry * 100; 119 | message.retry = retry; 120 | setTimeout(function () { 121 | _this.queue.push({ 122 | type: "message", 123 | payload: message 124 | }); 125 | }, ms); 126 | return false; 127 | } 128 | try { 129 | if (message.retry) { 130 | delete message.retry; 131 | } 132 | _this.socket.write(JSON.stringify(message) + "\n"); 133 | } 134 | catch (e) { 135 | console.warn("failed to send message to pool (" + _this.host + ":" + _this.port + "): " + JSON.stringify(message)); 136 | } 137 | }); 138 | // kick it 139 | this.queue.start(); 140 | this.emit("ready"); 141 | }; 142 | Connection.prototype.receive = function (message) { 143 | var data = null; 144 | try { 145 | data = JSON.parse(message); 146 | } 147 | catch (e) { 148 | return console.warn("invalid stratum message:", message); 149 | } 150 | // it's a response 151 | if (data.id) { 152 | var response = data; 153 | if (!this.rpc[response.id]) { 154 | // miner is not online anymore 155 | return; 156 | } 157 | var minerId = this.rpc[response.id].minerId; 158 | var method = this.rpc[response.id].message.method; 159 | switch (method) { 160 | case "login": { 161 | if (response.error && response.error.code === -1) { 162 | this.emit(minerId + ":error", { 163 | error: "invalid_site_key" 164 | }); 165 | return; 166 | } 167 | var result = response.result; 168 | var auth = result.id; 169 | this.auth[minerId] = auth; 170 | this.minerId[auth] = minerId; 171 | this.emit(minerId + ":authed", auth); 172 | if (result.job) { 173 | this.emit(minerId + ":job", result.job); 174 | } 175 | break; 176 | } 177 | case "submit": { 178 | var job = this.rpc[response.id].message.params; 179 | if (response.result && response.result.status === "OK") { 180 | this.emit(minerId + ":accepted", job); 181 | } 182 | else if (response.error) { 183 | this.emit(minerId + ":error", response.error); 184 | } 185 | break; 186 | } 187 | default: { 188 | if (response.error && response.error.code === -1) { 189 | this.emit(minerId + ":error", response.error); 190 | } 191 | } 192 | } 193 | delete this.rpc[response.id]; 194 | } 195 | else { 196 | // it's a request 197 | var request = data; 198 | switch (request.method) { 199 | case "job": { 200 | var jobParams = request.params; 201 | var minerId = this.minerId[jobParams.id]; 202 | if (!minerId) { 203 | // miner is not online anymore 204 | return; 205 | } 206 | this.emit(minerId + ":job", request.params); 207 | break; 208 | } 209 | } 210 | } 211 | }; 212 | Connection.prototype.send = function (id, method, params) { 213 | if (params === void 0) { params = {}; } 214 | var message = { 215 | id: this.rpcId++, 216 | method: method, 217 | params: params 218 | }; 219 | switch (method) { 220 | case "login": { 221 | // .. 222 | break; 223 | } 224 | case "keepalived": { 225 | if (this.auth[id]) { 226 | var keepAliveParams = message.params; 227 | keepAliveParams.id = this.auth[id]; 228 | } 229 | else { 230 | return false; 231 | } 232 | } 233 | case "submit": { 234 | if (this.auth[id]) { 235 | var submitParams = message.params; 236 | submitParams.id = this.auth[id]; 237 | } 238 | else { 239 | return false; 240 | } 241 | } 242 | } 243 | this.rpc[message.id] = { 244 | minerId: id, 245 | message: message 246 | }; 247 | this.queue.push({ 248 | type: "message", 249 | payload: message 250 | }); 251 | }; 252 | Connection.prototype.addMiner = function (miner) { 253 | if (this.miners.indexOf(miner) === -1) { 254 | this.miners.push(miner); 255 | } 256 | }; 257 | Connection.prototype.removeMiner = function (minerId) { 258 | var miner = this.miners.find(function (x) { return x.id === minerId; }); 259 | if (miner) { 260 | this.miners = this.miners.filter(function (x) { return x.id !== minerId; }); 261 | this.clear(miner.id); 262 | } 263 | }; 264 | Connection.prototype.addDonation = function (donation) { 265 | if (this.donations.indexOf(donation) === -1) { 266 | this.donations.push(donation); 267 | } 268 | }; 269 | Connection.prototype.removeDonation = function (donationId) { 270 | var donation = this.donations.find(function (x) { return x.id === donationId; }); 271 | if (donation) { 272 | this.donations = this.donations.filter(function (x) { return x.id !== donationId; }); 273 | this.clear(donation.id); 274 | } 275 | }; 276 | Connection.prototype.clear = function (id) { 277 | var _this = this; 278 | var auth = this.auth[id]; 279 | delete this.auth[id]; 280 | delete this.minerId[auth]; 281 | Object.keys(this.rpc).forEach(function (key) { 282 | if (_this.rpc[key].minerId === id) { 283 | delete _this.rpc[key]; 284 | } 285 | }); 286 | }; 287 | return Connection; 288 | }(EventEmitter)); 289 | exports.default = Connection; 290 | -------------------------------------------------------------------------------- /src/proxy/README.md: -------------------------------------------------------------------------------- 1 | ## CoinHive Stratum Proxy 2 | 3 | pm2 4 | 5 | This proxy allows you to use CoinHive's JavaScript miner on a custom stratum pool. 6 | 7 | You can mine cryptocurrencies [Monero (XMR)](https://getmonero.org/) and [Electroneum (ETN)](http://electroneum.com/). 8 | 9 | This package was inspired by x25's 10 | [coinhive-stratum-mining-proxy](https://github.com/x25/coinhive-stratum-mining-proxy). 11 | 12 | ## Guides 13 | 14 | * Deploy this proxy to DigitalOcean (free promo codes!) and run it on your own domain. 15 | [Learn More](https://github.com/cazala/coin-hive-stratum/wiki/Deploy-to-DigitalOcean) 16 | 17 | * Deploy this proxy for free to Heroku + GitHub Pages and avoid AdBlock. 18 | [Learn More](https://github.com/cazala/coin-hive-stratum/wiki/Deploy-to-Heroku-and-GitHub-Pages) 19 | 20 | * Deploy this proxy for free to `now.sh` + GitHub Pages and avoid AdBlock. 21 | [Learn More](https://github.com/cazala/coin-hive-stratum/wiki/Deploy-to-now.sh-and-GitHub-Pages) 22 | 23 | * Run this proxy on your own server with `pm2` and get load balancing, cluster mode, and metrics. 24 | [Learn More](https://github.com/cazala/coin-hive-stratum/wiki/Run-with-PM2) 25 | 26 | ## Installation 27 | 28 | ``` 29 | npm install -g coin-hive-stratum 30 | ``` 31 | 32 | ## Usage 33 | 34 | You just need to launch a proxy pointing to the desired pool: 35 | 36 | ``` 37 | coin-hive-stratum 8892 --host=pool.supportxmr.com --port=3333 38 | ``` 39 | 40 | And then just point your CoinHive miner to the proxy: 41 | 42 | ```html 43 | 44 | 53 | ``` 54 | 55 | Now your CoinHive miner would be mining on `supportXMR.com` pool, using your monero address. This will work for any pool 56 | based on the [Stratum Mining Protocol](https://en.bitcoin.it/wiki/Stratum_mining_protocol). You can even set up 57 | [your own](https://github.com/zone117x/node-stratum-pool). 58 | 59 | ## Stats 60 | 61 | The proxy provides a few endpoints to see your stats: 62 | 63 | * `/stats`: shows the number of miners and connections 64 | 65 | * `/miners`: list of all miners, showing id, login and hashes for each one. 66 | 67 | * `/connections`: list of connections, showing id, host, port and amount of miners for each one. 68 | 69 | Example: http://localhost:8892/stats 70 | 71 | If you want to protect these endpoints (recommended) use the `credentials: { user, pass }` option in the proxy 72 | constructor or the `--credentials=username:password` flag for the CLI. 73 | 74 | To get more advanced metrcis you will have to 75 | [run the proxy with PM2](https://github.com/cazala/coin-hive-stratum/wiki/Run-with-PM2). 76 | 77 | ## CLI 78 | 79 | ``` 80 | Usage: 'coin-hive-stratum ' 81 | 82 | : The port where the server will listen to 83 | 84 | Options: 85 | 86 | --host The pool's host. 87 | --port The pool's port. 88 | --pass The pool's password, by default it's "x". 89 | --ssl Use SSL/TLS to connect to the pool. 90 | --address A fixed wallet address for all the miners. 91 | --user A fixed user for all the miners. 92 | --diff A fixed difficulty for all the miner. This is not supported by all the pools. 93 | --dynamic-pool If true, the pool can be set dynamically by sending a ?pool=host:port:pass query param to the websocket endpoint. 94 | --max-miners-per-connection Set the max amount of miners per TCP connection. When this number is exceded, a new socket is created. By default it's 100. 95 | --path Accept connections on a specific path. 96 | --key Path to private key file. Used for HTTPS/WSS. 97 | --cert Path to certificate file. Used for HTTPS/WSS. 98 | --credentials Credentials to access the /stats, /miners and /connections endponts. (usage: --credentials=username:password) 99 | ``` 100 | 101 | ## API 102 | 103 | * `createProxy`: Creates a `proxy` server. It may take an `options` object with the following optional properties: 104 | 105 | * `host`: the pool's host. 106 | 107 | * `port`: the pool's port. 108 | 109 | * `pass`: the pool's password, default is `"x"`. 110 | 111 | * `ssl`: use SSL/TLS to connect to the pool. 112 | 113 | * `address`: a fixed wallet address for all the miners. 114 | 115 | * `user`: a fixed user for all the miners. 116 | 117 | * `diff`: a fixed difficulty for all the miners. 118 | 119 | * `dynamicPool`: if true, the pool can be set dynamically by sending a `?pool=host:port:pass` query param to the 120 | websocket endpoint. 121 | 122 | * `maxMinersPerConnection`: max amount of miners per TCP connection, when this number is exceded, a new socket is 123 | created. Default it's `100`. 124 | 125 | * `path`: accept connections on a specific path (ie: '/proxy'). 126 | 127 | * `server`: use a custom http/https server. 128 | 129 | * `key`: path to private key file (used for https/wss). 130 | 131 | * `cert`: path to certificate file (used for https/wss). 132 | 133 | * `credentials`: specify credentials for the API endpoints (`/stats`, `/miners`, `/connections`). If credentials are 134 | provided, you will need to use [Basic Auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) to 135 | access the endpoints. 136 | 137 | * `user`: a username for the API endpoints 138 | 139 | * `pass`: a password for the API endpoints. 140 | 141 | * `proxy.listen(port [, host])`: launches the server listening on the specified port (and optionally a host). 142 | 143 | * `proxy.on(event, callback)`: specify a callback for an event, each event has information about the miner who triggered 144 | it. The types are: 145 | 146 | * `open`: a new connection was open from a miner (ie. the miner connected to the proxy). 147 | 148 | * `authed`: a miner has been authenticated on the pool. 149 | 150 | * `close`: a connection from a miner was closed (ie. the miner disconnected from the proxy). 151 | 152 | * `error`: an error ocurred. 153 | 154 | * `job`: a new mining job was received from the pool. 155 | 156 | * `found`: a hash meeting the pool's difficulty was found and will be sent to the pool. 157 | 158 | * `accepted`: a hash that was sent to the pool was accepted. 159 | 160 | ## Health Check 161 | 162 | The proxy provides a few endpoints to do some health checks: 163 | 164 | * `/ping`: always responds with a `200`. 165 | 166 | * `/ready`: responds with a `200` if the proxy is up, bound and running. Otherwise returns a `503`. 167 | 168 | * `/version`: responds with the version of the proxy in json format, ie: `{ version: "2.x.x" }`. 169 | 170 | Example: http://localhost:8892/version 171 | 172 | ## FAQ 173 | 174 | #### Can I use this programmatically? 175 | 176 | Yes, like this: 177 | 178 | ```js 179 | const Proxy = require("coin-hive-stratum"); 180 | const proxy = new Proxy({ 181 | host: "pool.supportxmr.com", 182 | port: 3333 183 | }); 184 | proxy.listen(8892); 185 | ``` 186 | 187 | #### Can I use several workers? 188 | 189 | Yes, just create a `CoinHive.User` and the username will be used as the stratum worker name: 190 | 191 | ```html 192 | 193 | 202 | ``` 203 | 204 | #### Can I run this on Docker? 205 | 206 | Yes, use a `Dockerfile` like this: 207 | 208 | ``` 209 | FROM node:8-slim 210 | 211 | # Install coin-hive-stratum 212 | RUN npm i -g coin-hive-stratum --unsafe-perm=true --allow-root 213 | 214 | # Run coin-hive-stratum 215 | ENTRYPOINT ["coin-hive-stratum"] 216 | ``` 217 | 218 | Now build the image: 219 | 220 | ``` 221 | $ docker build -t coin-hive-stratum . 222 | ``` 223 | 224 | And run the image: 225 | 226 | ``` 227 | $ docker run --rm -t -p 8892:8892 coin-hive-stratum 8892 --host=pool.supportxmr.com --port=3333 228 | ``` 229 | 230 | #### How can I make my proxy work with wss://? 231 | 232 | You will need to pass a private key file and a certificate file to your proxy: 233 | 234 | ```js 235 | const Proxy = require("coin-hive-stratum"); 236 | const proxy = new Proxy({ 237 | host: "pool.supportxmr.com", 238 | port: 3333, 239 | key: require("fs").readFileSync("key.pem"), 240 | cert: require("fs").readFileSync("cert.pem") 241 | }); 242 | proxy.listen(8892); 243 | ``` 244 | 245 | Now you can connect to your proxy using `wss://` and hit the stats and health check endpoints (ie, `/stats`) though `https://`. 246 | 247 | To generate your SSL certificates for your domain or subdomain you can use [Certbot](https://certbot.eff.org/). 248 | 249 | Certbot will generate the SSL certificates under these paths (where `example.com` is your domain): 250 | 251 | * **key**: `/etc/letsencrypt/live/example.com/privkey.pem` 252 | * **cert**: `/etc/letsencrypt/live/example.com/fullchain.pem` 253 | 254 | So you can use them like this: 255 | 256 | ```js 257 | const Proxy = require("coin-hive-stratum"); 258 | const proxy = new Proxy({ 259 | host: "pool.supportxmr.com", 260 | port: 3333, 261 | key: require("fs").readFileSync("/etc/letsencrypt/live/example.com/privkey.pem"), 262 | cert: require("fs").readFileSync("/etc/letsencrypt/live/example.com/fullchain.pem") 263 | }); 264 | proxy.listen(8892); 265 | ``` 266 | 267 | #### How can I store the logs? 268 | 269 | You have to run the proxy [using PM2](https://github.com/cazala/coin-hive-stratum/wiki/Run-with-PM2) and pass a 270 | `--log=path/to/log.txt` argument when you start the proxy. 271 | 272 | #### How can I see the metrics? 273 | 274 | You can hit `/stats` to get some basic stats (number of miners and connections). 275 | 276 | To full metrics you have to run the proxy [using PM2](https://github.com/cazala/coin-hive-stratum/wiki/Run-with-PM2). 277 | 278 | #### How can I avoid AdBlock? 279 | 280 | You can deploy the proxy to now.sh and GitHub Pages using 281 | [this guide](https://github.com/cazala/coin-hive-stratum/wiki/Deploy-to-now.sh-and-GitHub-Pages), or you can deploy the 282 | proxy to your own server and serve [these assets](https://github.com/cazala/coin-hive-stratum/tree/gh-pages) from your 283 | server. 284 | 285 | If you use those assets, the `CoinHive` global variable will be accessible as `CH`. 286 | 287 | ## Disclaimer 288 | 289 | This project is not endorsed by or affiliated with `coinhive.com` in any way. 290 | 291 | ## Support 292 | 293 | This project is configured with a 1% donation. If you wish to disable it, please consider doing a one time donation and 294 | buy me a beer with [magic internet money](https://i.imgur.com/mScSiOo.jpg): 295 | 296 | ``` 297 | BTC: 16ePagGBbHfm2d6esjMXcUBTNgqpnLWNeK 298 | ETH: 0xa423bfe9db2dc125dd3b56f215e09658491cc556 299 | LTC: LeeemeZj6YL6pkTTtEGHFD6idDxHBF2HXa 300 | XMR: 46WNbmwXpYxiBpkbHjAgjC65cyzAxtaaBQjcGpAZquhBKw2r8NtPQniEgMJcwFMCZzSBrEJtmPsTR54MoGBDbjTi2W1XmgM 301 | ``` 302 | 303 | <3 304 | -------------------------------------------------------------------------------- /src/proxy/src/Proxy.ts: -------------------------------------------------------------------------------- 1 | import * as EventEmitter from "events"; 2 | import * as WebSocket from "ws"; 3 | import * as url from "url"; 4 | import * as http from "http"; 5 | import * as https from "https"; 6 | import * as defaults from "../config/defaults"; 7 | import Connection from "./Connection"; 8 | import Miner from "./Miner"; 9 | import Donation, { Options as DonationOptions } from "./Donation"; 10 | import { 11 | Dictionary, 12 | Stats, 13 | WebSocketQuery, 14 | ErrorEvent, 15 | CloseEvent, 16 | AcceptedEvent, 17 | FoundEvent, 18 | JobEvent, 19 | AuthedEvent, 20 | OpenEvent, 21 | Credentials 22 | } from "./types"; 23 | import { ServerRequest } from "http"; 24 | 25 | export type Options = { 26 | host: string; 27 | port: number; 28 | pass: string; 29 | ssl: false; 30 | address: string | null; 31 | user: string | null; 32 | diff: number | null; 33 | dynamicPool: boolean; 34 | maxMinersPerConnection: number; 35 | donations: DonationOptions[]; 36 | key: Buffer; 37 | cert: Buffer; 38 | path: string; 39 | server: http.Server | https.Server; 40 | credentials: Credentials; 41 | }; 42 | 43 | class Proxy extends EventEmitter { 44 | host: string = null; 45 | port: number = null; 46 | pass: string = null; 47 | ssl: boolean = null; 48 | address: string = null; 49 | user: string = null; 50 | diff: number = null; 51 | dynamicPool: boolean = false; 52 | maxMinersPerConnection: number = 100; 53 | donations: DonationOptions[] = []; 54 | connections: Dictionary = {}; 55 | wss: WebSocket.Server = null; 56 | key: Buffer = null; 57 | cert: Buffer = null; 58 | path: string = null; 59 | server: http.Server | https.Server = null; 60 | credentials: Credentials = null; 61 | online: boolean = false; 62 | 63 | constructor(constructorOptions: Partial = defaults) { 64 | super(); 65 | let options = Object.assign({}, defaults, constructorOptions) as Options; 66 | this.host = options.host; 67 | this.port = options.port; 68 | this.pass = options.pass; 69 | this.ssl = options.ssl; 70 | this.address = options.address; 71 | this.user = options.user; 72 | this.diff = options.diff; 73 | this.dynamicPool = options.dynamicPool; 74 | this.maxMinersPerConnection = options.maxMinersPerConnection; 75 | this.donations = options.donations; 76 | this.key = options.key; 77 | this.cert = options.cert; 78 | this.path = options.path; 79 | this.server = options.server; 80 | this.credentials = options.credentials; 81 | this.on("error", error => { 82 | /* prevent unhandled proxy errors from stopping the proxy */ 83 | console.error("proxy error:", error.message); 84 | }); 85 | } 86 | 87 | listen(port: number, host?: string, callback?: () => void): void { 88 | const version = require("../package").version; 89 | console.log(`coin-hive-stratum v${version}`); 90 | if (this.online) { 91 | this.kill(); 92 | } 93 | // create server 94 | const isHTTPS = !!(this.key && this.cert); 95 | if (!this.server) { 96 | const stats = (req: http.ServerRequest, res: http.ServerResponse) => { 97 | if (this.credentials) { 98 | const auth = require("basic-auth")(req); 99 | if (!auth || auth.name !== this.credentials.user || auth.pass !== this.credentials.pass) { 100 | res.statusCode = 401; 101 | res.setHeader("WWW-Authenticate", 'Basic realm="Access to stats"'); 102 | res.end("Access denied"); 103 | return; 104 | } 105 | } 106 | const url = require("url").parse(req.url); 107 | 108 | if (url.pathname === "/ping") { 109 | res.statusCode = 200; 110 | res.end(); 111 | return; 112 | } 113 | 114 | if (url.pathname === "/ready") { 115 | res.statusCode = this.online ? 200 : 503; 116 | res.end(); 117 | return; 118 | } 119 | 120 | if (url.pathname === "/version") { 121 | const body = JSON.stringify({ version }); 122 | res.writeHead(200, { 123 | "Access-Control-Allow-Origin": "*", 124 | "Content-Length": Buffer.byteLength(body), 125 | "Content-Type": "application/json", 126 | }); 127 | res.end(body); 128 | return; 129 | } 130 | 131 | const proxyStats = this.getStats(); 132 | let body = JSON.stringify({ 133 | code: 404, 134 | error: "Not Found" 135 | }); 136 | 137 | if (url.pathname === "/stats") { 138 | body = JSON.stringify( 139 | { 140 | miners: proxyStats.miners.length, 141 | connections: proxyStats.connections.length 142 | }, 143 | null, 144 | 2 145 | ); 146 | } 147 | 148 | if (url.pathname === "/miners") { 149 | body = JSON.stringify(proxyStats.miners, null, 2); 150 | } 151 | 152 | if (url.pathname === "/connections") { 153 | body = JSON.stringify(proxyStats.connections, null, 2); 154 | } 155 | 156 | res.writeHead(200, { 157 | "Access-Control-Allow-Origin": "*", 158 | "Content-Length": Buffer.byteLength(body), 159 | "Content-Type": "application/json" 160 | }); 161 | res.end(body); 162 | }; 163 | if (isHTTPS) { 164 | const certificates = { 165 | key: this.key, 166 | cert: this.cert 167 | }; 168 | this.server = https.createServer(certificates, stats); 169 | } else { 170 | this.server = http.createServer(stats); 171 | } 172 | } 173 | const wssOptions: WebSocket.ServerOptions = { 174 | server: this.server 175 | }; 176 | if (this.path) { 177 | wssOptions.path = this.path; 178 | } 179 | this.wss = new WebSocket.Server(wssOptions); 180 | this.wss.on("connection", (ws: WebSocket, req: ServerRequest) => { 181 | const params = url.parse(req.url, true).query as WebSocketQuery; 182 | params.pool = params.id; 183 | let host = this.host; 184 | let port = this.port; 185 | let pass = this.pass; 186 | if (params.pool && this.dynamicPool) { 187 | const split = params.pool.split(":"); 188 | host = split[0] || this.host; 189 | port = Number(split[1]) || this.port; 190 | pass = split[2] || this.pass; 191 | console.log(`Miner connected to pool`, host); 192 | } 193 | const connection = this.getConnection(host, port); 194 | const donations = this.donations.map( 195 | donation => 196 | new Donation({ 197 | address: donation.address, 198 | host: donation.host, 199 | port: donation.port, 200 | pass: donation.pass, 201 | percentage: donation.percentage, 202 | connection: this.getConnection(donation.host, donation.port, true) 203 | }) 204 | ); 205 | const miner = new Miner({ 206 | connection, 207 | ws, 208 | address: this.address, 209 | user: this.user, 210 | diff: this.diff, 211 | pass, 212 | donations 213 | }); 214 | miner.on("open", (data: OpenEvent) => this.emit("open", data)); 215 | miner.on("authed", (data: AuthedEvent) => this.emit("authed", data)); 216 | miner.on("job", (data: JobEvent) => this.emit("job", data)); 217 | miner.on("found", (data: FoundEvent) => this.emit("found", data)); 218 | miner.on("accepted", (data: AcceptedEvent) => this.emit("accepted", data)); 219 | miner.on("close", (data: CloseEvent) => this.emit("close", data)); 220 | miner.on("error", (data: ErrorEvent) => this.emit("error", data)); 221 | miner.connect(); 222 | }); 223 | if (!host && !callback) { 224 | this.server.listen(port); 225 | } else if (!host && callback) { 226 | this.server.listen(port, callback); 227 | } else if (host && !callback) { 228 | this.server.listen(port, host); 229 | } else { 230 | this.server.listen(port, host, callback); 231 | } 232 | this.wss.on("listening", () => { 233 | this.online = true; 234 | console.log(`listening on port ${port}` + (isHTTPS ? ", using a secure connection" : "")); 235 | console.log(`miners per connection:`, this.maxMinersPerConnection); 236 | if (wssOptions.path) { 237 | console.log(`path: ${wssOptions.path}`); 238 | } 239 | if (!this.dynamicPool) { 240 | console.log(`host: ${this.host}`); 241 | console.log(`port: ${this.port}`); 242 | console.log(`pass: ${this.pass}`); 243 | } 244 | }); 245 | } 246 | 247 | getConnection(host: string, port: number, donation: boolean = false): Connection { 248 | const connectionId = `${host}:${port}`; 249 | if (!this.connections[connectionId]) { 250 | this.connections[connectionId] = []; 251 | } 252 | const connections = this.connections[connectionId]; 253 | const availableConnections = connections.filter(connection => this.isAvailable(connection)); 254 | if (availableConnections.length === 0) { 255 | const connection = new Connection({ host, port, ssl: this.ssl, donation }); 256 | connection.connect(); 257 | connection.on("close", () => { 258 | console.log(`connection closed (${connectionId})`); 259 | }); 260 | connection.on("error", error => { 261 | console.log(`connection error (${connectionId}):`, error.message); 262 | }); 263 | connections.push(connection); 264 | return connection; 265 | } 266 | return availableConnections.pop(); 267 | } 268 | 269 | isAvailable(connection: Connection): boolean { 270 | return ( 271 | connection.miners.length < this.maxMinersPerConnection && 272 | connection.donations.length < this.maxMinersPerConnection 273 | ); 274 | } 275 | 276 | isEmpty(connection: Connection): boolean { 277 | return connection.miners.length === 0 && connection.donations.length === 0; 278 | } 279 | 280 | getStats(): Stats { 281 | return Object.keys(this.connections).reduce( 282 | (stats, key) => ({ 283 | miners: [ 284 | ...stats.miners, 285 | ...this.connections[key].reduce( 286 | (miners, connection) => [ 287 | ...miners, 288 | ...connection.miners.map(miner => ({ 289 | id: miner.id, 290 | login: miner.login, 291 | hashes: miner.hashes 292 | })) 293 | ], 294 | [] 295 | ) 296 | ], 297 | connections: [ 298 | ...stats.connections, 299 | ...this.connections[key].filter(connection => !connection.donation).map(connection => ({ 300 | id: connection.id, 301 | host: connection.host, 302 | port: connection.port, 303 | miners: connection.miners.length 304 | })) 305 | ] 306 | }), 307 | { 308 | miners: [], 309 | connections: [] 310 | } 311 | ); 312 | } 313 | 314 | kill() { 315 | Object.keys(this.connections).forEach(connectionId => { 316 | const connections = this.connections[connectionId]; 317 | connections.forEach(connection => { 318 | connection.kill(); 319 | connection.miners.forEach(miner => miner.kill()); 320 | }); 321 | }); 322 | this.wss.close(); 323 | this.online = false; 324 | console.log(`💀`); 325 | } 326 | } 327 | 328 | export default Proxy; 329 | -------------------------------------------------------------------------------- /src/proxy/build/Proxy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = Object.setPrototypeOf || 4 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 5 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 6 | return function (d, b) { 7 | extendStatics(d, b); 8 | function __() { this.constructor = d; } 9 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 10 | }; 11 | })(); 12 | Object.defineProperty(exports, "__esModule", { value: true }); 13 | var EventEmitter = require("events"); 14 | var WebSocket = require("ws"); 15 | var url = require("url"); 16 | var http = require("http"); 17 | var https = require("https"); 18 | var defaults = require("../config/defaults"); 19 | var Connection_1 = require("./Connection"); 20 | var Miner_1 = require("./Miner"); 21 | var Donation_1 = require("./Donation"); 22 | var Proxy = /** @class */ (function (_super) { 23 | __extends(Proxy, _super); 24 | function Proxy(constructorOptions) { 25 | if (constructorOptions === void 0) { constructorOptions = defaults; } 26 | var _this = _super.call(this) || this; 27 | _this.host = null; 28 | _this.port = null; 29 | _this.pass = null; 30 | _this.ssl = null; 31 | _this.address = null; 32 | _this.user = null; 33 | _this.diff = null; 34 | _this.dynamicPool = false; 35 | _this.maxMinersPerConnection = 100; 36 | _this.donations = []; 37 | _this.connections = {}; 38 | _this.wss = null; 39 | _this.key = null; 40 | _this.cert = null; 41 | _this.path = null; 42 | _this.server = null; 43 | _this.credentials = null; 44 | _this.online = false; 45 | var options = Object.assign({}, defaults, constructorOptions); 46 | _this.host = options.host; 47 | _this.port = options.port; 48 | _this.pass = options.pass; 49 | _this.ssl = options.ssl; 50 | _this.address = options.address; 51 | _this.user = options.user; 52 | _this.diff = options.diff; 53 | _this.dynamicPool = options.dynamicPool; 54 | _this.maxMinersPerConnection = options.maxMinersPerConnection; 55 | _this.donations = options.donations; 56 | _this.key = options.key; 57 | _this.cert = options.cert; 58 | _this.path = options.path; 59 | _this.server = options.server; 60 | _this.credentials = options.credentials; 61 | _this.on("error", function (error) { 62 | /* prevent unhandled proxy errors from stopping the proxy */ 63 | console.error("proxy error:", error.message); 64 | }); 65 | return _this; 66 | } 67 | Proxy.prototype.listen = function (port, host, callback) { 68 | var _this = this; 69 | var version = require("../package").version; 70 | if (this.online) { 71 | this.kill(); 72 | } 73 | // create server 74 | var isHTTPS = !!(this.key && this.cert); 75 | if (!this.server) { 76 | var stats = function (req, res) { 77 | if (_this.credentials) { 78 | var auth = require("basic-auth")(req); 79 | if (!auth || auth.name !== _this.credentials.user || auth.pass !== _this.credentials.pass) { 80 | res.statusCode = 401; 81 | res.setHeader("WWW-Authenticate", 'Basic realm="Access to stats"'); 82 | res.end("Access denied"); 83 | return; 84 | } 85 | } 86 | var url = require("url").parse(req.url); 87 | if (url.pathname === "/ping") { 88 | res.statusCode = 200; 89 | res.end(); 90 | return; 91 | } 92 | if (url.pathname === "/ready") { 93 | res.statusCode = _this.online ? 200 : 503; 94 | res.end(); 95 | return; 96 | } 97 | if (url.pathname === "/version") { 98 | var body_1 = JSON.stringify({ version: version }); 99 | res.writeHead(200, { 100 | "Access-Control-Allow-Origin": "*", 101 | "Content-Length": Buffer.byteLength(body_1), 102 | "Content-Type": "application/json", 103 | }); 104 | res.end(body_1); 105 | return; 106 | } 107 | var proxyStats = _this.getStats(); 108 | var body = JSON.stringify({ 109 | code: 404, 110 | error: "Not Found" 111 | }); 112 | if (url.pathname === "/stats") { 113 | body = JSON.stringify({ 114 | miners: proxyStats.miners.length, 115 | connections: proxyStats.connections.length 116 | }, null, 2); 117 | } 118 | if (url.pathname === "/miners") { 119 | body = JSON.stringify(proxyStats.miners, null, 2); 120 | } 121 | if (url.pathname === "/connections") { 122 | body = JSON.stringify(proxyStats.connections, null, 2); 123 | } 124 | res.writeHead(200, { 125 | "Access-Control-Allow-Origin": "*", 126 | "Content-Length": Buffer.byteLength(body), 127 | "Content-Type": "application/json" 128 | }); 129 | res.end(body); 130 | }; 131 | if (isHTTPS) { 132 | var certificates = { 133 | key: this.key, 134 | cert: this.cert 135 | }; 136 | this.server = https.createServer(certificates, stats); 137 | } 138 | else { 139 | this.server = http.createServer(stats); 140 | } 141 | } 142 | var wssOptions = { 143 | server: this.server 144 | }; 145 | if (this.path) { 146 | wssOptions.path = this.path; 147 | } 148 | this.wss = new WebSocket.Server(wssOptions); 149 | this.wss.on("connection", function (ws, req) { 150 | var params = url.parse(req.url, true).query; 151 | params.pool = params.id; 152 | var host = _this.host; 153 | var port = _this.port; 154 | var pass = _this.pass; 155 | if (params.pool && _this.dynamicPool) { 156 | var split = params.pool.split(":"); 157 | host = split[0] || _this.host; 158 | port = Number(split[1]) || _this.port; 159 | pass = split[2] || _this.pass; 160 | console.log("Miner connected to pool", host); 161 | } 162 | var connection = _this.getConnection(host, port); 163 | var donations = _this.donations.map(function (donation) { 164 | return new Donation_1.default({ 165 | address: donation.address, 166 | host: donation.host, 167 | port: donation.port, 168 | pass: donation.pass, 169 | percentage: donation.percentage, 170 | connection: _this.getConnection(donation.host, donation.port, true) 171 | }); 172 | }); 173 | var miner = new Miner_1.default({ 174 | connection: connection, 175 | ws: ws, 176 | address: _this.address, 177 | user: _this.user, 178 | diff: _this.diff, 179 | pass: pass, 180 | donations: donations 181 | }); 182 | miner.on("open", function (data) { return _this.emit("open", data); }); 183 | miner.on("authed", function (data) { return _this.emit("authed", data); }); 184 | miner.on("job", function (data) { return _this.emit("job", data); }); 185 | miner.on("found", function (data) { return _this.emit("found", data); }); 186 | miner.on("accepted", function (data) { return _this.emit("accepted", data); }); 187 | miner.on("close", function (data) { return _this.emit("close", data); }); 188 | miner.on("error", function (data) { return _this.emit("error", data); }); 189 | miner.connect(); 190 | }); 191 | if (!host && !callback) { 192 | this.server.listen(port); 193 | } 194 | else if (!host && callback) { 195 | this.server.listen(port, callback); 196 | } 197 | else if (host && !callback) { 198 | this.server.listen(port, host); 199 | } 200 | else { 201 | this.server.listen(port, host, callback); 202 | } 203 | this.wss.on("listening", function () { 204 | _this.online = true; 205 | console.log("listening on port " + port + (isHTTPS ? ", using a secure connection" : "")); 206 | console.log("miners per connection:", _this.maxMinersPerConnection); 207 | if (wssOptions.path) { 208 | console.log("path: " + wssOptions.path); 209 | } 210 | if (!_this.dynamicPool) { 211 | console.log("host: " + _this.host); 212 | console.log("port: " + _this.port); 213 | console.log("pass: " + _this.pass); 214 | } 215 | }); 216 | }; 217 | Proxy.prototype.getConnection = function (host, port, donation) { 218 | var _this = this; 219 | if (donation === void 0) { donation = false; } 220 | var connectionId = host + ":" + port; 221 | if (!this.connections[connectionId]) { 222 | this.connections[connectionId] = []; 223 | } 224 | var connections = this.connections[connectionId]; 225 | var availableConnections = connections.filter(function (connection) { return _this.isAvailable(connection); }); 226 | if (availableConnections.length === 0) { 227 | var connection = new Connection_1.default({ host: host, port: port, ssl: this.ssl, donation: donation }); 228 | connection.connect(); 229 | connection.on("close", function () { 230 | console.log("connection closed (" + connectionId + ")"); 231 | }); 232 | connection.on("error", function (error) { 233 | console.log("connection error (" + connectionId + "):", error.message); 234 | }); 235 | connections.push(connection); 236 | return connection; 237 | } 238 | return availableConnections.pop(); 239 | }; 240 | Proxy.prototype.isAvailable = function (connection) { 241 | return (connection.miners.length < this.maxMinersPerConnection && 242 | connection.donations.length < this.maxMinersPerConnection); 243 | }; 244 | Proxy.prototype.isEmpty = function (connection) { 245 | return connection.miners.length === 0 && connection.donations.length === 0; 246 | }; 247 | Proxy.prototype.getStats = function () { 248 | var _this = this; 249 | return Object.keys(this.connections).reduce(function (stats, key) { return ({ 250 | miners: stats.miners.concat(_this.connections[key].reduce(function (miners, connection) { return miners.concat(connection.miners.map(function (miner) { return ({ 251 | id: miner.id, 252 | login: miner.login, 253 | hashes: miner.hashes 254 | }); })); }, [])), 255 | connections: stats.connections.concat(_this.connections[key].filter(function (connection) { return !connection.donation; }).map(function (connection) { return ({ 256 | id: connection.id, 257 | host: connection.host, 258 | port: connection.port, 259 | miners: connection.miners.length 260 | }); })) 261 | }); }, { 262 | miners: [], 263 | connections: [] 264 | }); 265 | }; 266 | Proxy.prototype.kill = function () { 267 | var _this = this; 268 | Object.keys(this.connections).forEach(function (connectionId) { 269 | var connections = _this.connections[connectionId]; 270 | connections.forEach(function (connection) { 271 | connection.kill(); 272 | connection.miners.forEach(function (miner) { return miner.kill(); }); 273 | }); 274 | }); 275 | this.wss.close(); 276 | this.online = false; 277 | console.log("\uD83D\uDC80"); 278 | }; 279 | return Proxy; 280 | }(EventEmitter)); 281 | exports.default = Proxy; 282 | -------------------------------------------------------------------------------- /src/proxy/build/Miner.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = Object.setPrototypeOf || 4 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 5 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 6 | return function (d, b) { 7 | extendStatics(d, b); 8 | function __() { this.constructor = d; } 9 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 10 | }; 11 | })(); 12 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 13 | return new (P || (P = Promise))(function (resolve, reject) { 14 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 15 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 16 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 17 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 18 | }); 19 | }; 20 | var __generator = (this && this.__generator) || function (thisArg, body) { 21 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 22 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 23 | function verb(n) { return function (v) { return step([n, v]); }; } 24 | function step(op) { 25 | if (f) throw new TypeError("Generator is already executing."); 26 | while (_) try { 27 | if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; 28 | if (y = 0, t) op = [0, t.value]; 29 | switch (op[0]) { 30 | case 0: case 1: t = op; break; 31 | case 4: _.label++; return { value: op[1], done: false }; 32 | case 5: _.label++; y = op[1]; op = [0]; continue; 33 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 34 | default: 35 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 36 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 37 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 38 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 39 | if (t[2]) _.ops.pop(); 40 | _.trys.pop(); continue; 41 | } 42 | op = body.call(thisArg, _); 43 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 44 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 45 | } 46 | }; 47 | Object.defineProperty(exports, "__esModule", { value: true }); 48 | var EventEmitter = require("events"); 49 | var WebSocket = require("ws"); 50 | var uuid = require("uuid"); 51 | var Queue_1 = require("./Queue"); 52 | var Metrics_1 = require("./Metrics"); 53 | var Miner = /** @class */ (function (_super) { 54 | __extends(Miner, _super); 55 | function Miner(options) { 56 | var _this = _super.call(this) || this; 57 | _this.id = uuid.v4(); 58 | _this.login = null; 59 | _this.address = null; 60 | _this.user = null; 61 | _this.diff = null; 62 | _this.pass = null; 63 | _this.donations = null; 64 | _this.heartbeat = null; 65 | _this.connection = null; 66 | _this.queue = new Queue_1.default(); 67 | _this.ws = null; 68 | _this.online = false; 69 | _this.jobs = []; 70 | _this.hashes = 0; 71 | _this.connection = options.connection; 72 | _this.ws = options.ws; 73 | _this.address = options.address; 74 | _this.user = options.user; 75 | _this.diff = options.diff; 76 | _this.pass = options.pass; 77 | _this.donations = options.donations; 78 | return _this; 79 | } 80 | Miner.prototype.connect = function () { 81 | return __awaiter(this, void 0, void 0, function () { 82 | var _this = this; 83 | return __generator(this, function (_a) { 84 | switch (_a.label) { 85 | case 0: 86 | console.log("miner connected (" + this.id + ")"); 87 | Metrics_1.minersCounter.inc(); 88 | this.donations.forEach(function (donation) { return donation.connect(); }); 89 | this.ws.on("message", this.handleMessage.bind(this)); 90 | this.ws.on("close", function () { 91 | if (_this.online) { 92 | console.log("miner connection closed (" + _this.id + ")"); 93 | _this.kill(); 94 | } 95 | }); 96 | this.ws.on("error", function (error) { 97 | if (_this.online) { 98 | console.log("miner connection error (" + _this.id + "):", error.message); 99 | _this.kill(); 100 | } 101 | }); 102 | this.connection.addMiner(this); 103 | this.connection.on(this.id + ":authed", this.handleAuthed.bind(this)); 104 | this.connection.on(this.id + ":job", this.handleJob.bind(this)); 105 | this.connection.on(this.id + ":accepted", this.handleAccepted.bind(this)); 106 | this.connection.on(this.id + ":error", this.handleError.bind(this)); 107 | this.queue.on("message", function (message) { 108 | return _this.connection.send(_this.id, message.method, message.params); 109 | }); 110 | this.heartbeat = setInterval(function () { return _this.connection.send(_this.id, "keepalived"); }, 30000); 111 | this.online = true; 112 | return [4 /*yield*/, Promise.all(this.donations.map(function (donation) { return donation.ready; }))]; 113 | case 1: 114 | _a.sent(); 115 | if (this.online) { 116 | this.queue.start(); 117 | this.emit("open", { 118 | id: this.id 119 | }); 120 | } 121 | return [2 /*return*/]; 122 | } 123 | }); 124 | }); 125 | }; 126 | Miner.prototype.kill = function () { 127 | this.queue.stop(); 128 | this.connection.removeMiner(this.id); 129 | this.connection.removeAllListeners(this.id + ":authed"); 130 | this.connection.removeAllListeners(this.id + ":job"); 131 | this.connection.removeAllListeners(this.id + ":accepted"); 132 | this.connection.removeAllListeners(this.id + ":error"); 133 | this.donations.forEach(function (donation) { return donation.kill(); }); 134 | this.jobs = []; 135 | this.donations = []; 136 | this.hashes = 0; 137 | this.ws.close(); 138 | if (this.heartbeat) { 139 | clearInterval(this.heartbeat); 140 | this.heartbeat = null; 141 | } 142 | if (this.online) { 143 | this.online = false; 144 | Metrics_1.minersCounter.dec(); 145 | console.log("miner disconnected (" + this.id + ")"); 146 | this.emit("close", { 147 | id: this.id, 148 | login: this.login 149 | }); 150 | } 151 | this.removeAllListeners(); 152 | }; 153 | Miner.prototype.sendToMiner = function (payload) { 154 | var coinhiveMessage = JSON.stringify(payload); 155 | if (this.online && this.ws.readyState === WebSocket.OPEN) { 156 | try { 157 | this.ws.send(coinhiveMessage); 158 | } 159 | catch (e) { 160 | this.kill(); 161 | } 162 | } 163 | }; 164 | Miner.prototype.sendToPool = function (method, params) { 165 | this.queue.push({ 166 | type: "message", 167 | payload: { 168 | method: method, 169 | params: params 170 | } 171 | }); 172 | }; 173 | Miner.prototype.handleAuthed = function (auth) { 174 | console.log("miner authenticated (" + this.id + "):", auth); 175 | this.sendToMiner({ 176 | type: "authed", 177 | params: { 178 | token: "", 179 | hashes: 0 180 | } 181 | }); 182 | this.emit("authed", { 183 | id: this.id, 184 | login: this.login, 185 | auth: auth 186 | }); 187 | }; 188 | Miner.prototype.handleJob = function (job) { 189 | var _this = this; 190 | console.log("job arrived (" + this.id + "):", job.job_id); 191 | this.jobs.push(job); 192 | var donations = this.donations.filter(function (donation) { return donation.shouldDonateJob(); }); 193 | donations.forEach(function (donation) { 194 | _this.sendToMiner({ 195 | type: "job", 196 | params: donation.getJob() 197 | }); 198 | }); 199 | if (!this.hasPendingDonations() && donations.length === 0) { 200 | this.sendToMiner({ 201 | type: "job", 202 | params: this.jobs.pop() 203 | }); 204 | } 205 | this.emit("job", { 206 | id: this.id, 207 | login: this.login, 208 | job: job 209 | }); 210 | }; 211 | Miner.prototype.handleAccepted = function (job) { 212 | this.hashes++; 213 | console.log("shares accepted (" + this.id + "):", this.hashes); 214 | Metrics_1.sharesCounter.inc(); 215 | Metrics_1.sharesMeter.mark(); 216 | this.sendToMiner({ 217 | type: "hash_accepted", 218 | params: { 219 | hashes: this.hashes 220 | } 221 | }); 222 | this.emit("accepted", { 223 | id: this.id, 224 | login: this.login, 225 | hashes: this.hashes 226 | }); 227 | }; 228 | Miner.prototype.handleError = function (error) { 229 | console.warn("pool connection error (" + this.id + "):", error.error || (error && JSON.stringify(error)) || "unknown error"); 230 | if (this.online) { 231 | if (error.error === "invalid_site_key") { 232 | this.sendToMiner({ 233 | type: "error", 234 | params: error 235 | }); 236 | } 237 | this.emit("error", { 238 | id: this.id, 239 | login: this.login, 240 | error: error 241 | }); 242 | } 243 | this.kill(); 244 | }; 245 | Miner.prototype.handleMessage = function (message) { 246 | var data; 247 | try { 248 | data = JSON.parse(message); 249 | } 250 | catch (e) { 251 | console.warn("can't parse message as JSON from miner:", message, e.message); 252 | return; 253 | } 254 | switch (data.type) { 255 | case "auth": { 256 | var params = data.params; 257 | this.login = this.address || params.site_key; 258 | var user = this.user || params.user; 259 | if (user) { 260 | this.login += "." + user; 261 | } 262 | if (this.diff) { 263 | this.login += "+" + this.diff; 264 | } 265 | this.sendToPool("login", { 266 | login: this.login, 267 | pass: this.pass 268 | }); 269 | break; 270 | } 271 | case "submit": { 272 | var job = data.params; 273 | console.log("job submitted (" + this.id + "):", job.job_id); 274 | if (!this.isDonation(job)) { 275 | this.sendToPool("submit", job); 276 | } 277 | else { 278 | var donation = this.getDonation(job); 279 | donation.submit(job); 280 | this.sendToMiner({ 281 | type: "hash_accepted", 282 | params: { 283 | hashes: ++this.hashes 284 | } 285 | }); 286 | } 287 | this.emit("found", { 288 | id: this.id, 289 | login: this.login, 290 | job: job 291 | }); 292 | break; 293 | } 294 | } 295 | }; 296 | Miner.prototype.isDonation = function (job) { 297 | return this.donations.some(function (donation) { return donation.hasJob(job); }); 298 | }; 299 | Miner.prototype.getDonation = function (job) { 300 | return this.donations.find(function (donation) { return donation.hasJob(job); }); 301 | }; 302 | Miner.prototype.hasPendingDonations = function () { 303 | return this.donations.some(function (donation) { return donation.taken.filter(function (job) { return !job.done; }).length > 0; }); 304 | }; 305 | return Miner; 306 | }(EventEmitter)); 307 | exports.default = Miner; 308 | --------------------------------------------------------------------------------