Skip to content

Commit b9fdf1d

Browse files
authored
Merge pull request #7528 from BitGo/BTC-2732.wasm-bitgo-psbt.explain
feat(abstract-utxo): add wasm-utxo impl for explainPsbt
2 parents 3d6585e + cb91c1d commit b9fdf1d

File tree

6 files changed

+147
-6
lines changed

6 files changed

+147
-6
lines changed

modules/abstract-utxo/src/replayProtection.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as wasmUtxo from '@bitgo/wasm-utxo';
12
import * as utxolib from '@bitgo/utxo-lib';
23

34
export function getReplayProtectionAddresses(network: utxolib.Network): string[] {
@@ -13,6 +14,12 @@ export function getReplayProtectionAddresses(network: utxolib.Network): string[]
1314
return [];
1415
}
1516

17+
export function getReplayProtectionOutputScripts(network: utxolib.Network): Buffer[] {
18+
return getReplayProtectionAddresses(network).map((address) =>
19+
Buffer.from(wasmUtxo.utxolibCompat.toOutputScript(address, network))
20+
);
21+
}
22+
1623
export function isReplayProtectionUnspent<TNumber extends number | bigint>(
1724
u: utxolib.bitgo.Unspent<TNumber>,
1825
network: utxolib.Network

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import * as utxolib from '@bitgo/utxo-lib';
2-
import { isTriple, IWallet } from '@bitgo/sdk-core';
2+
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
3+
import { isTriple, IWallet, Triple } from '@bitgo/sdk-core';
34

45
import { getDescriptorMapFromWallet, isDescriptorWallet } from '../descriptor';
56
import { toBip32Triple } from '../keychains';
67
import { getPolicyForEnv } from '../descriptor/validatePolicy';
8+
import { getReplayProtectionOutputScripts } from '../replayProtection';
79

810
import type {
911
TransactionExplanationUtxolibLegacy,
1012
TransactionExplanationUtxolibPsbt,
13+
TransactionExplanationWasm,
1114
} from './fixedScript/explainTransaction';
1215
import * as fixedScript from './fixedScript';
1316
import * as descriptor from './descriptor';
@@ -17,15 +20,15 @@ import * as descriptor from './descriptor';
1720
* change amounts, and transaction outputs.
1821
*/
1922
export function explainTx<TNumber extends number | bigint>(
20-
tx: utxolib.bitgo.UtxoTransaction<TNumber> | utxolib.bitgo.UtxoPsbt,
23+
tx: utxolib.bitgo.UtxoTransaction<TNumber> | utxolib.bitgo.UtxoPsbt | fixedScriptWallet.BitGoPsbt,
2124
params: {
2225
wallet?: IWallet;
2326
pubs?: string[];
2427
txInfo?: { unspents?: utxolib.bitgo.Unspent<TNumber>[] };
2528
changeInfo?: fixedScript.ChangeAddressInfo[];
2629
},
2730
network: utxolib.Network
28-
): TransactionExplanationUtxolibLegacy | TransactionExplanationUtxolibPsbt {
31+
): TransactionExplanationUtxolibLegacy | TransactionExplanationUtxolibPsbt | TransactionExplanationWasm {
2932
if (params.wallet && isDescriptorWallet(params.wallet)) {
3033
if (tx instanceof utxolib.bitgo.UtxoPsbt) {
3134
if (!params.pubs || !isTriple(params.pubs)) {
@@ -44,6 +47,25 @@ export function explainTx<TNumber extends number | bigint>(
4447
}
4548
if (tx instanceof utxolib.bitgo.UtxoPsbt) {
4649
return fixedScript.explainPsbt(tx, params, network);
50+
} else if (tx instanceof fixedScriptWallet.BitGoPsbt) {
51+
const pubs = params.pubs;
52+
if (!pubs) {
53+
throw new Error('pub triple is required');
54+
}
55+
const walletXpubs: Triple<string> | undefined =
56+
pubs instanceof utxolib.bitgo.RootWalletKeys
57+
? (pubs.triple.map((k) => k.neutered().toBase58()) as Triple<string>)
58+
: isTriple(pubs)
59+
? (pubs as Triple<string>)
60+
: undefined;
61+
if (!walletXpubs) {
62+
throw new Error('pub triple must be valid triple or RootWalletKeys');
63+
}
64+
return fixedScript.explainPsbtWasm(tx, walletXpubs, {
65+
replayProtection: {
66+
outputScripts: getReplayProtectionOutputScripts(network),
67+
},
68+
});
4769
} else {
4870
return fixedScript.explainLegacyTx(tx, params, network);
4971
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { fixedScriptWallet } from '@bitgo/wasm-utxo';
2+
import { Triple } from '@bitgo/sdk-core';
3+
4+
import type { Output, FixedScriptWalletOutput } from '../../abstractUtxoCoin';
5+
6+
import type { TransactionExplanationWasm } from './explainTransaction';
7+
8+
function scriptToAddress(script: Uint8Array): string {
9+
return `scriptPubKey:${Buffer.from(script).toString('hex')}`;
10+
}
11+
12+
export function explainPsbtWasm(
13+
psbt: fixedScriptWallet.BitGoPsbt,
14+
walletXpubs: Triple<string>,
15+
params: {
16+
replayProtection: {
17+
checkSignature?: boolean;
18+
outputScripts: Buffer[];
19+
};
20+
}
21+
): TransactionExplanationWasm {
22+
const parsed = psbt.parseTransactionWithWalletKeys(walletXpubs, params.replayProtection);
23+
24+
const changeOutputs: FixedScriptWalletOutput[] = [];
25+
const outputs: Output[] = [];
26+
27+
parsed.outputs.forEach((output) => {
28+
const address = output.address ?? scriptToAddress(output.script);
29+
30+
if (output.scriptId) {
31+
// This is a change output
32+
changeOutputs.push({
33+
address,
34+
amount: output.value.toString(),
35+
chain: output.scriptId.chain,
36+
index: output.scriptId.index,
37+
external: false,
38+
});
39+
} else {
40+
// This is an external output
41+
outputs.push({
42+
address,
43+
amount: output.value.toString(),
44+
external: true,
45+
});
46+
}
47+
});
48+
49+
const changeAmount = changeOutputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
50+
const outputAmount = outputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
51+
52+
return {
53+
id: psbt.unsignedTxid(),
54+
outputAmount: outputAmount.toString(),
55+
changeAmount: changeAmount.toString(),
56+
outputs,
57+
changeOutputs,
58+
fee: parsed.minerFee.toString(),
59+
};
60+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,19 @@ interface TransactionExplanationWithSignatures<TFee = string> extends AbstractUt
4141
signatures: number;
4242
}
4343

44+
/** For our wasm backend, we do not return the deprecated fields. We set TFee to string for backwards compatibility. */
45+
export type TransactionExplanationWasm = AbstractUtxoTransactionExplanation<string>;
46+
4447
/** When parsing the legacy transaction format, we cannot always infer the fee so we set it to string | undefined */
4548
export type TransactionExplanationUtxolibLegacy = TransactionExplanationWithSignatures<string | undefined>;
4649

4750
/** When parsing a PSBT, we can infer the fee so we set TFee to string. */
4851
export type TransactionExplanationUtxolibPsbt = TransactionExplanationWithSignatures<string>;
4952

50-
export type TransactionExplanation = TransactionExplanationUtxolibLegacy | TransactionExplanationUtxolibPsbt;
53+
export type TransactionExplanation =
54+
| TransactionExplanationUtxolibLegacy
55+
| TransactionExplanationUtxolibPsbt
56+
| TransactionExplanationWasm;
5157

5258
export type ChangeAddressInfo = {
5359
address: string;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { explainPsbt, explainLegacyTx, ChangeAddressInfo } from './explainTransaction';
2+
export { explainPsbtWasm } from './explainPsbtWasm';
23
export { parseTransaction } from './parseTransaction';
34
export { CustomChangeOptions } from './parseOutput';
45
export { verifyTransaction } from './verifyTransaction';

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

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
import assert from 'node:assert/strict';
22

3+
import * as utxolib from '@bitgo/utxo-lib';
34
import { testutil } from '@bitgo/utxo-lib';
5+
import { fixedScriptWallet, Triple } from '@bitgo/wasm-utxo';
46

57
import type { TransactionExplanation } from '../../../../src/transaction/fixedScript/explainTransaction';
6-
import { explainPsbt } from '../../../../src/transaction/fixedScript';
8+
import { explainPsbt, explainPsbtWasm } from '../../../../src/transaction/fixedScript';
9+
10+
function hasWasmUtxoSupport(network: utxolib.Network): boolean {
11+
return ![
12+
utxolib.networks.bitcoincash,
13+
utxolib.networks.bitcoingold,
14+
utxolib.networks.ecash,
15+
utxolib.networks.zcash,
16+
].includes(utxolib.getMainnet(network));
17+
}
718

819
function describeTransactionWith(acidTest: testutil.AcidTest) {
920
describe(`${acidTest.name}`, function () {
21+
let psbtBytes: Buffer;
1022
let refExplanation: TransactionExplanation;
1123
before('prepare', function () {
1224
const psbt = acidTest.createPsbt();
1325
refExplanation = explainPsbt(psbt, { pubs: acidTest.rootWalletKeys }, acidTest.network, {
1426
strict: true,
1527
});
28+
psbtBytes = psbt.toBuffer();
1629
});
1730

1831
it('should match the expected values for explainPsbt', function () {
@@ -26,9 +39,41 @@ function describeTransactionWith(acidTest: testutil.AcidTest) {
2639
assert.strictEqual(typeof change.address, 'string');
2740
});
2841
});
42+
43+
it('should match explainPsbtWasm', function () {
44+
if (!hasWasmUtxoSupport(acidTest.network)) {
45+
return this.skip();
46+
}
47+
48+
const networkName = utxolib.getNetworkName(acidTest.network);
49+
assert(networkName);
50+
const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbtBytes, networkName);
51+
const walletXpubs = acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple<string>;
52+
const wasmExplanation = explainPsbtWasm(wasmPsbt, walletXpubs, {
53+
replayProtection: {
54+
outputScripts: [acidTest.getReplayProtectionOutputScript()],
55+
},
56+
});
57+
58+
for (const key of Object.keys(refExplanation)) {
59+
const refValue = refExplanation[key];
60+
const wasmValue = wasmExplanation[key];
61+
switch (key) {
62+
case 'displayOrder':
63+
case 'inputSignatures':
64+
case 'signatures':
65+
// these are deprecated fields that we want to get rid of
66+
assert.deepStrictEqual(wasmValue, undefined);
67+
break;
68+
default:
69+
assert.deepStrictEqual(wasmValue, refValue, `mismatch for key ${key}`);
70+
break;
71+
}
72+
}
73+
});
2974
});
3075
}
3176

32-
describe('explainPsbt', function () {
77+
describe('explainPsbt(Wasm)', function () {
3378
testutil.AcidTest.suite().forEach((test) => describeTransactionWith(test));
3479
});

0 commit comments

Comments
 (0)