Skip to content

Commit f3ecc52

Browse files
refactor(abstract-utxo): delete unreachable utxolib explainPsbt path
The utxolib variant of explainPsbt is no longer called from any src file (transaction/explainTransaction.ts dispatches only to explainPsbtWasm). Delete it along with helpers that became dead, and migrate the few test callers to explainPsbtWasm. Refs: T1-3279
1 parent 4242ff3 commit f3ecc52

5 files changed

Lines changed: 32 additions & 445 deletions

File tree

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export function explainTx<TNumber extends number | bigint>(
2323
pubs?: string[];
2424
customChangeXpubs?: Triple<string>;
2525
txInfo?: { unspents?: Unspent<TNumber>[] };
26-
changeInfo?: fixedScript.ChangeAddressInfo[];
2726
},
2827
coinName: UtxoCoinName
2928
): TransactionExplanationUtxolibPsbt | TransactionExplanationWasm {
Lines changed: 1 addition & 352 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
1-
import * as utxolib from '@bitgo/utxo-lib';
2-
import { bip322 } from '@bitgo/utxo-core';
3-
import { bitgo } from '@bitgo/utxo-lib';
4-
import { ITransactionExplanation as BaseTransactionExplanation, Triple } from '@bitgo/sdk-core';
5-
import { BIP32 } from '@bitgo/wasm-utxo';
6-
import * as utxocore from '@bitgo/utxo-core';
1+
import { ITransactionExplanation as BaseTransactionExplanation } from '@bitgo/sdk-core';
72

83
import type { Bip322Message } from '../../abstractUtxoCoin';
94
import type { Output, FixedScriptWalletOutput } from '../types';
10-
import { toExtendedAddressFormat } from '../recipient';
11-
import { getPayGoVerificationPubkey } from '../getPayGoVerificationPubkey';
12-
import { toBip32Triple } from '../../keychains';
13-
import { toUtxolibBIP32 } from '../../wasmUtil';
14-
import { getNetworkFromCoinName, UtxoCoinName } from '../../names';
155

166
// ===== Transaction Explanation Type Definitions =====
177

@@ -61,344 +51,3 @@ export type TransactionExplanationUtxolibPsbt = TransactionExplanationWithSignat
6151
export type TransactionExplanationDescriptor = TransactionExplanationWithSignatures<string, Output>;
6252

6353
export type TransactionExplanation = TransactionExplanationUtxolibPsbt | TransactionExplanationWasm;
64-
65-
export type ChangeAddressInfo = {
66-
address: string;
67-
chain: number;
68-
index: number;
69-
};
70-
71-
function toChangeOutput(
72-
txOutput: utxolib.TxOutput<number | bigint>,
73-
coinName: UtxoCoinName,
74-
changeInfo: ChangeAddressInfo[] | undefined
75-
): FixedScriptWalletOutput | undefined {
76-
if (!changeInfo) {
77-
return undefined;
78-
}
79-
const address = toExtendedAddressFormat(txOutput.script, coinName);
80-
const change = changeInfo.find((change) => change.address === address);
81-
if (!change) {
82-
return undefined;
83-
}
84-
return {
85-
address,
86-
amount: txOutput.value.toString(),
87-
chain: change.chain,
88-
index: change.index,
89-
external: false,
90-
};
91-
}
92-
93-
function outputSum(outputs: { amount: string | number }[]): bigint {
94-
return outputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
95-
}
96-
97-
function explainCommon<TNumber extends number | bigint>(
98-
tx: bitgo.UtxoTransaction<TNumber>,
99-
params: {
100-
changeInfo?: ChangeAddressInfo[];
101-
customChangeInfo?: ChangeAddressInfo[];
102-
feeInfo?: string;
103-
},
104-
coinName: UtxoCoinName
105-
) {
106-
const displayOrder = ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs'];
107-
const changeOutputs: FixedScriptWalletOutput[] = [];
108-
const customChangeOutputs: FixedScriptWalletOutput[] = [];
109-
const externalOutputs: Output[] = [];
110-
111-
const { changeInfo, customChangeInfo } = params;
112-
113-
tx.outs.forEach((currentOutput) => {
114-
// Try to encode the script pubkey with an address. If it fails, try to parse it as an OP_RETURN output with the prefix.
115-
// If that fails, then it is an unrecognized scriptPubkey and should fail
116-
const currentAddress = toExtendedAddressFormat(currentOutput.script, coinName);
117-
const currentAmount = BigInt(currentOutput.value);
118-
119-
const changeOutput = toChangeOutput(currentOutput, coinName, changeInfo);
120-
if (changeOutput) {
121-
changeOutputs.push(changeOutput);
122-
return;
123-
}
124-
125-
const customChangeOutput = toChangeOutput(currentOutput, coinName, customChangeInfo);
126-
if (customChangeOutput) {
127-
customChangeOutputs.push(customChangeOutput);
128-
return;
129-
}
130-
131-
externalOutputs.push({
132-
address: currentAddress,
133-
amount: currentAmount.toString(),
134-
// If changeInfo has a length greater than or equal to zero, it means that the change information
135-
// was provided to the function but the output was not identified as change. In this case,
136-
// the output is external, and we can set it as so. If changeInfo is undefined, it means we were
137-
// given no information about change outputs, so we can't determine anything about the output,
138-
// so we leave it undefined.
139-
external: changeInfo ? true : undefined,
140-
});
141-
});
142-
143-
const outputDetails = {
144-
outputs: externalOutputs,
145-
outputAmount: outputSum(externalOutputs).toString(),
146-
147-
changeOutputs,
148-
changeAmount: outputSum(changeOutputs).toString(),
149-
150-
customChangeAmount: outputSum(customChangeOutputs).toString(),
151-
customChangeOutputs,
152-
};
153-
154-
let fee: string | undefined;
155-
let locktime: number | undefined;
156-
157-
if (params.feeInfo) {
158-
displayOrder.push('fee');
159-
fee = params.feeInfo;
160-
}
161-
162-
if (Number.isInteger(tx.locktime) && tx.locktime > 0) {
163-
displayOrder.push('locktime');
164-
locktime = tx.locktime;
165-
}
166-
167-
return { displayOrder, id: tx.getId(), ...outputDetails, fee, locktime };
168-
}
169-
170-
function getRootWalletKeys(params: { pubs?: bitgo.RootWalletKeys | string[] }): bitgo.RootWalletKeys | undefined {
171-
if (params.pubs instanceof bitgo.RootWalletKeys) {
172-
return params.pubs;
173-
}
174-
const keys = params.pubs?.map((xpub) => toUtxolibBIP32(BIP32.fromBase58(xpub)));
175-
return keys && keys.length === 3 ? new bitgo.RootWalletKeys(keys as Triple<utxolib.BIP32Interface>) : undefined;
176-
}
177-
178-
function getPsbtInputSignaturesCount(
179-
psbt: bitgo.UtxoPsbt,
180-
params: {
181-
pubs?: bitgo.RootWalletKeys | string[];
182-
}
183-
) {
184-
const rootWalletKeys = getRootWalletKeys(params);
185-
return rootWalletKeys
186-
? bitgo.getSignatureValidationArrayPsbt(psbt, rootWalletKeys).map((sv) => sv[1].filter((v) => v).length)
187-
: (Array(psbt.data.inputs.length) as number[]).fill(0);
188-
}
189-
190-
function getChainAndIndexFromBip32Derivations(output: bitgo.PsbtOutput) {
191-
const derivations = output.bip32Derivation ?? output.tapBip32Derivation ?? undefined;
192-
if (!derivations) {
193-
return undefined;
194-
}
195-
const paths = derivations.map((d) => d.path);
196-
if (!paths || paths.length !== 3) {
197-
throw new Error('expected 3 paths in bip32Derivation or tapBip32Derivation');
198-
}
199-
if (!paths.every((p) => paths[0] === p)) {
200-
throw new Error('expected all paths to be the same');
201-
}
202-
203-
paths.forEach((path) => {
204-
if (paths[0] !== path) {
205-
throw new Error(
206-
'Unable to get a single chain and index on the output because there are different paths for different keys'
207-
);
208-
}
209-
});
210-
return utxolib.bitgo.getChainAndIndexFromPath(paths[0]);
211-
}
212-
213-
function getChangeInfo(
214-
psbt: bitgo.UtxoPsbt,
215-
walletKeys?: Triple<BIP32> | Triple<utxolib.BIP32Interface>
216-
): ChangeAddressInfo[] | undefined {
217-
let utxolibKeys: Triple<utxolib.BIP32Interface>;
218-
try {
219-
utxolibKeys = walletKeys
220-
? (walletKeys.map((k) => toUtxolibBIP32(k)) as Triple<utxolib.BIP32Interface>)
221-
: utxolib.bitgo.getSortedRootNodes(psbt);
222-
} catch (e) {
223-
if (e instanceof utxolib.bitgo.ErrorNoMultiSigInputFound) {
224-
return undefined;
225-
}
226-
throw e;
227-
}
228-
229-
return utxolib.bitgo.findWalletOutputIndices(psbt, utxolibKeys).map((i) => {
230-
const derivationInformation = getChainAndIndexFromBip32Derivations(psbt.data.outputs[i]);
231-
if (!derivationInformation) {
232-
throw new Error('could not find derivation information on bip32Derivation or tapBip32Derivation');
233-
}
234-
return {
235-
address: utxolib.address.fromOutputScript(psbt.txOutputs[i].script, psbt.network),
236-
external: false,
237-
...derivationInformation,
238-
};
239-
});
240-
}
241-
242-
/**
243-
* Extract PayGo address proof information from the PSBT if present
244-
* @returns Information about the PayGo proof, including the output index and address
245-
*/
246-
function getPayGoVerificationInfo(
247-
psbt: bitgo.UtxoPsbt,
248-
coinName: UtxoCoinName
249-
): { outputIndex: number; verificationPubkey: string } | undefined {
250-
let outputIndex: number | undefined = undefined;
251-
let address: string | undefined = undefined;
252-
// Check if this PSBT has any PayGo address proofs
253-
if (!utxocore.paygo.psbtOutputIncludesPaygoAddressProof(psbt)) {
254-
return undefined;
255-
}
256-
257-
// This pulls the pubkey depending on given network
258-
const verificationPubkey = getPayGoVerificationPubkey(coinName);
259-
// find which output index that contains the PayGo proof
260-
outputIndex = utxocore.paygo.getPayGoAddressProofOutputIndex(psbt);
261-
if (outputIndex === undefined || !verificationPubkey) {
262-
return undefined;
263-
}
264-
const network = getNetworkFromCoinName(coinName);
265-
const output = psbt.txOutputs[outputIndex];
266-
address = utxolib.address.fromOutputScript(output.script, network);
267-
if (!address) {
268-
throw new Error(`Can not derive address ${address} Pay Go Attestation.`);
269-
}
270-
271-
return { outputIndex, verificationPubkey };
272-
}
273-
274-
/**
275-
* Extract the BIP322 messages and addresses from the PSBT inputs and perform
276-
* verification on the transaction to ensure that it meets the BIP322 requirements.
277-
* @returns An array of objects containing the message and address for each input,
278-
* or undefined if no BIP322 messages are found.
279-
*/
280-
function getBip322MessageInfoAndVerify(psbt: bitgo.UtxoPsbt, coinName: UtxoCoinName): Bip322Message[] | undefined {
281-
const network = getNetworkFromCoinName(coinName);
282-
const bip322Messages: { message: string; address: string }[] = [];
283-
for (let i = 0; i < psbt.data.inputs.length; i++) {
284-
const message = bip322.getBip322ProofMessageAtIndex(psbt, i);
285-
if (message) {
286-
const input = psbt.data.inputs[i];
287-
if (!input.witnessUtxo) {
288-
throw new Error(`Missing witnessUtxo for input index ${i}`);
289-
}
290-
const scriptPubKey = input.witnessUtxo.script;
291-
292-
// Verify that the toSpend transaction can be recreated in the PSBT and is encoded correctly in the nonWitnessUtxo
293-
const toSpend = bip322.buildToSpendTransaction(scriptPubKey, message);
294-
295-
// Verify that the toSpend transaction ID matches the input's referenced transaction ID
296-
if (toSpend.getId() !== utxolib.bitgo.getOutputIdForInput(psbt.txInputs[i]).txid) {
297-
throw new Error(`ToSpend transaction ID does not match the input at index ${i}`);
298-
}
299-
300-
// Verify the input specifics
301-
if (psbt.txInputs[i].sequence !== 0) {
302-
throw new Error(`Unexpected sequence number at input index ${i}: ${psbt.txInputs[i].sequence}. Expected 0.`);
303-
}
304-
if (psbt.txInputs[i].index !== 0) {
305-
throw new Error(`Unexpected input index at position ${i}: ${psbt.txInputs[i].index}. Expected 0.`);
306-
}
307-
308-
bip322Messages.push({
309-
message: message.toString('utf8'),
310-
address: utxolib.address.fromOutputScript(scriptPubKey, network),
311-
});
312-
}
313-
}
314-
315-
if (bip322Messages.length > 0) {
316-
// If there is a BIP322 message in any input, all inputs must have one.
317-
if (bip322Messages.length !== psbt.data.inputs.length) {
318-
throw new Error('Inconsistent BIP322 messages across inputs.');
319-
}
320-
321-
// Verify the transaction specifics for BIP322
322-
if (psbt.version !== 0 && psbt.version !== 2) {
323-
throw new Error(`Unsupported PSBT version for BIP322: ${psbt.version}. Expected 0 `);
324-
}
325-
if (
326-
psbt.data.outputs.length !== 1 ||
327-
psbt.txOutputs[0].script.toString('hex') !== '6a' ||
328-
psbt.txOutputs[0].value !== 0n
329-
) {
330-
throw new Error(`Invalid PSBT outputs for BIP322. Expected exactly one OP_RETURN output with zero value.`);
331-
}
332-
333-
return bip322Messages;
334-
}
335-
336-
return undefined;
337-
}
338-
339-
/**
340-
* Decompose a raw psbt into useful information, such as the total amounts,
341-
* change amounts, and transaction outputs.
342-
*
343-
* @param psbt {bitgo.UtxoPsbt} The PSBT to explain
344-
* @param pubs {bitgo.RootWalletKeys | string[]} The public keys to use for the explanation
345-
* @param coinName {UtxoCoinName} The coin name to use for the explanation
346-
* @param strict {boolean} Whether to throw an error if the PayGo address proof is invalid
347-
*/
348-
export function explainPsbt(
349-
psbt: bitgo.UtxoPsbt,
350-
params: {
351-
pubs?: bitgo.RootWalletKeys | string[];
352-
customChangePubs?: bitgo.RootWalletKeys | string[];
353-
},
354-
coinName: UtxoCoinName,
355-
{ strict = true }: { strict?: boolean } = {}
356-
): TransactionExplanationUtxolibPsbt {
357-
const network = getNetworkFromCoinName(coinName);
358-
const payGoVerificationInfo = getPayGoVerificationInfo(psbt, coinName);
359-
if (payGoVerificationInfo) {
360-
try {
361-
utxocore.paygo.verifyPayGoAddressProof(
362-
psbt,
363-
payGoVerificationInfo.outputIndex,
364-
Buffer.from(BIP32.fromBase58(payGoVerificationInfo.verificationPubkey).publicKey)
365-
);
366-
} catch (e) {
367-
if (strict) {
368-
throw e;
369-
}
370-
console.error(e);
371-
}
372-
}
373-
374-
const messages = getBip322MessageInfoAndVerify(psbt, coinName);
375-
const changeInfo = getChangeInfo(psbt);
376-
const customChangeInfo = params.customChangePubs
377-
? getChangeInfo(psbt, toBip32Triple(params.customChangePubs))
378-
: undefined;
379-
const tx = psbt.getUnsignedTx();
380-
const common = explainCommon(tx, { ...params, changeInfo, customChangeInfo }, coinName);
381-
const inputSignaturesCount = getPsbtInputSignaturesCount(psbt, params);
382-
383-
// Set fee from subtracting inputs from outputs
384-
const outputAmount = psbt.txOutputs.reduce((cumulative, curr) => cumulative + BigInt(curr.value), BigInt(0));
385-
const inputAmount = psbt.txInputs.reduce((cumulative, txInput, i) => {
386-
const data = psbt.data.inputs[i];
387-
if (data.witnessUtxo) {
388-
return cumulative + BigInt(data.witnessUtxo.value);
389-
} else if (data.nonWitnessUtxo) {
390-
const tx = bitgo.createTransactionFromBuffer<bigint>(data.nonWitnessUtxo, network, { amountType: 'bigint' });
391-
return cumulative + BigInt(tx.outs[txInput.index].value);
392-
} else {
393-
throw new Error('could not find value on input');
394-
}
395-
}, BigInt(0));
396-
397-
return {
398-
...common,
399-
fee: (inputAmount - outputAmount).toString(),
400-
inputSignatures: inputSignaturesCount,
401-
signatures: inputSignaturesCount.reduce((prev, curr) => (curr > prev ? curr : prev), 0),
402-
messages,
403-
};
404-
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export { explainPsbt, ChangeAddressInfo } from './explainTransaction';
21
export {
32
explainPsbtWasm,
43
explainPsbtWasmBigInt,

0 commit comments

Comments
 (0)