├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------