Skip to content

Commit 24eaced

Browse files
feat(abstract-utxo): implement sign for descriptor wallets
Issue: BTC-1450
1 parent 048c240 commit 24eaced

File tree

5 files changed

+102
-5
lines changed

5 files changed

+102
-5
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
835835
async signTransaction<TNumber extends number | bigint = number>(
836836
params: SignTransactionOptions<TNumber>
837837
): Promise<SignedTransaction | HalfSignedUtxoTransaction> {
838-
return signTransaction<TNumber>(this, params);
838+
return signTransaction<TNumber>(this, this.bitgo, params);
839839
}
840840

841841
/**

modules/abstract-utxo/src/transaction/descriptor/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { explainPsbt } from './explainPsbt';
33
export { parse } from './parse';
44
export { parseToAmountType } from './parseToAmountType';
55
export { verifyTransaction } from './verifyTransaction';
6+
export { signPsbt } from './signPsbt';
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { DescriptorMap } from '../../core/descriptor';
3+
import { findDescriptorForInput } from '../../core/descriptor/psbt/findDescriptors';
4+
5+
export class ErrorUnknownInput extends Error {
6+
constructor(public vin: number) {
7+
super(`missing descriptor for input ${vin}`);
8+
}
9+
}
10+
11+
/**
12+
* Sign a PSBT with the given keychain.
13+
*
14+
* Checks the descriptor map for each input in the PSBT. If the input is not
15+
* found in the descriptor map, the behavior is determined by the `onUnknownInput`
16+
* parameter.
17+
*
18+
*
19+
* @param tx - psbt to sign
20+
* @param descriptorMap - map of input index to descriptor
21+
* @param signerKeychain - key to sign with
22+
* @param params - onUnknownInput: 'throw' | 'skip' | 'sign'.
23+
* Determines what to do when an input is not found in the
24+
* descriptor map.
25+
*/
26+
export function signPsbt(
27+
tx: utxolib.Psbt,
28+
descriptorMap: DescriptorMap,
29+
signerKeychain: utxolib.BIP32Interface,
30+
params: {
31+
onUnknownInput: 'throw' | 'skip' | 'sign';
32+
}
33+
): void {
34+
for (const [vin, input] of tx.data.inputs.entries()) {
35+
if (!findDescriptorForInput(input, descriptorMap)) {
36+
switch (params.onUnknownInput) {
37+
case 'skip':
38+
continue;
39+
case 'throw':
40+
throw new ErrorUnknownInput(vin);
41+
case 'sign':
42+
break;
43+
}
44+
}
45+
tx.signInputHD(vin, signerKeychain);
46+
}
47+
}

modules/abstract-utxo/src/transaction/signTransaction.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import _ from 'lodash';
2-
import { IWallet } from '@bitgo/sdk-core';
2+
import { BitGoBase } from '@bitgo/sdk-core';
33
import * as utxolib from '@bitgo/utxo-lib';
44
import { bip32 } from '@bitgo/utxo-lib';
55
import buildDebug from 'debug';
66

77
import { AbstractUtxoCoin, SignTransactionOptions } from '../abstractUtxoCoin';
8-
import { isDescriptorWallet } from '../descriptor';
8+
import { getDescriptorMapFromWallet, getPolicyForEnv, isDescriptorWallet } from '../descriptor';
99
import * as fixedScript from './fixedScript';
10+
import * as descriptor from './descriptor';
11+
import { fetchKeychains, toBip32Triple } from '../keychains';
1012

1113
const debug = buildDebug('bitgo:abstract-utxo:transaction:signTransaction');
1214

@@ -27,6 +29,7 @@ function getSignerKeychain(userPrv: unknown): utxolib.BIP32Interface | undefined
2729

2830
export async function signTransaction<TNumber extends number | bigint>(
2931
coin: AbstractUtxoCoin,
32+
bitgo: BitGoBase,
3033
params: SignTransactionOptions<TNumber>
3134
): Promise<{ txHex: string }> {
3235
const txPrebuild = params.txPrebuild;
@@ -40,8 +43,24 @@ export async function signTransaction<TNumber extends number | bigint>(
4043

4144
const tx = coin.decodeTransactionFromPrebuild(params.txPrebuild);
4245

43-
if (params.wallet && isDescriptorWallet(params.wallet as IWallet)) {
44-
throw new Error('Descriptor wallets are not supported');
46+
const signerKeychain = getSignerKeychain(params.prv);
47+
48+
const { wallet } = params;
49+
50+
if (wallet && isDescriptorWallet(wallet)) {
51+
if (!signerKeychain) {
52+
throw new Error('missing signer');
53+
}
54+
const walletKeys = toBip32Triple(await fetchKeychains(coin, wallet));
55+
const descriptorMap = getDescriptorMapFromWallet(wallet, walletKeys, getPolicyForEnv(bitgo.env));
56+
if (tx instanceof utxolib.bitgo.UtxoPsbt) {
57+
descriptor.signPsbt(tx, descriptorMap, signerKeychain, {
58+
onUnknownInput: 'throw',
59+
});
60+
return { txHex: tx.toHex() };
61+
} else {
62+
throw new Error('expected a UtxoPsbt object');
63+
}
4564
} else {
4665
return fixedScript.signTransaction(coin, tx, getSignerKeychain(params.prv), {
4766
walletId: params.txPrebuild.walletId,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { mockPsbtDefaultWithDescriptorTemplate } from '../../core/descriptor/psbt/mock.utils';
2+
import { signPsbt } from '../../../src/transaction/descriptor';
3+
import { getKeyTriple } from '../../core/key.utils';
4+
import { getDescriptorMap } from '../../core/descriptor/descriptor.utils';
5+
import assert from 'assert';
6+
import { ErrorUnknownInput } from '../../../src/transaction/descriptor/signPsbt';
7+
8+
describe('sign', function () {
9+
const psbtUnsigned = mockPsbtDefaultWithDescriptorTemplate('Wsh2Of3');
10+
const keychain = getKeyTriple('a');
11+
const descriptorMap = getDescriptorMap('Wsh2Of3', keychain);
12+
const emptyDescriptorMap = new Map();
13+
14+
it('should sign a transaction', async function () {
15+
const psbt = psbtUnsigned.clone();
16+
signPsbt(psbt, descriptorMap, keychain[0], { onUnknownInput: 'throw' });
17+
assert(psbt.validateSignaturesOfAllInputs());
18+
});
19+
20+
it('should be sensitive to onUnknownInput', async function () {
21+
const psbt = psbtUnsigned.clone();
22+
assert.throws(() => {
23+
signPsbt(psbt, emptyDescriptorMap, keychain[0], { onUnknownInput: 'throw' });
24+
}, new ErrorUnknownInput(0));
25+
signPsbt(psbt, emptyDescriptorMap, keychain[0], { onUnknownInput: 'skip' });
26+
assert(psbt.data.inputs[0].partialSig === undefined);
27+
signPsbt(psbt, emptyDescriptorMap, keychain[0], { onUnknownInput: 'sign' });
28+
assert(psbt.validateSignaturesOfAllInputs());
29+
});
30+
});

0 commit comments

Comments
 (0)