Skip to content

Commit e3b97e2

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): add aggregateTransactionExplanations with tests
Add comprehensive test coverage for the new aggregation function, verifying identity property for single explanations and additive property for multiple explanations. Issue: BTC-2768 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 781e2fe commit e3b97e2

3 files changed

Lines changed: 94 additions & 1 deletion

File tree

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,45 @@ export function explainPsbtWasm(
139139
fee: result.fee.toString(),
140140
};
141141
}
142+
143+
export interface AggregatedTransactionExplanation {
144+
inputCount: number;
145+
outputCount: number;
146+
changeOutputCount: number;
147+
inputAmount: bigint;
148+
outputAmount: bigint;
149+
changeAmount: bigint;
150+
fee: bigint;
151+
}
152+
153+
export function aggregateTransactionExplanations(
154+
explanations: TransactionExplanationBigInt[]
155+
): AggregatedTransactionExplanation {
156+
let inputCount = 0;
157+
let outputCount = 0;
158+
let changeOutputCount = 0;
159+
let fee = 0n;
160+
let inputAmount = 0n;
161+
let outputAmount = 0n;
162+
let changeAmount = 0n;
163+
164+
for (const e of explanations) {
165+
inputCount += e.inputs.length;
166+
outputCount += e.outputs.length;
167+
changeOutputCount += e.changeOutputs.length;
168+
fee += e.fee;
169+
inputAmount += e.inputAmount;
170+
outputAmount += e.outputAmount;
171+
changeAmount += e.changeAmount;
172+
}
173+
174+
return {
175+
inputCount,
176+
outputCount,
177+
changeOutputCount,
178+
inputAmount,
179+
outputAmount,
180+
changeAmount,
181+
fee,
182+
};
183+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ export { explainPsbt, explainLegacyTx, ChangeAddressInfo } from './explainTransa
22
export {
33
explainPsbtWasm,
44
explainPsbtWasmBigInt,
5+
aggregateTransactionExplanations,
56
type ExplainedInput,
67
type TransactionExplanationBigInt,
8+
type AggregatedTransactionExplanation,
79
} from './explainPsbtWasm';
810
export { parseTransaction } from './parseTransaction';
911
export { CustomChangeOptions } from './parseOutput';

modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import { testutil } from '@bitgo/utxo-lib';
55
import { fixedScriptWallet, Triple } from '@bitgo/wasm-utxo';
66

77
import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction';
8-
import { explainPsbt, explainPsbtWasm, explainPsbtWasmBigInt } from '../../../../src/transaction/fixedScript';
8+
import {
9+
explainPsbt,
10+
explainPsbtWasm,
11+
explainPsbtWasmBigInt,
12+
aggregateTransactionExplanations,
13+
type TransactionExplanationBigInt,
14+
} from '../../../../src/transaction/fixedScript';
915
import { getCoinName } from '../../../../src/names';
1016

1117
function describeTransactionWith(acidTest: testutil.AcidTest) {
@@ -135,3 +141,46 @@ function describeTransactionWith(acidTest: testutil.AcidTest) {
135141
describe('explainPsbt(Wasm)', function () {
136142
testutil.AcidTest.suite().forEach((test) => describeTransactionWith(test));
137143
});
144+
145+
describe('aggregateTransactionExplanations', function () {
146+
testutil.AcidTest.suite()
147+
.filter((t) => t.network === utxolib.networks.bitcoin)
148+
.forEach((acidTest) => {
149+
describe(acidTest.name, function () {
150+
let exp: TransactionExplanationBigInt;
151+
152+
before('prepare', function () {
153+
const psbtBytes = acidTest.createPsbt().toBuffer();
154+
const networkName = utxolib.getNetworkName(acidTest.network);
155+
assert(networkName);
156+
const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbtBytes, networkName);
157+
const walletXpubs = acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple<string>;
158+
exp = explainPsbtWasmBigInt(wasmPsbt, walletXpubs, {
159+
replayProtection: { publicKeys: [acidTest.getReplayProtectionPublicKey()] },
160+
});
161+
});
162+
163+
it('aggregating a single explanation is identity', function () {
164+
const agg = aggregateTransactionExplanations([exp]);
165+
assert.strictEqual(agg.inputCount, exp.inputs.length);
166+
assert.strictEqual(agg.outputCount, exp.outputs.length);
167+
assert.strictEqual(agg.changeOutputCount, exp.changeOutputs.length);
168+
assert.strictEqual(agg.inputAmount, exp.inputAmount);
169+
assert.strictEqual(agg.outputAmount, exp.outputAmount);
170+
assert.strictEqual(agg.changeAmount, exp.changeAmount);
171+
assert.strictEqual(agg.fee, exp.fee);
172+
});
173+
174+
it('aggregating two identical explanations doubles all counts and amounts', function () {
175+
const agg = aggregateTransactionExplanations([exp, exp]);
176+
assert.strictEqual(agg.inputCount, exp.inputs.length * 2);
177+
assert.strictEqual(agg.outputCount, exp.outputs.length * 2);
178+
assert.strictEqual(agg.changeOutputCount, exp.changeOutputs.length * 2);
179+
assert.strictEqual(agg.inputAmount, exp.inputAmount * 2n);
180+
assert.strictEqual(agg.outputAmount, exp.outputAmount * 2n);
181+
assert.strictEqual(agg.changeAmount, exp.changeAmount * 2n);
182+
assert.strictEqual(agg.fee, exp.fee * 2n);
183+
});
184+
});
185+
});
186+
});

0 commit comments

Comments
 (0)