From 2c01a77b42904bad9ab63cc31af4c9fdeea58f5e Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 24 Sep 2024 13:55:53 +0200 Subject: [PATCH] feat: add psbt signing for v1 wallets This commit adds optional PSBT signing to the v1 wallet API. Call the `signTransaction` method with a `psbt: string` (hex-encoded) argument to co-sign a PSBT. The method will return the signed PSBT as a hex-encoded string. Issue: BTC-1351 --- modules/sdk-api/src/v1/signPsbt.ts | 38 ++++++++++++++++++++++++ modules/sdk-api/src/v1/wallet.ts | 10 +++++++ modules/sdk-api/test/unit/v1/signPsbt.ts | 27 +++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 modules/sdk-api/src/v1/signPsbt.ts create mode 100644 modules/sdk-api/test/unit/v1/signPsbt.ts diff --git a/modules/sdk-api/src/v1/signPsbt.ts b/modules/sdk-api/src/v1/signPsbt.ts new file mode 100644 index 0000000000..1057ecacec --- /dev/null +++ b/modules/sdk-api/src/v1/signPsbt.ts @@ -0,0 +1,38 @@ +import * as utxolib from '@bitgo/utxo-lib'; + +import * as buildDebug from 'debug'; + +const debug = buildDebug('bitgo:v1:txb'); + +/** + * Co-sign a PSBT. + * Simply a wrapper around `utxolib.bitgo.createPsbtFromBuffer` and `psbt.signAllInputsHD`. + * @param params + */ +export function signPsbtRequest(params: { psbt: string; keychain: { xprv: string } } | unknown): { + psbt: string; +} { + if (typeof params !== 'object' || params === null) { + throw new Error(`invalid argument`); + } + + if (!('psbt' in params) || typeof params.psbt !== 'string') { + throw new Error(`invalid params.psbt`); + } + + if (!('keychain' in params) || typeof params.keychain !== 'object' || params.keychain === null) { + throw new Error(`invalid params.keychain`); + } + + if (!('xprv' in params.keychain) || typeof params.keychain.xprv !== 'string') { + throw new Error(`invalid params.keychain.xprv`); + } + + const psbt = utxolib.bitgo.createPsbtFromBuffer(Buffer.from(params.psbt, 'hex'), utxolib.networks.bitcoin); + const keypair = utxolib.bip32.fromBase58(params.keychain.xprv, utxolib.networks.bitcoin); + debug('signing PSBT with keychain %s', keypair.neutered().toBase58()); + utxolib.bitgo.withUnsafeNonSegwit(psbt, () => psbt.signAllInputsHD(keypair)); + return { + psbt: psbt.toBuffer().toString('hex'), + }; +} diff --git a/modules/sdk-api/src/v1/wallet.ts b/modules/sdk-api/src/v1/wallet.ts index 24d2fc9585..dd96d14fc3 100644 --- a/modules/sdk-api/src/v1/wallet.ts +++ b/modules/sdk-api/src/v1/wallet.ts @@ -26,6 +26,7 @@ import { } from '@bitgo/sdk-core'; import * as Bluebird from 'bluebird'; import * as _ from 'lodash'; +import { signPsbtRequest } from './signPsbt'; const TransactionBuilder = require('./transactionBuilder'); const PendingApproval = require('./pendingapproval'); @@ -896,6 +897,15 @@ Wallet.prototype.createTransaction = function (params, callback) { // callback(err, transaction) Wallet.prototype.signTransaction = function (params, callback) { params = _.extend({}, params); + + if (params.psbt) { + try { + return callback(null, signPsbtRequest(params)); + } catch (e) { + return callback(e); + } + } + common.validateParams(params, ['transactionHex'], [], callback); if (!Array.isArray(params.unspents)) { diff --git a/modules/sdk-api/test/unit/v1/signPsbt.ts b/modules/sdk-api/test/unit/v1/signPsbt.ts new file mode 100644 index 0000000000..fad70b361c --- /dev/null +++ b/modules/sdk-api/test/unit/v1/signPsbt.ts @@ -0,0 +1,27 @@ +import * as assert from 'assert'; +import * as utxolib from '@bitgo/utxo-lib'; +import { signPsbtRequest } from '../../../src/v1/signPsbt'; + +describe('signPsbt', function () { + it('signs psbt', function () { + const keys = utxolib.testutil.getDefaultWalletKeys(); + const psbt = utxolib.testutil.constructPsbt( + [{ scriptType: 'p2sh', value: BigInt(1e8) }], + [{ scriptType: 'p2sh', value: BigInt(1e8 - 1000) }], + utxolib.networks.bitcoin, + keys, + 'unsigned' + ); + const result = signPsbtRequest({ + psbt: psbt.toHex(), + keychain: { + xprv: keys.triple[0].toBase58(), + }, + }); + const halfSignedPsbt = utxolib.bitgo.createPsbtFromBuffer( + Buffer.from(result.psbt, 'hex'), + utxolib.networks.bitcoin + ); + assert.ok(halfSignedPsbt.validateSignaturesOfInputHD(0, keys.triple[0])); + }); +});