You're only signing the first input? tx.sign(0, owner);
Usually every input has to be signed somehow.
I also struggled with the switch to Psbt. Biggest difference was basically requirement to have a full transaction hex of every input for non-segwit inputs. Segwit inputs can be simpler but they can also be created using the full hex of the tx using non-segwit command so don't have to add extra logic differentiating between inputs. You can no longer just provide txid and vout, just add on the hex of tx that created each input for your object.
I really just followed examples https://github.com/bitcoinjs/bitcoinjs-lib#examples
So this is my take on a very general tx with psbt that I use for now:
import * as bitcoin from 'bitcoinjs-lib'
// not all parts of inputs have to be defined
// inputs - array of all data needed for inputs
// outputs - array of all data needed for outputs
const network = bitcoin.networks.testnet // or bitcoin.networks.bitcoin
const psbt = new bitcoin.Psbt({ network })
psbt.setVersion(2) // default
psbt.setLocktime(0) // default
// ************** create inputs **************
inputs.forEach(input=> {
psbt.addInput({
hash: input.txid, // txid number
index: input.vout, // output number
sequence: input.sequence, // often 0xfffffffe
nonWitnessUtxo: Buffer.from(input.hex, 'hex'), // works for witness inputs too!
redeemScript: input.redeemScript, // only if there's redeem script
witnessScript: input.witnessScript // only if there's witness script
})
})
// ************** create outputs **************
outputs.forEach(output => {
psbt.addOutput({
address: output.address, // only if not op_return type output (no address for those)
value: output.value,
script: output.data // only if there's string (utf8) data to embed in op_return
? bitcoin.payments.embed({ data: [Buffer.from(output.data, 'utf8')] }).output
: undefined
})
})
// ************** signing **************
inputs.forEach((input, index) => {
// sign regular inputs that can be simply signed
if (!input.redeemScript && !input.witnessScript) {
psbt.signInput(index, input.keyPair)
// give error if signature failed
if (!psbt.validateSignaturesOfInput(index)) {
throw new Error('Signature validation failed for input index ' + index.toString())
}
}
})
// ************** finalizing inputs **************
inputs.forEach((input, index) => {
// sign regular inputs that can be simply signed
if (!input.redeemScript && !input.witnessScript) {
psbt.finalizeInput(index)
}
// for p2sh or p2wsh script inputs
if (input.redeemScript || input.witnessScript) {
psbt.finalizeInput(
index,
getFinalScripts({
inputScript: input.inputScript,
network
})
)
}
})
// ************** make tx **************
const tx = psbt.extractTransaction()
const virtualSize = tx.virtualSize()
const txid = tx.getId()
const hex = tx.toHex()
console.log('tx virtualSize:', virtualSize)
console.log('tx txid:', txid)
console.log('tx hex:', hex)
And for some reason finalizeInput for custom scripts is not exactly built in yet but I found it in examples and just added as a function I import or put somewhere in same file.
function getFinalScripts ({ inputScript, network }) {
return function (
inputIndex,
input,
script,
isSegwit,
isP2SH,
isP2WSH,
) {
// Step 1: Check to make sure the meaningful script matches what you expect.
// Step 2: Create final scripts
let payment = {
network,
output: script,
input: inputScript,
};
if (isP2WSH && isSegwit)
payment = bitcoin.payments.p2wsh({
network,
redeem: payment,
});
if (isP2SH)
payment = bitcoin.payments.p2sh({
network,
redeem: payment,
});
function witnessStackToScriptWitness(witness) {
let buffer = Buffer.allocUnsafe(0);
function writeSlice(slice) {
buffer = Buffer.concat([buffer, Buffer.from(slice)]);
}
function writeVarInt(i) {
const currentLen = buffer.length;
const varintLen = varuint.encodingLength(i);
buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]);
varuint.encode(i, buffer, currentLen);
}
function writeVarSlice(slice) {
writeVarInt(slice.length);
writeSlice(slice);
}
function writeVector(vector) {
writeVarInt(vector.length);
vector.forEach(writeVarSlice);
}
writeVector(witness);
return buffer;
}
return {
finalScriptSig: payment.input,
finalScriptWitness:
payment.witness && payment.witness.length > 0
? witnessStackToScriptWitness(payment.witness)
: undefined,
};
}
}
I haven't tested every kind of input yet but this is basically what I use for p2wpkh and p2wsh inputs. I try to find examples spending exactly the type of input I need usually.