bitcoinjs/bitcoinjs-lib

Failing to form a coherent PSBT

Oskii opened this issue · 15 comments

My mission is to create a PSBT to sign and pull funds from a P2WSH HTLC transaction. I am using the bitcoinlib-js to create the HTLC transaction

import bitcoin from 'bitcoinjs-lib';
import crypto from 'crypto';

function createHTLC(secret, lockduration, recipientPubKey, senderPubKey, networkType) {
    const network = networkType === 'testnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;
    const secretHash = crypto.createHash('sha256').update(Buffer.from(secret, 'utf-8')).digest();

    const redeemScript = bitcoin.script.compile([
        bitcoin.opcodes.OP_IF,
        bitcoin.opcodes.OP_SHA256,
        secretHash,
        bitcoin.opcodes.OP_EQUALVERIFY,
        bitcoin.opcodes.OP_DUP,
        bitcoin.opcodes.OP_HASH160,
        Buffer.from(recipientPubKey, 'hex'),
        bitcoin.opcodes.OP_ELSE,
        bitcoin.script.number.encode(lockduration),
        bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
        bitcoin.opcodes.OP_DROP,
        bitcoin.opcodes.OP_DUP,
        bitcoin.opcodes.OP_HASH160,
        Buffer.from(senderPubKey, 'hex'),
        bitcoin.opcodes.OP_ENDIF,
        bitcoin.opcodes.OP_EQUALVERIFY,
        bitcoin.opcodes.OP_CHECKSIG,
    ]);

    // Calculate the P2WSH address and scriptPubKey
    const redeemScriptHash = bitcoin.crypto.sha256(redeemScript);
    const scriptPubKey = bitcoin.script.compile([
        bitcoin.opcodes.OP_0,  // Witness version 0
        redeemScriptHash
    ]);

    const p2wshAddress = bitcoin.payments.p2wsh({
        redeem: { output: redeemScript, network },
        network
    }).address;

    console.log('\nCreated an HTLC Script!');
    console.log('-------------------------------------------------');
    console.log('P2WSH Bitcoin Deposit Address for HTLC:', p2wshAddress);
    console.log('Witness Script Hex:', redeemScript.toString('hex'));
    console.log('Redeem Block Number:', lockduration);
    console.log('Secret (for spending):', secret);
    console.log('SHA256(Secret) (for HTLC creation):', secretHash.toString('hex'));
    console.log('ScriptPubKey Hex:', scriptPubKey.toString('hex'));
    console.log('-------------------------------------------------');

    // To fund the HTLC, send BTC to the p2wsh.address
    // Redeeming the HTLC would involve creating a transaction that spends from this address
    // using the provided witnessScript, which would be included in the transaction's witness field
}

// Example usage
createHTLC(
    'mysecret',
    1, // locktime in blocks
    "59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79",
    "59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79",
    'testnet'
);

This would output something like this:

Created an HTLC Script!
-------------------------------------------------
P2WSH Bitcoin Deposit Address for HTLC: tb1qx9qc7lf3c0h4cmq3as0hlu98kuj90ns4fmaxc5prwz5ucw4k4emqhnq9e3
Witness Script Hex: 63a820652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd08876a92059fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd796751b17576a92059fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd796888ac
Redeem Block Number: 1
Secret (for spending): mysecret
SHA256(Secret) (for HTLC creation): 652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd0
ScriptPubKey Hex: 002031418f7d31c3ef5c6c11ec1f7ff0a7b72457ce154efa6c502370a9cc3ab6ae76
-------------------------------------------------

Once that happens, I send Bitcoins to the HTLC directly to its P2WSH address like so:
https://mempool.space/testnet/tx/cd8342ec6843c9ae8ebe3b8cab448003760747b54c5934c7da26f53e67a0d4ff

Then I am trying to create a PSBT such that my xverse browser wallet (The same wallet that sent the funds in) will sign it via sats-connect. I think I am doing this (very?) wrongly, and creating a PSBT which is completely incoherent nonsense.

import * as bitcoin from 'bitcoinjs-lib';
import crypto from 'crypto';
import * as tinysecp256k1 from 'tiny-secp256k1';

// Initialize ECC library
import * as bitcoinjs from "bitcoinjs-lib";
import * as ecc from "tiny-secp256k1";

bitcoin.initEccLib(ecc);

function createSpendPSBT(secret, locktime, scriptPubKeyHex, htlcTxId, htlcOutputIndex, refundAmount, recipientAddress, networkType) {
    const network = networkType === 'testnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;

    // Recreate the HTLC script using the provided secret
    const secretHash = crypto.createHash('sha256').update(Buffer.from(secret)).digest();
    const redeemScript = bitcoin.script.compile([
        bitcoin.opcodes.OP_IF,
        bitcoin.opcodes.OP_SHA256,
        secretHash,
        bitcoin.opcodes.OP_EQUALVERIFY,
        bitcoin.opcodes.OP_DUP,
        bitcoin.opcodes.OP_HASH160,
        bitcoin.crypto.hash160(Buffer.from(recipientAddress, 'hex')),
        bitcoin.opcodes.OP_ELSE,
        bitcoin.script.number.encode(locktime),
        bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
        bitcoin.opcodes.OP_DROP,
        bitcoin.opcodes.OP_DUP,
        bitcoin.opcodes.OP_HASH160,
        bitcoin.crypto.hash160(Buffer.from(recipientAddress, 'hex')),
        bitcoin.opcodes.OP_ENDIF,
        bitcoin.opcodes.OP_EQUALVERIFY,
        bitcoin.opcodes.OP_CHECKSIG,
    ]);

    const scriptPubKey = Buffer.from(scriptPubKeyHex, 'hex');


    console.log("Creating PSBT");

    // Create a PSBT
    const psbt = new bitcoin.Psbt({ network: network })
        .addInput({
            hash: htlcTxId,
            index: htlcOutputIndex,
            sequence: 0xfffffffe, // Necessary for OP_CHECKLOCKTIMEVERIFY
            witnessUtxo: {
                script: scriptPubKey,
                value: refundAmount,
            },
            witnessScript: redeemScript,
        })
        .addOutput({
            address: recipientAddress,
            value: refundAmount - 1000, // Subtract a nominal fee
        })
        .setVersion(2)
        .setLocktime(locktime);

    console.log("PSBT to be signed:", psbt.toBase64());
}

// Example usage (Fill in the actual values)
createSpendPSBT(
    "mysecret", 
    0, 
    "002031418f7d31c3ef5c6c11ec1f7ff0a7b72457ce154efa6c502370a9cc3ab6ae76",
    "cd8342ec6843c9ae8ebe3b8cab448003760747b54c5934c7da26f53e67a0d4ff", 
    0, 
    1000, 
    "tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd", 
    "testnet"
);
//createSpendPSBT(secret, lockduration, scriptPubKey, htlcTxId, htlcOutputIndex, refundAmount, recipientAddress, networkType)

That gives me output like this:

o@OscarPC:~/bitcoin-htlc$ node js/create_p2wsh_htlc_spend_psbt.mjs
Creating PSBT
PSBT to be signed: cHNidP8BAF4CAAAAAf/UoGc+9SbaxzRZTLVHB3YDgESrjDu+jq7JQ2jsQoPNAAAAAAD+////AQAAAAAAAAAAIlEgmI+kZEmjRbl0oD6FTqJ7RKSipe+ZpSLIz0whDVrEu+MAAAAAAAEBK+gDAAAAAAAAIgAgMUGPfTHD71xsEewff/CntyRXzhVO+mxQI3CpzDq2rnYBBVljqCBlLH3Gh9mMmIkwTtLkCMdLYR6GpAyqUcS0Px3VkTxc0Ih2qRS0cqJm0L2JwTcGpBMsz7FvfDufy2cAsXV2qRS0cqJm0L2JwTcGpBMsz7FvfDufy2iIrAAA

Then I attempt to decode the PSBT to check if it was constructed correctly

o@OscarPC:~/bitcoin-htlc$ bitcoin-cli -testnet decodepsbt cHNidP8BAF4CAAAAAf/UoGc+9SbaxzRZTLVHB3YDgESrjDu+jq7JQ2jsQoPNAAAAAAD+////AQAAAAAAAAAAIlEgmI+kZEmjRbl0oD6FTqJ7RKSipe+ZpSLIz0whDVrEu+MAAAAAAAEBK+gDAAAAAAAAIgAgMUGPfTHD71xsEewff/CntyRXzhVO+mxQI3CpzDq2rnYBBVljqCBlLH3Gh9mMmIkwTtLkCMdLYR6GpAyqUcS0Px3VkTxc0Ih2qRS0cqJm0L2JwTcGpBMsz7FvfDufy2cAsXV2qRS0cqJm0L2JwTcGpBMsz7FvfDufy2iIrAAA
js/create_p2wsh_htlc_spend_psbt.mjs{
  "tx": {
    "txid": "9943b27a2eb4fcded18a3623a39cf2dc6983b4f94513bf61c82d47e59103592e",
    "hash": "9943b27a2eb4fcded18a3623a39cf2dc6983b4f94513bf61c82d47e59103592e",
    "version": 2,
    "size": 94,
    "vsize": 94,
    "weight": 376,
    "locktime": 0,
    "vin": [
      {
        "txid": "cd8342ec6843c9ae8ebe3b8cab448003760747b54c5934c7da26f53e67a0d4ff",
        "vout": 0,
        "scriptSig": {
          "asm": "",
          "hex": ""
        },
        "sequence": 4294967294
      }
    ],
    "vout": [
      {
        "value": 0.00000000,
        "n": 0,
        "scriptPubKey": {
          "asm": "1 988fa46449a345b974a03e854ea27b44a4a2a5ef99a522c8cf4c210d5ac4bbe3",
          "desc": "rawtr(988fa46449a345b974a03e854ea27b44a4a2a5ef99a522c8cf4c210d5ac4bbe3)#4xpnet5r",
          "hex": "5120988fa46449a345b974a03e854ea27b44a4a2a5ef99a522c8cf4c210d5ac4bbe3",
          "address": "tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd",
          "type": "witness_v1_taproot"
        }
      }
    ]
  },
  "global_xpubs": [
  ],
  "psbt_version": 0,
  "proprietary": [
  ],
  "unknown": {
  },
  "inputs": [
    {
      "witness_utxo": {
        "amount": 0.00001000,
        "scriptPubKey": {
          "asm": "0 31418f7d31c3ef5c6c11ec1f7ff0a7b72457ce154efa6c502370a9cc3ab6ae76",
          "desc": "addr(tb1qx9qc7lf3c0h4cmq3as0hlu98kuj90ns4fmaxc5prwz5ucw4k4emqhnq9e3)#z9rga855",
          "hex": "002031418f7d31c3ef5c6c11ec1f7ff0a7b72457ce154efa6c502370a9cc3ab6ae76",
          "address": "tb1qx9qc7lf3c0h4cmq3as0hlu98kuj90ns4fmaxc5prwz5ucw4k4emqhnq9e3",
          "type": "witness_v0_scripthash"
        }
      },
      "witness_script": {
        "asm": "OP_IF OP_SHA256 652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd0 OP_EQUALVERIFY OP_DUP OP_HASH160 b472a266d0bd89c13706a4132ccfb16f7c3b9fcb OP_ELSE 0 OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 b472a266d0bd89c13706a4132ccfb16f7c3b9fcb OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "63a820652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd08876a914b472a266d0bd89c13706a4132ccfb16f7c3b9fcb6700b17576a914b472a266d0bd89c13706a4132ccfb16f7c3b9fcb6888ac",
        "type": "nonstandard"
      }
    }
  ],
  "outputs": [
    {
    }
  ],
  "fee": 0.00001000
}

I don't see anything that sticks out as being incorrect. The input seems to have the correct amount and the addresses seem correct to me, but this is exhausting my expertise.

The final step for me is to try and sign this psbt using my xverse wallet and retrieve the funds. To do that I am using sats-connect like so:

const signPsbtOptions = {
      payload: {
        network: {
          type: 'Testnet' // Change to 'Regtest' or 'Mainnet' as necessary
        },
        psbtBase64: `cHNidP8BAF4CAAAAAf/UoGc+9SbaxzRZTLVHB3YDgESrjDu+jq7JQ2jsQoPNAAAAAAD+////AQAAAAAAAAAAIlEgmI+kZEmjRbl0oD6FTqJ7RKSipe+ZpSLIz0whDVrEu+MAAAAAAAEBK+gDAAAAAAAAIgAgMUGPfTHD71xsEewff/CntyRXzhVO+mxQI3CpzDq2rnYBBVljqCBlLH3Gh9mMmIkwTtLkCMdLYR6GpAyqUcS0Px3VkTxc0Ih2qRS0cqJm0L2JwTcGpBMsz7FvfDufy2cAsXV2qRS0cqJm0L2JwTcGpBMsz7FvfDufy2iIrAAA`,
        broadcast: false, // Set to true if you want to broadcast after signing
        inputsToSign: [
            {
                address: "tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd", //should this be the address of signer or the address of the input?
                signingIndexes: [0] // Assuming you want to sign the first input
            }
        ],
      },
      onFinish: (response) => {
        console.log('Signed PSBT:', response.psbtBase64);
        // Here, you could add additional code to handle the signed PSBT
      },
      onCancel: () => alert('Signing canceled'),
    };
  
    try {
      await signTransaction(signPsbtOptions);
    } catch (error) {
      console.error('Error signing PSBT:', error);
      alert('Failed to sign PSBT.');
    }

This is where I get the following error from within the xverse wallet - but I think xverse uses bitcoinlib-js anyway, so I sense this is an error bubbling up from bitcoinlib-js on xverse's side.

Input script doesn't have pubKey: 99,168,32,101,44,125,198,135,217,140,152,137,48,78,210,228,8,199,75,97,30,134,164,12,170,81,196,180,63,29,213,145,60,92,208,136,118,169,20,180,114,162,102,208,189,137,193,55,6,164,19,44,207,177,111,124,59,159,203,103,0,177,117,118,169,20,180,114,162,102,208,189,137,193,55,6,164,19,44,207,177,111,124,59,159,203,104,136,172

Is the problem that I am constructing the HTLC or PSBT in an incorrect way? Thank you so much for your assistance

A few problems I see:

  1. recipientPubKey and senderPubkey are being passed in as 32 bytes, but the output of OP_HASH160 is 20 bytes. These scripts will never succeed no matter what.
  2. bitcoinjs-lib searches all the relevant scripts of every input for A: The pubkey itself, B: The HASH160 of the pubkey. And if it doesn't find either one, it throws an error.

If we assume that xverse uses bitcoinjs-lib, then I think problem 1 is causing 2 as well.

You need to pass in the bitcoin.crypto.hash160(pubkey) of the 33 byte pubkey Buffer, and use the 20 byte output as the data in the redeemScript (witnessScript).

Also, I noticed the large compile call uses different data in the two code blocks. The first one uses 2 separate keys, while the second one uses the hash160 of the address (and not the pubkey).

Thank you for the quick response here. I believe I have correctly followed your advice.

I am now using the HASH160 of the recipientPubKey and senderPubKey. For simplicity I am using one public key for each which is the public key xverse gives me. For clarity:

Address: tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd
Public Key: 59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79

So now my HTLC creation script is as follows:

import bitcoin from 'bitcoinjs-lib';
import crypto from 'crypto';

function createHTLC(secret, lockduration, recipientPubKey, senderPubKey, networkType) {
    const network = networkType === 'testnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;
    const secretHash = crypto.createHash('sha256').update(Buffer.from(secret, 'utf-8')).digest();

    const recipientHash = bitcoin.crypto.hash160(Buffer.from(recipientPubKey, 'hex'));
    const senderHash = bitcoin.crypto.hash160(Buffer.from(senderPubKey, 'hex'));

    const redeemScript = bitcoin.script.compile([
        bitcoin.opcodes.OP_IF,
        bitcoin.opcodes.OP_SHA256,
        secretHash,
        bitcoin.opcodes.OP_EQUALVERIFY,
        bitcoin.opcodes.OP_DUP,
        bitcoin.opcodes.OP_HASH160,
        recipientHash, // Hashed recipient public key
        bitcoin.opcodes.OP_ELSE,
        bitcoin.script.number.encode(lockduration),
        bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
        bitcoin.opcodes.OP_DROP,
        bitcoin.opcodes.OP_DUP,
        bitcoin.opcodes.OP_HASH160,
        senderHash, // Hashed sender public key
        bitcoin.opcodes.OP_ENDIF,
        bitcoin.opcodes.OP_EQUALVERIFY,
        bitcoin.opcodes.OP_CHECKSIG,
    ]);

    // Calculate the P2WSH address and scriptPubKey
    const redeemScriptHash = bitcoin.crypto.sha256(redeemScript);
    const scriptPubKey = bitcoin.script.compile([
        bitcoin.opcodes.OP_0,  // Witness version 0
        redeemScriptHash
    ]);

    const p2wshAddress = bitcoin.payments.p2wsh({
        redeem: { output: redeemScript, network },
        network
    }).address;

    console.log('\nCreated an HTLC Script!');
    console.log('-------------------------------------------------');
    console.log('P2WSH Bitcoin Deposit Address for HTLC:', p2wshAddress);
    console.log('Witness Script Hex:', redeemScript.toString('hex'));
    console.log('Redeem Block Number:', lockduration);
    console.log('Secret (for spending):', secret);
    console.log('SHA256(Secret) (for HTLC creation):', secretHash.toString('hex'));
    console.log('ScriptPubKey Hex:', scriptPubKey.toString('hex'));
    console.log('-------------------------------------------------');

    // To fund the HTLC, send BTC to the p2wsh.address
    // Redeeming the HTLC would involve creating a transaction that spends from this address
    // using the provided witnessScript, which would be included in the transaction's witness field
}

// Example usage
createHTLC(
    'mysecret',
    1, // locktime in blocks
    "59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79",
    "59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79",
    'testnet'
);

This gives me the following output

Created an HTLC Script!
-------------------------------------------------
P2WSH Bitcoin Deposit Address for HTLC: tb1q5cyw034kcvsp6fsfx9skeq93vhvuqghmjc9y8xdhjll8m9thrn9q5mv0nr
Witness Script Hex: 63a820652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd08876a914e399056c4ca63571aca44fc2d11b3fdac69a37e06751b17576a914e399056c4ca63571aca44fc2d11b3fdac69a37e06888ac
Redeem Block Number: 1
Secret (for spending): mysecret
SHA256(Secret) (for HTLC creation): 652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd0
ScriptPubKey Hex: 0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca
-------------------------------------------------

Then I fund the script via xverse by sending bitcoin directly to
tb1q5cyw034kcvsp6fsfx9skeq93vhvuqghmjc9y8xdhjll8m9thrn9q5mv0nr
https://mempool.space/testnet/tx/be9cc0e300d1c01b7fdbeeff1c99acc0fb8a7d9e8d025547b7bfc9635dedcbb3

The ScriptPubKey on mempool.space seems to match mine
0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca

Then it's time to create the PSBT. I followed your suggestions and made sure the scripts match and using the correct HASH160 of the recipientPubKey and senderPubKey:

import * as bitcoin from 'bitcoinjs-lib';
import crypto from 'crypto';
import * as tinysecp256k1 from 'tiny-secp256k1';

// Initialize ECC library
import * as bitcoinjs from "bitcoinjs-lib";
import * as ecc from "tiny-secp256k1";

bitcoin.initEccLib(ecc);

function createSpendPSBT(secret, lockduration, scriptPubKeyHex, htlcTxId, htlcOutputIndex, refundAmount, recipientPubKey, senderPubKey, recipientAddress, networkType) {
    const network = networkType === 'testnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;

    const secretHash = crypto.createHash('sha256').update(Buffer.from(secret, 'utf-8')).digest();
    // Recreate the HTLC script using the provided secret
    const recipientHash = bitcoin.crypto.hash160(Buffer.from(recipientPubKey, 'hex'));
    const senderHash = bitcoin.crypto.hash160(Buffer.from(senderPubKey, 'hex'));

    const redeemScript = bitcoin.script.compile([
        bitcoin.opcodes.OP_IF,
        bitcoin.opcodes.OP_SHA256,
        secretHash,
        bitcoin.opcodes.OP_EQUALVERIFY,
        bitcoin.opcodes.OP_DUP,
        bitcoin.opcodes.OP_HASH160,
        recipientHash, // Hashed recipient public key
        bitcoin.opcodes.OP_ELSE,
        bitcoin.script.number.encode(lockduration),
        bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
        bitcoin.opcodes.OP_DROP,
        bitcoin.opcodes.OP_DUP,
        bitcoin.opcodes.OP_HASH160,
        senderHash, // Hashed sender public key
        bitcoin.opcodes.OP_ENDIF,
        bitcoin.opcodes.OP_EQUALVERIFY,
        bitcoin.opcodes.OP_CHECKSIG,
    ]);

    const scriptPubKey = Buffer.from(scriptPubKeyHex, 'hex');

    console.log("Creating PSBT");

    // Create a PSBT
    const psbt = new bitcoin.Psbt({ network: network })
        .addInput({
            hash: htlcTxId,
            index: htlcOutputIndex,
            sequence: 0xfffffffe, // Necessary for OP_CHECKLOCKTIMEVERIFY
            witnessUtxo: {
                script: scriptPubKey,
                value: refundAmount,
            },
            witnessScript: redeemScript,
        })
        .addOutput({
            address: recipientAddress,
            value: refundAmount - 1000, // Subtract a nominal fee
        })
        .setVersion(2)
        .setLocktime(lockduration);

    console.log("PSBT to be signed:", psbt.toBase64());
}

// Example usage (Fill in the actual values)
createSpendPSBT(
    "mysecret", 
    0, 
    "0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca",
    "be9cc0e300d1c01b7fdbeeff1c99acc0fb8a7d9e8d025547b7bfc9635dedcbb3", 
    0, 
    1000, 
    "59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79", 
    "59fff87c1bb3f75d34ea9d1588b72d0df43540695671c7a5ad3ec6a71d44bd79", 
    "tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd",
    "testnet",
);
//createSpendPSBT(secret, lockduration, scriptPubKey, htlcTxId, htlcOutputIndex, refundAmount, recipientPubKey, senderPubKey, recipientAddress, networkType)

This gives me the following PSBT:

cHNidP8BAF4CAAAAAbPL7V1jyb+3R1UCjZ59ivvArJkc/+7bfxvA0QDjwJy+AAAAAAD+////AQAAAAAAAAAAIlEgmI+kZEmjRbl0oD6FTqJ7RKSipe+ZpSLIz0whDVrEu+MAAAAAAAEBK+gDAAAAAAAAIgAgpgjnxrbDIB0mCTFhbICxZdnAIvuWCkOZt5f+fZV3HMoBBVljqCBlLH3Gh9mMmIkwTtLkCMdLYR6GpAyqUcS0Px3VkTxc0Ih2qRTjmQVsTKY1caykT8LRGz/axpo34GcAsXV2qRTjmQVsTKY1caykT8LRGz/axpo34GiIrAAA

When I decode it I get the following

{
  "tx": {
    "txid": "a1eaefe490f5d3be11fbd6a5afeffcff20a9e92cfde3363484168c9f5769c57a",
    "hash": "a1eaefe490f5d3be11fbd6a5afeffcff20a9e92cfde3363484168c9f5769c57a",
    "version": 2,
    "size": 94,
    "vsize": 94,
    "weight": 376,
    "locktime": 0,
    "vin": [
      {
        "txid": "be9cc0e300d1c01b7fdbeeff1c99acc0fb8a7d9e8d025547b7bfc9635dedcbb3",
        "vout": 0,
        "scriptSig": {
          "asm": "",
          "hex": ""
        },
        "sequence": 4294967294
      }
    ],
    "vout": [
      {
        "value": 0.00000000,
        "n": 0,
        "scriptPubKey": {
          "asm": "1 988fa46449a345b974a03e854ea27b44a4a2a5ef99a522c8cf4c210d5ac4bbe3",
          "desc": "rawtr(988fa46449a345b974a03e854ea27b44a4a2a5ef99a522c8cf4c210d5ac4bbe3)#4xpnet5r",
          "hex": "5120988fa46449a345b974a03e854ea27b44a4a2a5ef99a522c8cf4c210d5ac4bbe3",
          "address": "tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd",
          "type": "witness_v1_taproot"
        }
      }
    ]
  },
  "global_xpubs": [
  ],
  "psbt_version": 0,
  "proprietary": [
  ],
  "unknown": {
  },
  "inputs": [
    {
      "witness_utxo": {
        "amount": 0.00001000,
        "scriptPubKey": {
          "asm": "0 a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca",
          "desc": "addr(tb1q5cyw034kcvsp6fsfx9skeq93vhvuqghmjc9y8xdhjll8m9thrn9q5mv0nr)#wjcfmgw8",
          "hex": "0020a608e7c6b6c3201d260931616c80b165d9c022fb960a4399b797fe7d95771cca",
          "address": "tb1q5cyw034kcvsp6fsfx9skeq93vhvuqghmjc9y8xdhjll8m9thrn9q5mv0nr",
          "type": "witness_v0_scripthash"
        }
      },
      "witness_script": {
        "asm": "OP_IF OP_SHA256 652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd0 OP_EQUALVERIFY OP_DUP OP_HASH160 e399056c4ca63571aca44fc2d11b3fdac69a37e0 OP_ELSE 0 OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 e399056c4ca63571aca44fc2d11b3fdac69a37e0 OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "63a820652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd08876a914e399056c4ca63571aca44fc2d11b3fdac69a37e06700b17576a914e399056c4ca63571aca44fc2d11b3fdac69a37e06888ac",
        "type": "nonstandard"
      }
    }
  ],
  "outputs": [
    {
    }
  ],
  "fee": 0.00001000
}

Should the vin have more information inside it? Is this where - to your point - bitcoinjs-lib searches all the relevant scripts of every input for A: The pubkey itself, B: The HASH160 of the pubkey. And if it doesn't find either one, it throws an error. ? When I look at the inputs part of the PSBT, I can see that there is some input in there with my HTLC funds. I think this is the input I want to spend

"vin": [
      {
        "txid": "be9cc0e300d1c01b7fdbeeff1c99acc0fb8a7d9e8d025547b7bfc9635dedcbb3",
        "vout": 0,
        "scriptSig": {
          "asm": "",
          "hex": ""
        },
        "sequence": 4294967294
      }
    ]

From there I tried to sign it anyway:

const signPsbtOptions = {
      payload: {
        network: {
          type: 'Testnet' // Change to 'Regtest' or 'Mainnet' as necessary
        },
        psbtBase64: `cHNidP8BAF4CAAAAAbPL7V1jyb+3R1UCjZ59ivvArJkc/+7bfxvA0QDjwJy+AAAAAAD+////AQAAAAAAAAAAIlEgmI+kZEmjRbl0oD6FTqJ7RKSipe+ZpSLIz0whDVrEu+MAAAAAAAEBK+gDAAAAAAAAIgAgpgjnxrbDIB0mCTFhbICxZdnAIvuWCkOZt5f+fZV3HMoBBVljqCBlLH3Gh9mMmIkwTtLkCMdLYR6GpAyqUcS0Px3VkTxc0Ih2qRTjmQVsTKY1caykT8LRGz/axpo34GcAsXV2qRTjmQVsTKY1caykT8LRGz/axpo34GiIrAAA`,
        broadcast: false, // Set to true if you want to broadcast after signing
        inputsToSign: [
            {
                address: "tb1pnz86gezf5dzmja9q86z5agnmgjj29f00nxjj9jx0fsss6kkyh03sjkqhpd", //should this be the address of signer or the address of the input?
                signingIndexes: [0] // Assuming you want to sign the first input
            }
        ],
      },
      onFinish: (response) => {
        console.log('Signed PSBT:', response.psbtBase64);
        // Here, you could add additional code to handle the signed PSBT
      },
      onCancel: () => alert('Signing canceled'),
    };
  
    try {
      await signTransaction(signPsbtOptions);
    } catch (error) {
      console.error('Error signing PSBT:', error);
      alert('Failed to sign PSBT.');
    }

and I was met with the same error

Input script doesn't have pubKey

For pre-segwit scripts, pubkeys must be 33 or 65 bytes (DER format compressed and uncompressed)

For segwit v0 scripts, pubkeys must be 33 bytes (DER compressed)

For segwit v1 scripts (taproot), pubkeys must be 32 bytes (the header byte is assumed to be 0x02)

You are using 32 byte pubkeys when you must use 33 bytes.

Hi @junderw, thanks for all your help on this issue. We have successfully signed the psbt, and now we are at the stage where we need to add the witness script path for the transaction. In our HTLC, the script path we want to follow (in this case to unlock the htlc funds) is:

[Signature, PubKey, SecretHash, 1]

I have written a script in order to add this witness script path to the psbt, but for some reason I cannot finalize the transaction. Is there anything obvious you can see that I'm doing wrong?

import * as bitcoin from 'bitcoinjs-lib';
import { json } from 'stream/consumers';

// Your existing PSBT in base64
const psbtBase64 = "cHNidP8BAFMAAAAAAdioI2awQW/JCNj6qbo5qiJwrUSIx6hWacfdq9Cnab0aAAAAAAD+////ASgjAAAAAAAAF6kUg2Z2FQ8PWJJBbYvZvF6SO0lGd/OHAQAAAAABASsQJwAAAAAAACIAIDtghj9xIMdHClO11P7un5JyeCp4nP04N8HhGqdHsmXsIgICab96EwEYViV1jDJCIc9mFOSkEE+Q+D9jOoeQ9ikZqgpIMEUCIQCz7QOxTH2PkjDCvlyu5uuxeXRDKC1Cwj86VqskFxyy7AIgAhc64dUgGjMKduYz31CHUsD7gMXGIK6hgLGAMkL9PYoBAQVZY6ggZSx9xofZjJiJME7S5AjHS2EehqQMqlHEtD8d1ZE8XNCIdqkUpRAqdcBZk6oILKNltbp/Sb7VF1hnUbF1dqkUpRAqdcBZk6oILKNltbp/Sb7VF1hoiKwAAA==";

// Decode the PSBT
let psbt = bitcoin.Psbt.fromBase64(psbtBase64);

// Secret for the HTLC
const secret = "mysecret";
const secretBuffer = Buffer.from(secret, 'utf-8'); // Ensure it's in UTF-8 format, matching hash generation

const inputIndex = 0; // index of the input in the PSBT

// Check if the input exists and has partial signatures
if (!psbt.data.inputs[inputIndex] || !psbt.data.inputs[inputIndex].partialSig) {
    console.error("No partial signatures found for input at index", inputIndex);
    process.exit(1);
}

// Retrieve all partial signatures for the input
const partialSigs = psbt.data.inputs[inputIndex].partialSig;
if (!partialSigs || partialSigs.length === 0) {
    console.error("No signatures found in partialSig");
    process.exit(1);
}

const [signatureData] = partialSigs;

// Extract the signature and public key from the first (and only) partialSig entry
const signatureBuffer = signatureData.signature;
const publicKeyBuffer = signatureData.pubkey;

// Construct the final script witness stack for the unlock path (not using timelock)
const finalWitnessStack = [
    signatureBuffer,    // Signature in Buffer format
    publicKeyBuffer,    // Public key in Buffer format
    secretBuffer, // Secret in Buffer format (since the script checks SHA256 hash of secret)
    Buffer.from('01', 'hex') // Push '1' onto the stack to choose the unlock path
];

console.log(signatureData.signature.toString('hex'));
console.log(signatureData.pubkey.toString('hex'));
console.log(signatureData.signature.toString('hex'));
console.log('01');


// Manually set the witness stack for the input
if (!psbt.data.inputs[inputIndex].witnessUtxo) {
    console.error("Missing witnessUtxo for input");
    process.exit(1);
}

psbt.data.inputs[inputIndex].finalScriptWitness = finalWitnessStack;

console.log("----------------");
console.log(JSON.stringify(psbt));

// Complete and finalize the input
try {
    psbt.finalizeInput(inputIndex);
} catch (e) {
    console.error("Error finalizing input:", e.message);
    process.exit(1);
}

// Serialize the updated PSBT
const updatedPsbtBase64 = psbt.toBase64();
console.log("Updated PSBT:", updatedPsbtBase64);

The error comes when I try to finalize the input
Error finalizing input: Can not finalize input #0

You can't finalize something that's already finalized.

Before the try, you are doing what finalizeInput does.......... so it can't finalize it, because it's already finalized.

(Also, BitcoinJS lib finalize deletes a bunch of info from the input when done finalizing... It's probably better to write a finalizer and pass it in instead of doing what you're doing.)

You can also make a finalizer like this and pass it into the finalize method.

function getFinalScriptsSpecial(
  inputIndex, // which input is it (number)
  input, // psbt.data.inputs[inputIndex] (PsbtInput)
  script, // the "meaningful script" in this case the "redeem / witness Script" (Buffer)
  isSegwit, // true
  isP2SH, // false
  isP2WSH, // true
) {
  // ... do whatever you need to with the above info
  // Also, since this is JavaScript you can capture external state in this function before you pass it into the finalize method.
  return {
    finalScriptWitness: Buffer.from([]), // just to show that this must be a single serialized witness stack Buffer, not an Array.
  };
}

Here's a good explanation of the interface of the function in the code:

bitcoinjs-lib/ts_src/psbt.ts

Lines 1484 to 1507 in 3c26beb

/**
* This function must do two things:
* 1. Check if the `input` can be finalized. If it can not be finalized, throw.
* ie. `Can not finalize input #${inputIndex}`
* 2. Create the finalScriptSig and finalScriptWitness Buffers.
*/
type FinalScriptsFunc = (
inputIndex: number, // Which input is it?
input: PsbtInput, // The PSBT input contents
script: Buffer, // The "meaningful" locking script Buffer (redeemScript for P2SH etc.)
isSegwit: boolean, // Is it segwit?
isP2SH: boolean, // Is it P2SH?
isP2WSH: boolean, // Is it P2WSH?
) => {
finalScriptSig: Buffer | undefined;
finalScriptWitness: Buffer | undefined;
};
type FinalTaprootScriptsFunc = (
inputIndex: number, // Which input is it?
input: PsbtInput, // The PSBT input contents
tapLeafHashToFinalize?: Buffer, // Only finalize this specific leaf
) => {
finalScriptWitness: Buffer | undefined;
};

Ok thanks for your message, so if I understand correctly, my code is basically doing what finalizeInput is doing, so I have modified my script as suggested, with a finalizer function:

import * as bitcoin from 'bitcoinjs-lib';

const psbtBase64 = "cHNidP8BAFMAAAAAAdioI2awQW/JCNj6qbo5qiJwrUSIx6hWacfdq9Cnab0aAAAAAAD+////ASgjAAAAAAAAF6kUg2Z2FQ8PWJJBbYvZvF6SO0lGd/OHAQAAAAABASsQJwAAAAAAACIAIDtghj9xIMdHClO11P7un5JyeCp4nP04N8HhGqdHsmXsIgICab96EwEYViV1jDJCIc9mFOSkEE+Q+D9jOoeQ9ikZqgpIMEUCIQCz7QOxTH2PkjDCvlyu5uuxeXRDKC1Cwj86VqskFxyy7AIgAhc64dUgGjMKduYz31CHUsD7gMXGIK6hgLGAMkL9PYoBAQVZY6ggZSx9xofZjJiJME7S5AjHS2EehqQMqlHEtD8d1ZE8XNCIdqkUpRAqdcBZk6oILKNltbp/Sb7VF1hnUbF1dqkUpRAqdcBZk6oILKNltbp/Sb7VF1hoiKwAAA==";
let psbt = bitcoin.Psbt.fromBase64(psbtBase64);

const secret = "mysecret";
const secretBuffer = Buffer.from(secret, 'utf-8'); // Convert secret to buffer

function getFinalScriptsSpecial(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) {
    if (!input.partialSig || input.partialSig.length === 0) {
        throw new Error(`Cannot finalize input #${inputIndex}: Missing partial signatures`);
    }
    if (!input.witnessUtxo) {
        throw new Error(`Cannot finalize input #${inputIndex}: Missing witness UTXO`);
    }
    if (!script) {
        throw new Error(`Cannot finalize input #${inputIndex}: Missing script`);
    }

    const signature = input.partialSig[0].signature;
    const pubkey = input.partialSig[0].pubkey;

    const finalScriptWitness = bitcoin.script.compile([
        signature,
        pubkey,
        secretBuffer, 
        bitcoin.script.number.encode(1) // OP_TRUE or 1 to choose the "IF" branch
    ]);

    return {
        finalScriptWitness: finalScriptWitness
    };
}

// Finalize the input using the custom finalizer function
try {
    psbt.isSegwit = true;
    psbt.isP2SH = false;
    psbt.isP2WSH = true;
    psbt.finalizeInput(0, getFinalScriptsSpecial);
} catch (e) {
    console.error(`Error finalizing PSBT: ${e.message}`);
    process.exit(1);
}

const updatedPsbtBase64 = psbt.toBase64();
console.log("Updated PSBT:", updatedPsbtBase64);

The psbt returned is smaller than the initial psbt inputted (as you alluded to about the finalizer removing information from the psbt.

After:

cHNidP8BAFMAAAAAAdioI2awQW/JCNj6qbo5qiJwrUSIx6hWacfdq9Cnab0aAAAAAAD+////ASgjAAAAAAAAF6kUg2Z2FQ8PWJJBbYvZvF6SO0lGd/OHAQAAAAABASsQJwAAAAAAACIAIDtghj9xIMdHClO11P7un5JyeCp4nP04N8HhGqdHsmXsAQh1SDBFAiEAs+0DsUx9j5Iwwr5crubrsXl0QygtQsI/OlarJBccsuwCIAIXOuHVIBozCnbmM99Qh1LA+4DFxiCuoYCxgDJC/T2KASECab96EwEYViV1jDJCIc9mFOSkEE+Q+D9jOoeQ9ikZqgoIbXlzZWNyZXRRAAA=

Unfortunately this transaction is still not broadcast-able by Xverse wallet, its returning a generic Transaction Error message. Are there any other final steps before this becomes broadcastable? I think that something may now be malformed in my psbt as if I run

bitcoin-cli decodepsbt <psbt_after_running_this_script>

I receive an error:

error code: -22
error message:
TX decode failed DataStream::read(): end of data: iostream error

Thank you!

bitcoin.script.compile is insufficient. The witness stack serialization has a single varint at the beginning that stores the number of items on the stack.

Also, bitcoin.script.compile separates each item with a PUSH_DATA OP which is different than varint.

Witness stack requires varint at the beginning and varint between each item.

PUSHDATA and varint are exactly the same from 1 up until 75, but after 75, compile will try to use OP_PUSHDATA1 and put out [76, 76] which means 76 in PUSHDATA, but is not valid varint.

This function is an example of how to serialize.

/**
* Converts a witness stack to a script witness.
* @param witness The witness stack to convert.
* @returns The converted script witness.
*/
export function witnessStackToScriptWitness(witness: Buffer[]): Buffer {
let buffer = Buffer.allocUnsafe(0);
function writeSlice(slice: Buffer): void {
buffer = Buffer.concat([buffer, Buffer.from(slice)]);
}
function writeVarInt(i: number): void {
const currentLen = buffer.length;
const varintLen = varuint.encodingLength(i);
buffer = Buffer.concat([buffer, Buffer.allocUnsafe(varintLen)]);
varuint.encode(i, buffer, currentLen);
}
function writeVarSlice(slice: Buffer): void {
writeVarInt(slice.length);
writeSlice(slice);
}
function writeVector(vector: Buffer[]): void {
writeVarInt(vector.length);
vector.forEach(writeVarSlice);
}
writeVector(witness);
return buffer;
}

Ok great thank you for pointing out my error with bitcoin.script.compile. I have ammended my script and I believe I am close to have a finalized psbt.. The last (hopefully) piece of the puzzle is that I need to add my witnessScript into my psbt.

import * as bitcoin from 'bitcoinjs-lib';
import varuint from 'varuint-bitcoin';

const psbtBase64 = "cHNidP8BAFMAAAAAAdioI2awQW/JCNj6qbo5qiJwrUSIx6hWacfdq9Cnab0aAAAAAAD+////ASgjAAAAAAAAF6kUg2Z2FQ8PWJJBbYvZvF6SO0lGd/OHAQAAAAABASsQJwAAAAAAACIAIDtghj9xIMdHClO11P7un5JyeCp4nP04N8HhGqdHsmXsIgICab96EwEYViV1jDJCIc9mFOSkEE+Q+D9jOoeQ9ikZqgpIMEUCIQCz7QOxTH2PkjDCvlyu5uuxeXRDKC1Cwj86VqskFxyy7AIgAhc64dUgGjMKduYz31CHUsD7gMXGIK6hgLGAMkL9PYoBAQVZY6ggZSx9xofZjJiJME7S5AjHS2EehqQMqlHEtD8d1ZE8XNCIdqkUpRAqdcBZk6oILKNltbp/Sb7VF1hnUbF1dqkUpRAqdcBZk6oILKNltbp/Sb7VF1hoiKwAAA==";
let psbt = bitcoin.Psbt.fromBase64(psbtBase64);

const secret = "mysecret";
const secretBuffer = Buffer.from(secret, 'utf-8');

function witnessStackToScriptWitness(witness) {
    let buffer = Buffer.allocUnsafe(0);

    function writeSlice(slice) {
        buffer = Buffer.concat([buffer, Buffer.from(slice)]);
    }

    function writeVarInt(i) {
        const varintLen = varuint.encodingLength(i);
        const currentLen = buffer.length;
        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;
}

function getFinalScriptsSpecial(inputIndex, input, script, isSegwit, isP2SH, isP2WSH) {
    if (!input.partialSig || input.partialSig.length === 0) {
        throw new Error(`Cannot finalize input #${inputIndex}: Missing partial signatures`);
    }
    if (!input.witnessUtxo) {
        throw new Error(`Cannot finalize input #${inputIndex}: Missing witness UTXO`);
    }
    if (!script) {
        throw new Error(`Cannot finalize input #${inputIndex}: Missing script`);
    }

    const signature = input.partialSig[0].signature;
    const pubkey = input.partialSig[0].pubkey;

    const finalScriptWitness = witnessStackToScriptWitness([
        signature,
        pubkey,
        secretBuffer,
        Buffer.from([1]), // Byte array for OP_TRUE or 1
        script
    ]);

    return {
        finalScriptWitness: finalScriptWitness
    };
}

try {  
    psbt.finalizeInput(0, getFinalScriptsSpecial);
} catch (e) {
    console.error(`Error finalizing PSBT: ${e.message}`);
    process.exit(1);
}

const updatedPsbtBase64 = psbt.toBase64();
console.log("Updated PSBT:", updatedPsbtBase64);

My psbt currently has the following shape:

cHNidP8BAFMAAAAAAdioI2awQW/JCNj6qbo5qiJwrUSIx6hWacfdq9Cnab0aAAAAAAD+////ASgjAAAAAAAAF6kUg2Z2FQ8PWJJBbYvZvF6SO0lGd/OHAQAAAAABASsQJwAAAAAAACIAIDtghj9xIMdHClO11P7un5JyeCp4nP04N8HhGqdHsmXsAQjRBUgwRQIhALPtA7FMfY+SMMK+XK7m67F5dEMoLULCPzpWqyQXHLLsAiACFzrh1SAaMwp25jPfUIdSwPuAxcYgrqGAsYAyQv09igEhAmm/ehMBGFYldYwyQiHPZhTkpBBPkPg/YzqHkPYpGaoKCG15c2VjcmV0AQFZY6ggZSx9xofZjJiJME7S5AjHS2EehqQMqlHEtD8d1ZE8XNCIdqkUpRAqdcBZk6oILKNltbp/Sb7VF1hnUbF1dqkUpRAqdcBZk6oILKNltbp/Sb7VF1hoiKwAAA==

{
  "tx": {
    "txid": "b639a2017f2f3785e93f587fd11edb6fea22a5df2ee9b5dda3b4ee20f5d583fb",       
    "hash": "b639a2017f2f3785e93f587fd11edb6fea22a5df2ee9b5dda3b4ee20f5d583fb",       
    "version": 0,
    "size": 83,
    "vsize": 83,
    "weight": 332,
    "locktime": 1,
    "vin": [
      {
        "txid": "1abd69a7d0abddc76956a8c78844ad7022aa39baa9fad808c96f41b06623a8d8",   
        "vout": 0,
        "scriptSig": {
          "asm": "",
          "hex": ""
        },
        "sequence": 4294967294
      }
    ],
    "vout": [
      {
        "value": 0.00009000,
        "n": 0,
        "scriptPubKey": {
          "asm": "OP_HASH160 836676150f0f5892416d8bd9bc5e923b494677f3 OP_EQUAL",      
          "desc": "addr(3DfoBojgoDqjGLwvFLPjGcZZW7HDQ5N98X)#2qvledz7",
          "hex": "a914836676150f0f5892416d8bd9bc5e923b494677f387",
          "address": "3DfoBojgoDqjGLwvFLPjGcZZW7HDQ5N98X",
          "type": "scripthash"
        }
      }
    ]
  },
  "global_xpubs": [
  ],
  "psbt_version": 0,
  "proprietary": [
  ],
  "unknown": {
  },
  "inputs": [
    {
      "witness_utxo": {
        "amount": 0.00010000,
        "scriptPubKey": {
          "asm": "0 3b60863f7120c7470a53b5d4feee9f9272782a789cfd3837c1e11aa747b265ec",          "desc": "addr(bc1q8dsgv0m3yrr5wzjnkh20am5ljfe8s2ncnn7nsd7puyd2w3ajvhkqtt5xsc)#8ezhwxsz",
          "hex": "00203b60863f7120c7470a53b5d4feee9f9272782a789cfd3837c1e11aa747b265ec",
          "address": "bc1q8dsgv0m3yrr5wzjnkh20am5ljfe8s2ncnn7nsd7puyd2w3ajvhkqtt5xsc",          "type": "witness_v0_scripthash"
        }
      },
      "final_scriptwitness": [
        "3045022100b3ed03b14c7d8f9230c2be5caee6ebb1797443282d42c23f3a56ab24171cb2ec022002173ae1d5201a330a76e633df508752c0fb80c5c620aea180b1803242fd3d8a01",
        "0269bf7a1301185625758c324221cf6614e4a4104f90f83f633a8790f62919aa0a",
        "6d79736563726574",
        "01",
        "63a820652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd08876a914a5102a75c05993aa082ca365b5ba7f49bed517586751b17576a914a5102a75c05993aa082ca365b5ba7f49bed517586888ac"
      ]
    }
  ],
  "outputs": [
    {
    }
  ],
  "fee": 0.00001000
}

When I try to run this through xverse, it tells me that I'm missing the witness script in my psbt. How can I add this in?

Once again, many thanks for helping with this problem, it is greatly appreciated

That’s one of the fields that is removed during finalization.

witnessScript is already inside the finalized witnessStack, so having it in two places doesn’t make much sense.

you can always add the witnessScript attribute back to the psbt with updateInput iirc

edit: if there’s some check in there preventing adding it back in, then maybe just add it directly to psbt.data.inputs[i].witnessScript

Ok great thanks for your suggestions. I have managed to get the transaction finalized, for reference here is how I got xverse to play nice with signing the transaction:

psbt.data.inputs[0].witnessScript = Buffer.from(witnessHex, 'hex');

For context, xverse requires this information to sign the transaction. The transaction will now finalize using bitcoin-cli:

bitcoin-cli finalizepsbt cHNidP8BAFMCAAAAATmbVXTVZwwBNUZQvTZKZEiLQCte4yTWk8EP43pI4+mqAAAAAAD+////ASgjAAAAAAAAF6kUg2Z2FQ8PWJJBbYvZvF6SO0lGd/OHAQAAAAABASsQJwAAAAAAACIAIDtghj9xIMdHClO11P7un5JyeCp4nP04N8HhGqdHsmXsAQVZY6ggZSx9xofZjJiJME7S5AjHS2EehqQMqlHEtD8d1ZE8XNCIdqkUpRAqdcBZk6oILKNltbp/Sb7VF1hnUbF1dqkUpRAqdcBZk6oILKNltbp/Sb7VF1hoiKwBCNEFSDBFAiEA5D9G5c5eRCy8sUbKbvMbJwrWvxNWB43eI7nBpmcOX/ICIF2Nfw0S3Wv3xzpJWefu+EyKFEPZW52E/HtBnSbHufj1ASECab96EwEYViV1jDJCIc9mFOSkEE+Q+D9jOoeQ9ikZqgoIbXlzZWNyZXQBAVljqCBlLH3Gh9mMmIkwTtLkCMdLYR6GpAyqUcS0Px3VkTxc0Ih2qRSlECp1wFmTqggso2W1un9JvtUXWGdRsXV2qRSlECp1wFmTqggso2W1un9JvtUXWGiIrAAA
{
  "hex": "02000000000101399b5574d5670c01354650bd364a64488b402b5ee324d693c10fe37a48e3e9aa0000000000feffffff01282300000000000017a914836676150f0f5892416d8bd9bc5e923b494677f38705483045022100e43f46e5ce5e442cbcb146ca6ef31b270ad6bf1356078dde23b9c1a6670e5ff202205d8d7f0d12dd6bf7c73a4959e7eef84c8a1443d95b9d84fc7b419d26c7b9f8f501210269bf7a1301185625758c324221cf6614e4a4104f90f83f633a8790f62919aa0a086d7973656372657401015963a820652c7dc687d98c9889304ed2e408c74b611e86a40caa51c4b43f1dd5913c5cd08876a914a5102a75c05993aa082ca365b5ba7f49bed517586751b17576a914a5102a75c05993aa082ca365b5ba7f49bed517586888ac01000000",    
  "complete": true
}

Also after some fiddling, the transaction now broadcasts successfully on the testnet! Your help has been invaluable. If it's ok with you, we'd like to send you a token of our appreciation, if you are happy to send through your btc address

For context, xverse requires this information to sign the transaction.

I am confused... you can't sign after finalizing. You only finalize after all the signatures are done... but oh well, I guess if it works for you then ok.

If it's ok with you, we'd like to send you a token of our appreciation, if you are happy to send through your btc address

https://strike.me/junderwood/ is where you can tip me. Thank you.