Skip to content

Commit 9e36b9a

Browse files
Merge pull request #5244 from BitGo/BTC-1450.refactor-parseTx
refactor(abstract-utxo): factor fixedScript parseTransaction
2 parents 3735a7d + 254821c commit 9e36b9a

File tree

7 files changed

+285
-219
lines changed

7 files changed

+285
-219
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 18 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import debugLib from 'debug';
88
import BigNumber from 'bignumber.js';
99

1010
import {
11+
backupKeyRecovery,
1112
CrossChainRecoverySigned,
1213
CrossChainRecoveryUnsigned,
1314
forCoin,
1415
recoverCrossChain,
15-
RecoveryProvider,
16-
backupKeyRecovery,
1716
RecoverParams,
18-
V1RecoverParams,
17+
RecoveryProvider,
1918
v1BackupKeyRecovery,
20-
V1SweepParams,
19+
V1RecoverParams,
2120
v1Sweep,
21+
V1SweepParams,
2222
} from './recovery';
2323

2424
import {
@@ -44,19 +44,16 @@ import {
4444
P2trMusig2UnsupportedError,
4545
P2trUnsupportedError,
4646
P2wshUnsupportedError,
47-
ParsedTransaction as BaseParsedTransaction,
4847
ParseTransactionOptions as BaseParseTransactionOptions,
4948
PrecreateBitGoOptions,
5049
PresignTransactionOptions,
51-
promiseProps,
5250
RequestTracer,
5351
sanitizeLegacyPath,
5452
SignedTransaction,
5553
SignTransactionOptions as BaseSignTransactionOptions,
5654
SupplementGenerateWalletOptions,
5755
TransactionParams as BaseTransactionParams,
5856
TransactionPrebuild as BaseTransactionPrebuild,
59-
TransactionRecipient,
6057
Triple,
6158
UnexpectedAddressError,
6259
UnsupportedAddressTypeError,
@@ -66,11 +63,6 @@ import {
6663
Wallet,
6764
WalletData,
6865
} from '@bitgo/sdk-core';
69-
import { CustomChangeOptions, parseOutput } from './parseOutput';
70-
71-
const debug = debugLib('bitgo:v2:utxo');
72-
73-
import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
7466
import { isReplayProtectionUnspent } from './replayProtection';
7567
import { signAndVerifyPsbt, signAndVerifyWalletTransaction } from './sign';
7668
import { supportedCrossChainRecoveries } from './config';
@@ -82,9 +74,15 @@ import {
8274
getTxInputs,
8375
isScriptRecipient,
8476
} from './transaction';
85-
import { assertDescriptorWalletAddress } from './descriptor/assertDescriptorWalletAddress';
77+
import { assertDescriptorWalletAddress } from './descriptor';
8678

8779
import { getChainFromNetwork, getFamilyFromNetwork, getFullNameFromNetwork } from './names';
80+
import { CustomChangeOptions, parseTransaction } from './transaction/fixedScript';
81+
import { NamedKeychains } from './keychains';
82+
83+
const debug = debugLib('bitgo:v2:utxo');
84+
85+
import ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
8886

8987
type UtxoCustomSigningFunction<TNumber extends number | bigint> = {
9088
(params: {
@@ -221,12 +219,8 @@ export interface ParseTransactionOptions<TNumber extends number | bigint = numbe
221219
reqId?: IRequestTracer;
222220
}
223221

224-
export interface ParsedTransaction<TNumber extends number | bigint = number> extends BaseParsedTransaction {
225-
keychains: {
226-
user?: Keychain;
227-
backup?: Keychain;
228-
bitgo?: Keychain;
229-
};
222+
export type ParsedTransaction<TNumber extends number | bigint = number> = {
223+
keychains: NamedKeychains;
230224
keySignatures: {
231225
backupPub?: string;
232226
bitgoPub?: string;
@@ -240,7 +234,7 @@ export interface ParsedTransaction<TNumber extends number | bigint = number> ext
240234
implicitExternalSpendAmount: TNumber;
241235
needsCustomChangeKeySignatureVerification: boolean;
242236
customChange?: CustomChangeOptions;
243-
}
237+
};
244238

245239
export interface GenerateAddressOptions {
246240
addressType?: ScriptType2Of3;
@@ -532,6 +526,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
532526
}
533527

534528
/**
529+
<<<<<<< HEAD
535530
* @param first
536531
* @param second
537532
* @returns {Array} All outputs that are in the first array but not in the second
@@ -551,6 +546,8 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
551546
}
552547

553548
/**
549+
=======
550+
>>>>>>> 8cc76f6e8 (refactor(abstract-utxo): factor fixedScript parseTransaction)
554551
* Determine an address' type based on its witness and redeem script presence
555552
* @param addressDetails
556553
*/
@@ -607,196 +604,7 @@ export abstract class AbstractUtxoCoin extends BaseCoin {
607604
async parseTransaction<TNumber extends number | bigint = number>(
608605
params: ParseTransactionOptions<TNumber>
609606
): Promise<ParsedTransaction<TNumber>> {
610-
const { txParams, txPrebuild, wallet, verification = {}, reqId } = params;
611-
612-
if (!_.isUndefined(verification.disableNetworking) && !_.isBoolean(verification.disableNetworking)) {
613-
throw new Error('verification.disableNetworking must be a boolean');
614-
}
615-
const disableNetworking = verification.disableNetworking;
616-
617-
const fetchKeychains = async (wallet: IWallet): Promise<VerificationOptions['keychains']> => {
618-
return promiseProps({
619-
user: this.keychains().get({ id: wallet.keyIds()[KeyIndices.USER], reqId }),
620-
backup: this.keychains().get({ id: wallet.keyIds()[KeyIndices.BACKUP], reqId }),
621-
bitgo: this.keychains().get({ id: wallet.keyIds()[KeyIndices.BITGO], reqId }),
622-
});
623-
};
624-
625-
// obtain the keychains and key signatures
626-
let keychains: VerificationOptions['keychains'] | undefined = verification.keychains;
627-
if (!keychains) {
628-
if (disableNetworking) {
629-
throw new Error('cannot fetch keychains without networking');
630-
}
631-
keychains = await fetchKeychains(wallet);
632-
}
633-
634-
if (!keychains || !keychains.user || !keychains.backup || !keychains.bitgo) {
635-
throw new Error('keychains are required, but could not be fetched');
636-
}
637-
638-
const keychainArray: Triple<Keychain> = [keychains.user, keychains.backup, keychains.bitgo];
639-
640-
const keySignatures = _.get(wallet, '_wallet.keySignatures', {});
641-
642-
if (_.isUndefined(txPrebuild.txHex)) {
643-
throw new Error('missing required txPrebuild property txHex');
644-
}
645-
// obtain all outputs
646-
const explanation: TransactionExplanation = await this.explainTransaction<TNumber>({
647-
txHex: txPrebuild.txHex,
648-
txInfo: txPrebuild.txInfo,
649-
pubs: keychainArray.map((k) => k.pub) as Triple<string>,
650-
});
651-
const allOutputs = [...explanation.outputs, ...explanation.changeOutputs];
652-
653-
let expectedOutputs;
654-
if (txParams.rbfTxIds) {
655-
assert(txParams.rbfTxIds.length === 1);
656-
657-
const txToBeReplaced = await wallet.getTransaction({ txHash: txParams.rbfTxIds[0], includeRbf: true });
658-
expectedOutputs = txToBeReplaced.outputs.flatMap(
659-
(output: { valueString: string; address?: string; wallet?: string }) => {
660-
// For self-sends, the walletId will be the same as the wallet's id
661-
if (output.wallet === wallet.id()) {
662-
return [];
663-
}
664-
return [this.toCanonicalTransactionRecipient(output)];
665-
}
666-
);
667-
} else {
668-
// verify that each recipient from txParams has their own output
669-
expectedOutputs = _.get(txParams, 'recipients', [] as TransactionRecipient[]).flatMap((output) => {
670-
if (output.address === undefined) {
671-
if (output.amount.toString() !== '0') {
672-
throw new Error(`Only zero amounts allowed for non-encodeable scriptPubkeys: ${output}`);
673-
}
674-
return [output];
675-
}
676-
return [{ ...output, address: this.canonicalAddress(output.address) }];
677-
});
678-
if (params.txParams.allowExternalChangeAddress && params.txParams.changeAddress) {
679-
// when an external change address is explicitly specified, count all outputs going towards that
680-
// address in the expected outputs (regardless of the output amount)
681-
expectedOutputs.push(
682-
...allOutputs.flatMap((output) => {
683-
if (
684-
output.address === undefined ||
685-
output.address !== this.canonicalAddress(params.txParams.changeAddress as string)
686-
) {
687-
return [];
688-
}
689-
return [{ ...output, address: this.canonicalAddress(output.address) }];
690-
})
691-
);
692-
}
693-
}
694-
695-
const missingOutputs = AbstractUtxoCoin.outputDifference(expectedOutputs, allOutputs);
696-
697-
// get the keychains from the custom change wallet if needed
698-
let customChange: CustomChangeOptions | undefined;
699-
const { customChangeWalletId = undefined } = wallet.coinSpecific() || {};
700-
if (customChangeWalletId) {
701-
// fetch keychains from custom change wallet for deriving addresses.
702-
// These keychains should be signed and this should be verified in verifyTransaction
703-
const customChangeKeySignatures = wallet._wallet.customChangeKeySignatures;
704-
const customChangeWallet: Wallet = await this.wallets().get({ id: customChangeWalletId });
705-
const customChangeKeys = await fetchKeychains(customChangeWallet);
706-
707-
if (!customChangeKeys) {
708-
throw new Error('failed to fetch keychains for custom change wallet');
709-
}
710-
711-
if (customChangeKeys.user && customChangeKeys.backup && customChangeKeys.bitgo && customChangeWallet) {
712-
const customChangeKeychains: [Keychain, Keychain, Keychain] = [
713-
customChangeKeys.user,
714-
customChangeKeys.backup,
715-
customChangeKeys.bitgo,
716-
];
717-
718-
customChange = {
719-
keys: customChangeKeychains,
720-
signatures: [
721-
customChangeKeySignatures.user,
722-
customChangeKeySignatures.backup,
723-
customChangeKeySignatures.bitgo,
724-
],
725-
};
726-
}
727-
}
728-
729-
/**
730-
* Loop through all the outputs and classify each of them as either internal spends
731-
* or external spends by setting the "external" property to true or false on the output object.
732-
*/
733-
const allOutputDetails: Output[] = await Promise.all(
734-
allOutputs.map((currentOutput) => {
735-
return parseOutput({
736-
currentOutput,
737-
coin: this,
738-
txPrebuild,
739-
verification,
740-
keychainArray,
741-
wallet,
742-
txParams,
743-
customChange,
744-
reqId,
745-
});
746-
})
747-
);
748-
749-
const needsCustomChangeKeySignatureVerification = allOutputDetails.some(
750-
(output) => (output as FixedScriptWalletOutput)?.needsCustomChangeKeySignatureVerification
751-
);
752-
753-
const changeOutputs = _.filter(allOutputDetails, { external: false });
754-
755-
// these are all the outputs that were not originally explicitly specified in recipients
756-
// ideally change outputs or a paygo output that might have been added
757-
const implicitOutputs = AbstractUtxoCoin.outputDifference(allOutputDetails, expectedOutputs);
758-
759-
const explicitOutputs = AbstractUtxoCoin.outputDifference(allOutputDetails, implicitOutputs);
760-
761-
// these are all the non-wallet outputs that had been originally explicitly specified in recipients
762-
const explicitExternalOutputs = _.filter(explicitOutputs, { external: true });
763-
764-
// this is the sum of all the originally explicitly specified non-wallet output values
765-
const explicitExternalSpendAmount = utxolib.bitgo.toTNumber<TNumber>(
766-
explicitExternalOutputs.reduce((sum: bigint, o: Output) => sum + BigInt(o.amount), BigInt(0)) as bigint,
767-
this.amountType
768-
);
769-
770-
/**
771-
* The calculation of the implicit external spend amount pertains to verifying the pay-as-you-go-fee BitGo
772-
* automatically applies to transactions sending money out of the wallet. The logic is fairly straightforward
773-
* in that we compare the external spend amount that was specified explicitly by the user to the portion
774-
* that was specified implicitly. To protect customers from people tampering with the transaction outputs, we
775-
* define a threshold for the maximum percentage of the implicit external spend in relation to the explicit
776-
* external spend.
777-
*/
778-
779-
// make sure that all the extra addresses are change addresses
780-
// get all the additional external outputs the server added and calculate their values
781-
const implicitExternalOutputs = _.filter(implicitOutputs, { external: true });
782-
const implicitExternalSpendAmount = utxolib.bitgo.toTNumber<TNumber>(
783-
implicitExternalOutputs.reduce((sum: bigint, o: Output) => sum + BigInt(o.amount), BigInt(0)) as bigint,
784-
this.amountType
785-
);
786-
787-
return {
788-
keychains,
789-
keySignatures,
790-
outputs: allOutputDetails,
791-
missingOutputs,
792-
explicitExternalOutputs,
793-
implicitExternalOutputs,
794-
changeOutputs,
795-
explicitExternalSpendAmount,
796-
implicitExternalSpendAmount,
797-
needsCustomChangeKeySignatureVerification,
798-
customChange,
799-
};
607+
return parseTransaction(this, params);
800608
}
801609

802610
/**

modules/abstract-utxo/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ export * from './replayProtection';
55
export * from './sign';
66

77
export * as descriptor from './descriptor';
8+
export { fetchKeychains } from './keychains';
9+
export { toKeychainTriple } from './keychains';
10+
export { NamedKeychains } from './keychains';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { AbstractUtxoCoin } from './abstractUtxoCoin';
2+
import { IRequestTracer, IWallet, Keychain, KeyIndices, promiseProps, Triple } from '@bitgo/sdk-core';
3+
4+
export type NamedKeychains = {
5+
user?: Keychain;
6+
backup?: Keychain;
7+
bitgo?: Keychain;
8+
};
9+
10+
export function toKeychainTriple(keychains: NamedKeychains): Triple<Keychain> {
11+
const { user, backup, bitgo } = keychains;
12+
if (!user || !backup || !bitgo) {
13+
throw new Error('keychains must include user, backup, and bitgo');
14+
}
15+
return [user, backup, bitgo];
16+
}
17+
18+
export async function fetchKeychains(
19+
coin: AbstractUtxoCoin,
20+
wallet: IWallet,
21+
reqId?: IRequestTracer
22+
): Promise<NamedKeychains> {
23+
return promiseProps({
24+
user: coin.keychains().get({ id: wallet.keyIds()[KeyIndices.USER], reqId }),
25+
backup: coin.keychains().get({ id: wallet.keyIds()[KeyIndices.BACKUP], reqId }),
26+
bitgo: coin.keychains().get({ id: wallet.keyIds()[KeyIndices.BITGO], reqId }),
27+
});
28+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export { explainPsbt, explainLegacyTx, ChangeAddressInfo } from './explainTransaction';
2+
export { parseTransaction } from './parseTransaction';
3+
export { CustomChangeOptions } from './parseOutput';

modules/abstract-utxo/src/parseOutput.ts renamed to modules/abstract-utxo/src/transaction/fixedScript/parseOutput.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {
99
TransactionPrebuild,
1010
UnexpectedAddressError,
1111
VerificationOptions,
12+
ITransactionRecipient,
1213
} from '@bitgo/sdk-core';
13-
import { AbstractUtxoCoin, Output, TransactionParams, isWalletOutput } from './abstractUtxoCoin';
14+
import { AbstractUtxoCoin, Output, isWalletOutput } from '../../abstractUtxoCoin';
1415

1516
const debug = debugLib('bitgo:v2:parseoutput');
1617

@@ -90,7 +91,9 @@ interface HandleVerifyAddressErrorOptions {
9091
e: Error;
9192
currentAddress: string;
9293
wallet: IWallet;
93-
txParams: TransactionParams;
94+
txParams: {
95+
changeAddress?: string;
96+
};
9497
customChangeKeys?: CustomChangeOptions['keys'];
9598
coin: AbstractUtxoCoin;
9699
addressDetails?: any;
@@ -190,7 +193,10 @@ export interface ParseOutputOptions {
190193
verification: VerificationOptions;
191194
keychainArray: [Keychain, Keychain, Keychain];
192195
wallet: IWallet;
193-
txParams: TransactionParams;
196+
txParams: {
197+
recipients: ITransactionRecipient[];
198+
changeAddress?: string;
199+
};
194200
customChange?: CustomChangeOptions;
195201
reqId?: IRequestTracer;
196202
}

0 commit comments

Comments
 (0)