Creating Partially Signed Bitcoin Transactions (PSBTs) with Bitcoin Core and Node Js

 

Creating Partially Signed Bitcoin Transactions (PSBTs) with Bitcoin Core and Node Js

Photo by Dmitry Demidko on Unsplash

What are Partially Signed Bitcoin Transactions (PSBTs)

PSBTs are data format that allows wallets and other tools to exchange information about a Bitcoin transaction and the signatures necessary to complete it.

Let’s Setup our project

Create a directory on your computer, in your terminal enter the code below

mkdir psbt
cd psbt

Then initialize a new node application by entering

npm init -y

The code above would create a package.json file, which we can specify a start script and other details for our application.

Next we would create an index.js file and specify it as our point of entry into our application.

Add the line "start": "node index.js" into the scripts object. You can replace the default "test" command, which we won't be using.

Your package.json file should look like what’s below

{
  "name": "psbt",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Next, lets build out our project structure and files. In the project directory add an index.js file for our server and .env file to store our bitcoin credentials. Next, add a util folder with index.js also create the .gitignorefile, to avoid pushing our .envfile and node_modules folder to GitHub. The following commands, will do this in the command line:

touch index.js
touch .env
mkdir util
cd util
touch index.js
cd ..
touch .gitignore

Next we can install the additional javascript packages we’ll be using. Packages (or dependencies) are 3rd party javascript code we will utilize in our application. Install the needed dependencies with the following commands

npm install axios dotenv

dotenv - helps us read our credentials from the .env file
axios - helps us make http call to the Bitcoin Client

Getting Started with Code

Firstly, you have to ensure Bitcoin core is running on your machine. Let’s setup our .env file, we will be needing the RPC Username and Password for your Bitcoin core running on your machine, also we need the Port number of the network your Bitcoin Core is running on, either Signet, Regtest, Testnet or Mainnet. Your .env file should look like what’s below.

RPC_USER=username
RPC_PASSWORD=password
SIGNET_PORT=38332

I added the Signet port number, because that’s what i would be using for this article, but you can always choose what ever network your Bitcoin Core is running on and insert the appropriate number. Next, let’s create a RPC wrapper function that helps us make request to the Bitcoin client using Axios, we would be doing this in the util folder, the index.js file. Below is how the code would look like and i would explain in the next section.


const axios = require("axios");
const dotenv = require("dotenv");

dotenv.config();

const Axios = async (method, parameter = []) => {
  const USER = process.env.RPC_USER;
  const PASS = process.env.RPC_PASSWORD;
  const SIGNET_PORT = process.env.SIGNET_PORT;

  const RPC_URL = `http://127.0.0.1:${SIGNET_PORT}/`;
  const body = {
    jsonrpc: "1.0",
    id: "curltext",
    method: method,
    params: parameter,
  };

  try {
    const response = await axios.post(RPC_URL, JSON.stringify(body), {
      auth: {
        username: USER,
        password: PASS,
      },
    });
    return response;
  } catch (error) {
    throw error;
  }
};

module.exports = {
  Axios,
};

The Axios function receives to parameter, the first is the method parameter ( you could check here for more methods)which is use to pass a command to the Bitcoin Client, the second Parameter is an Array, which contains parameters that the method we are calling requires. we would see how to use this in the next section when making our first call to the Bitcoin Client.

Let’s make our first call to the Bitcoin client by getting all our unspent transactions (UTXOs) which we would use later as input for creating a psbt transaction. In the index.js file in the root directory write the code below:


const { Axios } = require("./util/index");

const GetUnspentUtxo = async () => {
  try {
    const response = await Axios("listunspent");
    console.log(response.data.result);
    return response.data.result;
  } catch (error) {
    console.log(error);
    throw error;
  }
};

GetUnspentUtxo();

The output should look like this:

[
{
txid: 'e0810d049284c8c36fb680cecb2c338de5708c9608e4fc5b27d108f3037c6329',
vout: 1,
address: 'tb1qkjpw77z3eg46em94deudaajzqw4nn6v3jxtumu',
scriptPubKey: '0014b482ef7851ca2bacecb56e78def64203ab39e991',
amount: 0.00037452,
confirmations: 42,
spendable: true,
solvable: true,
desc: "wpkh([b79bd340/84'/1'/0'/1/91]020ddea8c5575e6cb7e508750b5e27a7a2f555d178267de32f654b07c7ce2ea651)#jsnh98mc",
safe: true
},
...
]

The above snippet shows how we basically use the Axios function, in this case the listunspent method do not have parameter that’s why a second parameter was not passed.

Creating a PSBT Transaction

Creating a psbt transaction is very similar to creating a raw transaction, and we would explore how to use it, to create it we will use the walletcreatefundedpsbt method which takes in two basic parameters and other optional parameter which i won’t cover in this article, you could see more details here.

The first parameter is an array of input’s you want to spend in that transaction. Inputs are basically unspent UTXO’s, you reference the transaction_id and the output index the particular UTXO is in that transaction.

The second parameter is also an array of output’s, that’s basically the address of the user you are sending BTC to and the amount, it’s an object with the key being the address and the amount being the value.

This method helps calculate the fee for the transaction and also helps to insert a change back to one of your address. This method returns a base 64 string value which we would use for signing the transaction, it would also return the fee for the particular transaction.

I would write a simple helper function that helps me select UTXOs to use as inputs in a transaction, please this is not a standard method of selecting UTXOs don’t use in production🙏🙏.

const { Axios } = require("./util/index");

const GetUnspentUtxo = async () => {
  try {
    const response = await Axios("listunspent");
    return response.data.result;
  } catch (error) {
    throw error;
  }
};

const SelectUtxos = async (amount) => {
  const utxos = await GetUnspentUtxo();
  const selectedUtxos = [];
  let currentAmount = 0;
  for (let i = 0; i < utxos.length; i++) {
    let utxo = utxos[i];
    let input = { txid: utxo.txid, vout: utxo.vout };
    currentAmount = currentAmount + utxo.amount * 100000000;
    selectedUtxos.push(input);
    if (currentAmount > amount * 100000000) {
      break;
    }
  }
  console.log(selectedUtxos);
  return selectedUtxos;
};

SelectUtxos(0.0001);

Result

[
{
txid: 'e0810d049284c8c36fb680cecb2c338de5708c9608e4fc5b27d108f3037c6329',
vout: 1
}
]

The function above takes in the amount you would like to send and returns the possible UTXOs you could use in the transaction, please remember you shouldn’t use this in a real application, because you have to put in some basic checks like do i have enough BTC to send and stuffs like that.

Lets’s Create our PSBT transaction

const { Axios } = require("./util/index");

const GetUnspentUtxo = async () => {
  try {
    const response = await Axios("listunspent");
    // console.log(response.data.result);
    return response.data.result;
  } catch (error) {
    console.log(error);
    throw error;
  }
};

const SelectUtxos = async (amount) => {
  const utxos = await GetUnspentUtxo();
  const selectedUtxos = [];
  let currentAmount = 0;
  for (let i = 0; i < utxos.length; i++) {
    let utxo = utxos[i];
    let input = { txid: utxo.txid, vout: utxo.vout };
    currentAmount = currentAmount + utxo.amount * 100000000;
    selectedUtxos.push(input);
    if (currentAmount > amount * 100000000) {
      break;
    }
  }
  return selectedUtxos;
};

const CreatePSBTTransaction = async () => {
  const utxos = await SelectUtxos(0.0001);
  const body = [
    [...utxos],
    [{ tb1qpvf0hh2fmu8pp3mkwwvp38enfwtd534p096vzy: 0.0001 }],
  ];
  try {
    const transaction = await Axios("walletcreatefundedpsbt", body);
    console.log(transaction.data);
  } catch (error) {
    throw error;
  }
};

CreatePSBTTransaction();

Result

result: {
psbt: 'cHNidP8BAHECAAAAASljfAPzCNEnW/zkCJaMcOWNMyzLzoC2b8PIhJIEDYHgAQAAAAD....',
fee: 0.00000141,
changepos: 0
},

The psbt is a 64 string that we would use to sign this transaction, fee is the fee that would be charged for this transaction, and the changepos is the position of the change that’s being paid to my address in the output of this transaction.

Let proceed to sign this transaction, to sign this transaction we would use the walletprocesspsbt method which takes the psbt from the function we have above, you could see more info here. This method signs all the input your wallet keys can sign and returns a base 64 string psbt.


const { Axios } = require("./util/index");
const GetUnspentUtxo = async () => {
try {
const response = await Axios("listunspent");
// console.log(response.data.result);
return response.data.result;
} catch (error) {
console.log(error);
throw error;
}
};
const SelectUtxos = async (amount) => {
const utxos = await GetUnspentUtxo();
const selectedUtxos = [];
let currentAmount = 0;
for (let i = 0; i < utxos.length; i++) {
let utxo = utxos[i];
let input = { txid: utxo.txid, vout: utxo.vout };
currentAmount = currentAmount + utxo.amount * 100000000;
selectedUtxos.push(input);
if (currentAmount > amount * 100000000) {
break;
}
}
return selectedUtxos;
};
const CreatePSBTTransaction = async () => {
const utxos = await SelectUtxos(0.0001);
const body = [
[...utxos],
[{ tb1qpvf0hh2fmu8pp3mkwwvp38enfwtd534p096vzy: 0.0001 }],
];
try {
const transaction = await Axios("walletcreatefundedpsbt", body);
return transaction.data.result.psbt;
} catch (error) {
throw error;
}
};
const SignPsbtTransaction = async (body) => {
const psbt = await CreatePSBTTransaction();
try {
const response = await Axios("walletprocesspsbt", [psbt]);
// return response.data.result.psbt;
console.log(response.data);
} catch (error) {
throw error;
}
};
SignPsbtTransaction()

Result

result: {
psbt: 'cHNidP8BAHECAAAAASljfAPzCNEnW/zkCJaMcOWNMyzLzoC2b8PIhJIEDYHgAQAAAAD...',
complete: true
},

The psbt is a base 64 string value we would use to finalize this transaction, to finalize this transaction we would use the finalizepsbt method which requires the psbt value we got from the result above. It returns a psbt value if the transaction is partially signed and a hex value if the transaction is fully signed, the complete value could either be true or false depending if the transaction is fully signed, you can read more about it here.


const { Axios } = require("./util/index");
const GetUnspentUtxo = async () => {
try {
const response = await Axios("listunspent");
// console.log(response.data.result);
return response.data.result;
} catch (error) {
console.log(error);
throw error;
}
};
const SelectUtxos = async (amount) => {
const utxos = await GetUnspentUtxo();
const selectedUtxos = [];
let currentAmount = 0;
for (let i = 0; i < utxos.length; i++) {
let utxo = utxos[i];
let input = { txid: utxo.txid, vout: utxo.vout };
currentAmount = currentAmount + utxo.amount * 100000000;
selectedUtxos.push(input);
if (currentAmount > amount * 100000000) {
break;
}
}
return selectedUtxos;
};
const CreatePSBTTransaction = async () => {
const utxos = await SelectUtxos(0.0001);
const body = [
[...utxos],
[{ tb1qpvf0hh2fmu8pp3mkwwvp38enfwtd534p096vzy: 0.0001 }],
];
try {
const transaction = await Axios("walletcreatefundedpsbt", body);
return transaction.data.result.psbt;
} catch (error) {
throw error;
}
};
const SignPsbtTransaction = async (body) => {
const psbt = await CreatePSBTTransaction();
try {
const response = await Axios("walletprocesspsbt", [psbt]);
return response.data.result.psbt;
} catch (error) {
throw error;
}
};
const FinalizePsbt = async () => {
const body = await SignPsbtTransaction();
try {
const response = await Axios("finalizepsbt", [body]);
console.log(response.data);
} catch (error) {
throw error;
}
};
FinalizePsbt();

Result

result: {
hex: '0200000000010129637c03f308d1275bfce408968c70e58d332ccbce80b66fc3c88492040d81e00100000000ffffffff0210270000000000001600140b12fbdd49df0e10c7767398189f334b96da46a1af6a000000000000160014ceeea3eb8ce74cb929c7b79e059f9793317c4f4f0247304402205f239dc786333c3ab01b2d0ea81dc89d62443957c2543e19c94eb6004f0ded6602207c0534bcaf8d099bb39716c8852450be21bac4a61c31d76175b81fa42d0edf5f0121020ddea8c5575e6cb7e508750b5e27a7a2f555d178267de32f654b07c7ce2ea65100000000',
complete: true
}

Because our transaction is fully signed we got a hex value, if it’s not fully signed we would get psbt value, now that we have successfully created and signed a PSBT transaction, it would be nice if we could broadcast it to the network and to do that we would use the sendrawtransaction method which take the hex we got from the result above as it’s parameter and returns a transaction id. See more details here.

const { Axios } = require("./util/index");
const GetUnspentUtxo = async () => {
try {
const response = await Axios("listunspent");
// console.log(response.data.result);
return response.data.result;
} catch (error) {
console.log(error);
throw error;
}
};
const SelectUtxos = async (amount) => {
const utxos = await GetUnspentUtxo();
const selectedUtxos = [];
let currentAmount = 0;
for (let i = 0; i < utxos.length; i++) {
let utxo = utxos[i];
let input = { txid: utxo.txid, vout: utxo.vout };
currentAmount = currentAmount + utxo.amount * 100000000;
selectedUtxos.push(input);
if (currentAmount > amount * 100000000) {
break;
}
}
return selectedUtxos;
};
const CreatePSBTTransaction = async () => {
const utxos = await SelectUtxos(0.0001);
const body = [
[...utxos],
[{ tb1qpvf0hh2fmu8pp3mkwwvp38enfwtd534p096vzy: 0.0001 }],
];
try {
const transaction = await Axios("walletcreatefundedpsbt", body);
return transaction.data.result.psbt;
} catch (error) {
throw error;
}
};
const SignPsbtTransaction = async (body) => {
const psbt = await CreatePSBTTransaction();
try {
const response = await Axios("walletprocesspsbt", [psbt]);
return response.data.result.psbt;
} catch (error) {
throw error;
}
};
const FinalizePsbt = async () => {
const body = await SignPsbtTransaction();
try {
const response = await Axios("finalizepsbt", [body]);
// console.log(response.data);
return response.data.result.hex;
} catch (error) {
throw error;
}
};
const SendRawTransaction = async () => {
const hex = await FinalizePsbt();
try {
const response = await Axios("sendrawtransaction", [hex]);
// return response.data.result;
console.log(response.data);
} catch (error) {
throw error;
}
};
SendRawTransaction()

Result

{
result: '20b32efb5af9d23e010f4eb28fff8981cfb829a80054e6c7fceba97c58a71e62',
error: null,
id: 'curltext'
}

The result value is the transaction id for this transaction and you can always check it out on any explorer. With this we’ve successfully created and broadcasted a PSBT.🥳🥳

See Also :