Skip to content

Commit 897d369

Browse files
feat(abstract-utxo): add descriptorWallet explainPsbt
Not integrated yet TICKET: BTC-1450
1 parent baa9290 commit 897d369

File tree

5 files changed

+113
-0
lines changed

5 files changed

+113
-0
lines changed

modules/abstract-utxo/src/abstractUtxoCoin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export function isWalletOutput(output: Output): output is FixedScriptWalletOutpu
146146

147147
export interface TransactionExplanation extends BaseTransactionExplanation<string, string> {
148148
locktime: number;
149+
/** NOTE: this actually only captures external outputs */
149150
outputs: Output[];
150151
changeOutputs: Output[];
151152

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { ITransactionRecipient } from '@bitgo/sdk-core';
3+
4+
import * as coreDescriptors from '../../core/descriptor';
5+
import { ParsedOutput } from '../../core/descriptor/psbt/parse';
6+
import { toExtendedAddressFormat } from '../recipient';
7+
import { TransactionExplanation } from '../../abstractUtxoCoin';
8+
9+
function toRecipient(output: ParsedOutput, network: utxolib.Network): ITransactionRecipient {
10+
return {
11+
address: toExtendedAddressFormat(output.script, network),
12+
amount: output.value.toString(),
13+
};
14+
}
15+
16+
function sumValues(arr: { value: bigint }[]): bigint {
17+
return arr.reduce((sum, e) => sum + e.value, BigInt(0));
18+
}
19+
20+
function getInputSignaturesForInputIndex(psbt: utxolib.bitgo.UtxoPsbt, inputIndex: number): number {
21+
const { partialSig } = psbt.data.inputs[inputIndex];
22+
if (!partialSig) {
23+
return 0;
24+
}
25+
return partialSig.reduce((agg, p) => {
26+
const valid = psbt.validateSignaturesOfInputCommon(inputIndex, p.pubkey);
27+
return agg + (valid ? 1 : 0);
28+
}, 0);
29+
}
30+
31+
function getInputSignatures(psbt: utxolib.bitgo.UtxoPsbt): number[] {
32+
return psbt.data.inputs.map((_, i) => getInputSignaturesForInputIndex(psbt, i));
33+
}
34+
35+
export function explainPsbt(
36+
psbt: utxolib.bitgo.UtxoPsbt,
37+
descriptors: coreDescriptors.DescriptorMap
38+
): TransactionExplanation {
39+
const parsedTransaction = coreDescriptors.parse(psbt, descriptors, psbt.network);
40+
const { inputs, outputs } = parsedTransaction;
41+
const externalOutputs = outputs.filter((o) => o.scriptId === undefined);
42+
const changeOutputs = outputs.filter((o) => o.scriptId !== undefined);
43+
const fee = sumValues(inputs) - sumValues(outputs);
44+
const inputSignatures = getInputSignatures(psbt);
45+
return {
46+
inputSignatures,
47+
signatures: inputSignatures.reduce((a, b) => Math.min(a, b), Infinity),
48+
locktime: psbt.locktime,
49+
id: psbt.getUnsignedTx().getId(),
50+
outputs: externalOutputs.map((o) => toRecipient(o, psbt.network)),
51+
outputAmount: sumValues(externalOutputs).toString(),
52+
changeOutputs: changeOutputs.map((o) => toRecipient(o, psbt.network)),
53+
changeAmount: sumValues(changeOutputs).toString(),
54+
fee: fee.toString(),
55+
};
56+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { DescriptorMap } from '../../core/descriptor';
2+
export { explainPsbt } from './explainPsbt';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import assert from 'assert';
2+
3+
import { explainPsbt } from '../../../src/transaction/descriptor';
4+
import { mockPsbtDefaultWithDescriptorTemplate } from '../../core/descriptor/psbt/mock.utils';
5+
import { getDescriptorMap } from '../../core/descriptor/descriptor.utils';
6+
import { getFixture } from '../../core/fixtures.utils';
7+
import { getKeyTriple } from '../../core/key.utils';
8+
import { TransactionExplanation } from '../../../src';
9+
10+
async function assertEqualFixture(name: string, v: unknown) {
11+
assert.deepStrictEqual(v, await getFixture(__dirname + '/fixtures/' + name, v));
12+
}
13+
14+
function assertSignatureCount(expl: TransactionExplanation, signatures: number, inputSignatures: number[]) {
15+
assert.deepStrictEqual(expl.signatures, signatures);
16+
assert.deepStrictEqual(expl.inputSignatures, inputSignatures);
17+
}
18+
19+
describe('explainPsbt', function () {
20+
it('has expected values', async function () {
21+
const psbt = mockPsbtDefaultWithDescriptorTemplate('Wsh2Of3');
22+
const keys = getKeyTriple('a');
23+
const descriptorMap = getDescriptorMap('Wsh2Of3', keys);
24+
await assertEqualFixture('explainPsbt.a.json', explainPsbt(psbt, descriptorMap));
25+
psbt.signAllInputsHD(keys[0]);
26+
assertSignatureCount(explainPsbt(psbt, descriptorMap), 1, [1, 1]);
27+
psbt.signAllInputsHD(keys[1]);
28+
assertSignatureCount(explainPsbt(psbt, descriptorMap), 2, [2, 2]);
29+
});
30+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"inputSignatures": [
3+
0,
4+
0
5+
],
6+
"signatures": 0,
7+
"locktime": 0,
8+
"id": "b9b272a1f0407ea461aca2da115b3399311f424949653c37d5c0023102add158",
9+
"outputs": [
10+
{
11+
"address": "bc1qvn279lx29cg843u77p3c37npay7w4uc4xw5d92xxa92z8gd3lkuq4w8477",
12+
"amount": "400000"
13+
}
14+
],
15+
"outputAmount": "400000",
16+
"changeOutputs": [
17+
{
18+
"address": "bc1q2yau645jl7k577lmanqn9a0ulcgcqm0wrrmx09dppd3kcwguvyzqk86umj",
19+
"amount": "400000"
20+
}
21+
],
22+
"changeAmount": "400000",
23+
"fee": "1200000"
24+
}

0 commit comments

Comments
 (0)