/**
* This module provides functions for constructing and validating
* multisig transactions.
*
* @module transactions
*/
import BigNumber from 'bignumber.js';
import assert from "assert"
import {TransactionBuilder, Psbt, Transaction, script, payments} from "bitcoinjs-lib";
import {networkData} from "./networks";
import {P2SH_P2WSH} from "./p2sh_p2wsh";
import {P2WSH} from "./p2wsh";
import {
multisigRequiredSigners,
multisigPublicKeys,
multisigAddressType,
multisigRedeemScript,
multisigWitnessScript,
generateMultisigFromRaw,
} from "./multisig";
import {
validateMultisigSignature,
signatureNoSighashType,
} from "./signatures";
import {
validateMultisigInputs,
} from "./inputs";
import {validateOutputs} from "./outputs";
import {scriptToHex} from './script';
import {psbtInputFormatter, psbtOutputFormatter} from './psbt';
import {Braid} from "./braid";
import {ExtendedPublicKey} from "./keys";
/**
* Create an unsigned bitcoin transaction based on the network, inputs
* and outputs.
*
* Returns a [`Transaction`]{@link https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/types/transaction.d.ts|Transaction} object from bitcoinjs-lib.
*
* @param {module:networks.NETWORKS} network - bitcoin network
* @param {module:inputs.MultisigTransactionInput[]} inputs - transaction inputs
* @param {module:outputs.TransactionOutput[]} outputs - transaction outputs
* @returns {Transaction} an unsigned bitcoinjs-lib Transaction object
* @example
* import {
* generateMultisigFromPublicKeys, TESTNET, P2SH,
* unsignedMultisigTransaction,
* } from "unchained-bitcoin";
* const multisig = generateMultisigFromPublicKeys(TESTNET, P2SH, 2, "03a...", "03b...");
* const inputs = [
* {
* txid: "ae...",
* index: 0,
* multisig,
* },
* // other inputs...
* ];
* const outputs = [
* {
* address: "2N...",
* amountSats: 90000,
* },
* // other outputs...
* ];
* const unsignedTransaction = unsignedMultisigTransaction(TESTNET, inputs, outputs);
*
*/
export function unsignedMultisigTransaction(network, inputs, outputs) {
const multisigInputError = validateMultisigInputs(inputs);
assert(!multisigInputError.length, multisigInputError);
const multisigOutputError = validateOutputs(network, outputs);
assert(!multisigOutputError.length, multisigOutputError);
const transactionBuilder = new TransactionBuilder();
transactionBuilder.setVersion(1); // FIXME this depends on type...
transactionBuilder.network = networkData(network);
for (let inputIndex = 0; inputIndex < inputs.length; inputIndex += 1) {
const input = inputs[inputIndex];
transactionBuilder.addInput(input.txid, input.index);
}
for (let outputIndex = 0; outputIndex < outputs.length; outputIndex += 1) {
const output = outputs[outputIndex];
transactionBuilder.addOutput(output.address, BigNumber(output.amountSats).toNumber());
}
return transactionBuilder.buildIncomplete();
}
/**
* Create an unsigned bitcoin transaction based on the network, inputs
* and outputs stored as a PSBT object
*
* Returns a [`PSBT`]{@link https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/types/psbt.d.ts|PSBT} object from bitcoinjs-lib.
*
* @param {module:networks.NETWORKS} network - bitcoin network
* @param {module:inputs.MultisigTransactionInput[]} inputs - transaction inputs : NOTE - must be braid-aware
* @param {module:outputs.TransactionOutput[]} outputs - transaction outputs
* @param {Boolean} includeGlobalXpubs - include global xpub objects in the PSBT?
* @returns {Psbt} an unsigned bitcoinjs-lib Psbt object
*/
export function unsignedMultisigPSBT(network, inputs, outputs, includeGlobalXpubs=false) {
const multisigInputError = validateMultisigInputs(inputs, true);
assert(!multisigInputError.length, multisigInputError);
const multisigOutputError = validateOutputs(network, outputs);
assert(!multisigOutputError.length, multisigOutputError);
const psbt = new Psbt({ network: networkData(network) });
// FIXME: update fixtures with unsigned tx version 02000000 and proper signatures
psbt.setVersion(1); // Our fixtures currently sign transactions with version 0x01000000
const globalExtendedPublicKeys = [];
inputs.forEach((input) => {
const formattedInput = psbtInputFormatter({...input});
psbt.addInput(formattedInput);
const braidDetails = input.multisig.braidDetails;
if (braidDetails && includeGlobalXpubs) {
const braid = Braid.fromData(JSON.parse(braidDetails));
braid.extendedPublicKeys.forEach(extendedPublicKeyData => {
const extendedPublicKey = new ExtendedPublicKey(extendedPublicKeyData);
const alreadyFound = globalExtendedPublicKeys.find(
(existingExtendedPublicKey) => existingExtendedPublicKey.toBase58() === extendedPublicKey.toBase58()
);
if (!alreadyFound) {
globalExtendedPublicKeys.push(extendedPublicKey);
}
});
}
});
if (includeGlobalXpubs && globalExtendedPublicKeys.length > 0) {
const globalXpubs = globalExtendedPublicKeys.map(extendedPublicKey => ({
extendedPubkey: extendedPublicKey.encode(),
masterFingerprint: Buffer.from(extendedPublicKey.rootFingerprint, 'hex'),
path: extendedPublicKey.path,
})
);
psbt.updateGlobal({globalXpub: globalXpubs});
}
const psbtOutputs = outputs.map((output) => psbtOutputFormatter({...output}));
psbt.addOutputs(psbtOutputs);
psbt.txn = psbt.data.globalMap.unsignedTx.tx.toHex();
return psbt;
}
/**
* Returns an unsigned Transaction object from bitcoinjs-lib that is not
* generated via the TransactionBuilder (deprecating soon)
*
* FIXME: try squat out old implementation with the new PSBT one and see if
* everything works (the tx is the same)
*
* @param {Object} psbt - the PSBT object which has your transaction inside
* @returns {Transaction} an unsigned {@link https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/types/transaction.d.ts|Transaction} object (unsigned)
*/
export function unsignedTransactionObjectFromPSBT(psbt) {
return Transaction.fromHex(psbt.txn);
}
/**
* Create a fully signed multisig transaction based on the unsigned
* transaction, inputs, and their signatures.
*
* @param {module:networks.NETWORKS} network - bitcoin network
* @param {module:inputs.MultisigTransactionInput[]} inputs - multisig transaction inputs
* @param {module:outputs.TransactionOutput[]} outputs - transaction outputs
* @param {Object[]} transactionSignatures - array of transaction signatures, each an array of input signatures (1 per input)
* @returns {Transaction} a signed {@link https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/types/transaction.d.ts|Transaction} object
* @example
* import {
* generateMultisigFromPublicKeys, TESTNET, P2SH,
* signedMultisigTransaction,
* } from "unchained-bitcoin";
* const pubkey1 = "03a...";
* const pubkey2 = "03b...";
* const multisig = generateMultisigFromPublicKeys(TESTNET, P2SH, 2, pubkey1, pubkey2);
* const inputs = [
* {
* txid: "ae...",
* index: 0,
* multisig,
* },
* // other inputs...
* ];
* const outputs = [
* {
* address: "2N...",
* amountSats: 90000,
* },
* // other outputs...
* ];
* const transactionSignatures = [
* // Each element is an array of signatures from a given key, one per input.
* [
* "301a...",
* // more, 1 per input
* ],
* [
* "301b...",
* // more, 1 per input
* ],
* // More transaction signatures if required, based on inputs
* ];
* const signedTransaction = signedMultisigTransaction(TESTNET, inputs, outputs, transactionSignatures)
*/
export function signedMultisigTransaction(network, inputs, outputs, transactionSignatures) {
const unsignedTransaction = unsignedMultisigTransaction(network, inputs, outputs); // validates inputs & outputs
if (!transactionSignatures || transactionSignatures.length === 0) { throw new Error("At least one transaction signature is required."); }
transactionSignatures.forEach((transactionSignature, transactionSignatureIndex) => {
if (transactionSignature.length < inputs.length) {
throw new Error(`Insufficient input signatures for transaction signature ${transactionSignatureIndex + 1}: require ${inputs.length}, received ${transactionSignature.length}.`);
}
});
const signedTransaction = Transaction.fromHex(unsignedTransaction.toHex()); // FIXME inefficient?
for (let inputIndex=0; inputIndex < inputs.length; inputIndex++) {
const input = inputs[inputIndex];
const inputSignatures = transactionSignatures
.map((transactionSignature) => transactionSignature[inputIndex])
.filter((inputSignature) => Boolean(inputSignature));
const requiredSignatures = multisigRequiredSigners(input.multisig);
if (inputSignatures.length < requiredSignatures) {
throw new Error(`Insufficient signatures for input ${inputIndex + 1}: require ${requiredSignatures}, received ${inputSignatures.length}.`);
}
const inputSignaturesByPublicKey = {};
inputSignatures.forEach((inputSignature) => {
let publicKey;
try {
publicKey = validateMultisigSignature(network, inputs, outputs, inputIndex, inputSignature);
} catch(e) {
throw new Error(`Invalid signature for input ${inputIndex + 1}: ${inputSignature} (${e})`);
}
if (inputSignaturesByPublicKey[publicKey]) {
throw new Error(`Duplicate signature for input ${inputIndex + 1}: ${inputSignature}`);
}
inputSignaturesByPublicKey[publicKey] = inputSignature;
});
// Sort the signatures for this input by the index of their
// corresponding public key within this input's redeem script.
const publicKeys = multisigPublicKeys(input.multisig);
const sortedSignatures = publicKeys
.map((publicKey) => (inputSignaturesByPublicKey[publicKey]))
.filter((signature) => signature ? signatureNoSighashType(signature) : signature); // FIXME why not filter out the empty sigs?
if (multisigAddressType(input.multisig) === P2WSH) {
const witness = multisigWitnessField(input.multisig, sortedSignatures);
signedTransaction.setWitness(inputIndex, witness);
} else if (multisigAddressType(input.multisig) === P2SH_P2WSH) {
const witness = multisigWitnessField(input.multisig, sortedSignatures);
signedTransaction.setWitness(inputIndex, witness);
const scriptSig = multisigRedeemScript(input.multisig);
signedTransaction.ins[inputIndex].script = Buffer.from([scriptSig.output.length, ...scriptSig.output]);
} else {
const scriptSig = multisigScriptSig(input.multisig, sortedSignatures);
signedTransaction.ins[inputIndex].script = scriptSig.input;
}
}
return signedTransaction;
}
// TODO: implement this parallel function
// /**
// * Create a fully signed multisig transaction based on the unsigned
// * transaction, braid-aware inputs, and their signatures.
// *
// * @param {module:networks.NETWORKS} network - bitcoin network
// * @param {module:inputs.MultisigTransactionInput[]} inputs - braid-aware multisig transaction inputs
// * @param {module:outputs.TransactionOutput[]} outputs - transaction outputs
// * @param {Object[]} transactionSignatures - array of transaction signatures, each an array of input signatures (1 per input)
// * @returns {Transaction} a signed {@link https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/types/transaction.d.ts} Transaction object
// */
// export function signedMultisigPSBT(network, inputs, outputs, transactionSignatures) {
// const psbt = unsignedMultisigPSBT(network, inputs, outputs);
// const unsignedTransaction = unsignedTransactionObjectFromPSBT(psbt); // validates inputs & outputs
// if (!transactionSignatures || transactionSignatures.length === 0) { throw new Error("At least one transaction signature is required."); }
//
// transactionSignatures.forEach((transactionSignature, transactionSignatureIndex) => {
// if (transactionSignature.length < inputs.length) {
// throw new Error(`Insufficient input signatures for transaction signature ${transactionSignatureIndex + 1}: require ${inputs.length}, received ${transactionSignature.length}.`);
// }d
// });
// console.log(unsignedTransaction);
// FIXME - add each signature to the PSBT
// then finalizeAllInputs()
// then extractTransaction()
//
// return signedTransaction;
// }
function multisigWitnessField(multisig, sortedSignatures) {
const witness = [""].concat(sortedSignatures.map(s => signatureNoSighashType(s) +'01'));
const witnessScript = multisigWitnessScript(multisig);
witness.push(scriptToHex(witnessScript));
return witness.map(wit => Buffer.from(wit, 'hex'));
}
function multisigScriptSig(multisig, signersInputSignatures) {
const signatureOps = signersInputSignatures.map((signature) => (`${signatureNoSighashType(signature)}01`)).join(' '); // 01 => SIGHASH_ALL
const inputScript = `OP_0 ${signatureOps}`;
const inputScriptBuffer = script.fromASM(inputScript);
const rawMultisig = payments.p2ms({
network: multisig.network,
output: Buffer.from(multisigRedeemScript(multisig).output, 'hex'),
input: inputScriptBuffer,
});
return generateMultisigFromRaw(multisigAddressType(multisig), rawMultisig);
}