Skip to content

Commit

Permalink
feat(utxo-bin): add command to generate addresses from descriptor
Browse files Browse the repository at this point in the history
Issue: BTC-1351
  • Loading branch information
OttoAllmendinger committed Oct 7, 2024
1 parent 892339a commit 587b4e6
Show file tree
Hide file tree
Showing 5 changed files with 360 additions and 89 deletions.
1 change: 1 addition & 0 deletions modules/utxo-bin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@bitgo/blockapis": "^1.10.5",
"@bitgo/statics": "^50.1.0",
"@bitgo/utxo-lib": "^11.0.0",
"@bitgo/wasm-miniscript": "^1.8.0",
"archy": "^1.0.0",
"bech32": "^2.0.0",
"bitcoinjs-lib": "npm:@bitgo-forks/[email protected]",
Expand Down
42 changes: 42 additions & 0 deletions modules/utxo-bin/src/commands/cmdAddress/cmdGenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { CommandModule } from 'yargs';
import { getNetworkOptionsDemand, keyOptions, KeyOptions } from '../../args';
import {
formatAddressTree,
formatDescriptorAddress,
formatFixedScriptAddress,
generateDescriptorAddress,
generateFixedScriptAddress,
getDescriptorAddressPlaceholderDescription,
getFixedScriptAddressPlaceholderDescription,
getRange,
parseIndexRange,
Expand Down Expand Up @@ -74,3 +77,42 @@ export const cmdGenerateFixedScript: CommandModule<unknown, ArgsGenerateAddressF
}
},
};

type ArgsGenerateDescriptorAddress = {
network: utxolib.Network;
descriptor: string;
format: string;
} & IndexLimitOptions;

export const cmdFromDescriptor: CommandModule<unknown, ArgsGenerateDescriptorAddress> = {
command: 'fromDescriptor [descriptor]',
describe: 'generate address from descriptor',
builder(b) {
return b
.options(getNetworkOptionsDemand('bitcoin'))
.positional('descriptor', {
type: 'string',
demandOption: true,
})
.options({
format: {
type: 'string',
description: `Format string.\nPlaceholders:\n${getDescriptorAddressPlaceholderDescription()}`,
default: '%i\t%a',
},
})
.options(indexLimitOptions);
},
handler(argv) {
for (const address of generateDescriptorAddress({
...argv,
index: getIndexRangeFromArgv(argv),
})) {
if (argv.format === 'tree') {
console.log(formatAddressTree(address));
} else {
console.log(formatDescriptorAddress(address, argv.format));
}
}
},
};
49 changes: 48 additions & 1 deletion modules/utxo-bin/src/generateAddress.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as assert from 'assert';

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

import { Parser } from './Parser';
import { parseUnknown } from './parseUnknown';
Expand Down Expand Up @@ -84,7 +85,7 @@ function getAddressProperties(
};
}

export function formatAddressTree(props: FixedScriptAddressProperties): string {
export function formatAddressTree(props: FixedScriptAddressProperties | DescriptorAddressProperties): string {
const parser = new Parser();
return formatTree(parseUnknown(parser, 'address', props));
}
Expand Down Expand Up @@ -141,3 +142,49 @@ export function* generateFixedScriptAddress(
}
}
}

type DescriptorAddressProperties = {
descriptor: string;
index: number;
explicitScript: string;
scriptPubKey: string;
address: string;
};

const descriptorAddressPlaceholders = {
'%d': 'descriptor',
'%i': 'index',
'%e': 'explicitScript',
'%s': 'scriptPubKey',
'%a': 'address',
} as const;

export function getDescriptorAddressPlaceholderDescription(): string {
return getAsPlaceholderDescription(descriptorAddressPlaceholders);
}

export function formatDescriptorAddress(props: DescriptorAddressProperties, format: string): string {
return formatAddressWithFormatString(props, descriptorAddressPlaceholders, format);
}

export function* generateDescriptorAddress(argv: {
network: utxolib.Network;
descriptor: string;
format: string;
index: number[];
}): Generator<DescriptorAddressProperties> {
const descriptor = Descriptor.fromString(argv.descriptor, 'derivable');
for (const i of argv.index) {
const derived = descriptor.atDerivationIndex(i);
const explicitScript = Buffer.from(derived.encode());
const scriptPubKey = Buffer.from(derived.scriptPubkey());
const address = utxolib.address.fromOutputScript(scriptPubKey, argv.network);
yield {
descriptor: derived.toString(),
index: i,
address,
explicitScript: explicitScript.toString('hex'),
scriptPubKey: scriptPubKey.toString('hex'),
};
}
}
41 changes: 33 additions & 8 deletions modules/utxo-bin/test/generateAddress.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import * as assert from 'assert';

import { formatFixedScriptAddress, generateFixedScriptAddress, parseIndexRange } from '../src/generateAddress';
import * as utxolib from '@bitgo/utxo-lib';

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

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

describe('generateAddresses', function () {
const [userKey, backupKey, bitgoKey] = getKeyTriple('generateAddress').map((k) => k.neutered().toBase58());
// addr${chain}${index}
const [addr00, addr10, addr01, addr11] = [
'38FHxcU7KY4E2nDEezEVcKWGvHy9717ehF',
'35Qg1UqVWSJdtF1ysfz9h3KRGdk9uH8iYx',
'3ARnshsLXE9QfJemQdoKL2kp6TRqGohLDz',
'3QxKW93NN8CQrKaNqkDsAXPyxPsrxfTYME',
];
it('should generate addresses', function () {
const [userKey, backupKey, bitgoKey] = getKeyTriple('generateAddress').map((k) => k.neutered().toBase58());
const lines = [];
for (const l of generateFixedScriptAddress({
userKey,
Expand All @@ -20,11 +35,21 @@ describe('generateAddresses', function () {
}

assert.strictEqual(lines.length, 4);
assert.deepStrictEqual(lines, [
'38FHxcU7KY4E2nDEezEVcKWGvHy9717ehF',
'35Qg1UqVWSJdtF1ysfz9h3KRGdk9uH8iYx',
'3ARnshsLXE9QfJemQdoKL2kp6TRqGohLDz',
'3QxKW93NN8CQrKaNqkDsAXPyxPsrxfTYME',
]);
assert.deepStrictEqual(lines, [addr00, addr10, addr01, addr11]);
});

it('should generate descriptor addresses', function () {
// only generate addresses for chain 0
const xpubs = [userKey, backupKey, bitgoKey].map((x) => x + '/0/0/0/*');
const lines = [];
for (const l of generateDescriptorAddress({
network: utxolib.networks.bitcoin,
descriptor: `sh(multi(2,${xpubs.join(',')}))`,
index: parseIndexRange(['0-1']),
format: '%d',
})) {
lines.push(formatDescriptorAddress(l, '%a'));
}
assert.deepStrictEqual(lines, [addr00, addr01]);
});
});
Loading

0 comments on commit 587b4e6

Please sign in to comment.