Skip to content

Commit 138578a

Browse files
OttoAllmendingerllm-git
andcommitted
feat(utxo-bin): add descriptor fromFixedScript command
Add capability to convert BitGo FixedScript RootWalletKeys to output descriptors. The new command allows users to generate descriptors from wallet keys with proper formatting options. Issue: BTC-2170 Co-authored-by: llm-git <[email protected]>
1 parent a67bcb2 commit 138578a

File tree

6 files changed

+110
-1
lines changed

6 files changed

+110
-1
lines changed

modules/utxo-bin/bin/index.ts

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

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

66
yargs
77
.command(cmdParseTx)
88
.command(cmdAddress)
9+
.command(cmdDescriptor)
910
.command(cmdParseScript)
1011
.command(cmdPsbt)
1112
.command(cmdBip32)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { CommandModule } from 'yargs';
2+
import { getNamedDescriptorsForRootWalletKeys } from '@bitgo/utxo-core/descriptor';
3+
4+
import { FormatTreeOrJson, formatTreeOrJson, getRootWalletKeys, keyOptions, KeyOptions } from '../../args';
5+
import { formatObjAsTree } from '../../format';
6+
7+
type ArgsFixedScriptToDescriptor = KeyOptions & {
8+
format: FormatTreeOrJson;
9+
};
10+
11+
export const cmdFromFixedScript: CommandModule<unknown, ArgsFixedScriptToDescriptor> = {
12+
command: 'fromFixedScript',
13+
describe: 'Convert BitGo FixedScript RootWalletKeys to output descriptors',
14+
builder(b) {
15+
return b.options(keyOptions).options({ format: formatTreeOrJson });
16+
},
17+
handler(argv): void {
18+
const rootWalletKeys = getRootWalletKeys(argv);
19+
const descriptorMap = getNamedDescriptorsForRootWalletKeys(rootWalletKeys);
20+
const obj = Object.fromEntries(
21+
[...descriptorMap].map(([name, descriptor]) => [name, descriptor?.toString() ?? null])
22+
);
23+
if (argv.format === 'tree') {
24+
console.log(formatObjAsTree('descriptors', obj));
25+
} else if (argv.format === 'json') {
26+
console.log(JSON.stringify(obj, null, 2));
27+
} else {
28+
throw new Error(`Invalid format: ${argv.format}. Expected 'tree' or 'json'.`);
29+
}
30+
},
31+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { CommandModule } from 'yargs';
2+
import { cmdFromFixedScript } from './fromFixedScript';
3+
4+
export * from './fromFixedScript';
5+
6+
export const cmdDescriptor: CommandModule<unknown, unknown> = {
7+
command: 'descriptor <command>',
8+
describe: 'descriptor commands',
9+
builder(b) {
10+
return b.strict().command(cmdFromFixedScript).demandCommand();
11+
},
12+
handler() {
13+
// do nothing
14+
},
15+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './cmdParseTx';
22
export * from './cmdAddress';
3+
export * from './cmdDescriptor';
34
export * from './cmdParseScript';
45
export * from './cmdBip32';
56
export * from './cmdPsbt';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as assert from 'assert';
2+
import yargs from 'yargs';
3+
4+
import { cmdFromFixedScript } from '../../src/commands/cmdDescriptor';
5+
import { getFixtureString } from '../fixtures';
6+
import { getKeyTriple } from '../bip32.util';
7+
import { captureConsole } from '../captureConsole';
8+
9+
describe('cmdDescriptor fromFixedScript', function () {
10+
it('should be a yargs command', function () {
11+
assert.strictEqual(typeof cmdFromFixedScript.command, 'string');
12+
assert.strictEqual(typeof cmdFromFixedScript.describe, 'string');
13+
assert.strictEqual(typeof cmdFromFixedScript.builder, 'function');
14+
assert.strictEqual(typeof cmdFromFixedScript.handler, 'function');
15+
});
16+
17+
it('should output expected descriptor for valid keys', async function () {
18+
const [userKey, backupKey, bitgoKey] = getKeyTriple('generateAddress').map((k) => k.neutered().toBase58());
19+
const argv = [
20+
'fromFixedScript',
21+
'--userKey',
22+
userKey,
23+
'--backupKey',
24+
backupKey,
25+
'--bitgoKey',
26+
bitgoKey,
27+
'--scriptType',
28+
'p2sh',
29+
'--network',
30+
'testnet',
31+
];
32+
33+
const y = yargs(argv)
34+
.command(cmdFromFixedScript)
35+
.exitProcess(false)
36+
.fail((msg, err) => {
37+
throw err || new Error(msg);
38+
});
39+
40+
const { stdout, stderr } = await captureConsole(async () => {
41+
await y.parse();
42+
});
43+
44+
// Compare output to fixture, or check for expected descriptor substring
45+
const expected = await getFixtureString('test/fixtures/fromFixedScript/descriptors.txt', stdout);
46+
assert.strictEqual(stdout.trim(), expected.trim());
47+
assert.strictEqual(stderr, '');
48+
});
49+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
descriptors
2+
├── p2sh/external: "sh(multi(2,xpub661MyMwAqRbcFDYKGMFUdU7DbgCM5xcyaHzofQnvQK2vEV9wfHCyrh3xyXoMRL1DBURzdqjnAZdX91qbJv6C1UsiJUPDF7EBf6wez7VrEJR/0/0/0/*,xpub661MyMwAqRbcFawpks1M1Ky7LfJ8WA8Ck4tcp6bGEUNKCFdCC1s7CxuYTjpAQV9eN7W59ctCYyTFPBPRiiBm4CnsYdnvdYN4nwu3FsCKnDw/0/0/0/*,xpub661MyMwAqRbcFHxWrAKX6D3Uc7PzpaTSHuXVzCxfY9qFaBcREfy23vx7RtT8CC9tcTKp5JNbcK1pQgjGnBWi3Br8wUryLwLS13CyGREyFs3/0/0/0/*))#5npd8z8f"
3+
├── p2sh/internal: "sh(multi(2,xpub661MyMwAqRbcFDYKGMFUdU7DbgCM5xcyaHzofQnvQK2vEV9wfHCyrh3xyXoMRL1DBURzdqjnAZdX91qbJv6C1UsiJUPDF7EBf6wez7VrEJR/0/0/1/*,xpub661MyMwAqRbcFawpks1M1Ky7LfJ8WA8Ck4tcp6bGEUNKCFdCC1s7CxuYTjpAQV9eN7W59ctCYyTFPBPRiiBm4CnsYdnvdYN4nwu3FsCKnDw/0/0/1/*,xpub661MyMwAqRbcFHxWrAKX6D3Uc7PzpaTSHuXVzCxfY9qFaBcREfy23vx7RtT8CC9tcTKp5JNbcK1pQgjGnBWi3Br8wUryLwLS13CyGREyFs3/0/0/1/*))#ung2jfhq"
4+
├── p2shP2wsh/external: "sh(wsh(multi(2,xpub661MyMwAqRbcFDYKGMFUdU7DbgCM5xcyaHzofQnvQK2vEV9wfHCyrh3xyXoMRL1DBURzdqjnAZdX91qbJv6C1UsiJUPDF7EBf6wez7VrEJR/0/0/10/*,xpub661MyMwAqRbcFawpks1M1Ky7LfJ8WA8Ck4tcp6bGEUNKCFdCC1s7CxuYTjpAQV9eN7W59ctCYyTFPBPRiiBm4CnsYdnvdYN4nwu3FsCKnDw/0/0/10/*,xpub661MyMwAqRbcFHxWrAKX6D3Uc7PzpaTSHuXVzCxfY9qFaBcREfy23vx7RtT8CC9tcTKp5JNbcK1pQgjGnBWi3Br8wUryLwLS13CyGREyFs3/0/0/10/*)))#nlkef9fp"
5+
├── p2shP2wsh/internal: "sh(wsh(multi(2,xpub661MyMwAqRbcFDYKGMFUdU7DbgCM5xcyaHzofQnvQK2vEV9wfHCyrh3xyXoMRL1DBURzdqjnAZdX91qbJv6C1UsiJUPDF7EBf6wez7VrEJR/0/0/11/*,xpub661MyMwAqRbcFawpks1M1Ky7LfJ8WA8Ck4tcp6bGEUNKCFdCC1s7CxuYTjpAQV9eN7W59ctCYyTFPBPRiiBm4CnsYdnvdYN4nwu3FsCKnDw/0/0/11/*,xpub661MyMwAqRbcFHxWrAKX6D3Uc7PzpaTSHuXVzCxfY9qFaBcREfy23vx7RtT8CC9tcTKp5JNbcK1pQgjGnBWi3Br8wUryLwLS13CyGREyFs3/0/0/11/*)))#3v6t34rd"
6+
├── p2wsh/external: "wsh(multi(2,xpub661MyMwAqRbcFDYKGMFUdU7DbgCM5xcyaHzofQnvQK2vEV9wfHCyrh3xyXoMRL1DBURzdqjnAZdX91qbJv6C1UsiJUPDF7EBf6wez7VrEJR/0/0/20/*,xpub661MyMwAqRbcFawpks1M1Ky7LfJ8WA8Ck4tcp6bGEUNKCFdCC1s7CxuYTjpAQV9eN7W59ctCYyTFPBPRiiBm4CnsYdnvdYN4nwu3FsCKnDw/0/0/20/*,xpub661MyMwAqRbcFHxWrAKX6D3Uc7PzpaTSHuXVzCxfY9qFaBcREfy23vx7RtT8CC9tcTKp5JNbcK1pQgjGnBWi3Br8wUryLwLS13CyGREyFs3/0/0/20/*))#2sputcy5"
7+
├── p2wsh/internal: "wsh(multi(2,xpub661MyMwAqRbcFDYKGMFUdU7DbgCM5xcyaHzofQnvQK2vEV9wfHCyrh3xyXoMRL1DBURzdqjnAZdX91qbJv6C1UsiJUPDF7EBf6wez7VrEJR/0/0/21/*,xpub661MyMwAqRbcFawpks1M1Ky7LfJ8WA8Ck4tcp6bGEUNKCFdCC1s7CxuYTjpAQV9eN7W59ctCYyTFPBPRiiBm4CnsYdnvdYN4nwu3FsCKnDw/0/0/21/*,xpub661MyMwAqRbcFHxWrAKX6D3Uc7PzpaTSHuXVzCxfY9qFaBcREfy23vx7RtT8CC9tcTKp5JNbcK1pQgjGnBWi3Br8wUryLwLS13CyGREyFs3/0/0/21/*))#9ush3alg"
8+
├── p2tr/external: null
9+
├── p2tr/internal: null
10+
├── p2trMusig2/external: null
11+
└── p2trMusig2/internal: null
12+

0 commit comments

Comments
 (0)