Skip to content

Commit

Permalink
Merge pull request #4979 from BitGo/BTC-1351.utxo-bin-bip32
Browse files Browse the repository at this point in the history
refactor(utxo-bin): add bip32 subcommand
  • Loading branch information
OttoAllmendinger authored Oct 3, 2024
2 parents 64a6f48 + 824ed10 commit ecef044
Show file tree
Hide file tree
Showing 17 changed files with 151 additions and 89 deletions.
5 changes: 3 additions & 2 deletions modules/utxo-bin/bin/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#!/usr/bin/env node
import * as yargs from 'yargs';

import { cmdParseTx, cmdParseAddress, cmdParseScript, cmdGenerateAddress, cmdParseXpub } from '../src/commands';
import { cmdParseTx, cmdParseScript, cmdBip32, cmdGenerateAddress, cmdParseAddress } from '../src/commands';

yargs
.command(cmdParseTx)
.command(cmdParseAddress)
.command(cmdParseScript)
.command(cmdGenerateAddress)
.command(cmdParseXpub)
.command(cmdBip32)
.strict()
.demandCommand()
.help()
.parse();
13 changes: 13 additions & 0 deletions modules/utxo-bin/src/args/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type FormatTreeOrJson = 'tree' | 'json';

export const formatTreeOrJson = {
type: 'string',
choices: ['tree', 'json'] as const,
default: 'tree',
coerce(arg: string): 'tree' | 'json' {
if (arg !== 'tree' && arg !== 'json') {
throw new Error(`invalid format ${arg}`);
}
return arg;
},
} as const;
1 change: 1 addition & 0 deletions modules/utxo-bin/src/args/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './parseString';
export * from './walletKeys';
export * from './parseNetwork';
export * from './format';
17 changes: 9 additions & 8 deletions modules/utxo-bin/src/bip32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ import * as utxolib from '@bitgo/utxo-lib';
import { Parser, ParserNode } from './Parser';
import { parseUnknown } from './parseUnknown';

export function parseXpub(xpub: string, params: { derive?: string }): ParserNode {
if (!xpub.startsWith('xpub')) {
throw new Error('expected xpub');
}
export function parseBip32(bip32Key: string, params: { derive?: string }): ParserNode {
const parser = new Parser();
let xpubObj = utxolib.bip32.fromBase58(xpub);
let bip32 = utxolib.bip32.fromBase58(bip32Key);
if (params.derive) {
xpubObj = xpubObj.derivePath(params.derive);
bip32 = bip32.derivePath(params.derive);
}
const node = parseUnknown(parser, 'xpub', xpubObj, {
const label = bip32.isNeutered() ? 'xpub' : 'xprv';
const node = parseUnknown(parser, label, bip32, {
omit: ['network', '__Q', '__D', '__DEPTH', '__INDEX', '__PARENT_FINGERPRINT'],
});
node.value = xpubObj.toBase58();
if (!bip32.isNeutered()) {
node.nodes?.unshift(parser.node('xpub', bip32.neutered().toBase58()));
}
node.value = bip32.toBase58();
return node;
}
60 changes: 60 additions & 0 deletions modules/utxo-bin/src/commands/cmdBip32.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as crypto from 'crypto';

import * as yargs from 'yargs';
import { CommandModule } from 'yargs';
import * as utxolib from '@bitgo/utxo-lib';

import { parseBip32 } from '../bip32';
import { formatTreeOrJson, FormatTreeOrJson } from '../args';

import { formatString } from './formatString';

type ArgsBip32Generate = {
format: FormatTreeOrJson;
bip32Key: string;
derive?: string;
all: boolean;
};

export const cmdBip32Parse: CommandModule<unknown, ArgsBip32Generate> = {
command: 'parse [xpub|xprv]',
describe: 'show xpub info',
builder(b: yargs.Argv<unknown>): yargs.Argv<ArgsBip32Generate> {
return b
.options({ format: formatTreeOrJson })
.positional('bip32Key', { type: 'string', demandOption: true })
.option('all', { type: 'boolean', default: false })
.option('derive', { type: 'string', description: 'show xpub derived with path' });
},
handler(argv): void {
console.log(formatString(parseBip32(argv.bip32Key, { derive: argv.derive }), argv));
},
};

type GenerateBip32Args = {
seed: string;
};

export const cmdBip32Generate: CommandModule<unknown, GenerateBip32Args> = {
command: 'generateFromSeed',
describe: 'generate keypair from seed - do not use for real keys!',
builder(b) {
return b.option('seed', { type: 'string', demandOption: true, default: 'setec astronomy' });
},
handler(argv) {
const key = utxolib.bip32.fromSeed(crypto.createHash('sha256').update(argv.seed).digest());
console.log(key.toBase58());
console.log(key.neutered().toBase58());
},
};

export const cmdBip32: CommandModule<unknown, unknown> = {
command: 'bip32 <command>',
describe: 'bip32 commands',
builder(b: yargs.Argv<unknown>): yargs.Argv<unknown> {
return b.strict().command(cmdBip32Parse).command(cmdBip32Generate).demandCommand();
},
handler(): void {
// do nothing
},
};
7 changes: 3 additions & 4 deletions modules/utxo-bin/src/commands/cmdParseAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import * as utxolib from '@bitgo/utxo-lib';
import * as yargs from 'yargs';

import { AddressParser } from '../AddressParser';
import { getNetworkOptions } from '../args';
import { formatTreeOrJson, getNetworkOptions, FormatTreeOrJson } from '../args';

import { OutputFormat } from './cmdParseTx';
import { formatString } from './formatString';

export type ArgsParseAddress = {
network?: utxolib.Network;
all: boolean;
format: OutputFormat;
format: FormatTreeOrJson;
convert: boolean;
address: string;
};
Expand All @@ -26,7 +25,7 @@ export const cmdParseAddress = {
builder(b: yargs.Argv<unknown>): yargs.Argv<ArgsParseAddress> {
return b
.options(getNetworkOptions())
.option('format', { choices: ['tree', 'json'], default: 'tree' } as const)
.option('format', formatTreeOrJson)
.option('convert', { type: 'boolean', default: false })
.option('all', { type: 'boolean', default: false })
.positional('address', { type: 'string', demandOption: true });
Expand Down
7 changes: 3 additions & 4 deletions modules/utxo-bin/src/commands/cmdParseScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import * as utxolib from '@bitgo/utxo-lib';
import * as yargs from 'yargs';

import { ScriptParser } from '../ScriptParser';
import { getNetworkOptions, stringToBuffer } from '../args';
import { formatTreeOrJson, getNetworkOptions, FormatTreeOrJson, stringToBuffer } from '../args';

import { OutputFormat } from './cmdParseTx';
import { formatString } from './formatString';

export type ArgsParseScript = {
network?: utxolib.Network;
format: OutputFormat;
format: FormatTreeOrJson;
all: boolean;
script: string;
};
Expand All @@ -24,7 +23,7 @@ export const cmdParseScript = {
builder(b: yargs.Argv<unknown>): yargs.Argv<ArgsParseScript> {
return b
.options(getNetworkOptions())
.option('format', { choices: ['tree', 'json'], default: 'tree' } as const)
.option('format', formatTreeOrJson)
.option('all', { type: 'boolean', default: false })
.positional('script', { type: 'string', demandOption: true });
},
Expand Down
15 changes: 11 additions & 4 deletions modules/utxo-bin/src/commands/cmdParseTx.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import * as utxolib from '@bitgo/utxo-lib';
import * as yargs from 'yargs';

import { argToString, getNetworkOptionsDemand, ReadStringOptions, readStringOptions, stringToBuffer } from '../args';
import {
argToString,
formatTreeOrJson,
getNetworkOptionsDemand,
FormatTreeOrJson,
ReadStringOptions,
readStringOptions,
stringToBuffer,
} from '../args';
import { TxParser, TxParserArgs } from '../TxParser';
import {
fetchOutputSpends,
Expand All @@ -17,15 +25,14 @@ import { Parser } from '../Parser';

import { formatString } from './formatString';

export type OutputFormat = 'tree' | 'json';
export type ArgsParseTransaction = ReadStringOptions & {
network: utxolib.Network;
txid?: string;
blockHeight?: number;
txIndex?: number;
all: boolean;
cache: boolean;
format: OutputFormat;
format: FormatTreeOrJson;
fetchAll: boolean;
fetchStatus: boolean;
fetchInputs: boolean;
Expand Down Expand Up @@ -95,7 +102,7 @@ export const cmdParseTx = {
default: false,
description: 'use local cache for http responses',
})
.option('format', { choices: ['tree', 'json'], default: 'tree' } as const)
.option('format', formatTreeOrJson)
.option('parseError', { choices: ['continue', 'throw'], default: 'continue' } as const);
},

Expand Down
32 changes: 0 additions & 32 deletions modules/utxo-bin/src/commands/cmdParseXpub.ts

This file was deleted.

5 changes: 2 additions & 3 deletions modules/utxo-bin/src/commands/formatString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import * as yargs from 'yargs';

import { ParserNode } from '../Parser';
import { formatTree } from '../format';

import { OutputFormat } from './cmdParseTx';
import { FormatTreeOrJson } from '../args';

export type FormatStringArgs = {
format: OutputFormat;
format: FormatTreeOrJson;
all: boolean;
};

Expand Down
2 changes: 1 addition & 1 deletion modules/utxo-bin/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export * from './cmdParseTx';
export * from './cmdParseAddress';
export * from './cmdParseScript';
export * from './cmdGenerateAddress';
export * from './cmdParseXpub';
export * from './cmdBip32';
19 changes: 10 additions & 9 deletions modules/utxo-bin/test/bip32.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import * as assert from 'assert';

import { parseXpub } from '../src/bip32';
import { parseBip32 } from '../src/bip32';

import { formatTreeNoColor, getFixtureString } from './fixtures';
import { getKey } from './bip32.util';

function runTest(xpub: string, args: { derive?: string }) {
describe('parse xpub', function () {
it('parses xpub', async function () {
const formatted = formatTreeNoColor(parseXpub(xpub, args), { showAll: true });
const filename = [xpub];
function runTest(bip32Key: string, args: { derive?: string }) {
describe(`parse bip32 ${JSON.stringify(args)}`, function () {
it('has expected output', async function () {
const formatted = formatTreeNoColor(parseBip32(bip32Key, args), { showAll: true });
const filename = [bip32Key];
if (args.derive) {
filename.push(args.derive.replace(/\//g, '_'));
}
assert.strictEqual(await getFixtureString(`test/fixtures/xpub/${filename.join('_')}.txt`, formatted), formatted);
assert.strictEqual(await getFixtureString(`test/fixtures/bip32/${filename.join('_')}.txt`, formatted), formatted);
});
});
}

runTest(getKey('parseXpub').neutered().toBase58(), {});
runTest(getKey('parseXpub').neutered().toBase58(), { derive: 'm/0/0' });
runTest(getKey('bip32').toBase58(), {});
runTest(getKey('bip32').toBase58(), { derive: 'm/0/0' });
runTest(getKey('bip32').neutered().toBase58(), { derive: 'm/0/0' });
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
xprv: xprv9s21ZrQH143K2Fnv8HGStbw1ckdpUZkV5DLHGmw82uiQo1rY59ZiZYhpG2u5HGENgJ5eykfEBC9nuFibB2cjiKXYUwrZ7q8aMRbpWA7N2Pt
├── xpub: xpub661MyMwAqRbcEjsPEJoTFjskAnUJt2ULSSFt5ALjbFFPfpBgcgsy7M2J7Kz66TNWwfYUBSKbhqNp7YJTAeVRfQMkjFjyTw8GzJiNaPQifio
├── lowR: false
├── chainCode: 13cd969b6f5948b45fe7a5e3e22e9d94830c82e9c2b61448af16565131736757 (32 bytes)
├── depth: 0
├── index: 0
├── parentFingerprint: 0
├── identifier: c3b7cdefd973965dd7aef07784908ed11a3e13fb (20 bytes)
├── fingerprint: c3b7cdef (4 bytes)
├── compressed: true
├── publicKey: 03faab4c92cfdb174adb4df27dddbd984dcec499d57b308a6ae343876d8015d0da (33 bytes)
└── privateKey: ebe706fb107727a8af3a75ef3c96672c9ddf4ccd4377b047df7ec43a5020c1ab (32 bytes)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
xprv: xprv9wLeZuTuzBVPggSspWwPzWp9WDL2NTDx5KhyfS3MNGdMpoYME6dZzXVukBSj1sWAJttfUoXck2GrnFHV2sUpmcRiFv26YGSe373QvEaMKpc
├── xpub: xpub6AKzyQzopZ3guAXLvYUQMekt4FAWmuwoSYdaTpSxvcALhbsVmdwpYKpPbST69B5kuSSn5LJFoGSmkcohTCc2Q5kjfa5vcuJaSVEqBY7B2Qk
├── lowR: false
├── chainCode: 4161a1f543ed7c95cc138bd8bfca549a9ba304349e926c6a0bc612a0c5a4e293 (32 bytes)
├── depth: 2
├── index: 0
├── parentFingerprint: 1273784303
├── identifier: 7f8e7c282a46dbeb6b2634faea4d204eaad4eb85 (20 bytes)
├── fingerprint: 7f8e7c28 (4 bytes)
├── compressed: true
├── publicKey: 0224051902c8f2ccd8cf2f27bc5b25dfa12f84573b994007a0943acdd764d873f3 (33 bytes)
└── privateKey: 27340d788194503cf1fd5a1b453e3400537f0c26d40f317211a36d02435584f4 (32 bytes)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
xpub: xpub6AKzyQzopZ3guAXLvYUQMekt4FAWmuwoSYdaTpSxvcALhbsVmdwpYKpPbST69B5kuSSn5LJFoGSmkcohTCc2Q5kjfa5vcuJaSVEqBY7B2Qk
├── lowR: false
├── chainCode: 4161a1f543ed7c95cc138bd8bfca549a9ba304349e926c6a0bc612a0c5a4e293 (32 bytes)
├── depth: 2
├── index: 0
├── parentFingerprint: 1273784303
├── identifier: 7f8e7c282a46dbeb6b2634faea4d204eaad4eb85 (20 bytes)
├── fingerprint: 7f8e7c28 (4 bytes)
├── compressed: true
├── publicKey: 0224051902c8f2ccd8cf2f27bc5b25dfa12f84573b994007a0943acdd764d873f3 (33 bytes)
└── privateKey: undefined

This file was deleted.

This file was deleted.

0 comments on commit ecef044

Please sign in to comment.