├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src └── index.js ├── test └── index.js ├── ts_src └── index.ts ├── tsconfig.json ├── tslint.json └── types └── index.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitcoinjs/regtest-client/a76f36a51b68949c853ab804bf06bc32a6bfbac9/.prettierignore -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | services: 4 | - docker 5 | before_install: 6 | - if [ $TEST_SUITE = "unit" ]; then 7 | docker pull junderw/bitcoinjs-regtest-server && 8 | docker run -d -p 127.0.0.1:8080:8080 junderw/bitcoinjs-regtest-server && 9 | docker ps -a; 10 | fi 11 | node_js: 12 | - "8" 13 | - "lts/*" 14 | env: 15 | - TEST_SUITE=unit 16 | matrix: 17 | include: 18 | - node_js: "lts/*" 19 | env: TEST_SUITE=format:ci 20 | - node_js: "lts/*" 21 | env: TEST_SUITE=formatjs:ci 22 | - node_js: "lts/*" 23 | env: TEST_SUITE=gitdiff:ci 24 | - node_js: "lts/*" 25 | env: TEST_SUITE=lint 26 | script: npm run $TEST_SUITE -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2019 bitcoinjs-lib contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Regtest Client 2 | 3 | A library for managing regtest server. (Good for integration tests) 4 | 5 | ## You must specify a server 6 | 7 | Default URL is `http://127.0.0.1:8080/1`, and the recommended way to set up a server 8 | is to run a docker container locally. 9 | 10 | You can override the URL with `APIURL` environment variable or by using the 11 | optional second arg `new RegtestUtils(bitcoin, { APIURL: 'xxx' })` at runtime. 12 | 13 | You can also override the API password (set on the server side) with `APIPASS` 14 | env variable, or `new RegtestUtils(bitcoin, { APIURL: 'xxx', APIPASS: 'yyy' })` 15 | 16 | The optional second arg can have either, both, or none of the two overrides. 17 | 18 | ## Docker 19 | 20 | Check the docker folder on [regtest-server](https://github.com/bitcoinjs/regtest-server) 21 | to run a server locally. 22 | 23 | Also, see bitcoinjs-lib travis config to see how to use the docker image with CI. 24 | [Check the .travis.yml here.](https://github.com/bitcoinjs/bitcoinjs-lib/blob/b3def6b4006683190657ef40efa7a8bcbb78b5cd/.travis.yml#L3-L10) 25 | 26 | ## TypeScript support 27 | 28 | Types are automatically generated. Develop in TypeScript, commit JS and types. 29 | Pull requests must all contain TS, JS, and types where needed. 30 | 31 | ## Usage 32 | 33 | ```js 34 | // inside an async function to use await 35 | 36 | // bitcoinjs-lib must be the >=5.0.6 to use. 37 | // For bitcoinjs-lib >=4.0.3, use version v0.0.8 of regtest-client 38 | const bitcoin = require('bitcoinjs-lib') 39 | const { RegtestUtils } = require('regtest-client') 40 | const regtestUtils = new RegtestUtils(bitcoin) 41 | 42 | const network = regtestUtils.network // regtest network params 43 | 44 | const keyPair = bitcoin.ECPair.makeRandom({ network }) 45 | const p2pkh = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network }) 46 | 47 | // Tell the server to send you coins (satoshis) 48 | // Can pass address 49 | const unspent = await regtestUtils.faucet(p2pkh.address, 2e4) 50 | 51 | // Tell the server to send you coins (satoshis) 52 | // Can pass Buffer of the scriptPubkey (in case address can not be parsed by bitcoinjs-lib) 53 | // Non-standard outputs will be rejected, though. 54 | const unspentComplex = await regtestUtils.faucetComplex(p2pkh.output, 1e4) 55 | 56 | // Get all current unspents of the address. 57 | const unspents = await regtestUtils.unspents(p2pkh.address) 58 | 59 | // Get data of a certain transaction 60 | const fetchedTx = await regtestUtils.fetch(unspent.txId) 61 | 62 | // Mine 6 blocks, returns an Array of the block hashes 63 | // All of the above faucet payments will confirm 64 | const results = await regtestUtils.mine(6) 65 | 66 | // Send our own transaction 67 | const txb = new bitcoin.TransactionBuilder(network) 68 | txb.addInput(unspent.txId, unspent.vout) 69 | txb.addInput(unspentComplex.txId, unspentComplex.vout) 70 | // regtestUtils.RANDOM_ADDRESS is created on first load. 71 | // regtestUtils.randomAddress() will return a new random address every time. 72 | // (You won't have the private key though.) 73 | txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4) 74 | 75 | txb.sign({ 76 | prevOutScriptType: 'p2pkh', 77 | vin: 0, 78 | keyPair, 79 | }) 80 | txb.sign({ 81 | prevOutScriptType: 'p2pkh', 82 | vin: 1, 83 | keyPair, 84 | }) 85 | const tx = txb.build() 86 | 87 | // build and broadcast to the Bitcoin Local RegTest server 88 | await regtestUtils.broadcast(tx.toHex()) 89 | 90 | // This verifies that the vout output of txId transaction is actually for value 91 | // in satoshis and is locked for the address given. 92 | // The utxo can be unconfirmed. We are just verifying it was at least placed in 93 | // the mempool. 94 | await regtestUtils.verify({ 95 | txId: tx.getId(), 96 | address: regtestUtils.RANDOM_ADDRESS, 97 | vout: 0, 98 | value: 1e4 99 | }) 100 | 101 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regtest-client", 3 | "version": "0.2.1", 4 | "description": "A client for regtest bitcoin usage. Requires regtest-server from bitcoinjs github.", 5 | "main": "src/index.js", 6 | "types": "types/index.d.ts", 7 | "files": [ 8 | "src", 9 | "types" 10 | ], 11 | "scripts": { 12 | "build": "npm run clean && tsc -p ./tsconfig.json && npm run formatjs", 13 | "clean": "rimraf src/ types/", 14 | "format": "npm run prettier -- --write", 15 | "formatjs": "npm run prettierjs -- --write", 16 | "formatjs:ci": "npm run prettierjs -- --check", 17 | "format:ci": "npm run prettier -- --check", 18 | "gitdiff:ci": "npm run build && git diff --exit-code", 19 | "lint": "tslint -p tsconfig.json -c tslint.json", 20 | "nobuild:gitdiff:ci": "git diff --exit-code", 21 | "nobuild:unit": "mocha --timeout 50000", 22 | "prepublishOnly": "npm run test && npm run nobuild:gitdiff:ci", 23 | "prettier": "prettier 'ts_src/**/*.ts' --ignore-path ./.prettierignore", 24 | "prettierjs": "prettier 'src/**/*.js' --ignore-path ./.prettierignore", 25 | "test": "npm run build && npm run format:ci && npm run lint && npm run nobuild:unit", 26 | "unit": "npm run build && npm run nobuild:unit" 27 | }, 28 | "keywords": [ 29 | "bitcoin", 30 | "regtest", 31 | "client" 32 | ], 33 | "author": "Jonathan Underwood", 34 | "license": "MIT", 35 | "dependencies": { 36 | "bs58check": "^2.1.2", 37 | "dhttp": "^3.0.3", 38 | "randombytes": "^2.1.0" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^11.13.0", 42 | "@types/randombytes": "^2.0.0", 43 | "bitcoinjs-lib": "^5.2.0", 44 | "mocha": "^10.2.0", 45 | "prettier": "^1.16.4", 46 | "rimraf": "^2.6.3", 47 | "tslint": "^5.15.0", 48 | "typescript": "^3.4.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | exports.RegtestUtils = void 0; 4 | const assert = require('assert'); 5 | const rng = require('randombytes'); 6 | const bs58check = require('bs58check'); 7 | const dhttpCallback = require('dhttp/200'); 8 | let RANDOM_ADDRESS; 9 | class RegtestUtils { 10 | constructor(_opts) { 11 | this._APIURL = 12 | (_opts || {}).APIURL || process.env.APIURL || 'http://127.0.0.1:8080/1'; 13 | this._APIPASS = (_opts || {}).APIPASS || process.env.APIPASS || 'satoshi'; 14 | // regtest network parameters 15 | this.network = { 16 | messagePrefix: '\x18Bitcoin Signed Message:\n', 17 | bech32: 'bcrt', 18 | bip32: { 19 | public: 0x043587cf, 20 | private: 0x04358394, 21 | }, 22 | pubKeyHash: 0x6f, 23 | scriptHash: 0xc4, 24 | wif: 0xef, 25 | }; 26 | } 27 | get RANDOM_ADDRESS() { 28 | if (RANDOM_ADDRESS === undefined) { 29 | RANDOM_ADDRESS = this.randomAddress(); 30 | } 31 | return RANDOM_ADDRESS; 32 | } 33 | // use Promises 34 | async dhttp(options) { 35 | return new Promise((resolve, reject) => { 36 | return dhttpCallback(options, (err, data) => { 37 | if (err) return reject(err); 38 | else return resolve(data); 39 | }); 40 | }); 41 | } 42 | async broadcast(txHex) { 43 | return this.dhttp({ 44 | method: 'POST', 45 | url: this._APIURL + '/t/push', 46 | body: txHex, 47 | }); 48 | } 49 | async mine(count) { 50 | return this.dhttp({ 51 | method: 'POST', 52 | url: `${this._APIURL}/r/generate?count=${count}&key=${this._APIPASS}`, 53 | }); 54 | } 55 | async height() { 56 | return this.dhttp({ 57 | method: 'GET', 58 | url: this._APIURL + '/b/best/height', 59 | }); 60 | } 61 | async fetch(txId) { 62 | return this.dhttp({ 63 | method: 'GET', 64 | url: `${this._APIURL}/t/${txId}/json`, 65 | }); 66 | } 67 | async unspents(address) { 68 | return this.dhttp({ 69 | method: 'GET', 70 | url: `${this._APIURL}/a/${address}/unspents`, 71 | }); 72 | } 73 | async faucet(address, value) { 74 | const requester = _faucetRequestMaker( 75 | 'faucet', 76 | 'address', 77 | this.dhttp, 78 | this._APIURL, 79 | this._APIPASS, 80 | ); 81 | const faucet = _faucetMaker(this, requester); 82 | return faucet(address, value); 83 | } 84 | async faucetComplex(output, value) { 85 | const outputString = output.toString('hex'); 86 | const requester = _faucetRequestMaker( 87 | 'faucetScript', 88 | 'script', 89 | this.dhttp, 90 | this._APIURL, 91 | this._APIPASS, 92 | ); 93 | const faucet = _faucetMaker(this, requester); 94 | return faucet(outputString, value); 95 | } 96 | async verify(txo) { 97 | const tx = await this.fetch(txo.txId); 98 | const txoActual = tx.outs[txo.vout]; 99 | if (txo.address) assert.strictEqual(txoActual.address, txo.address); 100 | if (txo.value) assert.strictEqual(txoActual.value, txo.value); 101 | } 102 | randomAddress() { 103 | // Fake P2PKH address with regtest/testnet version byte 104 | return bs58check.encode(Buffer.concat([Buffer.from([0x6f]), rng(20)])); 105 | } 106 | } 107 | exports.RegtestUtils = RegtestUtils; 108 | function _faucetRequestMaker(name, paramName, dhttp, url, pass) { 109 | return async (address, value) => 110 | dhttp({ 111 | method: 'POST', 112 | url: `${url}/r/${name}?${paramName}=${address}&value=${value}&key=${pass}`, 113 | }); 114 | } 115 | function _faucetMaker(self, _requester) { 116 | return async (address, value) => { 117 | let count = 0; 118 | let _unspents = []; 119 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); 120 | const randInt = (min, max) => 121 | min + Math.floor((max - min + 1) * Math.random()); 122 | const txId = await _requester(address, value).then( 123 | v => v, // Pass success value as is 124 | async err => { 125 | // Bad Request error is fixed by making sure height is >= 432 126 | const currentHeight = await self.height(); 127 | if (err.message === 'Bad Request' && currentHeight < 432) { 128 | await self.mine(432 - currentHeight); 129 | return _requester(address, value); 130 | } else if (err.message === 'Bad Request' && currentHeight >= 432) { 131 | return _requester(address, value); 132 | } else { 133 | throw err; 134 | } 135 | }, 136 | ); 137 | while (_unspents.length === 0) { 138 | if (count > 0) { 139 | if (count >= 5) { 140 | // Sometimes indexd takes more than 60 secs to sync. 141 | console.log('WARNING: Indexd is busy. Using getrawtransaction RPC.'); 142 | const tx = await self.fetch(txId); 143 | const outs = tx.outs.filter(x => x.address === address); 144 | const out = outs.pop(); 145 | if (out) { 146 | const vout = tx.outs.indexOf(out); 147 | const v = out.value; 148 | return { txId, vout, value: v }; 149 | } else { 150 | throw new Error('Missing Inputs'); 151 | } 152 | } 153 | console.log('Missing Inputs, retry #' + count); 154 | await sleep(randInt(150, 250)); 155 | } 156 | await sleep(randInt(50, 150)); 157 | const results = await self.unspents(address); 158 | _unspents = results.filter(x => x.txId === txId); 159 | count++; 160 | } 161 | return _unspents.pop(); 162 | }; 163 | } 164 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require('mocha') 2 | const assert = require('assert') 3 | const bitcoin = require('bitcoinjs-lib') 4 | const { RegtestUtils } = require('..') 5 | const regtestUtils = new RegtestUtils() 6 | const { network } = regtestUtils 7 | const sleep = ms => new Promise(r => setTimeout(r, ms)) 8 | 9 | describe('regtest utils', () => { 10 | it('should get the current height', async () => { 11 | assert.strictEqual(typeof (await regtestUtils.height()), 'number') 12 | }) 13 | it('should mine blocks', async () => { 14 | const results = await regtestUtils.mine(2) 15 | assert.strictEqual(Array.isArray(results), true) 16 | assert.strictEqual(!!results[0].match(/^[0-9a-f]+$/), true) 17 | }) 18 | it('should get random address', async () => { 19 | assert.strictEqual(typeof regtestUtils.randomAddress(), 'string') 20 | }) 21 | it('should have random address', async () => { 22 | assert.strictEqual(typeof regtestUtils.RANDOM_ADDRESS, 'string') 23 | }) 24 | 25 | it('should get faucet, broadcast, verify', async () => { 26 | const keyPair = bitcoin.ECPair.makeRandom({ network }) 27 | const p2pkh = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network }) 28 | 29 | const unspent = await regtestUtils.faucet(p2pkh.address, 2e4) 30 | 31 | const unspentComplex = await regtestUtils.faucetComplex(p2pkh.output, 1e4) 32 | 33 | await sleep(100); 34 | 35 | const unspents = await regtestUtils.unspents(p2pkh.address) 36 | 37 | const fetchedTx = await regtestUtils.fetch(unspent.txId) 38 | 39 | assert.strictEqual(fetchedTx.txId, unspent.txId) 40 | 41 | assert.deepStrictEqual( 42 | unspent, 43 | unspents.filter(v => v.value === unspent.value)[0], 44 | 'unspents must be equal' 45 | ) 46 | 47 | assert.deepStrictEqual( 48 | unspentComplex, 49 | unspents.filter(v => v.value === unspentComplex.value)[0], 50 | 'unspents must be equal' 51 | ) 52 | 53 | const txb = new bitcoin.TransactionBuilder(network) 54 | txb.addInput(unspent.txId, unspent.vout) 55 | txb.addInput(unspentComplex.txId, unspentComplex.vout) 56 | txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4) 57 | 58 | txb.sign({ 59 | prevOutScriptType: 'p2pkh', 60 | vin: 0, 61 | keyPair, 62 | }) 63 | txb.sign({ 64 | prevOutScriptType: 'p2pkh', 65 | vin: 1, 66 | keyPair, 67 | }) 68 | const tx = txb.build() 69 | 70 | // build and broadcast to the Bitcoin RegTest network 71 | await regtestUtils.broadcast(tx.toHex()) 72 | 73 | await regtestUtils.verify({ 74 | txId: tx.getId(), 75 | address: regtestUtils.RANDOM_ADDRESS, 76 | vout: 0, 77 | value: 1e4 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /ts_src/index.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as rng from 'randombytes'; 3 | const bs58check = require('bs58check'); 4 | 5 | interface Network { 6 | messagePrefix: string; 7 | bech32: string; 8 | bip32: Bip32; 9 | pubKeyHash: number; 10 | scriptHash: number; 11 | wif: number; 12 | } 13 | 14 | interface Bip32 { 15 | public: number; 16 | private: number; 17 | } 18 | 19 | type DhttpResponse = Unspent[] | Request | string | number | void | null; 20 | 21 | interface Unspent { 22 | value: number; 23 | txId: string; 24 | vout: number; 25 | address?: string; 26 | height?: number; 27 | } 28 | 29 | interface Input { 30 | txId: string; 31 | vout: number; 32 | script: string; 33 | sequence: string; 34 | } 35 | 36 | interface Output { 37 | value: number; 38 | script: string; 39 | address?: string; 40 | } 41 | 42 | interface Request { 43 | method?: string; 44 | url?: string; 45 | body?: string; 46 | } 47 | 48 | interface Transaction { 49 | txId: string; 50 | txHex: string; 51 | vsize: number; 52 | version: number; 53 | locktime: number; 54 | ins: Input[]; 55 | outs: Output[]; 56 | } 57 | 58 | interface RegUtilOpts { 59 | APIPASS?: string; 60 | APIURL?: string; 61 | } 62 | 63 | const dhttpCallback = require('dhttp/200'); 64 | 65 | let RANDOM_ADDRESS: string | undefined; 66 | 67 | export class RegtestUtils { 68 | network: Network; 69 | private _APIURL: string; 70 | private _APIPASS: string; 71 | 72 | constructor(_opts?: RegUtilOpts) { 73 | this._APIURL = 74 | (_opts || {}).APIURL || process.env.APIURL || 'http://127.0.0.1:8080/1'; 75 | this._APIPASS = (_opts || {}).APIPASS || process.env.APIPASS || 'satoshi'; 76 | // regtest network parameters 77 | this.network = { 78 | messagePrefix: '\x18Bitcoin Signed Message:\n', 79 | bech32: 'bcrt', 80 | bip32: { 81 | public: 0x043587cf, 82 | private: 0x04358394, 83 | }, 84 | pubKeyHash: 0x6f, 85 | scriptHash: 0xc4, 86 | wif: 0xef, 87 | }; 88 | } 89 | 90 | get RANDOM_ADDRESS(): string { 91 | if (RANDOM_ADDRESS === undefined) { 92 | RANDOM_ADDRESS = this.randomAddress(); 93 | } 94 | return RANDOM_ADDRESS; 95 | } 96 | 97 | // use Promises 98 | async dhttp(options: Request): Promise { 99 | return new Promise((resolve, reject): void => { 100 | return dhttpCallback(options, (err: Error, data: DhttpResponse) => { 101 | if (err) return reject(err); 102 | else return resolve(data); 103 | }); 104 | }); 105 | } 106 | 107 | async broadcast(txHex: string): Promise { 108 | return this.dhttp({ 109 | method: 'POST', 110 | url: this._APIURL + '/t/push', 111 | body: txHex, 112 | }) as Promise; 113 | } 114 | 115 | async mine(count: number): Promise { 116 | return this.dhttp({ 117 | method: 'POST', 118 | url: `${this._APIURL}/r/generate?count=${count}&key=${this._APIPASS}`, 119 | }) as Promise; 120 | } 121 | 122 | async height(): Promise { 123 | return this.dhttp({ 124 | method: 'GET', 125 | url: this._APIURL + '/b/best/height', 126 | }) as Promise; 127 | } 128 | 129 | async fetch(txId: string): Promise { 130 | return this.dhttp({ 131 | method: 'GET', 132 | url: `${this._APIURL}/t/${txId}/json`, 133 | }) as Promise; 134 | } 135 | 136 | async unspents(address: string): Promise { 137 | return this.dhttp({ 138 | method: 'GET', 139 | url: `${this._APIURL}/a/${address}/unspents`, 140 | }) as Promise; 141 | } 142 | 143 | async faucet(address: string, value: number): Promise { 144 | const requester = _faucetRequestMaker( 145 | 'faucet', 146 | 'address', 147 | this.dhttp, 148 | this._APIURL, 149 | this._APIPASS, 150 | ); 151 | const faucet = _faucetMaker(this, requester); 152 | return faucet(address, value); 153 | } 154 | 155 | async faucetComplex(output: Buffer, value: number): Promise { 156 | const outputString = output.toString('hex'); 157 | const requester = _faucetRequestMaker( 158 | 'faucetScript', 159 | 'script', 160 | this.dhttp, 161 | this._APIURL, 162 | this._APIPASS, 163 | ); 164 | const faucet = _faucetMaker(this, requester); 165 | return faucet(outputString, value); 166 | } 167 | 168 | async verify(txo: Unspent): Promise { 169 | const tx = await this.fetch(txo.txId); 170 | 171 | const txoActual = tx.outs[txo.vout]; 172 | if (txo.address) assert.strictEqual(txoActual.address, txo.address); 173 | if (txo.value) assert.strictEqual(txoActual.value, txo.value); 174 | } 175 | 176 | randomAddress(): string { 177 | // Fake P2PKH address with regtest/testnet version byte 178 | return bs58check.encode(Buffer.concat([Buffer.from([0x6f]), rng(20)])); 179 | } 180 | } 181 | 182 | function _faucetRequestMaker( 183 | name: string, 184 | paramName: string, 185 | dhttp: any, 186 | url: string, 187 | pass: string, 188 | ): (address: string, value: number) => Promise { 189 | return async (address: string, value: number): Promise => 190 | dhttp({ 191 | method: 'POST', 192 | url: `${url}/r/${name}?${paramName}=${address}&value=${value}&key=${pass}`, 193 | }) as Promise; 194 | } 195 | 196 | function _faucetMaker( 197 | self: RegtestUtils, 198 | _requester: (address: string, value: number) => Promise, 199 | ): (address: string, value: number) => Promise { 200 | return async (address: string, value: number): Promise => { 201 | let count = 0; 202 | let _unspents: Unspent[] = []; 203 | const sleep = (ms: number): Promise => 204 | new Promise((resolve): number => setTimeout(resolve, ms)); 205 | const randInt = (min: number, max: number): number => 206 | min + Math.floor((max - min + 1) * Math.random()); 207 | const txId = await _requester(address, value).then( 208 | v => v, // Pass success value as is 209 | async err => { 210 | // Bad Request error is fixed by making sure height is >= 432 211 | const currentHeight = (await self.height()) as number; 212 | if (err.message === 'Bad Request' && currentHeight < 432) { 213 | await self.mine(432 - currentHeight); 214 | return _requester(address, value); 215 | } else if (err.message === 'Bad Request' && currentHeight >= 432) { 216 | return _requester(address, value); 217 | } else { 218 | throw err; 219 | } 220 | }, 221 | ); 222 | while (_unspents.length === 0) { 223 | if (count > 0) { 224 | if (count >= 5) { 225 | // Sometimes indexd takes more than 60 secs to sync. 226 | console.log('WARNING: Indexd is busy. Using getrawtransaction RPC.'); 227 | const tx = await self.fetch(txId); 228 | const outs = tx.outs.filter(x => x.address === address); 229 | const out = outs.pop(); 230 | if (out) { 231 | const vout = tx.outs.indexOf(out); 232 | const v = out.value; 233 | return { txId, vout, value: v }; 234 | } else { 235 | throw new Error('Missing Inputs'); 236 | } 237 | } 238 | console.log('Missing Inputs, retry #' + count); 239 | await sleep(randInt(150, 250)); 240 | } 241 | 242 | await sleep(randInt(50, 150)); 243 | 244 | const results = await self.unspents(address); 245 | 246 | _unspents = results.filter(x => x.txId === txId); 247 | 248 | count++; 249 | } 250 | 251 | return _unspents.pop()!; 252 | }; 253 | } 254 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "outDir": "./src", 6 | "declaration": true, 7 | "declarationDir": "./types", 8 | "rootDir": "./ts_src", 9 | "types": [ 10 | "node" 11 | ], 12 | "allowJs": false, 13 | "strict": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true, 17 | "strictBindCallApply": true, 18 | "strictPropertyInitialization": true, 19 | "noImplicitThis": true, 20 | "alwaysStrict": true, 21 | "esModuleInterop": false, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true 24 | }, 25 | "include": [ 26 | "ts_src/**/*.ts" 27 | ], 28 | "exclude": [ 29 | "**/*.spec.ts", 30 | "node_modules/**/*" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "rules": { 5 | "arrow-parens": [true, "ban-single-arg-parens"], 6 | "curly": false, 7 | "indent": [ 8 | true, 9 | "spaces", 10 | 2 11 | ], 12 | "interface-name": [false], 13 | "match-default-export-name": true, 14 | "max-classes-per-file": [false], 15 | "member-access": [true, "no-public"], 16 | "no-bitwise": false, 17 | "no-console": false, 18 | "no-empty": [true, "allow-empty-catch"], 19 | "no-implicit-dependencies": true, 20 | "no-return-await": true, 21 | "no-var-requires": false, 22 | "no-unused-expression": false, 23 | "object-literal-sort-keys": false, 24 | "quotemark": [true, "single"], 25 | "typedef": [ 26 | true, 27 | "call-signature", 28 | "arrow-call-signature", 29 | "property-declaration" 30 | ], 31 | "variable-name": [ 32 | true, 33 | "ban-keywords", 34 | "check-format", 35 | "allow-leading-underscore", 36 | "allow-pascal-case" 37 | ] 38 | }, 39 | "rulesDirectory": [] 40 | } 41 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface Network { 3 | messagePrefix: string; 4 | bech32: string; 5 | bip32: Bip32; 6 | pubKeyHash: number; 7 | scriptHash: number; 8 | wif: number; 9 | } 10 | interface Bip32 { 11 | public: number; 12 | private: number; 13 | } 14 | declare type DhttpResponse = Unspent[] | Request | string | number | void | null; 15 | interface Unspent { 16 | value: number; 17 | txId: string; 18 | vout: number; 19 | address?: string; 20 | height?: number; 21 | } 22 | interface Input { 23 | txId: string; 24 | vout: number; 25 | script: string; 26 | sequence: string; 27 | } 28 | interface Output { 29 | value: number; 30 | script: string; 31 | address?: string; 32 | } 33 | interface Request { 34 | method?: string; 35 | url?: string; 36 | body?: string; 37 | } 38 | interface Transaction { 39 | txId: string; 40 | txHex: string; 41 | vsize: number; 42 | version: number; 43 | locktime: number; 44 | ins: Input[]; 45 | outs: Output[]; 46 | } 47 | interface RegUtilOpts { 48 | APIPASS?: string; 49 | APIURL?: string; 50 | } 51 | export declare class RegtestUtils { 52 | network: Network; 53 | private _APIURL; 54 | private _APIPASS; 55 | constructor(_opts?: RegUtilOpts); 56 | get RANDOM_ADDRESS(): string; 57 | dhttp(options: Request): Promise; 58 | broadcast(txHex: string): Promise; 59 | mine(count: number): Promise; 60 | height(): Promise; 61 | fetch(txId: string): Promise; 62 | unspents(address: string): Promise; 63 | faucet(address: string, value: number): Promise; 64 | faucetComplex(output: Buffer, value: number): Promise; 65 | verify(txo: Unspent): Promise; 66 | randomAddress(): string; 67 | } 68 | export {}; 69 | --------------------------------------------------------------------------------