Skip to content

Commit 9496493

Browse files
Merge pull request #7548 from BitGo/BTC-2732.explain-custom-change-outputs
feat(abstract-utxo): support custom change wallets in explainPsbt(Wasm)
2 parents abd1b53 + 80284e0 commit 9496493

6 files changed

Lines changed: 219 additions & 71 deletions

File tree

modules/abstract-utxo/src/keychains.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import assert from 'assert';
22

33
import * as t from 'io-ts';
4+
import { bitgo } from '@bitgo/utxo-lib';
45
import { BIP32Interface, bip32 } from '@bitgo/secp256k1';
56
import { IRequestTracer, IWallet, KeyIndices, promiseProps, Triple } from '@bitgo/sdk-core';
67

@@ -47,9 +48,15 @@ export function toKeychainTriple(keychains: UtxoNamedKeychains): Triple<UtxoKeyc
4748
}
4849

4950
export function toBip32Triple(
50-
keychains: UtxoNamedKeychains | Triple<{ pub: string }> | Triple<string>
51+
keychains: bitgo.RootWalletKeys | UtxoNamedKeychains | Triple<{ pub: string }> | string[]
5152
): Triple<BIP32Interface> {
53+
if (keychains instanceof bitgo.RootWalletKeys) {
54+
return keychains.triple;
55+
}
5256
if (Array.isArray(keychains)) {
57+
if (keychains.length !== 3) {
58+
throw new Error('expected 3 keychains');
59+
}
5360
return keychains.map((keychain: { pub: string } | string) => {
5461
const v = typeof keychain === 'string' ? keychain : keychain.pub;
5562
return bip32.fromBase58(v);
@@ -59,6 +66,34 @@ export function toBip32Triple(
5966
return toBip32Triple(toKeychainTriple(keychains));
6067
}
6168

69+
function toXpub(keychain: { pub: string } | string | BIP32Interface): string {
70+
if (typeof keychain === 'string') {
71+
if (keychain.startsWith('xpub')) {
72+
return keychain;
73+
}
74+
throw new Error('expected xpub');
75+
}
76+
if ('neutered' in keychain) {
77+
return keychain.neutered().toBase58();
78+
}
79+
if ('pub' in keychain) {
80+
return toXpub(keychain.pub);
81+
}
82+
throw new Error('expected keychain');
83+
}
84+
85+
export function toXpubTriple(
86+
keychains: UtxoNamedKeychains | Triple<{ pub: string }> | Triple<string> | Triple<BIP32Interface>
87+
): Triple<string> {
88+
if (Array.isArray(keychains)) {
89+
if (keychains.length !== 3) {
90+
throw new Error('expected 3 keychains');
91+
}
92+
return keychains.map((k) => toXpub(k)) as Triple<string>;
93+
}
94+
return toXpubTriple(toKeychainTriple(keychains));
95+
}
96+
6297
export async function fetchKeychains(
6398
coin: AbstractUtxoCoin,
6499
wallet: IWallet,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ITransactionRecipient } from '@bitgo/sdk-core';
33
import * as coreDescriptors from '@bitgo/utxo-core/descriptor';
44

55
import { toExtendedAddressFormat } from '../recipient';
6-
import type { TransactionExplanationUtxolibPsbt } from '../fixedScript/explainTransaction';
6+
import type { TransactionExplanationDescriptor } from '../fixedScript/explainTransaction';
77

88
function toRecipient(output: coreDescriptors.ParsedOutput, network: utxolib.Network): ITransactionRecipient {
99
return {
@@ -34,7 +34,7 @@ function getInputSignatures(psbt: utxolib.bitgo.UtxoPsbt): number[] {
3434
export function explainPsbt(
3535
psbt: utxolib.bitgo.UtxoPsbt,
3636
descriptors: coreDescriptors.DescriptorMap
37-
): TransactionExplanationUtxolibPsbt {
37+
): TransactionExplanationDescriptor {
3838
const parsedTransaction = coreDescriptors.parse(psbt, descriptors, psbt.network);
3939
const { inputs, outputs } = parsedTransaction;
4040
const externalOutputs = outputs.filter((o) => o.scriptId === undefined);

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

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,35 @@ function scriptToAddress(script: Uint8Array): string {
99
return `scriptPubKey:${Buffer.from(script).toString('hex')}`;
1010
}
1111

12+
type ParsedWalletOutput = fixedScriptWallet.ParsedOutput & { scriptId: fixedScriptWallet.ScriptId };
13+
type ParsedExternalOutput = fixedScriptWallet.ParsedOutput & { scriptId: null };
14+
15+
function isParsedWalletOutput(output: ParsedWalletOutput | ParsedExternalOutput): output is ParsedWalletOutput {
16+
return output.scriptId !== null;
17+
}
18+
19+
function isParsedExternalOutput(output: ParsedWalletOutput | ParsedExternalOutput): output is ParsedExternalOutput {
20+
return output.scriptId === null;
21+
}
22+
23+
function toChangeOutput(output: ParsedWalletOutput): FixedScriptWalletOutput {
24+
return {
25+
address: output.address ?? scriptToAddress(output.script),
26+
amount: output.value.toString(),
27+
chain: output.scriptId.chain,
28+
index: output.scriptId.index,
29+
external: false,
30+
};
31+
}
32+
33+
function toExternalOutput(output: ParsedExternalOutput): Output {
34+
return {
35+
address: output.address ?? scriptToAddress(output.script),
36+
amount: output.value.toString(),
37+
external: true,
38+
};
39+
}
40+
1241
export function explainPsbtWasm(
1342
psbt: fixedScriptWallet.BitGoPsbt,
1443
walletXpubs: Triple<string>,
@@ -17,44 +46,45 @@ export function explainPsbtWasm(
1746
checkSignature?: boolean;
1847
outputScripts: Buffer[];
1948
};
49+
customChangeWalletXpubs?: Triple<string>;
2050
}
2151
): TransactionExplanationWasm {
2252
const parsed = psbt.parseTransactionWithWalletKeys(walletXpubs, params.replayProtection);
2353

2454
const changeOutputs: FixedScriptWalletOutput[] = [];
2555
const outputs: Output[] = [];
56+
const parsedCustomChangeOutputs = params.customChangeWalletXpubs
57+
? psbt.parseOutputsWithWalletKeys(params.customChangeWalletXpubs)
58+
: undefined;
2659

27-
parsed.outputs.forEach((output) => {
28-
const address = output.address ?? scriptToAddress(output.script);
60+
const customChangeOutputs: FixedScriptWalletOutput[] = [];
2961

30-
if (output.scriptId) {
62+
parsed.outputs.forEach((output, i) => {
63+
const parseCustomChangeOutput = parsedCustomChangeOutputs?.[i];
64+
if (isParsedWalletOutput(output)) {
3165
// 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-
});
66+
changeOutputs.push(toChangeOutput(output));
67+
} else if (parseCustomChangeOutput && isParsedWalletOutput(parseCustomChangeOutput)) {
68+
customChangeOutputs.push(toChangeOutput(parseCustomChangeOutput));
69+
} else if (isParsedExternalOutput(output)) {
70+
outputs.push(toExternalOutput(output));
3971
} else {
40-
// This is an external output
41-
outputs.push({
42-
address,
43-
amount: output.value.toString(),
44-
external: true,
45-
});
72+
throw new Error('Invalid output');
4673
}
4774
});
4875

49-
const changeAmount = changeOutputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
5076
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));
5179

5280
return {
5381
id: psbt.unsignedTxid(),
5482
outputAmount: outputAmount.toString(),
5583
changeAmount: changeAmount.toString(),
84+
customChangeAmount: customChangeAmount.toString(),
5685
outputs,
5786
changeOutputs,
87+
customChangeOutputs,
5888
fee: parsed.minerFee.toString(),
5989
};
6090
}

0 commit comments

Comments
 (0)