Skip to content

Commit 24fc92b

Browse files
authored
Merge pull request #5310 from BitGo/BTC-1690-allow-max
feat: use descriptor outputDifference method
2 parents 4d23192 + 87d681d commit 24fc92b

File tree

6 files changed

+41
-132
lines changed

6 files changed

+41
-132
lines changed

modules/abstract-utxo/src/transaction/descriptor/parse.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import { IDescriptorWallet } from '../../descriptor/descriptorWallet';
1414
import * as coreDescriptors from '../../core/descriptor';
1515
import { ParsedOutput } from '../../core/descriptor/psbt/parse';
1616
import { fromExtendedAddressFormatToScript, toExtendedAddressFormat } from '../recipient';
17-
18-
import { outputDifferencesWithExpected, OutputDifferenceWithExpected } from './outputDifference';
17+
import { outputDifferencesWithExpected, OutputDifferenceWithExpected } from '../outputDifference';
1918

2019
export type RecipientOutput = Omit<ParsedOutput, 'value'> & {
2120
value: bigint | 'max';

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

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,14 @@ import {
1313
ParseTransactionOptions,
1414
} from '../../abstractUtxoCoin';
1515
import { fetchKeychains, getKeySignatures, toKeychainTriple, UtxoKeychain, UtxoNamedKeychains } from '../../keychains';
16+
import { ComparableOutput, outputDifference } from '../outputDifference';
17+
import { fromExtendedAddressFormatToScript, toExtendedAddressFormat } from '../recipient';
1618

1719
import { CustomChangeOptions, parseOutput } from './parseOutput';
1820

19-
/**
20-
* @param first
21-
* @param second
22-
* @returns {Array} All outputs that are in the first array but not in the second
23-
*/
24-
export function outputDifference(first: Output[], second: Output[]): Output[] {
25-
const keyFunc = ({ address, amount }: Output): string => `${address}:${amount}`;
26-
const groupedOutputs = _.groupBy(first, keyFunc);
27-
28-
second.forEach((output) => {
29-
const group = groupedOutputs[keyFunc(output)];
30-
if (group) {
31-
group.pop();
32-
}
33-
});
34-
35-
return _.flatten(_.values(groupedOutputs));
36-
}
21+
export type ComparableOutputWithExternal<TValue> = ComparableOutput<TValue> & {
22+
external: boolean | undefined;
23+
};
3724

3825
export async function parseTransaction<TNumber extends bigint | number>(
3926
coin: AbstractUtxoCoin,
@@ -116,8 +103,6 @@ export async function parseTransaction<TNumber extends bigint | number>(
116103
}
117104
}
118105

119-
const missingOutputs = outputDifference(expectedOutputs, allOutputs);
120-
121106
// get the keychains from the custom change wallet if needed
122107
let customChange: CustomChangeOptions | undefined;
123108
const { customChangeWalletId = undefined } = wallet.coinSpecific() || {};
@@ -175,18 +160,30 @@ export async function parseTransaction<TNumber extends bigint | number>(
175160

176161
const changeOutputs = _.filter(allOutputDetails, { external: false });
177162

178-
// these are all the outputs that were not originally explicitly specified in recipients
179-
// ideally change outputs or a paygo output that might have been added
180-
const implicitOutputs = outputDifference(allOutputDetails, expectedOutputs);
163+
function toComparableOutputsWithExternal(outputs: Output[]): ComparableOutputWithExternal<bigint | 'max'>[] {
164+
return outputs.map((output) => ({
165+
script: fromExtendedAddressFormatToScript(output.address, coin.network),
166+
value: output.amount === 'max' ? 'max' : (BigInt(output.amount) as bigint | 'max'),
167+
external: output.external,
168+
}));
169+
}
170+
171+
const missingOutputs = outputDifference(
172+
toComparableOutputsWithExternal(expectedOutputs),
173+
toComparableOutputsWithExternal(allOutputs)
174+
);
181175

182-
const explicitOutputs = outputDifference(allOutputDetails, implicitOutputs);
176+
const implicitOutputs = outputDifference(
177+
toComparableOutputsWithExternal(allOutputDetails),
178+
toComparableOutputsWithExternal(expectedOutputs)
179+
);
180+
const explicitOutputs = outputDifference(toComparableOutputsWithExternal(allOutputDetails), implicitOutputs);
183181

184182
// these are all the non-wallet outputs that had been originally explicitly specified in recipients
185-
const explicitExternalOutputs = _.filter(explicitOutputs, { external: true });
186-
183+
const explicitExternalOutputs = explicitOutputs.filter((output) => output.external);
187184
// this is the sum of all the originally explicitly specified non-wallet output values
188185
const explicitExternalSpendAmount = utxolib.bitgo.toTNumber<TNumber>(
189-
explicitExternalOutputs.reduce((sum: bigint, o: Output) => sum + BigInt(o.amount), BigInt(0)) as bigint,
186+
explicitExternalOutputs.reduce((sum: bigint, o) => sum + BigInt(o.value), BigInt(0)) as bigint,
190187
coin.amountType
191188
);
192189

@@ -201,19 +198,27 @@ export async function parseTransaction<TNumber extends bigint | number>(
201198

202199
// make sure that all the extra addresses are change addresses
203200
// get all the additional external outputs the server added and calculate their values
204-
const implicitExternalOutputs = _.filter(implicitOutputs, { external: true });
201+
const implicitExternalOutputs = implicitOutputs.filter((output) => output.external);
205202
const implicitExternalSpendAmount = utxolib.bitgo.toTNumber<TNumber>(
206-
implicitExternalOutputs.reduce((sum: bigint, o: Output) => sum + BigInt(o.amount), BigInt(0)) as bigint,
203+
implicitExternalOutputs.reduce((sum: bigint, o) => sum + BigInt(o.value), BigInt(0)) as bigint,
207204
coin.amountType
208205
);
209206

207+
function toOutputs(outputs: ComparableOutputWithExternal<bigint | 'max'>[]): Output[] {
208+
return outputs.map((output) => ({
209+
address: toExtendedAddressFormat(output.script, coin.network),
210+
amount: output.value.toString(),
211+
external: output.external,
212+
}));
213+
}
214+
210215
return {
211216
keychains,
212217
keySignatures: getKeySignatures(wallet) ?? {},
213218
outputs: allOutputDetails,
214-
missingOutputs,
215-
explicitExternalOutputs,
216-
implicitExternalOutputs,
219+
missingOutputs: toOutputs(missingOutputs),
220+
explicitExternalOutputs: toOutputs(explicitExternalOutputs),
221+
implicitExternalOutputs: toOutputs(implicitExternalOutputs),
217222
changeOutputs,
218223
explicitExternalSpendAmount,
219224
implicitExternalSpendAmount,

modules/abstract-utxo/src/transaction/descriptor/outputDifference.ts renamed to modules/abstract-utxo/src/transaction/outputDifference.ts

File renamed without changes.

modules/abstract-utxo/test/outputDifference.ts

Lines changed: 0 additions & 95 deletions
This file was deleted.

modules/abstract-utxo/test/transaction/descriptor/outputDifference.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
matchingOutput,
77
outputDifference,
88
outputDifferencesWithExpected,
9-
} from '../../../src/transaction/descriptor/outputDifference';
9+
} from '../../../src/transaction/outputDifference';
1010

1111
describe('outputDifference', function () {
1212
function output(script: string, value: bigint | number): ActualOutput;

modules/bitgo/test/v2/unit/coins/abstractUtxoCoin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('Abstract UTXO Coin:', () => {
2626
};
2727

2828
const wallet = sinon.createStubInstance(Wallet, {
29-
migratedFrom: 'v1_wallet_base_address',
29+
migratedFrom: '2MzJxAENaesCFu3orrCdj22c69tLEsKXQoR',
3030
});
3131

3232
const outputAmount = (0.01 * 1e8).toString();
@@ -95,7 +95,7 @@ describe('Abstract UTXO Coin:', () => {
9595
);
9696

9797
it('should classify outputs which spend to addresses not on the wallet as external', async function () {
98-
return runClassifyOutputsTest('external_address', verification, true);
98+
return runClassifyOutputsTest('2Mxjx4E2EEe4yJuLvdEuAdMUd4id1emPCZs', verification, true);
9999
});
100100

101101
it('should accept a custom change address', async function () {

0 commit comments

Comments
 (0)