├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json ├── payout.sample.conf ├── server.sample.conf ├── service.sample.conf ├── spec ├── ChainSampleData.spec.js ├── NQ25sampleData.spec.js ├── NQ43sampleData.spec.js ├── PoolAgent.spec.js ├── PoolPayout.spec.js ├── PoolService.spec.js ├── spec.js └── support │ └── jasmine.json ├── sql ├── create.sql └── drop.sql ├── src ├── Config.js ├── Helper.js ├── MetricsServer.js ├── PoolAgent.js ├── PoolPayout.js ├── PoolServer.js └── PoolService.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: true 3 | 4 | language: node_js 5 | node_js: 6 | - "9" 7 | cache: yarn 8 | 9 | addons: 10 | mariadb: '10.0' 11 | 12 | before_install: 13 | - sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('root') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;" 14 | - sudo mysql_upgrade --password=root 15 | - sudo service mysql restart 16 | 17 | install: 18 | - yarn 19 | 20 | script: 21 | - yarn run jasmine 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nimiq Mining-Pool Server 2 | This mining pool server combines resources of multiple clients mining on the Nimiq blockchain. 3 | Clients are independent network nodes and generate or validate blocks themselves to support decentralization. 4 | Details about the mining pool protocol can be found [here](https://nimiq-network.github.io/developer-reference/chapters/pool-protocol.html#mining-pool-protocol). 5 | A mining pool client is implemented in [Nimiq Core](https://github.com/nimiq-network/core/tree/master/src/main/generic/miner). 6 | 7 | **Operating a public mining-pool in the mainnet makes you responsible for other people's money. Test your pool setup in the testnet first!** 8 | 9 | ## Architecture 10 | The pool server consists of three parts which communicate through a common MySQL-compatible database (schema see `sql/create.sql`) 11 | * The pool **server** interacts with clients and verifies their shares. There can be multiple pool server instances. 12 | * The pool **service** computes client rewards using a PPLNS reward system. 13 | * The pool **payout** processes automatic payouts above a certain user balance and payout requests. 14 | 15 | While the server(s) and the service are designed to run continuously, the pool payout has to be executed whenever a payout is desired. 16 | 17 | ## Run 18 | Run `node index.js --config=[CONFIG_FILE]`. See `[server|service|payout].sample.conf` for sample configuration files and clarifications. 19 | 20 | ## License 21 | Copyright 2018 The Nimiq Foundation 22 | 23 | Licensed under the Apache License, Version 2.0 (the "License"); 24 | you may not use this file except in compliance with the License. 25 | You may obtain a copy of the License at 26 | 27 | http://www.apache.org/licenses/LICENSE-2.0 28 | 29 | Unless required by applicable law or agreed to in writing, software 30 | distributed under the License is distributed on an "AS IS" BASIS, 31 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 32 | See the License for the specific language governing permissions and 33 | limitations under the License. 34 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Nimiq = require('@nimiq/core'); 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | const config = require('./src/Config.js')(argv.config); 4 | 5 | const PoolServer = require('./src/PoolServer.js'); 6 | const PoolService = require('./src/PoolService.js'); 7 | const PoolPayout = require('./src/PoolPayout.js'); 8 | const MetricsServer = require('./src/MetricsServer.js'); 9 | 10 | const START = Date.now(); 11 | const TAG = 'Node'; 12 | const $ = {}; 13 | 14 | if (!config) { 15 | Nimiq.Log.e(TAG, 'Specify a valid config file with --config=FILE'); 16 | process.exit(1); 17 | } 18 | if (config.poolServer.enabled && config.type !== 'full') { 19 | Nimiq.Log.e(TAG, 'Pool server must run as a \'full\' node'); 20 | process.exit(1); 21 | } 22 | if (config.poolPayout.enabled && (config.poolServer.enabled || config.poolService.enabled)) { 23 | Nimiq.Log.e(TAG, 'Pool payout needs to run separately from pool server'); 24 | process.exit(1); 25 | } 26 | 27 | Nimiq.Log.instance.level = config.log.level; 28 | for (const tag in config.log.tags) { 29 | Nimiq.Log.instance.setLoggable(tag, config.log.tags[tag]); 30 | } 31 | 32 | for (const key in config.constantOverrides) { 33 | Nimiq.ConstantHelper.instance.set(key, config.constantOverrides[key]); 34 | } 35 | 36 | Nimiq.GenesisConfig.init(Nimiq.GenesisConfig.CONFIGS[config.network]); 37 | 38 | for (const seedPeer of config.seedPeers) { 39 | Nimiq.GenesisConfig.SEED_PEERS.push(Nimiq.WsPeerAddress.seed(seedPeer.host, seedPeer.port, seedPeer.publicKey)); 40 | } 41 | 42 | (async () => { 43 | const networkConfig = config.dumb 44 | ? new Nimiq.DumbNetworkConfig() 45 | : new Nimiq.WsNetworkConfig(config.host, config.port, config.tls.key, config.tls.cert); 46 | 47 | switch (config.type) { 48 | case 'full': 49 | $.consensus = await Nimiq.Consensus.full(networkConfig); 50 | break; 51 | case 'light': 52 | $.consensus = await Nimiq.Consensus.light(networkConfig); 53 | break; 54 | case 'nano': 55 | $.consensus = await Nimiq.Consensus.nano(networkConfig); 56 | break; 57 | } 58 | 59 | $.blockchain = $.consensus.blockchain; 60 | $.accounts = $.blockchain.accounts; 61 | $.mempool = $.consensus.mempool; 62 | $.network = $.consensus.network; 63 | 64 | Nimiq.Log.i(TAG, `Peer address: ${networkConfig.peerAddress.toString()} - public key: ${networkConfig.keyPair.publicKey.toHex()}`); 65 | 66 | // TODO: Wallet key. 67 | $.walletStore = await new Nimiq.WalletStore(); 68 | if (!config.wallet.seed) { 69 | // Load or create default wallet. 70 | $.wallet = await $.walletStore.getDefault(); 71 | } else if (config.wallet.seed) { 72 | // Load wallet from seed. 73 | const mainWallet = await Nimiq.Wallet.loadPlain(config.wallet.seed); 74 | await $.walletStore.put(mainWallet); 75 | await $.walletStore.setDefault(mainWallet.address); 76 | $.wallet = mainWallet; 77 | } 78 | 79 | if (config.poolServer.enabled) { 80 | const poolServer = new PoolServer($.consensus, config.pool, config.poolServer.port, config.poolServer.mySqlPsw, config.poolServer.mySqlHost, config.poolServer.sslKeyPath, config.poolServer.sslCertPath); 81 | 82 | if (config.poolMetricsServer.enabled) { 83 | $.metricsServer = new MetricsServer(config.poolServer.sslKeyPath, config.poolServer.sslCertPath, config.poolMetricsServer.port, config.poolMetricsServer.password); 84 | $.metricsServer.init(poolServer); 85 | } 86 | 87 | process.on('SIGTERM', () => { 88 | poolServer.stop(); 89 | process.exit(0); 90 | }); 91 | process.on('SIGINT', () => { 92 | poolServer.stop(); 93 | process.exit(0); 94 | }); 95 | } 96 | if (config.poolService.enabled) { 97 | const poolService = new PoolService($.consensus, config.pool, config.poolService.mySqlPsw, config.poolService.mySqlHost); 98 | poolService.start(); 99 | } 100 | if (config.poolPayout.enabled) { 101 | const wallet = await $.walletStore.get(Nimiq.Address.fromString(config.pool.address)); 102 | if (!wallet) Nimiq.Log.i(TAG, 'Wallet for pool address not found, will fallback to default wallet for payouts.'); 103 | const poolPayout = new PoolPayout($.consensus, wallet || $.wallet, config.pool, config.poolPayout.mySqlPsw, config.poolPayout.mySqlHost); 104 | poolPayout.start(); 105 | } 106 | 107 | const addresses = await $.walletStore.list(); 108 | Nimiq.Log.i(TAG, `Managing wallets [${addresses.map(address => address.toUserFriendlyAddress())}]`); 109 | 110 | const isNano = config.type === 'nano'; 111 | const account = !isNano ? await $.accounts.get($.wallet.address) : null; 112 | Nimiq.Log.i(TAG, `Wallet initialized for address ${$.wallet.address.toUserFriendlyAddress()}.` 113 | + (!isNano ? ` Balance: ${Nimiq.Policy.satoshisToCoins(account.balance)} NIM` : '')); 114 | 115 | Nimiq.Log.i(TAG, `Blockchain state: height=${$.blockchain.height}, headHash=${$.blockchain.headHash}`); 116 | 117 | $.blockchain.on('head-changed', (head) => { 118 | if ($.consensus.established || head.height % 100 === 0) { 119 | Nimiq.Log.i(TAG, `Now at block: ${head.height}`); 120 | } 121 | }); 122 | 123 | $.network.on('peer-joined', (peer) => { 124 | Nimiq.Log.i(TAG, `Connected to ${peer.peerAddress.toString()}`); 125 | }); 126 | 127 | $.consensus.on('established', () => { 128 | Nimiq.Log.i(TAG, `Blockchain ${config.type}-consensus established in ${(Date.now() - START) / 1000}s.`); 129 | Nimiq.Log.i(TAG, `Current state: height=${$.blockchain.height}, totalWork=${$.blockchain.totalWork}, headHash=${$.blockchain.headHash}`); 130 | }); 131 | 132 | $.network.connect(); 133 | })().catch(e => { 134 | console.error(e); 135 | process.exit(1); 136 | }); 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nimiq-pool", 3 | "version": "1.0.0", 4 | "homepage": "https://nimiq.com/", 5 | "description": "", 6 | "private": true, 7 | "author": { 8 | "name": "The Nimiq Core Development Team", 9 | "url": "https://nimiq.com/" 10 | }, 11 | "license": "Apache-2.0", 12 | "bugs": { 13 | "url": "https://github.com/nimiq-network/mining-pool/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/nimiq-network/mining-pool.git" 18 | }, 19 | "engines": { 20 | "node": ">=7.9.0" 21 | }, 22 | "devDependencies": { 23 | "jasmine": "^3.1.0" 24 | }, 25 | "dependencies": { 26 | "@nimiq/core": "^1.1.0", 27 | "btoa": "^1.2.1", 28 | "jasmine-spec-reporter": "^4.2.1", 29 | "json5": "^1.0.1", 30 | "lodash.merge": "^4.6.1", 31 | "minimist": "^1.2.0", 32 | "mysql2": "^1.5.3", 33 | "uws": "^9.147.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /payout.sample.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Pool payout configuration 3 | poolPayout: { 4 | // Whether a pool payout should run. 5 | // Default: false 6 | enabled: true, 7 | 8 | // Password of the MySQL pool_payout user. 9 | mySqlPsw: 'password', 10 | 11 | // Host the MySQL database runs on. 12 | // Default: 'localhost' 13 | mySqlHost: 'localhost' 14 | }, 15 | 16 | // General mining pool configuration 17 | pool: { 18 | // Name announced to the client. 19 | name: 'My Nimiq Pool', 20 | 21 | // Pool address which the clients will set as miner address. 22 | address: 'NQ07 0000 0000 0000 0000 0000 0000 0000 0000', 23 | 24 | // Confirmations required before shares for a block are rewarded. 25 | // Default: 10 26 | //payoutConfirmations: 10, 27 | 28 | // The pool will automatically pay out users having accumulated over autoPayOutLimit satoshis. 29 | // Default: 5000000 (50 NIM) 30 | //autoPayOutLimit: 5000000, 31 | 32 | // The pool will keep (blockReward + feesInBlock) * poolFee for itself. 33 | // Default: 0.01 34 | //poolFee: 0.01, 35 | 36 | // Network fee used by the pool for payouts (in satoshi per byte). 37 | // Default: 1 38 | //networkFee: 1, 39 | 40 | // Desired shares per second (SPS) for connected clients, regulates share submission rate. 41 | // Default: 0.2 42 | //desiredSps: 0.2, 43 | 44 | // Shares submitted over spsTimeUnit [ms] are used to adjust a clients share difficulty. 45 | // Default: 60000 (1 min) 46 | //spsTimeUnit: 60000, 47 | 48 | // The lower bound for the share difficulty. 49 | // Default: 1 50 | //minDifficulty: 1, 51 | 52 | // If no valid shares are sent over a connection during connectionTimeout [ms], it is closed. 53 | // Default: 60 * 1000 * 10 (10 min) 54 | //connectionTimeout: 60 * 1000 * 10, 55 | 56 | // Number of previous shares taken into account for block payout. 57 | // Default: 1000 58 | //pplnsShares: 1000, 59 | 60 | // Number of allowed errors (invalid shares) between new settings. 61 | // Default: 3 62 | //allowedErrors: 3 63 | }, 64 | 65 | // Nimiq Core configuration 66 | // See https://github.com/nimiq-network/core/blob/master/clients/nodejs/sample.conf for more information. 67 | 68 | host: "my.domain", 69 | //port: 8443, 70 | tls: { 71 | cert: "./my.domain.cer", 72 | key: "./my.domain.key" 73 | }, 74 | //dumb: "yes", 75 | //type: "full", 76 | //network: "test", 77 | log: { 78 | //level: "verbose" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server.sample.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Pool server configuration 3 | poolServer: { 4 | // Whether a pool server should run. 5 | // Default: false 6 | enabled: true, 7 | 8 | // Specifies which port to listen on for connections. 9 | // Possible values: any valid port number 10 | // Default: 8444 11 | //port: 8444, 12 | 13 | // Certificate file and private key file to use for the TLS secured server. 14 | sslCertPath: './my.domain.cer', 15 | sslKeyPath: './my.domain.key', 16 | 17 | // Password of the MySQL pool_server user. 18 | mySqlPsw: 'password', 19 | 20 | // Host the MySQL database runs on. 21 | // Default: 'localhost' 22 | mySqlHost: 'localhost' 23 | }, 24 | 25 | // General mining pool configuration 26 | pool: { 27 | // Name announced to the client. 28 | name: 'My Nimiq Pool', 29 | 30 | // Pool address which the clients will set as miner address. 31 | address: 'NQ07 0000 0000 0000 0000 0000 0000 0000 0000', 32 | 33 | // Confirmations required before shares for a block are rewarded. 34 | // Default: 10 35 | //payoutConfirmations: 10, 36 | 37 | // The pool will automatically pay out users having accumulated over autoPayOutLimit satoshis. 38 | // Default: 5000000 (50 NIM) 39 | //autoPayOutLimit: 5000000, 40 | 41 | // The pool will keep (blockReward + feesInBlock) * poolFee for itself. 42 | // Default: 0.01 43 | //poolFee: 0.01, 44 | 45 | // Network fee used by the pool for payouts (in satoshi per byte). 46 | // Default: 1 47 | //networkFee: 1, 48 | 49 | // Desired shares per second (SPS) for connected clients, regulates share submission rate. 50 | // Default: 0.2 51 | //desiredSps: 0.2, 52 | 53 | // Shares submitted over spsTimeUnit [ms] are used to adjust a clients share difficulty. 54 | // Default: 60000 (1 min) 55 | //spsTimeUnit: 60000, 56 | 57 | // Difficulty which will be announced to and expected from a client before adaptation 58 | // Default: 1 59 | //startDifficulty: 1, 60 | 61 | // The lower bound for the share difficulty. 62 | // Default: 1 63 | //minDifficulty: 1, 64 | 65 | // If no valid shares are sent over a connection during connectionTimeout [ms], it is closed. 66 | // Default: 60 * 1000 * 10 (10 min) 67 | //connectionTimeout: 60 * 1000 * 10, 68 | 69 | // Number of previous shares taken into account for block payout. 70 | // Default: 1000 71 | //pplnsShares: 1000, 72 | 73 | // Number of allowed errors (invalid shares) between new settings. 74 | // Default: 3 75 | //allowedErrors: 3 76 | }, 77 | 78 | // Nimiq Core configuration 79 | // See https://github.com/nimiq-network/core/blob/master/clients/nodejs/sample.conf for more information. 80 | 81 | host: "my.domain", 82 | //port: 8443, 83 | tls: { 84 | cert: "./my.domain.cer", 85 | key: "./my.domain.key" 86 | }, 87 | //dumb: "yes", 88 | //type: "full", 89 | //network: "test", 90 | log: { 91 | //level: "verbose" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /service.sample.conf: -------------------------------------------------------------------------------- 1 | { 2 | // Pool service configuration 3 | poolService: { 4 | // Whether a pool service should run. 5 | // Default: false 6 | enabled: true, 7 | 8 | // Password of the MySQL pool_service user. 9 | mySqlPsw: 'password', 10 | 11 | // Host the MySQL database runs on. 12 | // Default: 'localhost' 13 | mySqlHost: 'localhost' 14 | }, 15 | 16 | // General mining pool configuration 17 | pool: { 18 | // Name announced to the client. 19 | name: 'My Nimiq Pool', 20 | 21 | // Pool address which the clients will set as miner address. 22 | address: 'NQ07 0000 0000 0000 0000 0000 0000 0000 0000', 23 | 24 | // Confirmations required before shares for a block are rewarded. 25 | // Default: 10 26 | //payoutConfirmations: 10, 27 | 28 | // The pool will automatically pay out users having accumulated over autoPayOutLimit satoshis. 29 | // Default: 5000000 (50 NIM) 30 | //autoPayOutLimit: 5000000, 31 | 32 | // The pool will keep (blockReward + feesInBlock) * poolFee for itself. 33 | // Default: 0.01 34 | //poolFee: 0.01, 35 | 36 | // Network fee used by the pool for payouts (in satoshi per byte). 37 | // Default: 1 38 | //networkFee: 1, 39 | 40 | // Desired shares per second (SPS) for connected clients, regulates share submission rate. 41 | // Default: 0.2 42 | //desiredSps: 0.2, 43 | 44 | // Shares submitted over spsTimeUnit [ms] are used to adjust a clients share difficulty. 45 | // Default: 60000 (1 min) 46 | //spsTimeUnit: 60000, 47 | 48 | // The lower bound for the share difficulty. 49 | // Default: 1 50 | //minDifficulty: 1, 51 | 52 | // If no valid shares are sent over a connection during connectionTimeout [ms], it is closed. 53 | // Default: 60 * 1000 * 10 (10 min) 54 | //connectionTimeout: 60 * 1000 * 10, 55 | 56 | // Number of previous shares taken into account for block payout. 57 | // Default: 1000 58 | //pplnsShares: 1000, 59 | 60 | // Number of allowed errors (invalid shares) between new settings. 61 | // Default: 3 62 | //allowedErrors: 3 63 | }, 64 | 65 | // Nimiq Core configuration 66 | // See https://github.com/nimiq-network/core/blob/master/clients/nodejs/sample.conf for more information. 67 | 68 | host: "my.domain", 69 | //port: 8443, 70 | tls: { 71 | cert: "./my.domain.cer", 72 | key: "./my.domain.key" 73 | }, 74 | //dumb: "yes", 75 | //type: "full", 76 | //network: "test", 77 | log: { 78 | //level: "verbose" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /spec/ChainSampleData.spec.js: -------------------------------------------------------------------------------- 1 | const Nimiq = require('@nimiq/core'); 2 | 3 | ChainSampleData = {}; 4 | 5 | ChainSampleData.block1 = Nimiq.Block.unserialize(Nimiq.BufferUtils.fromBase64( 6 | "AAH6/T8cyAAwnLkxCBJXtOftsPnqQAGHxVD9kUFEfqCGpZEzwRF7tseL/hGulqbi1xtu7ih6/xnc1EUAx+iXQJZRAejPuRJsKB1zWYhrHI3Y+dsAtwuM17bn5H87ZQGTCVJsfaKhCoweRBsyqJEgb7br+BaYlE4HG7kVCdp1Oi5FIx8BAAAAAAACAAAAAQAAAwEBgAGArhhOf91579MxW9DSJTajUwWGPgAAAAAA" 7 | )); 8 | ChainSampleData.block2 = Nimiq.Block.unserialize(Nimiq.BufferUtils.fromBase64( 9 | "AAFO1AeOG9e/9f+w0HACxbe+zsxDztr1irNw/A5OWTYDGINsrNPDd9C0jjEDvdez7LOJ393QSqhI8CCjrO0j49DmAejPuRJsKB1zWYhrHI3Y+dsAtwuM17bn5H87ZQGTCVIarptjxFVJ4Yp1fkQHMqXUxDzjwCfXIV4A1W5S54WbfB8A/eYAAAADAAAAAgABhgMCwAGArhhOf91579MxW9DSJTajUwWGPgAAAAAA" 10 | )); 11 | ChainSampleData.block3 = Nimiq.Block.unserialize(Nimiq.BufferUtils.fromBase64( 12 | "AAF01zUvAbz1LPF4wBBHJW+/g0g/K8fyLow7Ef4NcWz+ThavJXMi9PHe6aLG4qoqbnOy19Gkjtw77UsZuzIMtLGHAejPuRJsKB1zWYhrHI3Y+dsAtwuM17bn5H87ZQGTCVKWZdA28NRd19avJ8euXprPHO7Gn5pcUBCnuB0KFE08sB8A+8kAAAAEAAAAAwAAALUH/gGArhhOf91579MxW9DSJTajUwWGPgAAAAAA" 13 | )); 14 | ChainSampleData.block4 = Nimiq.Block.unserialize(Nimiq.BufferUtils.fromBase64( 15 | "AAG880v+Zd22szLwjKivh9R2M6GhEqdco0pv59WCtpykKu5grS7EqMMF1YKshMYVgUxXUBX+g27/BauhNmbygQzzAejPuRJsKB1zWYhrHI3Y+dsAtwuM17bn5H87ZQGTCVKcqbQlRbC10rb1tEzwlNSlVughCIhCID4j8XznKc7gEB8A+acAAAAFAAAABAABMeMHvnTXNS8BvPUs8XjAEEclb7+DSD8rx/IujDsR/g1xbP5OAYCuGE5/3Xnv0zFb0NIlNqNTBYY+AAAAAAA=" 16 | )); 17 | ChainSampleData.block5 = Nimiq.Block.unserialize(Nimiq.BufferUtils.fromBase64( 18 | "AAGjytCIvUgqBJZH3dDY/+ah0jZqckxQGbqVLVF1kwZx1e5grS7EqMMF1YKshMYVgUxXUBX+g27/BauhNmbygQzzAejPuRJsKB1zWYhrHI3Y+dsAtwuM17bn5H87ZQGTCVLOCXEMEIr7GG3ZnvKGtbDYZ8Lw4jw5Dy9SSHmOEc69wh8A94EAAAAGAAAABQAApfwHvnTXNS8BvPUs8XjAEEclb7+DSD8rx/IujDsR/g1xbP5OAYCuGE5/3Xnv0zFb0NIlNqNTBYY+AAAAAAA=" 19 | )); 20 | -------------------------------------------------------------------------------- /spec/NQ25sampleData.spec.js: -------------------------------------------------------------------------------- 1 | const Nimiq = require('@nimiq/core'); 2 | 3 | NQ25sampleData = {}; 4 | 5 | NQ25sampleData.address = Nimiq.Address.fromUserFriendlyAddress('NQ25 FGPF A68A TBQ4 7KUU 3TFG 418D 1J49 HRLN'); 6 | 7 | NQ25sampleData.register = { 8 | message: 'register', 9 | address: 'NQ25 FGPF A68A TBQ4 7KUU 3TFG 418D 1J49 HRLN', 10 | deviceId: 6614501121, 11 | mode: 'smart', 12 | genesisHash: Nimiq.BufferUtils.toBase64(Nimiq.GenesisConfig.GENESIS_HASH.serialize()) 13 | }; 14 | module.exports = exports = NQ25sampleData; 15 | -------------------------------------------------------------------------------- /spec/NQ43sampleData.spec.js: -------------------------------------------------------------------------------- 1 | const Nimiq = require('@nimiq/core'); 2 | 3 | NQ43sampleData = {}; 4 | 5 | NQ43sampleData.address = Nimiq.Address.fromUserFriendlyAddress('NQ43 SXSE XAS0 HYXJ M1U4 DCJ3 0SXE 8KUH 5DU7'); 6 | 7 | NQ43sampleData.register = { 8 | message: 'register', 9 | address: 'NQ43 SXSE XAS0 HYXJ M1U4 DCJ3 0SXE 8KUH 5DU7', 10 | deviceId: 1513202621, 11 | mode: 'smart', 12 | genesisHash: Nimiq.BufferUtils.toBase64(Nimiq.GenesisConfig.GENESIS_HASH.serialize()) 13 | }; 14 | module.exports = exports = NQ43sampleData; 15 | -------------------------------------------------------------------------------- /spec/PoolAgent.spec.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql2/promise'); 2 | 3 | const Nimiq = require('@nimiq/core'); 4 | 5 | const PoolAgent = require('../src/PoolAgent.js'); 6 | const PoolServer = require('../src/PoolServer.js'); 7 | 8 | describe('PoolAgent', () => { 9 | 10 | beforeEach(() => { 11 | spyOn(PoolServer, 'createServer').and.callFake(() => { 12 | return { 13 | on: () => {}, 14 | close: () => {} 15 | }; 16 | }); 17 | }); 18 | 19 | async function generateBlockMessage(minerAddr, extraData, fixTime, target) { 20 | const nonces = { 21 | 's4Xy6xUCf/WegBZhQTUdN5zq1knUpXLMyqDMyuaGsjU=': 225614, 22 | '4ulvVBd/xJU1VdbQo2nzkPZVrBErxIYDNMHLLj51KPQ=': 12188, 23 | '/Txmmo6uRktXSQCQ8P5OqDXo3iXdDmb4LZ7veHotXq8=': 28027 24 | }; 25 | const accounts = await Nimiq.Accounts.createVolatile(); 26 | const transactionStore = await Nimiq.TransactionStore.createVolatile(); 27 | const blockchain = await Nimiq.FullChain.createVolatile(accounts, fixTime, transactionStore); 28 | const mempool = new Nimiq.Mempool(blockchain, accounts); 29 | const miner = new Nimiq.Miner(blockchain, accounts, mempool, fixTime, minerAddr, extraData); 30 | const block = await miner.getNextBlock(); 31 | const blockHeader64 = block.header.hash().toBase64(); 32 | // console.log(blockHeader64); 33 | // miner.shareTarget = target; 34 | // miner.on('share', async (block) => { 35 | // console.log(block); 36 | // }); 37 | // miner.startWork(); 38 | block.header.nonce = nonces[blockHeader64]; 39 | 40 | return { 41 | message: 'share', 42 | blockHeader: Nimiq.BufferUtils.toBase64(block.header.serialize()), 43 | minerAddrProof: Nimiq.BufferUtils.toBase64((Nimiq.MerklePath.compute(block.body.getMerkleLeafs(), block.minerAddr)).serialize()), 44 | extraDataProof: Nimiq.BufferUtils.toBase64((Nimiq.MerklePath.compute(block.body.getMerkleLeafs(), block.body.extraData)).serialize()) 45 | }; 46 | } 47 | 48 | it('verifies shares (smart mode)', (done) => { 49 | (async () => { 50 | const consensus = await Nimiq.Consensus.volatileFull(); 51 | const poolServer = new PoolServer(consensus, POOL_CONFIG, 9999, '', '', '', ''); 52 | await poolServer.start(); 53 | 54 | const time = new Nimiq.Time(); 55 | spyOn(time, 'now').and.callFake(() => 0); 56 | 57 | const poolAgent = new PoolAgent(poolServer, { 58 | close: () => {}, 59 | send: async (json) => { 60 | const msg = JSON.parse(json); 61 | if (msg.message === 'settings') { 62 | const poolAddress = Nimiq.Address.fromUserFriendlyAddress(msg.address); 63 | const extraData = Nimiq.BufferUtils.fromBase64(msg.extraData); 64 | const target = parseFloat(msg.target); 65 | 66 | let userId = await poolServer.getStoreUserId(NQ43sampleData.address); 67 | 68 | // valid share 69 | let shareMsg = await generateBlockMessage(poolAddress, extraData, time, target); 70 | await poolAgent._onMessage(shareMsg); 71 | let hash = Nimiq.BlockHeader.unserialize(Nimiq.BufferUtils.fromBase64(shareMsg.blockHeader)).hash(); 72 | expect(await poolServer.containsShare(userId, hash)).toBeTruthy(); 73 | 74 | // wrong miner address 75 | shareMsg = await generateBlockMessage(Nimiq.Address.fromUserFriendlyAddress('NQ57 LUAL 6R8F ETD3 VE77 6NK5 HEUK 009H C06B'), extraData, time, target); 76 | await poolAgent._onMessageData(JSON.stringify(shareMsg)); 77 | hash = Nimiq.BlockHeader.unserialize(Nimiq.BufferUtils.fromBase64(shareMsg.blockHeader)).hash(); 78 | expect(await poolServer.containsShare(userId, hash)).toBeFalsy(); 79 | 80 | // wrong extra data 81 | shareMsg = await generateBlockMessage(poolAddress, Nimiq.BufferUtils.fromAscii('wrong'), time, target); 82 | await poolAgent._onMessageData(JSON.stringify(shareMsg)); 83 | hash = Nimiq.BlockHeader.unserialize(Nimiq.BufferUtils.fromBase64(shareMsg.blockHeader)).hash(); 84 | expect(await poolServer.containsShare(userId, hash)).toBeFalsy(); 85 | 86 | done(); 87 | } 88 | } 89 | }, Nimiq.NetAddress.fromIP('1.2.3.4')); 90 | await poolAgent._onMessage(NQ43sampleData.register); 91 | })().catch(done.fail); 92 | }); 93 | 94 | it('does not count shares onto old blocks (smart mode)', (done) => { 95 | (async () => { 96 | const consensus = await Nimiq.Consensus.volatileFull(); 97 | const poolServer = new PoolServer(consensus, POOL_CONFIG, 9999, '', '', '', ''); 98 | await poolServer.start(); 99 | 100 | let fixFakeTime = 0; 101 | const time = new Nimiq.Time(); 102 | spyOn(time, 'now').and.callFake(() => fixFakeTime); 103 | 104 | const poolAgent = new PoolAgent(poolServer, { 105 | close: () => {}, 106 | send: async (json) => { 107 | const msg = JSON.parse(json); 108 | if (msg.message === 'settings') { 109 | const poolAddress = Nimiq.Address.fromUserFriendlyAddress(msg.address); 110 | const extraData = Nimiq.BufferUtils.fromBase64(msg.extraData); 111 | const target = parseFloat(msg.target); 112 | 113 | let userId = await poolServer.getStoreUserId(NQ43sampleData.address); 114 | 115 | // valid share 116 | let shareMsg = await generateBlockMessage(poolAddress, extraData, time, target); 117 | await poolAgent._onMessage(shareMsg); 118 | let hash = Nimiq.BlockHeader.unserialize(Nimiq.BufferUtils.fromBase64(shareMsg.blockHeader)).hash(); 119 | expect(await poolServer.containsShare(userId, hash)).toBeTruthy(); 120 | 121 | fixFakeTime = 2000; 122 | shareMsg = await generateBlockMessage(poolAddress, extraData, time, target); 123 | await poolAgent._onMessage(shareMsg); 124 | hash = Nimiq.BlockHeader.unserialize(Nimiq.BufferUtils.fromBase64(shareMsg.blockHeader)).hash(); 125 | expect(await poolServer.containsShare(userId, hash)).toBeTruthy(); 126 | 127 | done(); 128 | } 129 | } 130 | }, Nimiq.NetAddress.fromIP('1.2.3.4')); 131 | await poolAgent._onMessage(NQ43sampleData.register); 132 | })().catch(done.fail); 133 | }); 134 | 135 | it('bans clients with too many invalid shares', (done) => { 136 | (async () => { 137 | const consensus = await Nimiq.Consensus.volatileFull(); 138 | const poolServer = new PoolServer(consensus, POOL_CONFIG, 9999, '', '', '', ''); 139 | await poolServer.start(); 140 | const time = new Nimiq.Time(); 141 | spyOn(time, 'now').and.callFake(() => 0); 142 | 143 | const poolAgent = new PoolAgent(poolServer, { 144 | close: () => { }, 145 | send: async (json) => { 146 | const msg = JSON.parse(json); 147 | if (msg.message === 'settings') { 148 | const extraData = Nimiq.BufferUtils.fromBase64(msg.extraData); 149 | const target = parseFloat(msg.target); 150 | 151 | poolServer.config.allowedErrors = 2; 152 | 153 | expect(poolServer.numIpsBanned).toEqual(0); 154 | for (let i = 0; i < poolServer.config.allowedErrors + 1; i++) { 155 | // wrong miner address, won't be stored and won't ban instantly 156 | shareMsg = await generateBlockMessage(Nimiq.Address.fromUserFriendlyAddress('NQ57 LUAL 6R8F ETD3 VE77 6NK5 HEUK 009H C06B'), extraData, time, target); 157 | await poolAgent._onMessageData(JSON.stringify(shareMsg)); 158 | } 159 | expect(poolServer.numIpsBanned).toEqual(1); 160 | done(); 161 | } 162 | } 163 | }, Nimiq.NetAddress.fromIP('1.2.3.4')); 164 | await poolAgent._onMessage(NQ43sampleData.register); 165 | })().catch(done.fail); 166 | }); 167 | 168 | it('handles payout requests', (done) => { 169 | (async () => { 170 | const keyPair = Nimiq.KeyPair.generate(); 171 | const clientAddress = keyPair.publicKey.toAddress(); 172 | 173 | const consensus = await Nimiq.Consensus.volatileFull(); 174 | const poolServer = new PoolServer(consensus, POOL_CONFIG, 9999, '', 'localhost', '', ''); 175 | await poolServer.start(); 176 | const poolAgent = new PoolAgent(poolServer, { close: () => {}, send: () => {} }, Nimiq.NetAddress.fromIP('1.2.3.4')); 177 | spyOn(poolAgent, '_regenerateNonce').and.callFake(() => { poolAgent._nonce = 42 }); 178 | 179 | const registerMsg = { 180 | message: 'register', 181 | address: clientAddress.toUserFriendlyAddress(), 182 | deviceId: 111111111, 183 | mode: 'smart', 184 | genesisHash: Nimiq.BufferUtils.toBase64(Nimiq.GenesisConfig.GENESIS_HASH.serialize()) 185 | }; 186 | await poolAgent._onMessage(registerMsg); 187 | 188 | async function sendSignedPayoutRequest(usedKeyPair) { 189 | let buf = new Nimiq.SerialBuffer(8 + PoolAgent.PAYOUT_NONCE_PREFIX.length); 190 | buf.writeString(PoolAgent.PAYOUT_NONCE_PREFIX, PoolAgent.PAYOUT_NONCE_PREFIX.length); 191 | buf.writeUint64(42); 192 | let signature = Nimiq.Signature.create(usedKeyPair.privateKey, usedKeyPair.publicKey, buf); 193 | return Nimiq.SignatureProof.singleSig(usedKeyPair.publicKey, signature); 194 | } 195 | 196 | const connection = await mysql.createConnection({ host: 'localhost', user: 'root', password: 'root', database: 'pool', multipleStatements: true }); 197 | 198 | // garbage signature 199 | let request = { message: 'payout', proof: 'AAAAAAAAAAAAAAAAAAaaaaaaaa' }; 200 | await poolAgent._onMessageData(JSON.stringify(request)); 201 | 202 | let userId = await poolServer.getStoreUserId(clientAddress); 203 | let [rows, fields] = await connection.execute('SELECT * FROM payout_request WHERE user=?', [userId]); 204 | expect(rows.length).toEqual(0); 205 | 206 | // invalid signature 207 | signatureProof = await sendSignedPayoutRequest(Nimiq.KeyPair.generate()); 208 | request = { message: 'payout', proof: Nimiq.BufferUtils.toBase64(signatureProof.serialize()) }; 209 | await poolAgent._onMessageData(JSON.stringify(request)); 210 | 211 | userId = await poolServer.getStoreUserId(clientAddress); 212 | [rows, fields] = await connection.execute('SELECT * FROM payout_request WHERE user=?', [userId]); 213 | expect(rows.length).toEqual(0); 214 | 215 | // valid signature 216 | signatureProof = await sendSignedPayoutRequest(keyPair); 217 | request = { message: 'payout', proof: Nimiq.BufferUtils.toBase64(signatureProof.serialize()) }; 218 | await poolAgent._onMessage(request); 219 | 220 | userId = await poolServer.getStoreUserId(clientAddress); 221 | [rows, fields] = await connection.execute('SELECT * FROM payout_request WHERE user=?', [userId]); 222 | expect(rows.length).toEqual(1); 223 | 224 | done(); 225 | })().catch(done.fail); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /spec/PoolPayout.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const mysql = require('mysql2/promise'); 3 | 4 | const Nimiq = require('@nimiq/core'); 5 | 6 | const PoolPayout = require('../src/PoolPayout.js'); 7 | 8 | describe('PoolPayout', () => { 9 | 10 | xit('processes payins', (done) => { 11 | (async () => { 12 | const connection = await mysql.createConnection({ host: 'localhost', user: 'root', password: 'root', database: 'pool', multipleStatements: true }); 13 | await connection.execute('INSERT INTO block (id, hash, height) VALUES (?, ?, ?)', [1, 'a', 1]); 14 | await connection.execute('INSERT INTO block (id, hash, height) VALUES (?, ?, ?)', [2, 'b', 2]); 15 | 16 | await connection.execute('INSERT INTO user (id, address) VALUES (?, ?)', [1, Nimiq.Address.fromUserFriendlyAddress('NQ25 FGPF A68A TBQ4 7KUU 3TFG 418D 1J49 HRLN').toBase64()]); 17 | await connection.execute('INSERT INTO user (id, address) VALUES (?, ?)', [2, Nimiq.Address.fromUserFriendlyAddress('NQ43 SXSE XAS0 HYXJ M1U4 DCJ3 0SXE 8KUH 5DU7').toBase64()]); 18 | 19 | await connection.execute('INSERT INTO payin (user, amount, datetime, block) VALUES (?, ?, ?, ?)', [1, 5 * Nimiq.Policy.SATOSHIS_PER_COIN, Date.now(), 1]); 20 | await connection.execute('INSERT INTO payin (user, amount, datetime, block) VALUES (?, ?, ?, ?)', [2, 4 * Nimiq.Policy.SATOSHIS_PER_COIN, Date.now(), 1]); 21 | 22 | await connection.execute('INSERT INTO payin (user, amount, datetime, block) VALUES (?, ?, ?, ?)', [1, 4 * Nimiq.Policy.SATOSHIS_PER_COIN, Date.now(), 2]); 23 | await connection.execute('INSERT INTO payin (user, amount, datetime, block) VALUES (?, ?, ?, ?)', [2, 12 * Nimiq.Policy.SATOSHIS_PER_COIN, Date.now(), 2]); 24 | 25 | POOL_CONFIG.payoutConfirmations = 4; 26 | 27 | const consensus = await Nimiq.Consensus.volatileFull(); 28 | await consensus.blockchain.pushBlock(ChainSampleData.block1); 29 | await consensus.blockchain.pushBlock(ChainSampleData.block2); 30 | await consensus.blockchain.pushBlock(ChainSampleData.block3); 31 | await consensus.blockchain.pushBlock(ChainSampleData.block4); 32 | 33 | const walletStore = await new Nimiq.WalletStore(); 34 | const wallet = await walletStore.getDefault(); 35 | const poolPayout = new PoolPayout(consensus, wallet, POOL_CONFIG); 36 | await poolPayout.start(); 37 | await poolPayout._processPayouts(); 38 | 39 | let [rows, fields] = await connection.execute('SELECT * FROM payout'); 40 | expect(rows.length).toEqual(2); 41 | [rows, fields] = await connection.execute('SELECT * FROM payout WHERE user=?', [1]); 42 | expect(rows.length).toEqual(1); 43 | expect(rows[0].amount).toEqual((5) * Nimiq.Policy.SATOSHIS_PER_COIN); 44 | [rows, fields] = await connection.execute('SELECT * FROM payout WHERE user=?', [2]); 45 | expect(rows.length).toEqual(1); 46 | expect(rows[0].amount).toEqual((4) * Nimiq.Policy.SATOSHIS_PER_COIN); 47 | 48 | await consensus.blockchain.pushBlock(ChainSampleData.block5); 49 | 50 | await poolPayout._processPayouts(); 51 | 52 | [rows, fields] = await connection.execute('SELECT * FROM payout'); 53 | expect(rows.length).toEqual(4); 54 | [rows, fields] = await connection.execute('SELECT sum(amount) as sum FROM payout WHERE user=?', [1]); 55 | expect(rows[0].sum).toEqual((5+4) * Nimiq.Policy.SATOSHIS_PER_COIN); 56 | [rows, fields] = await connection.execute('SELECT sum(amount) as sum FROM payout WHERE user=?', [2]); 57 | expect(rows[0].sum).toEqual((4+12) * Nimiq.Policy.SATOSHIS_PER_COIN); 58 | 59 | done(); 60 | })().catch(done.fail); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /spec/PoolService.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const mysql = require('mysql2/promise'); 3 | 4 | const Nimiq = require('@nimiq/core'); 5 | 6 | const PoolAgent = require('../src/PoolAgent.js'); 7 | const PoolServer = require('../src/PoolServer.js'); 8 | const PoolService = require('../src/PoolService.js'); 9 | 10 | describe('PoolService', () => { 11 | 12 | beforeEach(() => { 13 | spyOn(PoolServer, 'createServer').and.callFake(() => { 14 | return { 15 | on: () => {}, 16 | close: () => {} 17 | }; 18 | }); 19 | }); 20 | 21 | it('computes payins', (done) => { 22 | (async () => { 23 | const consensus = await Nimiq.Consensus.volatileFull(); 24 | const poolServer = new PoolServer(consensus, POOL_CONFIG, 9999, '', '', '', ''); 25 | await poolServer.start(); 26 | 27 | let poolAgent = new PoolAgent(poolServer, { close: () => {}, send: () => {}, _socket: { remoteAddress: '1.2.3.4' } }); 28 | await poolAgent._onRegisterMessage(NQ25sampleData.register); 29 | 30 | poolAgent = new PoolAgent(poolServer, { close: () => {}, send: () => {}, _socket: { remoteAddress: '1.2.3.4' } }); 31 | await poolAgent._onRegisterMessage(NQ43sampleData.register); 32 | 33 | const poolService = new PoolService(consensus, POOL_CONFIG); 34 | await poolService.start(); 35 | 36 | // console.log(await consensus.blockchain.pushBlock(ChainSampleData.block1)); 37 | await poolService._distributePayinsForBlock(ChainSampleData.block1); 38 | done(); 39 | })().catch(done.fail); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /spec/spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const mysql = require('mysql2/promise'); 3 | const Nimiq = require('@nimiq/core'); 4 | 5 | NETCONFIG = new Nimiq.WsNetworkConfig('node1.test', 9000, 'key1', 'cert1'); 6 | NETCONFIG._keyPair = Nimiq.KeyPair.fromHex('ab05e735f870ff4482a997eab757ea78f8a83356ea443ac68969824184b82903a5ea83e7ee0c8c7ad863c3ceffd31a63679e1ea34a5f89e3ae0f90c5d281d4a900'); 7 | 8 | /** @type {PoolConfig} */ 9 | POOL_CONFIG = require('../src/Config.js').DEFAULT_CONFIG.pool; 10 | POOL_CONFIG.name = 'Test Pool'; 11 | POOL_CONFIG.address = 'NQ10 G2P1 GKKY TMUX YLRH BF8D 499N LD9G B1HX'; 12 | 13 | Nimiq.GenesisConfig.CONFIGS['tests'] = { 14 | NETWORK_ID: 4, 15 | NETWORK_NAME: 'tests', 16 | GENESIS_BLOCK: new Nimiq.Block( 17 | new Nimiq.BlockHeader( 18 | new Nimiq.Hash(null), 19 | new Nimiq.Hash(null), 20 | Nimiq.Hash.fromBase64('nVtxMP3RlCdAbx1Hd4jsH4ZsZQsu/1UK+zUFsUNWgbs='), 21 | Nimiq.Hash.fromBase64('v6zYHGQ3Z/O/G/ZCyXtO/TPa7/Kw00HGEzRK5wbu2zg='), 22 | Nimiq.BlockUtils.difficultyToCompact(1), 23 | 1, 24 | 0, 25 | 101720, 26 | Nimiq.BlockHeader.Version.V1), 27 | new Nimiq.BlockInterlink([], new Nimiq.Hash(null)), 28 | new Nimiq.BlockBody(Nimiq.Address.fromBase64('G+RAkZY0pv47pfinGB/ku4ISwTw='), []) 29 | ), 30 | GENESIS_ACCOUNTS: 'AAIP7R94Gl77Xrk4xvszHLBXdCzC9AAAAHKYqT3gAAh2jadJcsL852C50iDDRIdlFjsNAAAAcpipPeAA', 31 | SEED_PEERS: [Nimiq.WsPeerAddress.seed('node1.test', 9000, NETCONFIG.publicKey.toHex())] 32 | }; 33 | Nimiq.GenesisConfig.init(Nimiq.GenesisConfig.CONFIGS['tests']); 34 | 35 | beforeEach((done) => { 36 | (async () => { 37 | try { 38 | let data = fs.readFileSync('./sql/drop.sql', 'utf8'); 39 | connection = await mysql.createConnection({ 40 | host: 'localhost', 41 | user: 'root', 42 | password: 'root', 43 | multipleStatements: true 44 | }); 45 | await connection.query(data); 46 | } catch (e) { 47 | Nimiq.Log.w('Spec', e); 48 | } 49 | 50 | data = fs.readFileSync('./sql/create.sql', 'utf8'); 51 | connection = await mysql.createConnection({ host: 'localhost', user: 'root', password: 'root', multipleStatements: true }); 52 | await connection.query(data); 53 | 54 | done(); 55 | })().catch(done.fail); 56 | }); 57 | 58 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 12000; 59 | 60 | const ChainSampleData = require('./ChainSampleData.spec.js'); 61 | const NQ25sampleData = require('./NQ25sampleData.spec.js'); 62 | const NQ43sampleData = require('./NQ43sampleData.spec.js'); 63 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*.[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "spec.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /sql/create.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE pool; 2 | 3 | CREATE USER 'pool_payout'@'localhost'; 4 | CREATE USER 'pool_service'@'localhost'; 5 | CREATE USER 'pool_server'@'localhost'; 6 | CREATE USER 'pool_info'@'localhost'; 7 | 8 | CREATE TABLE pool.user ( 9 | id INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, 10 | address VARCHAR(64) NOT NULL UNIQUE 11 | ); 12 | CREATE INDEX idx_user_address ON pool.user (address); 13 | 14 | CREATE TABLE pool.block ( 15 | id INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, 16 | hash BINARY(32) NOT NULL UNIQUE, 17 | height INTEGER NOT NULL, 18 | main_chain BOOLEAN NOT NULL DEFAULT false 19 | ); 20 | CREATE INDEX idx_block_hash ON pool.block (hash); 21 | CREATE INDEX idx_block_height ON pool.block (height); 22 | 23 | CREATE TABLE pool.share ( 24 | id INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, 25 | user INTEGER NOT NULL REFERENCES pool.user(id), 26 | device INTEGER UNSIGNED NOT NULL, 27 | datetime BIGINT NOT NULL, 28 | prev_block INTEGER NOT NULL REFERENCES pool.block(id), 29 | difficulty DOUBLE NOT NULL, 30 | hash BINARY(32) NOT NULL UNIQUE 31 | ); 32 | 33 | CREATE INDEX idx_share_prev ON pool.share (prev_block); 34 | CREATE INDEX idx_share_hash ON pool.share (hash); 35 | 36 | CREATE TABLE pool.payin ( 37 | id INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, 38 | user INTEGER NOT NULL REFERENCES pool.user(id), 39 | amount DOUBLE NOT NULL, 40 | datetime BIGINT NOT NULL, 41 | block INTEGER NOT NULL REFERENCES pool.block(id), 42 | UNIQUE(user, block) 43 | ); 44 | 45 | CREATE TABLE pool.payout ( 46 | id INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, 47 | user INTEGER NOT NULL REFERENCES pool.user(id), 48 | amount DOUBLE NOT NULL, 49 | datetime BIGINT NOT NULL, 50 | transaction BINARY(32) 51 | ); 52 | 53 | CREATE TABLE pool.payout_request ( 54 | id INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT, 55 | user INTEGER NOT NULL UNIQUE REFERENCES pool.user(id) 56 | ); 57 | 58 | GRANT SELECT,INSERT ON pool.user TO 'pool_server'@'localhost'; 59 | GRANT SELECT ON pool.user TO 'pool_service'@'localhost'; 60 | GRANT SELECT ON pool.user TO 'pool_payout'@'localhost'; 61 | GRANT SELECT ON pool.user TO 'pool_info'@'localhost'; 62 | 63 | GRANT SELECT,INSERT ON pool.block TO 'pool_server'@'localhost'; 64 | GRANT SELECT,INSERT,UPDATE ON pool.block TO 'pool_service'@'localhost'; 65 | GRANT SELECT ON pool.block TO 'pool_payout'@'localhost'; 66 | GRANT SELECT ON pool.block TO 'pool_info'@'localhost'; 67 | 68 | GRANT SELECT,INSERT ON pool.share TO 'pool_server'@'localhost'; 69 | GRANT SELECT ON pool.share TO 'pool_service'@'localhost'; 70 | GRANT SELECT ON pool.share TO 'pool_info'@'localhost'; 71 | 72 | GRANT SELECT ON pool.payin TO 'pool_server'@'localhost'; 73 | GRANT SELECT,INSERT,DELETE ON pool.payin TO 'pool_service'@'localhost'; 74 | GRANT SELECT ON pool.payin TO 'pool_payout'@'localhost'; 75 | GRANT SELECT ON pool.payin TO 'pool_info'@'localhost'; 76 | 77 | GRANT SELECT ON pool.payout TO 'pool_server'@'localhost'; 78 | GRANT SELECT,INSERT ON pool.payout TO 'pool_service'@'localhost'; 79 | GRANT SELECT,INSERT ON pool.payout TO 'pool_payout'@'localhost'; 80 | GRANT SELECT ON pool.payout TO 'pool_info'@'localhost'; 81 | 82 | GRANT SELECT,INSERT,DELETE ON pool.payout_request TO 'pool_server'@'localhost'; 83 | GRANT SELECT,DELETE ON pool.payout_request TO 'pool_payout'@'localhost'; 84 | GRANT SELECT ON pool.payout_request TO 'pool_info'@'localhost'; 85 | -------------------------------------------------------------------------------- /sql/drop.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE pool; 2 | 3 | DROP USER 'pool_payout'@'localhost'; 4 | DROP USER 'pool_service'@'localhost'; 5 | DROP USER 'pool_server'@'localhost'; 6 | DROP USER 'pool_info'@'localhost'; 7 | -------------------------------------------------------------------------------- /src/Config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const JSON5 = require('json5'); 3 | const merge = require('lodash.merge'); 4 | 5 | const Log = require('@nimiq/core').Log; 6 | const TAG = 'Config'; 7 | 8 | /** 9 | * @typedef {object} PoolConfig 10 | * @property {string} name 11 | * @property {string} address 12 | * @property {number} payoutConfirmations 13 | * @property {number} autoPayOutLimit 14 | * @property {number} poolFee 15 | * @property {number} networkFee 16 | * @property {number} minDifficulty 17 | * @property {number} spsTimeUnit 18 | * @property {number} desiredSps 19 | * @property {number} connectionTimeout 20 | * @property {number} pplnsShares 21 | */ 22 | 23 | /** 24 | * @typedef {object} Config 25 | * @property {string} host 26 | * @property {{cert: string, key: string}} tls 27 | * @property {number} port 28 | * @property {boolean} dumb 29 | * @property {string} type 30 | * @property {string} network 31 | * @property {PoolConfig} pool 32 | * @property {{enabled: boolean, port: number, sslCertPath: string, sslKeyPath: string, mySqlPsw: string, mySqlHost: string}} poolServer 33 | * @property {{enabled: boolean, mySqlPsw: string, mySqlHost: string}} poolService 34 | * @property {{enabled: boolean, mySqlPsw: string, mySqlHost: string}} poolPayout 35 | * @property {{seed: string, address: string}} wallet 36 | * @property {{level: string, tags: object}} log 37 | * @property {Array.<{host: string, port: number, publicKey: string}>} seedPeers 38 | * @property {object} constantOverrides 39 | */ 40 | 41 | const DEFAULT_CONFIG = /** @type {Config} */ { 42 | host: null, 43 | tls: { 44 | cert: null, 45 | key: null 46 | }, 47 | port: 8443, 48 | dumb: false, 49 | type: 'full', 50 | network: 'main', 51 | pool: { 52 | name: null, 53 | address: null, 54 | payoutConfirmations: 10, 55 | autoPayOutLimit: 5000000, // 50 NIM 56 | poolFee: 0.01, // 1% 57 | networkFee: 1, // satoshi per byte 58 | startDifficulty: 1, 59 | minDifficulty: 1, 60 | spsTimeUnit: 60000, // 1 minute 61 | desiredSps: 0.2, // desired shares per second 62 | connectionTimeout: 60 * 1000 * 10, // 10 minutes 63 | pplnsShares: 1000, 64 | allowedErrors: 3 65 | }, 66 | poolServer: { 67 | enabled: false, 68 | port: 8444, 69 | sslCertPath: null, 70 | sslKeyPath: null, 71 | mySqlPsw: null, 72 | mySqlHost: null 73 | }, 74 | poolService: { 75 | enabled: false, 76 | mySqlPsw: null, 77 | mySqlHost: null 78 | }, 79 | poolPayout: { 80 | enabled: false, 81 | mySqlPsw: null, 82 | mySqlHost: null 83 | }, 84 | poolMetricsServer: { 85 | enabled: false, 86 | port: 8650, 87 | password: null 88 | }, 89 | wallet: { 90 | seed: null, 91 | }, 92 | log: { 93 | level: 'info', 94 | tags: {} 95 | }, 96 | seedPeers: [], 97 | constantOverrides: {} 98 | }; 99 | 100 | const CONFIG_TYPES = { 101 | host: 'string', 102 | tls: { 103 | type: 'object', sub: { 104 | cert: 'string', 105 | key: 'string' 106 | } 107 | }, 108 | port: 'number', 109 | dumb: 'boolean', 110 | type: {type: 'string', values: ['full', 'light', 'nano']}, 111 | network: 'string', 112 | statistics: 'number', 113 | pool: { 114 | type: 'object', sub: { 115 | name: 'string', 116 | address: 'string', 117 | payoutConfirmations: 'number', 118 | autoPayOutLimit: 'number', 119 | poolFee: 'number', 120 | networkFee: 'number', 121 | minDifficulty: 'number', 122 | startDifficulty: 'number', 123 | spsTimeUnit: 'number', 124 | desiredSps: 'number', 125 | connectionTimeout: 'number', 126 | pplnsShares: 'number', 127 | allowedErrors: 'number' 128 | } 129 | }, 130 | poolServer: { 131 | type: 'object', sub: { 132 | enabled: 'boolean', 133 | port: 'number', 134 | certPath: 'string', 135 | keyPath: 'string', 136 | mySqlPsw: 'string', 137 | mySqlHost: 'string' 138 | } 139 | }, 140 | poolService: { 141 | type: 'object', sub: { 142 | enabled: 'boolean', 143 | mySqlPsw: 'string', 144 | mySqlHost: 'string' 145 | } 146 | }, 147 | poolPayout: { 148 | type: 'object', sub: { 149 | enabled: 'boolean', 150 | mySqlPsw: 'string', 151 | mySqlHost: 'string' 152 | } 153 | }, 154 | poolMetricsServer: { 155 | type: 'object', sub: { 156 | enabled: 'boolean', 157 | port: 'number', 158 | password: 'string' 159 | } 160 | }, 161 | wallet: { 162 | type: 'object', sub: { 163 | seed: 'string', 164 | } 165 | }, 166 | log: { 167 | type: 'object', sub: { 168 | level: {type: 'string', values: ['trace', 'verbose', 'debug', 'info', 'warning', 'error', 'assert']}, 169 | tags: 'object' 170 | } 171 | }, 172 | seedPeers: { 173 | type: 'array', inner: { 174 | type: 'object', sub: { 175 | host: 'string', 176 | port: 'number', 177 | publicKey: 'string' 178 | } 179 | } 180 | }, 181 | constantOverrides: 'object' 182 | }; 183 | 184 | function validateItemType(config, key, type, error = true) { 185 | let valid = true; 186 | if (typeof type === 'string') { 187 | if (type === 'boolean') { 188 | if (config[key] === 'yes' || config[key] === 1) config[key] = true; 189 | if (config[key] === 'no' || config[key] === 0) config[key] = false; 190 | } 191 | if (type === 'number' && typeof config[key] === 'string') { 192 | if (!isNaN(parseInt(config[key]))) { 193 | Log.i(TAG, `Configuration option '${key}' should be of type 'number', but is of type 'string', will parse it.`); 194 | config[key] = parseInt(config[key]); 195 | } 196 | } 197 | if (type === 'string' && typeof config[key] === 'number') { 198 | Log.i(TAG, `Configuration option '${key}' should be of type 'string', but is of type 'number', will convert it.`); 199 | config[key] = config[key].toString(); 200 | } 201 | if (typeof config[key] !== type) { 202 | if (error) Log.w(TAG, `Configuration option '${key}' is of type '${typeof config[key]}', but '${type}' is required`); 203 | valid = false; 204 | } 205 | } else if (typeof type === 'object') { 206 | if (['string', 'number', 'object'].includes(type.type)) { 207 | if (!validateItemType(config, key, type.type)) { 208 | valid = false; 209 | } 210 | } 211 | if (type.type === 'array') { 212 | if (!Array.isArray(config[key])) { 213 | if (error) Log.w(TAG, `Configuration option '${key}' should be an array.`); 214 | valid = false; 215 | } else if (type.inner) { 216 | for (let i = 0; i < config[key].length; i++) { 217 | if (!validateItemType(config[key], i, type.inner, false)) { 218 | if (error) Log.w(TAG, `Element ${i} of configuration option '${key}' is invalid.`); 219 | valid = false; 220 | } 221 | } 222 | } 223 | } 224 | if (Array.isArray(type.values)) { 225 | if (!type.values.includes(config[key])) { 226 | if (error) Log.w(TAG, `Configuration option '${key}' is '${config[key]}', but must be one of '${type.values.slice(0, type.values.length - 1).join('\', \'')}' or '${type.values[type.values.length - 1]}'.`); 227 | valid = false; 228 | } 229 | } 230 | if (typeof config[key] === 'object' && type.type === 'object' && typeof type.sub === 'object') { 231 | if (!validateObjectType(config[key], type.sub, error)) { 232 | valid = false; 233 | } 234 | } 235 | if (type.type === 'mixed' && Array.isArray(type.types)) { 236 | let subvalid = false; 237 | for (const subtype of type.types) { 238 | if (validateItemType(config, key, subtype, false)) { 239 | subvalid = true; 240 | break; 241 | } 242 | } 243 | if (!subvalid) { 244 | if (error) Log.w(TAG, `Configuration option '${key}' is invalid`); 245 | valid = false; 246 | } 247 | } 248 | } 249 | return valid; 250 | } 251 | 252 | function validateObjectType(config, types = CONFIG_TYPES, error = true) { 253 | let valid = true; 254 | for (const key in types) { 255 | if (!(key in config) || config[key] === undefined || config[key] === null) { 256 | if (typeof types[key] === 'object' && types[key].required) { 257 | if (error) Log.w(TAG, `Required configuration option '${key}' is missing`); 258 | valid = false; 259 | } 260 | continue; 261 | } 262 | if (!validateItemType(config, key, types[key], error)) { 263 | valid = false; 264 | } 265 | } 266 | return valid; 267 | } 268 | 269 | if (!validateObjectType(DEFAULT_CONFIG)) { 270 | throw new Error('Default config is invalid according to type specification.'); 271 | } 272 | 273 | /** 274 | * @param {string} file 275 | * @param {object} oldConfig 276 | * @returns {Config|boolean} 277 | */ 278 | function readFromFile(file, oldConfig = merge({}, DEFAULT_CONFIG)) { 279 | if (typeof file === 'undefined') { 280 | Log.e(TAG, 'No configuration file given'); 281 | return false; 282 | } 283 | try { 284 | const config = JSON5.parse(fs.readFileSync(file)); 285 | if (!validateObjectType(config)) { 286 | Log.e(TAG, `Configuration file ${file} is invalid.`); 287 | return false; 288 | } else { 289 | return merge(oldConfig, config); 290 | } 291 | } catch (e) { 292 | Log.e(TAG, `Failed to read file ${file}: ${e.message}`); 293 | return false; 294 | } 295 | } 296 | 297 | module.exports = readFromFile; 298 | module.exports.DEFAULT_CONFIG = DEFAULT_CONFIG; 299 | -------------------------------------------------------------------------------- /src/Helper.js: -------------------------------------------------------------------------------- 1 | const Nimiq = require('@nimiq/core'); 2 | 3 | class Helper { 4 | 5 | /** 6 | * @param {PoolConfig} config 7 | * @param {Nimiq.Block} block 8 | * @returns {number} 9 | */ 10 | static getPayableBlockReward(config, block) { 11 | return (1 - config.poolFee) * (Nimiq.Policy.blockRewardAt(block.height) + block.transactions.reduce((sum, tx) => sum + tx.fee, 0)); 12 | } 13 | 14 | /** 15 | * @param {PoolConfig} config 16 | * @param {mysql2.Pool} connectionPool 17 | * @param {number} userId 18 | * @param {number} currChainHeight 19 | * @param {boolean} includeVirtual 20 | * @returns {Promise.} 21 | */ 22 | static async getUserBalance(config, connectionPool, userId, currChainHeight, includeVirtual = false) { 23 | const query = ` 24 | SELECT IFNULL(payin_sum, 0) - IFNULL(payout_sum, 0) AS balance 25 | FROM ( 26 | ( 27 | SELECT user, SUM(amount) AS payin_sum 28 | FROM payin p 29 | INNER JOIN block b ON b.id = p.block 30 | WHERE p.user = ? AND b.main_chain = true AND b.height <= ? 31 | ) t1 32 | LEFT JOIN 33 | ( 34 | SELECT user, SUM(amount) AS payout_sum 35 | FROM payout 36 | WHERE user = ? 37 | ) t2 38 | ON t2.user = t1.user 39 | )`; 40 | const queryHeight = includeVirtual ? currChainHeight : currChainHeight - config.payoutConfirmations; 41 | const queryArgs = [userId, queryHeight, userId]; 42 | const [rows, fields] = await connectionPool.execute(query, queryArgs); 43 | if (rows.length === 1) { 44 | return rows[0].balance; 45 | } 46 | return 0; 47 | } 48 | 49 | /** 50 | * @param {mysql2.Pool} connectionPool 51 | * @param id 52 | * @returns {Promise.} 53 | */ 54 | static async getUser(connectionPool, id) { 55 | const [rows, fields] = await connectionPool.execute("SELECT address FROM user WHERE id=?", [id]); 56 | return Nimiq.Address.fromBase64(rows[0].address); 57 | } 58 | 59 | /** 60 | * @param {mysql2.Pool} connectionPool 61 | * @param {Nimiq.Address} address 62 | * @returns {Promise.} 63 | */ 64 | static async getUserId(connectionPool, address) { 65 | const [rows, fields] = await connectionPool.execute("SELECT id FROM user WHERE address=?", [address.toBase64()]); 66 | return rows[0].id; 67 | } 68 | 69 | /** 70 | * @param {mysql2.Pool} connectionPool 71 | * @param {Nimiq.Hash} blockHash 72 | * @param {number} height 73 | * @returns {Promise.} 74 | */ 75 | static async getStoreBlockId(connectionPool, blockHash, height) { 76 | await connectionPool.execute("INSERT IGNORE INTO block (hash, height) VALUES (?, ?)", [blockHash.serialize(), height]); 77 | return await Helper.getBlockId(connectionPool, blockHash); 78 | } 79 | 80 | /** 81 | * @param {mysql2.Pool} connectionPool 82 | * @param {Nimiq.Hash} blockHash 83 | * @returns {Promise.} 84 | */ 85 | static async getBlockId(connectionPool, blockHash) { 86 | const [rows, fields] = await connectionPool.execute("SELECT id FROM block WHERE hash=?", [blockHash.serialize()]); 87 | if (rows.length > 0) { 88 | return rows[0].id; 89 | } else { 90 | return -1; 91 | } 92 | } 93 | } 94 | 95 | module.exports = exports = Helper; 96 | -------------------------------------------------------------------------------- /src/MetricsServer.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const https = require('https'); 3 | const btoa = require('btoa'); 4 | const Nimiq = require('@nimiq/core'); 5 | 6 | class MetricsServer { 7 | constructor(sslKeyPath, sslCertPath, port, password) { 8 | 9 | const options = { 10 | key: fs.readFileSync(sslKeyPath), 11 | cert: fs.readFileSync(sslCertPath) 12 | }; 13 | 14 | https.createServer(options, (req, res) => { 15 | if (req.url !== '/metrics') { 16 | res.writeHead(301, {'Location': '/metrics'}); 17 | res.end(); 18 | } else if (password && req.headers.authorization !== `Basic ${btoa(`metrics:${password}`)}`) { 19 | res.writeHead(401, {'WWW-Authenticate': 'Basic realm="Use username metrics and user-defined password to access metrics." charset="UTF-8"'}); 20 | res.end(); 21 | } else { 22 | this._metrics(res); 23 | res.end(); 24 | } 25 | }).listen(port); 26 | 27 | /** @type {Map.} */ 28 | this._messageMeasures = new Map(); 29 | } 30 | 31 | /** 32 | * @param {PoolServer} poolServer 33 | */ 34 | init(poolServer) { 35 | /** @type {PoolServer} */ 36 | this._poolServer = poolServer; 37 | } 38 | 39 | get _desc() { 40 | return { 41 | name: this._poolServer.name 42 | }; 43 | } 44 | 45 | /** 46 | * @param {object} more 47 | * @returns {object} 48 | * @private 49 | */ 50 | _with(more) { 51 | const res = this._desc; 52 | Object.assign(res, more); 53 | return res; 54 | } 55 | 56 | _metrics(res) { 57 | const clientCounts = this._poolServer.getClientModeCounts(); 58 | MetricsServer._metric(res, 'pool_clients', this._with({client: 'unregistered'}), clientCounts.unregistered); 59 | MetricsServer._metric(res, 'pool_clients', this._with({client: 'smart'}), clientCounts.smart); 60 | MetricsServer._metric(res, 'pool_clients', this._with({client: 'nano'}), clientCounts.nano); 61 | 62 | MetricsServer._metric(res, 'pool_ips_banned', this._desc, this._poolServer.numIpsBanned); 63 | MetricsServer._metric(res, 'pool_blocks_mined', this._desc, this._poolServer.numBlocksMined); 64 | MetricsServer._metric(res, 'pool_total_share_difficulty', this._desc, this._poolServer.totalShareDifficulty); 65 | } 66 | 67 | /** 68 | * @param res 69 | * @param {string} key 70 | * @param {object} attributes 71 | * @param {number} value 72 | * @private 73 | */ 74 | static _metric(res, key, attributes, value) { 75 | res.write(`${key}{${Object.keys(attributes).map((a) => `${a}="${attributes[a]}"`).join(',')}} ${value}\n`); 76 | } 77 | } 78 | 79 | module.exports = exports = MetricsServer; 80 | -------------------------------------------------------------------------------- /src/PoolAgent.js: -------------------------------------------------------------------------------- 1 | const Nimiq = require('@nimiq/core'); 2 | 3 | class PoolAgent extends Nimiq.Observable { 4 | constructor(pool, ws, netAddress) { 5 | super(); 6 | 7 | /** @type {PoolServer} */ 8 | this._pool = pool; 9 | 10 | /** @type {WebSocket} */ 11 | this._ws = ws; 12 | this._ws.onmessage = (msg) => this._onMessageData(msg.data); 13 | this._ws.onerror = () => this._onError(); 14 | this._ws.onclose = () => this._onClose(); 15 | 16 | /** @type {Nimiq.NetAddress} */ 17 | this._netAddress = netAddress; 18 | 19 | /** @type {PoolAgent.Mode} */ 20 | this.mode = PoolAgent.Mode.UNREGISTERED; 21 | 22 | /** @type {number} */ 23 | this._difficulty = this._pool.config.startDifficulty; 24 | 25 | /** @type {number} */ 26 | this._sharesSinceReset = 0; 27 | 28 | /** @type {number} */ 29 | this._lastSpsReset = 0; 30 | 31 | /** @type {number} */ 32 | this._errorsSinceReset = 0; 33 | 34 | /** @type {boolean} */ 35 | this._registered = false; 36 | 37 | /** @type {Nimiq.Timers} */ 38 | this._timers = new Nimiq.Timers(); 39 | this._timers.resetTimeout('connection-timeout', () => this._onError(), this._pool.config.connectionTimeout); 40 | } 41 | 42 | /** 43 | * @param {Nimiq.Block} prevBlock 44 | * @param {Array.} transactions 45 | * @param {Array.} prunedAccounts 46 | * @param {Nimiq.Hash} accountsHash 47 | */ 48 | async updateBlock(prevBlock, transactions, prunedAccounts, accountsHash) { 49 | if (this.mode !== PoolAgent.Mode.NANO) return; 50 | if (!prevBlock || !transactions || !prunedAccounts || !accountsHash) return; 51 | 52 | this._currentBody = new Nimiq.BlockBody(this._pool.poolAddress, transactions, this._extraData, prunedAccounts); 53 | const bodyHash = await this._currentBody.hash(); 54 | this._accountsHash = accountsHash; 55 | this._prevBlock = prevBlock; 56 | 57 | this._send({ 58 | message: PoolAgent.MESSAGE_NEW_BLOCK, 59 | bodyHash: Nimiq.BufferUtils.toBase64(bodyHash.serialize()), 60 | accountsHash: Nimiq.BufferUtils.toBase64(accountsHash.serialize()), 61 | previousBlock: Nimiq.BufferUtils.toBase64(prevBlock.serialize()) 62 | }); 63 | this._errorsSinceReset = 0; 64 | } 65 | 66 | async sendBalance() { 67 | this._send({ 68 | message: PoolAgent.MESSAGE_BALANCE, 69 | balance: Math.floor(await this._pool.getUserBalance(this._userId, true)), 70 | confirmedBalance: Math.floor(await this._pool.getUserBalance(this._userId)), 71 | payoutRequestActive: await this._pool.hasPayoutRequest(this._userId) 72 | }); 73 | } 74 | 75 | /** 76 | * @param {string} data 77 | * @private 78 | */ 79 | async _onMessageData(data) { 80 | try { 81 | await this._onMessage(JSON.parse(data)); 82 | } catch (e) { 83 | Nimiq.Log.e(PoolAgent, e); 84 | this._pool.banIp(this._netAddress); 85 | this._ws.close(); 86 | } 87 | } 88 | 89 | /** 90 | * @param {Object} msg 91 | * @private 92 | */ 93 | async _onMessage(msg) { 94 | Nimiq.Log.v(PoolAgent, () => `IN: ${JSON.stringify(msg)}`); 95 | if (msg.message === PoolAgent.MESSAGE_REGISTER) { 96 | await this._onRegisterMessage(msg); 97 | return; 98 | } 99 | 100 | if (!this._registered) { 101 | this._sendError('registration required'); 102 | throw new Error('Client did not register'); 103 | } 104 | 105 | switch (msg.message) { 106 | case PoolAgent.MESSAGE_SHARE: { 107 | if (this.mode === PoolAgent.Mode.NANO) { 108 | await this._onNanoShareMessage(msg); 109 | } else if (this.mode === PoolAgent.Mode.SMART) { 110 | await this._onSmartShareMessage(msg); 111 | } 112 | this._sharesSinceReset++; 113 | if (this._sharesSinceReset > 3 && 1000 * this._sharesSinceReset / Math.abs(Date.now() - this._lastSpsReset) > this._pool.config.desiredSps * 2) { 114 | this._recalcDifficulty(); 115 | } 116 | this._timers.resetTimeout('connection-timeout', () => this._onError(), this._pool.config.connectionTimeout); 117 | break; 118 | } 119 | case PoolAgent.MESSAGE_PAYOUT: { 120 | await this._onPayoutMessage(msg); 121 | break; 122 | } 123 | } 124 | } 125 | 126 | /** 127 | * @param {Object} msg 128 | * @private 129 | */ 130 | async _onRegisterMessage(msg) { 131 | if (this._registered) { 132 | this._sendError('already registered'); 133 | return; 134 | } 135 | 136 | this._address = Nimiq.Address.fromUserFriendlyAddress(msg.address); 137 | this._deviceId = msg.deviceId; 138 | switch (msg.mode) { 139 | case PoolAgent.MODE_SMART: 140 | this.mode = PoolAgent.Mode.SMART; 141 | break; 142 | case PoolAgent.MODE_NANO: 143 | this.mode = PoolAgent.Mode.NANO; 144 | break; 145 | default: 146 | throw new Error('Client did not specify mode'); 147 | } 148 | 149 | const genesisHash = Nimiq.Hash.unserialize(Nimiq.BufferUtils.fromBase64(msg.genesisHash)); 150 | if (!genesisHash.equals(Nimiq.GenesisConfig.GENESIS_HASH)) { 151 | this._sendError('different genesis block'); 152 | throw new Error('Client has different genesis block'); 153 | } 154 | 155 | this._sharesSinceReset = 0; 156 | this._lastSpsReset = Date.now(); 157 | this._timers.resetTimeout('recalc-difficulty', () => this._recalcDifficulty(), this._pool.config.spsTimeUnit); 158 | this._userId = await this._pool.getStoreUserId(this._address); 159 | this._regenerateNonce(); 160 | this._regenerateExtraData(); 161 | 162 | this._registered = true; 163 | this._send({ 164 | message: PoolAgent.MESSAGE_REGISTERED 165 | }); 166 | 167 | this._sendSettings(); 168 | if (this.mode === PoolAgent.Mode.NANO) { 169 | this._pool.requestCurrentHead(this); 170 | } 171 | await this.sendBalance(); 172 | this._timers.resetInterval('send-balance', () => this.sendBalance(), 1000 * 60 * 5); 173 | this._timers.resetInterval('send-keep-alive-ping', () => this._ws.ping(), 1000 * 10); 174 | 175 | Nimiq.Log.i(PoolAgent, `REGISTER ${this._address.toUserFriendlyAddress()}, current balance: ${await this._pool.getUserBalance(this._userId)}`); 176 | } 177 | 178 | /** 179 | * @param {Object} msg 180 | * @private 181 | */ 182 | async _onNanoShareMessage(msg) { 183 | const lightBlock = Nimiq.Block.unserialize(Nimiq.BufferUtils.fromBase64(msg.block)); 184 | const block = lightBlock.toFull(this._currentBody); 185 | const hash = block.hash(); 186 | 187 | const invalidReason = await this._isNanoShareValid(block, hash); 188 | if (invalidReason !== null) { 189 | Nimiq.Log.d(PoolAgent, `INVALID share from ${this._address.toUserFriendlyAddress()} (nano): ${invalidReason}`); 190 | this._sendError('invalid share: ' + invalidReason); 191 | this._countNewError(); 192 | return; 193 | } 194 | 195 | const prevBlock = await this._pool.consensus.blockchain.getBlock(block.prevHash); 196 | const nextTarget = await this._pool.consensus.blockchain.getNextTarget(prevBlock); 197 | if (Nimiq.BlockUtils.isProofOfWork(await block.header.pow(), nextTarget)) { 198 | this._pool.consensus.blockchain.pushBlock(block); 199 | this.fire('block', block.header); 200 | } 201 | 202 | await this._pool.storeShare(this._userId, this._deviceId, block.header.prevHash, block.header.height - 1, this._difficulty, hash); 203 | 204 | Nimiq.Log.v(PoolAgent, () => `SHARE from ${this._address.toUserFriendlyAddress()} (nano), prev ${block.header.prevHash} : ${hash}`); 205 | 206 | this.fire('share', block.header, this._difficulty); 207 | } 208 | 209 | /** 210 | * @param {Nimiq.Block} block 211 | * @param {Nimiq.Hash} hash 212 | * @returns {Promise.} 213 | * @private 214 | */ 215 | async _isNanoShareValid(block, hash) { 216 | // Check if the share was already submitted 217 | if (await this._pool.containsShare(this._userId, hash)) { 218 | throw new Error('Client submitted share twice'); 219 | } 220 | 221 | // Check if the body hash is the one we've sent 222 | if (!block.header.bodyHash.equals(this._currentBody.hash())) { 223 | return 'wrong body hash'; 224 | } 225 | 226 | // Check if the account hash is the one we've sent 227 | if (!block.header._accountsHash.equals(this._accountsHash)) { 228 | return 'wrong accounts hash'; 229 | } 230 | 231 | // Check if the share fulfills the difficulty set for this client 232 | const pow = await block.header.pow(); 233 | if (!Nimiq.BlockUtils.isProofOfWork(pow, Nimiq.BlockUtils.difficultyToTarget(this._difficulty))) { 234 | return 'invalid pow'; 235 | } 236 | 237 | // Check that the timestamp is not too far into the future. 238 | if (block.header.timestamp * 1000 > this._pool.consensus.network.time + Nimiq.Block.TIMESTAMP_DRIFT_MAX * 1000) { 239 | return 'bad timestamp'; 240 | } 241 | 242 | // Verify that the interlink is valid. 243 | if (!block._verifyInterlink()) { 244 | return 'bad interlink'; 245 | } 246 | 247 | if (!(await block.isImmediateSuccessorOf(this._prevBlock))) { 248 | return 'bad prev'; 249 | } 250 | 251 | return null; 252 | } 253 | 254 | /** 255 | * @param {Object} msg 256 | * @private 257 | */ 258 | async _onSmartShareMessage(msg) { 259 | const header = Nimiq.BlockHeader.unserialize(Nimiq.BufferUtils.fromBase64(msg.blockHeader)); 260 | const hash = await header.hash(); 261 | const minerAddrProof = Nimiq.MerklePath.unserialize(Nimiq.BufferUtils.fromBase64(msg.minerAddrProof)); 262 | const extraDataProof = Nimiq.MerklePath.unserialize(Nimiq.BufferUtils.fromBase64(msg.extraDataProof)); 263 | const fullBlock = msg.block ? Nimiq.Block.unserialize(Nimiq.BufferUtils.fromBase64(msg.block)) : null; 264 | 265 | const invalidReason = await this._isSmartShareValid(header, hash, minerAddrProof, extraDataProof, fullBlock); 266 | if (invalidReason !== null) { 267 | Nimiq.Log.d(PoolAgent, `INVALID share from ${this._address.toUserFriendlyAddress()} (smart): ${invalidReason}`); 268 | this._sendError('invalid share: ' + invalidReason); 269 | this._countNewError(); 270 | return; 271 | } 272 | 273 | // If we know a successor of the block mined onto, it does not make sense to mine onto that block anymore 274 | const prevBlock = await this._pool.consensus.blockchain.getBlock(header.prevHash); 275 | if (prevBlock !== null) { 276 | const successors = await this._pool.consensus.blockchain.getSuccessorBlocks(prevBlock, true); 277 | if (successors.length > 0) { 278 | this._sendError('share expired'); 279 | return; 280 | } 281 | } 282 | 283 | const nextTarget = await this._pool.consensus.blockchain.getNextTarget(prevBlock); 284 | if (Nimiq.BlockUtils.isProofOfWork(await header.pow(), nextTarget)) { 285 | if (fullBlock && (await this._pool.consensus.blockchain.pushBlock(fullBlock)) === Nimiq.FullChain.ERR_INVALID) { 286 | this._sendError('invalid block'); 287 | throw new Error('Client sent invalid block'); 288 | } 289 | 290 | this.fire('block', header); 291 | } 292 | 293 | await this._pool.storeShare(this._userId, this._deviceId, header.prevHash, header.height - 1, this._difficulty, hash); 294 | 295 | Nimiq.Log.v(PoolAgent, () => `SHARE from ${this._address.toUserFriendlyAddress()} (smart), prev ${header.prevHash} : ${hash}`); 296 | 297 | this.fire('share', header, this._difficulty); 298 | } 299 | 300 | /** 301 | * @param {Nimiq.BodyHeader} header 302 | * @param {Nimiq.Hash} hash 303 | * @param {Nimiq.MerklePath} minerAddrProof 304 | * @param {Nimiq.MerklePath} extraDataProof 305 | * @param {Nimiq.Block} fullBlock 306 | * @returns {Promise.} 307 | * @private 308 | */ 309 | async _isSmartShareValid(header, hash, minerAddrProof, extraDataProof, fullBlock) { 310 | // Check if the share was already submitted 311 | if (await this._pool.containsShare(this._userId, hash)) { 312 | throw new Error('Client submitted share twice'); 313 | } 314 | 315 | // Check if we are the _miner or the share 316 | if (!(await minerAddrProof.computeRoot(this._pool.poolAddress)).equals(header.bodyHash)) { 317 | return 'miner address mismatch'; 318 | } 319 | 320 | // Check if the extra data is in the share 321 | if (!(await extraDataProof.computeRoot(this._extraData)).equals(header.bodyHash)) { 322 | return 'extra data mismatch'; 323 | } 324 | 325 | // Check that the timestamp is not too far into the future. 326 | if (header.timestamp * 1000 > this._pool.consensus.network.time + Nimiq.Block.TIMESTAMP_DRIFT_MAX * 1000) { 327 | return 'bad timestamp'; 328 | } 329 | 330 | // Check if the share fulfills the difficulty set for this client 331 | const pow = await header.pow(); 332 | if (!Nimiq.BlockUtils.isProofOfWork(pow, Nimiq.BlockUtils.difficultyToTarget(this._difficulty))) { 333 | return 'invalid pow'; 334 | } 335 | 336 | // Check if the full block matches the header. 337 | if (fullBlock && !hash.equals(fullBlock.hash())) { 338 | throw new Error('full block announced but mismatches') 339 | } 340 | 341 | return null; 342 | } 343 | 344 | /** 345 | * @param {Object} msg 346 | * @private 347 | */ 348 | async _onPayoutMessage(msg) { 349 | const proofValid = await this._verifyProof(Nimiq.BufferUtils.fromBase64(msg.proof), PoolAgent.PAYOUT_NONCE_PREFIX); 350 | if (proofValid) { 351 | await this._pool.storePayoutRequest(this._userId); 352 | this._regenerateNonce(); 353 | this._sendSettings(); 354 | } else { 355 | throw new Error('Client provided invalid proof for payout request'); 356 | } 357 | } 358 | 359 | /** 360 | * @param {Nimiq.SerialBuffer} msgProof 361 | * @param {string} prefix 362 | * @returns {Promise.} 363 | * @private 364 | */ 365 | async _verifyProof(msgProof, prefix) { 366 | const proof = Nimiq.SignatureProof.unserialize(msgProof); 367 | const buf = new Nimiq.SerialBuffer(8 + prefix.length); 368 | buf.writeString(prefix, prefix.length); 369 | buf.writeUint64(this._nonce); 370 | return await proof.verify(this._address, buf); 371 | } 372 | 373 | /** 374 | * To reduce network traffic, we set the minimum share difficulty for a user according to their number of shares in the last SPS_TIME_UNIT 375 | */ 376 | _recalcDifficulty() { 377 | this._timers.clearTimeout('recalc-difficulty'); 378 | const sharesPerMinute = 1000 * this._sharesSinceReset / Math.abs(Date.now() - this._lastSpsReset); 379 | Nimiq.Log.d(PoolAgent, `SPS for ${this._address.toUserFriendlyAddress()}: ${sharesPerMinute.toFixed(2)} at difficulty ${this._difficulty}`); 380 | if (sharesPerMinute / this._pool.config.desiredSps > 2) { 381 | this._difficulty = Math.round(this._difficulty * 1.2 * 1000) / 1000; 382 | this._regenerateExtraData(); 383 | this._sendSettings(); 384 | } else if (sharesPerMinute === 0 || this._pool.config.desiredSps / sharesPerMinute > 2) { 385 | this._difficulty = Math.max(this._pool.config.minDifficulty, Math.round(this._difficulty / 1.2 * 1000) / 1000); 386 | this._regenerateExtraData(); 387 | this._sendSettings(); 388 | } 389 | this._sharesSinceReset = 0; 390 | this._lastSpsReset = Date.now(); 391 | this._timers.resetTimeout('recalc-difficulty', () => this._recalcDifficulty(), this._pool.config.spsTimeUnit); 392 | } 393 | 394 | _countNewError() { 395 | this._errorsSinceReset++; 396 | if (this._errorsSinceReset > this._pool.config.allowedErrors) { 397 | throw new Error('Too many errors'); 398 | } 399 | } 400 | 401 | _sendError(errorString) { 402 | this._send({ 403 | message: PoolAgent.MESSAGE_ERROR, 404 | reason: errorString 405 | }); 406 | } 407 | 408 | _sendSettings() { 409 | const settingsMessage = { 410 | message: PoolAgent.MESSAGE_SETTINGS, 411 | address: this._pool.poolAddress.toUserFriendlyAddress(), 412 | extraData: Nimiq.BufferUtils.toBase64(this._extraData), 413 | target: Nimiq.BlockUtils.difficultyToTarget(this._difficulty), 414 | targetCompact: Nimiq.BlockUtils.difficultyToCompact(this._difficulty), 415 | nonce: this._nonce 416 | }; 417 | this._send(settingsMessage); 418 | this._errorsSinceReset = 0; 419 | } 420 | 421 | _regenerateNonce() { 422 | /** @type {number} */ 423 | this._nonce = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 424 | } 425 | 426 | _regenerateExtraData() { 427 | this._extraData = new Nimiq.SerialBuffer(this._pool.name.length + this._address.serializedSize + 9); 428 | this._extraData.write(Nimiq.BufferUtils.fromAscii(this._pool.name)); 429 | this._extraData.writeUint8(0); 430 | this._address.serialize(this._extraData); 431 | this._extraData.writeUint32(this._deviceId); 432 | this._extraData.writeUint32(Nimiq.BlockUtils.difficultyToCompact(this._difficulty)); 433 | } 434 | 435 | /** 436 | * @param {Object} msg 437 | * @private 438 | */ 439 | _send(msg) { 440 | Nimiq.Log.v(PoolAgent, () => `OUT: ${JSON.stringify(msg)}`); 441 | try { 442 | this._ws.send(JSON.stringify(msg)); 443 | } catch (e) { 444 | Nimiq.Log.e(PoolAgent, e); 445 | this._onError(); 446 | } 447 | } 448 | 449 | _onClose() { 450 | this._offAll(); 451 | 452 | this._timers.clearAll(); 453 | this._pool.removeAgent(this); 454 | } 455 | 456 | _onError() { 457 | this._pool.removeAgent(this); 458 | this._ws.close(); 459 | } 460 | } 461 | PoolAgent.MESSAGE_REGISTER = 'register'; 462 | PoolAgent.MESSAGE_REGISTERED = 'registered'; 463 | PoolAgent.MESSAGE_PAYOUT = 'payout'; 464 | PoolAgent.MESSAGE_SHARE = 'share'; 465 | PoolAgent.MESSAGE_SETTINGS = 'settings'; 466 | PoolAgent.MESSAGE_BALANCE = 'balance'; 467 | PoolAgent.MESSAGE_NEW_BLOCK = 'new-block'; 468 | PoolAgent.MESSAGE_ERROR = 'error'; 469 | 470 | PoolAgent.MODE_NANO = 'nano'; 471 | PoolAgent.MODE_SMART = 'smart'; 472 | 473 | /** @enum {number} */ 474 | PoolAgent.Mode = { 475 | UNREGISTERED: 0, 476 | NANO: 1, 477 | SMART: 2 478 | }; 479 | 480 | PoolAgent.PAYOUT_NONCE_PREFIX = 'POOL_PAYOUT'; 481 | 482 | module.exports = exports = PoolAgent; 483 | -------------------------------------------------------------------------------- /src/PoolPayout.js: -------------------------------------------------------------------------------- 1 | const Nimiq = require('@nimiq/core'); 2 | const mysql = require('mysql2/promise'); 3 | 4 | const Helper = require('./Helper.js'); 5 | 6 | class PoolPayout extends Nimiq.Observable { 7 | /** 8 | * @param {Nimiq.BaseConsensus} consensus 9 | * @param {Nimiq.Wallet} wallet 10 | * @param {PoolConfig} config 11 | * @param {string} mySqlPsw 12 | * @param {string} mySqlHost 13 | */ 14 | constructor(consensus, wallet, config, mySqlPsw, mySqlHost) { 15 | super(); 16 | /** @type {Nimiq.BaseConsensus} */ 17 | this._consensus = consensus; 18 | 19 | /** @type {Nimiq.Wallet} */ 20 | this._wallet = wallet; 21 | 22 | /** @type {PoolConfig} */ 23 | this._config = config; 24 | 25 | /** @type {string} */ 26 | this._mySqlPsw = mySqlPsw; 27 | 28 | /** @type {string} */ 29 | this._mySqlHost = mySqlHost; 30 | 31 | /** @type {Nimiq.Timers} */ 32 | this._timers = new Nimiq.Timers(); 33 | } 34 | 35 | async start() { 36 | this.connectionPool = await mysql.createPool({ 37 | host: this._mySqlHost, 38 | user: 'pool_payout', 39 | password: this._mySqlPsw, 40 | database: 'pool' 41 | }); 42 | this.consensus.on('established', async () => { 43 | await this._processPayouts(); 44 | this._timers.resetTimeout('wait-for-relayed', this._quit.bind(this), 30000); 45 | this.consensus.on('transaction-relayed', (tx) => { 46 | if (tx.sender.equals(this.wallet.address)) { 47 | this._timers.resetTimeout('wait-for-relayed', this._quit.bind(this), 10000); 48 | } 49 | }); 50 | }); 51 | Nimiq.Log.i(PoolPayout, `Starting payout process using address ${this.wallet.address.toUserFriendlyAddress()}`); 52 | } 53 | 54 | _quit() { 55 | Nimiq.Log.i(PoolPayout, 'Finished, exiting now.'); 56 | process.exit(0); 57 | } 58 | 59 | async _processPayouts() { 60 | const payinsValid = await this._validatePayins(); 61 | if (!payinsValid) { 62 | throw new Error('Payin inconsistency'); 63 | } 64 | 65 | const autoPayouts = await this._getAutoPayouts(); 66 | Nimiq.Log.i(PoolPayout, `Processing ${autoPayouts.length} auto payouts`); 67 | for (const payout of autoPayouts) { 68 | await this._payout(payout.userId, payout.userAddress, payout.amount, false); 69 | } 70 | 71 | const payoutRequests = await this._getPayoutRequests(); 72 | Nimiq.Log.i(PoolPayout, `Processing ${payoutRequests.length} payout requests`); 73 | for (const payoutRequest of payoutRequests) { 74 | const balance = await Helper.getUserBalance(this._config, this.connectionPool, payoutRequest.userId, this.consensus.blockchain.height); 75 | await this._payout(payoutRequest.userId, payoutRequest.userAddress, balance, true); 76 | await this._removePayoutRequest(payoutRequest.userId); 77 | } 78 | 79 | if (autoPayouts.length == 0 && payoutRequests.length == 0) { 80 | this._quit(); 81 | } 82 | } 83 | 84 | /** 85 | * @param {number} recipientId 86 | * @param {Nimiq.Address} recipientAddress 87 | * @param {number} amount 88 | * @param {boolean} deductFees 89 | * @private 90 | */ 91 | async _payout(recipientId, recipientAddress, amount, deductFees) { 92 | const fee = 138 * this._config.networkFee; // FIXME: Use from transaction 93 | const txAmount = Math.floor(deductFees ? amount - fee : amount); 94 | if (txAmount > 0) { 95 | Nimiq.Log.i(PoolPayout, `PAYING ${Nimiq.Policy.satoshisToCoins(txAmount)} NIM to ${recipientAddress.toUserFriendlyAddress()}`); 96 | const tx = this.wallet.createTransaction(recipientAddress, txAmount, fee, this.consensus.blockchain.height); 97 | await this._storePayout(recipientId, amount, Date.now(), tx.hash()); 98 | await this.consensus.mempool.pushTransaction(tx); 99 | 100 | // TODO remove payouts that are never mined into a block 101 | } 102 | } 103 | 104 | /** 105 | * @returns {Promise.>} 106 | * @private 107 | */ 108 | async _getAutoPayouts() { 109 | const query = ` 110 | SELECT user.id AS user_id, user.address AS user_address, IFNULL(payin_sum, 0) AS payin_sum, IFNULL(payout_sum, 0) AS payout_sum 111 | FROM ( 112 | ( 113 | SELECT user, SUM(amount) AS payin_sum 114 | FROM payin 115 | INNER JOIN block ON block.id = payin.block 116 | WHERE block.main_chain = true AND block.height <= ? 117 | GROUP BY payin.user 118 | ) t1 119 | LEFT JOIN 120 | ( 121 | SELECT user, SUM(amount) AS payout_sum 122 | FROM payout 123 | GROUP BY user 124 | ) t2 125 | ON t2.user = t1.user 126 | LEFT JOIN user ON user.id = t1.user 127 | ) 128 | WHERE payin_sum - IFNULL(payout_sum, 0) > ?`; 129 | const blocksConfirmedHeight = this.consensus.blockchain.height - this._config.payoutConfirmations; 130 | const queryArgs = [blocksConfirmedHeight, this._config.autoPayOutLimit]; 131 | const [rows, fields] = await this.connectionPool.execute(query, queryArgs); 132 | 133 | const ret = []; 134 | for (const row of rows) { 135 | ret.push({ 136 | userAddress: Nimiq.Address.fromBase64(row.user_address), 137 | userId: row.user_id, 138 | amount: row.payin_sum - row.payout_sum 139 | }); 140 | } 141 | return ret; 142 | } 143 | 144 | /** 145 | * @returns {Promise.>} 146 | * @private 147 | */ 148 | async _getPayoutRequests() { 149 | const query = ` 150 | SELECT user, address 151 | FROM payout_request 152 | LEFT JOIN user ON payout_request.user = user.id`; 153 | const [rows, fields] = await this.connectionPool.execute(query); 154 | 155 | let ret = []; 156 | for (const row of rows) { 157 | ret.push({ 158 | userAddress: Nimiq.Address.fromBase64(row.address), 159 | userId: row.user 160 | }); 161 | } 162 | return ret; 163 | } 164 | 165 | /** 166 | * @param {number} userId 167 | * @private 168 | */ 169 | async _removePayoutRequest(userId) { 170 | const query = `DELETE FROM payout_request WHERE user=?`; 171 | const queryArgs = [userId]; 172 | await this.connectionPool.execute(query, queryArgs); 173 | } 174 | 175 | async _validatePayins() { 176 | const query = ` 177 | SELECT block.hash AS hash, SUM(payin.amount) AS payin_sum 178 | FROM payin 179 | INNER JOIN block ON block.id = payin.block 180 | WHERE block.main_chain = true 181 | GROUP BY block.hash`; 182 | const [rows, fields] = await this.connectionPool.execute(query); 183 | 184 | for (const row of rows) { 185 | const hashBuf = new Nimiq.SerialBuffer(Uint8Array.from(row.hash)); 186 | const hash = Nimiq.Hash.unserialize(hashBuf); 187 | const block = await this.consensus.blockchain.getBlock(hash, false, true); 188 | if (!block.minerAddr.equals(this.wallet.address)) { 189 | Nimiq.Log.e(PoolPayout, `Wrong miner address in block ${block.hash()}`); 190 | return false; 191 | } 192 | const payableBlockReward = Helper.getPayableBlockReward(this._config, block); 193 | if (row.payin_sum > payableBlockReward) { 194 | Nimiq.Log.e(PoolPayout, `Stored payins are greater than the payable block reward for block ${block.hash()}`); 195 | return false; 196 | } 197 | } 198 | return true; 199 | } 200 | 201 | /** 202 | * @param {number} userId 203 | * @param {number} amount 204 | * @param {number} datetime 205 | * @param {Nimiq.Hash} transactionHash 206 | * @returns {Promise.} 207 | * @private 208 | */ 209 | async _storePayout(userId, amount, datetime, transactionHash) { 210 | const query = 'INSERT INTO payout (user, amount, datetime, transaction) VALUES (?, ?, ?, ?)'; 211 | const queryArgs = [userId, amount, datetime, transactionHash.serialize()]; 212 | await this.connectionPool.execute(query, queryArgs); 213 | } 214 | 215 | /** 216 | * @type {Nimiq.Wallet} 217 | * */ 218 | get wallet() { 219 | return this._wallet; 220 | } 221 | 222 | /** 223 | * @type {Nimiq.BaseConsensus} 224 | * */ 225 | get consensus() { 226 | return this._consensus; 227 | } 228 | } 229 | 230 | module.exports = exports = PoolPayout; 231 | -------------------------------------------------------------------------------- /src/PoolServer.js: -------------------------------------------------------------------------------- 1 | const Nimiq = require('@nimiq/core'); 2 | const https = require('https'); 3 | const WebSocket = require('uws'); 4 | const mysql = require('mysql2/promise'); 5 | const fs = require('fs'); 6 | 7 | const PoolAgent = require('./PoolAgent.js'); 8 | const Helper = require('./Helper.js'); 9 | 10 | class PoolServer extends Nimiq.Observable { 11 | /** 12 | * @param {Nimiq.FullConsensus} consensus 13 | * @param {PoolConfig} config 14 | * @param {number} port 15 | * @param {string} mySqlPsw 16 | * @param {string} mySqlHost 17 | * @param {string} sslKeyPath 18 | * @param {string} sslCertPath 19 | */ 20 | constructor(consensus, config, port, mySqlPsw, mySqlHost, sslKeyPath, sslCertPath) { 21 | super(); 22 | 23 | /** @type {Nimiq.FullConsensus} */ 24 | this._consensus = consensus; 25 | 26 | /** @type {string} */ 27 | this.name = config.name; 28 | 29 | /** @type {Nimiq.Address} */ 30 | this.poolAddress = Nimiq.Address.fromUserFriendlyAddress(config.address); 31 | 32 | /** @type {PoolConfig} */ 33 | this._config = config; 34 | 35 | /** @type {number} */ 36 | this.port = port; 37 | 38 | /** @type {string} */ 39 | this._mySqlPsw = mySqlPsw; 40 | 41 | /** @type {string} */ 42 | this._mySqlHost = mySqlHost; 43 | 44 | /** @type {string} */ 45 | this._sslKeyPath = sslKeyPath; 46 | 47 | /** @type {string} */ 48 | this._sslCertPath = sslCertPath; 49 | 50 | /** @type {Nimiq.Miner} */ 51 | this._miner = new Nimiq.Miner(consensus.blockchain, consensus.blockchain.accounts, consensus.mempool, consensus.network.time, this.poolAddress); 52 | 53 | /** @type {Set.} */ 54 | this._agents = new Set(); 55 | 56 | /** @type {Nimiq.HashMap.} */ 57 | this._bannedIPv4IPs = new Nimiq.HashMap(); 58 | 59 | /** @type {Nimiq.HashMap.} */ 60 | this._bannedIPv6IPs = new Nimiq.HashMap(); 61 | 62 | /** @type {number} */ 63 | this._numBlocksMined = 0; 64 | 65 | /** @type {number} */ 66 | this._totalShareDifficulty = 0; 67 | 68 | /** @type {number} */ 69 | this._lastShareDifficulty = 0; 70 | 71 | /** @type {number[]} */ 72 | this._hashrates = []; 73 | 74 | /** @type {number} */ 75 | this._averageHashrate = 0; 76 | 77 | /** @type {boolean} */ 78 | this._started = false; 79 | 80 | setInterval(() => this._checkUnbanIps(), PoolServer.UNBAN_IPS_INTERVAL); 81 | 82 | setInterval(() => this._calculateHashrate(), PoolServer.HASHRATE_INTERVAL); 83 | 84 | this.consensus.on('established', () => this.start()); 85 | } 86 | 87 | async start() { 88 | if (this._started) return; 89 | this._started = true; 90 | 91 | this._currentLightHead = this.consensus.blockchain.head.toLight(); 92 | await this._updateTransactions(); 93 | 94 | this.connectionPool = await mysql.createPool({ 95 | host: this._mySqlHost, 96 | user: 'pool_server', 97 | password: this._mySqlPsw, 98 | database: 'pool' 99 | }); 100 | 101 | this._wss = PoolServer.createServer(this.port, this._sslKeyPath, this._sslCertPath); 102 | this._wss.on('connection', (ws, req) => this._onConnection(ws, req)); 103 | 104 | this.consensus.blockchain.on('head-changed', (head) => this._announceHeadToNano(head)); 105 | } 106 | 107 | static createServer(port, sslKeyPath, sslCertPath) { 108 | const sslOptions = { 109 | key: fs.readFileSync(sslKeyPath), 110 | cert: fs.readFileSync(sslCertPath) 111 | }; 112 | const httpsServer = https.createServer(sslOptions, (req, res) => { 113 | res.writeHead(200); 114 | res.end('Nimiq Pool Server\n'); 115 | }).listen(port); 116 | 117 | // We have to access socket.remoteAddress here because otherwise req.connection.remoteAddress won't be set in the WebSocket's 'connection' event (yay) 118 | httpsServer.on('secureConnection', socket => socket.remoteAddress); 119 | 120 | Nimiq.Log.i(PoolServer, "Started server on port " + port); 121 | return new WebSocket.Server({server: httpsServer}); 122 | } 123 | 124 | stop() { 125 | if (this._wss) { 126 | this._wss.close(); 127 | } 128 | } 129 | 130 | /** 131 | * @param {WebSocket} ws 132 | * @param {http.IncomingMessage} req 133 | * @private 134 | */ 135 | _onConnection(ws, req) { 136 | try { 137 | const netAddress = Nimiq.NetAddress.fromIP(req.connection.remoteAddress); 138 | if (this._isIpBanned(netAddress)) { 139 | Nimiq.Log.i(PoolServer, `Banned IP tried to connect ${netAddress}`); 140 | ws.close(); 141 | } else { 142 | const agent = new PoolAgent(this, ws, netAddress); 143 | agent.on('share', (header, difficulty) => this._onShare(header, difficulty)); 144 | agent.on('block', (header) => this._onBlock(header)); 145 | this._agents.add(agent); 146 | } 147 | } catch (e) { 148 | Nimiq.Log.e(PoolServer, e); 149 | ws.close(); 150 | } 151 | } 152 | 153 | /** 154 | * @param {Nimiq.BlockHeader} header 155 | * @param {number} difficulty 156 | * @private 157 | */ 158 | _onShare(header, difficulty) { 159 | this._totalShareDifficulty += difficulty; 160 | } 161 | 162 | /** 163 | * @param {BlockHeader} header 164 | * @private 165 | */ 166 | _onBlock(header) { 167 | this._numBlocksMined++; 168 | } 169 | 170 | /** 171 | * @param {PoolAgent} agent 172 | */ 173 | requestCurrentHead(agent) { 174 | agent.updateBlock(this._currentLightHead, this._nextTransactions, this._nextPrunedAccounts, this._nextAccountsHash); 175 | } 176 | 177 | /** 178 | * @param {Nimiq.BlockHead} head 179 | * @private 180 | */ 181 | async _announceHeadToNano(head) { 182 | this._currentLightHead = head.toLight(); 183 | await this._updateTransactions(); 184 | this._announceNewNextToNano(); 185 | } 186 | 187 | async _updateTransactions() { 188 | try { 189 | const block = await this._miner.getNextBlock(); 190 | this._nextTransactions = block.body.transactions; 191 | this._nextPrunedAccounts = block.body.prunedAccounts; 192 | this._nextAccountsHash = block.header._accountsHash; 193 | } catch(e) { 194 | setTimeout(() => this._updateTransactions(), 100); 195 | } 196 | } 197 | 198 | _announceNewNextToNano() { 199 | for (const poolAgent of this._agents.values()) { 200 | if (poolAgent.mode === PoolAgent.Mode.NANO) { 201 | poolAgent.updateBlock(this._currentLightHead, this._nextTransactions, this._nextPrunedAccounts, this._nextAccountsHash); 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * @param {Nimiq.NetAddress} netAddress 208 | */ 209 | banIp(netAddress) { 210 | if (!netAddress.isPrivate()) { 211 | Nimiq.Log.i(PoolServer, `Banning IP ${netAddress}`); 212 | if (netAddress.isIPv4()) { 213 | this._bannedIPv4IPs.put(netAddress, Date.now() + PoolServer.DEFAULT_BAN_TIME); 214 | } else if (netAddress.isIPv6()) { 215 | // Ban IPv6 IPs prefix based 216 | this._bannedIPv6IPs.put(netAddress.ip.subarray(0,8), Date.now() + PoolServer.DEFAULT_BAN_TIME); 217 | } 218 | } 219 | } 220 | 221 | /** 222 | * @param {Nimiq.NetAddress} netAddress 223 | * @returns {boolean} 224 | * @private 225 | */ 226 | _isIpBanned(netAddress) { 227 | if (netAddress.isPrivate()) return false; 228 | if (netAddress.isIPv4()) { 229 | return this._bannedIPv4IPs.contains(netAddress); 230 | } else if (netAddress.isIPv6()) { 231 | const prefix = netAddress.ip.subarray(0, 8); 232 | return this._bannedIPv6IPs.contains(prefix); 233 | } 234 | return false; 235 | } 236 | 237 | _checkUnbanIps() { 238 | const now = Date.now(); 239 | for (const netAddress of this._bannedIPv4IPs.keys()) { 240 | if (this._bannedIPv4IPs.get(netAddress) < now) { 241 | this._bannedIPv4IPs.remove(netAddress); 242 | } 243 | } 244 | for (const prefix of this._bannedIPv6IPs.keys()) { 245 | if (this._bannedIPv6IPs.get(prefix) < now) { 246 | this._bannedIPv6IPs.remove(prefix); 247 | } 248 | } 249 | } 250 | 251 | _calculateHashrate() { 252 | if (!this.consensus.established) return; 253 | 254 | const shareDifficulty = this._totalShareDifficulty - this._lastShareDifficulty; 255 | this._lastShareDifficulty = this._totalShareDifficulty; 256 | 257 | const hashrate = shareDifficulty / (PoolServer.HASHRATE_INTERVAL / 1000) * Math.pow(2 ,16); 258 | this._hashrates.push(Math.round(hashrate)); 259 | if (this._hashrates.length > 10) this._hashrates.shift(); 260 | 261 | let hashrateSum = 0; 262 | for (const hr of this._hashrates) { 263 | hashrateSum += hr; 264 | } 265 | this._averageHashrate = hashrateSum / this._hashrates.length; 266 | 267 | Nimiq.Log.d(PoolServer, `Pool hashrate is ${Math.round(this._averageHashrate)} H/s (10 min average)`); 268 | } 269 | 270 | /** 271 | * @param {number} userId 272 | * @param {number} deviceId 273 | * @param {Nimiq.Hash} prevHash 274 | * @param {number} prevHashHeight 275 | * @param {number} difficulty 276 | * @param {Nimiq.Hash} shareHash 277 | */ 278 | async storeShare(userId, deviceId, prevHash, prevHashHeight, difficulty, shareHash) { 279 | let prevHashId = await Helper.getStoreBlockId(this.connectionPool, prevHash, prevHashHeight); 280 | const query = "INSERT INTO share (user, device, datetime, prev_block, difficulty, hash) VALUES (?, ?, ?, ?, ?, ?)"; 281 | const queryArgs = [userId, deviceId, Date.now(), prevHashId, difficulty, shareHash.serialize()]; 282 | await this.connectionPool.execute(query, queryArgs); 283 | } 284 | 285 | /** 286 | * @param {number} user 287 | * @param {string} shareHash 288 | * @returns {boolean} 289 | */ 290 | async containsShare(user, shareHash) { 291 | const query = "SELECT * FROM share WHERE user=? AND hash=?"; 292 | const queryArgs = [user, shareHash.serialize()]; 293 | const [rows, fields] = await this.connectionPool.execute(query, queryArgs); 294 | return rows.length > 0; 295 | } 296 | 297 | /** 298 | * @param {number} userId 299 | * @param {boolean} includeVirtual 300 | * @returns {Promise} 301 | */ 302 | async getUserBalance(userId, includeVirtual = false) { 303 | return await Helper.getUserBalance(this._config, this.connectionPool, userId, this.consensus.blockchain.height, includeVirtual); 304 | } 305 | 306 | /** 307 | * @param {number} userId 308 | */ 309 | async storePayoutRequest(userId) { 310 | const query = "INSERT IGNORE INTO payout_request (user) VALUES (?)"; 311 | const queryArgs = [userId]; 312 | await this.connectionPool.execute(query, queryArgs); 313 | } 314 | 315 | /** 316 | * @param {number} userId 317 | * @returns {Promise.} 318 | */ 319 | async hasPayoutRequest(userId) { 320 | const query = `SELECT * FROM payout_request WHERE user=?`; 321 | const [rows, fields] = await this.connectionPool.execute(query, [userId]); 322 | return rows.length > 0; 323 | } 324 | 325 | /** 326 | * @param {Nimiq.Address} addr 327 | * @returns {Promise.} 328 | */ 329 | async getStoreUserId(addr) { 330 | await this.connectionPool.execute("INSERT IGNORE INTO user (address) VALUES (?)", [addr.toBase64()]); 331 | const [rows, fields] = await this.connectionPool.execute("SELECT id FROM user WHERE address=?", [addr.toBase64()]); 332 | return rows[0].id; 333 | } 334 | 335 | /** 336 | * @param {PoolAgent} agent 337 | */ 338 | removeAgent(agent) { 339 | this._agents.delete(agent); 340 | } 341 | 342 | /** 343 | * @type {{ unregistered: number, smart: number, nano: number}} 344 | */ 345 | getClientModeCounts() { 346 | let unregistered = 0, smart = 0, nano = 0; 347 | for (const agent of this._agents) { 348 | switch (agent.mode) { 349 | case PoolAgent.Mode.SMART: 350 | smart++; 351 | break; 352 | case PoolAgent.Mode.NANO: 353 | nano++; 354 | break; 355 | case PoolAgent.Mode.UNREGISTERED: 356 | unregistered++; 357 | break; 358 | } 359 | } 360 | return { unregistered: unregistered, smart: smart, nano: nano }; 361 | } 362 | 363 | /** 364 | * @type {Nimiq.FullConsensus} 365 | * */ 366 | get consensus() { 367 | return this._consensus; 368 | } 369 | 370 | /** @type {PoolConfig} */ 371 | get config() { 372 | return this._config; 373 | } 374 | 375 | /** 376 | * @type {number} 377 | */ 378 | get numIpsBanned() { 379 | return this._bannedIPv4IPs.length + this._bannedIPv6IPs.length; 380 | } 381 | 382 | /** 383 | * @type {number} 384 | */ 385 | get numBlocksMined() { 386 | return this._numBlocksMined; 387 | } 388 | 389 | /** 390 | * @type {number} 391 | */ 392 | get totalShareDifficulty() { 393 | return this._totalShareDifficulty; 394 | } 395 | 396 | /** 397 | * @type {number} 398 | */ 399 | get averageHashrate() { 400 | return this._averageHashrate; 401 | } 402 | } 403 | PoolServer.DEFAULT_BAN_TIME = 1000 * 60 * 10; // 10 minutes 404 | PoolServer.UNBAN_IPS_INTERVAL = 1000 * 60; // 1 minute 405 | PoolServer.HASHRATE_INTERVAL = 1000 * 60; // 1 minute 406 | 407 | module.exports = exports = PoolServer; 408 | -------------------------------------------------------------------------------- /src/PoolService.js: -------------------------------------------------------------------------------- 1 | const Nimiq = require('@nimiq/core'); 2 | const mysql = require('mysql2/promise'); 3 | 4 | const Helper = require('./Helper.js'); 5 | 6 | class PoolService extends Nimiq.Observable { 7 | /** 8 | * @param {Nimiq.BaseConsensus} consensus 9 | * @param {PoolConfig} config 10 | * @param {string} mySqlPsw 11 | * @param {string} mySqlHost 12 | */ 13 | constructor(consensus, config, mySqlPsw, mySqlHost) { 14 | super(); 15 | 16 | /** @type {Nimiq.BaseConsensus} */ 17 | this._consensus = consensus; 18 | 19 | /** @type {Nimiq.Address} */ 20 | this.poolAddress = Nimiq.Address.fromUserFriendlyAddress(config.address); 21 | 22 | /** @type {PoolConfig} */ 23 | this._config = config; 24 | 25 | /** @type {string} */ 26 | this._mySqlPsw = mySqlPsw; 27 | 28 | /** @type {string} */ 29 | this._mySqlHost = mySqlHost; 30 | 31 | /** @type {Nimiq.Synchronizer} */ 32 | this._synchronizer = new Nimiq.Synchronizer(); 33 | } 34 | 35 | async start() { 36 | this.connectionPool = await mysql.createPool({ 37 | host: this._mySqlHost, 38 | user: 'pool_service', 39 | password: this._mySqlPsw, 40 | database: 'pool' 41 | }); 42 | 43 | this.consensus.blockchain.on('head-changed', (head) => this._distributePayinsForBlock(head)); 44 | this.consensus.blockchain.on('head-changed', (head) => this._synchronizer.push(() => this._setBlockOnMainChain(head, head.height, true))); 45 | this.consensus.blockchain.on('block-reverted', (head) => this._synchronizer.push(() => this._setBlockOnMainChain(head, head.height, false))); 46 | } 47 | 48 | /** 49 | * Reward type: Pay Per Last N Shares 50 | * @param {Nimiq.Block} block 51 | * @private 52 | */ 53 | async _distributePayinsForBlock(block) { 54 | Nimiq.Log.d(PoolService, 'Miner addr ' + block.minerAddr.toUserFriendlyAddress() + ' our ' + this.poolAddress.toUserFriendlyAddress()); 55 | if (block.minerAddr.equals(this.poolAddress)) { 56 | const blockId = await Helper.getStoreBlockId(this.connectionPool, block.hash(), block.height); 57 | const [difficultyByAddress, totalDifficulty] = await this._getLastNShares(block, this._config.pplnsShares); 58 | let payableBlockReward = Helper.getPayableBlockReward(this._config, block); 59 | Nimiq.Log.i(PoolService, `Distributing payable value of ${Nimiq.Policy.satoshisToCoins(payableBlockReward)} NIM to ${difficultyByAddress.size} users`); 60 | for (const [addr, difficulty] of difficultyByAddress) { 61 | const userReward = Math.floor(difficulty * payableBlockReward / totalDifficulty); 62 | await this._storePayin(addr, userReward, Date.now(), blockId); 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * @param {Nimiq.Block} lastBlock 69 | * @param {number} n 70 | * @returns {[Map., number]} 71 | * @private 72 | */ 73 | async _getLastNShares(lastBlock, n) { 74 | const ret = new Map(); 75 | const query = ` 76 | SELECT user, SUM(difficulty) AS difficulty_sum 77 | FROM 78 | ( 79 | SELECT user, difficulty 80 | FROM share 81 | INNER JOIN block ON block.id = share.prev_block 82 | WHERE block.main_chain = true AND block.height <= ? 83 | ORDER BY block.height DESC 84 | LIMIT ? 85 | ) t1 86 | GROUP BY user`; 87 | const queryArgs = [lastBlock.height, n]; 88 | const [rows, fields] = await this.connectionPool.execute(query, queryArgs); 89 | 90 | let totalDifficulty = 0; 91 | for (const row of rows) { 92 | const address = await Helper.getUser(this.connectionPool, row.user); 93 | ret.set(address, row.difficulty_sum); 94 | totalDifficulty += row.difficulty_sum; 95 | } 96 | return [ret, totalDifficulty]; 97 | } 98 | 99 | /** 100 | * @param {Nimiq.Address} userAddress 101 | * @param {number} amount 102 | * @param {number} datetime 103 | * @param {number} blockId 104 | * @private 105 | */ 106 | async _storePayin(userAddress, amount, datetime, blockId) { 107 | const userId = await Helper.getUserId(this.connectionPool, userAddress); 108 | 109 | const query = "INSERT INTO payin (user, amount, datetime, block) VALUES (?, ?, ?, ?)"; 110 | const queryArgs = [userId, amount, datetime, blockId]; 111 | await this.connectionPool.execute(query, queryArgs); 112 | } 113 | 114 | /** 115 | * @param {Nimiq.Block} block 116 | * @param {number} height 117 | * @param {boolean} onMainChain 118 | * @private 119 | */ 120 | async _setBlockOnMainChain(block, height, onMainChain) { 121 | const query = ` 122 | INSERT INTO block (hash, height, main_chain) VALUES (?, ?, ?) 123 | ON DUPLICATE KEY UPDATE main_chain=?`; 124 | const queryArgs = [ block.hash().serialize(), block.height, onMainChain, onMainChain ]; 125 | await this.connectionPool.execute(query, queryArgs); 126 | } 127 | 128 | /** 129 | * @type {Nimiq.BaseConsensus} 130 | * */ 131 | get consensus() { 132 | return this._consensus; 133 | } 134 | } 135 | 136 | module.exports = exports = PoolService; 137 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@nimiq/core@^1.1.0": 6 | version "1.1.0" 7 | resolved "https://registry.yarnpkg.com/@nimiq/core/-/core-1.1.0.tgz#d29da86e45a88bfba8703af5915195df32c8526b" 8 | dependencies: 9 | "@nimiq/jungle-db" "^0.9.11" 10 | atob "^2.0.3" 11 | bindings "^1.3.0" 12 | btoa "^1.1.2" 13 | chalk "^2.3.2" 14 | json5 "^1.0.1" 15 | lodash.merge "^4.6.1" 16 | minimist "^1.2.0" 17 | nan "^2.10.0" 18 | uws "^9.14.0" 19 | optionalDependencies: 20 | bufferutil "^3.0.4" 21 | utf-8-validate "^4.0.1" 22 | 23 | "@nimiq/jungle-db@^0.9.11": 24 | version "0.9.11" 25 | resolved "https://registry.yarnpkg.com/@nimiq/jungle-db/-/jungle-db-0.9.11.tgz#bb8fef96bf542e346e6faf8160d6b330ee3ad9c7" 26 | dependencies: 27 | atob "^2.0.3" 28 | btoa "^1.1.2" 29 | fs "^0.0.1-security" 30 | level-sublevel "^6.6.1" 31 | leveldown "^3.0.0" 32 | levelup "^2.0.2" 33 | node-lmdb "0.5.1" 34 | 35 | abstract-leveldown@~0.12.1: 36 | version "0.12.4" 37 | resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-0.12.4.tgz#29e18e632e60e4e221d5810247852a63d7b2e410" 38 | dependencies: 39 | xtend "~3.0.0" 40 | 41 | abstract-leveldown@~4.0.0: 42 | version "4.0.3" 43 | resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-4.0.3.tgz#cb636f4965fbe117f5c8b76a7d51dd42aaed0580" 44 | dependencies: 45 | xtend "~4.0.0" 46 | 47 | ansi-regex@^2.0.0: 48 | version "2.1.1" 49 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 50 | 51 | ansi-styles@^3.2.1: 52 | version "3.2.1" 53 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 54 | dependencies: 55 | color-convert "^1.9.0" 56 | 57 | ansicolors@~0.2.1: 58 | version "0.2.1" 59 | resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" 60 | 61 | aproba@^1.0.3: 62 | version "1.2.0" 63 | resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" 64 | 65 | are-we-there-yet@~1.1.2: 66 | version "1.1.4" 67 | resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" 68 | dependencies: 69 | delegates "^1.0.0" 70 | readable-stream "^2.0.6" 71 | 72 | atob@^2.0.3: 73 | version "2.1.0" 74 | resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.0.tgz#ab2b150e51d7b122b9efc8d7340c06b6c41076bc" 75 | 76 | balanced-match@^1.0.0: 77 | version "1.0.0" 78 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 79 | 80 | bindings@^1.2.1, bindings@^1.3.0, bindings@~1.3.0: 81 | version "1.3.0" 82 | resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" 83 | 84 | bl@^1.0.0: 85 | version "1.2.2" 86 | resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" 87 | dependencies: 88 | readable-stream "^2.3.5" 89 | safe-buffer "^5.1.1" 90 | 91 | bl@~0.8.1: 92 | version "0.8.2" 93 | resolved "https://registry.yarnpkg.com/bl/-/bl-0.8.2.tgz#c9b6bca08d1bc2ea00fc8afb4f1a5fd1e1c66e4e" 94 | dependencies: 95 | readable-stream "~1.0.26" 96 | 97 | brace-expansion@^1.1.7: 98 | version "1.1.11" 99 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 100 | dependencies: 101 | balanced-match "^1.0.0" 102 | concat-map "0.0.1" 103 | 104 | btoa@^1.1.2, btoa@^1.2.1: 105 | version "1.2.1" 106 | resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" 107 | 108 | buffer-alloc-unsafe@^0.1.0: 109 | version "0.1.1" 110 | resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-0.1.1.tgz#ffe1f67551dd055737de253337bfe853dfab1a6a" 111 | 112 | buffer-alloc@^1.1.0: 113 | version "1.1.0" 114 | resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.1.0.tgz#05514d33bf1656d3540c684f65b1202e90eca303" 115 | dependencies: 116 | buffer-alloc-unsafe "^0.1.0" 117 | buffer-fill "^0.1.0" 118 | 119 | buffer-fill@^0.1.0: 120 | version "0.1.1" 121 | resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-0.1.1.tgz#76d825c4d6e50e06b7a31eb520c04d08cc235071" 122 | 123 | bufferutil@^3.0.4: 124 | version "3.0.4" 125 | resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-3.0.4.tgz#b9ea85d3749388110736d564a09ebd7cf6785138" 126 | dependencies: 127 | bindings "~1.3.0" 128 | nan "~2.10.0" 129 | prebuild-install "~2.5.0" 130 | 131 | bytewise-core@^1.2.2: 132 | version "1.2.3" 133 | resolved "https://registry.yarnpkg.com/bytewise-core/-/bytewise-core-1.2.3.tgz#3fb410c7e91558eb1ab22a82834577aa6bd61d42" 134 | dependencies: 135 | typewise-core "^1.2" 136 | 137 | bytewise@~1.1.0: 138 | version "1.1.0" 139 | resolved "https://registry.yarnpkg.com/bytewise/-/bytewise-1.1.0.tgz#1d13cbff717ae7158094aa881b35d081b387253e" 140 | dependencies: 141 | bytewise-core "^1.2.2" 142 | typewise "^1.0.3" 143 | 144 | cardinal@1.0.0: 145 | version "1.0.0" 146 | resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-1.0.0.tgz#50e21c1b0aa37729f9377def196b5a9cec932ee9" 147 | dependencies: 148 | ansicolors "~0.2.1" 149 | redeyed "~1.0.0" 150 | 151 | chalk@^2.3.2: 152 | version "2.4.1" 153 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" 154 | dependencies: 155 | ansi-styles "^3.2.1" 156 | escape-string-regexp "^1.0.5" 157 | supports-color "^5.3.0" 158 | 159 | chownr@^1.0.1: 160 | version "1.0.1" 161 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" 162 | 163 | code-point-at@^1.0.0: 164 | version "1.1.0" 165 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 166 | 167 | color-convert@^1.9.0: 168 | version "1.9.1" 169 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" 170 | dependencies: 171 | color-name "^1.1.1" 172 | 173 | color-name@^1.1.1: 174 | version "1.1.3" 175 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 176 | 177 | colors@1.1.2: 178 | version "1.1.2" 179 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" 180 | 181 | concat-map@0.0.1: 182 | version "0.0.1" 183 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 184 | 185 | console-control-strings@^1.0.0, console-control-strings@~1.1.0: 186 | version "1.1.0" 187 | resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" 188 | 189 | core-util-is@~1.0.0: 190 | version "1.0.2" 191 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 192 | 193 | decompress-response@^3.3.0: 194 | version "3.3.0" 195 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" 196 | dependencies: 197 | mimic-response "^1.0.0" 198 | 199 | deep-extend@~0.4.0: 200 | version "0.4.2" 201 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" 202 | 203 | deferred-leveldown@~0.2.0: 204 | version "0.2.0" 205 | resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz#2cef1f111e1c57870d8bbb8af2650e587cd2f5b4" 206 | dependencies: 207 | abstract-leveldown "~0.12.1" 208 | 209 | deferred-leveldown@~3.0.0: 210 | version "3.0.0" 211 | resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-3.0.0.tgz#bff7241bf156aa3635f520bedf34330c408d3307" 212 | dependencies: 213 | abstract-leveldown "~4.0.0" 214 | 215 | delegates@^1.0.0: 216 | version "1.0.0" 217 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 218 | 219 | denque@1.2.3: 220 | version "1.2.3" 221 | resolved "https://registry.yarnpkg.com/denque/-/denque-1.2.3.tgz#98c50c8dd8cdfae318cc5859cc8ee3da0f9b0cc2" 222 | 223 | detect-libc@^1.0.3: 224 | version "1.0.3" 225 | resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" 226 | 227 | end-of-stream@^1.0.0, end-of-stream@^1.1.0: 228 | version "1.4.1" 229 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" 230 | dependencies: 231 | once "^1.4.0" 232 | 233 | errno@~0.1.1: 234 | version "0.1.7" 235 | resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" 236 | dependencies: 237 | prr "~1.0.1" 238 | 239 | escape-string-regexp@^1.0.5: 240 | version "1.0.5" 241 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 242 | 243 | esprima@~3.0.0: 244 | version "3.0.0" 245 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" 246 | 247 | expand-template@^1.0.2: 248 | version "1.1.0" 249 | resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-1.1.0.tgz#e09efba977bf98f9ee0ed25abd0c692e02aec3fc" 250 | 251 | fast-future@~1.0.2: 252 | version "1.0.2" 253 | resolved "https://registry.yarnpkg.com/fast-future/-/fast-future-1.0.2.tgz#8435a9aaa02d79248d17d704e76259301d99280a" 254 | 255 | fs-constants@^1.0.0: 256 | version "1.0.0" 257 | resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" 258 | 259 | fs.realpath@^1.0.0: 260 | version "1.0.0" 261 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 262 | 263 | fs@^0.0.1-security: 264 | version "0.0.1-security" 265 | resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4" 266 | 267 | gauge@~2.7.3: 268 | version "2.7.4" 269 | resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" 270 | dependencies: 271 | aproba "^1.0.3" 272 | console-control-strings "^1.0.0" 273 | has-unicode "^2.0.0" 274 | object-assign "^4.1.0" 275 | signal-exit "^3.0.0" 276 | string-width "^1.0.1" 277 | strip-ansi "^3.0.1" 278 | wide-align "^1.1.0" 279 | 280 | generate-function@^2.0.0: 281 | version "2.0.0" 282 | resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" 283 | 284 | github-from-package@0.0.0: 285 | version "0.0.0" 286 | resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" 287 | 288 | glob@^7.0.6: 289 | version "7.1.2" 290 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 291 | dependencies: 292 | fs.realpath "^1.0.0" 293 | inflight "^1.0.4" 294 | inherits "2" 295 | minimatch "^3.0.4" 296 | once "^1.3.0" 297 | path-is-absolute "^1.0.0" 298 | 299 | has-flag@^3.0.0: 300 | version "3.0.0" 301 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 302 | 303 | has-unicode@^2.0.0: 304 | version "2.0.1" 305 | resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 306 | 307 | iconv-lite@^0.4.18: 308 | version "0.4.21" 309 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798" 310 | dependencies: 311 | safer-buffer "^2.1.0" 312 | 313 | inflight@^1.0.4: 314 | version "1.0.6" 315 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 316 | dependencies: 317 | once "^1.3.0" 318 | wrappy "1" 319 | 320 | inherits@2, inherits@^2.0.1, inherits@~2.0.1, inherits@~2.0.3: 321 | version "2.0.3" 322 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 323 | 324 | ini@~1.3.0: 325 | version "1.3.5" 326 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" 327 | 328 | is-fullwidth-code-point@^1.0.0: 329 | version "1.0.0" 330 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 331 | dependencies: 332 | number-is-nan "^1.0.0" 333 | 334 | isarray@0.0.1: 335 | version "0.0.1" 336 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 337 | 338 | isarray@~1.0.0: 339 | version "1.0.0" 340 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 341 | 342 | jasmine-core@~3.1.0: 343 | version "3.1.0" 344 | resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.1.0.tgz#a4785e135d5df65024dfc9224953df585bd2766c" 345 | 346 | jasmine-spec-reporter@^4.2.1: 347 | version "4.2.1" 348 | resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz#1d632aec0341670ad324f92ba84b4b32b35e9e22" 349 | dependencies: 350 | colors "1.1.2" 351 | 352 | jasmine@^3.1.0: 353 | version "3.1.0" 354 | resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.1.0.tgz#2bd59fd7ec6ec0e8acb64e09f45a68ed2ad1952a" 355 | dependencies: 356 | glob "^7.0.6" 357 | jasmine-core "~3.1.0" 358 | 359 | json5@^1.0.1: 360 | version "1.0.1" 361 | resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" 362 | dependencies: 363 | minimist "^1.2.0" 364 | 365 | level-errors@~1.1.0: 366 | version "1.1.2" 367 | resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-1.1.2.tgz#4399c2f3d3ab87d0625f7e3676e2d807deff404d" 368 | dependencies: 369 | errno "~0.1.1" 370 | 371 | level-iterator-stream@~2.0.0: 372 | version "2.0.0" 373 | resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-2.0.0.tgz#e0fe4273a0322177c81bb87684016bb5b90a98b4" 374 | dependencies: 375 | inherits "^2.0.1" 376 | readable-stream "^2.0.5" 377 | xtend "^4.0.0" 378 | 379 | level-post@^1.0.7: 380 | version "1.0.7" 381 | resolved "https://registry.yarnpkg.com/level-post/-/level-post-1.0.7.tgz#19ccca9441a7cc527879a0635000f06d5e8f27d0" 382 | dependencies: 383 | ltgt "^2.1.2" 384 | 385 | level-sublevel@^6.6.1: 386 | version "6.6.1" 387 | resolved "https://registry.yarnpkg.com/level-sublevel/-/level-sublevel-6.6.1.tgz#f9a77f7521ab70a8f8e92ed56f21a3c7886a4485" 388 | dependencies: 389 | bytewise "~1.1.0" 390 | levelup "~0.19.0" 391 | ltgt "~2.1.1" 392 | pull-level "^2.0.3" 393 | pull-stream "^3.4.5" 394 | typewiselite "~1.0.0" 395 | xtend "~4.0.0" 396 | 397 | leveldown@^3.0.0: 398 | version "3.0.1" 399 | resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-3.0.1.tgz#eb1b4eb4ff79606a87e50f7224ea89e97d4faca1" 400 | dependencies: 401 | abstract-leveldown "~4.0.0" 402 | bindings "~1.3.0" 403 | fast-future "~1.0.2" 404 | nan "~2.10.0" 405 | prebuild-install "^2.1.0" 406 | 407 | levelup@^2.0.2: 408 | version "2.0.2" 409 | resolved "https://registry.yarnpkg.com/levelup/-/levelup-2.0.2.tgz#83dd22ffd5ee14482143c37cddfb8457854d3727" 410 | dependencies: 411 | deferred-leveldown "~3.0.0" 412 | level-errors "~1.1.0" 413 | level-iterator-stream "~2.0.0" 414 | xtend "~4.0.0" 415 | 416 | levelup@~0.19.0: 417 | version "0.19.1" 418 | resolved "https://registry.yarnpkg.com/levelup/-/levelup-0.19.1.tgz#f3a6a7205272c4b5f35e412ff004a03a0aedf50b" 419 | dependencies: 420 | bl "~0.8.1" 421 | deferred-leveldown "~0.2.0" 422 | errno "~0.1.1" 423 | prr "~0.0.0" 424 | readable-stream "~1.0.26" 425 | semver "~5.1.0" 426 | xtend "~3.0.0" 427 | 428 | lodash.merge@^4.6.1: 429 | version "4.6.1" 430 | resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" 431 | 432 | long@^4.0.0: 433 | version "4.0.0" 434 | resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" 435 | 436 | looper@^2.0.0: 437 | version "2.0.0" 438 | resolved "https://registry.yarnpkg.com/looper/-/looper-2.0.0.tgz#66cd0c774af3d4fedac53794f742db56da8f09ec" 439 | 440 | looper@^3.0.0: 441 | version "3.0.0" 442 | resolved "https://registry.yarnpkg.com/looper/-/looper-3.0.0.tgz#2efa54c3b1cbaba9b94aee2e5914b0be57fbb749" 443 | 444 | lru-cache@2.5.0: 445 | version "2.5.0" 446 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.5.0.tgz#d82388ae9c960becbea0c73bb9eb79b6c6ce9aeb" 447 | 448 | lru-cache@4.1.1: 449 | version "4.1.1" 450 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" 451 | dependencies: 452 | pseudomap "^1.0.2" 453 | yallist "^2.1.2" 454 | 455 | ltgt@^2.1.2: 456 | version "2.2.1" 457 | resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" 458 | 459 | ltgt@~2.1.1: 460 | version "2.1.3" 461 | resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.1.3.tgz#10851a06d9964b971178441c23c9e52698eece34" 462 | 463 | mimic-response@^1.0.0: 464 | version "1.0.0" 465 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e" 466 | 467 | minimatch@^3.0.4: 468 | version "3.0.4" 469 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 470 | dependencies: 471 | brace-expansion "^1.1.7" 472 | 473 | minimist@0.0.8: 474 | version "0.0.8" 475 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 476 | 477 | minimist@^1.2.0: 478 | version "1.2.0" 479 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 480 | 481 | mkdirp@^0.5.1: 482 | version "0.5.1" 483 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 484 | dependencies: 485 | minimist "0.0.8" 486 | 487 | mysql2@^1.5.3: 488 | version "1.5.3" 489 | resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-1.5.3.tgz#d905a1a06da0469828287608ce68647b8130748f" 490 | dependencies: 491 | cardinal "1.0.0" 492 | denque "1.2.3" 493 | generate-function "^2.0.0" 494 | iconv-lite "^0.4.18" 495 | long "^4.0.0" 496 | lru-cache "4.1.1" 497 | named-placeholders "1.1.1" 498 | object-assign "^4.1.1" 499 | readable-stream "2.3.5" 500 | safe-buffer "^5.0.1" 501 | seq-queue "0.0.5" 502 | sqlstring "2.3.1" 503 | 504 | named-placeholders@1.1.1: 505 | version "1.1.1" 506 | resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.1.tgz#3b7a0d26203dd74b3a9df4c9cfb827b2fb907e64" 507 | dependencies: 508 | lru-cache "2.5.0" 509 | 510 | nan@^2.10.0, nan@^2.6.2, nan@~2.10.0: 511 | version "2.10.0" 512 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" 513 | 514 | node-abi@^2.2.0: 515 | version "2.4.0" 516 | resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.4.0.tgz#3c27515cb842f5bbc132a31254f9f1e1c55c7b83" 517 | dependencies: 518 | semver "^5.4.1" 519 | 520 | node-lmdb@0.5.1: 521 | version "0.5.1" 522 | resolved "https://registry.yarnpkg.com/node-lmdb/-/node-lmdb-0.5.1.tgz#a1ac3a8641ccd22512d0d8da17569547ef4b1a25" 523 | dependencies: 524 | bindings "^1.2.1" 525 | nan "^2.6.2" 526 | 527 | noop-logger@^0.1.1: 528 | version "0.1.1" 529 | resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" 530 | 531 | npmlog@^4.0.1: 532 | version "4.1.2" 533 | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" 534 | dependencies: 535 | are-we-there-yet "~1.1.2" 536 | console-control-strings "~1.1.0" 537 | gauge "~2.7.3" 538 | set-blocking "~2.0.0" 539 | 540 | number-is-nan@^1.0.0: 541 | version "1.0.1" 542 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 543 | 544 | object-assign@^4.1.0, object-assign@^4.1.1: 545 | version "4.1.1" 546 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 547 | 548 | once@^1.3.0, once@^1.3.1, once@^1.4.0: 549 | version "1.4.0" 550 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 551 | dependencies: 552 | wrappy "1" 553 | 554 | os-homedir@^1.0.1: 555 | version "1.0.2" 556 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 557 | 558 | path-is-absolute@^1.0.0: 559 | version "1.0.1" 560 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 561 | 562 | prebuild-install@^2.1.0, prebuild-install@~2.5.0: 563 | version "2.5.3" 564 | resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.5.3.tgz#9f65f242782d370296353710e9bc843490c19f69" 565 | dependencies: 566 | detect-libc "^1.0.3" 567 | expand-template "^1.0.2" 568 | github-from-package "0.0.0" 569 | minimist "^1.2.0" 570 | mkdirp "^0.5.1" 571 | node-abi "^2.2.0" 572 | noop-logger "^0.1.1" 573 | npmlog "^4.0.1" 574 | os-homedir "^1.0.1" 575 | pump "^2.0.1" 576 | rc "^1.1.6" 577 | simple-get "^2.7.0" 578 | tar-fs "^1.13.0" 579 | tunnel-agent "^0.6.0" 580 | which-pm-runs "^1.0.0" 581 | 582 | process-nextick-args@~2.0.0: 583 | version "2.0.0" 584 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" 585 | 586 | prr@~0.0.0: 587 | version "0.0.0" 588 | resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" 589 | 590 | prr@~1.0.1: 591 | version "1.0.1" 592 | resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" 593 | 594 | pseudomap@^1.0.2: 595 | version "1.0.2" 596 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" 597 | 598 | pull-cat@^1.1.9: 599 | version "1.1.11" 600 | resolved "https://registry.yarnpkg.com/pull-cat/-/pull-cat-1.1.11.tgz#b642dd1255da376a706b6db4fa962f5fdb74c31b" 601 | 602 | pull-level@^2.0.3: 603 | version "2.0.4" 604 | resolved "https://registry.yarnpkg.com/pull-level/-/pull-level-2.0.4.tgz#4822e61757c10bdcc7cf4a03af04c92734c9afac" 605 | dependencies: 606 | level-post "^1.0.7" 607 | pull-cat "^1.1.9" 608 | pull-live "^1.0.1" 609 | pull-pushable "^2.0.0" 610 | pull-stream "^3.4.0" 611 | pull-window "^2.1.4" 612 | stream-to-pull-stream "^1.7.1" 613 | 614 | pull-live@^1.0.1: 615 | version "1.0.1" 616 | resolved "https://registry.yarnpkg.com/pull-live/-/pull-live-1.0.1.tgz#a4ecee01e330155e9124bbbcf4761f21b38f51f5" 617 | dependencies: 618 | pull-cat "^1.1.9" 619 | pull-stream "^3.4.0" 620 | 621 | pull-pushable@^2.0.0: 622 | version "2.2.0" 623 | resolved "https://registry.yarnpkg.com/pull-pushable/-/pull-pushable-2.2.0.tgz#5f2f3aed47ad86919f01b12a2e99d6f1bd776581" 624 | 625 | pull-stream@^3.2.3, pull-stream@^3.4.0, pull-stream@^3.4.5: 626 | version "3.6.7" 627 | resolved "https://registry.yarnpkg.com/pull-stream/-/pull-stream-3.6.7.tgz#fe4ae4f7cc3a9ee3ac82cd5be32729f2f0d5f02b" 628 | 629 | pull-window@^2.1.4: 630 | version "2.1.4" 631 | resolved "https://registry.yarnpkg.com/pull-window/-/pull-window-2.1.4.tgz#fc3b86feebd1920c7ae297691e23f705f88552f0" 632 | dependencies: 633 | looper "^2.0.0" 634 | 635 | pump@^1.0.0: 636 | version "1.0.3" 637 | resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" 638 | dependencies: 639 | end-of-stream "^1.1.0" 640 | once "^1.3.1" 641 | 642 | pump@^2.0.1: 643 | version "2.0.1" 644 | resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" 645 | dependencies: 646 | end-of-stream "^1.1.0" 647 | once "^1.3.1" 648 | 649 | rc@^1.1.6: 650 | version "1.2.6" 651 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092" 652 | dependencies: 653 | deep-extend "~0.4.0" 654 | ini "~1.3.0" 655 | minimist "^1.2.0" 656 | strip-json-comments "~2.0.1" 657 | 658 | readable-stream@2.3.5: 659 | version "2.3.5" 660 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" 661 | dependencies: 662 | core-util-is "~1.0.0" 663 | inherits "~2.0.3" 664 | isarray "~1.0.0" 665 | process-nextick-args "~2.0.0" 666 | safe-buffer "~5.1.1" 667 | string_decoder "~1.0.3" 668 | util-deprecate "~1.0.1" 669 | 670 | readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.3.5: 671 | version "2.3.6" 672 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 673 | dependencies: 674 | core-util-is "~1.0.0" 675 | inherits "~2.0.3" 676 | isarray "~1.0.0" 677 | process-nextick-args "~2.0.0" 678 | safe-buffer "~5.1.1" 679 | string_decoder "~1.1.1" 680 | util-deprecate "~1.0.1" 681 | 682 | readable-stream@~1.0.26: 683 | version "1.0.34" 684 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" 685 | dependencies: 686 | core-util-is "~1.0.0" 687 | inherits "~2.0.1" 688 | isarray "0.0.1" 689 | string_decoder "~0.10.x" 690 | 691 | redeyed@~1.0.0: 692 | version "1.0.1" 693 | resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-1.0.1.tgz#e96c193b40c0816b00aec842698e61185e55498a" 694 | dependencies: 695 | esprima "~3.0.0" 696 | 697 | safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 698 | version "5.1.1" 699 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 700 | 701 | safe-buffer@^5.1.1: 702 | version "5.1.2" 703 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 704 | 705 | safer-buffer@^2.1.0: 706 | version "2.1.2" 707 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 708 | 709 | semver@^5.4.1: 710 | version "5.5.0" 711 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" 712 | 713 | semver@~5.1.0: 714 | version "5.1.1" 715 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19" 716 | 717 | seq-queue@0.0.5: 718 | version "0.0.5" 719 | resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" 720 | 721 | set-blocking@~2.0.0: 722 | version "2.0.0" 723 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 724 | 725 | signal-exit@^3.0.0: 726 | version "3.0.2" 727 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 728 | 729 | simple-concat@^1.0.0: 730 | version "1.0.0" 731 | resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" 732 | 733 | simple-get@^2.7.0: 734 | version "2.8.1" 735 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d" 736 | dependencies: 737 | decompress-response "^3.3.0" 738 | once "^1.3.1" 739 | simple-concat "^1.0.0" 740 | 741 | sqlstring@2.3.1: 742 | version "2.3.1" 743 | resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" 744 | 745 | stream-to-pull-stream@^1.7.1: 746 | version "1.7.2" 747 | resolved "https://registry.yarnpkg.com/stream-to-pull-stream/-/stream-to-pull-stream-1.7.2.tgz#757609ae1cebd33c7432d4afbe31ff78650b9dde" 748 | dependencies: 749 | looper "^3.0.0" 750 | pull-stream "^3.2.3" 751 | 752 | string-width@^1.0.1, string-width@^1.0.2: 753 | version "1.0.2" 754 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 755 | dependencies: 756 | code-point-at "^1.0.0" 757 | is-fullwidth-code-point "^1.0.0" 758 | strip-ansi "^3.0.0" 759 | 760 | string_decoder@~0.10.x: 761 | version "0.10.31" 762 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 763 | 764 | string_decoder@~1.0.3: 765 | version "1.0.3" 766 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 767 | dependencies: 768 | safe-buffer "~5.1.0" 769 | 770 | string_decoder@~1.1.1: 771 | version "1.1.1" 772 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" 773 | dependencies: 774 | safe-buffer "~5.1.0" 775 | 776 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 777 | version "3.0.1" 778 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 779 | dependencies: 780 | ansi-regex "^2.0.0" 781 | 782 | strip-json-comments@~2.0.1: 783 | version "2.0.1" 784 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 785 | 786 | supports-color@^5.3.0: 787 | version "5.4.0" 788 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" 789 | dependencies: 790 | has-flag "^3.0.0" 791 | 792 | tar-fs@^1.13.0: 793 | version "1.16.0" 794 | resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.0.tgz#e877a25acbcc51d8c790da1c57c9cf439817b896" 795 | dependencies: 796 | chownr "^1.0.1" 797 | mkdirp "^0.5.1" 798 | pump "^1.0.0" 799 | tar-stream "^1.1.2" 800 | 801 | tar-stream@^1.1.2: 802 | version "1.5.7" 803 | resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.7.tgz#7f54380f49019231a8c5ddffbd1da882d2f66057" 804 | dependencies: 805 | bl "^1.0.0" 806 | buffer-alloc "^1.1.0" 807 | end-of-stream "^1.0.0" 808 | fs-constants "^1.0.0" 809 | readable-stream "^2.0.0" 810 | to-buffer "^1.1.0" 811 | xtend "^4.0.0" 812 | 813 | to-buffer@^1.1.0: 814 | version "1.1.1" 815 | resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" 816 | 817 | tunnel-agent@^0.6.0: 818 | version "0.6.0" 819 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 820 | dependencies: 821 | safe-buffer "^5.0.1" 822 | 823 | typewise-core@^1.2, typewise-core@^1.2.0: 824 | version "1.2.0" 825 | resolved "https://registry.yarnpkg.com/typewise-core/-/typewise-core-1.2.0.tgz#97eb91805c7f55d2f941748fa50d315d991ef195" 826 | 827 | typewise@^1.0.3: 828 | version "1.0.3" 829 | resolved "https://registry.yarnpkg.com/typewise/-/typewise-1.0.3.tgz#1067936540af97937cc5dcf9922486e9fa284651" 830 | dependencies: 831 | typewise-core "^1.2.0" 832 | 833 | typewiselite@~1.0.0: 834 | version "1.0.0" 835 | resolved "https://registry.yarnpkg.com/typewiselite/-/typewiselite-1.0.0.tgz#c8882fa1bb1092c06005a97f34ef5c8508e3664e" 836 | 837 | utf-8-validate@^4.0.1: 838 | version "4.0.1" 839 | resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-4.0.1.tgz#ec12589a42bbf0d77709baf5c082c610bd5b5fa6" 840 | dependencies: 841 | bindings "~1.3.0" 842 | nan "~2.10.0" 843 | prebuild-install "~2.5.0" 844 | 845 | util-deprecate@~1.0.1: 846 | version "1.0.2" 847 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 848 | 849 | uws@^9.14.0: 850 | version "9.148.0" 851 | resolved "https://registry.yarnpkg.com/uws/-/uws-9.148.0.tgz#56aff36cb95f7594573daff2a21105ec4b764664" 852 | 853 | uws@^9.147.0: 854 | version "9.147.0" 855 | resolved "https://registry.yarnpkg.com/uws/-/uws-9.147.0.tgz#aec775fb28074ab520f65802eff609c6662be7e8" 856 | 857 | which-pm-runs@^1.0.0: 858 | version "1.0.0" 859 | resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" 860 | 861 | wide-align@^1.1.0: 862 | version "1.1.2" 863 | resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" 864 | dependencies: 865 | string-width "^1.0.2" 866 | 867 | wrappy@1: 868 | version "1.0.2" 869 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 870 | 871 | xtend@^4.0.0, xtend@~4.0.0: 872 | version "4.0.1" 873 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 874 | 875 | xtend@~3.0.0: 876 | version "3.0.0" 877 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a" 878 | 879 | yallist@^2.1.2: 880 | version "2.1.2" 881 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 882 | --------------------------------------------------------------------------------