Skip to content

Commit dcac55e

Browse files
chore(abstract-utxo): address review comments
TICKET: BTC-1582
1 parent 04f8518 commit dcac55e

File tree

2 files changed

+60
-31
lines changed

2 files changed

+60
-31
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,15 @@ import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
7676
import { isReplayProtectionUnspent } from './replayProtection';
7777
import { signAndVerifyPsbt, signAndVerifyWalletTransaction } from './sign';
7878
import { supportedCrossChainRecoveries } from './config';
79-
import { explainPsbt, explainTx, getPsbtTxInputs, getTxInputs } from './transaction';
79+
import {
80+
assertValidTransactionRecipient,
81+
explainPsbt,
82+
explainTx,
83+
fromExtendedAddressFormat,
84+
getPsbtTxInputs,
85+
getTxInputs,
86+
isExtendedAddressFormat,
87+
} from './transaction';
8088
import { assertDescriptorWalletAddress } from './descriptor/assertDescriptorWalletAddress';
8189

8290
type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
@@ -104,7 +112,6 @@ type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
104112
}): Promise<SignedTransaction>;
105113
};
106114

107-
export const ScriptRecipientPrefix = 'scriptPubkey:';
108115
const { getExternalChainCode, isChainCode, scriptTypeForChain, outputScripts } = bitgo;
109116
type Unspent<TNumber extends number | bigint = number> = bitgo.Unspent<TNumber>;
110117

@@ -453,11 +460,8 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
453460
params.recipients =
454461
params.recipients instanceof Array
455462
? params?.recipients?.map((recipient) => {
456-
if (recipient.address.startsWith(ScriptRecipientPrefix)) {
457-
const { address, ...rest } = recipient;
458-
return { ...rest, script: address.replace(ScriptRecipientPrefix, '') };
459-
}
460-
return recipient;
463+
const { address, ...rest } = recipient;
464+
return { ...rest, ...fromExtendedAddressFormat(address) };
461465
})
462466
: params.recipients;
463467
}
@@ -478,12 +482,8 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
478482
}
479483

480484
checkRecipient(recipient: { address: string; amount: number | string }): void {
481-
if (recipient.address.startsWith(ScriptRecipientPrefix)) {
482-
const amount = BigInt(recipient.amount);
483-
if (amount !== BigInt(0)) {
484-
throw new Error('Only zero amounts allowed for non-encodeable scriptPubkeys');
485-
}
486-
} else {
485+
assertValidTransactionRecipient(recipient);
486+
if (!isExtendedAddressFormat(recipient.address)) {
487487
super.checkRecipient(recipient);
488488
}
489489
}
@@ -540,6 +540,18 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
540540
return utxolib.bitgo.createTransactionFromHex<TNumber>(hex, this.network, this.amountType);
541541
}
542542

543+
toCanonicalTransactionRecipient(output: { valueString: string; address?: string }): {
544+
amount: bigint;
545+
address?: string;
546+
} {
547+
const amount = BigInt(output.valueString);
548+
assertValidTransactionRecipient({ amount, address: output.address });
549+
if (!output.address) {
550+
return { amount };
551+
}
552+
return { amount, address: this.canonicalAddress(output.address) };
553+
}
554+
543555
/**
544556
* Extract and fill transaction details such as internal/change spend, external spend (explicit vs. implicit), etc.
545557
* @param params
@@ -602,15 +614,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
602614
if (output.wallet === wallet.id()) {
603615
return [];
604616
}
605-
// In the case that this is an OP_RETURN output or another non-encodable scriptPubkey, we dont have an address.
606-
// We will verify that the amount is zero, and if it isnt then we will throw an error.
607-
if (!output.address) {
608-
if (output.valueString !== '0') {
609-
throw new Error(`Only zero amounts allowed for non-encodeable scriptPubkeys: ${JSON.stringify(output)}`);
610-
}
611-
return [{ amount: BigInt(0) }];
612-
}
613-
return [{ amount: BigInt(output.valueString), address: this.canonicalAddress(output.address) }];
617+
return [this.toCanonicalTransactionRecipient(output)];
614618
}
615619
);
616620
} else {

modules/abstract-utxo/src/transaction.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,42 @@ import {
88
TransactionExplanation,
99
TransactionPrebuild,
1010
Output,
11-
ScriptRecipientPrefix,
1211
} from './abstractUtxoCoin';
1312
import { bip32, BIP32Interface, bitgo } from '@bitgo/utxo-lib';
1413

15-
// https://github.com/bitcoin/bitcoin/blob/5961b23898ee7c0af2626c46d5d70e80136578d3/src/script/script.h#L47
16-
const OP_RETURN_IDENTIFIER = Buffer.from('6a', 'hex');
14+
const ScriptRecipientPrefix = 'scriptPubkey:';
15+
16+
/**
17+
* An extended address is one that encodes either a regular address or a hex encoded script with the prefix `scriptPubkey:`.
18+
* This function converts the extended address format to either a script or an address.
19+
* @param extendedAddress
20+
*/
21+
export function fromExtendedAddressFormat(extendedAddress: string): { address: string } | { script: string } {
22+
if (extendedAddress.startsWith(ScriptRecipientPrefix)) {
23+
return { script: extendedAddress.replace(ScriptRecipientPrefix, '') };
24+
}
25+
return { address: extendedAddress };
26+
}
27+
28+
export function toExtendedAddressFormat(script: Buffer, network: utxolib.Network): string {
29+
return script[0] === utxolib.opcodes.OP_RETURN
30+
? `${ScriptRecipientPrefix}${script.toString('hex')}`
31+
: utxolib.address.fromOutputScript(script, network);
32+
}
33+
34+
export function isExtendedAddressFormat(address: string): boolean {
35+
return address.startsWith(ScriptRecipientPrefix);
36+
}
37+
38+
export function assertValidTransactionRecipient(output: { amount: bigint | number | string; address?: string }): void {
39+
// In the case that this is an OP_RETURN output or another non-encodable scriptPubkey, we dont have an address.
40+
// We will verify that the amount is zero, and if it isnt then we will throw an error.
41+
if (!output.address || output.address.startsWith(ScriptRecipientPrefix)) {
42+
if (output.amount.toString() !== '0') {
43+
throw new Error(`Only zero amounts allowed for non-encodeable scriptPubkeys: ${JSON.stringify(output)}`);
44+
}
45+
}
46+
}
1747

1848
/**
1949
* Get the inputs for a psbt from a prebuild.
@@ -112,12 +142,7 @@ function explainCommon<TNumber extends number | bigint>(
112142
tx.outs.forEach((currentOutput) => {
113143
// Try to encode the script pubkey with an address. If it fails, try to parse it as an OP_RETURN output with the prefix.
114144
// If that fails, then it is an unrecognized scriptPubkey and should fail
115-
let currentAddress: string;
116-
if (currentOutput.script.subarray(0, 1).equals(OP_RETURN_IDENTIFIER)) {
117-
currentAddress = `${ScriptRecipientPrefix}${currentOutput.script.toString('hex')}`;
118-
} else {
119-
currentAddress = utxolib.address.fromOutputScript(currentOutput.script, network);
120-
}
145+
const currentAddress = toExtendedAddressFormat(currentOutput.script, network);
121146
const currentAmount = BigInt(currentOutput.value);
122147

123148
if (changeAddresses.includes(currentAddress)) {

0 commit comments

Comments
 (0)