Sitemap

Bitcoin Taproot Hello World

7 min readMay 10, 2025

--

Ok, it’s been a while, but I’m back. And this time, I’m going all-in on Bitcoin tutorials.

In this tutorial, you will learn how to create, verify, and publish a transaction on the Bitcoin blockchain using bitcoinjs-lib on Node.js.

Development Environment Setup

  1. Download and install nvm (win, mac)
  2. $ nvm install --lts
  3. $ nvm use --lts
  4. Launch VSCode or your favorite IDE and enable JavaScript extensions for code completion.
  5. Open a terminal and create a directory called btc-helloworld
  6. $ cd btc-helloworld
  7. $ npm init
  8. $ npm install bitcoinjs-lib@7.0.0-rc.0 tiny-secp256k1 ecpair bip32 bip39

At the time of writing, it installed the following library versions:

bip32 5.0.0-rc.0
bip39 3.1.0
bitcoinjs-lib 7.0.0-rc.0
ecpair 3.0.0
tiny-secp256k1 2.2.3

Generate Bitcoin Wallet Addresses

First, let’s create a file called generate_address.js to be able to create bitcoin addresses, don’t use this for production.
Use for development and testing on test nets ONLY:

// A javascript Bitcoin library for node.js and browsers.
const bitcoin = require('bitcoinjs-lib');
// JavaScript implementation of Bitcoin BIP39: Mnemonic code for generating deterministic keys
const bip39 = require('bip39');
// Hierarchical Deterministic Wallets (Bip32) Implementation in JavaScript
const { BIP32Factory } = require('bip32');
// Secp256k1 is the name of the elliptic curve used by Bitcoin to implement its public key cryptography
const ecc = require('tiny-secp256k1');

// Initialize libraries
bitcoin.initEccLib(ecc);
const BIP32 = BIP32Factory(ecc);

Insert the lines below to configure for testnet

const network = bitcoin.networks.testnet;
let path = '';

// m / purpose' / coin_type' / account' / change / address_index
if (network == bitcoin.networks.testnet) {
// test net
path = "m/86'/1'/0'/0/0";
} else {
// main net
path = "m/86'/0'/0'/0/0";
}

Now the below lines to create a random mnemonic, and get the child to generate the p2tr address

const mnemonic = bip39.generateMnemonic();
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = BIP32.fromSeed(seed, network);
const child = root.derivePath(path);

Get the X-Coordinate alone to create the taproot output script p2tr:

// Removes the first byte (the 0x02 or 0x03 prefix) from the 33-byte compressed public key
// leaving the 32-byte X-coordinate to construct the Taproot output script (P2TR address).
// Schnorr signatures and Taproot scripts operate on the X-coordinate alone, assuming the Y-coordinate has even parity (a convention that simplifies processing and reduces size).
// By dropping the prefix byte, the public key becomes 32 bytes, which is used as the internal public key for Taproot.
internalPubkey = Buffer.from(child.publicKey.slice(1, 33));

const p2tr = bitcoin.payments.p2tr({
internalPubkey: internalPubkey,
network: network
});

Last, print the mnemonic and p2tr:

console.log('Mnemonic:', mnemonic);
console.log('Taproot Address:', p2tr.address);

Node run the following command from terminal to generate your very first taproot address for testing purposes only:

$ node generate_address.js

Output, example:

Mnemonic: diagram average divide urban office bench ready dirt popular point robust lawn
Taproot Address: tb1pzt53e6nghqw40zglfzn56cdj82xx68nsd84z3yn5pl6xxj2v3n9qrsheeh

Fund the p2tr address

Go to https://mempool.space/testnet4/faucet to fund your newly created taproot address

Press enter or click to view image in full size

Click on the transaction link with the transaction that funds your address and it should look something like this:

Press enter or click to view image in full size

From the transaction details page, copy the Transaction ID and output index with your p2tr address. (you can get the index by hovering over the flow image chart like shown below):

Press enter or click to view image in full size

Create the Transaction

Create a file called btc_transaction.js, add the following code, very similar to the generate_address file above but I add the ecpair library to be able to sign the transaction for broadcasting it in the bitcoin network and also added the change derived path to send our change bitcoin after our transaction:

// A javascript Bitcoin library for node.js and browsers.
const bitcoin = require('bitcoinjs-lib');
// JavaScript implementation of Bitcoin BIP39: Mnemonic code for generating deterministic keys
const bip39 = require('bip39');
// Hierarchical Deterministic Wallets (Bip32) Implementation in JavaScript
const { BIP32Factory } = require('bip32');
// Secp256k1 is the name of the elliptic curve used by Bitcoin to implement its public key cryptography
const ecc = require('tiny-secp256k1');
// A library for managing SECP256k1 keypairs
const { ECPairFactory } = require('ecpair');

// Initialize libraries
bitcoin.initEccLib(ecc);
const bip32 = BIP32Factory(ecc);
const ECPair = ECPairFactory(ecc);

const network = bitcoin.networks.testnet;
let path = '';
let path_change = '';

// m / purpose' / coin_type' / account' / change / address_index
if (network == bitcoin.networks.testnet) {
// test net
path = "m/86'/1'/0'/0/0";
path_change = "m/86'/1'/0'/1/0";
} else {
// main net
path = "m/86'/0'/0'/0/0";
path_change = "m/86'/0'/0'/1/0";
}

Paste your mnemonic:

// Wallet Mnemonic
const mnemonic = 'diagram average divide urban office bench ready dirt popular point robust lawn';

Now paste the details from the transaction you copied from the transaction details page above

//  Funding Transaction Details
const fundingTxId = '1862e5293c83e138a5768de66a4dc1dc529cbd8189236a88f2923b1249b63601';
const fundingIndex = 1;
const fundingAmount = BigInt(5000);

Now enter the new output transaction details:

// New Transaction Output Details
const message = 'Hello World';
const fee = 150n;

Generate the seed, keypair and p2tr from the mnemonic

// Generate seed from mnemonic
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bip32.fromSeed(seed, network);
const taprootChild = root.derivePath(path);
// Generate the key pair to verify and sign the input we're going to spend from the transaction
const taprootKeyPair = ECPair.fromPrivateKey(taprootChild.privateKey, { network: network });
// Internal public key (x-only, 32 bytes)
const internalPubkey = taprootKeyPair.publicKey.slice(1, 33);
// Generate the p2tr to get the output for the spending input
const p2tr = bitcoin.payments.p2tr({ internalPubkey: internalPubkey, network });

Now create the change p2tr so that we receive the change after paying for fees for our transaction, for simplicity I’m using the same mnemonic to generate change addresses:

// Create the change p2tr to get the address where we're going to receive the change
const changeChild = root.derivePath(path_change);
const changeInternalPubkey = changeChild.publicKey.slice(1, 33);
const changep2tr = bitcoin.payments.p2tr({ internalPubkey: changeInternalPubkey, network });

Create the transaction using psbt and add the transaction input details we want to spend

// Create PSBT
const psbt = new bitcoin.Psbt({ network });

// Add Taproot input
psbt.addInput({
hash: fundingTxId,
index: fundingIndex,
witnessUtxo: {
script: Uint8Array.from(p2tr.output),
value: inputAmount,
},
tapInternalKey: internalPubkey,
sequence: 0xfffffffd // Enable RBF by setting sequence < 0xffffffff
});

hash: funding ID, you copied this from the transaction details page when you funded your addresses using the faucet

index: output index you’re trying to spend, in this case, the one that received the bitcoin from the faucet

witnessUtxo: contains the script and the value, these must match exacly as in the transaction details page

tapInternalKey: the internal x-only Pubkey

sequence: Enables RBF by setting sequence < 0xffffffff

Add the first output, in our case this is where we’re going to print Hello World using OP_RETURN as shown below, we don’t specify a value here because we’re going to add the change to the other output:

// Add OP_RETURN output
const data = Buffer.from(message, 'utf8');
const opReturnScript = bitcoin.script.compile([bitcoin.opcodes.OP_RETURN, data]);
psbt.addOutput({
script: Uint8Array.from(opReturnScript), // Convert Buffer to Uint8Array
value: 0, // OP_RETURN outputs should have a value of 0
});

Add the change output, the change value is the amount we’re taking from the input we’re spending minus the fee we want ot pay miners, for testnet I’ve been using 150 as fee and it has been working well:

// Add change output
const changeAmount = fundingAmount - fee;
if (changeAmount < 0n) {
throw new Error("Input amount is less than the fee");
}

psbt.addOutput({
address: changep2tr.address,
value: changeAmount,
});

Sign and finalize

// Used for signing, since the output and address are using a tweaked key
// We must tweak the signer in the same way.
const tweakedChildNode = taprootKeyPair.tweak(
Buffer.from(bitcoin.crypto.taggedHash('TapTweak', internalPubkey))
);

psbt.signInput(0, tweakedChildNode);
psbt.finalizeAllInputs();

const tx = psbt.extractTransaction();
console.log('Transaction:', tx.toHex());

Run it!

node btc_transaction.js

Output example:

Transaction: 020000000001010136b649123b92f2886a238981bd9c52dcc14d6ae68d76a538e1833c29e562180100000000fdffffff0200000000000000000d6a0b48656c6c6f20576f726c64f21200000000000022512021833c7c66c2a2a503f5502275926d2b07cdabc5fe5b2a38c3001fe2209abb26014091b3884099860ea37a104359bfa4a06a95bacf8333f6a0377abfb530b92365d48e1908d8089158faf0dda6606e69eb3e4bea1cc46beb7dd430ace846ac0cd03b00000000

Test the transaction on https://mempool.space/testnet4/tx/test and it should show a green checkmark if it passes the test as shown below

Press enter or click to view image in full size

Broadcast the transaction using https://mempool.space/testnet4/tx/push as shown below; If successful it’s going to show a link to the broadcasted transaction.

Press enter or click to view image in full size

Open the transaction details page and you should see the “Hello World” test in one of the output:

Press enter or click to view image in full size

Congratulations, you have broadcasted your first bitcoin transaction on testnet4 🚀

Here’s the repository with the full source code:

Ideas:

  • Modify the code to use an API to test, and publish the transaction directly from javascript

--

--

Salvador Guerrero
Salvador Guerrero

Written by Salvador Guerrero

Computer Science Engineer, Cross-Platform App Developer, Open Source contributor. 🇲🇽🇺🇸