Skip to content
Merged
37 changes: 36 additions & 1 deletion modules/abstract-utxo/src/keychains.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'assert';

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

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

export function toBip32Triple(
keychains: UtxoNamedKeychains | Triple<{ pub: string }> | Triple<string>
keychains: bitgo.RootWalletKeys | UtxoNamedKeychains | Triple<{ pub: string }> | string[]
): Triple<BIP32Interface> {
if (keychains instanceof bitgo.RootWalletKeys) {
return keychains.triple;
}
if (Array.isArray(keychains)) {
if (keychains.length !== 3) {
throw new Error('expected 3 keychains');
}
return keychains.map((keychain: { pub: string } | string) => {
const v = typeof keychain === 'string' ? keychain : keychain.pub;
return bip32.fromBase58(v);
Expand All @@ -59,6 +66,34 @@ export function toBip32Triple(
return toBip32Triple(toKeychainTriple(keychains));
}

function toXpub(keychain: { pub: string } | string | BIP32Interface): string {
if (typeof keychain === 'string') {
if (keychain.startsWith('xpub')) {
return keychain;
}
throw new Error('expected xpub');
}
if ('neutered' in keychain) {
return keychain.neutered().toBase58();
}
if ('pub' in keychain) {
return toXpub(keychain.pub);
}
throw new Error('expected keychain');
}

export function toXpubTriple(
keychains: UtxoNamedKeychains | Triple<{ pub: string }> | Triple<string> | Triple<BIP32Interface>
): Triple<string> {
if (Array.isArray(keychains)) {
if (keychains.length !== 3) {
throw new Error('expected 3 keychains');
}
return keychains.map((k) => toXpub(k)) as Triple<string>;
}
return toXpubTriple(toKeychainTriple(keychains));
}

export async function fetchKeychains(
coin: AbstractUtxoCoin,
wallet: IWallet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ITransactionRecipient } from '@bitgo/sdk-core';
import * as coreDescriptors from '@bitgo/utxo-core/descriptor';

import { toExtendedAddressFormat } from '../recipient';
import type { TransactionExplanationUtxolibPsbt } from '../fixedScript/explainTransaction';
import type { TransactionExplanationDescriptor } from '../fixedScript/explainTransaction';

function toRecipient(output: coreDescriptors.ParsedOutput, network: utxolib.Network): ITransactionRecipient {
return {
Expand Down Expand Up @@ -34,7 +34,7 @@ function getInputSignatures(psbt: utxolib.bitgo.UtxoPsbt): number[] {
export function explainPsbt(
psbt: utxolib.bitgo.UtxoPsbt,
descriptors: coreDescriptors.DescriptorMap
): TransactionExplanationUtxolibPsbt {
): TransactionExplanationDescriptor {
const parsedTransaction = coreDescriptors.parse(psbt, descriptors, psbt.network);
const { inputs, outputs } = parsedTransaction;
const externalOutputs = outputs.filter((o) => o.scriptId === undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ function scriptToAddress(script: Uint8Array): string {
return `scriptPubKey:${Buffer.from(script).toString('hex')}`;
}

type ParsedWalletOutput = fixedScriptWallet.ParsedOutput & { scriptId: fixedScriptWallet.ScriptId };
type ParsedExternalOutput = fixedScriptWallet.ParsedOutput & { scriptId: null };

function isParsedWalletOutput(output: ParsedWalletOutput | ParsedExternalOutput): output is ParsedWalletOutput {
return output.scriptId !== null;
}

function isParsedExternalOutput(output: ParsedWalletOutput | ParsedExternalOutput): output is ParsedExternalOutput {
return output.scriptId === null;
}

function toChangeOutput(output: ParsedWalletOutput): FixedScriptWalletOutput {
return {
address: output.address ?? scriptToAddress(output.script),
amount: output.value.toString(),
chain: output.scriptId.chain,
index: output.scriptId.index,
external: false,
};
}

function toExternalOutput(output: ParsedExternalOutput): Output {
return {
address: output.address ?? scriptToAddress(output.script),
amount: output.value.toString(),
external: true,
};
}

export function explainPsbtWasm(
psbt: fixedScriptWallet.BitGoPsbt,
walletXpubs: Triple<string>,
Expand All @@ -17,44 +46,45 @@ export function explainPsbtWasm(
checkSignature?: boolean;
outputScripts: Buffer[];
};
customChangeWalletXpubs?: Triple<string>;
}
): TransactionExplanationWasm {
const parsed = psbt.parseTransactionWithWalletKeys(walletXpubs, params.replayProtection);

const changeOutputs: FixedScriptWalletOutput[] = [];
const outputs: Output[] = [];
const parsedCustomChangeOutputs = params.customChangeWalletXpubs
? psbt.parseOutputsWithWalletKeys(params.customChangeWalletXpubs)
: undefined;

parsed.outputs.forEach((output) => {
const address = output.address ?? scriptToAddress(output.script);
const customChangeOutputs: FixedScriptWalletOutput[] = [];

if (output.scriptId) {
parsed.outputs.forEach((output, i) => {
const parseCustomChangeOutput = parsedCustomChangeOutputs?.[i];
if (isParsedWalletOutput(output)) {
// This is a change output
changeOutputs.push({
address,
amount: output.value.toString(),
chain: output.scriptId.chain,
index: output.scriptId.index,
external: false,
});
changeOutputs.push(toChangeOutput(output));
} else if (parseCustomChangeOutput && isParsedWalletOutput(parseCustomChangeOutput)) {
customChangeOutputs.push(toChangeOutput(parseCustomChangeOutput));
} else if (isParsedExternalOutput(output)) {
outputs.push(toExternalOutput(output));
} else {
// This is an external output
outputs.push({
address,
amount: output.value.toString(),
external: true,
});
throw new Error('Invalid output');
}
});

const changeAmount = changeOutputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
const outputAmount = outputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
const changeAmount = changeOutputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));
const customChangeAmount = customChangeOutputs.reduce((sum, output) => sum + BigInt(output.amount), BigInt(0));

return {
id: psbt.unsignedTxid(),
outputAmount: outputAmount.toString(),
changeAmount: changeAmount.toString(),
customChangeAmount: customChangeAmount.toString(),
outputs,
changeOutputs,
customChangeOutputs,
fee: parsed.minerFee.toString(),
};
}
Loading