Skip to content

Commit 2d36d4e

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): add support for parsing legacy transactions
Implement parsing support for legacy transactions in addition to PSBT format. Add helper function to extract change information from PSBTs for use with legacy transaction parsing. Issue: BTC-2732 Co-authored-by: llm-git <[email protected]>
1 parent 7c982bf commit 2d36d4e

File tree

1 file changed

+74
-15
lines changed
  • modules/abstract-utxo/test/unit/transaction/fixedScript

1 file changed

+74
-15
lines changed

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

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ import { parseTransaction } from '../../../../src/transaction/fixedScript/parseT
88
import { ParsedTransaction } from '../../../../src/transaction/types';
99
import { UtxoWallet } from '../../../../src/wallet';
1010
import { getUtxoCoin } from '../../util';
11-
import { explainPsbt } from '../../../../src/transaction/fixedScript';
12-
import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction';
11+
import { explainLegacyTx, explainPsbt } from '../../../../src/transaction/fixedScript';
12+
import type {
13+
TransactionExplanation,
14+
ChangeAddressInfo,
15+
} from '../../../../src/transaction/fixedScript/explainTransaction';
1316
import { getChainFromNetwork } from '../../../../src/names';
17+
import { TransactionPrebuild } from '../../../../src/abstractUtxoCoin';
1418

1519
function getTxParamsFromExplanation(explanation: TransactionExplanation): {
1620
recipients: ITransactionRecipient[];
@@ -30,15 +34,39 @@ function getTxParamsFromExplanation(explanation: TransactionExplanation): {
3034
};
3135
}
3236

37+
function getChangeInfoFromPsbt(psbt: utxolib.bitgo.UtxoPsbt): ChangeAddressInfo[] | undefined {
38+
try {
39+
return utxolib.bitgo.findInternalOutputIndices(psbt).map((i) => {
40+
const output = psbt.data.outputs[i];
41+
const derivations = output.bip32Derivation ?? output.tapBip32Derivation ?? undefined;
42+
if (!derivations || derivations.length !== 3) {
43+
throw new Error('expected 3 derivation paths');
44+
}
45+
const path = derivations[0].path;
46+
const { chain, index } = utxolib.bitgo.getChainAndIndexFromPath(path);
47+
return {
48+
address: utxolib.address.fromOutputScript(psbt.txOutputs[i].script, psbt.network),
49+
chain,
50+
index,
51+
};
52+
});
53+
} catch (e) {
54+
if (e instanceof utxolib.bitgo.ErrorNoMultiSigInputFound) {
55+
return undefined;
56+
}
57+
throw e;
58+
}
59+
}
60+
3361
function describeParseTransactionWith(
3462
acidTest: utxolib.testutil.AcidTest,
63+
label: string,
3564
{
36-
label = 'default',
3765
txParams,
3866
expectedExplicitExternalSpendAmount,
3967
expectedImplicitExternalSpendAmount,
68+
txFormat = 'psbt',
4069
}: {
41-
label?: string;
4270
txParams:
4371
| {
4472
recipients: ITransactionRecipient[];
@@ -47,6 +75,7 @@ function describeParseTransactionWith(
4775
| 'inferFromExplanation';
4876
expectedExplicitExternalSpendAmount: bigint;
4977
expectedImplicitExternalSpendAmount: bigint;
78+
txFormat?: 'psbt' | 'legacy';
5079
}
5180
) {
5281
describe(`${acidTest.name}/${label}`, function () {
@@ -61,9 +90,21 @@ function describeParseTransactionWith(
6190

6291
// Create PSBT and explanation
6392
const psbt = acidTest.createPsbt();
64-
const explanation = explainPsbt(psbt, { pubs: acidTest.rootWalletKeys }, acidTest.network, {
65-
strict: true,
66-
});
93+
94+
let explanation: TransactionExplanation;
95+
if (txFormat === 'psbt') {
96+
explanation = explainPsbt(psbt, { pubs: acidTest.rootWalletKeys }, acidTest.network, {
97+
strict: true,
98+
});
99+
} else if (txFormat === 'legacy') {
100+
const tx = psbt.getUnsignedTx();
101+
const pubs = acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58());
102+
// Extract change info from PSBT to pass to explainLegacyTx
103+
const changeInfo = getChangeInfoFromPsbt(psbt);
104+
explanation = explainLegacyTx(tx, { pubs, changeInfo }, acidTest.network);
105+
} else {
106+
throw new Error(`Invalid txFormat: ${txFormat}`);
107+
}
67108

68109
// Determine txParams
69110
const resolvedTxParams =
@@ -92,12 +133,23 @@ function describeParseTransactionWith(
92133
// Stub explainTransaction to return the explanation without making network calls
93134
stubExplainTransaction = sinon.stub(coin, 'explainTransaction').resolves(explanation);
94135

136+
let txPrebuild: TransactionPrebuild<bigint>;
137+
if (txFormat === 'psbt') {
138+
txPrebuild = {
139+
txHex: psbt.toHex(),
140+
};
141+
} else if (txFormat === 'legacy') {
142+
txPrebuild = {
143+
txHex: psbt.getUnsignedTx().toHex(),
144+
};
145+
} else {
146+
throw new Error(`Invalid txFormat: ${txFormat}`);
147+
}
148+
95149
refParsedTransaction = await parseTransaction(coin, {
96150
wallet: mockWallet as unknown as UtxoWallet,
97151
txParams: resolvedTxParams,
98-
txPrebuild: {
99-
txHex: psbt.toHex(),
100-
},
152+
txPrebuild,
101153
verification,
102154
});
103155
});
@@ -143,19 +195,26 @@ function describeParseTransactionWith(
143195
});
144196
}
145197

146-
describe('parsePsbt', function () {
198+
describe('parseTransaction', function () {
147199
utxolib.testutil.AcidTest.suite().forEach((test) => {
148-
// Default case: infer recipients from explanation
149-
describeParseTransactionWith(test, {
200+
// Default case: psbt format, infer recipients from explanation
201+
describeParseTransactionWith(test, 'default', {
150202
txParams: 'inferFromExplanation',
151203
expectedExplicitExternalSpendAmount: 2700n,
152204
expectedImplicitExternalSpendAmount: 0n,
153205
});
154206

155207
if (test.network === utxolib.networks.bitcoin) {
156208
// extended test suite for bitcoin
157-
describeParseTransactionWith(test, {
158-
label: 'empty recipients',
209+
210+
describeParseTransactionWith(test, 'legacy', {
211+
txFormat: 'legacy',
212+
txParams: 'inferFromExplanation',
213+
expectedExplicitExternalSpendAmount: 2700n,
214+
expectedImplicitExternalSpendAmount: 0n,
215+
});
216+
217+
describeParseTransactionWith(test, 'empty recipients', {
159218
txParams: {
160219
recipients: [],
161220
changeAddress: undefined,

0 commit comments

Comments
 (0)