Skip to content

Commit

Permalink
Merge pull request #4994 from BitGo/BTC-1351.utxo-bin-address-subcommand
Browse files Browse the repository at this point in the history
feat(utxo-bin): add address subcommand
  • Loading branch information
OttoAllmendinger authored Oct 7, 2024
2 parents 1d46fb3 + 6266c62 commit 892339a
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 97 deletions.
5 changes: 2 additions & 3 deletions modules/utxo-bin/bin/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#!/usr/bin/env node
import * as yargs from 'yargs';

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

yargs
.command(cmdParseTx)
.command(cmdParseAddress)
.command(cmdAddress)
.command(cmdParseScript)
.command(cmdGenerateAddress)
.command(cmdBip32)
.strict()
.demandCommand()
Expand Down
76 changes: 76 additions & 0 deletions modules/utxo-bin/src/commands/cmdAddress/cmdGenerate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as utxolib from '@bitgo/utxo-lib';
import { CommandModule } from 'yargs';

import { getNetworkOptionsDemand, keyOptions, KeyOptions } from '../../args';
import {
formatAddressTree,
formatFixedScriptAddress,
generateFixedScriptAddress,
getFixedScriptAddressPlaceholderDescription,
getRange,
parseIndexRange,
} from '../../generateAddress';

type IndexLimitOptions = {
index?: string[];
limit?: number;
};

const indexLimitOptions = {
index: {
type: 'string',
array: true,
description: 'Address index. Can be given as a range (e.g. 0-99). Takes precedence over --limit.',
},
limit: {
type: 'number',
description: 'Alias for --index with range starting at 0 to limit-1.',
default: 100,
},
} as const;

function getIndexRangeFromArgv(argv: IndexLimitOptions): number[] {
if (argv.index) {
return parseIndexRange(argv.index);
}
if (argv.limit) {
return getRange(0, argv.limit - 1);
}
throw new Error(`no index or limit`);
}

type ArgsGenerateAddressFixedScript = KeyOptions &
IndexLimitOptions & {
network: utxolib.Network;
chain?: number[];
format: string;
};

export const cmdGenerateFixedScript: CommandModule<unknown, ArgsGenerateAddressFixedScript> = {
command: 'fromFixedScript',
describe: 'generate bitgo fixed-script addresses',
builder(b) {
return b
.options(getNetworkOptionsDemand('bitcoin'))
.options(keyOptions)
.option('format', {
type: 'string',
default: '%p0\t%a',
description: `Format string.\nPlaceholders:\n${getFixedScriptAddressPlaceholderDescription()}`,
})
.option('chain', { type: 'number', array: true, description: 'Address chain' })
.options(indexLimitOptions);
},
handler(argv): void {
for (const address of generateFixedScriptAddress({
...argv,
index: getIndexRangeFromArgv(argv),
})) {
if (argv.format === 'tree') {
console.log(formatAddressTree(address));
} else {
console.log(formatFixedScriptAddress(address, argv.format));
}
}
},
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as utxolib from '@bitgo/utxo-lib';
import * as yargs from 'yargs';

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

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

export type ArgsParseAddress = {
network?: utxolib.Network;
all: boolean;
format: FormatTreeOrJson;
all: boolean;
convert: boolean;
address: string;
};
Expand All @@ -18,8 +18,8 @@ export function getAddressParser(argv: ArgsParseAddress): AddressParser {
return new AddressParser(argv);
}

export const cmdParseAddress = {
command: 'parseAddress [address]',
export const cmdParse = {
command: 'parse [address]',
aliases: ['address'],
describe: 'parse address',
builder(b: yargs.Argv<unknown>): yargs.Argv<ArgsParseAddress> {
Expand Down
15 changes: 15 additions & 0 deletions modules/utxo-bin/src/commands/cmdAddress/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CommandModule } from 'yargs';

import { cmdGenerateFixedScript } from './cmdGenerate';
import { cmdParse } from './cmdParse';

export const cmdAddress: CommandModule<unknown, unknown> = {
command: 'address <command>',
describe: 'address commands',
builder(b) {
return b.strict().command(cmdGenerateFixedScript).command(cmdParse).demandCommand();
},
handler() {
// do nothing
},
};
67 changes: 0 additions & 67 deletions modules/utxo-bin/src/commands/cmdGenerateAddress.ts

This file was deleted.

4 changes: 1 addition & 3 deletions modules/utxo-bin/src/commands/formatString.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as yargs from 'yargs';

import { ParserNode } from '../Parser';
import { formatTree } from '../format';
import { FormatTreeOrJson } from '../args';
Expand All @@ -9,7 +7,7 @@ export type FormatStringArgs = {
all: boolean;
};

export function formatString(parsed: ParserNode, argv: yargs.Arguments<FormatStringArgs>): string {
export function formatString(parsed: ParserNode, argv: FormatStringArgs): string {
switch (argv.format) {
case 'json':
return JSON.stringify(parsed, null, 2);
Expand Down
3 changes: 1 addition & 2 deletions modules/utxo-bin/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './cmdParseTx';
export * from './cmdParseAddress';
export * from './cmdAddress';
export * from './cmdParseScript';
export * from './cmdGenerateAddress';
export * from './cmdBip32';
32 changes: 22 additions & 10 deletions modules/utxo-bin/src/generateAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function getDefaultChainCodes(): number[] {
);
}

type AddressProperties = {
type FixedScriptAddressProperties = {
chain: utxolib.bitgo.ChainCode;
index: number;
type: utxolib.bitgo.outputScripts.ScriptType;
Expand All @@ -30,7 +30,7 @@ type AddressProperties = {
address: string;
};

const placeholders = {
const fixedScriptPlaceholders = {
'%c': 'chain',
'%i': 'index',
'%p': 'userPath',
Expand All @@ -47,18 +47,22 @@ const placeholders = {
'%a': 'address',
} as const;

export function getAddressPlaceholderDescription(): string {
return Object.entries(placeholders)
export function getAsPlaceholderDescription(v: Record<string, string>): string {
return Object.entries(v)
.map(([placeholder, prop]) => `${placeholder} -> ${prop}`)
.join('\n');
}

export function getFixedScriptAddressPlaceholderDescription(): string {
return getAsPlaceholderDescription(fixedScriptPlaceholders);
}

function getAddressProperties(
keys: utxolib.bitgo.RootWalletKeys,
chain: utxolib.bitgo.ChainCode,
index: number,
network: utxolib.Network
): AddressProperties {
): FixedScriptAddressProperties {
const [userPath, backupPath, bitgoPath] = keys.triple.map((k) => keys.getDerivationPath(k, chain, index));
const scripts = utxolib.bitgo.getWalletOutputScripts(keys, chain, index);
const [userKey, backupKey, bitgoKey] = keys.triple.map((k) => k.derivePath(userPath).publicKey.toString('hex'));
Expand All @@ -80,23 +84,31 @@ function getAddressProperties(
};
}

export function formatAddressTree(props: AddressProperties): string {
export function formatAddressTree(props: FixedScriptAddressProperties): string {
const parser = new Parser();
return formatTree(parseUnknown(parser, 'address', props));
}

export function formatAddressWithFormatString(props: AddressProperties, format: string): string {
export function formatAddressWithFormatString(
props: Record<string, unknown>,
placeholders: Record<string, string>,
format: string
): string {
// replace all patterns with a % prefix from format string with the corresponding property
// e.g. %p0 -> userPath, %k1 -> backupKey, etc.
return format.replace(/%[a-z0-9]+/gi, (match) => {
if (match in placeholders) {
const prop = placeholders[match as keyof typeof placeholders];
const prop = placeholders[match];
return String(props[prop]);
}
return match;
});
}

export function formatFixedScriptAddress(props: FixedScriptAddressProperties, format: string): string {
return formatAddressWithFormatString(props, fixedScriptPlaceholders, format);
}

export function getRange(start: number, end: number): number[] {
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
Expand All @@ -111,14 +123,14 @@ export function parseIndexRange(ranges: string[]): number[] {
});
}

export function* generateAddress(
export function* generateFixedScriptAddress(
argv: KeyOptions & {
network?: utxolib.Network;
chain?: number[];
format: string;
index: number[];
}
): Generator<AddressProperties> {
): Generator<FixedScriptAddressProperties> {
const rootXpubs = getRootWalletKeys(argv);
const chains = argv.chain ?? getDefaultChainCodes();
for (const i of argv.index) {
Expand Down
6 changes: 3 additions & 3 deletions modules/utxo-bin/test/generateAddress.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import * as assert from 'assert';

import { formatAddressWithFormatString, generateAddress, parseIndexRange } from '../src/generateAddress';
import { formatFixedScriptAddress, generateFixedScriptAddress, parseIndexRange } from '../src/generateAddress';

import { getKeyTriple } from './bip32.util';

describe('generateAddresses', function () {
it('should generate addresses', function () {
const [userKey, backupKey, bitgoKey] = getKeyTriple('generateAddress').map((k) => k.neutered().toBase58());
const lines = [];
for (const l of generateAddress({
for (const l of generateFixedScriptAddress({
userKey,
backupKey,
bitgoKey,
index: parseIndexRange(['0-1']),
format: '%a',
chain: [0, 1],
})) {
lines.push(formatAddressWithFormatString(l, '%a'));
lines.push(formatFixedScriptAddress(l, '%a'));
}

assert.strictEqual(lines.length, 4);
Expand Down
5 changes: 2 additions & 3 deletions modules/utxo-bin/test/parseAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import * as assert from 'assert';
import * as yargs from 'yargs';
import * as utxolib from '@bitgo/utxo-lib';

import { cmdParseAddress, getAddressParser } from '../src/commands';

import { formatTreeNoColor, getFixtureString } from './fixtures';
import { getKeyTriple, KeyTriple } from './bip32.util';
import { getAddressParser, cmdParse } from '../src/commands/cmdAddress/cmdParse';

const scriptTypesSingleSig = ['p2pkh', 'p2wkh'] as const;
const scriptTypes = [...utxolib.bitgo.outputScripts.scriptTypes2Of3, ...scriptTypesSingleSig] as const;
Expand Down Expand Up @@ -67,7 +66,7 @@ function getAddresses(n: utxolib.Network): [type: string, format: string, addres
}

function parse(address: string, args: string[]) {
return getAddressParser(yargs.command(cmdParseAddress).parseSync(args)).parse(address);
return getAddressParser(yargs.command(cmdParse).parseSync(args)).parse(address);
}

function testParseAddress(
Expand Down

0 comments on commit 892339a

Please sign in to comment.