├── .coveralls.yml ├── .eslintrc.yml ├── .github └── workflows │ └── test_and_release.yml ├── .gitignore ├── LICENSE ├── README.md ├── lib └── index.js ├── package-lock.json ├── package.json ├── promise.js └── test └── index.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: QwBzswlfN6Ip1yWdnRwZHaRgdBC440VcS -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: airbnb-base 2 | env: 3 | node: true 4 | mocha: true 5 | -------------------------------------------------------------------------------- /.github/workflows/test_and_release.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: 7 | - published 8 | pull_request: 9 | branches: 10 | - master 11 | - v[0-9]+.[0-9]+-dev 12 | 13 | jobs: 14 | test: 15 | name: Run Dashd RPC tests 16 | runs-on: ubuntu-20.04 17 | timeout-minutes: 10 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - uses: actions/setup-node@v2 22 | with: 23 | node-version: '16' 24 | 25 | - name: Enable NPM cache 26 | uses: actions/cache@v2 27 | with: 28 | path: '~/.npm' 29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node- 32 | 33 | - name: Install NPM dependencies 34 | run: npm ci 35 | 36 | - name: Run tests 37 | run: npm run test 38 | 39 | - name: Run coverage tests 40 | run: npm run coverage 41 | 42 | release: 43 | name: Release NPM package 44 | runs-on: ubuntu-20.04 45 | needs: test 46 | if: ${{ github.event_name == 'release' }} 47 | steps: 48 | - uses: actions/checkout@v2 49 | 50 | - uses: actions/setup-node@v2 51 | with: 52 | node-version: '16' 53 | 54 | - name: Check package version matches tag 55 | uses: geritol/match-tag-to-package-version@0.1.0 56 | env: 57 | TAG_PREFIX: refs/tags/v 58 | 59 | - name: Enable NPM cache 60 | uses: actions/cache@v2 61 | with: 62 | path: '~/.npm' 63 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 64 | restore-keys: | 65 | ${{ runner.os }}-node- 66 | 67 | - name: Install NPM dependencies 68 | run: npm ci 69 | 70 | - name: Set release tag 71 | uses: actions/github-script@v3 72 | id: tag 73 | with: 74 | result-encoding: string 75 | script: | 76 | const tag = context.payload.release.tag_name; 77 | const [, major, minor] = tag.match(/^v([0-9]+)\.([0-9]+)/); 78 | return (tag.includes('dev') ? `${major}.${minor}-dev` : 'latest'); 79 | 80 | - name: Publish NPM package 81 | uses: JS-DevTools/npm-publish@v1 82 | with: 83 | token: ${{ secrets.NPM_TOKEN }} 84 | tag: ${{ steps.tag.outputs.result }} 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # intelliJ project files 31 | .idea 32 | 33 | # vscode project files 34 | launch.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2015 BitPay, Inc. 4 | Copyright (c) 2017-2018 Dash Core Group, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dashd-rpc 2 | 3 | [![Build Status](https://github.com/dashevo/dashd-rpc/actions/workflows/test.yml/badge.svg)](https://github.com/dashevo/dashd-rpc/actions/workflows/test.yml) 4 | [![NPM Package](https://img.shields.io/npm/v/@dashevo/dashd-rpc.svg)](https://www.npmjs.org/package/@dashevo/dashd-rpc) 5 | 6 | Dash Client Library to connect to Dash Core (dashd) via RPC 7 | 8 | ## Install 9 | 10 | dashd-rpc runs on [node](http://nodejs.org/), and can be installed via [npm](https://npmjs.org/): 11 | 12 | ```bash 13 | npm install @dashevo/dashd-rpc 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### RpcClient 19 | 20 | Config parameters : 21 | 22 | - protocol : (string - optional) - (default: 'https') - Set the protocol to be used. Either `http` or `https`. 23 | - user : (string - optional) - (default: 'user') - Set the user credential. 24 | - pass : (string - optional) - (default: 'pass') - Set the password credential. 25 | - host : (string - optional) - (default: '127.0.0.1') - The host you want to connect with. 26 | - port : (integer - optional) - (default: 9998) - Set the port on which perform the RPC command. 27 | 28 | Promise vs callback based 29 | 30 | - `require('@dashevo/dashd-rpc/promise')` to have promises returned 31 | - `require('@dashevo/dashd-rpc')` to have callback functions returned 32 | 33 | ### Examples 34 | 35 | Config: 36 | 37 | ```javascript 38 | var config = { 39 | protocol: 'http', 40 | user: 'dash', 41 | pass: 'local321', 42 | host: '127.0.0.1', 43 | port: 19998 44 | }; 45 | ``` 46 | 47 | Promise based: 48 | 49 | ```javascript 50 | var RpcClient = require('@dashevo/dashd-rpc/promise'); 51 | var rpc = new RpcClient(config); 52 | 53 | rpc.getRawMemPool() 54 | .then(ret => { 55 | return Promise.all(ret.result.map(r => rpc.getRawTransaction(r))) 56 | }) 57 | .then(rawTxs => { 58 | rawTxs.forEach(rawTx => { 59 | console.log(`RawTX: ${rawTx.result}`); 60 | }) 61 | }) 62 | .catch(err => { 63 | console.log(err) 64 | }) 65 | ``` 66 | 67 | Callback based (legacy): 68 | 69 | ```javascript 70 | var run = function() { 71 | var bitcore = require('@dashevo/dashcore-lib'); 72 | var RpcClient = require('@dashevo/dashd-rpc'); 73 | var rpc = new RpcClient(config); 74 | 75 | var txids = []; 76 | 77 | function showNewTransactions() { 78 | rpc.getRawMemPool(function (err, ret) { 79 | if (err) { 80 | console.error(err); 81 | return setTimeout(showNewTransactions, 10000); 82 | } 83 | 84 | function batchCall() { 85 | ret.result.forEach(function (txid) { 86 | if (txids.indexOf(txid) === -1) { 87 | rpc.getRawTransaction(txid); 88 | } 89 | }); 90 | } 91 | 92 | rpc.batch(batchCall, function(err, rawtxs) { 93 | if (err) { 94 | console.error(err); 95 | return setTimeout(showNewTransactions, 10000); 96 | } 97 | 98 | rawtxs.map(function (rawtx) { 99 | var tx = new bitcore.Transaction(rawtx.result); 100 | console.log('\n\n\n' + tx.id + ':', tx.toObject()); 101 | }); 102 | 103 | txids = ret.result; 104 | setTimeout(showNewTransactions, 2500); 105 | }); 106 | }); 107 | } 108 | 109 | showNewTransactions(); 110 | }; 111 | ``` 112 | 113 | ### Help 114 | 115 | You can dynamically access to the help of each method by doing 116 | 117 | ``` 118 | const RpcClient = require('@dashevo/dashd-rpc'); 119 | var client = new RPCclient({ 120 | protocol:'http', 121 | user: 'dash', 122 | pass: 'local321', 123 | host: '127.0.0.1', 124 | port: 19998, 125 | timeout: 1000 126 | }); 127 | 128 | var cb = function (err, data) { 129 | console.log(data) 130 | }; 131 | 132 | // Get full help 133 | client.help(cb); 134 | 135 | // Get help of specific method 136 | client.help('getinfo',cb); 137 | ``` 138 | 139 | ## Contributing 140 | 141 | Feel free to dive in! [Open an issue](https://github.com/dashevo/dash-std-template/issues/new) or submit PRs. 142 | 143 | ## License 144 | 145 | [MIT](LICENSE) © Dash Core Group, Inc. 146 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const http = require('http'); 4 | const https = require('https'); 5 | const async = require('async'); 6 | 7 | function RpcClient(opts) { 8 | opts = opts || {}; 9 | this.host = opts.host || '127.0.0.1'; 10 | this.port = opts.port || 9998; 11 | this.user = opts.user || 'user'; 12 | this.pass = opts.pass || 'pass'; 13 | this.timeout = opts.timeout; 14 | this.protocol = opts.protocol === 'http' ? http : https; 15 | this.batchedCalls = null; 16 | this.disableAgent = opts.disableAgent || false; 17 | const queueSize = opts.queue || 16; 18 | 19 | const isRejectUnauthorized = typeof opts.rejectUnauthorized !== 'undefined'; 20 | this.rejectUnauthorized = isRejectUnauthorized ? opts.rejectUnauthorized : true; 21 | 22 | if (RpcClient.config.log) { 23 | this.log = RpcClient.config.log; 24 | } else { 25 | this.log = RpcClient.loggers[RpcClient.config.logger || 'normal']; 26 | } 27 | 28 | this.queue = async.queue((task, callback) => { 29 | task(callback); 30 | }, queueSize); 31 | } 32 | 33 | const cl = console.log.bind(console); 34 | 35 | const noop = function () { 36 | }; 37 | 38 | RpcClient.loggers = { 39 | none: { 40 | info: noop, warn: noop, err: noop, debug: noop, 41 | }, 42 | normal: { 43 | info: cl, warn: cl, err: cl, debug: noop, 44 | }, 45 | debug: { 46 | info: cl, warn: cl, err: cl, debug: cl, 47 | }, 48 | }; 49 | 50 | RpcClient.config = { 51 | logger: 'normal', // none, normal, debug, 52 | }; 53 | 54 | function rpc(request, callback) { 55 | const self = this; 56 | const task = function (taskCallback) { 57 | const newCallback = function () { 58 | callback.apply(undefined, arguments); 59 | taskCallback(); 60 | }; 61 | innerRpc.call(self, request, newCallback); 62 | }; 63 | 64 | this.queue.push(task); 65 | } 66 | function innerRpc(request, callback) { 67 | const self = this; 68 | const path = request.path; 69 | delete request.path; 70 | request = JSON.stringify(request); 71 | const auth = Buffer.from(`${self.user}:${self.pass}`).toString('base64'); 72 | 73 | const options = { 74 | host: self.host, 75 | path, 76 | method: 'POST', 77 | port: self.port, 78 | rejectUnauthorized: self.rejectUnauthorized, 79 | agent: self.disableAgent ? false : undefined, 80 | }; 81 | 82 | if (self.timeout) { 83 | options.timeout = self.timeout; 84 | }; 85 | 86 | if (self.httpOptions) { 87 | for (const k in self.httpOptions) { 88 | options[k] = self.httpOptions[k]; 89 | } 90 | } 91 | 92 | let called = false; 93 | 94 | const errorMessage = 'Dash JSON-RPC: '; 95 | 96 | const req = this.protocol.request(options, (res) => { 97 | let buf = ''; 98 | res.on('data', (data) => { 99 | buf += data; 100 | }); 101 | 102 | res.on('end', () => { 103 | if (called) { 104 | return; 105 | } 106 | called = true; 107 | 108 | if (res.statusCode === 401) { 109 | callback(new Error(`${errorMessage}Connection Rejected: 401 Unnauthorized`)); 110 | return; 111 | } 112 | if (res.statusCode === 403) { 113 | callback(new Error(`${errorMessage}Connection Rejected: 403 Forbidden`)); 114 | return; 115 | } 116 | if (res.statusCode === 403) { 117 | callback(new Error(`${errorMessage}Connection Rejected: 403 Forbidden`)); 118 | return; 119 | } 120 | 121 | if (res.statusCode === 500 && buf.toString('utf8') === 'Work queue depth exceeded') { 122 | const exceededError = new Error(`Dash JSON-RPC: ${buf.toString('utf8')}`); 123 | exceededError.code = 429; // Too many requests 124 | callback(exceededError); 125 | return; 126 | } 127 | 128 | let parsedBuf; 129 | try { 130 | parsedBuf = JSON.parse(buf); 131 | } catch (e) { 132 | self.log.err(e.stack); 133 | self.log.err(buf); 134 | self.log.err(`HTTP Status code:${res.statusCode}`); 135 | const err = new Error(`${errorMessage}Error Parsing JSON: ${e.message}`); 136 | callback(err); 137 | return; 138 | } 139 | 140 | callback(parsedBuf.error, parsedBuf); 141 | }); 142 | }); 143 | 144 | req.on('error', (e) => { 145 | const err = new Error(`${errorMessage}Request Error: ${e.message}`); 146 | if (!called) { 147 | called = true; 148 | callback(err); 149 | } 150 | }); 151 | 152 | req.on('timeout', () => { 153 | const err = new Error(`Timeout Error: ${options.timeout}ms exceeded`); 154 | called = true; 155 | callback(err); 156 | }); 157 | 158 | req.setHeader('Content-Length', request.length); 159 | req.setHeader('Content-Type', 'application/json'); 160 | req.setHeader('Authorization', `Basic ${auth}`); 161 | req.write(request); 162 | req.end(); 163 | } 164 | 165 | RpcClient.prototype.batch = function (batchCallback, resultCallback) { 166 | this.batchedCalls = []; 167 | batchCallback(); 168 | rpc.call(this, this.batchedCalls, resultCallback); 169 | this.batchedCalls = null; 170 | }; 171 | 172 | RpcClient.prototype.setTimeout = function (timeout) { 173 | this.timeout = timeout; 174 | } 175 | 176 | // For definitions of RPC calls, see various files in: https://github.com/dashpay/dash/tree/master/src 177 | RpcClient.callspec = { 178 | abandonTransaction: 'str', 179 | addMultiSigAddress: 'int str str', 180 | addNode: 'str str', 181 | backupWallet: 'str', 182 | clearBanned: '', 183 | createMultiSig: 'int str', 184 | createRawTransaction: 'str str int', 185 | createWallet: 'str bool bool str bool bool bool', 186 | debug: 'str', 187 | decodeRawTransaction: 'str', 188 | decodeScript: 'str', 189 | disconnectNode: 'str', 190 | dumpPrivKey: 'str', 191 | dumpWallet: 'str', 192 | encryptWallet: 'str', 193 | estimateFee: 'int', 194 | estimatePriority: 'int', 195 | estimateSmartFee: 'int', 196 | estimateSmartPriority: 'int', 197 | fundRawTransaction: 'str bool', 198 | generate: 'int', 199 | generateToAddress: 'int str', 200 | getAccount: 'str', 201 | getAccountAddress: 'str', 202 | getAddressMempool: 'obj', 203 | getAddressUtxos: 'obj', 204 | getAddressBalance: 'obj', 205 | getAddressDeltas: 'obj', 206 | getAddressTxids: 'obj', 207 | getAddressesByAccount: '', 208 | getAddedNodeInfo: 'bool str', 209 | getBalance: 'str int bool', 210 | getBestBlockHash: '', 211 | getBestChainLock: '', 212 | getBlock: 'str bool', 213 | getBlockchainInfo: '', 214 | getBlockCount: '', 215 | getBlockHashes: 'int int', 216 | getBlockHash: 'int', 217 | getBlockHeader: 'str bool', 218 | getBlockHeaders: 'str int bool', 219 | getBlockStats: 'int_str obj', 220 | getBlockTemplate: '', 221 | getConnectionCount: '', 222 | getChainTips: 'int int', 223 | getDifficulty: '', 224 | getGenerate: '', 225 | getGovernanceInfo: '', 226 | getInfo: '', 227 | getMemPoolInfo: '', 228 | getMerkleBlocks: 'str str int', 229 | getMiningInfo: '', 230 | getNewAddress: '', 231 | getNetTotals: '', 232 | getNetworkInfo: '', 233 | getNetworkHashps: 'int int', 234 | getPeerInfo: '', 235 | getPoolInfo: '', 236 | getRawMemPool: 'bool', 237 | getRawChangeAddress: '', 238 | getRawTransaction: 'str int', 239 | getRawTransactionMulti: 'obj bool', 240 | getReceivedByAccount: 'str int', 241 | getReceivedByAddress: 'str int', 242 | getSpentInfo: 'obj', 243 | getSuperBlockBudget: 'int', 244 | getTransaction: '', 245 | getTxOut: 'str int bool', 246 | getTxOutProof: 'str str', 247 | getTxOutSetInfo: '', 248 | getWalletInfo: '', 249 | help: 'str', 250 | importAddress: 'str str bool', 251 | instantSendToAddress: 'str int str str bool', 252 | gobject: 'str str', 253 | invalidateBlock: 'str', 254 | importPrivKey: 'str str bool', 255 | importPubKey: 'str str bool', 256 | importElectrumWallet: 'str int', 257 | importWallet: 'str', 258 | keyPoolRefill: 'int', 259 | listAccounts: 'int bool', 260 | listAddressGroupings: '', 261 | listBanned: '', 262 | listReceivedByAccount: 'int bool', 263 | listReceivedByAddress: 'int bool', 264 | listSinceBlock: 'str int', 265 | listTransactions: 'str int int bool', 266 | listUnspent: 'int int str', 267 | listLockUnspent: 'bool', 268 | lockUnspent: 'bool obj', 269 | masternode: 'str', 270 | masternodeBroadcast: 'str', 271 | masternodelist: 'str str', 272 | mnsync: '', 273 | move: 'str str float int str', 274 | ping: '', 275 | prioritiseTransaction: 'str float int', 276 | privateSend: 'str', 277 | protx: 'str str str', 278 | quorum: 'str int str str str str int', 279 | reconsiderBlock: 'str', 280 | resendWalletTransactions: '', 281 | sendFrom: 'str str float int str str', 282 | sendMany: 'str obj int str str bool bool', 283 | sendRawTransaction: 'str float bool', 284 | sendToAddress: 'str float str str', 285 | sentinelPing: 'str', 286 | setAccount: '', 287 | setBan: 'str str int bool', 288 | setGenerate: 'bool int', 289 | setTxFee: 'float', 290 | setMockTime: 'int', 291 | spork: 'str', 292 | sporkupdate: 'str int', 293 | signMessage: 'str str', 294 | signRawTransaction: 'str str str str', 295 | stop: '', 296 | submitBlock: 'str str', 297 | validateAddress: 'str', 298 | verifyMessage: 'str str str', 299 | verifyChain: 'int int', 300 | verifyChainLock: 'str str int', 301 | verifyIsLock: 'str str str int', 302 | verifyTxOutProof: 'str', 303 | voteRaw: 'str int', 304 | waitForNewBlock: 'int', 305 | waitForBlockHeight: 'int int', 306 | walletLock: '', 307 | walletPassPhrase: 'str int bool', 308 | walletPassphraseChange: 'str str', 309 | getUser: 'str', 310 | }; 311 | 312 | const slice = function (arr, start, end) { 313 | return Array.prototype.slice.call(arr, start, end); 314 | }; 315 | 316 | function generateRPCMethods(constructor, apiCalls, rpc) { 317 | function createRPCMethod(methodName, argMap) { 318 | return function () { 319 | let path = '/'; 320 | let slicedArguments = slice(arguments); 321 | 322 | const length = slicedArguments.length; 323 | 324 | // The last optional parameter of requested method is a wallet name. We don't want to pass it to core, 325 | // that's why we remove it. And since the latest parameter here is a callback, we use length - 2, 326 | // instead of length - 1 327 | if (length > 0 && typeof slicedArguments[length - 2] === 'object' && slicedArguments[length - 2].wallet) { 328 | path = '/wallet/' + slicedArguments[length - 2].wallet; 329 | slicedArguments.splice(length - 2, 1); 330 | } 331 | 332 | let limit = slicedArguments.length - 1; 333 | 334 | if (this.batchedCalls) { 335 | limit = slicedArguments.length; 336 | } 337 | 338 | for (let i = 0; i < limit; i++) { 339 | if (argMap[i]) { 340 | slicedArguments[i] = argMap[i](slicedArguments[i]); 341 | } 342 | } 343 | 344 | if (this.batchedCalls) { 345 | this.batchedCalls.push({ 346 | path, 347 | jsonrpc: '2.0', 348 | method: methodName, 349 | params: slice(slicedArguments), 350 | id: getRandomId(), 351 | }); 352 | } else { 353 | rpc.call(this, { 354 | path, 355 | method: methodName, 356 | params: slice(slicedArguments, 0, slicedArguments.length - 1), 357 | id: getRandomId(), 358 | }, arguments[arguments.length - 1]); 359 | } 360 | }; 361 | } 362 | 363 | const types = { 364 | str(arg) { 365 | return arg.toString(); 366 | }, 367 | int(arg) { 368 | return parseFloat(arg); 369 | }, 370 | int_str(arg) { 371 | if (typeof arg === 'number') { 372 | return parseFloat(arg) 373 | } 374 | 375 | return arg.toString() 376 | }, 377 | float(arg) { 378 | return parseFloat(arg); 379 | }, 380 | bool(arg) { 381 | return (arg === true || arg == '1' || arg == 'true' || arg.toString().toLowerCase() == 'true'); 382 | }, 383 | obj(arg) { 384 | if (typeof arg === 'string') { 385 | return JSON.parse(arg); 386 | } 387 | return arg; 388 | }, 389 | }; 390 | 391 | for (const k in apiCalls) { 392 | const spec = apiCalls[k].split(' '); 393 | for (let i = 0; i < spec.length; i++) { 394 | if (types[spec[i]]) { 395 | spec[i] = types[spec[i]]; 396 | } else { 397 | spec[i] = types.str; 398 | } 399 | } 400 | const methodName = k.toLowerCase(); 401 | constructor.prototype[k] = createRPCMethod(methodName, spec); 402 | constructor.prototype[methodName] = constructor.prototype[k]; 403 | } 404 | 405 | constructor.prototype.apiCalls = apiCalls; 406 | } 407 | 408 | function getRandomId() { 409 | return parseInt(Math.random() * 100000); 410 | } 411 | 412 | generateRPCMethods(RpcClient, RpcClient.callspec, rpc); 413 | 414 | module.exports = RpcClient; 415 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dashevo/dashd-rpc", 3 | "description": "Dash Client Library to connect to Dash Core (dashd) via RPC", 4 | "version": "19.0.0", 5 | "author": { 6 | "name": "Stephen Pair", 7 | "email": "stephen@bitpay.com" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Jeff Garzik", 12 | "email": "jgarzik@bitpay.com" 13 | }, 14 | { 15 | "name": "Manuel Araoz", 16 | "email": "manuelaraoz@gmail.com" 17 | }, 18 | { 19 | "name": "Matias Alejo Garcia", 20 | "email": "ematiu@gmail.com" 21 | }, 22 | { 23 | "name": "Braydon Fuller", 24 | "email": "braydon@bitpay.com" 25 | }, 26 | { 27 | "name": "Alex Werner", 28 | "email": "alex.werner@dash.org" 29 | } 30 | ], 31 | "keywords": [ 32 | "dash", 33 | "rpc" 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/dashevo/dashd-rpc" 38 | }, 39 | "license": "MIT", 40 | "main": "lib/index.js", 41 | "scripts": { 42 | "test": "mocha test -R spec", 43 | "coverage": "nyc npm run test" 44 | }, 45 | "dependencies": { 46 | "async": "^3.2.4", 47 | "bluebird": "^3.7.2" 48 | }, 49 | "devDependencies": { 50 | "chai": "^4.2.0", 51 | "eslint": "^7.15.0", 52 | "eslint-config-airbnb-base": "^14.2.1", 53 | "eslint-plugin-import": "^2.22.1", 54 | "mocha": "^8.2.1", 55 | "nyc": "15.1.0", 56 | "sinon": "^9.2.2" 57 | }, 58 | "bugs": { 59 | "url": "https://github.com/dashevo/dashd-rpc/issues" 60 | }, 61 | "homepage": "https://github.com/dashevo/dashd-rpc" 62 | } 63 | -------------------------------------------------------------------------------- /promise.js: -------------------------------------------------------------------------------- 1 | const Bluebird = require('bluebird'); 2 | const RPCClient = require('./lib'); 3 | 4 | class PromisifyModule { 5 | constructor(options) { 6 | const client = new RPCClient(options); 7 | 8 | // eslint-disable-next-line guard-for-in,no-restricted-syntax 9 | for (const method in client.apiCalls) { 10 | const promise = Bluebird.promisify(client[method]); 11 | client[method] = promise; 12 | client[method.toLowerCase()] = promise; 13 | } 14 | 15 | return client; 16 | } 17 | } 18 | module.exports = PromisifyModule; 19 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var RpcClient = require('../'); 5 | var util = require('util'); 6 | var EventEmitter = require('events').EventEmitter; 7 | var sinon = require('sinon'); 8 | var should = chai.should(); 9 | var http = require('http'); 10 | var https = require('https'); 11 | var async = require('async'); 12 | 13 | if(!setImmediate) setImmediate = setTimeout; 14 | 15 | describe('RpcClient', function() { 16 | 17 | it('should initialize the main object', function() { 18 | should.exist(RpcClient); 19 | }); 20 | 21 | it('should be able to create instance', function() { 22 | var s = new RpcClient(); 23 | should.exist(s); 24 | }); 25 | 26 | it('default to rejectUnauthorized as true', function() { 27 | var s = new RpcClient(); 28 | should.exist(s); 29 | s.rejectUnauthorized.should.equal(true); 30 | }); 31 | 32 | it('should be able to define a custom logger', function() { 33 | var customLogger = { 34 | info: function(){}, 35 | warn: function(){}, 36 | err: function(){}, 37 | debug: function(){} 38 | }; 39 | RpcClient.config.log = customLogger; 40 | var s = new RpcClient(); 41 | s.log.should.equal(customLogger); 42 | RpcClient.config.log = false; 43 | }); 44 | 45 | it('should be able to define the logger to normal', function() { 46 | RpcClient.config.logger = 'normal'; 47 | var s = new RpcClient(); 48 | s.log.should.equal(RpcClient.loggers.normal); 49 | }); 50 | 51 | it('should be able to define the logger to none', function() { 52 | RpcClient.config.logger = 'none'; 53 | var s = new RpcClient(); 54 | s.log.should.equal(RpcClient.loggers.none); 55 | }); 56 | 57 | function FakeResponse(){ 58 | EventEmitter.call(this); 59 | } 60 | util.inherits(FakeResponse, EventEmitter); 61 | 62 | function FakeRequest(){ 63 | EventEmitter.call(this); 64 | return this; 65 | } 66 | util.inherits(FakeRequest, EventEmitter); 67 | FakeRequest.prototype.setHeader = function() {}; 68 | FakeRequest.prototype.write = function(data) { 69 | this.data = data; 70 | }; 71 | FakeRequest.prototype.end = function() {}; 72 | 73 | it('should use https', function() { 74 | 75 | var client = new RpcClient({ 76 | user: 'user', 77 | pass: 'pass', 78 | port: 8332, 79 | }); 80 | client.protocol.should.equal(https); 81 | 82 | }); 83 | 84 | it('should use http', function() { 85 | 86 | var client = new RpcClient({ 87 | user: 'user', 88 | pass: 'pass', 89 | host: 'localhost', 90 | port: 8332, 91 | protocol: 'http' 92 | }); 93 | client.protocol.should.equal(http); 94 | 95 | }); 96 | 97 | it('should call a method and receive response', function(done) { 98 | 99 | var client = new RpcClient({ 100 | user: 'user', 101 | pass: 'pass', 102 | host: 'localhost', 103 | port: 8332, 104 | rejectUnauthorized: true, 105 | disableAgent: true 106 | }); 107 | 108 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 109 | var res = new FakeResponse(); 110 | var req = new FakeRequest(); 111 | setImmediate(function(){ 112 | res.emit('data', '{}'); 113 | res.emit('end'); 114 | }); 115 | callback(res); 116 | return req; 117 | }); 118 | 119 | client.setTxFee(0.01, function(error, parsedBuf) { 120 | requestStub.restore(); 121 | should.not.exist(error); 122 | should.exist(parsedBuf); 123 | done(); 124 | }); 125 | 126 | }); 127 | 128 | it('accept many values for bool', function(done) { 129 | 130 | var client = new RpcClient({ 131 | user: 'user', 132 | pass: 'pass', 133 | host: 'localhost', 134 | port: 8332, 135 | rejectUnauthorized: true, 136 | disableAgent: false 137 | }); 138 | 139 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 140 | var res = new FakeResponse(); 141 | var req = new FakeRequest(); 142 | setImmediate(function(){ 143 | res.emit('data', req.data); 144 | res.emit('end'); 145 | }); 146 | callback(res); 147 | return req; 148 | }); 149 | 150 | async.eachSeries([true, 'true', 1, '1', 'True'], function(i, next) { 151 | client.importAddress('n28S35tqEMbt6vNad7A5K3mZ7vdn8dZ86X', '', i, function(error, parsedBuf) { 152 | should.not.exist(error); 153 | should.exist(parsedBuf); 154 | parsedBuf.params[2].should.equal(true); 155 | next(); 156 | }); 157 | }, function(err) { 158 | requestStub.restore(); 159 | done(); 160 | }); 161 | 162 | }); 163 | 164 | it('should process int_str arguments', async () => { 165 | const client = new RpcClient({ 166 | user: 'user', 167 | pass: 'pass', 168 | host: 'localhost', 169 | port: 8332, 170 | rejectUnauthorized: true, 171 | disableAgent: false, 172 | }); 173 | 174 | const requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 175 | const res = new FakeResponse(); 176 | const req = new FakeRequest(); 177 | setImmediate(() => { 178 | res.emit('data', req.data); 179 | res.emit('end'); 180 | }); 181 | callback(res); 182 | return req; 183 | }); 184 | 185 | const blockByHeight = await (new Promise((res) => client.getBlockStats(1, ['height'], (err, data) => res(data)))); 186 | 187 | should.exist(blockByHeight); 188 | blockByHeight.params[0].should.equal(1); 189 | blockByHeight.params[1].should.deep.equal(['height']); 190 | 191 | const blockByHash = await (new Promise((res) => client.getBlockStats('fake_hash', ['height'], (err, data) => res(data)))); 192 | 193 | should.exist(blockByHash); 194 | blockByHash.params[0].should.equal('fake_hash'); 195 | blockByHash.params[1].should.deep.equal(['height']); 196 | 197 | requestStub.restore(); 198 | }); 199 | 200 | it('should batch calls for a method and receive a response', function(done) { 201 | 202 | var client = new RpcClient({ 203 | user: 'user', 204 | pass: 'pass', 205 | host: 'localhost', 206 | port: 8332, 207 | rejectUnauthorized: true, 208 | disableAgent: false 209 | }); 210 | 211 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 212 | var res = new FakeResponse(); 213 | setImmediate(function(){ 214 | res.emit('data', '[{}, {}, {}]'); 215 | res.emit('end'); 216 | }); 217 | callback(res); 218 | return new FakeRequest(); 219 | }); 220 | 221 | client.batchedCalls = []; 222 | client.listReceivedByAccount(1, true); 223 | client.listReceivedByAccount(2, true); 224 | client.listReceivedByAccount(3, true); 225 | client.batchedCalls.length.should.equal(3); 226 | client.batch(function(){ 227 | // batch started 228 | }, function(error, result){ 229 | // batch ended 230 | requestStub.restore(); 231 | should.not.exist(error); 232 | should.exist(result); 233 | result.length.should.equal(3); 234 | done(); 235 | }); 236 | 237 | }); 238 | 239 | it('should handle connection rejected 401 unauthorized', function(done) { 240 | 241 | var client = new RpcClient({ 242 | user: 'user', 243 | pass: 'pass', 244 | host: 'localhost', 245 | port: 8332, 246 | rejectUnauthorized: true, 247 | disableAgent: true 248 | }); 249 | 250 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 251 | var res = new FakeResponse(); 252 | res.statusCode = 401; 253 | setImmediate(function(){ 254 | res.emit('end'); 255 | }); 256 | callback(res); 257 | return new FakeRequest(); 258 | }); 259 | 260 | client.getBalance('n28S35tqEMbt6vNad7A5K3mZ7vdn8dZ86X', 6, function(error, parsedBuf) { 261 | requestStub.restore(); 262 | should.exist(error); 263 | error.message.should.equal('Dash JSON-RPC: Connection Rejected: 401 Unnauthorized'); 264 | done(); 265 | }); 266 | 267 | }); 268 | 269 | it('should handle connection rejected 401 forbidden', function(done) { 270 | 271 | var client = new RpcClient({ 272 | user: 'user', 273 | pass: 'pass', 274 | host: 'localhost', 275 | port: 8332, 276 | rejectUnauthorized: true, 277 | disableAgent: true 278 | }); 279 | 280 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 281 | var res = new FakeResponse(); 282 | res.statusCode = 403; 283 | setImmediate(function(){ 284 | res.emit('end'); 285 | }); 286 | callback(res); 287 | return new FakeRequest(); 288 | }); 289 | 290 | client.getDifficulty(function(error, parsedBuf) { 291 | requestStub.restore(); 292 | should.exist(error); 293 | error.message.should.equal('Dash JSON-RPC: Connection Rejected: 403 Forbidden'); 294 | done(); 295 | }); 296 | 297 | }); 298 | 299 | it('should handle 500 work limit exceeded error', function(done) { 300 | 301 | var client = new RpcClient({ 302 | user: 'user', 303 | pass: 'pass', 304 | host: 'localhost', 305 | port: 8332, 306 | rejectUnauthorized: true, 307 | disableAgent: true 308 | }); 309 | 310 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 311 | var res = new FakeResponse(); 312 | res.statusCode = 500; 313 | setImmediate(function(){ 314 | res.emit('data', 'Work queue depth exceeded'); 315 | res.emit('end'); 316 | }); 317 | callback(res); 318 | return new FakeRequest(); 319 | }); 320 | 321 | client.getDifficulty(function(error, parsedBuf) { 322 | requestStub.restore(); 323 | should.exist(error); 324 | error.message.should.equal('Dash JSON-RPC: Work queue depth exceeded'); 325 | done(); 326 | }); 327 | 328 | }); 329 | 330 | it('should handle EPIPE error case 1', function(done) { 331 | 332 | var client = new RpcClient({ 333 | user: 'user', 334 | pass: 'pass', 335 | host: 'localhost', 336 | port: 8332, 337 | rejectUnauthorized: true, 338 | disableAgent: true 339 | }); 340 | 341 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 342 | var req = new FakeRequest(); 343 | setImmediate(function(){ 344 | req.emit('error', new Error('write EPIPE')); 345 | }); 346 | var res = new FakeResponse(); 347 | setImmediate(function(){ 348 | res.emit('data', '{}'); 349 | res.emit('end'); 350 | }); 351 | callback(res); 352 | return req; 353 | }); 354 | 355 | client.getDifficulty(function(error, parsedBuf) { 356 | requestStub.restore(); 357 | should.exist(error); 358 | error.message.should.equal('Dash JSON-RPC: Request Error: write EPIPE'); 359 | done(); 360 | }); 361 | 362 | }); 363 | 364 | it('should handle EPIPE error case 2', function(done) { 365 | 366 | var client = new RpcClient({ 367 | user: 'user', 368 | pass: 'pass', 369 | host: 'localhost', 370 | port: 8332, 371 | rejectUnauthorized: true, 372 | disableAgent: true 373 | }); 374 | 375 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 376 | var res = new FakeResponse(); 377 | setImmediate(function(){ 378 | res.emit('data', '{}'); 379 | res.emit('end'); 380 | }); 381 | var req = new FakeRequest(); 382 | setImmediate(function(){ 383 | req.emit('error', new Error('write EPIPE')); 384 | }); 385 | callback(res); 386 | req.on('error', function(err) { 387 | requestStub.restore(); 388 | done(); 389 | }); 390 | return req; 391 | }); 392 | 393 | client.getDifficulty(function(error, parsedBuf) {}); 394 | 395 | }); 396 | 397 | it('should handle ECONNREFUSED error', function(done) { 398 | 399 | var client = new RpcClient({ 400 | user: 'user', 401 | pass: 'pass', 402 | host: 'localhost', 403 | port: 8332, 404 | rejectUnauthorized: true, 405 | disableAgent: true 406 | }); 407 | 408 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 409 | var res = new FakeResponse(); 410 | var req = new FakeRequest(); 411 | setImmediate(function(){ 412 | req.emit('error', new Error('connect ECONNREFUSED')); 413 | }); 414 | callback(res); 415 | return req; 416 | }); 417 | 418 | client.getDifficulty(function(error, parsedBuf) { 419 | requestStub.restore(); 420 | should.exist(error); 421 | error.message.should.equal('Dash JSON-RPC: Request Error: connect ECONNREFUSED'); 422 | done(); 423 | }); 424 | 425 | }); 426 | 427 | it('should callback with error if invalid json', function(done) { 428 | 429 | var client = new RpcClient({ 430 | user: 'user', 431 | pass: 'pass', 432 | host: 'localhost', 433 | port: 8332, 434 | rejectUnauthorized: true, 435 | disableAgent: true 436 | }); 437 | 438 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 439 | var res = new FakeResponse(); 440 | setImmediate(function(){ 441 | res.emit('data', 'not a json string'); 442 | res.emit('end'); 443 | }); 444 | var req = new FakeRequest(); 445 | callback(res); 446 | return req; 447 | }); 448 | 449 | client.getDifficulty(function(error, parsedBuf) { 450 | requestStub.restore(); 451 | should.exist(error); 452 | error.message.should.equal('Dash JSON-RPC: Error Parsing JSON: Unexpected token o in JSON at position 1'); 453 | done(); 454 | }); 455 | 456 | }); 457 | 458 | it('should callback with error if blank response', function(done) { 459 | 460 | var client = new RpcClient({ 461 | user: 'user', 462 | pass: 'pass', 463 | host: 'localhost', 464 | port: 8332, 465 | rejectUnauthorized: true, 466 | disableAgent: true 467 | }); 468 | 469 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 470 | var res = new FakeResponse(); 471 | setImmediate(function(){ 472 | res.emit('data', ''); 473 | res.emit('end'); 474 | }); 475 | var req = new FakeRequest(); 476 | callback(res); 477 | return req; 478 | }); 479 | 480 | client.getDifficulty(function(error, parsedBuf) { 481 | requestStub.restore(); 482 | should.exist(error); 483 | error.message.should.equal('Dash JSON-RPC: Error Parsing JSON: Unexpected end of JSON input'); 484 | done(); 485 | }); 486 | 487 | }); 488 | 489 | it('should add additional http options', function(done) { 490 | 491 | var client = new RpcClient({ 492 | user: 'user', 493 | pass: 'pass', 494 | host: 'localhost', 495 | port: 8332, 496 | rejectUnauthorized: true, 497 | disableAgent: true 498 | }); 499 | 500 | client.httpOptions = { 501 | port: 20001 502 | }; 503 | 504 | var calledPort = false; 505 | 506 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 507 | calledPort = options.port; 508 | var res = new FakeResponse(); 509 | setImmediate(function(){ 510 | res.emit('data', '{}'); 511 | res.emit('end'); 512 | }); 513 | var req = new FakeRequest(); 514 | callback(res); 515 | return req; 516 | }); 517 | 518 | client.getDifficulty(function(error, parsedBuf) { 519 | should.not.exist(error); 520 | should.exist(parsedBuf); 521 | calledPort.should.equal(20001); 522 | requestStub.restore(); 523 | done(); 524 | }); 525 | 526 | }); 527 | 528 | it('should throw error when timeout is triggered', (done) => { 529 | var client = new RpcClient({ 530 | user: 'user', 531 | pass: 'pass', 532 | host: 'localhost', 533 | port: 8332, 534 | }); 535 | 536 | client.httpOptions = { 537 | timeout: 100 538 | }; 539 | 540 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function (options, callback) { 541 | var req = new FakeRequest(); 542 | setTimeout(function () { 543 | req.emit('timeout'); 544 | }, options.timeout); 545 | return req; 546 | }); 547 | 548 | client.getDifficulty((error, parsedBuf) => { 549 | should.exist(error); 550 | should.not.exist(parsedBuf); 551 | error.message.should.equal(`Timeout Error: ${client.httpOptions.timeout}ms exceeded`); 552 | requestStub.restore(); 553 | done(); 554 | }) 555 | }); 556 | 557 | it('should call wallet method and receive the response', (done) => { 558 | var client = new RpcClient({ 559 | user: 'user', 560 | pass: 'pass', 561 | host: 'localhost', 562 | port: 8332, 563 | rejectUnauthorized: true, 564 | disableAgent: true 565 | }); 566 | 567 | var requestStub = sinon.stub(client.protocol, 'request').callsFake(function(options, callback){ 568 | var res = new FakeResponse(); 569 | var req = new FakeRequest(); 570 | setImmediate(function(){ 571 | res.emit('data', '{}'); 572 | res.emit('end'); 573 | }); 574 | callback(res); 575 | return req; 576 | }); 577 | 578 | client.getBalance('n28S35tqEMbt6vNad7A5K3mZ7vdn8dZ86X', 6, { wallet: 'default' }, function(error, parsedBuf) { 579 | requestStub.restore(); 580 | should.not.exist(error); 581 | should.exist(parsedBuf); 582 | done(); 583 | }); 584 | }); 585 | }) 586 | --------------------------------------------------------------------------------