Skip to content

Commit 892339a

Browse files
Merge pull request #4994 from BitGo/BTC-1351.utxo-bin-address-subcommand
feat(utxo-bin): add address subcommand
2 parents 1d46fb3 + 6266c62 commit 892339a

File tree

10 files changed

+128
-97
lines changed

10 files changed

+128
-97
lines changed

modules/utxo-bin/bin/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
#!/usr/bin/env node
22
import * as yargs from 'yargs';
33

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

66
yargs
77
.command(cmdParseTx)
8-
.command(cmdParseAddress)
8+
.command(cmdAddress)
99
.command(cmdParseScript)
10-
.command(cmdGenerateAddress)
1110
.command(cmdBip32)
1211
.strict()
1312
.demandCommand()
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { CommandModule } from 'yargs';
3+
4+
import { getNetworkOptionsDemand, keyOptions, KeyOptions } from '../../args';
5+
import {
6+
formatAddressTree,
7+
formatFixedScriptAddress,
8+
generateFixedScriptAddress,
9+
getFixedScriptAddressPlaceholderDescription,
10+
getRange,
11+
parseIndexRange,
12+
} from '../../generateAddress';
13+
14+
type IndexLimitOptions = {
15+
index?: string[];
16+
limit?: number;
17+
};
18+
19+
const indexLimitOptions = {
20+
index: {
21+
type: 'string',
22+
array: true,
23+
description: 'Address index. Can be given as a range (e.g. 0-99). Takes precedence over --limit.',
24+
},
25+
limit: {
26+
type: 'number',
27+
description: 'Alias for --index with range starting at 0 to limit-1.',
28+
default: 100,
29+
},
30+
} as const;
31+
32+
function getIndexRangeFromArgv(argv: IndexLimitOptions): number[] {
33+
if (argv.index) {
34+
return parseIndexRange(argv.index);
35+
}
36+
if (argv.limit) {
37+
return getRange(0, argv.limit - 1);
38+
}
39+
throw new Error(`no index or limit`);
40+
}
41+
42+
type ArgsGenerateAddressFixedScript = KeyOptions &
43+
IndexLimitOptions & {
44+
network: utxolib.Network;
45+
chain?: number[];
46+
format: string;
47+
};
48+
49+
export const cmdGenerateFixedScript: CommandModule<unknown, ArgsGenerateAddressFixedScript> = {
50+
command: 'fromFixedScript',
51+
describe: 'generate bitgo fixed-script addresses',
52+
builder(b) {
53+
return b
54+
.options(getNetworkOptionsDemand('bitcoin'))
55+
.options(keyOptions)
56+
.option('format', {
57+
type: 'string',
58+
default: '%p0\t%a',
59+
description: `Format string.\nPlaceholders:\n${getFixedScriptAddressPlaceholderDescription()}`,
60+
})
61+
.option('chain', { type: 'number', array: true, description: 'Address chain' })
62+
.options(indexLimitOptions);
63+
},
64+
handler(argv): void {
65+
for (const address of generateFixedScriptAddress({
66+
...argv,
67+
index: getIndexRangeFromArgv(argv),
68+
})) {
69+
if (argv.format === 'tree') {
70+
console.log(formatAddressTree(address));
71+
} else {
72+
console.log(formatFixedScriptAddress(address, argv.format));
73+
}
74+
}
75+
},
76+
};

modules/utxo-bin/src/commands/cmdParseAddress.ts renamed to modules/utxo-bin/src/commands/cmdAddress/cmdParse.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import * as utxolib from '@bitgo/utxo-lib';
22
import * as yargs from 'yargs';
33

4-
import { AddressParser } from '../AddressParser';
5-
import { formatTreeOrJson, getNetworkOptions, FormatTreeOrJson } from '../args';
4+
import { AddressParser } from '../../AddressParser';
5+
import { formatTreeOrJson, FormatTreeOrJson, getNetworkOptions } from '../../args';
66

7-
import { formatString } from './formatString';
7+
import { formatString } from '../formatString';
88

99
export type ArgsParseAddress = {
1010
network?: utxolib.Network;
11-
all: boolean;
1211
format: FormatTreeOrJson;
12+
all: boolean;
1313
convert: boolean;
1414
address: string;
1515
};
@@ -18,8 +18,8 @@ export function getAddressParser(argv: ArgsParseAddress): AddressParser {
1818
return new AddressParser(argv);
1919
}
2020

21-
export const cmdParseAddress = {
22-
command: 'parseAddress [address]',
21+
export const cmdParse = {
22+
command: 'parse [address]',
2323
aliases: ['address'],
2424
describe: 'parse address',
2525
builder(b: yargs.Argv<unknown>): yargs.Argv<ArgsParseAddress> {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { CommandModule } from 'yargs';
2+
3+
import { cmdGenerateFixedScript } from './cmdGenerate';
4+
import { cmdParse } from './cmdParse';
5+
6+
export const cmdAddress: CommandModule<unknown, unknown> = {
7+
command: 'address <command>',
8+
describe: 'address commands',
9+
builder(b) {
10+
return b.strict().command(cmdGenerateFixedScript).command(cmdParse).demandCommand();
11+
},
12+
handler() {
13+
// do nothing
14+
},
15+
};

modules/utxo-bin/src/commands/cmdGenerateAddress.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

modules/utxo-bin/src/commands/formatString.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as yargs from 'yargs';
2-
31
import { ParserNode } from '../Parser';
42
import { formatTree } from '../format';
53
import { FormatTreeOrJson } from '../args';
@@ -9,7 +7,7 @@ export type FormatStringArgs = {
97
all: boolean;
108
};
119

12-
export function formatString(parsed: ParserNode, argv: yargs.Arguments<FormatStringArgs>): string {
10+
export function formatString(parsed: ParserNode, argv: FormatStringArgs): string {
1311
switch (argv.format) {
1412
case 'json':
1513
return JSON.stringify(parsed, null, 2);
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export * from './cmdParseTx';
2-
export * from './cmdParseAddress';
2+
export * from './cmdAddress';
33
export * from './cmdParseScript';
4-
export * from './cmdGenerateAddress';
54
export * from './cmdBip32';

modules/utxo-bin/src/generateAddress.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function getDefaultChainCodes(): number[] {
1414
);
1515
}
1616

17-
type AddressProperties = {
17+
type FixedScriptAddressProperties = {
1818
chain: utxolib.bitgo.ChainCode;
1919
index: number;
2020
type: utxolib.bitgo.outputScripts.ScriptType;
@@ -30,7 +30,7 @@ type AddressProperties = {
3030
address: string;
3131
};
3232

33-
const placeholders = {
33+
const fixedScriptPlaceholders = {
3434
'%c': 'chain',
3535
'%i': 'index',
3636
'%p': 'userPath',
@@ -47,18 +47,22 @@ const placeholders = {
4747
'%a': 'address',
4848
} as const;
4949

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

56+
export function getFixedScriptAddressPlaceholderDescription(): string {
57+
return getAsPlaceholderDescription(fixedScriptPlaceholders);
58+
}
59+
5660
function getAddressProperties(
5761
keys: utxolib.bitgo.RootWalletKeys,
5862
chain: utxolib.bitgo.ChainCode,
5963
index: number,
6064
network: utxolib.Network
61-
): AddressProperties {
65+
): FixedScriptAddressProperties {
6266
const [userPath, backupPath, bitgoPath] = keys.triple.map((k) => keys.getDerivationPath(k, chain, index));
6367
const scripts = utxolib.bitgo.getWalletOutputScripts(keys, chain, index);
6468
const [userKey, backupKey, bitgoKey] = keys.triple.map((k) => k.derivePath(userPath).publicKey.toString('hex'));
@@ -80,23 +84,31 @@ function getAddressProperties(
8084
};
8185
}
8286

83-
export function formatAddressTree(props: AddressProperties): string {
87+
export function formatAddressTree(props: FixedScriptAddressProperties): string {
8488
const parser = new Parser();
8589
return formatTree(parseUnknown(parser, 'address', props));
8690
}
8791

88-
export function formatAddressWithFormatString(props: AddressProperties, format: string): string {
92+
export function formatAddressWithFormatString(
93+
props: Record<string, unknown>,
94+
placeholders: Record<string, string>,
95+
format: string
96+
): string {
8997
// replace all patterns with a % prefix from format string with the corresponding property
9098
// e.g. %p0 -> userPath, %k1 -> backupKey, etc.
9199
return format.replace(/%[a-z0-9]+/gi, (match) => {
92100
if (match in placeholders) {
93-
const prop = placeholders[match as keyof typeof placeholders];
101+
const prop = placeholders[match];
94102
return String(props[prop]);
95103
}
96104
return match;
97105
});
98106
}
99107

108+
export function formatFixedScriptAddress(props: FixedScriptAddressProperties, format: string): string {
109+
return formatAddressWithFormatString(props, fixedScriptPlaceholders, format);
110+
}
111+
100112
export function getRange(start: number, end: number): number[] {
101113
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
102114
}
@@ -111,14 +123,14 @@ export function parseIndexRange(ranges: string[]): number[] {
111123
});
112124
}
113125

114-
export function* generateAddress(
126+
export function* generateFixedScriptAddress(
115127
argv: KeyOptions & {
116128
network?: utxolib.Network;
117129
chain?: number[];
118130
format: string;
119131
index: number[];
120132
}
121-
): Generator<AddressProperties> {
133+
): Generator<FixedScriptAddressProperties> {
122134
const rootXpubs = getRootWalletKeys(argv);
123135
const chains = argv.chain ?? getDefaultChainCodes();
124136
for (const i of argv.index) {

modules/utxo-bin/test/generateAddress.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import * as assert from 'assert';
22

3-
import { formatAddressWithFormatString, generateAddress, parseIndexRange } from '../src/generateAddress';
3+
import { formatFixedScriptAddress, generateFixedScriptAddress, parseIndexRange } from '../src/generateAddress';
44

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

77
describe('generateAddresses', function () {
88
it('should generate addresses', function () {
99
const [userKey, backupKey, bitgoKey] = getKeyTriple('generateAddress').map((k) => k.neutered().toBase58());
1010
const lines = [];
11-
for (const l of generateAddress({
11+
for (const l of generateFixedScriptAddress({
1212
userKey,
1313
backupKey,
1414
bitgoKey,
1515
index: parseIndexRange(['0-1']),
1616
format: '%a',
1717
chain: [0, 1],
1818
})) {
19-
lines.push(formatAddressWithFormatString(l, '%a'));
19+
lines.push(formatFixedScriptAddress(l, '%a'));
2020
}
2121

2222
assert.strictEqual(lines.length, 4);

modules/utxo-bin/test/parseAddress.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import * as assert from 'assert';
33
import * as yargs from 'yargs';
44
import * as utxolib from '@bitgo/utxo-lib';
55

6-
import { cmdParseAddress, getAddressParser } from '../src/commands';
7-
86
import { formatTreeNoColor, getFixtureString } from './fixtures';
97
import { getKeyTriple, KeyTriple } from './bip32.util';
8+
import { getAddressParser, cmdParse } from '../src/commands/cmdAddress/cmdParse';
109

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

6968
function parse(address: string, args: string[]) {
70-
return getAddressParser(yargs.command(cmdParseAddress).parseSync(args)).parse(address);
69+
return getAddressParser(yargs.command(cmdParse).parseSync(args)).parse(address);
7170
}
7271

7372
function testParseAddress(

0 commit comments

Comments
 (0)