Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/bitcore-sh/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
56 changes: 56 additions & 0 deletions packages/bitcore-sh/README.md
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions packages/bitcore-sh/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions packages/bitcore-sh/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
70 changes: 70 additions & 0 deletions packages/bitcore-sh/src/config.ts
Original file line number Diff line number Diff line change
@@ -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;
41 changes: 41 additions & 0 deletions packages/bitcore-sh/src/rpc.ts
Original file line number Diff line number Diff line change
@@ -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;
102 changes: 102 additions & 0 deletions packages/bitcore-sh/src/run.ts
Original file line number Diff line number Diff line change
@@ -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;
}
35 changes: 35 additions & 0 deletions packages/bitcore-sh/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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/**/*"
]
}