Skip to content

Commit 298591d

Browse files
refactor(abstract-utxo): split transaction.ts into multiple files
Issue: BTC-1450
1 parent 4c06e7e commit 298591d

File tree

4 files changed

+139
-130
lines changed

4 files changed

+139
-130
lines changed

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

Lines changed: 7 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,16 @@
1+
import { bip32, BIP32Interface, bitgo } from '@bitgo/utxo-lib';
12
import * as utxolib from '@bitgo/utxo-lib';
2-
import { BitGoBase, IRequestTracer, Triple } from '@bitgo/sdk-core';
3+
import { Triple } from '@bitgo/sdk-core';
4+
35
import {
4-
AbstractUtxoCoin,
56
DecoratedExplainTransactionOptions,
6-
WalletOutput,
77
ExplainTransactionOptions,
8-
TransactionExplanation,
9-
TransactionPrebuild,
108
Output,
11-
} from './abstractUtxoCoin';
12-
import { bip32, BIP32Interface, bitgo } from '@bitgo/utxo-lib';
13-
14-
const ScriptRecipientPrefix = 'scriptPubKey:';
15-
16-
/**
17-
* Check if the address is a script recipient (starts with `scriptPubKey:`).
18-
* @param address
19-
*/
20-
export function isScriptRecipient(address: string): boolean {
21-
return address.toLowerCase().startsWith(ScriptRecipientPrefix.toLowerCase());
22-
}
23-
24-
/**
25-
* An extended address is one that encodes either a regular address or a hex encoded script with the prefix `scriptPubKey:`.
26-
* This function converts the extended address format to either a script or an address.
27-
* @param extendedAddress
28-
*/
29-
export function fromExtendedAddressFormat(extendedAddress: string): { address: string } | { script: string } {
30-
if (isScriptRecipient(extendedAddress)) {
31-
return { script: extendedAddress.slice(ScriptRecipientPrefix.length) };
32-
}
33-
return { address: extendedAddress };
34-
}
35-
36-
/**
37-
* Convert a script or address to the extended address format.
38-
* @param script
39-
* @param network
40-
* @returns if the script is an OP_RETURN script, then it will be prefixed with `scriptPubKey:`, otherwise it will be converted to an address.
41-
*/
42-
export function toExtendedAddressFormat(script: Buffer, network: utxolib.Network): string {
43-
return script[0] === utxolib.opcodes.OP_RETURN
44-
? `${ScriptRecipientPrefix}${script.toString('hex')}`
45-
: utxolib.address.fromOutputScript(script, network);
46-
}
47-
48-
export function assertValidTransactionRecipient(output: { amount: bigint | number | string; address?: string }): void {
49-
// In the case that this is an OP_RETURN output or another non-encodable scriptPubkey, we dont have an address.
50-
// We will verify that the amount is zero, and if it isnt then we will throw an error.
51-
if (!output.address || isScriptRecipient(output.address)) {
52-
if (output.amount.toString() !== '0') {
53-
throw new Error(`Only zero amounts allowed for non-encodeable scriptPubkeys: ${JSON.stringify(output)}`);
54-
}
55-
}
56-
}
57-
58-
/**
59-
* Get the inputs for a psbt from a prebuild.
60-
*/
61-
export function getPsbtTxInputs(
62-
psbtArg: string | utxolib.bitgo.UtxoPsbt,
63-
network: utxolib.Network
64-
): { address: string; value: bigint; valueString: string }[] {
65-
const psbt = psbtArg instanceof utxolib.bitgo.UtxoPsbt ? psbtArg : utxolib.bitgo.createPsbtFromHex(psbtArg, network);
66-
const txInputs = psbt.txInputs;
67-
return psbt.data.inputs.map((input, index) => {
68-
let address: string;
69-
let value: bigint;
70-
if (input.witnessUtxo) {
71-
address = utxolib.address.fromOutputScript(input.witnessUtxo.script, network);
72-
value = input.witnessUtxo.value;
73-
} else if (input.nonWitnessUtxo) {
74-
const tx = utxolib.bitgo.createTransactionFromBuffer<bigint>(input.nonWitnessUtxo, network, {
75-
amountType: 'bigint',
76-
});
77-
const txId = (Buffer.from(txInputs[index].hash).reverse() as Buffer).toString('hex');
78-
if (tx.getId() !== txId) {
79-
throw new Error('input transaction hex does not match id');
80-
}
81-
const prevTxOutputIndex = txInputs[index].index;
82-
address = utxolib.address.fromOutputScript(tx.outs[prevTxOutputIndex].script, network);
83-
value = tx.outs[prevTxOutputIndex].value;
84-
} else {
85-
throw new Error('psbt input is missing both witnessUtxo and nonWitnessUtxo');
86-
}
87-
return { address, value, valueString: value.toString() };
88-
});
89-
}
9+
TransactionExplanation,
10+
WalletOutput,
11+
} from '../abstractUtxoCoin';
9012

91-
/**
92-
* Get the inputs for a transaction from a prebuild.
93-
*/
94-
export async function getTxInputs<TNumber extends number | bigint>(params: {
95-
txPrebuild: TransactionPrebuild<TNumber>;
96-
bitgo: BitGoBase;
97-
coin: AbstractUtxoCoin;
98-
disableNetworking: boolean;
99-
reqId?: IRequestTracer;
100-
}): Promise<{ address: string; value: TNumber; valueString: string }[]> {
101-
const { txPrebuild, bitgo, coin, disableNetworking, reqId } = params;
102-
if (!txPrebuild.txHex) {
103-
throw new Error(`txPrebuild.txHex not set`);
104-
}
105-
const transaction = coin.createTransactionFromHex<TNumber>(txPrebuild.txHex);
106-
const transactionCache = {};
107-
return await Promise.all(
108-
transaction.ins.map(async (currentInput): Promise<{ address: string; value: TNumber; valueString: string }> => {
109-
const transactionId = (Buffer.from(currentInput.hash).reverse() as Buffer).toString('hex');
110-
const txHex = txPrebuild.txInfo?.txHexes?.[transactionId];
111-
if (txHex) {
112-
const localTx = coin.createTransactionFromHex<TNumber>(txHex);
113-
if (localTx.getId() !== transactionId) {
114-
throw new Error('input transaction hex does not match id');
115-
}
116-
const currentOutput = localTx.outs[currentInput.index];
117-
const address = utxolib.address.fromOutputScript(currentOutput.script, coin.network);
118-
return {
119-
address,
120-
value: currentOutput.value,
121-
valueString: currentOutput.value.toString(),
122-
};
123-
} else if (!transactionCache[transactionId]) {
124-
if (disableNetworking) {
125-
throw new Error('attempting to retrieve transaction details externally with networking disabled');
126-
}
127-
if (reqId) {
128-
bitgo.setRequestTracer(reqId);
129-
}
130-
transactionCache[transactionId] = await bitgo.get(coin.url(`/public/tx/${transactionId}`)).result();
131-
}
132-
const transactionDetails = transactionCache[transactionId];
133-
return transactionDetails.outputs[currentInput.index];
134-
})
135-
);
136-
}
13+
import { toExtendedAddressFormat } from './recipient';
13714

13815
function explainCommon<TNumber extends number | bigint>(
13916
tx: bitgo.UtxoTransaction<TNumber>,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { BitGoBase, IRequestTracer } from '@bitgo/sdk-core';
3+
4+
import { AbstractUtxoCoin, TransactionPrebuild } from '../abstractUtxoCoin';
5+
6+
/**
7+
* Get the inputs for a psbt from a prebuild.
8+
*/
9+
export function getPsbtTxInputs(
10+
psbtArg: string | utxolib.bitgo.UtxoPsbt,
11+
network: utxolib.Network
12+
): { address: string; value: bigint; valueString: string }[] {
13+
const psbt = psbtArg instanceof utxolib.bitgo.UtxoPsbt ? psbtArg : utxolib.bitgo.createPsbtFromHex(psbtArg, network);
14+
const txInputs = psbt.txInputs;
15+
return psbt.data.inputs.map((input, index) => {
16+
let address: string;
17+
let value: bigint;
18+
if (input.witnessUtxo) {
19+
address = utxolib.address.fromOutputScript(input.witnessUtxo.script, network);
20+
value = input.witnessUtxo.value;
21+
} else if (input.nonWitnessUtxo) {
22+
const tx = utxolib.bitgo.createTransactionFromBuffer<bigint>(input.nonWitnessUtxo, network, {
23+
amountType: 'bigint',
24+
});
25+
const txId = (Buffer.from(txInputs[index].hash).reverse() as Buffer).toString('hex');
26+
if (tx.getId() !== txId) {
27+
throw new Error('input transaction hex does not match id');
28+
}
29+
const prevTxOutputIndex = txInputs[index].index;
30+
address = utxolib.address.fromOutputScript(tx.outs[prevTxOutputIndex].script, network);
31+
value = tx.outs[prevTxOutputIndex].value;
32+
} else {
33+
throw new Error('psbt input is missing both witnessUtxo and nonWitnessUtxo');
34+
}
35+
return { address, value, valueString: value.toString() };
36+
});
37+
}
38+
39+
/**
40+
* Get the inputs for a transaction from a prebuild.
41+
*/
42+
export async function getTxInputs<TNumber extends number | bigint>(params: {
43+
txPrebuild: TransactionPrebuild<TNumber>;
44+
bitgo: BitGoBase;
45+
coin: AbstractUtxoCoin;
46+
disableNetworking: boolean;
47+
reqId?: IRequestTracer;
48+
}): Promise<{ address: string; value: TNumber; valueString: string }[]> {
49+
const { txPrebuild, bitgo, coin, disableNetworking, reqId } = params;
50+
if (!txPrebuild.txHex) {
51+
throw new Error(`txPrebuild.txHex not set`);
52+
}
53+
const transaction = coin.createTransactionFromHex<TNumber>(txPrebuild.txHex);
54+
const transactionCache = {};
55+
return await Promise.all(
56+
transaction.ins.map(async (currentInput): Promise<{ address: string; value: TNumber; valueString: string }> => {
57+
const transactionId = (Buffer.from(currentInput.hash).reverse() as Buffer).toString('hex');
58+
const txHex = txPrebuild.txInfo?.txHexes?.[transactionId];
59+
if (txHex) {
60+
const localTx = coin.createTransactionFromHex<TNumber>(txHex);
61+
if (localTx.getId() !== transactionId) {
62+
throw new Error('input transaction hex does not match id');
63+
}
64+
const currentOutput = localTx.outs[currentInput.index];
65+
const address = utxolib.address.fromOutputScript(currentOutput.script, coin.network);
66+
return {
67+
address,
68+
value: currentOutput.value,
69+
valueString: currentOutput.value.toString(),
70+
};
71+
} else if (!transactionCache[transactionId]) {
72+
if (disableNetworking) {
73+
throw new Error('attempting to retrieve transaction details externally with networking disabled');
74+
}
75+
if (reqId) {
76+
bitgo.setRequestTracer(reqId);
77+
}
78+
transactionCache[transactionId] = await bitgo.get(coin.url(`/public/tx/${transactionId}`)).result();
79+
}
80+
const transactionDetails = transactionCache[transactionId];
81+
return transactionDetails.outputs[currentInput.index];
82+
})
83+
);
84+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './recipient';
2+
export * from './explainTransaction';
3+
export * from './fetchInputs';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
3+
const ScriptRecipientPrefix = 'scriptPubKey:';
4+
5+
/**
6+
* Check if the address is a script recipient (starts with `scriptPubKey:`).
7+
* @param address
8+
*/
9+
export function isScriptRecipient(address: string): boolean {
10+
return address.toLowerCase().startsWith(ScriptRecipientPrefix.toLowerCase());
11+
}
12+
13+
/**
14+
* An extended address is one that encodes either a regular address or a hex encoded script with the prefix `scriptPubKey:`.
15+
* This function converts the extended address format to either a script or an address.
16+
* @param extendedAddress
17+
*/
18+
export function fromExtendedAddressFormat(extendedAddress: string): { address: string } | { script: string } {
19+
if (isScriptRecipient(extendedAddress)) {
20+
return { script: extendedAddress.slice(ScriptRecipientPrefix.length) };
21+
}
22+
return { address: extendedAddress };
23+
}
24+
25+
/**
26+
* Convert a script or address to the extended address format.
27+
* @param script
28+
* @param network
29+
* @returns if the script is an OP_RETURN script, then it will be prefixed with `scriptPubKey:`, otherwise it will be converted to an address.
30+
*/
31+
export function toExtendedAddressFormat(script: Buffer, network: utxolib.Network): string {
32+
return script[0] === utxolib.opcodes.OP_RETURN
33+
? `${ScriptRecipientPrefix}${script.toString('hex')}`
34+
: utxolib.address.fromOutputScript(script, network);
35+
}
36+
37+
export function assertValidTransactionRecipient(output: { amount: bigint | number | string; address?: string }): void {
38+
// In the case that this is an OP_RETURN output or another non-encodable scriptPubkey, we dont have an address.
39+
// We will verify that the amount is zero, and if it isnt then we will throw an error.
40+
if (!output.address || isScriptRecipient(output.address)) {
41+
if (output.amount.toString() !== '0') {
42+
throw new Error(`Only zero amounts allowed for non-encodeable scriptPubkeys: ${JSON.stringify(output)}`);
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)