Skip to content

Commit 7c908ad

Browse files
Merge pull request #5218 from BitGo/BTC-1450.namespace-fixedscript
refactor(abstract-utxo): namespace fixedScript types and funcs
2 parents 80977da + edaad94 commit 7c908ad

File tree

5 files changed

+244
-231
lines changed

5 files changed

+244
-231
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ import { signAndVerifyPsbt, signAndVerifyWalletTransaction } from './sign';
7676
import { supportedCrossChainRecoveries } from './config';
7777
import {
7878
assertValidTransactionRecipient,
79-
explainPsbt,
8079
explainTx,
8180
fromExtendedAddressFormat,
8281
getPsbtTxInputs,
@@ -133,16 +132,18 @@ export interface BaseOutput {
133132
external?: boolean;
134133
}
135134

136-
export interface WalletOutput extends BaseOutput {
135+
export interface FixedScriptWalletOutput extends BaseOutput {
137136
needsCustomChangeKeySignatureVerification?: boolean;
138137
chain: number;
139138
index: number;
140139
}
141140

142-
export type Output = BaseOutput | WalletOutput;
141+
export type Output = BaseOutput | FixedScriptWalletOutput;
143142

144-
export function isWalletOutput(output: Output): output is WalletOutput {
145-
return (output as WalletOutput).chain !== undefined && (output as WalletOutput).index !== undefined;
143+
export function isWalletOutput(output: Output): output is FixedScriptWalletOutput {
144+
return (
145+
(output as FixedScriptWalletOutput).chain !== undefined && (output as FixedScriptWalletOutput).index !== undefined
146+
);
146147
}
147148

148149
export interface TransactionExplanation extends BaseTransactionExplanation<string, string> {
@@ -746,7 +747,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
746747
);
747748

748749
const needsCustomChangeKeySignatureVerification = allOutputDetails.some(
749-
(output) => (output as WalletOutput)?.needsCustomChangeKeySignatureVerification
750+
(output) => (output as FixedScriptWalletOutput)?.needsCustomChangeKeySignatureVerification
750751
);
751752

752753
const changeOutputs = _.filter(allOutputDetails, { external: false });
@@ -1535,12 +1536,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
15351536
if (typeof txHex !== 'string' || !txHex.match(/^([a-f0-9]{2})+$/i)) {
15361537
throw new Error('invalid transaction hex, must be a valid hex string');
15371538
}
1538-
const tx = this.decodeTransaction(txHex);
1539-
if (tx instanceof bitgo.UtxoPsbt) {
1540-
return explainPsbt(tx, params, this.network);
1541-
} else {
1542-
return explainTx(tx, params, this.network);
1543-
}
1539+
return explainTx(this.decodeTransaction(txHex), params, this.network);
15441540
}
15451541

15461542
/**
Lines changed: 8 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -1,231 +1,21 @@
1-
import { bip32, BIP32Interface, bitgo } from '@bitgo/utxo-lib';
21
import * as utxolib from '@bitgo/utxo-lib';
3-
import { Triple } from '@bitgo/sdk-core';
42

5-
import {
6-
DecoratedExplainTransactionOptions,
7-
ExplainTransactionOptions,
8-
Output,
9-
TransactionExplanation,
10-
WalletOutput,
11-
} from '../abstractUtxoCoin';
3+
import { ExplainTransactionOptions, TransactionExplanation } from '../abstractUtxoCoin';
124

13-
import { toExtendedAddressFormat } from './recipient';
14-
15-
function explainCommon<TNumber extends number | bigint>(
16-
tx: bitgo.UtxoTransaction<TNumber>,
17-
params: DecoratedExplainTransactionOptions<TNumber>,
18-
network: utxolib.Network
19-
) {
20-
const displayOrder = ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs'];
21-
let spendAmount = BigInt(0);
22-
let changeAmount = BigInt(0);
23-
const changeOutputs: WalletOutput[] = [];
24-
const outputs: Output[] = [];
25-
26-
const { changeInfo } = params;
27-
const changeAddresses = changeInfo?.map((info) => info.address) ?? [];
28-
29-
tx.outs.forEach((currentOutput) => {
30-
// Try to encode the script pubkey with an address. If it fails, try to parse it as an OP_RETURN output with the prefix.
31-
// If that fails, then it is an unrecognized scriptPubkey and should fail
32-
const currentAddress = toExtendedAddressFormat(currentOutput.script, network);
33-
const currentAmount = BigInt(currentOutput.value);
34-
35-
if (changeAddresses.includes(currentAddress)) {
36-
// this is change
37-
changeAmount += currentAmount;
38-
const change = changeInfo?.find((change) => change.address === currentAddress);
39-
40-
if (!change) {
41-
throw new Error('changeInfo must have change information for all change outputs');
42-
}
43-
changeOutputs.push({
44-
address: currentAddress,
45-
amount: currentAmount.toString(),
46-
chain: change.chain,
47-
index: change.index,
48-
external: false,
49-
});
50-
return;
51-
}
52-
53-
spendAmount += currentAmount;
54-
outputs.push({
55-
address: currentAddress,
56-
amount: currentAmount.toString(),
57-
// If changeInfo has a length greater than or equal to zero, it means that the change information
58-
// was provided to the function but the output was not identified as change. In this case,
59-
// the output is external, and we can set it as so. If changeInfo is undefined, it means we were
60-
// given no information about change outputs, so we can't determine anything about the output,
61-
// so we leave it undefined.
62-
external: changeInfo ? true : undefined,
63-
});
64-
});
65-
66-
const outputDetails = {
67-
outputAmount: spendAmount.toString(),
68-
changeAmount: changeAmount.toString(),
69-
outputs,
70-
changeOutputs,
71-
};
72-
73-
let fee: string | undefined;
74-
let locktime: number | undefined;
75-
76-
if (params.feeInfo) {
77-
displayOrder.push('fee');
78-
fee = params.feeInfo;
79-
}
80-
81-
if (Number.isInteger(tx.locktime) && tx.locktime > 0) {
82-
displayOrder.push('locktime');
83-
locktime = tx.locktime;
84-
}
85-
86-
return { displayOrder, id: tx.getId(), ...outputDetails, fee, locktime };
87-
}
88-
89-
function getRootWalletKeys<TNumber extends number | bigint>(params: ExplainTransactionOptions<TNumber>) {
90-
const keys = params.pubs?.map((xpub) => bip32.fromBase58(xpub));
91-
return keys && keys.length === 3 ? new bitgo.RootWalletKeys(keys as Triple<BIP32Interface>) : undefined;
92-
}
93-
94-
function getPsbtInputSignaturesCount<TNumber extends number | bigint>(
95-
psbt: bitgo.UtxoPsbt,
96-
params: ExplainTransactionOptions<TNumber>
97-
) {
98-
const rootWalletKeys = getRootWalletKeys(params);
99-
return rootWalletKeys
100-
? bitgo.getSignatureValidationArrayPsbt(psbt, rootWalletKeys).map((sv) => sv[1].filter((v) => v).length)
101-
: (Array(psbt.data.inputs.length) as number[]).fill(0);
102-
}
103-
104-
function getTxInputSignaturesCount<TNumber extends number | bigint>(
105-
tx: bitgo.UtxoTransaction<TNumber>,
106-
params: ExplainTransactionOptions<TNumber>,
107-
network: utxolib.Network
108-
) {
109-
const prevOutputs = params.txInfo?.unspents?.map((u) => bitgo.toOutput<TNumber>(u, network));
110-
const rootWalletKeys = getRootWalletKeys(params);
111-
const { unspents = [] } = params.txInfo ?? {};
112-
113-
// get the number of signatures per input
114-
return tx.ins.map((input, idx): number => {
115-
if (unspents.length !== tx.ins.length) {
116-
return 0;
117-
}
118-
if (!prevOutputs) {
119-
throw new Error(`invalid state`);
120-
}
121-
if (!rootWalletKeys) {
122-
// no pub keys or incorrect number of pub keys
123-
return 0;
124-
}
125-
try {
126-
return bitgo.verifySignatureWithUnspent<TNumber>(tx, idx, unspents, rootWalletKeys).filter((v) => v).length;
127-
} catch (e) {
128-
// some other error occurred and we can't validate the signatures
129-
return 0;
130-
}
131-
});
132-
}
133-
134-
/**
135-
* Decompose a raw psbt into useful information, such as the total amounts,
136-
* change amounts, and transaction outputs.
137-
*/
138-
export function explainPsbt<TNumber extends number | bigint, Tx extends bitgo.UtxoTransaction<bigint>>(
139-
psbt: bitgo.UtxoPsbt<Tx>,
140-
params: ExplainTransactionOptions<TNumber>,
141-
network: utxolib.Network
142-
): TransactionExplanation {
143-
const txOutputs = psbt.txOutputs;
144-
145-
function getChainAndIndexFromBip32Derivations(output: bitgo.PsbtOutput) {
146-
const derivations = output.bip32Derivation ?? output.tapBip32Derivation ?? undefined;
147-
if (!derivations) {
148-
return undefined;
149-
}
150-
const paths = derivations.map((d) => d.path);
151-
if (!paths || paths.length !== 3) {
152-
throw new Error('expected 3 paths in bip32Derivation or tapBip32Derivation');
153-
}
154-
if (!paths.every((p) => paths[0] === p)) {
155-
throw new Error('expected all paths to be the same');
156-
}
157-
158-
paths.forEach((path) => {
159-
if (paths[0] !== path) {
160-
throw new Error(
161-
'Unable to get a single chain and index on the output because there are different paths for different keys'
162-
);
163-
}
164-
});
165-
return utxolib.bitgo.getChainAndIndexFromPath(paths[0]);
166-
}
167-
168-
function getChangeInfo() {
169-
try {
170-
return utxolib.bitgo.findInternalOutputIndices(psbt).map((i) => {
171-
const derivationInformation = getChainAndIndexFromBip32Derivations(psbt.data.outputs[i]);
172-
if (!derivationInformation) {
173-
throw new Error('could not find derivation information on bip32Derivation or tapBip32Derivation');
174-
}
175-
return {
176-
address: utxolib.address.fromOutputScript(txOutputs[i].script, network),
177-
external: false,
178-
...derivationInformation,
179-
};
180-
});
181-
} catch (e) {
182-
if (e instanceof utxolib.bitgo.ErrorNoMultiSigInputFound) {
183-
return undefined;
184-
}
185-
throw e;
186-
}
187-
}
188-
const changeInfo = getChangeInfo();
189-
const tx = psbt.getUnsignedTx() as bitgo.UtxoTransaction<TNumber>;
190-
const common = explainCommon(tx, { ...params, txInfo: params.txInfo, changeInfo }, network);
191-
const inputSignaturesCount = getPsbtInputSignaturesCount(psbt, params);
192-
193-
// Set fee from subtracting inputs from outputs
194-
const outputAmount = txOutputs.reduce((cumulative, curr) => cumulative + BigInt(curr.value), BigInt(0));
195-
const inputAmount = psbt.txInputs.reduce((cumulative, txInput, i) => {
196-
const data = psbt.data.inputs[i];
197-
if (data.witnessUtxo) {
198-
return cumulative + BigInt(data.witnessUtxo.value);
199-
} else if (data.nonWitnessUtxo) {
200-
const tx = bitgo.createTransactionFromBuffer<bigint>(data.nonWitnessUtxo, network, { amountType: 'bigint' });
201-
return cumulative + BigInt(tx.outs[txInput.index].value);
202-
} else {
203-
throw new Error('could not find value on input');
204-
}
205-
}, BigInt(0));
206-
207-
return {
208-
...common,
209-
fee: (inputAmount - outputAmount).toString(),
210-
inputSignatures: inputSignaturesCount,
211-
signatures: inputSignaturesCount.reduce((prev, curr) => (curr > prev ? curr : prev), 0),
212-
} as TransactionExplanation;
213-
}
5+
import * as fixedScript from './fixedScript';
2146

2157
/**
2168
* Decompose a raw transaction into useful information, such as the total amounts,
2179
* change amounts, and transaction outputs.
21810
*/
21911
export function explainTx<TNumber extends number | bigint>(
220-
tx: bitgo.UtxoTransaction<TNumber>,
12+
tx: utxolib.bitgo.UtxoTransaction<TNumber> | utxolib.bitgo.UtxoPsbt,
22113
params: ExplainTransactionOptions<TNumber>,
22214
network: utxolib.Network
22315
): TransactionExplanation {
224-
const common = explainCommon(tx, params, network);
225-
const inputSignaturesCount = getTxInputSignaturesCount(tx, params, network);
226-
return {
227-
...common,
228-
inputSignatures: inputSignaturesCount,
229-
signatures: inputSignaturesCount.reduce((prev, curr) => (curr > prev ? curr : prev), 0),
230-
} as TransactionExplanation;
16+
if (tx instanceof utxolib.bitgo.UtxoPsbt) {
17+
return fixedScript.explainPsbt(tx, params, network);
18+
} else {
19+
return fixedScript.explainLegacyTx(tx, params, network);
20+
}
23121
}

0 commit comments

Comments
 (0)