From c45214093ac71c4009fbf4b0571d4869f8705844 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 24 Sep 2024 14:02:59 +0200 Subject: [PATCH 1/9] refactor: factor out certain util funcs in utxo-bin TICKET: BTC-1351 --- modules/utxo-bin/bin/index.ts | 2 +- modules/utxo-bin/src/commands.ts | 62 +++++++++++------------ modules/utxo-bin/src/fetch.ts | 17 ++++++- modules/utxo-bin/src/generateAddress.ts | 29 ++++------- modules/utxo-bin/src/getNetworkForName.ts | 13 +++++ modules/utxo-bin/src/walletKeys.ts | 25 +++++++++ 6 files changed, 95 insertions(+), 53 deletions(-) create mode 100644 modules/utxo-bin/src/getNetworkForName.ts create mode 100644 modules/utxo-bin/src/walletKeys.ts diff --git a/modules/utxo-bin/bin/index.ts b/modules/utxo-bin/bin/index.ts index b0f2a8ce53..3ba460e052 100644 --- a/modules/utxo-bin/bin/index.ts +++ b/modules/utxo-bin/bin/index.ts @@ -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) diff --git a/modules/utxo-bin/src/commands.ts b/modules/utxo-bin/src/commands.ts index 5501a11c36..b8adf61414 100644 --- a/modules/utxo-bin/src/commands.ts +++ b/modules/utxo-bin/src/commands.ts @@ -2,7 +2,6 @@ import * as yargs from 'yargs'; import * as fs from 'fs'; import * as process from 'process'; -import { promisify } from 'util'; import clipboardy from 'clipboardy-cjs'; import * as utxolib from '@bitgo/utxo-lib'; @@ -15,10 +14,10 @@ import { fetchPrevOutputSpends, fetchTransactionHex, fetchTransactionStatus, + getClient, } from './fetch'; import { TxParser, TxParserArgs } from './TxParser'; import { AddressParser } from './AddressParser'; -import { BaseHttpClient, CachingHttpClient, HttpClient } from '@bitgo/blockapis'; import { readStdin } from './readStdin'; import { parseUnknown } from './parseUnknown'; import { getParserTxProperties } from './ParserTx'; @@ -33,6 +32,7 @@ import { parseIndexRange, } from './generateAddress'; import { parseXpub } from './bip32'; +import { getNetwork, getNetworkForName } from './getNetworkForName'; type OutputFormat = 'tree' | 'json'; @@ -84,27 +84,14 @@ export type ArgsGenerateAddress = { limit?: number; }; -async function getClient({ cache }: { cache: boolean }): Promise { - if (cache) { - const mkdir = promisify(fs.mkdir); - const dir = `${process.env.HOME}/.cache/utxo-bin/`; - await mkdir(dir, { recursive: true }); - return new CachingHttpClient(dir); - } - return new BaseHttpClient(); -} - -function getNetworkForName(name: string) { - const network = utxolib.networks[name as utxolib.NetworkName]; - if (!network) { - throw new Error(`invalid network ${name}`); - } - return network; -} - -function getNetwork(argv: yargs.Arguments<{ network: string }>): utxolib.Network { - return getNetworkForName(argv.network); -} +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; type FormatStringArgs = { format: OutputFormat; @@ -167,7 +154,11 @@ export const cmdParseTx = { return b .option('path', { type: 'string', nargs: 1, default: '' }) .option('stdin', { type: 'boolean', default: false }) - .option('data', { type: 'string', description: 'transaction bytes (hex or base64)', alias: 'hex' }) + .option('data', { + type: 'string', + description: 'transaction bytes (hex or base64)', + alias: 'hex', + }) .option('clipboard', { type: 'boolean', default: false }) .option('txid', { type: 'string' }) .option('blockHeight', { type: 'number' }) @@ -346,12 +337,7 @@ export const cmdGenerateAddress = { builder(b: yargs.Argv): yargs.Argv { return b .option('network', { alias: 'n', type: 'string' }) - .option('userKey', { type: 'string', demandOption: true }) - .option('userKeyPrefix', { type: 'string', default: '0/0' }) - .option('backupKey', { type: 'string', demandOption: true }) - .option('backupKeyPrefix', { type: 'string', default: '0/0' }) - .option('bitgoKey', { type: 'string', demandOption: true }) - .option('bitgoKeyPrefix', { type: 'string', default: '0/0' }) + .options(keyOptions) .option('format', { type: 'string', default: '%p0\t%a', @@ -396,14 +382,26 @@ export const cmdGenerateAddress = { export const cmdParseXpub = { command: 'parseXpub [xpub]', describe: 'show xpub info', - builder(b: yargs.Argv): yargs.Argv<{ xpub: string; derive?: string } & FormatStringArgs> { + builder(b: yargs.Argv): yargs.Argv< + { + xpub: string; + derive?: string; + } & FormatStringArgs + > { return b .positional('xpub', { type: 'string', demandOption: true }) .option('format', { choices: ['tree', 'json'], default: 'tree' } as const) .option('all', { type: 'boolean', default: false }) .option('derive', { type: 'string', description: 'show xpub derived with path' }); }, - handler(argv: yargs.Arguments<{ xpub: string; derive?: string } & FormatStringArgs>): void { + handler( + argv: yargs.Arguments< + { + xpub: string; + derive?: string; + } & FormatStringArgs + > + ): void { console.log(formatString(parseXpub(argv.xpub, { derive: argv.derive }), argv)); }, }; diff --git a/modules/utxo-bin/src/fetch.ts b/modules/utxo-bin/src/fetch.ts index 06c9ef6db3..82a539f952 100644 --- a/modules/utxo-bin/src/fetch.ts +++ b/modules/utxo-bin/src/fetch.ts @@ -1,8 +1,13 @@ +import * as fs from 'fs'; +import * as process from 'process'; + import * as utxolib from '@bitgo/utxo-lib'; import * as blockapis from '@bitgo/blockapis'; +import { BaseHttpClient, CachingHttpClient, getTransactionIdsAtHeight, HttpClient } from '@bitgo/blockapis'; import { coins, UtxoCoin } from '@bitgo/statics'; -import { getTransactionIdsAtHeight, HttpClient } from '@bitgo/blockapis'; + import { ParserTx } from './ParserTx'; +import { promisify } from 'util'; function getTxOutPoints(tx: ParserTx): utxolib.bitgo.TxOutPoint[] { if (tx instanceof utxolib.bitgo.UtxoTransaction) { @@ -114,3 +119,13 @@ export async function fetchOutputSpends( return []; } } + +export async function getClient({ cache }: { cache: boolean }): Promise { + if (cache) { + const mkdir = promisify(fs.mkdir); + const dir = `${process.env.HOME}/.cache/utxo-bin/`; + await mkdir(dir, { recursive: true }); + return new CachingHttpClient(dir); + } + return new BaseHttpClient(); +} diff --git a/modules/utxo-bin/src/generateAddress.ts b/modules/utxo-bin/src/generateAddress.ts index f7de37bc7a..69fcdf6797 100644 --- a/modules/utxo-bin/src/generateAddress.ts +++ b/modules/utxo-bin/src/generateAddress.ts @@ -3,6 +3,7 @@ import * as utxolib from '@bitgo/utxo-lib'; import { Parser } from './Parser'; import { parseUnknown } from './parseUnknown'; import { formatTree } from './format'; +import { KeyOptions, getRootWalletKeys } from './walletKeys'; function getDefaultChainCodes(): number[] { return utxolib.bitgo.chainCodes.filter( @@ -108,25 +109,15 @@ export function parseIndexRange(ranges: string[]): number[] { }); } -export function* generateAddress(argv: { - network?: utxolib.Network; - userKey: string; - userKeyPrefix?: string; - backupKey: string; - backupKeyPrefix?: string; - bitgoKey: string; - bitgoKeyPrefix?: string; - chain?: number[]; - format: string; - index: number[]; -}): Generator { - const xpubs = [argv.userKey, argv.backupKey, argv.bitgoKey].map((k) => utxolib.bip32.fromBase58(k)); - assert(utxolib.bitgo.isTriple(xpubs)); - const rootXpubs = new utxolib.bitgo.RootWalletKeys(xpubs, [ - argv.userKeyPrefix ?? utxolib.bitgo.RootWalletKeys.defaultPrefix, - argv.backupKeyPrefix ?? utxolib.bitgo.RootWalletKeys.defaultPrefix, - argv.bitgoKeyPrefix ?? utxolib.bitgo.RootWalletKeys.defaultPrefix, - ]); +export function* generateAddress( + argv: KeyOptions & { + network?: utxolib.Network; + chain?: number[]; + format: string; + index: number[]; + } +): Generator { + const rootXpubs = getRootWalletKeys(argv); const chains = argv.chain ?? getDefaultChainCodes(); for (const i of argv.index) { for (const chain of chains) { diff --git a/modules/utxo-bin/src/getNetworkForName.ts b/modules/utxo-bin/src/getNetworkForName.ts new file mode 100644 index 0000000000..cdc5464298 --- /dev/null +++ b/modules/utxo-bin/src/getNetworkForName.ts @@ -0,0 +1,13 @@ +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); +} diff --git a/modules/utxo-bin/src/walletKeys.ts b/modules/utxo-bin/src/walletKeys.ts new file mode 100644 index 0000000000..b541f4c1ea --- /dev/null +++ b/modules/utxo-bin/src/walletKeys.ts @@ -0,0 +1,25 @@ +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 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, + ]); +} From 25707169c5ce2fcc01e3c7baa0339b8571b0570b Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 25 Sep 2024 17:41:12 +0200 Subject: [PATCH 2/9] refactor: add argToString Issue: BTC-1351 --- modules/utxo-bin/src/commands.ts | 91 +++++++++++++++++++------------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/modules/utxo-bin/src/commands.ts b/modules/utxo-bin/src/commands.ts index b8adf61414..47129773dd 100644 --- a/modules/utxo-bin/src/commands.ts +++ b/modules/utxo-bin/src/commands.ts @@ -141,6 +141,57 @@ export function getScriptParser(argv: ArgsParseScript): ScriptParser { return new ScriptParser(resolveNetwork(argv)); } +/** + * @param argv + * @param input - optional input data. If set, this function just ensures that nothing else is set. + * @return string from specified source + */ +async function argToString( + argv: { + clipboard?: boolean; + path?: string; + data?: string; + stdin: boolean; + }, + input?: string +): Promise { + 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; +} + export const cmdParseTx = { command: 'parseTx [path]', aliases: ['parse', 'tx'], @@ -215,46 +266,12 @@ export const cmdParseTx = { ); } - if (argv.stdin || argv.path === '-') { - if (data) { - 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) { - data = await readStdin(); - } else { - data = await fs.promises.readFile('/dev/stdin', 'utf8'); - } - } - - if (argv.clipboard) { - if (data) { - throw new Error(`conflicting arguments`); - } - data = await clipboardy.read(); - } - - if (argv.path) { - if (data) { - throw new Error(`conflicting arguments`); - } - data = (await fs.promises.readFile(argv.path, 'utf8')).toString(); - } - - if (argv.data) { - if (data) { - throw new Error(`conflicting arguments`); - } - data = argv.data; - } - - // strip whitespace - if (!data) { + const string = await argToString(argv, data); + if (!string) { throw new Error(`no txdata`); } - const bytes = stringToBuffer(data, ['hex', 'base64']); + const bytes = stringToBuffer(string, 'hex'); let tx = utxolib.bitgo.isPsbt(bytes) ? utxolib.bitgo.createPsbtFromBuffer(bytes, network) From af3df64ed0418bad653f59c557fa9129678aaca7 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 26 Sep 2024 09:24:28 +0200 Subject: [PATCH 3/9] refactor: move argToString to parseString Issue: BTC-1351 --- modules/utxo-bin/src/commands.ts | 58 +---------------------------- modules/utxo-bin/src/parseString.ts | 58 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/modules/utxo-bin/src/commands.ts b/modules/utxo-bin/src/commands.ts index 47129773dd..a99c2ad962 100644 --- a/modules/utxo-bin/src/commands.ts +++ b/modules/utxo-bin/src/commands.ts @@ -1,9 +1,5 @@ /* eslint-disable no-console */ import * as yargs from 'yargs'; -import * as fs from 'fs'; -import * as process from 'process'; - -import clipboardy from 'clipboardy-cjs'; import * as utxolib from '@bitgo/utxo-lib'; import { Parser, ParserNode } from './Parser'; @@ -18,11 +14,10 @@ import { } from './fetch'; import { TxParser, TxParserArgs } from './TxParser'; import { AddressParser } from './AddressParser'; -import { readStdin } from './readStdin'; import { parseUnknown } from './parseUnknown'; import { getParserTxProperties } from './ParserTx'; import { ScriptParser } from './ScriptParser'; -import { stringToBuffer } from './parseString'; +import { argToString, stringToBuffer } from './parseString'; import { formatAddressTree, formatAddressWithFormatString, @@ -141,57 +136,6 @@ export function getScriptParser(argv: ArgsParseScript): ScriptParser { return new ScriptParser(resolveNetwork(argv)); } -/** - * @param argv - * @param input - optional input data. If set, this function just ensures that nothing else is set. - * @return string from specified source - */ -async function argToString( - argv: { - clipboard?: boolean; - path?: string; - data?: string; - stdin: boolean; - }, - input?: string -): Promise { - 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; -} - export const cmdParseTx = { command: 'parseTx [path]', aliases: ['parse', 'tx'], diff --git a/modules/utxo-bin/src/parseString.ts b/modules/utxo-bin/src/parseString.ts index 6572998b3f..40ec9819d9 100644 --- a/modules/utxo-bin/src/parseString.ts +++ b/modules/utxo-bin/src/parseString.ts @@ -1,3 +1,10 @@ +import * as process from 'process'; +import * as fs from 'fs'; + +import clipboardy from 'clipboardy-cjs'; + +import { readStdin } from './readStdin'; + type Format = 'hex' | 'base64'; export function stringToBuffer(data: string, format: Format | Format[]): Buffer { if (typeof format !== 'string') { @@ -25,3 +32,54 @@ export function stringToBuffer(data: string, format: Format | Format[]): Buffer } return buf; } + +/** + * @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: { + clipboard?: boolean; + path?: string; + data?: string; + stdin: boolean; + }, + input?: string +): Promise { + 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; +} From fe16ae72b00bc6dcadde87bc818128613d36b9d1 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 26 Sep 2024 09:27:36 +0200 Subject: [PATCH 4/9] refactor: factor out readStringOptions Issue: BTC-1351 --- modules/utxo-bin/src/commands.ts | 11 ++--------- modules/utxo-bin/src/parseString.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/utxo-bin/src/commands.ts b/modules/utxo-bin/src/commands.ts index a99c2ad962..c4d7cad4f1 100644 --- a/modules/utxo-bin/src/commands.ts +++ b/modules/utxo-bin/src/commands.ts @@ -17,7 +17,7 @@ import { AddressParser } from './AddressParser'; import { parseUnknown } from './parseUnknown'; import { getParserTxProperties } from './ParserTx'; import { ScriptParser } from './ScriptParser'; -import { argToString, stringToBuffer } from './parseString'; +import { readStringOptions, argToString, stringToBuffer } from './parseString'; import { formatAddressTree, formatAddressWithFormatString, @@ -147,14 +147,7 @@ export const cmdParseTx = { builder(b: yargs.Argv): yargs.Argv { return b - .option('path', { type: 'string', nargs: 1, default: '' }) - .option('stdin', { type: 'boolean', default: false }) - .option('data', { - type: 'string', - description: 'transaction bytes (hex or base64)', - alias: 'hex', - }) - .option('clipboard', { type: 'boolean', default: false }) + .options(readStringOptions) .option('txid', { type: 'string' }) .option('blockHeight', { type: 'number' }) .option('txIndex', { type: 'number' }) diff --git a/modules/utxo-bin/src/parseString.ts b/modules/utxo-bin/src/parseString.ts index 40ec9819d9..93a788c3ef 100644 --- a/modules/utxo-bin/src/parseString.ts +++ b/modules/utxo-bin/src/parseString.ts @@ -33,6 +33,17 @@ export function stringToBuffer(data: string, format: Format | Format[]): Buffer 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; + /** * @param argv * @param input - optional input data. If set, this function just ensures that nothing else is set. From a09818ff7339b7591b0c63bf8d9012ac1610c6bf Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 26 Sep 2024 09:38:39 +0200 Subject: [PATCH 5/9] refactor: move readStdin to parseString Issue: BTC-1351 --- modules/utxo-bin/src/parseString.ts | 43 +++++++++++++++++++++++++++-- modules/utxo-bin/src/readStdin.ts | 41 --------------------------- 2 files changed, 41 insertions(+), 43 deletions(-) delete mode 100644 modules/utxo-bin/src/readStdin.ts diff --git a/modules/utxo-bin/src/parseString.ts b/modules/utxo-bin/src/parseString.ts index 93a788c3ef..abd2581b7e 100644 --- a/modules/utxo-bin/src/parseString.ts +++ b/modules/utxo-bin/src/parseString.ts @@ -3,8 +3,6 @@ import * as fs from 'fs'; import clipboardy from 'clipboardy-cjs'; -import { readStdin } from './readStdin'; - type Format = 'hex' | 'base64'; export function stringToBuffer(data: string, format: Format | Format[]): Buffer { if (typeof format !== 'string') { @@ -44,6 +42,47 @@ export const readStringOptions = { clipboard: { type: 'boolean', default: false }, } as const; +/** + * Reads from stdin until Ctrl-D is pressed. + */ +export async function readStdin(): Promise { + /* + * 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. diff --git a/modules/utxo-bin/src/readStdin.ts b/modules/utxo-bin/src/readStdin.ts deleted file mode 100644 index 1b1d566549..0000000000 --- a/modules/utxo-bin/src/readStdin.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Contains a high-performance implementation of reading from stdin. - * Standard readline is extremely slow for long lines. - */ - -/** - * Reads from stdin until Ctrl-D is pressed. - */ -export async function readStdin(): Promise { - 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); - }); - }); -} From 5a193de492fa3b350c055f42ae6c23e4b9c664c5 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 26 Sep 2024 09:39:52 +0200 Subject: [PATCH 6/9] refactor: move parseString.ts to args/ Issue: BTC-1351 --- modules/utxo-bin/src/args/index.ts | 1 + modules/utxo-bin/src/{ => args}/parseString.ts | 17 ++++++++--------- modules/utxo-bin/src/commands.ts | 8 ++------ .../utxo-bin/test/{ => args}/stringToBuffer.ts | 2 +- 4 files changed, 12 insertions(+), 16 deletions(-) create mode 100644 modules/utxo-bin/src/args/index.ts rename modules/utxo-bin/src/{ => args}/parseString.ts (93%) rename modules/utxo-bin/test/{ => args}/stringToBuffer.ts (89%) diff --git a/modules/utxo-bin/src/args/index.ts b/modules/utxo-bin/src/args/index.ts new file mode 100644 index 0000000000..04e059e7b8 --- /dev/null +++ b/modules/utxo-bin/src/args/index.ts @@ -0,0 +1 @@ +export * from './parseString'; diff --git a/modules/utxo-bin/src/parseString.ts b/modules/utxo-bin/src/args/parseString.ts similarity index 93% rename from modules/utxo-bin/src/parseString.ts rename to modules/utxo-bin/src/args/parseString.ts index abd2581b7e..0068310b6d 100644 --- a/modules/utxo-bin/src/parseString.ts +++ b/modules/utxo-bin/src/args/parseString.ts @@ -42,6 +42,13 @@ export const readStringOptions = { 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. */ @@ -88,15 +95,7 @@ export async function readStdin(): Promise { * @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: { - clipboard?: boolean; - path?: string; - data?: string; - stdin: boolean; - }, - input?: string -): Promise { +export async function argToString(argv: ReadStringOptions, input?: string): Promise { if (argv.stdin || argv.path === '-') { if (input) { throw new Error(`conflicting arguments`); diff --git a/modules/utxo-bin/src/commands.ts b/modules/utxo-bin/src/commands.ts index c4d7cad4f1..6ce6c63e0f 100644 --- a/modules/utxo-bin/src/commands.ts +++ b/modules/utxo-bin/src/commands.ts @@ -17,7 +17,7 @@ import { AddressParser } from './AddressParser'; import { parseUnknown } from './parseUnknown'; import { getParserTxProperties } from './ParserTx'; import { ScriptParser } from './ScriptParser'; -import { readStringOptions, argToString, stringToBuffer } from './parseString'; +import { argToString, stringToBuffer, readStringOptions, ReadStringOptions } from './args'; import { formatAddressTree, formatAddressWithFormatString, @@ -31,15 +31,11 @@ import { getNetwork, getNetworkForName } from './getNetworkForName'; type OutputFormat = 'tree' | 'json'; -type ArgsParseTransaction = { +type ArgsParseTransaction = ReadStringOptions & { network: string; - stdin: boolean; - clipboard: boolean; - path?: string; txid?: string; blockHeight?: number; txIndex?: number; - data?: string; all: boolean; cache: boolean; format: OutputFormat; diff --git a/modules/utxo-bin/test/stringToBuffer.ts b/modules/utxo-bin/test/args/stringToBuffer.ts similarity index 89% rename from modules/utxo-bin/test/stringToBuffer.ts rename to modules/utxo-bin/test/args/stringToBuffer.ts index 1f6c99b528..1af01228f9 100644 --- a/modules/utxo-bin/test/stringToBuffer.ts +++ b/modules/utxo-bin/test/args/stringToBuffer.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; -import { stringToBuffer } from '../src/parseString'; +import { stringToBuffer } from '../../src/args/parseString'; describe('stringToBuffer', function () { const bytes = Buffer.alloc(32, 42); From 81f3f281637c93bdb86d3992ea82b5a2fc77307e Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 26 Sep 2024 09:41:38 +0200 Subject: [PATCH 7/9] refactor: move walletKeys.ts to args/ Issue: BTC-1351 --- modules/utxo-bin/src/args/index.ts | 1 + modules/utxo-bin/src/{ => args}/walletKeys.ts | 9 +++++++++ modules/utxo-bin/src/commands.ts | 16 ++-------------- modules/utxo-bin/src/generateAddress.ts | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) rename modules/utxo-bin/src/{ => args}/walletKeys.ts (71%) diff --git a/modules/utxo-bin/src/args/index.ts b/modules/utxo-bin/src/args/index.ts index 04e059e7b8..caf90a1c2e 100644 --- a/modules/utxo-bin/src/args/index.ts +++ b/modules/utxo-bin/src/args/index.ts @@ -1 +1,2 @@ export * from './parseString'; +export * from './walletKeys'; diff --git a/modules/utxo-bin/src/walletKeys.ts b/modules/utxo-bin/src/args/walletKeys.ts similarity index 71% rename from modules/utxo-bin/src/walletKeys.ts rename to modules/utxo-bin/src/args/walletKeys.ts index b541f4c1ea..a1a0fdfaf6 100644 --- a/modules/utxo-bin/src/walletKeys.ts +++ b/modules/utxo-bin/src/args/walletKeys.ts @@ -5,6 +5,15 @@ 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; diff --git a/modules/utxo-bin/src/commands.ts b/modules/utxo-bin/src/commands.ts index 6ce6c63e0f..7ffb8f98e8 100644 --- a/modules/utxo-bin/src/commands.ts +++ b/modules/utxo-bin/src/commands.ts @@ -17,7 +17,7 @@ import { AddressParser } from './AddressParser'; import { parseUnknown } from './parseUnknown'; import { getParserTxProperties } from './ParserTx'; import { ScriptParser } from './ScriptParser'; -import { argToString, stringToBuffer, readStringOptions, ReadStringOptions } from './args'; +import { argToString, KeyOptions, keyOptions, readStringOptions, ReadStringOptions, stringToBuffer } from './args'; import { formatAddressTree, formatAddressWithFormatString, @@ -64,26 +64,14 @@ type ArgsParseScript = { script: string; }; -export type ArgsGenerateAddress = { +export type ArgsGenerateAddress = KeyOptions & { network?: string; - userKey: string; - backupKey: string; - bitgoKey: string; chain?: number[]; format: string; index?: string[]; limit?: number; }; -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; - type FormatStringArgs = { format: OutputFormat; all: boolean; diff --git a/modules/utxo-bin/src/generateAddress.ts b/modules/utxo-bin/src/generateAddress.ts index 69fcdf6797..2417816100 100644 --- a/modules/utxo-bin/src/generateAddress.ts +++ b/modules/utxo-bin/src/generateAddress.ts @@ -3,7 +3,7 @@ import * as utxolib from '@bitgo/utxo-lib'; import { Parser } from './Parser'; import { parseUnknown } from './parseUnknown'; import { formatTree } from './format'; -import { KeyOptions, getRootWalletKeys } from './walletKeys'; +import { KeyOptions, getRootWalletKeys } from './args'; function getDefaultChainCodes(): number[] { return utxolib.bitgo.chainCodes.filter( From 97e8dea8a8adebaa67f3ab8b233e5e2d11f82c56 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 26 Sep 2024 09:43:47 +0200 Subject: [PATCH 8/9] refactor: move getNetworkForName to args/ TICKET: BTC-1351 --- modules/utxo-bin/src/args/index.ts | 1 + .../{getNetworkForName.ts => args/parseNetwork.ts} | 0 modules/utxo-bin/src/commands.ts | 12 ++++++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) rename modules/utxo-bin/src/{getNetworkForName.ts => args/parseNetwork.ts} (100%) diff --git a/modules/utxo-bin/src/args/index.ts b/modules/utxo-bin/src/args/index.ts index caf90a1c2e..264640f0a8 100644 --- a/modules/utxo-bin/src/args/index.ts +++ b/modules/utxo-bin/src/args/index.ts @@ -1,2 +1,3 @@ export * from './parseString'; export * from './walletKeys'; +export * from './parseNetwork'; diff --git a/modules/utxo-bin/src/getNetworkForName.ts b/modules/utxo-bin/src/args/parseNetwork.ts similarity index 100% rename from modules/utxo-bin/src/getNetworkForName.ts rename to modules/utxo-bin/src/args/parseNetwork.ts diff --git a/modules/utxo-bin/src/commands.ts b/modules/utxo-bin/src/commands.ts index 7ffb8f98e8..95c530dfc3 100644 --- a/modules/utxo-bin/src/commands.ts +++ b/modules/utxo-bin/src/commands.ts @@ -17,7 +17,16 @@ import { AddressParser } from './AddressParser'; import { parseUnknown } from './parseUnknown'; import { getParserTxProperties } from './ParserTx'; import { ScriptParser } from './ScriptParser'; -import { argToString, KeyOptions, keyOptions, readStringOptions, ReadStringOptions, stringToBuffer } from './args'; +import { + argToString, + KeyOptions, + keyOptions, + readStringOptions, + ReadStringOptions, + stringToBuffer, + getNetwork, + getNetworkForName, +} from './args'; import { formatAddressTree, formatAddressWithFormatString, @@ -27,7 +36,6 @@ import { parseIndexRange, } from './generateAddress'; import { parseXpub } from './bip32'; -import { getNetwork, getNetworkForName } from './getNetworkForName'; type OutputFormat = 'tree' | 'json'; From c37c2c358c8ad9321d08e9357c82caed2bbb5fbc Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 26 Sep 2024 09:45:27 +0200 Subject: [PATCH 9/9] refactor: use getNetworkOptions(Demand) for network options Gets rid of some boilerplate to convert a string to a network object Issue: BTC-1351 --- modules/utxo-bin/src/args/parseNetwork.ts | 30 +++++++++++++ modules/utxo-bin/src/commands.ts | 51 +++++++++-------------- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/modules/utxo-bin/src/args/parseNetwork.ts b/modules/utxo-bin/src/args/parseNetwork.ts index cdc5464298..7c51daa4bc 100644 --- a/modules/utxo-bin/src/args/parseNetwork.ts +++ b/modules/utxo-bin/src/args/parseNetwork.ts @@ -11,3 +11,33 @@ export function getNetworkForName(name: string): utxolib.Network { export function getNetwork(argv: { network: string }): utxolib.Network { return getNetworkForName(argv.network); } + +type DemandOption = T & { demandOption: true }; + +type NetworkOption = { + type: 'string'; + description: string; + default: TDefault; + coerce: (arg: string) => utxolib.Network; +}; + +export function getNetworkOptions(defaultValue?: string): { + network: NetworkOption; +} { + return { + network: { + type: 'string', + description: 'network name', + default: defaultValue, + coerce: getNetworkForName, + }, + }; +} + +export function getNetworkOptionsDemand(defaultValue?: string): { + network: DemandOption>; +} { + return { + network: { ...getNetworkOptions(defaultValue).network, demandOption: true }, + }; +} diff --git a/modules/utxo-bin/src/commands.ts b/modules/utxo-bin/src/commands.ts index 95c530dfc3..bbf4c6413a 100644 --- a/modules/utxo-bin/src/commands.ts +++ b/modules/utxo-bin/src/commands.ts @@ -19,13 +19,13 @@ import { getParserTxProperties } from './ParserTx'; import { ScriptParser } from './ScriptParser'; import { argToString, - KeyOptions, - keyOptions, + stringToBuffer, readStringOptions, ReadStringOptions, - stringToBuffer, - getNetwork, - getNetworkForName, + KeyOptions, + keyOptions, + getNetworkOptions, + getNetworkOptionsDemand, } from './args'; import { formatAddressTree, @@ -40,7 +40,7 @@ import { parseXpub } from './bip32'; type OutputFormat = 'tree' | 'json'; type ArgsParseTransaction = ReadStringOptions & { - network: string; + network: utxolib.Network; txid?: string; blockHeight?: number; txIndex?: number; @@ -58,7 +58,7 @@ type ArgsParseTransaction = ReadStringOptions & { } & Omit; type ArgsParseAddress = { - network?: string; + network?: utxolib.Network; all: boolean; format: OutputFormat; convert: boolean; @@ -66,14 +66,14 @@ type ArgsParseAddress = { }; type ArgsParseScript = { - network?: string; + network?: utxolib.Network; format: OutputFormat; all: boolean; script: string; }; export type ArgsGenerateAddress = KeyOptions & { - network?: string; + network: utxolib.Network; chain?: number[]; format: string; index?: string[]; @@ -95,17 +95,6 @@ function formatString(parsed: ParserNode, argv: yargs.Arguments( - args: T -): T & { - network?: utxolib.Network; -} { - if (args.network) { - return { ...args, network: getNetworkForName(args.network) }; - } - return { ...args, network: undefined }; -} - export function getTxParser(argv: yargs.Arguments): TxParser { if (argv.all) { return new TxParser({ ...argv, ...TxParser.PARSE_ALL }); @@ -121,11 +110,11 @@ export function getTxParser(argv: yargs.Arguments): TxPars } export function getAddressParser(argv: ArgsParseAddress): AddressParser { - return new AddressParser(resolveNetwork(argv)); + return new AddressParser(argv); } export function getScriptParser(argv: ArgsParseScript): ScriptParser { - return new ScriptParser(resolveNetwork(argv)); + return new ScriptParser(argv); } export const cmdParseTx = { @@ -140,6 +129,7 @@ export const cmdParseTx = { builder(b: yargs.Argv): yargs.Argv { return b .options(readStringOptions) + .options(getNetworkOptionsDemand()) .option('txid', { type: 'string' }) .option('blockHeight', { type: 'number' }) .option('txIndex', { type: 'number' }) @@ -147,7 +137,6 @@ export const cmdParseTx = { .option('fetchStatus', { type: 'boolean', default: false }) .option('fetchInputs', { type: 'boolean', default: false }) .option('fetchSpends', { type: 'boolean', default: false }) - .option('network', { alias: 'n', type: 'string', demandOption: true }) .option('parseScriptAsm', { alias: 'scriptasm', type: 'boolean', default: false }) .option('parseScriptData', { alias: 'scriptdata', type: 'boolean', default: false }) .option('parseSignatureData', { alias: 'sigdata', type: 'boolean', default: false }) @@ -178,7 +167,6 @@ export const cmdParseTx = { }, async handler(argv: yargs.Arguments): Promise { - const network = getNetwork(argv); let data; const httpClient = await getClient({ cache: argv.cache }); @@ -191,7 +179,7 @@ export const cmdParseTx = { blockHeight: argv.blockHeight, txIndex: argv.txIndex, }, - network + argv.network ); } @@ -203,8 +191,8 @@ export const cmdParseTx = { const bytes = stringToBuffer(string, 'hex'); let tx = utxolib.bitgo.isPsbt(bytes) - ? utxolib.bitgo.createPsbtFromBuffer(bytes, network) - : utxolib.bitgo.createTransactionFromBuffer(bytes, network, { amountType: 'bigint' }); + ? utxolib.bitgo.createPsbtFromBuffer(bytes, argv.network) + : utxolib.bitgo.createTransactionFromBuffer(bytes, argv.network, { amountType: 'bigint' }); const { id: txid } = getParserTxProperties(tx, undefined); if (tx instanceof utxolib.bitgo.UtxoTransaction) { @@ -228,7 +216,7 @@ export const cmdParseTx = { } const parsed = getTxParser(argv).parse(tx, { - status: argv.fetchStatus && txid ? await fetchTransactionStatus(httpClient, txid, network) : undefined, + status: argv.fetchStatus && txid ? await fetchTransactionStatus(httpClient, txid, argv.network) : undefined, prevOutputs: argv.fetchInputs ? await fetchPrevOutputs(httpClient, tx) : undefined, prevOutputSpends: argv.fetchSpends ? await fetchPrevOutputSpends(httpClient, tx) : undefined, outputSpends: @@ -247,7 +235,7 @@ export const cmdParseAddress = { describe: 'parse address', builder(b: yargs.Argv): yargs.Argv { return b - .option('network', { alias: 'n', type: 'string' }) + .options(getNetworkOptions()) .option('format', { choices: ['tree', 'json'], default: 'tree' } as const) .option('convert', { type: 'boolean', default: false }) .option('all', { type: 'boolean', default: false }) @@ -265,7 +253,7 @@ export const cmdParseScript = { describe: 'parse script', builder(b: yargs.Argv): yargs.Argv { return b - .option('network', { alias: 'n', type: 'string' }) + .options(getNetworkOptions()) .option('format', { choices: ['tree', 'json'], default: 'tree' } as const) .option('all', { type: 'boolean', default: false }) .positional('script', { type: 'string', demandOption: true }); @@ -282,7 +270,7 @@ export const cmdGenerateAddress = { describe: 'generate addresses', builder(b: yargs.Argv): yargs.Argv { return b - .option('network', { alias: 'n', type: 'string' }) + .options(getNetworkOptionsDemand('bitcoin')) .options(keyOptions) .option('format', { type: 'string', @@ -314,7 +302,6 @@ export const cmdGenerateAddress = { for (const address of generateAddress({ ...argv, index: indexRange, - network: getNetworkForName(argv.network ?? 'bitcoin'), })) { if (argv.format === 'tree') { console.log(formatAddressTree(address));