diff --git a/packages/bitcore-sh/.gitignore b/packages/bitcore-sh/.gitignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/packages/bitcore-sh/.gitignore @@ -0,0 +1 @@ +build diff --git a/packages/bitcore-sh/README.md b/packages/bitcore-sh/README.md new file mode 100644 index 0000000000..243389001f --- /dev/null +++ b/packages/bitcore-sh/README.md @@ -0,0 +1,56 @@ +# Bitcore SH +A repl environment to interact with `crypto-rpc` using the rpcs in the bitcore config + +## Usage +In packages/bitcore-sh run: +``` +npm start +``` +Starts repl environment +``` +> +``` +Run a command +``` +> BTC regtest getTip +{ + height: 1280, + hash: '1cb2e03e7c644f6346db5d0ca06f48bb989e3dc4088ce348bc0817a89cbfd6aa' +} +``` +Run a command with arguments +``` +> BTC regtest getBlock --hash 1cb2e03e7c644f6346db5d0ca06f48bb989e3dc4088ce348bc0817a89cbfd6aa +{ + hash: '1cb2e03e7c644f6346db5d0ca06f48bb989e3dc4088ce348bc0817a89cbfd6aa', + confirmations: 1, + height: 1280, + version: 536870912, + versionHex: '20000000', + merkleroot: 'b7fe62d037fe3577ba8531676ad772dad5879e0f8fee2d79e4707883bef0bb1d', + time: 1778809084, + mediantime: 1778807940, + nonce: 1, + bits: '207fffff', + difficulty: 4.656542373906925e-10, + chainwork: '0000000000000000000000000000000000000000000000000000000000000a02', + nTx: 1, + previousblockhash: '6d044b0b7b84518f601c8b84c7e65b5387a9afa440968ae1c8bbe9062cf8105a', + strippedsize: 214, + size: 250, + weight: 892, + tx: [ + 'b7fe62d037fe3577ba8531676ad772dad5879e0f8fee2d79e4707883bef0bb1d' + ] +} +``` +The `use` command speeds up execution by appending arguments to the start of the following commands +``` +> use BTC regtest +BTC regtest> getTip +{ + height: 1280, + hash: '1cb2e03e7c644f6346db5d0ca06f48bb989e3dc4088ce348bc0817a89cbfd6aa' +} +``` +`bitcore-sh` includes command completion, try using tab diff --git a/packages/bitcore-sh/package-lock.json b/packages/bitcore-sh/package-lock.json new file mode 100644 index 0000000000..a76564496d --- /dev/null +++ b/packages/bitcore-sh/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "bitcore-sh", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bitcore-sh", + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/packages/bitcore-sh/package.json b/packages/bitcore-sh/package.json new file mode 100644 index 0000000000..10fe11616d --- /dev/null +++ b/packages/bitcore-sh/package.json @@ -0,0 +1,27 @@ +{ + "name": "bitcore-sh", + "description": "", + "author": "BitPay, Inc", + "license": "MIT", + "version": "1.0.0", + "scripts": { + "start": "npm run compile && node build/run.js", + "compile": "npm run clean && npm run tsc", + "clean": "rm -rf build", + "tsc": "tsc", + "fix:errors": "eslint --fix --quiet .", + "fix:all": "eslint --fix .", + "fix": "npm run fix:errors" + }, + "keywords": [ + "rpc", + "typescript", + "bitcoin" + ], + "contributors": [ + { + "name": "Micah Maphet", + "email": "mmaphet@bitpay.com" + } + ] +} diff --git a/packages/bitcore-sh/src/config.ts b/packages/bitcore-sh/src/config.ts new file mode 100644 index 0000000000..0262b98774 --- /dev/null +++ b/packages/bitcore-sh/src/config.ts @@ -0,0 +1,70 @@ +import fs from 'fs'; +import { homedir } from 'os'; +import path from 'path'; + +interface ConfigType { + [chain: string]: { + [network: string]: { + host: string; + port: number | string; + username: string; + password: string; + protocol?: 'http' | 'https' | 'ws' | 'wss' | 'ipc'; + }; + }; +}; + +const args = process.argv.slice(2); +const DEFAULT_CONFIG = path.join(__dirname, '../../../../bitcore.config.json'); +let bitcoreConfigPath: string; + +const pathIndex = args.indexOf('--path'); +if (pathIndex !== -1) { + bitcoreConfigPath = args[pathIndex + 1]; +} else { + bitcoreConfigPath = process.env.BITCORE_CONFIG_PATH || DEFAULT_CONFIG +} + +if (bitcoreConfigPath[0] === '~') { + bitcoreConfigPath = bitcoreConfigPath.replace('~', homedir()); +} + +if (!fs.existsSync(bitcoreConfigPath)) { + throw new Error(`No bitcore config exists at ${bitcoreConfigPath}`); +} + +const bitcoreConfigStat = fs.statSync(bitcoreConfigPath); + +if (bitcoreConfigStat.isDirectory()) { + if (!fs.existsSync(path.join(bitcoreConfigPath, 'bitcore.config.json'))) { + throw new Error(`No bitcore config exists in directory ${bitcoreConfigPath}`); + } + bitcoreConfigPath = path.join(bitcoreConfigPath, 'bitcore.config.json'); +} + +let rawBitcoreConfig; +try { + rawBitcoreConfig = fs.readFileSync(bitcoreConfigPath).toString(); +} catch (error) { + throw new Error(`Error in loading bitcore config\nFound file at ${bitcoreConfigPath}\n${error}`); +} + +let bitcoreConfig: object; +try { + bitcoreConfig = JSON.parse(rawBitcoreConfig).bitcoreNode.chains; +} catch (error) { + throw new Error(`Error in parsing bitcore config\nFound and loaded file at ${bitcoreConfigPath}\n${error}`); +} + +const config: ConfigType = {}; +for (const chain in bitcoreConfig) { + const chainConfig = bitcoreConfig[chain]; + config[chain] = {}; + for (const network in chainConfig) { + const networkConfig = chainConfig[network]; + const rpcConfig = networkConfig.rpc || networkConfig.provider || networkConfig.providers[0]; + config[chain][network] = rpcConfig; + } +} + +export default config; diff --git a/packages/bitcore-sh/src/rpc.ts b/packages/bitcore-sh/src/rpc.ts new file mode 100644 index 0000000000..1975ef604a --- /dev/null +++ b/packages/bitcore-sh/src/rpc.ts @@ -0,0 +1,41 @@ +import config from './config'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { CryptoRpc } = require('../../crypto-rpc'); + +class Rpc { + methods: string[] = Object.getOwnPropertyNames(CryptoRpc.prototype) + .filter(p => typeof CryptoRpc.prototype[p] === 'function' && p !== 'constructor'); + + get(chain: string, network: string) { + if (!config[chain][network]) + return; + const rpcConfig = config[chain][network]; + + return new CryptoRpc({ + chain, + protocol: rpcConfig.protocol || 'http', + host: rpcConfig.host, + port: rpcConfig.port, + user: rpcConfig.username, + pass: rpcConfig.password + }).get(chain); + }; + + getMethodParams(chain: string, network: string, rpcMethod: string): string[] { + const rpc = this.get(chain, network); + if (!rpc || !rpc[rpcMethod]) + return []; + const methodString = rpc[rpcMethod].toString(); + const match = methodString.match(/\{\s*([^}]+)\s*\}/); + if (!match) return []; + + return match[1] + .split(',') + .map((key: string) => key.trim()) + .map((key: string) => '--' + (key.substring(0, key.indexOf(' ')) || key)) + .filter((key: string) => key); + }; +} + +const RPC = new Rpc(); +export default RPC; diff --git a/packages/bitcore-sh/src/run.ts b/packages/bitcore-sh/src/run.ts new file mode 100644 index 0000000000..8d59c40315 --- /dev/null +++ b/packages/bitcore-sh/src/run.ts @@ -0,0 +1,102 @@ +import readline from 'readline'; +import config from './config'; +import RPC from './rpc'; + +// all the commands prepended by the use command +const context: string[] = []; +let exiting = false; + +// complete the chain, network, rpcCommand, and paramers +const completer = (line: string) => { + let args = [...context, ...line.split(' ')]; + if (args.includes('use')) + args = args.filter(arg => arg !== 'use'); + + const completions: string[] = line.includes(' ') ? [] : ['use']; + let hits: string[] = []; + + if (args.length <= 1) { + completions.push(...Object.keys(config)); + hits = completions.filter(c => c.toLowerCase().startsWith(args[0].toLowerCase())); + } else if (args.length === 2) { + if (Object.keys(config).includes(args[0].toUpperCase())) { + completions.push(...Object.keys(config[args[0].toUpperCase()])); + hits = completions.filter(c => c.startsWith(args[1])); + } + } else if (args.length === 3) { + const rpc = RPC.get(args[0].toUpperCase(), args[1]); + if (rpc) { + completions.push(...RPC.methods); + hits = completions.filter(c => c.startsWith(args[2])); + } + } else if (args.length === 4) { + completions.push(...RPC.getMethodParams(args[0], args[1], args[2])); + hits = completions.filter(c => c.startsWith(args[3])); + } + return [hits.length ? hits : completions, args[args.length - 1]]; +}; + +// repl environment +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + completer +}); +nextCommand(); + +// handle all user commands +rl.on('line', async (line) => { + let args = line.split(' '); + if (args[0] === 'use') { + for (const arg of args.slice(1)) { + if (arg === '..') { + context.pop(); + } else { + context.push(arg); + } + } + nextCommand(); + return; + } else if (args[0] === 'exit') { + process.exit(0); + } + args = [...context, ...args]; + + try { + const chain = args[0].toUpperCase(); + const network = args[1]; + const command = args[2]; + args.splice(0, 3); + + const rpcArgs = {}; + for (let i = 0; i < args.length; i++) { + if (!args[i].startsWith('--')) + continue; + rpcArgs[args[i].slice(2)] = args[i + 1]; + args.splice(i, 2); + i--; + } + + const rpc = RPC.get(chain, network); + if (rpc) + console.log(await rpc[command](rpcArgs)); + } catch (e) { + console.log(e); + } + nextCommand(); +}); + +rl.on('SIGINT', () => { + if (exiting) { + process.exit(0); + } + rl.setPrompt(`${context.join(' ')}> \n(To exit, press Ctrl+C again or Ctrl+D or type exit)\n${context.join(' ')}> `); + rl.prompt(); + exiting = true; +}); + +function nextCommand() { + rl.setPrompt(`${context.join(' ')}> `); + rl.prompt(); + exiting = false; +} diff --git a/packages/bitcore-sh/tsconfig.json b/packages/bitcore-sh/tsconfig.json new file mode 100644 index 0000000000..227261eed0 --- /dev/null +++ b/packages/bitcore-sh/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "ES2020", + "esnext", + "esnext.asynciterable", + "ES2022", + "DOM" + ], + "downlevelIteration": true, + "allowJs": true, + "sourceMap": true, + "outDir": "build", + "strict": true, + "noImplicitAny": false, + "skipLibCheck": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "experimentalDecorators": true + }, + "include": [ + "./src/**/*", + "./scripts/**/*", + "./test/**/*" + ] +}