Skip to content

Commit 781e2fe

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): add explainPsbtWasmBigInt for bigint amounts
Refactor explainPsbtWasm to use explainPsbtWasmBigInt that returns bigint amounts instead of strings. The original function now delegates to the new implementation and converts results to strings for backward compatibility. Add input details (address, value) to transaction explanations. Export utility functions (decodePsbt, decodePsbtWith, isPsbt, getVSize) from transaction module for external use. BTC-2768 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 2a5ab1a commit 781e2fe

5 files changed

Lines changed: 122 additions & 32 deletions

File tree

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
22
import { Triple } from '@bitgo/sdk-core';
33

4-
import type { FixedScriptWalletOutput, Output } from '../types';
4+
import type { FixedScriptWalletOutput, Output, BitGoPsbt } from '../types';
55

66
import type { TransactionExplanationWasm } from './explainTransaction';
77

@@ -20,71 +20,122 @@ function isParsedExternalOutput(output: ParsedWalletOutput | ParsedExternalOutpu
2020
return output.scriptId === null;
2121
}
2222

23-
function toChangeOutput(output: ParsedWalletOutput): FixedScriptWalletOutput {
23+
function toChangeOutputBigInt(output: ParsedWalletOutput): FixedScriptWalletOutput<bigint> {
2424
return {
2525
address: output.address ?? scriptToAddress(output.script),
26-
amount: output.value.toString(),
26+
amount: output.value,
2727
chain: output.scriptId.chain,
2828
index: output.scriptId.index,
2929
external: false,
3030
};
3131
}
3232

33-
function toExternalOutput(output: ParsedExternalOutput): Output {
33+
function toExternalOutputBigInt(output: ParsedExternalOutput): Output<bigint> {
3434
return {
3535
address: output.address ?? scriptToAddress(output.script),
36-
amount: output.value.toString(),
36+
amount: output.value,
3737
external: true,
3838
};
3939
}
4040

41-
export function explainPsbtWasm(
42-
psbt: fixedScriptWallet.BitGoPsbt,
41+
interface ExplainPsbtWasmParams {
42+
replayProtection: {
43+
checkSignature?: boolean;
44+
publicKeys: Buffer[];
45+
};
46+
customChangeWalletXpubs?: Triple<string> | fixedScriptWallet.RootWalletKeys;
47+
}
48+
49+
export interface ExplainedInput<TAmount = bigint> {
50+
address: string;
51+
value: TAmount;
52+
}
53+
54+
export interface TransactionExplanationBigInt {
55+
id: string;
56+
inputs: ExplainedInput[];
57+
inputAmount: bigint;
58+
outputs: Output<bigint>[];
59+
changeOutputs: FixedScriptWalletOutput<bigint>[];
60+
customChangeOutputs: FixedScriptWalletOutput<bigint>[];
61+
outputAmount: bigint;
62+
changeAmount: bigint;
63+
customChangeAmount: bigint;
64+
fee: bigint;
65+
}
66+
67+
export function explainPsbtWasmBigInt(
68+
psbt: BitGoPsbt,
4369
walletXpubs: Triple<string> | fixedScriptWallet.RootWalletKeys,
44-
params: {
45-
replayProtection: {
46-
checkSignature?: boolean;
47-
publicKeys: Buffer[];
48-
};
49-
customChangeWalletXpubs?: Triple<string> | fixedScriptWallet.RootWalletKeys;
50-
}
51-
): TransactionExplanationWasm {
70+
params: ExplainPsbtWasmParams
71+
): TransactionExplanationBigInt {
5272
const parsed = psbt.parseTransactionWithWalletKeys(walletXpubs, { replayProtection: params.replayProtection });
5373

54-
const changeOutputs: FixedScriptWalletOutput[] = [];
55-
const outputs: Output[] = [];
74+
const changeOutputs: FixedScriptWalletOutput<bigint>[] = [];
75+
const outputs: Output<bigint>[] = [];
5676
const parsedCustomChangeOutputs = params.customChangeWalletXpubs
5777
? psbt.parseOutputsWithWalletKeys(params.customChangeWalletXpubs)
5878
: undefined;
5979

60-
const customChangeOutputs: FixedScriptWalletOutput[] = [];
80+
const customChangeOutputs: FixedScriptWalletOutput<bigint>[] = [];
6181

6282
parsed.outputs.forEach((output, i) => {
6383
const parseCustomChangeOutput = parsedCustomChangeOutputs?.[i];
6484
if (isParsedWalletOutput(output)) {
65-
// This is a change output
66-
changeOutputs.push(toChangeOutput(output));
85+
changeOutputs.push(toChangeOutputBigInt(output));
6786
} else if (parseCustomChangeOutput && isParsedWalletOutput(parseCustomChangeOutput)) {
68-
customChangeOutputs.push(toChangeOutput(parseCustomChangeOutput));
87+
customChangeOutputs.push(toChangeOutputBigInt(parseCustomChangeOutput));
6988
} else if (isParsedExternalOutput(output)) {
70-
outputs.push(toExternalOutput(output));
89+
outputs.push(toExternalOutputBigInt(output));
7190
} else {
7291
throw new Error('Invalid output');
7392
}
7493
});
7594

76-
const outputAmount = outputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
77-
const changeAmount = changeOutputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
78-
const customChangeAmount = customChangeOutputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
95+
const inputs = parsed.inputs.map((input) => ({ address: input.address, value: input.value }));
96+
const inputAmount = inputs.reduce((sum, input) => sum + input.value, 0n);
97+
const outputAmount = outputs.reduce((sum, output) => sum + output.amount, 0n);
98+
const changeAmount = changeOutputs.reduce((sum, output) => sum + output.amount, 0n);
99+
const customChangeAmount = customChangeOutputs.reduce((sum, output) => sum + output.amount, 0n);
79100

80101
return {
81102
id: psbt.unsignedTxId(),
82-
outputAmount: outputAmount.toString(),
83-
changeAmount: changeAmount.toString(),
84-
customChangeAmount: customChangeAmount.toString(),
103+
inputs,
104+
inputAmount,
85105
outputs,
86106
changeOutputs,
87107
customChangeOutputs,
88-
fee: parsed.minerFee.toString(),
108+
outputAmount,
109+
changeAmount,
110+
customChangeAmount,
111+
fee: parsed.minerFee,
112+
};
113+
}
114+
115+
function stringifyOutput(output: Output<bigint>): Output {
116+
return { ...output, amount: output.amount.toString() };
117+
}
118+
119+
function stringifyChangeOutput(output: FixedScriptWalletOutput<bigint>): FixedScriptWalletOutput {
120+
return { ...output, amount: output.amount.toString() };
121+
}
122+
123+
export function explainPsbtWasm(
124+
psbt: BitGoPsbt,
125+
walletXpubs: Triple<string> | fixedScriptWallet.RootWalletKeys,
126+
params: ExplainPsbtWasmParams
127+
): TransactionExplanationWasm {
128+
const result = explainPsbtWasmBigInt(psbt, walletXpubs, params);
129+
return {
130+
id: result.id,
131+
inputs: result.inputs.map((i) => ({ address: i.address, value: i.value.toString() })),
132+
inputAmount: result.inputAmount.toString(),
133+
outputAmount: result.outputAmount.toString(),
134+
changeAmount: result.changeAmount.toString(),
135+
customChangeAmount: result.customChangeAmount.toString(),
136+
outputs: result.outputs.map(stringifyOutput),
137+
changeOutputs: result.changeOutputs.map(stringifyChangeOutput),
138+
customChangeOutputs: result.customChangeOutputs.map(stringifyChangeOutput),
139+
fee: result.fee.toString(),
89140
};
90141
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ interface TransactionExplanationWithSignatures<TFee = string, TChangeOutput exte
5151
}
5252

5353
/** For our wasm backend, we do not return the deprecated fields. We set TFee to string for backwards compatibility. */
54-
export type TransactionExplanationWasm = AbstractUtxoTransactionExplanation<string, FixedScriptWalletOutput>;
54+
export type TransactionExplanationWasm = AbstractUtxoTransactionExplanation<string, FixedScriptWalletOutput> & {
55+
inputs: Array<{ address: string; value: string }>;
56+
inputAmount: string;
57+
};
5558

5659
/** When parsing the legacy transaction format, we cannot always infer the fee so we set it to string | undefined */
5760
export type TransactionExplanationUtxolibLegacy = TransactionExplanationWithSignatures<string | undefined>;

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
export { explainPsbt, explainLegacyTx, ChangeAddressInfo } from './explainTransaction';
2-
export { explainPsbtWasm } from './explainPsbtWasm';
2+
export {
3+
explainPsbtWasm,
4+
explainPsbtWasmBigInt,
5+
type ExplainedInput,
6+
type TransactionExplanationBigInt,
7+
} from './explainPsbtWasm';
38
export { parseTransaction } from './parseTransaction';
49
export { CustomChangeOptions } from './parseOutput';
510
export { verifyTransaction } from './verifyTransaction';

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export { parseTransaction } from './parseTransaction';
55
export { verifyTransaction } from './verifyTransaction';
66
export * from './fetchInputs';
77
export * as bip322 from './bip322';
8+
export { decodePsbt, decodePsbtWith } from './decode';

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ 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 } from '../../../../src/transaction/fixedScript';
8+
import { explainPsbt, explainPsbtWasm, explainPsbtWasmBigInt } from '../../../../src/transaction/fixedScript';
99
import { getCoinName } from '../../../../src/names';
1010

1111
function describeTransactionWith(acidTest: testutil.AcidTest) {
@@ -79,12 +79,42 @@ function describeTransactionWith(acidTest: testutil.AcidTest) {
7979
break;
8080
}
8181
}
82+
83+
// verify new fields are present and stringified
84+
assert.strictEqual(typeof wasmExplanation.inputAmount, 'string');
85+
assert.ok(Array.isArray(wasmExplanation.inputs));
86+
assert.ok(wasmExplanation.inputs.length > 0);
87+
for (const input of wasmExplanation.inputs) {
88+
assert.strictEqual(typeof input.address, 'string');
89+
assert.strictEqual(typeof input.value, 'string');
90+
}
8291
});
8392

8493
if (acidTest.network !== utxolib.networks.bitcoin) {
8594
return;
8695
}
8796

97+
it('explainPsbtWasmBigInt returns bigint amounts and inputs array', function () {
98+
const result = explainPsbtWasmBigInt(wasmPsbt, walletXpubs, {
99+
replayProtection: { publicKeys: [acidTest.getReplayProtectionPublicKey()] },
100+
});
101+
assert.strictEqual(typeof result.fee, 'bigint');
102+
assert.strictEqual(typeof result.outputAmount, 'bigint');
103+
assert.strictEqual(typeof result.changeAmount, 'bigint');
104+
assert.strictEqual(typeof result.inputAmount, 'bigint');
105+
assert.ok(result.inputs.length > 0);
106+
for (const input of result.inputs) {
107+
assert.strictEqual(typeof input.address, 'string');
108+
assert.strictEqual(typeof input.value, 'bigint');
109+
}
110+
const sumInputs = result.inputs.reduce((s, i) => s + i.value, 0n);
111+
assert.strictEqual(result.inputAmount, sumInputs);
112+
assert.strictEqual(
113+
result.fee,
114+
result.inputAmount - result.outputAmount - result.changeAmount - result.customChangeAmount
115+
);
116+
});
117+
88118
// extended test suite for bitcoin
89119

90120
it('returns custom change outputs when parameter is set', function () {

0 commit comments

Comments
 (0)