Skip to content

Commit

Permalink
Merge pull request #4950 from BitGo/BTC-1351.utxo-bin-cleanup-args
Browse files Browse the repository at this point in the history
refactor(utxo-bin): improve yargs argument parsing
  • Loading branch information
OttoAllmendinger committed Sep 26, 2024
2 parents ae0b25d + c37c2c3 commit ac85492
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 205 deletions.
2 changes: 1 addition & 1 deletion modules/utxo-bin/bin/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import * as yargs from 'yargs';
import { cmdGenerateAddress, cmdParseAddress, cmdParseScript, cmdParseTx, cmdParseXpub } from '../src/commands';
import { cmdParseTx, cmdParseAddress, cmdParseScript, cmdGenerateAddress, cmdParseXpub } from '../src/commands';

yargs
.command(cmdParseTx)
Expand Down
3 changes: 3 additions & 0 deletions modules/utxo-bin/src/args/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './parseString';
export * from './walletKeys';
export * from './parseNetwork';
43 changes: 43 additions & 0 deletions modules/utxo-bin/src/args/parseNetwork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as utxolib from '@bitgo/utxo-lib';

export function getNetworkForName(name: string): utxolib.Network {
const network = utxolib.networks[name as utxolib.NetworkName];
if (!network) {
throw new Error(`invalid network ${name}`);
}
return network;
}

export function getNetwork(argv: { network: string }): utxolib.Network {
return getNetworkForName(argv.network);
}

type DemandOption<T> = T & { demandOption: true };

type NetworkOption<TDefault> = {
type: 'string';
description: string;
default: TDefault;
coerce: (arg: string) => utxolib.Network;
};

export function getNetworkOptions(defaultValue?: string): {
network: NetworkOption<typeof defaultValue>;
} {
return {
network: {
type: 'string',
description: 'network name',
default: defaultValue,
coerce: getNetworkForName,
},
};
}

export function getNetworkOptionsDemand(defaultValue?: string): {
network: DemandOption<NetworkOption<typeof defaultValue>>;
} {
return {
network: { ...getNetworkOptions(defaultValue).network, demandOption: true },
};
}
134 changes: 134 additions & 0 deletions modules/utxo-bin/src/args/parseString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as process from 'process';
import * as fs from 'fs';

import clipboardy from 'clipboardy-cjs';

type Format = 'hex' | 'base64';
export function stringToBuffer(data: string, format: Format | Format[]): Buffer {
if (typeof format !== 'string') {
for (const f of format) {
try {
return stringToBuffer(data, f);
} catch (err) {
// ignore, try next
}
}
throw new Error(`could not parse data, formats: ${format}`);
}

// strip all whitespace
data = data.replace(/\s*/g, '');

if (format === 'hex') {
data = data.toLowerCase();
}

const buf = Buffer.from(data, format);
// make sure there were no decoding errors
if (buf.toString(format) !== data) {
throw new Error(`invalid ${format}`);
}
return buf;
}

export const readStringOptions = {
path: { type: 'string', nargs: 1, default: '' },
stdin: { type: 'boolean', default: false },
data: {
type: 'string',
description: 'hex or base64',
alias: 'hex',
},
clipboard: { type: 'boolean', default: false },
} as const;

export type ReadStringOptions = {
clipboard?: boolean;
path?: string;
data?: string;
stdin: boolean;
};

/**
* Reads from stdin until Ctrl-D is pressed.
*/
export async function readStdin(): Promise<string> {
/*
* High-performance implementation of reading from stdin.
* Standard readline is extremely slow for long lines.
*/
return new Promise((resolve, reject) => {
// Using readline is not an option because it is extremely slow for long lines.
// By enabling raw mode, we can read more than 4096 bytes, but it requires manual Ctrl-C/Ctrl-D handling
if (!process.stdin.setRawMode) {
throw new Error('stdin is not a tty');
}
process.stdin.setRawMode(true);
const buf: Buffer[] = [];

process.stdin.on('data', (chunk) => {
if (chunk[0] === 0x03) {
// Ctrl-C
process.exit(130);
}
if (chunk[0] === 0x04) {
// Ctrl-D
process.stdin.emit('end');
return;
}
buf.push(chunk);
process.stdout.write(chunk);
});

process.stdin.on('end', () => {
resolve(Buffer.concat(buf).toString('utf8'));
});

process.stdin.on('error', (err) => {
reject(err);
});
});
}

/**
* @param argv
* @param input - optional input data. If set, this function just ensures that nothing else is set.
* @return string from specified source
*/
export async function argToString(argv: ReadStringOptions, input?: string): Promise<string | undefined> {
if (argv.stdin || argv.path === '-') {
if (input) {
throw new Error(`conflicting arguments`);
}
console.log('Reading from stdin. Please paste hex-encoded transaction data.');
console.log('After inserting data, press Ctrl-D to finish. Press Ctrl-C to cancel.');
if (process.stdin.isTTY) {
input = await readStdin();
} else {
input = await fs.promises.readFile('/dev/stdin', 'utf8');
}
}

if (argv.clipboard) {
if (input) {
throw new Error(`conflicting arguments`);
}
input = await clipboardy.read();
}

if (argv.path) {
if (input) {
throw new Error(`conflicting arguments`);
}
input = (await fs.promises.readFile(argv.path, 'utf8')).toString();
}

if (argv.data) {
if (input) {
throw new Error(`conflicting arguments`);
}
input = argv.data;
}

return input;
}
34 changes: 34 additions & 0 deletions modules/utxo-bin/src/args/walletKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as assert from 'assert';
import * as utxolib from '@bitgo/utxo-lib';

export function isWalletKeyName(name: string): name is utxolib.bitgo.KeyName {
return name === 'user' || name === 'backup' || name === 'bitgo';
}

export const keyOptions = {
userKey: { type: 'string', demandOption: true },
userKeyPrefix: { type: 'string', default: '0/0' },
backupKey: { type: 'string', demandOption: true },
backupKeyPrefix: { type: 'string', default: '0/0' },
bitgoKey: { type: 'string', demandOption: true },
bitgoKeyPrefix: { type: 'string', default: '0/0' },
} as const;

export type KeyOptions = {
userKey: string;
userKeyPrefix?: string;
backupKey: string;
backupKeyPrefix?: string;
bitgoKey: string;
bitgoKeyPrefix?: string;
};

export function getRootWalletKeys(argv: KeyOptions): utxolib.bitgo.RootWalletKeys {
const xpubs = [argv.userKey, argv.backupKey, argv.bitgoKey].map((k) => utxolib.bip32.fromBase58(k));
assert(utxolib.bitgo.isTriple(xpubs));
return new utxolib.bitgo.RootWalletKeys(xpubs, [
argv.userKeyPrefix ?? utxolib.bitgo.RootWalletKeys.defaultPrefix,
argv.backupKeyPrefix ?? utxolib.bitgo.RootWalletKeys.defaultPrefix,
argv.bitgoKeyPrefix ?? utxolib.bitgo.RootWalletKeys.defaultPrefix,
]);
}
Loading

0 comments on commit ac85492

Please sign in to comment.