UNPKG

paid-services

Version:
172 lines (138 loc) 5.51 kB
const {address} = require('bitcoinjs-lib'); const {networks} = require('bitcoinjs-lib'); const {Transaction} = require('bitcoinjs-lib'); const isOpenWitnessScript = require('./is_open_witness_script'); const bufferAsHex = buffer => buffer.toString('hex'); const {fromHex} = Transaction; const hashFromTxId = id => Buffer.from(id, 'hex').reverse(); const hexAsBuffer = hex => Buffer.from(hex, 'hex'); const {isArray} = Array; const sequence = 0; const {toOutputScript} = address; /** Put together the replacement transaction { [add_funds_transaction_id]: <Add Funds Transaction Id Hex String> [add_funds_transaction_vout]: <Add Funds Transaction Output Index Number> bitcoinjs_network: <BitcoinJs Network Name String> close_transaction: <Force Closed Transaction Hex String> decrease: [{ output: <Output Script Hex Encoded String> tokens: <Decrease By Tokens Number> }] funding_address: <Replacement Channel Funding Address String> funding_tokens: <Replacement Channel Funding Tokens Number> transaction_id: <Original Channel Funding Transaction Id Hex String> transaction_vout: <Original Channel Funding Output Index Number> } @throws <Error> @returns { [add_funds_vin]: <Add Funds Input Index> transaction: <Unsigned Transaction Hex String> transaction_id: <Transaction Id Hex String> transaction_vin: <Input Index of Original Channel Spend Number> transaction_vout: <Replacement Channel Transaction Output Index Number> } */ module.exports = args => { if (!args.bitcoinjs_network) { throw new Error('ExpectedBitcoinJsNetworkNameToAssembleReplacementTx'); } if (!args.close_transaction) { throw new Error('ExpectedCloseTransactionToAssembleReplacementTx'); } if (!isArray(args.decrease)) { throw new Error('ExpectedArrayOfDecreasesToAssembleReplacementTx'); } if (!args.funding_address) { throw new Error('ExpectedFundingAddressToAssembleReplacementTx'); } if (!args.funding_tokens) { throw new Error('ExpectedFundingTokensToAssembleReplacementTx'); } if (!args.transaction_id) { throw new Error('ExpectedFundingTransactionIdToAssembleReplacementTx'); } if (args.transaction_vout === undefined) { throw new Error('ExpectedFundingTransactionVoutToAssembleReplacementTx'); } const replacement = new Transaction(); // The replacement will spend the same input as the close transaction const [{witness}, secondInput] = fromHex(args.close_transaction).ins; // Confirm that the witness stack is a 2:2 if (!isOpenWitnessScript({script: bufferAsHex(witness.slice().pop())})) { throw new Error('ExpectedMultiSigWitnessInCloseTransaction'); } if (!!secondInput) { throw new Error('ExpectedOnlySingleInputForCloseTransaction'); } // The replacement will spend the funding outpoint const hash = hashFromTxId(args.transaction_id); const index = args.transaction_vout; // The new channel multi-sig will be the replacement output const network = networks[args.bitcoinjs_network]; // The replacement spends to the channel funding output const fundingOutput = { script: toOutputScript(args.funding_address, network), tokens: args.funding_tokens, }; // There can also be other outputs attached const decreaseOutputs = args.decrease.map(({output, tokens}) => ({ tokens, script: hexAsBuffer(output), })); const outputs = [].concat(decreaseOutputs).concat(fundingOutput); // Sort outputs by BIP 69 outputs.sort((a, b) => { // Sort by tokens ascending when no tie breaker needed if (a.tokens !== b.tokens) { return a.tokens - b.tokens; } // Otherwise compare output scripts lexicographically ascending return a.script.compare(b.script); }); const respendOutputIndex = outputs.findIndex(out => { return out.script.equals(fundingOutput.script); }); // Add the outputs to the replacement transaction outputs.forEach(({script, tokens}) => replacement.addOutput(script, tokens)); // Exit early when not adding more inputs to the transaction if (!args.add_funds_transaction_id) { replacement.addInput(hash, index, sequence); return { transaction: replacement.toHex(), transaction_id: replacement.getId(), transaction_vin: replacement.ins.findIndex(input => { return input.hash.equals(hash) && input.index === index; }), transaction_vout: respendOutputIndex, }; } // An additional input needs to be added when adding funds const add = { hash: hashFromTxId(args.add_funds_transaction_id), index: args.add_funds_transaction_vout, }; // Adding funds means spending the channel output plus the transit one const spends = [{hash, index}, add]; // Sort the spending inputs for BIP 69 spends.sort((a, b) => { const aHash = bufferAsHex(a.hash); const bHash = bufferAsHex(b.hash); return aHash.localeCompare(bHash) || a.index - b.index; }); // Add the spends as inputs to the replacement tx spends.forEach(({hash, index}) => replacement.addInput(hash, index)); return { add_funds_vin: replacement.ins.findIndex(input => { return input.hash.equals(add.hash) && input.index === add.index; }), transaction: replacement.toHex(), transaction_id: replacement.getId(), transaction_vin: replacement.ins.findIndex(input => { return input.hash.equals(hash) && input.index === index; }), transaction_vout: respendOutputIndex, }; };