Skip to content

Commit 73a2fb5

Browse files
Merge pull request #7523 from BitGo/BTC-2732.refactor-explaintx
refactor(abstract-utxo): refactor explainTransaction return type
2 parents 48d0f5a + 2a7be6c commit 73a2fb5

File tree

11 files changed

+96
-68
lines changed

11 files changed

+96
-68
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
InvalidAddressError,
1818
IRequestTracer,
1919
isTriple,
20-
ITransactionExplanation as BaseTransactionExplanation,
2120
IWallet,
2221
KeychainsTriplet,
2322
KeyIndices,
@@ -66,6 +65,7 @@ import {
6665
parseTransaction,
6766
verifyTransaction,
6867
} from './transaction';
68+
import type { TransactionExplanation } from './transaction/fixedScript/explainTransaction';
6969
import {
7070
AggregateValidationError,
7171
ErrorMissingOutputs,
@@ -200,29 +200,6 @@ export function isWalletOutput(output: Output): output is FixedScriptWalletOutpu
200200
);
201201
}
202202

203-
export interface TransactionExplanation<TFee = string> extends BaseTransactionExplanation<TFee, string> {
204-
locktime?: number;
205-
/** NOTE: this actually only captures external outputs */
206-
outputs: Output[];
207-
changeOutputs: Output[];
208-
209-
/**
210-
* Number of input signatures per input.
211-
*/
212-
inputSignatures: number[];
213-
214-
/**
215-
* Highest input signature count for the transaction
216-
*/
217-
signatures: number;
218-
219-
/**
220-
* BIP322 messages extracted from the transaction inputs.
221-
* These messages are used for verifying the transaction against the BIP322 standard.
222-
*/
223-
messages?: Bip322Message[];
224-
}
225-
226203
export interface TransactionInfo<TNumber extends number | bigint = number> {
227204
/** Maps txid to txhex. Required for offline signing. */
228205
txHexes?: Record<string, string>;
@@ -870,9 +847,9 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
870847
* change amounts, and transaction outputs.
871848
* @param params
872849
*/
873-
async explainTransaction<TNumber extends number | bigint = number>(
850+
override async explainTransaction<TNumber extends number | bigint = number>(
874851
params: ExplainTransactionOptions<TNumber>
875-
): Promise<TransactionExplanation<string | undefined>> {
852+
): Promise<TransactionExplanation> {
876853
return explainTx(this.decodeTransactionFromPrebuild(params), params, this.network);
877854
}
878855

modules/abstract-utxo/src/impl/doge/doge.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import {
66
UtxoNetwork,
77
SignTransactionOptions,
88
ExplainTransactionOptions,
9-
TransactionExplanation,
109
ParseTransactionOptions,
1110
ParsedTransaction,
1211
VerifyTransactionOptions,
1312
RecoverFromWrongChainOptions,
1413
TransactionInfo,
1514
TransactionPrebuild,
1615
} from '../../abstractUtxoCoin';
16+
import type { TransactionExplanation } from '../../transaction/fixedScript/explainTransaction';
1717
import type { CrossChainRecoverySigned, CrossChainRecoveryUnsigned } from '../../recovery/crossChainRecovery';
1818

1919
type UnspentJSON = bitgo.Unspent<number> & { valueString: string };
@@ -114,7 +114,7 @@ export class Doge extends AbstractUtxoCoin {
114114

115115
async explainTransaction<TNumber extends number | bigint = bigint>(
116116
params: ExplainTransactionOptions<TNumber> | (ExplainTransactionOptions<TNumber> & { txInfo: TransactionInfoJSON })
117-
): Promise<TransactionExplanation<string | undefined>> {
117+
): Promise<TransactionExplanation> {
118118
return super.explainTransaction({
119119
...params,
120120
txInfo: params.txInfo ? parseTransactionInfo(params.txInfo as TransactionInfoJSON) : undefined,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ITransactionRecipient } from '@bitgo/sdk-core';
33
import * as coreDescriptors from '@bitgo/utxo-core/descriptor';
44

55
import { toExtendedAddressFormat } from '../recipient';
6-
import { TransactionExplanation } from '../../abstractUtxoCoin';
6+
import type { TransactionExplanationUtxolibPsbt } from '../fixedScript/explainTransaction';
77

88
function toRecipient(output: coreDescriptors.ParsedOutput, network: utxolib.Network): ITransactionRecipient {
99
return {
@@ -34,7 +34,7 @@ function getInputSignatures(psbt: utxolib.bitgo.UtxoPsbt): number[] {
3434
export function explainPsbt(
3535
psbt: utxolib.bitgo.UtxoPsbt,
3636
descriptors: coreDescriptors.DescriptorMap
37-
): TransactionExplanation<string> {
37+
): TransactionExplanationUtxolibPsbt {
3838
const parsedTransaction = coreDescriptors.parse(psbt, descriptors, psbt.network);
3939
const { inputs, outputs } = parsedTransaction;
4040
const externalOutputs = outputs.filter((o) => o.scriptId === undefined);

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import * as utxolib from '@bitgo/utxo-lib';
22
import { isTriple, IWallet } from '@bitgo/sdk-core';
33

4-
import { TransactionExplanation } from '../abstractUtxoCoin';
54
import { getDescriptorMapFromWallet, isDescriptorWallet } from '../descriptor';
65
import { toBip32Triple } from '../keychains';
76
import { getPolicyForEnv } from '../descriptor/validatePolicy';
87

8+
import type {
9+
TransactionExplanationUtxolibLegacy,
10+
TransactionExplanationUtxolibPsbt,
11+
} from './fixedScript/explainTransaction';
912
import * as fixedScript from './fixedScript';
1013
import * as descriptor from './descriptor';
1114

@@ -22,7 +25,7 @@ export function explainTx<TNumber extends number | bigint>(
2225
changeInfo?: fixedScript.ChangeAddressInfo[];
2326
},
2427
network: utxolib.Network
25-
): TransactionExplanation<string | undefined> {
28+
): TransactionExplanationUtxolibLegacy | TransactionExplanationUtxolibPsbt {
2629
if (params.wallet && isDescriptorWallet(params.wallet)) {
2730
if (tx instanceof utxolib.bitgo.UtxoPsbt) {
2831
if (!params.pubs || !isTriple(params.pubs)) {

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

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,53 @@ import * as utxolib from '@bitgo/utxo-lib';
22
import { bip322 } from '@bitgo/utxo-core';
33
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
44
import { bitgo } from '@bitgo/utxo-lib';
5-
import { Triple } from '@bitgo/sdk-core';
5+
import { ITransactionExplanation as BaseTransactionExplanation, Triple } from '@bitgo/sdk-core';
66
import * as utxocore from '@bitgo/utxo-core';
77

8-
import { Output, TransactionExplanation, Bip322Message, FixedScriptWalletOutput } from '../../abstractUtxoCoin';
8+
import type { Output, Bip322Message, FixedScriptWalletOutput } from '../../abstractUtxoCoin';
99
import { toExtendedAddressFormat } from '../recipient';
1010
import { getPayGoVerificationPubkey } from '../getPayGoVerificationPubkey';
1111

12+
// ===== Transaction Explanation Type Definitions =====
13+
14+
export interface AbstractUtxoTransactionExplanation<TFee = string> extends BaseTransactionExplanation<TFee, string> {
15+
/** NOTE: this actually only captures external outputs */
16+
outputs: Output[];
17+
changeOutputs: Output[];
18+
19+
/**
20+
* BIP322 messages extracted from the transaction inputs.
21+
* These messages are used for verifying the transaction against the BIP322 standard.
22+
*/
23+
messages?: Bip322Message[];
24+
}
25+
26+
/** @deprecated - the signature fields are not very useful */
27+
interface TransactionExplanationWithSignatures<TFee = string> extends AbstractUtxoTransactionExplanation<TFee> {
28+
/** @deprecated - unused outside of tests */
29+
locktime?: number;
30+
31+
/**
32+
* Number of input signatures per input.
33+
* @deprecated - this is not very useful without knowing who signed each input.
34+
*/
35+
inputSignatures: number[];
36+
37+
/**
38+
* Highest input signature count for the transaction
39+
* @deprecated - this is not very useful without knowing who signed each input.
40+
*/
41+
signatures: number;
42+
}
43+
44+
/** When parsing the legacy transaction format, we cannot always infer the fee so we set it to string | undefined */
45+
export type TransactionExplanationUtxolibLegacy = TransactionExplanationWithSignatures<string | undefined>;
46+
47+
/** When parsing a PSBT, we can infer the fee so we set TFee to string. */
48+
export type TransactionExplanationUtxolibPsbt = TransactionExplanationWithSignatures<string>;
49+
50+
export type TransactionExplanation = TransactionExplanationUtxolibLegacy | TransactionExplanationUtxolibPsbt;
51+
1252
export type ChangeAddressInfo = {
1353
address: string;
1454
chain: number;
@@ -302,7 +342,7 @@ export function explainPsbt(
302342
},
303343
network: utxolib.Network,
304344
{ strict = false }: { strict?: boolean } = {}
305-
): TransactionExplanation {
345+
): TransactionExplanationUtxolibPsbt {
306346
const payGoVerificationInfo = getPayGoVerificationInfo(psbt, network);
307347
if (payGoVerificationInfo) {
308348
try {
@@ -356,7 +396,7 @@ export function explainLegacyTx<TNumber extends number | bigint>(
356396
changeInfo?: { address: string; chain: number; index: number }[];
357397
},
358398
network: utxolib.Network
359-
): TransactionExplanation<string | undefined> {
399+
): TransactionExplanationUtxolibLegacy {
360400
const common = explainCommon(tx, params, network);
361401
const inputSignaturesCount = getTxInputSignaturesCount(tx, params, network);
362402
return {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ import _ from 'lodash';
44
import { Triple, VerificationOptions, Wallet } from '@bitgo/sdk-core';
55
import * as utxolib from '@bitgo/utxo-lib';
66

7-
import {
7+
import type {
88
AbstractUtxoCoin,
99
FixedScriptWalletOutput,
1010
Output,
11-
TransactionExplanation,
1211
ParsedTransaction,
1312
ParseTransactionOptions,
1413
} from '../../abstractUtxoCoin';
1514
import { fetchKeychains, getKeySignatures, toKeychainTriple, UtxoKeychain, UtxoNamedKeychains } from '../../keychains';
1615
import { ComparableOutput, outputDifference } from '../outputDifference';
1716
import { fromExtendedAddressFormatToScript, toExtendedAddressFormat } from '../recipient';
1817

18+
import type { TransactionExplanation } from './explainTransaction';
1919
import { CustomChangeOptions, parseOutput } from './parseOutput';
2020

2121
export type ComparableOutputWithExternal<TValue> = ComparableOutput<TValue> & {
@@ -53,7 +53,7 @@ export async function parseTransaction<TNumber extends bigint | number>(
5353
}
5454

5555
// obtain all outputs
56-
const explanation: TransactionExplanation<string | undefined> = await coin.explainTransaction<TNumber>({
56+
const explanation: TransactionExplanation = await coin.explainTransaction<TNumber>({
5757
txHex: txPrebuild.txHex,
5858
txInfo: txPrebuild.txInfo,
5959
pubs: keychainArray.map((k) => k.pub) as Triple<string>,

modules/abstract-utxo/test/unit/explainTransaction.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe('Explain Transaction', function () {
2828
assert.strictEqual(result.outputs.length, 1);
2929
assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a');
3030
assert.strictEqual(result.fee, '0');
31+
assert.ok('signatures' in result);
3132
assert.strictEqual(result.signatures, 0);
3233
assert.ok(result.messages);
3334
result.messages?.forEach((obj) => {
@@ -45,6 +46,7 @@ describe('Explain Transaction', function () {
4546
assert.strictEqual(result.outputs.length, 1);
4647
assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a');
4748
assert.strictEqual(result.fee, '0');
49+
assert.ok('signatures' in result);
4850
assert.strictEqual(result.signatures, 1);
4951
assert.ok(result.messages);
5052
result.messages?.forEach((obj) => {
@@ -62,6 +64,7 @@ describe('Explain Transaction', function () {
6264
assert.strictEqual(result.outputs.length, 1);
6365
assert.strictEqual(result.outputs[0].address, 'scriptPubKey:6a');
6466
assert.strictEqual(result.fee, '0');
67+
assert.ok('signatures' in result);
6568
assert.strictEqual(result.signatures, 2);
6669
assert.ok(result.messages);
6770
result.messages?.forEach((obj) => {

modules/abstract-utxo/test/unit/parseTransaction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import assert from 'assert';
33
import * as sinon from 'sinon';
44
import { Wallet, UnexpectedAddressError, VerificationOptions } from '@bitgo/sdk-core';
55

6-
import { UtxoWallet, Output, TransactionExplanation, TransactionParams } from '../../src';
6+
import { UtxoWallet, Output, TransactionParams } from '../../src';
7+
import type { TransactionExplanation } from '../../src/transaction/fixedScript/explainTransaction';
78

89
import { getUtxoCoin } from './util';
910

modules/abstract-utxo/test/unit/transaction.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -564,12 +564,16 @@ function run<TNumber extends number | bigint = number>(
564564
? 2
565565
: undefined;
566566

567-
explanation.inputSignatures.should.eql(
568-
// FIXME(BG-35154): implement signature verification for replay protection inputs
569-
inputScripts.map((type) => (type === 'p2shP2pk' ? 0 : expectedSignatureCount))
570-
);
571-
explanation.signatures.should.eql(expectedSignatureCount);
572-
explanation.changeAmount.should.eql('0'); // no change addresses given
567+
if ('inputSignatures' in explanation) {
568+
explanation.inputSignatures.should.eql(
569+
// FIXME(BG-35154): implement signature verification for replay protection inputs
570+
inputScripts.map((type) => (type === 'p2shP2pk' ? 0 : expectedSignatureCount))
571+
);
572+
}
573+
if ('signatures' in explanation) {
574+
explanation.signatures.should.eql(expectedSignatureCount);
575+
explanation.changeAmount.should.eql('0'); // no change addresses given
576+
}
573577
let expectedOutputAmount =
574578
BigInt((txFormat === 'psbt' ? getUnspentsForPsbt() : getUnspents()).length) * BigInt(value);
575579
inputScripts.forEach((type) => {

modules/abstract-utxo/test/unit/transaction/descriptor/explainPsbt.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import assert from 'assert';
33
import { getKeyTriple } from '@bitgo/utxo-core/testutil';
44
import { getDescriptorMap, mockPsbtDefaultWithDescriptorTemplate } from '@bitgo/utxo-core/testutil/descriptor';
55

6-
import { TransactionExplanation } from '../../../../src';
6+
import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction';
77
import { explainPsbt } from '../../../../src/transaction/descriptor';
88

99
import { getFixtureRoot } from './fixtures.utils';
1010

1111
const { assertEqualFixture } = getFixtureRoot(__dirname + '/fixtures');
1212

1313
function assertSignatureCount(expl: TransactionExplanation, signatures: number, inputSignatures: number[]) {
14+
assert.ok('signatures' in expl);
15+
assert.ok('inputSignatures' in expl);
1416
assert.deepStrictEqual(expl.signatures, signatures);
1517
assert.deepStrictEqual(expl.inputSignatures, inputSignatures);
1618
}

0 commit comments

Comments
 (0)