├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── BitcoinJsonRpc.test.ts ├── BitcoinJsonRpc.ts ├── BitcoinJsonRpcError.ts ├── index.ts ├── json-rpc.ts ├── schemas.test.ts ├── schemas.ts ├── types.ts └── utils.ts ├── tsconfig.cjs.json └── tsconfig.esm.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | # yarn v2 111 | 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .pnp.* 116 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and not Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Stores VSCode versions used for testing VSCode extensions 107 | .vscode-test 108 | 109 | # yarn v2 110 | 111 | .yarn/cache 112 | .yarn/unplugged 113 | .yarn/build-state.yml 114 | .pnp.* 115 | 116 | src 117 | README.md 118 | 119 | .npmignore 120 | .gitignore 121 | tsconfig.json 122 | jest.config.* 123 | 124 | package-lock.json 125 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andreas Brekken 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-json-rpc 2 | 3 | Bitcoin JSON RPC for TypeScript with Response Type Enforcement 4 | 5 | ## Installing 6 | 7 | `npm install bitcoin-json-rpc` 8 | 9 | ## Example usage 10 | 11 | ```typescript 12 | import BitcoinJsonRpc from 'bitcoin-json-rpc'; 13 | 14 | const rpc = new BitcoinJsonRpc('http://localhost:8332'); 15 | 16 | const balance = await rpc.getBalance(); 17 | console.log(balance); 18 | ``` 19 | 20 | ## Non-standard methods 21 | 22 | Methods not exposed by Bitcoin Core, such as `omni_getwalletaddressbalances` 23 | or Liquid's `getbalance` are prefixed i.e. `getLiquidBalanceForAsset`. 24 | 25 | ## Motivation 26 | 27 | There are plenty of Bitcoin forks, including Omni, Zcash, and Blockstream Liquid. 28 | These forks often introduce breaking changes to the JSON RPC responses that 29 | are not reflected in their documentation. 30 | 31 | This library provides TypeScript types for RPC commands and validates the responses 32 | using `io-ts`. If the response does not match the schema, an error is thrown. 33 | 34 | ## Author 35 | 36 | Andreas Brekken 37 | 38 | ## License 39 | 40 | MIT 41 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/src'], 5 | moduleNameMapper: { 6 | // Rewrite ./foo/bar.js to ./foo/bar 7 | '^(\\..+)\\.js': '$1', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitcoin-json-rpc", 3 | "version": "1.4.1", 4 | "description": "Bitcoin JSON RPC with type validation", 5 | "type": "module", 6 | "main": "dist/cjs/index.js", 7 | "exports": { 8 | "require": "./dist/cjs/index.js", 9 | "import": "./dist/esm/index.js" 10 | }, 11 | "scripts": { 12 | "build:cjs": "tsc -p tsconfig.cjs.json", 13 | "build:esm": "tsc -p tsconfig.esm.json", 14 | "build": "npm run build:cjs && npm run build:esm", 15 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", 16 | "prepublishOnly": "npm run build" 17 | }, 18 | "keywords": [], 19 | "author": "Andreas Brekken ", 20 | "repository": "github:abrkn/bitcoin-json-rpc", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@types/debug": "4.1.12", 24 | "@types/jest": "^29.5.12", 25 | "@types/node": "12.12.35", 26 | "jest": "^29.7.0", 27 | "ts-jest": "^29.1.4", 28 | "typescript": "^5.4.5" 29 | }, 30 | "dependencies": { 31 | "axios": "^0.21.1", 32 | "debug": ">=4.3.5", 33 | "delay": ">=4.3.0 <6", 34 | "zod": "^3.23.8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/BitcoinJsonRpc.test.ts: -------------------------------------------------------------------------------- 1 | import { jsonRpcCmd } from './json-rpc'; 2 | import BitcoinJsonRpc from './BitcoinJsonRpc'; 3 | 4 | jest.mock('delay'); 5 | 6 | jest.mock('./json-rpc', () => ({ 7 | jsonRpcCmd: jest.fn(), 8 | })); 9 | 10 | describe('bitcoin-json-rpc', () => { 11 | const rpc = new BitcoinJsonRpc('https://localhost'); 12 | 13 | // When there's not enough funds there's no reason to retry 14 | it('should not retry insufficient funds', () => { 15 | expect.assertions(5); 16 | 17 | (jsonRpcCmd as jest.Mock).mockImplementationOnce(async (url: string, method: string, params: any) => { 18 | expect(method).toBe('sendtoaddress'); 19 | expect(params).toEqual(['1Bitcoin', '1.2']); 20 | 21 | throw new Error('Insufficient funds'); 22 | }); 23 | 24 | return rpc.sendToAddress('1Bitcoin', '1.2').catch((error) => { 25 | const { bitcoinJsonRpc } = error.data; 26 | expect(bitcoinJsonRpc.methodIsPure).toBe(false); 27 | expect(bitcoinJsonRpc.attempts).toBe(1); 28 | expect(error.executed).toBe(false); 29 | }); 30 | }); 31 | 32 | it('should retry getRawMempool', () => { 33 | expect.assertions(3); 34 | 35 | (jsonRpcCmd as jest.Mock) 36 | .mockImplementationOnce(async (url: string, method: string, params: any) => { 37 | expect(method).toBe('getrawmempool'); 38 | 39 | throw new Error(); 40 | }) 41 | .mockImplementationOnce(async (url: string, method: string, params: any) => { 42 | expect(method).toBe('getrawmempool'); 43 | 44 | return ['one', 'two']; 45 | }); 46 | 47 | return rpc.getRawMempool().then((result) => { 48 | expect(result).toEqual(['one', 'two']); 49 | }); 50 | }); 51 | 52 | it('should retry send on ECONNREFUSED', () => { 53 | expect.assertions(5); 54 | 55 | (jsonRpcCmd as jest.Mock) 56 | .mockImplementationOnce(async (url: string, method: string, params: any) => { 57 | expect(method).toBe('sendtoaddress'); 58 | expect(params).toEqual(['1Bitcoin', '1.2']); 59 | 60 | throw new Error('ECONNREFUSED'); 61 | }) 62 | .mockImplementationOnce(async (url: string, method: string, params: any) => { 63 | expect(method).toBe('sendtoaddress'); 64 | expect(params).toEqual(['1Bitcoin', '1.2']); 65 | 66 | return 'hash'; 67 | }); 68 | 69 | return rpc.sendToAddress('1Bitcoin', '1.2').then((result) => { 70 | expect(result).toEqual('hash'); 71 | }); 72 | }); 73 | 74 | it('throw if getBalance returns an object', () => { 75 | // expect.assertions(5); 76 | 77 | (jsonRpcCmd as jest.Mock).mockImplementationOnce(async (url: string, method: string, params: any) => { 78 | expect(url).toBe('https://localhost'); 79 | expect(method).toBe('getbalance'); 80 | expect(params).toEqual([]); 81 | 82 | return { balance: 1 }; 83 | }); 84 | 85 | return rpc.getBalance().catch((error) => { 86 | expect(error.executed).toBe(true); 87 | }); 88 | }); 89 | 90 | describe('sendToAddress', () => { 91 | it('should fall back to defaults', async () => { 92 | (jsonRpcCmd as jest.Mock) 93 | .mockImplementationOnce(async (url: string, method: string, params: any) => { 94 | expect(url).toBe('https://localhost'); 95 | expect(method).toBe('sendtoaddress'); 96 | expect(params).toEqual(['1address', '1.01', '', '', false, true]); 97 | 98 | return 'hash'; 99 | }) 100 | .mockImplementationOnce(async (url: string, method: string, params: any) => { 101 | expect(url).toBe('https://localhost'); 102 | expect(method).toBe('sendtoaddress'); 103 | expect(params).toEqual(['1address', '1.01', 'something', 'someone']); 104 | 105 | return 'hash'; 106 | }) 107 | .mockImplementationOnce(async (url: string, method: string, params: any) => { 108 | expect(url).toBe('https://localhost'); 109 | expect(method).toBe('sendtoaddress'); 110 | expect(params).toEqual(['1address', '1.01', '', 'someone', true, true]); 111 | 112 | return 'hash'; 113 | }); 114 | 115 | let result = await rpc.sendToAddress('1address', '1.01', undefined, undefined, undefined, true); 116 | expect(result).toBe('hash'); 117 | result = await rpc.sendToAddress('1address', '1.01', 'something', 'someone'); 118 | expect(result).toBe('hash'); 119 | result = await rpc.sendToAddress('1address', '1.01', undefined, 'someone', true, true); 120 | expect(result).toBe('hash'); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /src/BitcoinJsonRpc.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import delay from 'delay'; 3 | import createDebug from 'debug'; 4 | import { CreateBitcoinJsonRpcOptions, BitcoinFeeEstimateMode } from './types.js'; 5 | import { jsonRpcCmd } from './json-rpc.js'; 6 | import { PURE_METHODS, getWasExecutedFromError, getShouldRetry } from './utils.js'; 7 | import { BitcoinJsonRpcError } from './BitcoinJsonRpcError.js'; 8 | import * as schemas from './schemas.js'; 9 | 10 | const MAX_ATTEMPTS = 5; 11 | const DELAY_BETWEEN_ATTEMPTS = 5000; 12 | 13 | const debug = createDebug('bitcoin-json-rpc'); 14 | 15 | export default class BitcoinJsonRpc { 16 | constructor(readonly url: string, readonly options: CreateBitcoinJsonRpcOptions = {}) { 17 | this.url = url; 18 | this.options = options; 19 | } 20 | 21 | public cmd(method: string, ...params: any[]): Promise { 22 | return jsonRpcCmd(this.url, method, params); 23 | } 24 | 25 | public cmdWithRetry(method: string, ...params: any[]): Promise { 26 | const methodIsPure = PURE_METHODS.includes(method); 27 | const maxAttempts = MAX_ATTEMPTS; 28 | 29 | const attempt: (attemptN?: number) => any = async (attemptN = 1) => { 30 | const getErrrorData = () => ({ 31 | bitcoinJsonRpc: { 32 | method, 33 | params, 34 | methodIsPure, 35 | maxAttempts, 36 | attempts: attemptN, 37 | }, 38 | }); 39 | 40 | try { 41 | const result = await this.cmd(method, ...params); 42 | return result; 43 | } catch (error: any) { 44 | 45 | const executed = getWasExecutedFromError(method, error); 46 | const hadEffects = !methodIsPure && executed !== false; 47 | const shouldRetry = !hadEffects && getShouldRetry(method, error); 48 | 49 | debug(`Command failed: ${error.message}`, { 50 | method, 51 | methodIsPure, 52 | params, 53 | executed, 54 | attemptN, 55 | maxAttempts, 56 | hadEffects, 57 | shouldRetry, 58 | }); 59 | 60 | if (attemptN === maxAttempts) { 61 | throw new BitcoinJsonRpcError(error, executed, getErrrorData()); 62 | } 63 | 64 | if (shouldRetry) { 65 | await delay(DELAY_BETWEEN_ATTEMPTS); 66 | 67 | // NOTE: Stack deepening 68 | return attempt(attemptN + 1); 69 | } 70 | 71 | debug(`Cannot retry`, { 72 | method, 73 | methodIsPure, 74 | executed, 75 | attemptN, 76 | maxAttempts, 77 | }); 78 | 79 | throw new BitcoinJsonRpcError(error, executed, getErrrorData()); 80 | } 81 | }; 82 | 83 | return attempt(); 84 | } 85 | 86 | private async cmdWithRetryAndParse( 87 | schema: z.ZodSchema, 88 | method: string, 89 | ...params: any[] 90 | ): Promise { 91 | const unsafe = await this.cmdWithRetry(method, ...params); 92 | 93 | try { 94 | const parsed = schema.parse(unsafe); 95 | 96 | return parsed; 97 | } catch (error: any) { 98 | throw Object.assign(error, { executed: true }); 99 | } 100 | } 101 | 102 | public async sendRawTransaction(hex: string) { 103 | return this.cmdWithRetryAndParse(schemas.sendRawTransactionResultSchema, 'sendrawtransaction', hex); 104 | } 105 | 106 | // https://bitcoin-rpc.github.io/en/doc/0.17.99/rpc/wallet/sendtoaddress/ 107 | public async sendToAddress(address: string, amount: string, comment?: string, commentTo?: string, subtractFeeFromAmount?: boolean, replaceable?: boolean) { 108 | const params: any[] = [address, amount]; 109 | 110 | if (replaceable !== undefined) { 111 | // Argument #6 112 | params.push(comment ?? '', commentTo ?? '', subtractFeeFromAmount ?? false, replaceable); 113 | } else if (subtractFeeFromAmount !== undefined) { 114 | // Argument #5 115 | params.push(comment ?? '', commentTo ?? '', subtractFeeFromAmount); 116 | } else if (commentTo !== undefined) { 117 | // Argument #4 118 | params.push(comment ?? '', commentTo); 119 | } else if (commentTo) { 120 | // Argument #3 121 | params.push(comment); 122 | } 123 | 124 | return this.cmdWithRetryAndParse(schemas.sendToAddressResultSchema, 'sendtoaddress', ...params); 125 | } 126 | 127 | public async signRawTransactionWithWallet(hex: string) { 128 | return this.cmdWithRetryAndParse( 129 | schemas.signRawTransactionWithWalletResultSchema, 130 | 'signrawtransactionwithwallet', 131 | hex 132 | ); 133 | } 134 | 135 | public async lockUnspent(unlock: boolean, transactions: { txid: string; vout: number }[]) { 136 | return this.cmdWithRetryAndParse(schemas.lockUnspentResultSchema, 'lockunspent', unlock, transactions); 137 | } 138 | 139 | // Arguments: 140 | // 1. "inputs" (array, required) A json array of json objects 141 | // [ 142 | // { 143 | // "txid":"id", (string, required) The transaction id 144 | // "vout":n, (numeric, required) The output number 145 | // "sequence":n (numeric, optional) The sequence number 146 | // } 147 | // ,... 148 | // ] 149 | // 2. "outputs" (array, required) a json array with outputs (key-value pairs) 150 | // [ 151 | // { 152 | // "address": x.xxx, (obj, optional) A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in BCH 153 | // }, 154 | // { 155 | // "data": "hex" (obj, optional) A key-value pair. The key must be "data", the value is hex encoded data 156 | // } 157 | // ,... More key-value pairs of the above form. For compatibility reasons, a dictionary, which holds the key-value pairs directly, is also 158 | // accepted as second parameter. 159 | // ] 160 | // 3. locktime (numeric, optional, default=0) Raw locktime. Non-0 value also locktime-activates inputs 161 | // Result: 162 | // "transaction" (string) hex string of the transaction 163 | public async createRawTransaction( 164 | inputs: { txid: string; vout: number; sequence?: number }[], 165 | outputs: Record, 166 | lockTime?: number 167 | ) { 168 | return this.cmdWithRetryAndParse( 169 | schemas.createRawTransactionResultSchema, 170 | 'createrawtransaction', 171 | inputs, 172 | outputs, 173 | lockTime 174 | ); 175 | } 176 | 177 | // Arguments: 178 | // 1. hexstring (string, required) The hex string of the raw transaction 179 | // 2. options (json object, optional) for backward compatibility: passing in a true instead of an object will result in {"includeWatching":true} 180 | // { 181 | // "changeAddress": "str", (string, optional, default=pool address) The bitcoin address to receive the change 182 | // "changePosition": n, (numeric, optional, default=random) The index of the change output 183 | // "change_type": "str", (string, optional, default=set by -changetype) The output type to use. Only valid if changeAddress is not specified. Options are "legacy", "p2sh-segwit", and "bech32". 184 | // "includeWatching": bool, (boolean, optional, default=true for watch-only wallets, otherwise false) Also select inputs which are watch only. 185 | // Only solvable inputs can be used. Watch-only destinations are solvable if the public key and/or output script was imported, 186 | // e.g. with 'importpubkey' or 'importmulti' with the 'pubkeys' or 'desc' field. 187 | // "lockUnspents": bool, (boolean, optional, default=false) Lock selected unspent outputs 188 | // "feeRate": amount, (numeric or string, optional, default=not set: makes wallet determine the fee) Set a specific fee rate in BTC/kB 189 | // "subtractFeeFromOutputs": [ (json array, optional, default=empty array) A json array of integers. 190 | // The fee will be equally deducted from the amount of each specified output. 191 | // Those recipients will receive less bitcoins than you enter in their corresponding amount field. 192 | // If no outputs are specified here, the sender pays the fee. 193 | // vout_index, (numeric) The zero-based output index, before a change output is added. 194 | // ... 195 | // ], 196 | // "replaceable": bool, (boolean, optional, default=wallet default) Marks this transaction as BIP125 replaceable. 197 | // Allows this transaction to be replaced by a transaction with higher fees 198 | // "conf_target": n, (numeric, optional, default=wallet default) Confirmation target (in blocks) 199 | // "estimate_mode": "str", (string, optional, default=UNSET) The fee estimate mode, must be one of: 200 | // "UNSET" 201 | // "ECONOMICAL" 202 | // "CONSERVATIVE" 203 | // } 204 | // 3. iswitness (boolean, optional, default=depends on heuristic tests) Whether the transaction hex is a serialized witness transaction. 205 | // If iswitness is not present, heuristic tests will be used in decoding. 206 | // If true, only witness deserialization will be tried. 207 | // If false, only non-witness deserialization will be tried. 208 | // This boolean should reflect whether the transaction has inputs 209 | // (e.g. fully valid, or on-chain transactions), if known by the caller. 210 | // Result: 211 | // { 212 | // "hex": "value", (string) The resulting raw transaction (hex-encoded string) 213 | // "fee": n, (numeric) Fee in BTC the resulting transaction pays 214 | // "changepos": n (numeric) The position of the added change output, or -1 215 | // } 216 | public async fundRawTransaction( 217 | hex: string, 218 | options: { 219 | changeAddress?: string, 220 | changePosition?: number, 221 | change_type?: string, 222 | includeWatching?: boolean, 223 | lockUnspents?: boolean, 224 | feeRate?: number, 225 | subtractFeeFromOutputs?: number[], 226 | replaceable?: boolean, 227 | conf_target?: number, 228 | estimate_mode?: BitcoinFeeEstimateMode 229 | }, 230 | iswitness?: boolean 231 | ) { 232 | //@todo impl with iswitness option 233 | return this.cmdWithRetryAndParse( 234 | schemas.fundRawTransactionResultSchema, 235 | 'fundrawtransaction', 236 | hex, 237 | options 238 | ); 239 | } 240 | 241 | // Arguments: 242 | // 1. "address" (string, required) The bitcoin address to send to. 243 | // 2. "amount" (numeric or string, required) The amount in BTC to send. eg 0.1 244 | // 3. "comment" (string, optional) A comment used to store what the transaction is for. 245 | // This is not part of the transaction, just kept in your wallet. 246 | // 4. "comment_to" (string, optional) A comment to store the name of the person or organization 247 | // to which you're sending the transaction. This is not part of the 248 | // transaction, just kept in your wallet. 249 | // 5. subtractfeefromamount (boolean, optional, default=false) The fee will be deducted from the amount being sent. 250 | // The recipient will receive less bitcoins than you enter in the amount field. 251 | // 6. replaceable (boolean, optional) Allow this transaction to be replaced by a transaction with higher fees via BIP 125 252 | // 7. conf_target (numeric, optional) Confirmation target (in blocks) 253 | // 8. "estimate_mode" (string, optional, default=UNSET) The fee estimate mode, must be one of: 254 | // "UNSET" 255 | // "ECONOMICAL" 256 | // "CONSERVATIVE" 257 | // 9. avoid_reuse (boolean, optional, default=true) (only available if avoid_reuse wallet flag is set) 258 | // Avoid spending from dirty addresses; addresses are considered 259 | // dirty if they have previously been used in a transaction. 260 | // If true, this also activates avoidpartialspends, grouping outputs by their addresses. 261 | // 10. assetlabel (string, optional) Hex asset id or asset label for balance. 262 | public async liquidSendToAddress( 263 | address: string, 264 | amount: string, 265 | comment: string | null, 266 | commentTo: string | null, 267 | subtractFeeFromAmount: boolean | null, 268 | replaceable: boolean | null, 269 | confTarget: number | null, 270 | estimateMode: BitcoinFeeEstimateMode | null, 271 | avoidReuse: boolean | null, 272 | asset: string | null 273 | ) { 274 | return this.cmdWithRetryAndParse( 275 | schemas.sendToAddressResultSchema, 276 | 'sendtoaddress', 277 | address, 278 | amount, 279 | comment, 280 | commentTo, 281 | subtractFeeFromAmount, 282 | replaceable, 283 | confTarget, 284 | estimateMode, 285 | avoidReuse, 286 | asset 287 | ); 288 | } 289 | 290 | public async getTransaction(txhash: string) { 291 | return this.cmdWithRetryAndParse(schemas.getTransactionResultSchema, 'gettransaction', txhash); 292 | } 293 | 294 | public async liquidGetTransaction(txhash: string) { 295 | return this.cmdWithRetryAndParse(schemas.liquidGetTransactionResultSchema, 'gettransaction', txhash); 296 | } 297 | 298 | public async getInfo() { 299 | return this.cmdWithRetryAndParse(schemas.getInfoResultSchema, 'getinfo'); 300 | } 301 | 302 | public async getBlockchainInfo() { 303 | return this.cmdWithRetryAndParse(schemas.getBlockchainInfoResultSchema, 'getblockchaininfo'); 304 | } 305 | 306 | public async getRawTransactionAsObject(txhash: string) { 307 | return this.cmdWithRetryAndParse(schemas.getRawTransactionAsObjectResultSchema, 'getrawtransaction', txhash, 1); 308 | } 309 | 310 | public async getBlockHashFromHeight(height: number) { 311 | return this.cmdWithRetryAndParse(schemas.getBlockHashFromHeightResultSchema, 'getblockhash', height); 312 | } 313 | 314 | public async getBlockFromHash(blockHash: string) { 315 | return this.cmdWithRetryAndParse(schemas.getBlockFromHashResultSchema, 'getblock', blockHash); 316 | } 317 | 318 | public async getBlockCount() { 319 | return this.cmdWithRetryAndParse(schemas.getBlockCountResultSchema, 'getblockcount'); 320 | } 321 | 322 | public async getRawMempool() { 323 | return this.cmdWithRetryAndParse(schemas.getRawMempoolResultSchema, 'getrawmempool'); 324 | } 325 | 326 | public async validateAddress(address: string) { 327 | return this.cmdWithRetryAndParse(schemas.validateAddressResultSchema, 'validateaddress', address); 328 | } 329 | 330 | public async liquidValidateAddress(address: string) { 331 | return this.cmdWithRetryAndParse(schemas.liquidValidateAddressResultSchema, 'validateaddress', address); 332 | } 333 | 334 | public async getNewAddress() { 335 | return this.cmdWithRetryAndParse(schemas.getNewAddressResultSchema, 'getnewaddress'); 336 | } 337 | 338 | public async getBalance(minConf = 0) { 339 | return this.cmdWithRetryAndParse(schemas.getBalanceResultSchema, 'getbalance', '*', minConf); 340 | } 341 | 342 | public async generateToAddress(nblocks: number, address:string) { 343 | return this.cmdWithRetryAndParse(schemas.generateToAddressResultSchema, 'generatetoaddress', nblocks, address); 344 | } 345 | 346 | public async getLiquidBalanceForAsset( 347 | minConf: number | null = null, 348 | includeWatchOnly: boolean | null = null, 349 | avoidReuse: boolean | null = null, 350 | assetLabel: string 351 | ) { 352 | return this.cmdWithRetryAndParse( 353 | schemas.getLiquidBalanceForAssetResultSchema, 354 | 'getbalance', 355 | '*', 356 | minConf, 357 | includeWatchOnly, 358 | avoidReuse, 359 | assetLabel 360 | ); 361 | } 362 | 363 | public async getLiquidBalance( 364 | minConf: number | null = null, 365 | includeWatchOnly: boolean | null = null, 366 | assetLabel: string 367 | ) { 368 | return this.cmdWithRetryAndParse( 369 | schemas.getLiquidBalanceResultSchema, 370 | 'getbalance', 371 | '*', 372 | minConf, 373 | includeWatchOnly 374 | ); 375 | } 376 | 377 | public async omniGetWalletAddressBalances() { 378 | return this.cmdWithRetryAndParse( 379 | schemas.omniGetWalletAddressBalancesResultSchema, 380 | 'omni_getwalletaddressbalances' 381 | ); 382 | } 383 | 384 | public async ancientGetInfo() { 385 | return this.cmdWithRetryAndParse(schemas.ancientGetInfoResultSchema, 'getinfo'); 386 | } 387 | 388 | // Arguments: 389 | // 1. fromaddress (string, required) the address to send the tokens from 390 | // 2. toaddress (string, required) the address of the receiver 391 | // 3. propertyid (number, required) the identifier of the tokens to send 392 | // 4. amount (string, required) the amount to send 393 | // 5. feeaddress (string, required) the address that is used for change and to pay for fees, if needed 394 | 395 | // Result: 396 | // "hash" (string) the hex-encoded transaction hash 397 | public async omniFundedSend( 398 | fromAddress: string, 399 | toAddress: string, 400 | propertyId: number, 401 | amount: string, 402 | feeAddress: string 403 | ) { 404 | return this.cmdWithRetryAndParse( 405 | schemas.omniFundedSendResultSchema, 406 | 'omni_funded_send', 407 | fromAddress, 408 | toAddress, 409 | propertyId, 410 | amount, 411 | feeAddress 412 | ); 413 | } 414 | 415 | public async omniFundedSendAll(fromAddress: string, toAddress: string, ecosystem: 1 | 2, feeAddress: string) { 416 | return this.cmdWithRetryAndParse( 417 | schemas.omniFundedSendAllResultSchema, 418 | 'omni_funded_sendall', 419 | fromAddress, 420 | toAddress, 421 | ecosystem, 422 | feeAddress 423 | ); 424 | } 425 | 426 | public async omniGetTransaction(txid: string) { 427 | return this.cmdWithRetryAndParse(schemas.omniGetTransactionResultSchema, 'omni_gettransaction', txid); 428 | } 429 | 430 | public async omniListPendingTransactions() { 431 | return this.cmdWithRetryAndParse(schemas.omniListPendingTransactionsSchema, 'omni_listpendingtransactions'); 432 | } 433 | 434 | public async zcashGetOperationResult(operationIds: string[]) { 435 | return this.cmdWithRetryAndParse(schemas.zcashGetOperationResultSchema, 'z_getoperationresult', operationIds); 436 | } 437 | 438 | public async zcashGetBalanceForAddress(address: string) { 439 | return this.cmdWithRetryAndParse(schemas.zcashGetBalanceForAddressSchema, 'z_getbalance', address); 440 | } 441 | 442 | public async zcashSendMany( 443 | fromAddress: string, 444 | amounts: { 445 | address: string; 446 | amount: number; 447 | memo?: string; 448 | }[], 449 | minConf?: number, 450 | fee?: number, 451 | privacyPolicy?: string 452 | ) { 453 | const args: any[] = [fromAddress, amounts]; 454 | 455 | if (minConf !== undefined) { 456 | args.push(minConf); 457 | 458 | if (fee !== undefined) { 459 | args.push(fee); 460 | 461 | if (privacyPolicy !== undefined) { 462 | args.push(privacyPolicy); 463 | } 464 | } 465 | } else if (fee !== undefined) { 466 | throw new Error('Cannot specify fee without specifying minConf'); 467 | } 468 | 469 | return this.cmdWithRetryAndParse(schemas.zcashSendManySchema, 'z_sendmany', ...args); 470 | } 471 | 472 | public async zcashValidateAddress(address: string) { 473 | return this.cmdWithRetryAndParse(schemas.zcashValidateAddressSchema, 'z_validateaddress', address); 474 | } 475 | 476 | // Arguments: 477 | // 1. fromaddress (string, required) the address to send from 478 | // 2. toaddress (string, required) the address of the receiver 479 | // 3. propertyid (number, required) the identifier of the tokens to send 480 | // 4. amount (string, required) the amount to send 481 | // 5. redeemaddress (string, optional) an address that can spend the transaction dust (sender by default) 482 | // 6. referenceamount (string, optional) a bitcoin amount that is sent to the receiver (minimal by default) 483 | public async omniSend(fromAddress: string, toAddress: string, propertyId: number, amount: string) { 484 | return this.cmdWithRetryAndParse( 485 | schemas.omniSendSchema, 486 | 'omni_send', 487 | fromAddress, 488 | toAddress, 489 | propertyId, 490 | amount 491 | ); 492 | } 493 | 494 | public async zcashGetNewAddress(type?: string) { 495 | const args: any[] = type === undefined ? [] : [type]; 496 | 497 | return this.cmdWithRetryAndParse(schemas.zcashGetNewAddressSchema, 'z_getnewaddress', ...args); 498 | } 499 | 500 | public async zcashListUnspent(minConf?: number) { 501 | const args: any[] = minConf === undefined ? [] : [minConf]; 502 | 503 | return this.cmdWithRetryAndParse(schemas.zcashListUnspentSchema, 'z_listunspent', ...args); 504 | } 505 | 506 | public async listUnspent(minConf?: number) { 507 | const args: any[] = minConf === undefined ? [] : [minConf]; 508 | 509 | return this.cmdWithRetryAndParse(schemas.listUnspentSchema, 'listunspent', ...args); 510 | } 511 | 512 | public async dumpPrivateKey(address: string) { 513 | return this.cmdWithRetryAndParse(schemas.dumpPrivateKeySchema, 'dumpprivkey', address); 514 | } 515 | 516 | public async ecashIsFinalTransaction(txid: string, blockhash?: string) { 517 | return this.cmdWithRetryAndParse(schemas.ecashIsFinalTransactionSchema, 'isfinaltransaction', txid, blockhash); 518 | } 519 | 520 | public async isReady() { 521 | try { 522 | if (this.options.ancient === true) { 523 | await this.ancientGetInfo(); 524 | } else { 525 | await this.getBlockchainInfo(); 526 | } 527 | 528 | return true; 529 | } catch (error) { 530 | return false; 531 | } 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /src/BitcoinJsonRpcError.ts: -------------------------------------------------------------------------------- 1 | export class BitcoinJsonRpcError extends Error { 2 | /** 3 | * Whether the command executed. true is definiyely yes, false if definitely no, else null 4 | */ 5 | public readonly executed: boolean | null; 6 | 7 | public data: any; 8 | 9 | constructor(inner: Error & { data?: any }, executed: boolean | null, data?: any) { 10 | super(inner.message); 11 | 12 | this.executed = executed; 13 | this.data = Object.assign( 14 | {}, 15 | inner.data, 16 | { 17 | bitcoinJsonRpc: { 18 | executed, 19 | }, 20 | }, 21 | data 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import BitcoinJsonRpc from './BitcoinJsonRpc.js'; 2 | export * from './json-rpc.js'; 3 | export * from './types.js'; 4 | export * from './BitcoinJsonRpcError.js'; 5 | export * as schemas from './schemas.js'; 6 | export default BitcoinJsonRpc; 7 | -------------------------------------------------------------------------------- /src/json-rpc.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from 'axios'; 2 | import createDebug from 'debug'; 3 | import { throwIfErrorInResponseDataWithExtraProps, maybeShortenErrorMessage } from './utils.js'; 4 | 5 | const debug = createDebug('bitcoin-json-rpc'); 6 | 7 | const MAX_LOG_LENGTH = 250; 8 | 9 | export const jsonRpcCmd: (url: string, method: string, params?: any) => Promise = async ( 10 | url: string, 11 | method: string, 12 | params: any[], 13 | _options: object | undefined = {} 14 | ) => { 15 | const payload = { 16 | jsonrpc: '2.0', 17 | id: 1, 18 | method, 19 | params, 20 | }; 21 | 22 | debug(`--> REQ`, payload); 23 | 24 | let response: AxiosResponse; 25 | 26 | try { 27 | response = await axios.post(url, payload); 28 | } catch (error) { 29 | const errorAsAny = error as any; 30 | 31 | if (errorAsAny.response && errorAsAny.response.data) { 32 | throwIfErrorInResponseDataWithExtraProps(errorAsAny.response.data, errorAsAny, { 33 | data: { 34 | jsonRpcRequest: { 35 | url, 36 | method, 37 | params, 38 | }, 39 | }, 40 | }); 41 | } 42 | 43 | throw error; 44 | } 45 | 46 | const { data } = response; 47 | const contentTypeIsJson = response.headers['content-type'] === 'application/json'; 48 | 49 | // NOTE: Incorrect if the response is actually a JSON string? 50 | const dataStrict = contentTypeIsJson && typeof data === 'string' ? JSON.parse(data) : data; 51 | 52 | if (dataStrict !== undefined) { 53 | throwIfErrorInResponseDataWithExtraProps(dataStrict, undefined, { 54 | data: { 55 | jsonRpcRequest: { 56 | url, 57 | method, 58 | params, 59 | }, 60 | }, 61 | }); 62 | } 63 | 64 | const { result } = dataStrict; 65 | 66 | if (result === undefined) { 67 | const dataAsText = typeof dataStrict === 'string' ? dataStrict : JSON.stringify(dataStrict); 68 | 69 | throw Object.assign(new Error(maybeShortenErrorMessage(`Result missing from ${dataAsText}`)), { 70 | data: { 71 | jsonRpcRequest: { 72 | url, 73 | method, 74 | params, 75 | }, 76 | }, 77 | }); 78 | } 79 | 80 | const resultForLogging = JSON.stringify(result).substr(0, MAX_LOG_LENGTH); 81 | 82 | debug(`<-- RES`, resultForLogging); 83 | 84 | return result; 85 | }; 86 | -------------------------------------------------------------------------------- /src/schemas.test.ts: -------------------------------------------------------------------------------- 1 | import { getRawTransactionAsObjectResultOutputSchema, liquidGetTransactionResultSchema } from './schemas'; 2 | 3 | describe('getRawTransactionAsObjectResultOutputSchema', () => { 4 | it('should support Litecoin mimblewimble output 1', () => { 5 | const value = { 6 | txid: '702570a8271507bd5883c2db305b93a3e1c1b8f141f23d259ebc9cd09e7bfd73', 7 | hash: '702570a8271507bd5883c2db305b93a3e1c1b8f141f23d259ebc9cd09e7bfd73', 8 | version: 2, 9 | size: 1334, 10 | vsize: 31, 11 | weight: 124, 12 | locktime: 2267842, 13 | vin: [ 14 | { 15 | ismweb: true, 16 | output_id: 17 | '77f58ae1ce8fe07fae474db9dcfee1dccf13c6a32f48125a31c59fbcc8c153f3', 18 | }, 19 | ], 20 | vout: [ 21 | { 22 | ismweb: true, 23 | output_id: 24 | '94935c75222d6a2856987faa7f2d6270b70abdd40ae4b0b84193f2c956ffd227', 25 | }, 26 | ], 27 | vkern: [ 28 | { 29 | kernel_id: 30 | '73fd7b9ed09cbc9e253df241f1b8c1e1a3935b30dbc28358bd071527a8702570', 31 | fee: 0.0000251, 32 | pegin: 0, 33 | pegout: [ 34 | { 35 | value: 0.0998749, 36 | scriptPubKey: { 37 | asm: '0 e55017504d6e791f2d2a77ff9f15d5acede4a548', 38 | hex: '0014e55017504d6e791f2d2a77ff9f15d5acede4a548', 39 | reqSigs: 1, 40 | type: 'witness_v0_keyhash', 41 | addresses: ['ltc1qu4gpw5zddeu37tf2wlle79w44nk7ff2ggd0ppv'], 42 | }, 43 | }, 44 | ], 45 | }, 46 | ], 47 | hex: '020000000008000001824c85a6f79b29ca7acecb8e56174f9694f168c568f750c63f31e7d741197fbe18b93fbe788d1e14b1328c80ee4debcc2b21c2c6a4e5c9dbf245a516db8c5221010177f58ae1ce8fe07fae474db9dcfee1dccf13c6a32f48125a31c59fbcc8c153f308ac686fe3f263612f5080507449b2e9025fb964797ee7db0b4a4d5aa3aeb360450276f776210dbc3751724e607d5ff3c92643404bdaa5d7c813b21d95cc79be3ab7038001b5ed5990533abf7b404b769b76ed352a349281992624c5101b4d656827331527399e2e6e7614de10d647fc333405ee25dfd9cda3fcd05f15e695f913d6411a04fb520a1d994c82af923573c193926cb35c3148178fffe1b9bda279c899e60109522b62d76525ffad11cbda82dae762d0dfa31bdbe9810ad27c45279437fa26b0036d8a0fb3368dd4ac6cf3af3eb597856f8c4591d6df9c28b11a41714e8017b31503171af268373ff95fe19b64f1ae8172fcb5978d75cf4ad4d1992e4c281d72184f0103f6f2be16630141b255d23777b226637d8eda669c73be75d38f9b179ba33812e5f277d6dffbeeb96af50566facd9d1172154e6b2edd17389132c299ddc846be28fd5daa6706dfed2b54f3074649d158248ff7614effa3a5ee6f791f71484b91a0f7717e25e98cf6c27202a83138e11fbfaee95d82d31b6cf938018386beb628e809956bc9430f0ab2942d9e2145e258a8154b07f45798851654184aba3dd8c68d3d27098acb0f0ddc43e65337bd747a54a381a42a4a262e249b2c5802292600278d24dea8ac0d33cbf1a8868fd000ca01091936acdaa947fc67f1ecbc200e8c586c5dd40d7fe27cc090fb27610ce4ad5689da63ce15336105de9fcc2c9d18f60a8200f7af00caf491c1c4e1cef94604f12dbe196e7048981e4c72877dbcebdc5e77098d91c6f910f378d8ba51c0e1563eeeb662348da5b05bbbfbb210cf309263cc7ae4d4e5b00b46a877fedd28dd37ee41ad7f07bc842d6d37c4bff131cf6b26b2e0baccf687d4960e7f42d5485fe65dc0a1246c35e3b938cf1238cc8e39b14b49dd029584d0046328d39d87886d9ab784902be5fad85e2ff0ea5e02964ae20bb305ff877506c29affd4312d0cb2f03967c9b7ef4cdae0580ec7668bac5a10548aa52953064afb5cc314bf5781077321ff8aa9503220469f5622fcc96d4638a0bd09d289d156cfb201e40cdd930ec80a2378edabd82d6231502e426818d277eb99f198d2762424ff72946c5095f43e6465440406de1e32342bce1c9f2d722ef6be7f2895fd2758a18261beecdeeffc249357b76290aa6c110639a74fde2aaec3a428ca521a21fe50d36ed792cc34e51cf9e60f8b0b6b6a13cc9c24e11fb36de66c7353d1ca5db7191a8185b0142841f0a22e82df96e8d3921d2573dcc28cdedd65ca798a28973c57de5a666b6a2a9162b95580029e88965292102fd571cea191ffdd6e5de75d0df701778c1f8e13f0c82dc93f0fdf2a39e9f3223bfc302cc644849b870bd5aa7b51cf582e4748d9be0471e299c897c23d13a3091bc6b92fb8813c4075ef5ef5cf1677692e58927d5c73f99bf85dc3fbb0376ad11aca428976524f71f85b0aa56b8920931549cd40463916c0622afaaa9e8e6c2d713d0115924e0183e0ca22160014e55017504d6e791f2d2a77ff9f15d5acede4a54803e72533d05bb04021a325a6197aed08148ee1b6f39224443d3d32fa16fa703583091c74370b6bc2228bf51a8beb6a37744fb99bb65b97065d8f123b18be2e9d5b3f25bfd97c9a88904b81f938afff8a0dbc9d64f8f6f0f74afcc1dc10fd894be7d448c5aa38e2c0cf992241ac99fd8b41f25916edb1b9c1946e36c0ec96af1573b4c29a2200', 48 | }; 49 | 50 | getRawTransactionAsObjectResultOutputSchema.parse(value); 51 | }); 52 | 53 | it('should support Litecoin mimblewimble output 2', () => { 54 | const value = { 55 | txid: '0b465c970d09704c0cb3462e167e93e0fcb08d80fcc819196ce0f5bceffd7057', 56 | hash: '887b76faa04e6dc427ec862d6f237631bc502056bf9c8e2006bbe83abbd9e5b0', 57 | version: 2, 58 | size: 2201, 59 | vsize: 163, 60 | weight: 649, 61 | locktime: 2267858, 62 | vin: [ 63 | { 64 | ismweb: false, 65 | txid: '203ca0149844f52f7f849c41634ba51af39d38e9bd423dbfc8b5b9d7cbb424be', 66 | vout: 0, 67 | scriptSig: { 68 | asm: '', 69 | hex: '', 70 | }, 71 | txinwitness: [ 72 | '3044022042a290ed92f6b03699dc87ed97cbea77da8249782bd3a75411b4bdfcd6e80adb0220773020b894e6ab2977ef522535098194336cc630b28a7cb91d1304682736484701', 73 | '023b1d978872e931706227aed619d85c220cabf6dc674b1c99c30f104c5622a500', 74 | ], 75 | sequence: 4294967294, 76 | }, 77 | ], 78 | vout: [ 79 | { 80 | ismweb: false, 81 | value: 0.01399282, 82 | n: 0, 83 | scriptPubKey: { 84 | asm: '9 6b483366570e582846d91436f6a5657b12a8367335a9a2cab7eaec26d94a54e5', 85 | hex: '59206b483366570e582846d91436f6a5657b12a8367335a9a2cab7eaec26d94a54e5', 86 | type: 'witness_mweb_pegin', 87 | }, 88 | }, 89 | { 90 | ismweb: true, 91 | output_id: 92 | '4380e2fb1dd63d7ab5280af24ee441c3c9c94340b4021944c1dcd95795ad0317', 93 | }, 94 | { 95 | ismweb: true, 96 | output_id: 97 | 'ba14596eef8ed8bf8475183799e23cc3321ff69c5d256a25d280c06d6570937b', 98 | }, 99 | ], 100 | vkern: [ 101 | { 102 | kernel_id: 103 | '6b483366570e582846d91436f6a5657b12a8367335a9a2cab7eaec26d94a54e5', 104 | fee: 0.000039, 105 | pegin: 0.01399282, 106 | pegout: [], 107 | }, 108 | ], 109 | hex: '02000000000901be24b4cbd7b9b5c8bf3d42bde9389df31aa54b63419c847f2ff5449814a03c200000000000feffffff01f2591500000000002259206b483366570e582846d91436f6a5657b12a8367335a9a2cab7eaec26d94a54e502473044022042a290ed92f6b03699dc87ed97cbea77da8249782bd3a75411b4bdfcd6e80adb0220773020b894e6ab2977ef522535098194336cc630b28a7cb91d130468273648470121023b1d978872e931706227aed619d85c220cabf6dc674b1c99c30f104c5622a50001d34d2fd16b1583fa49b54451f3410131a0e2a5b16575d3b1c6e1e304b26a6cc39a8303af06753d3c7f2a3c9d810811da3d33a0d0e8d417120458d76691e61d83000209d5afab9d3ae89dabbd95fa1d8e0faca4bcd3219b96d32f88d271bc1f1fe697ed02012656036484c97e21eee8b82f45585b98309d5a6c0c3434bdaeaaf9fbd7c9af02cda357f5d40121ca25992fc415dd5dfe0968f5e544be227f9af36ddf0d6dcfb30102096ebf2f9e05c2f6b939d9e199aa0fb3bd2dfc521a52606ead24d8882b53b8edf786d5ecd6da09058e73f0521b9d23979229de0845f357c57165aed0eea0115f8a7e3a931dbd3dd2078fefb728eea1f1a489a4704574bc1cad6b8e84993093fcab08902b46ca9cf0d1a958655c298ae26366ead7a01e9bb545095c0f2297b673a976c81de6e810810d095f5adb2101a56bfc7bfbe3b78b2265017648c7e89a11fbeda9403d0eacdc854f458da08cc6368bff971a29fd7597b3029d9714f0751b1d836c360921b85e95ad435bd86349feeb2b67d295b2c1fd37eed6884dfe4b532aff4adf28f60f5de1c24aedcacee9a78c74c567187af87f9125bf91a35153bdb3c5aab6446eb6696b4fbce5ec68116337a06f770635a64551ec6e5ecc3dc00541c6115b313f53e7c627b7b778ec9c69ebcf6ee99acbbc005819a09e880e03a418cd8953a1ebe5e75a47e2c7852d69414aad7e58efdda213726c5a889e7bde84dd2c8ee63531d3e4e1a3a4490475f97d4f923508ab886d9ccf60b0958c4a252e7fed24efa6d238524b0e4dd915227ee09ab73d6464030e31c6570d02b016dadbd91c4b103c224a330c1264d08ea07a0246025ce9d5e16e8a58f84866750f2957dd51f995db1280aead7c22093e1bb128316bc8f89e4b3474f9743f0f076d62c8751c3233361d227085a6b752cbe939f85bab4c3ca92507c335408574ecb094da7d1d4b90a1a8f7945070e0dbe67d34b817e11039f25b2f80fb14f907173f1fb7ef952828d16743e0e9ecc08c519ea80eecc640ffd983a241bf044ea919e925b668ae4847be03d3689deb3078238b810c86e46b4e0809701b1556df12a9569b6f2a968621efeec95328a2f770cb0cb3dbe7b7d8c01ea02eb3cf5bb89278b23b0e54fb941aea69cadb79c17cffe02bd77f505fc8d21784abc508824add8a6e019aa6833e73c732ffeecd292d71bfa4e1aa8f0c03494252d13bef53581aa2fde0344cbe2b82a87254f771fc2140a9b895d304df55e2454b734b17a04cdfbda0fbc83f667d4e8657b8086e7a0dac41524b0c8968b9360b65a242387f0c64192b9a12e5a7701d74f2ca849f8f75e42db9cf5976b1cee705cdc083b0af4750097d9194f3d7ef47cf80657281b0e9f9f3d9ad4d6090a7f81a83e0f2543143591e0243393546e07625eeb4174864997b2b0d16bdf068e72e91e1935fe0773d4e69ee037942aebfa7d63e93c81c88b42be729753ee1a8e00a8cf369d6cb5aeb104b5d6b0103f1762f6d24b0142b644d83f50f31104c305375257f4aa4f8d76a7c6be9c13347a7a8e773dfc91763f141ebeb4f82fc2ae7966b85c4019a805f13bba22e17a0700b330b49a1068b70e901086cbceafeeabbd10a77b66be9fbfa220bafadc3607c6c7c22c6d691811c06c5ba6306f5230cb16bc2e197ef1b1cdc0097d8000033dadd2d8a5d80034e71ad25db6cc4ef2d23e075f3b129b47e30b3383b312c857c8f5e4b33afb40863202a8813149e966cc7db50e4ffd3e550902704518641aa39282422477c50b26b36dbb8eb13e70999d5eeb04dce91267ad2089e539ac6bdf1039ce2775445b3cc55b17fa788433d9d7c22f7d0d25ead445b834d4b09fc511e8b1d77a03f696fa19c6e2303d1288039c9ac7f6baf01601fd49d79874a1ead042e2566e7559a1ea910ebd81f4c2fe7ec0d9a1f299ccb863d3f426d6f8f037f1ed19d4aea367652378a9a37cd6cbf00fa8ea7037f205892c8350d9831cfaa42f19fbe9183cbd7e24dd28c97813b7f3c8909bd26d866e4942a3a39f42c8d13fea9bea556b55ef8d861cb375db824949645ca4ee3bc67c1cc1aa29a7c1703d45defd763caaa19fbe5165d8e13c0b77b311ece08ef70fcae2b81e7e2abbfbf4e544760a81234bf46e83723df0bcd36c319fb2ea02a5eb03e120ecb1e528d8aaa9e3a6b257581679d75d6381e97c1d0788b2ccb2dd1201af90cc43093766f7e3ec6ad78d6ea37307e3e81506f336926b4cc20da9c9fb9245602baba9bf8f7821b0cb505f5232a5fe451bc1c21a03ef98b6408d14ac182eabe8a6e06524b20ea49fc43de8037cfc0cebcf29193b26fd0fa8b54d06057d427c19e0bd8e4ac1f1378175e7ca0e239f8f3e1cfe41ff045d167065df8c4b3ecfceedf04124216ad6e1b518dea0e52b648d7f274d7df1798926a71120a4896ef8ad867d0109c0550af6080f7c9e7fdf15f85c8afccec994908297b48c86536ea1d1c8efe9484eb45509366b460c7ab52b25cd33fa4a7de8c78e3800fe50ce6e3b94aad6a7ca4572c2f5fa18733ef00570a92c5a4874b0a2e4b9337dbcd7ff1e03183962ba13163898be028d52ef3059bb3aa60869d8d7dd7cc4447ec0d51cec85eb5bfe35c529278f801139d3cd4b27202f88b2d1295a7521ad9819bf9d5a3cbeefd07bf0d9721998461ccf81d44731c4c096fa785226d4b520c641a46c5ed8d4b7811d8e8c1ecc549633206219b6e4b7ac14bcf0fbc11088312fc0c7e5a9f4b8da67174f8b7afd0153dbe23beec97ba84df2d392147b53fd55bc6dd30664274edcddb0d68cfb0c29de19f0d77bdf005bc14d29a2200', 110 | }; 111 | 112 | getRawTransactionAsObjectResultOutputSchema.parse(value); 113 | }); 114 | }); 115 | 116 | it("should parse liquid tx with blinders", () => { 117 | const tx = { 118 | amount: { 119 | bitcoin: 0.1, 120 | }, 121 | confirmations: 3, 122 | blockhash: 123 | "83796fb45blockhash", 124 | blockheight: 3355294, 125 | blockindex: 2, 126 | blocktime: 1746002050, 127 | txid: "254betxid", 128 | walletconflicts: [], 129 | time: 1746002045, 130 | timereceived: 1746002045, 131 | "bip125-replaceable": "no", 132 | details: [ 133 | { 134 | address: "ex1address", 135 | category: "receive", 136 | amount: 0.1, 137 | amountblinder: 138 | "b6809c8ab6c4288d5ce5398f80", 139 | asset: 140 | "6f0279e9ed041c3d710a9f57d0", 141 | assetblinder: 142 | "f6655e164700ec045bff521659", 143 | label: "", 144 | vout: 0, 145 | }, 146 | ], 147 | hex: "0251682002002505005050050", 148 | }; 149 | 150 | const result = liquidGetTransactionResultSchema.parse(tx); 151 | 152 | expect(result.details[0].amountblinder).toBe("b6809c8ab6c4288d5ce5398f80"); 153 | expect(result.details[0].assetblinder).toBe("f6655e164700ec045bff521659"); 154 | }); -------------------------------------------------------------------------------- /src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const signRawTransactionWithWalletResultSchema = z.object({ 4 | hex: z.string(), 5 | complete: z.boolean(), 6 | // TODO: errors field 7 | }); 8 | 9 | export type SignRawTransactionWithWalletResult = z.infer< 10 | typeof signRawTransactionWithWalletResultSchema 11 | >; 12 | 13 | export const sendToAddressResultSchema = z.string(); 14 | 15 | export type SendToAddressResult = z.infer; 16 | 17 | export const lockUnspentResultSchema = z.boolean(); 18 | 19 | export type LockUnspentResult = z.infer; 20 | 21 | export const sendRawTransactionResultSchema = z.string(); 22 | 23 | export type SendRawTransactionResult = z.infer< 24 | typeof sendRawTransactionResultSchema 25 | >; 26 | 27 | export const fundRawTransactionResultSchema = z.object({ 28 | hex: z.string(), 29 | fee: z.number(), 30 | changepos: z.number(), 31 | }); 32 | 33 | export type FundRawTransactionResult = z.infer< 34 | typeof fundRawTransactionResultSchema 35 | >; 36 | 37 | export const createRawTransactionResultSchema = z.string(); 38 | 39 | export type CreateRawTransactionResult = z.infer< 40 | typeof createRawTransactionResultSchema 41 | >; 42 | 43 | export const getTransactionResultSchema = z.object({ 44 | fee: z.union([z.number(), z.undefined()]), 45 | blockhash: z.union([z.string(), z.undefined()]), 46 | }); 47 | 48 | export type GetTransactionResult = z.infer; 49 | 50 | export const liquidGetTransactionResultSchema = z.object({ 51 | amount: z.record(z.string(), z.number()), 52 | fee: z.union([z.undefined(), z.record(z.string(), z.number())]), 53 | confirmations: z.union([z.number(), z.undefined()]), 54 | blockhash: z.union([z.string(), z.undefined()]), 55 | txid: z.string(), 56 | details: z.array( 57 | z.object({ 58 | // Address can be undefined when issuing 59 | address: z.union([z.string(), z.undefined()]), 60 | category: z.union([z.literal('send'), z.literal('receive')]), 61 | amount: z.number(), 62 | amountblinder: z.string().optional(), 63 | asset: z.string(), 64 | assetblinder: z.string().optional(), 65 | vout: z.number(), 66 | fee: z.union([z.number(), z.undefined()]), 67 | }), 68 | ), 69 | }); 70 | 71 | export type LiquidGetTransactionResult = z.infer< 72 | typeof liquidGetTransactionResultSchema 73 | >; 74 | 75 | export const getInfoResultSchema = z.object({ 76 | blocks: z.number(), 77 | }); 78 | 79 | export type GetInfoResult = z.infer; 80 | 81 | export const getBlockchainInfoResultSchema = z.object({ 82 | blocks: z.number(), 83 | headers: z.union([z.number(), z.undefined()]), 84 | initial_block_download_complete: z.union([z.boolean(), z.undefined()]), 85 | }); 86 | 87 | export type GetBlockchainInfoResult = z.infer< 88 | typeof getBlockchainInfoResultSchema 89 | >; 90 | 91 | export const getRawTransactionAsObjectResultOutputSchema = z.object({ 92 | // NOTE: Can be `undefined` on Litecoin with Mimblewimble 93 | n: z.union([z.number(), z.undefined()]), 94 | // NOTE: Can be undefined Liquid 95 | value: z.union([z.number(), z.undefined()]), 96 | // NOTE: Can be `undefined` on Litecoin with Mimblewimble 97 | scriptPubKey: z.union([ 98 | z.object({ 99 | hex: z.string(), 100 | addresses: z.union([z.array(z.string()), z.undefined()]), 101 | address: z.union([z.string(), z.undefined()]), 102 | type: z.union([z.literal('scripthash'), z.string()]), 103 | reqSigs: z.union([z.number(), z.undefined()]), 104 | }), 105 | z.undefined(), 106 | ]), 107 | }); 108 | 109 | export type GetRawTransactionAsObjectResultOutput = z.infer< 110 | typeof getRawTransactionAsObjectResultOutputSchema 111 | >; 112 | 113 | export const getRawTransactionAsObjectResultSchema = z.object({ 114 | txid: z.string(), 115 | hash: z.union([z.string(), z.undefined()]), 116 | blockhash: z.union([z.string(), z.undefined()]), 117 | vout: z.array(getRawTransactionAsObjectResultOutputSchema), 118 | vin: z.array( 119 | z.object({ 120 | // NOTE: txid is undefined for coinbase tx 121 | txid: z.union([z.string(), z.undefined()]), 122 | // NOTE: vout is undefined for coinbase tx 123 | vout: z.union([z.number(), z.undefined()]), 124 | }), 125 | ), 126 | }); 127 | 128 | export type GetRawTransactionAsObjectResult = z.infer< 129 | typeof getRawTransactionAsObjectResultSchema 130 | >; 131 | 132 | export const getBlockHashFromHeightResultSchema = z.string(); 133 | 134 | export type GetBlockHashFromHeightResult = z.infer< 135 | typeof getBlockHashFromHeightResultSchema 136 | >; 137 | 138 | // If verbosity is 0, returns a string that is serialized, hex-encoded data for block 'hash'. 139 | // If verbosity is 1, returns an Object with information about block . 140 | // If verbosity is 2, returns an Object with information about block and information about each transaction. 141 | // NOTE: This is for verbosity equals 1 142 | export const getBlockFromHashResultSchema = z.object({ 143 | tx: z.array(z.string()), 144 | height: z.number(), 145 | }); 146 | 147 | export type GetBlockFromHashResult = z.infer< 148 | typeof getBlockFromHashResultSchema 149 | >; 150 | 151 | export const getBlockCountResultSchema = z.number(); 152 | 153 | export type GetBlockCountResult = z.infer; 154 | 155 | export const getRawMempoolResultSchema = z.array(z.string()); 156 | 157 | export type GetRawMempoolResult = z.infer; 158 | 159 | export const getNewAddressResultSchema = z.string(); 160 | 161 | export type GetNewAddressResult = z.infer; 162 | 163 | export const validateAddressResultSchema = z.object({ 164 | isvalid: z.boolean(), 165 | address: z.union([z.string(), z.undefined()]), 166 | ismweb: z.union([z.boolean(), z.undefined()]), 167 | }); 168 | 169 | export type ValidateAddressResult = z.infer< 170 | typeof validateAddressResultSchema 171 | >; 172 | 173 | export const liquidValidateAddressResultSchema = validateAddressResultSchema.extend({ 174 | unconfidential: z.string(), 175 | address: z.string(), 176 | }); 177 | 178 | export type LiquidValidateAddressResult = z.infer< 179 | typeof liquidValidateAddressResultSchema 180 | >; 181 | 182 | export const getBalanceResultSchema = z.number(); 183 | 184 | export type GetBalanceResult = z.infer; 185 | 186 | export const generateToAddressResultSchema = z.array(z.string()); 187 | 188 | export type GenerateToAddressResult = z.infer< 189 | typeof generateToAddressResultSchema 190 | >; 191 | 192 | export const getLiquidBalanceResultSchema = z.record(z.string(), z.number()); 193 | 194 | export const getLiquidBalanceForAssetResultSchema = z.number(); 195 | 196 | export type GetLiquidBalanceResult = z.infer< 197 | typeof getLiquidBalanceResultSchema 198 | >; 199 | 200 | export const omniGetWalletAddressBalancesResultSchema = z.array( 201 | z.object({ 202 | address: z.string(), 203 | balances: z.array( 204 | z.object({ 205 | propertyid: z.number(), 206 | name: z.string(), 207 | balance: z.string(), 208 | reserved: z.string(), 209 | frozen: z.string(), 210 | }), 211 | ), 212 | }), 213 | ); 214 | 215 | export type OmniGetWalletAddressBalancesResult = z.infer< 216 | typeof omniGetWalletAddressBalancesResultSchema 217 | >; 218 | 219 | export const getGetBlockchainInfoResultSchema = z.array( 220 | z.object({ 221 | blocks: z.number(), 222 | headers: z.union([z.number(), z.undefined()]), 223 | }), 224 | ); 225 | 226 | export type GetGetBlockchainInfoResult = z.infer< 227 | typeof getGetBlockchainInfoResultSchema 228 | >; 229 | 230 | export const ancientGetInfoResultSchema = z.object({ 231 | blocks: z.number(), 232 | }); 233 | 234 | export type AncientGetInfoResult = z.infer; 235 | 236 | export const omniFundedSendResultSchema = z.string(); 237 | 238 | export const omniFundedSendAllResultSchema = z.string(); 239 | 240 | export type OmniFundedSendResult = z.infer; 241 | 242 | // { 243 | // "txid" : "hash", (string) the hex-encoded hash of the transaction 244 | // "sendingaddress" : "address", (string) the Bitcoin address of the sender 245 | // "referenceaddress" : "address", (string) a Bitcoin address used as reference (if any) 246 | // "ismine" : true|false, (boolean) whether the transaction involes an address in the wallet 247 | // "confirmations" : nnnnnnnnnn, (number) the number of transaction confirmations 248 | // "fee" : "n.nnnnnnnn", (string) the transaction fee in bitcoins 249 | // "blocktime" : nnnnnnnnnn, (number) the timestamp of the block that contains the transaction 250 | // "valid" : true|false, (boolean) whether the transaction is valid 251 | // "invalidreason" : "reason", (string) if a transaction is invalid, the reason 252 | // "version" : n, (number) the transaction version 253 | // "type_int" : n, (number) the transaction type as number 254 | // "type" : "type", (string) the transaction type as string 255 | // [...] (mixed) other transaction type specific properties 256 | // } 257 | export const omniGetTransactionResultSchema = z.object({ 258 | txid: z.string(), 259 | amount: z.union([z.string(), z.undefined()]), 260 | propertyid: z.union([z.number(), z.undefined()]), 261 | valid: z.union([z.boolean(), z.undefined()]), 262 | invalidreason: z.union([z.string(), z.undefined()]), 263 | type: z.string(), 264 | type_int: z.number(), 265 | version: z.number(), 266 | referenceaddress: z.union([z.string(), z.undefined()]), 267 | }); 268 | 269 | export type OmniGetTransactionResult = z.infer< 270 | typeof omniGetTransactionResultSchema 271 | >; 272 | 273 | export const omniListPendingTransactionsSchema = z.array( 274 | z.object({ 275 | txid: z.string(), 276 | amount: z.union([z.string(), z.undefined()]), 277 | propertyid: z.union([z.number(), z.undefined()]), 278 | type_int: z.number(), 279 | type: z.string(), 280 | version: z.number(), 281 | referenceaddress: z.union([z.string(), z.undefined()]), 282 | }), 283 | ); 284 | 285 | export type OmniListPendingTransactionsResult = z.infer< 286 | typeof omniListPendingTransactionsSchema 287 | >; 288 | 289 | export const zcashGetOperationResultSchema = z.array(z.any()); 290 | 291 | export type ZcashGetOperationResultResult = z.infer< 292 | typeof zcashGetOperationResultSchema 293 | >; 294 | 295 | export const zcashGetBalanceForAddressSchema = z.number(); 296 | 297 | export type ZcashGetBalanceForAddressResult = z.infer< 298 | typeof zcashGetBalanceForAddressSchema 299 | >; 300 | 301 | export const zcashSendManySchema = z.string(); 302 | 303 | export type ZcashSendManyResult = z.infer; 304 | 305 | export const zcashValidateAddressSchema = z.object({ 306 | isvalid: z.boolean(), 307 | adddress: z.union([z.string(), z.undefined()]), 308 | type: z.union([z.string(), z.undefined()]), 309 | }); 310 | 311 | export type ZcashValidateAddressResult = z.infer< 312 | typeof zcashValidateAddressSchema 313 | >; 314 | 315 | export const omniSendSchema = z.string(); 316 | 317 | export type OmniSendResult = z.infer; 318 | 319 | export const zcashGetNewAddressSchema = z.string(); 320 | 321 | export type ZcashGetNewAddressResult = z.infer< 322 | typeof zcashGetNewAddressSchema 323 | >; 324 | 325 | export const zcashListUnspentSchema = z.array( 326 | z.object({ 327 | txid: z.string(), 328 | address: z.string(), 329 | change: z.boolean(), 330 | amount: z.number(), 331 | outindex: z.union([z.number(), z.undefined()]), 332 | }), 333 | ); 334 | 335 | export type ZcashListUnspentResult = z.infer; 336 | 337 | export const listUnspentSchema = z.array( 338 | z.object({ 339 | txid: z.string(), 340 | vout: z.number(), 341 | address: z.string(), 342 | amount: z.number(), 343 | confirmations: z.number(), 344 | spendable: z.boolean(), 345 | solvable: z.union([z.boolean(), z.undefined()]), 346 | safe: z.union([z.boolean(), z.undefined()]), 347 | }), 348 | ); 349 | 350 | export type ListUnspentResult = z.infer; 351 | 352 | export const dumpPrivateKeySchema = z.string(); 353 | 354 | export const ecashIsFinalTransactionSchema = z.boolean(); 355 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface CreateBitcoinJsonRpcOptions { 2 | ancient?: boolean; 3 | } 4 | 5 | export type BitcoinFeeEstimateMode = 'UNSET' | 'ECONOMICAL' | 'CONSERVATIVE'; 6 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug'; 2 | 3 | const MAX_ERROR_MESSAGE_LENGTH = 150; 4 | 5 | const debug = createDebug('bitcoin-json-rpc'); 6 | 7 | export const maybeShortenErrorMessage = (value: string) => value.substr(0, MAX_ERROR_MESSAGE_LENGTH); 8 | 9 | export const mergeErrorStacks = (error: Error, prevError: Error) => 10 | prevError 11 | ? Object.assign(error, { 12 | stack: [error.stack, prevError.stack].join('\n'), 13 | }) 14 | : error; 15 | 16 | export const throwIfErrorInResponseData = (data: any, prevError?: any) => { 17 | if (data === undefined) { 18 | throw mergeErrorStacks(new Error('data is undefined'), prevError); 19 | } 20 | 21 | if (typeof data === 'string') { 22 | throw mergeErrorStacks( 23 | Object.assign(new Error(maybeShortenErrorMessage(data)), { 24 | data: { jsonRpcResponse: data }, 25 | }), 26 | prevError 27 | ); 28 | } 29 | 30 | if (data.error === undefined || data.error === null) { 31 | return; 32 | } 33 | 34 | if (data.error.message) { 35 | debug(`<-- ERR`, data.error.message); 36 | throw Object.assign(new Error(maybeShortenErrorMessage(data.error.message)), { 37 | data: { jsonRpcResponse: data }, 38 | }); 39 | } 40 | 41 | throw Object.assign(new Error(maybeShortenErrorMessage(JSON.stringify(data))), { 42 | data: { jsonRpcResponse: data }, 43 | }); 44 | }; 45 | 46 | export const throwIfErrorInResponseDataWithExtraProps = (data: any, prevError: Error | undefined, props: any) => { 47 | try { 48 | throwIfErrorInResponseData(data, prevError); 49 | } catch (error: any) { 50 | const mergedError = prevError ? mergeErrorStacks(error, prevError) : error; 51 | throw Object.assign(mergedError, props); 52 | } 53 | }; 54 | 55 | export const PURE_METHODS = [ 56 | 'getinfo', 57 | 'getblockchaininfo', 58 | 'getrawtransaction', 59 | 'getblockcount', 60 | 'getblockhash', 61 | 'getrawmempool', 62 | 'validateaddress', 63 | 'getbalance', 64 | 'omni_getwalletaddressbalances', 65 | 'omni_gettransaction', 66 | 'omni_listpendingtransactions', 67 | 'z_getoperationresult', 68 | 'z_getbalance', 69 | 'z_validateaddress', 70 | 'z_listunspent', 71 | 'listunspent', 72 | 'dumpprivkey', 73 | 'gettransaction', 74 | 'isfinaltransaction', 75 | ]; 76 | 77 | export const getWasExecutedFromError = (method: string, error: Error) => { 78 | const notExecutedErrorMessages = [ 79 | /^Work queue depth exceeded$/, 80 | /^Loading block index/, 81 | /^Rewinding blocks/, 82 | /^Error creating transaction/, 83 | /^Loading P2P addresses/, 84 | /^Insufficient funds$/, 85 | /^Error with selected inputs/, 86 | /^Sender has insufficient balance/, 87 | /^Insufficient funds/, 88 | /^ECONNREFUSED$/, // TODO: Move to jsonRpcCmd 89 | /^Verifying blocks/, 90 | /^Loading wallet/, 91 | /fees may not be sufficient/, 92 | /Error choosing inputs for the send transaction/, 93 | /Rewinding blocks/, 94 | /Invalid amount/, 95 | /^Activating best chain/, 96 | /^Parsing Omni Layer transactions/, 97 | /^Upgrading/, 98 | /^Error committing transaction/, 99 | ]; 100 | 101 | if (notExecutedErrorMessages.some((_) => error.message.match(_) !== null)) { 102 | return false; 103 | } 104 | 105 | return null; 106 | }; 107 | 108 | // NOTE: Assumes there were no effects 109 | export const getShouldRetry = (method: string, error: Error) => { 110 | if (error.message.match(/^Insufficient funds$/)) { 111 | return false; 112 | } 113 | 114 | if (method === 'gettransaction' && error.message.match(/Invalid or non-wallet transaction id/)) { 115 | return false; 116 | } 117 | 118 | if (method.match(/^omni_/) && error.message.match(/Error creating transaction/)) { 119 | return false; 120 | } 121 | 122 | if (method.match(/^omni_/) && error.message.match(/Error choosing inputs/)) { 123 | return false; 124 | } 125 | 126 | // Transaction has dropped out of mempool (and is unlikely to resurface from retrying) 127 | if (method === 'getrawtransaction' && error.message.match(/No such mempool/)) { 128 | return false; 129 | } 130 | 131 | return true; 132 | }; 133 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "lib": ["es2017"], 6 | "declaration": true, 7 | "outDir": "dist/cjs", 8 | "strict": true, 9 | "esModuleInterop": true 10 | }, 11 | "include": ["src/index.ts"], 12 | "exclude": ["node_modules", "dist"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "declaration": true, 7 | "outDir": "dist/esm", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "moduleResolution": "node" 11 | }, 12 | "include": ["src/index.ts"], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | --------------------------------------------------------------------------------