From 385e59438e039eafccdbff03f478da9e23d5a361 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Wed, 29 Nov 2023 11:54:26 -0500 Subject: [PATCH] feat(utxo-lib): parse basic info from the psbt TICKET: BTC-637 --- modules/utxo-lib/src/bitgo/wallet/Psbt.ts | 37 ++++++++++++++++++++++- modules/utxo-lib/test/bitgo/psbt/Psbt.ts | 14 ++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/modules/utxo-lib/src/bitgo/wallet/Psbt.ts b/modules/utxo-lib/src/bitgo/wallet/Psbt.ts index e998cc7622..0841aac4ab 100644 --- a/modules/utxo-lib/src/bitgo/wallet/Psbt.ts +++ b/modules/utxo-lib/src/bitgo/wallet/Psbt.ts @@ -9,7 +9,7 @@ import { UtxoTransaction } from '../UtxoTransaction'; import { createOutputScript2of3, getLeafHash, scriptTypeForChain, toXOnlyPublicKey } from '../outputScripts'; import { DerivedWalletKeys, RootWalletKeys } from './WalletKeys'; import { toPrevOutputWithPrevTx } from '../Unspent'; -import { createPsbtFromHex, createPsbtFromTransaction } from '../transaction'; +import { createPsbtFromHex, createPsbtFromTransaction, createTransactionFromBuffer } from '../transaction'; import { isWalletUnspent, WalletUnspent } from './Unspent'; import { @@ -584,3 +584,38 @@ export function deleteWitnessUtxoForNonSegwitInputs(psbt: UtxoPsbt): void { } }); } + +export function getTransactionInfoFromPsbt(psbt: UtxoPsbt): { + inputCount: number; + outputCount: number; + inputAmount: bigint; + outputAmount: bigint; + fee: bigint; +} { + const inputCount = psbt.data.inputs.length; + const outputCount = psbt.data.outputs.length; + const txInputs = psbt.txInputs; + const txOutputs = psbt.txOutputs; + const inputAmount = psbt.data.inputs.reduce((acc, input, inputIndex) => { + if (input.witnessUtxo) { + return acc + BigInt(input.witnessUtxo.value); + } else if (input.nonWitnessUtxo) { + const tx = createTransactionFromBuffer(input.nonWitnessUtxo, psbt.network, { amountType: 'bigint' }); + return acc + tx.outs[txInputs[inputIndex].index].value; + } else { + throw new Error('missing witnessUtxo and nonWitnessUtxo'); + } + }, BigInt(0)); + const outputAmount = psbt.data.outputs.reduce( + (acc, output, outputIndex) => acc + txOutputs[outputIndex].value, + BigInt(0) + ); + const fee = inputAmount - outputAmount; + return { + inputCount, + outputCount, + inputAmount, + outputAmount, + fee, + }; +} diff --git a/modules/utxo-lib/test/bitgo/psbt/Psbt.ts b/modules/utxo-lib/test/bitgo/psbt/Psbt.ts index 645f145f9e..b3f4b7c58c 100644 --- a/modules/utxo-lib/test/bitgo/psbt/Psbt.ts +++ b/modules/utxo-lib/test/bitgo/psbt/Psbt.ts @@ -33,6 +33,8 @@ import { deleteWitnessUtxoForNonSegwitInputs, getPsbtInputScriptType, withUnsafeNonSegwit, + getTransactionInfoFromPsbt, + WalletUnspent, } from '../../../src/bitgo'; import { createOutputScript2of3, @@ -876,8 +878,9 @@ function testUtxoPsbt(coinNetwork: Network) { describe(`Testing UtxoPsbt (de)serialization for ${getNetworkName(coinNetwork)} network`, function () { let psbt: UtxoPsbt; let psbtHex: string; + let unspents: (WalletUnspent | Unspent)[]; before(async function () { - const unspents = mockUnspents(rootWalletKeys, ['p2sh'], BigInt('10000000000000'), coinNetwork); + unspents = mockUnspents(rootWalletKeys, ['p2sh'], BigInt('10000000000000'), coinNetwork); const txBuilderParams = { signer: 'user', cosigner: 'bitgo', @@ -905,6 +908,15 @@ function testUtxoPsbt(coinNetwork: Network) { assert.deepStrictEqual(createPsbtFromHex(psbtHex, coinNetwork, false).toBuffer(), psbt.toBuffer()); }); + it('should be able to get transaction info from psbt', function () { + const txInfo = getTransactionInfoFromPsbt(psbt); + assert.strictEqual(txInfo.fee, FEE); + assert.strictEqual(txInfo.inputCount, unspents.length); + assert.strictEqual(txInfo.inputAmount, BigInt('10000000000000') * BigInt(unspents.length)); + assert.strictEqual(txInfo.outputAmount, BigInt('10000000000000') * BigInt(unspents.length) - FEE); + assert.strictEqual(txInfo.outputCount, psbt.data.outputs.length); + }); + function deserializeBip32PathsCorrectly(bip32PathsAbsolute: boolean): void { function checkDerivationPrefix(bip32Derivation: { path: string }): void { const path = bip32Derivation.path.split('/');