Skip to content

Commit 55be38f

Browse files
feat(utxo-bin): add command to generate addresses from descriptor
Issue: BTC-1533
1 parent 892339a commit 55be38f

File tree

5 files changed

+360
-89
lines changed

5 files changed

+360
-89
lines changed

modules/utxo-bin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@bitgo/blockapis": "^1.10.5",
3030
"@bitgo/statics": "^50.1.0",
3131
"@bitgo/utxo-lib": "^11.0.0",
32+
"@bitgo/wasm-miniscript": "^1.8.0",
3233
"archy": "^1.0.0",
3334
"bech32": "^2.0.0",
3435
"bitcoinjs-lib": "npm:@bitgo-forks/[email protected]",

modules/utxo-bin/src/commands/cmdAddress/cmdGenerate.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import { CommandModule } from 'yargs';
44
import { getNetworkOptionsDemand, keyOptions, KeyOptions } from '../../args';
55
import {
66
formatAddressTree,
7+
formatDescriptorAddress,
78
formatFixedScriptAddress,
9+
generateDescriptorAddress,
810
generateFixedScriptAddress,
11+
getDescriptorAddressPlaceholderDescription,
912
getFixedScriptAddressPlaceholderDescription,
1013
getRange,
1114
parseIndexRange,
@@ -74,3 +77,42 @@ export const cmdGenerateFixedScript: CommandModule<unknown, ArgsGenerateAddressF
7477
}
7578
},
7679
};
80+
81+
type ArgsGenerateDescriptorAddress = {
82+
network: utxolib.Network;
83+
descriptor: string;
84+
format: string;
85+
} & IndexLimitOptions;
86+
87+
export const cmdFromDescriptor: CommandModule<unknown, ArgsGenerateDescriptorAddress> = {
88+
command: 'fromDescriptor [descriptor]',
89+
describe: 'generate address from descriptor',
90+
builder(b) {
91+
return b
92+
.options(getNetworkOptionsDemand('bitcoin'))
93+
.positional('descriptor', {
94+
type: 'string',
95+
demandOption: true,
96+
})
97+
.options({
98+
format: {
99+
type: 'string',
100+
description: `Format string.\nPlaceholders:\n${getDescriptorAddressPlaceholderDescription()}`,
101+
default: '%i\t%a',
102+
},
103+
})
104+
.options(indexLimitOptions);
105+
},
106+
handler(argv) {
107+
for (const address of generateDescriptorAddress({
108+
...argv,
109+
index: getIndexRangeFromArgv(argv),
110+
})) {
111+
if (argv.format === 'tree') {
112+
console.log(formatAddressTree(address));
113+
} else {
114+
console.log(formatDescriptorAddress(address, argv.format));
115+
}
116+
}
117+
},
118+
};

modules/utxo-bin/src/generateAddress.ts

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

33
import * as utxolib from '@bitgo/utxo-lib';
4+
import { Descriptor } from '@bitgo/wasm-miniscript';
45

56
import { Parser } from './Parser';
67
import { parseUnknown } from './parseUnknown';
@@ -84,7 +85,7 @@ function getAddressProperties(
8485
};
8586
}
8687

87-
export function formatAddressTree(props: FixedScriptAddressProperties): string {
88+
export function formatAddressTree(props: FixedScriptAddressProperties | DescriptorAddressProperties): string {
8889
const parser = new Parser();
8990
return formatTree(parseUnknown(parser, 'address', props));
9091
}
@@ -141,3 +142,49 @@ export function* generateFixedScriptAddress(
141142
}
142143
}
143144
}
145+
146+
type DescriptorAddressProperties = {
147+
descriptor: string;
148+
index: number;
149+
explicitScript: string;
150+
scriptPubKey: string;
151+
address: string;
152+
};
153+
154+
const descriptorAddressPlaceholders = {
155+
'%d': 'descriptor',
156+
'%i': 'index',
157+
'%e': 'explicitScript',
158+
'%s': 'scriptPubKey',
159+
'%a': 'address',
160+
} as const;
161+
162+
export function getDescriptorAddressPlaceholderDescription(): string {
163+
return getAsPlaceholderDescription(descriptorAddressPlaceholders);
164+
}
165+
166+
export function formatDescriptorAddress(props: DescriptorAddressProperties, format: string): string {
167+
return formatAddressWithFormatString(props, descriptorAddressPlaceholders, format);
168+
}
169+
170+
export function* generateDescriptorAddress(argv: {
171+
network: utxolib.Network;
172+
descriptor: string;
173+
format: string;
174+
index: number[];
175+
}): Generator<DescriptorAddressProperties> {
176+
const descriptor = Descriptor.fromString(argv.descriptor, 'derivable');
177+
for (const i of argv.index) {
178+
const derived = descriptor.atDerivationIndex(i);
179+
const explicitScript = Buffer.from(derived.encode());
180+
const scriptPubKey = Buffer.from(derived.scriptPubkey());
181+
const address = utxolib.address.fromOutputScript(scriptPubKey, argv.network);
182+
yield {
183+
descriptor: derived.toString(),
184+
index: i,
185+
address,
186+
explicitScript: explicitScript.toString('hex'),
187+
scriptPubKey: scriptPubKey.toString('hex'),
188+
};
189+
}
190+
}
Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
import * as assert from 'assert';
22

3-
import { formatFixedScriptAddress, generateFixedScriptAddress, parseIndexRange } from '../src/generateAddress';
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
5+
import {
6+
formatDescriptorAddress,
7+
formatFixedScriptAddress,
8+
generateDescriptorAddress,
9+
generateFixedScriptAddress,
10+
parseIndexRange,
11+
} from '../src/generateAddress';
412

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

715
describe('generateAddresses', function () {
16+
const [userKey, backupKey, bitgoKey] = getKeyTriple('generateAddress').map((k) => k.neutered().toBase58());
17+
// addr${chain}${index}
18+
const [addr00, addr10, addr01, addr11] = [
19+
'38FHxcU7KY4E2nDEezEVcKWGvHy9717ehF',
20+
'35Qg1UqVWSJdtF1ysfz9h3KRGdk9uH8iYx',
21+
'3ARnshsLXE9QfJemQdoKL2kp6TRqGohLDz',
22+
'3QxKW93NN8CQrKaNqkDsAXPyxPsrxfTYME',
23+
];
824
it('should generate addresses', function () {
9-
const [userKey, backupKey, bitgoKey] = getKeyTriple('generateAddress').map((k) => k.neutered().toBase58());
1025
const lines = [];
1126
for (const l of generateFixedScriptAddress({
1227
userKey,
@@ -20,11 +35,21 @@ describe('generateAddresses', function () {
2035
}
2136

2237
assert.strictEqual(lines.length, 4);
23-
assert.deepStrictEqual(lines, [
24-
'38FHxcU7KY4E2nDEezEVcKWGvHy9717ehF',
25-
'35Qg1UqVWSJdtF1ysfz9h3KRGdk9uH8iYx',
26-
'3ARnshsLXE9QfJemQdoKL2kp6TRqGohLDz',
27-
'3QxKW93NN8CQrKaNqkDsAXPyxPsrxfTYME',
28-
]);
38+
assert.deepStrictEqual(lines, [addr00, addr10, addr01, addr11]);
39+
});
40+
41+
it('should generate descriptor addresses', function () {
42+
// only generate addresses for chain 0
43+
const xpubs = [userKey, backupKey, bitgoKey].map((x) => x + '/0/0/0/*');
44+
const lines = [];
45+
for (const l of generateDescriptorAddress({
46+
network: utxolib.networks.bitcoin,
47+
descriptor: `sh(multi(2,${xpubs.join(',')}))`,
48+
index: parseIndexRange(['0-1']),
49+
format: '%d',
50+
})) {
51+
lines.push(formatDescriptorAddress(l, '%a'));
52+
}
53+
assert.deepStrictEqual(lines, [addr00, addr01]);
2954
});
3055
});

0 commit comments

Comments
 (0)