Skip to content

Commit 0fba61c

Browse files
Merge pull request #7703 from BitGo/BTC-2806.signtx-route
feat(abstract-utxo): enable wasm-utxo via decodeWith parameter
2 parents 5641490 + 0f5c86c commit 0fba61c

File tree

9 files changed

+177
-82
lines changed

9 files changed

+177
-82
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
isValidPrv,
4343
isValidXprv,
4444
} from '@bitgo/sdk-core';
45+
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
4546

4647
import {
4748
backupKeyRecovery,
@@ -68,6 +69,7 @@ import {
6869
verifyTransaction,
6970
} from './transaction';
7071
import type { TransactionExplanation } from './transaction/fixedScript/explainTransaction';
72+
import { Musig2Participant } from './transaction/fixedScript/musig2';
7173
import {
7274
AggregateValidationError,
7375
ErrorMissingOutputs,
@@ -76,8 +78,8 @@ import {
7678
import { assertDescriptorWalletAddress, getDescriptorMapFromWallet, isDescriptorWallet } from './descriptor';
7779
import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names';
7880
import { assertFixedScriptWalletAddress } from './address/fixedScript';
79-
import { ParsedTransaction } from './transaction/types';
80-
import { decodePsbtWith, stringToBufferTryFormats } from './transaction/decode';
81+
import { isSdkBackend, ParsedTransaction, SdkBackend } from './transaction/types';
82+
import { decodePsbtWith, encodeTransaction, stringToBufferTryFormats } from './transaction/decode';
8183
import { toBip32Triple, UtxoKeychain } from './keychains';
8284
import { verifyKeySignature, verifyUserPublicKey } from './verifyKey';
8385
import { getPolicyForEnv } from './descriptor/validatePolicy';
@@ -213,6 +215,7 @@ export interface ExplainTransactionOptions<TNumber extends number | bigint = num
213215
txInfo?: TransactionInfo<TNumber>;
214216
feeInfo?: string;
215217
pubs?: Triple<string>;
218+
decodeWith?: SdkBackend;
216219
}
217220

218221
export interface DecoratedExplainTransactionOptions<TNumber extends number | bigint = number>
@@ -225,6 +228,7 @@ export type UtxoNetwork = utxolib.Network;
225228
export interface TransactionPrebuild<TNumber extends number | bigint = number> extends BaseTransactionPrebuild {
226229
txInfo?: TransactionInfo<TNumber>;
227230
blockHeight?: number;
231+
decodeWith?: SdkBackend;
228232
}
229233

230234
export interface TransactionParams extends BaseTransactionParams {
@@ -284,6 +288,7 @@ type UtxoBaseSignTransactionOptions<TNumber extends number | bigint = number> =
284288
walletId?: string;
285289
txHex: string;
286290
txInfo?: TransactionInfo<TNumber>;
291+
decodeWith?: SdkBackend;
287292
};
288293
/** xpubs triple for wallet (user, backup, bitgo). Required only when txPrebuild.txHex is not a PSBT */
289294
pubs?: Triple<string>;
@@ -360,7 +365,10 @@ export interface SignPsbtResponse {
360365
psbt: string;
361366
}
362367

363-
export abstract class AbstractUtxoCoin extends BaseCoin {
368+
export abstract class AbstractUtxoCoin
369+
extends BaseCoin
370+
implements Musig2Participant<utxolib.bitgo.UtxoPsbt>, Musig2Participant<fixedScriptWallet.BitGoPsbt>
371+
{
364372
public altScriptHash?: number;
365373
public supportAltScriptDestination?: boolean;
366374
public readonly amountType: 'number' | 'bigint';
@@ -509,7 +517,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
509517
if (_.isUndefined(prebuild.blockHeight)) {
510518
prebuild.blockHeight = (await this.getLatestBlockHeight()) as number;
511519
}
512-
return _.extend({}, prebuild, { txHex: tx.toHex() });
520+
return _.extend({}, prebuild, { txHex: encodeTransaction(tx).toString('hex') });
513521
}
514522

515523
/**
@@ -526,38 +534,52 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
526534
return utxolib.bitgo.createTransactionFromHex<TNumber>(hex, this.network, this.amountType);
527535
}
528536

529-
decodeTransaction<TNumber extends number | bigint>(input: Buffer | string): DecodedTransaction<TNumber> {
537+
decodeTransaction<TNumber extends number | bigint>(
538+
input: Buffer | string,
539+
decodeWith?: SdkBackend
540+
): DecodedTransaction<TNumber> {
530541
if (typeof input === 'string') {
531542
const buffer = stringToBufferTryFormats(input, ['hex', 'base64']);
532-
return this.decodeTransaction(buffer);
543+
return this.decodeTransaction(buffer, decodeWith);
533544
}
534545

535546
if (utxolib.bitgo.isPsbt(input)) {
536-
return decodePsbtWith(input, this.network, 'utxolib');
547+
return decodePsbtWith(input, this.network, decodeWith ?? 'utxolib');
537548
} else {
549+
if (decodeWith ?? 'utxolib' !== 'utxolib') {
550+
console.error('received decodeWith hint %s, ignoring for legacy transaction', decodeWith);
551+
}
538552
return utxolib.bitgo.createTransactionFromBuffer(input, this.network, {
539553
amountType: this.amountType,
540554
});
541555
}
542556
}
543557

544-
decodeTransactionAsPsbt(input: Buffer | string): utxolib.bitgo.UtxoPsbt {
558+
decodeTransactionAsPsbt(input: Buffer | string): utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt {
545559
const decoded = this.decodeTransaction(input);
546-
if (!(decoded instanceof utxolib.bitgo.UtxoPsbt)) {
547-
throw new Error('expected psbt but got transaction');
560+
if (decoded instanceof fixedScriptWallet.BitGoPsbt || decoded instanceof utxolib.bitgo.UtxoPsbt) {
561+
return decoded;
548562
}
549-
return decoded;
563+
throw new Error('expected psbt but got transaction');
550564
}
551565

552566
decodeTransactionFromPrebuild<TNumber extends number | bigint>(prebuild: {
553567
txHex?: string;
554568
txBase64?: string;
569+
decodeWith?: string;
555570
}): DecodedTransaction<TNumber> {
556571
const string = prebuild.txHex ?? prebuild.txBase64;
557572
if (!string) {
558573
throw new Error('missing required txHex or txBase64 property');
559574
}
560-
return this.decodeTransaction(string);
575+
let { decodeWith } = prebuild;
576+
if (decodeWith !== undefined) {
577+
if (typeof decodeWith !== 'string' || !isSdkBackend(decodeWith)) {
578+
console.error('decodeWith %s is not a valid value, using default', decodeWith);
579+
decodeWith = undefined;
580+
}
581+
}
582+
return this.decodeTransaction(string, decodeWith);
561583
}
562584

563585
toCanonicalTransactionRecipient(output: { valueString: string; address?: string }): {
@@ -720,16 +742,29 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
720742

721743
/**
722744
* @returns input psbt added with deterministic MuSig2 nonce for bitgo key for each MuSig2 inputs.
723-
* @param psbtHex all MuSig2 inputs should contain user MuSig2 nonce
745+
* @param psbt all MuSig2 inputs should contain user MuSig2 nonce
724746
* @param walletId
725747
*/
726-
async getMusig2Nonces(psbt: utxolib.bitgo.UtxoPsbt, walletId: string): Promise<utxolib.bitgo.UtxoPsbt> {
727-
const params: SignPsbtRequest = { psbt: psbt.toHex() };
748+
async getMusig2Nonces(psbt: utxolib.bitgo.UtxoPsbt, walletId: string): Promise<utxolib.bitgo.UtxoPsbt>;
749+
async getMusig2Nonces(psbt: fixedScriptWallet.BitGoPsbt, walletId: string): Promise<fixedScriptWallet.BitGoPsbt>;
750+
async getMusig2Nonces<T extends utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt>(
751+
psbt: T,
752+
walletId: string
753+
): Promise<T>;
754+
async getMusig2Nonces<T extends utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt>(
755+
psbt: T,
756+
walletId: string
757+
): Promise<T> {
758+
const buffer = encodeTransaction(psbt);
728759
const response = await this.bitgo
729760
.post(this.url('/wallet/' + walletId + '/tx/signpsbt'))
730-
.send(params)
761+
.send({ psbt: buffer.toString('hex') })
731762
.result();
732-
return this.decodeTransactionAsPsbt(response.psbt);
763+
if (psbt instanceof utxolib.bitgo.UtxoPsbt) {
764+
return decodePsbtWith(response.psbt, this.network, 'utxolib') as T;
765+
} else {
766+
return decodePsbtWith(response.psbt, this.network, 'wasm-utxo') as T;
767+
}
733768
}
734769

735770
/**
@@ -739,7 +774,8 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
739774
* @param walletId
740775
*/
741776
async signPsbt(psbtHex: string, walletId: string): Promise<SignPsbtResponse> {
742-
return { psbt: (await this.getMusig2Nonces(this.decodeTransactionAsPsbt(psbtHex), walletId)).toHex() };
777+
const psbt = await this.getMusig2Nonces(this.decodeTransactionAsPsbt(psbtHex), walletId);
778+
return { psbt: encodeTransaction(psbt).toString('hex') };
743779
}
744780

745781
/**
@@ -749,11 +785,10 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
749785
async signPsbtFromOVC(ovcJson: Record<string, unknown>): Promise<Record<string, unknown>> {
750786
assert(ovcJson['psbtHex'], 'ovcJson must contain psbtHex');
751787
assert(ovcJson['walletId'], 'ovcJson must contain walletId');
752-
const psbt = await this.getMusig2Nonces(
753-
this.decodeTransactionAsPsbt(ovcJson['psbtHex'] as string),
754-
ovcJson['walletId'] as string
755-
);
756-
return _.extend(ovcJson, { txHex: psbt.toHex() });
788+
const hex = ovcJson['psbtHex'] as string;
789+
const walletId = ovcJson['walletId'] as string;
790+
const psbt = await this.getMusig2Nonces(this.decodeTransactionAsPsbt(hex), walletId);
791+
return _.extend(ovcJson, { txHex: encodeTransaction(psbt).toString('hex') });
757792
}
758793

759794
/**

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,15 @@ export function decodePsbtWith(
5757
return fixedScriptWallet.BitGoPsbt.fromBytes(psbt, toNetworkName(network));
5858
}
5959
}
60+
61+
export function encodeTransaction(
62+
transaction: utxolib.bitgo.UtxoTransaction<bigint | number> | utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt
63+
): Buffer {
64+
if (transaction instanceof utxolib.bitgo.UtxoTransaction) {
65+
return transaction.toBuffer();
66+
} else if (transaction instanceof utxolib.bitgo.UtxoPsbt) {
67+
return transaction.toBuffer();
68+
} else {
69+
return Buffer.from(transaction.serialize());
70+
}
71+
}

modules/abstract-utxo/src/transaction/fixedScript/signPsbtWasm.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const PSBT_CACHE_WASM = new Map<string, fixedScriptWallet.BitGoPsbt>();
2020

2121
function hasKeyPathSpendInput(
2222
tx: fixedScriptWallet.BitGoPsbt,
23-
rootWalletKeys: fixedScriptWallet.IWalletKeys,
23+
rootWalletKeys: fixedScriptWallet.RootWalletKeys,
2424
replayProtection: ReplayProtectionKeys
2525
): boolean {
2626
const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, replayProtection);
@@ -36,10 +36,10 @@ function hasKeyPathSpendInput(
3636
export function signAndVerifyPsbtWasm(
3737
tx: fixedScriptWallet.BitGoPsbt,
3838
signerKeychain: BIP32Interface,
39-
rootWalletKeys: fixedScriptWallet.IWalletKeys,
39+
rootWalletKeys: fixedScriptWallet.RootWalletKeys,
4040
replayProtection: ReplayProtectionKeys,
4141
{ isLastSignature }: { isLastSignature: boolean }
42-
): fixedScriptWallet.BitGoPsbt | Uint8Array {
42+
): fixedScriptWallet.BitGoPsbt | Buffer {
4343
const wasmSigner = toWasmBIP32(signerKeychain);
4444
const parsed = tx.parseTransactionWithWalletKeys(rootWalletKeys, replayProtection);
4545

@@ -85,7 +85,7 @@ export function signAndVerifyPsbtWasm(
8585

8686
if (isLastSignature) {
8787
tx.finalizeAllInputs();
88-
return tx.extractTransaction();
88+
return Buffer.from(tx.extractTransaction());
8989
}
9090

9191
return tx;
@@ -100,17 +100,17 @@ export async function signPsbtWithMusig2ParticipantWasm(
100100
coin: Musig2Participant<fixedScriptWallet.BitGoPsbt>,
101101
tx: fixedScriptWallet.BitGoPsbt,
102102
signerKeychain: BIP32Interface | undefined,
103-
rootWalletKeys: fixedScriptWallet.IWalletKeys,
104-
replayProtection: ReplayProtectionKeys,
103+
rootWalletKeys: fixedScriptWallet.RootWalletKeys,
105104
params: {
105+
replayProtection: ReplayProtectionKeys;
106106
isLastSignature: boolean;
107107
signingStep: 'signerNonce' | 'cosignerNonce' | 'signerSignature' | undefined;
108108
walletId: string | undefined;
109109
}
110-
): Promise<fixedScriptWallet.BitGoPsbt | Uint8Array> {
110+
): Promise<fixedScriptWallet.BitGoPsbt | Buffer> {
111111
const wasmSigner = signerKeychain ? toWasmBIP32(signerKeychain) : undefined;
112112

113-
if (hasKeyPathSpendInput(tx, rootWalletKeys, replayProtection)) {
113+
if (hasKeyPathSpendInput(tx, rootWalletKeys, params.replayProtection)) {
114114
// We can only be the first signature on a transaction with taproot key path spend inputs because
115115
// we require the secret nonce in the cache of the first signer, which is impossible to retrieve if
116116
// deserialized from a hex.
@@ -162,7 +162,7 @@ export async function signPsbtWithMusig2ParticipantWasm(
162162
}
163163

164164
assert(signerKeychain);
165-
return signAndVerifyPsbtWasm(tx, signerKeychain, rootWalletKeys, replayProtection, {
165+
return signAndVerifyPsbtWasm(tx, signerKeychain, rootWalletKeys, params.replayProtection, {
166166
isLastSignature: params.isLastSignature,
167167
});
168168
}

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1+
import assert from 'assert';
2+
3+
import { isTriple } from '@bitgo/sdk-core';
14
import _ from 'lodash';
25
import { BIP32Interface } from '@bitgo/secp256k1';
36
import { bitgo } from '@bitgo/utxo-lib';
47
import * as utxolib from '@bitgo/utxo-lib';
5-
6-
import { DecodedTransaction } from '../types';
8+
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
79

810
import { Musig2Participant } from './musig2';
911
import { signLegacyTransaction } from './signLegacyTransaction';
1012
import { signPsbtWithMusig2Participant } from './signPsbt';
13+
import { signPsbtWithMusig2ParticipantWasm } from './signPsbtWasm';
14+
import { getReplayProtectionPubkeys } from './replayProtection';
1115

12-
export async function signTransaction(
13-
coin: Musig2Participant<utxolib.bitgo.UtxoPsbt>,
14-
tx: DecodedTransaction<bigint | number>,
16+
export async function signTransaction<
17+
T extends utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction<bigint | number> | fixedScriptWallet.BitGoPsbt
18+
>(
19+
coin: Musig2Participant<utxolib.bitgo.UtxoPsbt> | Musig2Participant<fixedScriptWallet.BitGoPsbt>,
20+
tx: T,
1521
signerKeychain: BIP32Interface | undefined,
1622
network: utxolib.Network,
1723
params: {
@@ -24,19 +30,39 @@ export async function signTransaction(
2430
pubs: string[] | undefined;
2531
cosignerPub: string | undefined;
2632
}
27-
): Promise<utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction<bigint | number>> {
33+
): Promise<
34+
utxolib.bitgo.UtxoPsbt | utxolib.bitgo.UtxoTransaction<bigint | number> | fixedScriptWallet.BitGoPsbt | Buffer
35+
> {
2836
let isLastSignature = false;
2937
if (_.isBoolean(params.isLastSignature)) {
3038
// if build is called instead of buildIncomplete, no signature placeholders are left in the sig script
3139
isLastSignature = params.isLastSignature;
3240
}
3341

3442
if (tx instanceof bitgo.UtxoPsbt) {
35-
return signPsbtWithMusig2Participant(coin, tx, signerKeychain, {
43+
return signPsbtWithMusig2Participant(coin as Musig2Participant<utxolib.bitgo.UtxoPsbt>, tx, signerKeychain, {
3644
isLastSignature,
3745
signingStep: params.signingStep,
3846
walletId: params.walletId,
3947
});
48+
} else if (tx instanceof fixedScriptWallet.BitGoPsbt) {
49+
assert(params.pubs, 'pubs are required for fixed script signing');
50+
assert(isTriple(params.pubs), 'pubs must be a triple');
51+
const rootWalletKeys = fixedScriptWallet.RootWalletKeys.fromXpubs(params.pubs);
52+
return signPsbtWithMusig2ParticipantWasm(
53+
coin as Musig2Participant<fixedScriptWallet.BitGoPsbt>,
54+
tx,
55+
signerKeychain,
56+
rootWalletKeys,
57+
{
58+
replayProtection: {
59+
publicKeys: getReplayProtectionPubkeys(network),
60+
},
61+
isLastSignature,
62+
signingStep: params.signingStep,
63+
walletId: params.walletId,
64+
}
65+
);
4066
}
4167

4268
return signLegacyTransaction(tx, signerKeychain, {

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { fetchKeychains, toBip32Triple } from '../keychains';
1010

1111
import * as fixedScript from './fixedScript';
1212
import * as descriptor from './descriptor';
13+
import { encodeTransaction } from './decode';
1314

1415
const debug = buildDebug('bitgo:abstract-utxo:transaction:signTransaction');
1516

@@ -72,6 +73,7 @@ export async function signTransaction<TNumber extends number | bigint>(
7273
pubs: params.pubs,
7374
cosignerPub: params.cosignerPub,
7475
});
75-
return { txHex: signedTx.toBuffer().toString('hex') };
76+
const buffer = Buffer.isBuffer(signedTx) ? signedTx : encodeTransaction(signedTx);
77+
return { txHex: buffer.toString('hex') };
7678
}
7779
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import * as utxolib from '@bitgo/utxo-lib';
2+
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
23

34
import type { UtxoNamedKeychains } from '../keychains';
45

56
import type { CustomChangeOptions } from './fixedScript';
67

78
export type SdkBackend = 'utxolib' | 'wasm-utxo';
89

10+
export function isSdkBackend(backend: string): backend is SdkBackend {
11+
return backend === 'utxolib' || backend === 'wasm-utxo';
12+
}
13+
914
export type DecodedTransaction<TNumber extends number | bigint> =
1015
| utxolib.bitgo.UtxoTransaction<TNumber>
11-
| utxolib.bitgo.UtxoPsbt;
16+
| utxolib.bitgo.UtxoPsbt
17+
| fixedScriptWallet.BitGoPsbt;
1218

1319
export interface BaseOutput<TAmount = string | number> {
1420
address: string;

0 commit comments

Comments
 (0)