Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
252f877
added pmt builder considering the witnesses
julianlen Mar 30, 2026
a033ac2
index is left as it was, getWtxid is more an utils than part of the i…
julianlen Mar 30, 2026
94d878e
renamings, deleted useless exports, moved shared methods into a utils
julianlen Apr 1, 2026
f1dc68c
modified getTransactionWithRetry docjs and added semicolon to functions
julianlen Apr 1, 2026
247f327
fixed getAllTxs jsdoc with transactionsClient
julianlen Apr 1, 2026
2816e5b
deleted unaccurate comment
julianlen Apr 1, 2026
74c65da
renaming targetTxWTxId to targetWtxid
julianlen Apr 1, 2026
8a39ee6
added to fail if there is no rawTx
julianlen Apr 1, 2026
ab4f395
fixed README
julianlen Apr 6, 2026
59cb5d0
Improved getWtxid and started using getWtxids to avoid repeating. Als…
julianlen Apr 6, 2026
c01ecf4
extracted 429 to a constant
julianlen Apr 6, 2026
8794e52
getTransactionWithRetry will throw an error instead of returning null
julianlen Apr 7, 2026
765c82b
Modified comments and improved the error thrown by getTransactionWith…
julianlen Apr 7, 2026
987ae3f
renamed and reorder parameters in getInformationReadyForRegister file…
julianlen Apr 7, 2026
db4801f
Extracted getBlockTxIdsByTransactionHash so pmt-builder and pmt-witne…
julianlen Apr 7, 2026
aaf2d0f
both getInformationReadyForRegister shared the same setup, so extract…
julianlen Apr 7, 2026
e578cf1
modified tests to have blockIds with witness, without, and mixed
julianlen Apr 7, 2026
eabd63c
improved pmt-builder-uitls comment
julianlen Apr 8, 2026
bcbec72
improved assertion method name assertGetWtxidsResult
julianlen Apr 8, 2026
49dbf22
getInformationReadyForRegisterBtcTransaction returns only one pmt dep…
julianlen Apr 8, 2026
9eda8a8
tiny renaming in getInformationReadyForRegisterBtcTransaction
julianlen Apr 8, 2026
26f6444
created a getBlockWtxidsWithTargetWtxidByTransactionHash to avoid get…
julianlen Apr 8, 2026
7882627
avoid to.be.an(array)
julianlen Apr 8, 2026
deba852
renamed tests and variables
julianlen Apr 8, 2026
4e73115
if hasWitness it should call getWtxids
julianlen Apr 8, 2026
ff9b8c7
unify naming to expectedWtxid in testing
julianlen Apr 8, 2026
de4949e
avoid assigning and then returning
julianlen Apr 8, 2026
3f61b5a
renamed getWtxids to fetchBlockWtxidsWithTargetWtxid
julianlen Apr 8, 2026
158814b
instead of testing fetchBlockWtxidsWithTargetWtxid we are testing bui…
julianlen Apr 8, 2026
e37266d
renamed blockTxIds with blockTxids
julianlen Apr 8, 2026
88dacbf
Added 4 test cases real txs and blocks with and without witness for m…
julianlen Apr 13, 2026
6e24a04
Deleted the sleep in fetchBlockWtxidsWithTargetWtxid since any error …
julianlen Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This library can be used to build a partial merkle tree for a block of transacti

To build the tree, simply call the `buildPMT` function and pass the `leaves` as a list of strings and a `filteredHash` in string format, i.e buildPMT(leaves: string[], filteredHash: string)

First parameter {leaves}: An array of transaction hash of all transactions without witness(txid) in a block.
First parameter {leaves}: An array of transaction hashes of all transactions without witness(txid) in a block.
Second parameter {filteredHash}: transaction hash (the hash without witness).

The function returns an object with the following fields:
Expand Down Expand Up @@ -50,12 +50,11 @@ console.log("Result: ", resultPmt);

This library can be used via a tool(this tool uses mempool.js api to get transactions) that exists in the tool/pmt-builder.js file via the following command:

`node tool/pmt-builder.js network blockHash txHash`,
`node tool/pmt-builder.js network txHash`,

For example: `node tool/pmt-builder.js testnet 00000000000003d91235b675366fc6c26e0ea4b2f125fd292f164247d4e5b07e ac72bd61c72ac8143e5638998f479bdfc5834fce9576fa2054c7be93313abd66`
For example: `node tool/pmt-builder.js testnet ac72bd61c72ac8143e5638998f479bdfc5834fce9576fa2054c7be93313abd66`

`network`: testnet or mainnet
`blockHash`: block hash in hex format
`txHash`: filtered transaction hash in hex format

### Getting a pegin btc transaction information ready to register
Expand Down Expand Up @@ -94,7 +93,7 @@ Simply go to `Write Contract`, then click on `Connect Wallet`, Click on `registe

Notice that the hex value of these fields (`tx` and `pmt`) have the `0x` prefix, that's because the `registerBtcTransaction` requires them to be like that.

To check if it has been successfully registered, go to `Read Contract`, then click on `isBtcTxHashAlreadyProcessed` to expand it, paste the btc transaction hash and click on `Read`. If it returns `true`, then it has been registred. You can also use `getBtcTxHashProcessedHeight` to get the block number where it was processed.
To check if it has been successfully registered, go to `Read Contract`, then click on `isBtcTxHashAlreadyProcessed` to expand it, paste the btc transaction hash and click on `Read`. If it returns `true`, then it has been registered. You can also use `getBtcTxHashProcessedHeight` to get the block number where it was processed.

### Getting a coinbase transaction information ready to register

Expand All @@ -104,7 +103,7 @@ To ease the process of registering a coinbase transaction, the function `getInfo

This is how to use it:

> node tool/getInformationReadyForRegisterCoinbaseBtcTransaction.js<network> <btcCoinbaseTransactionHash>
> node tool/getInformationReadyForRegisterCoinbaseBtcTransaction.js <network> <btcCoinbaseTransactionHash>

For example:

Expand Down
56 changes: 54 additions & 2 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
let expect = require('chai').expect;
let pmtBuilder = require('../index');
const expect = require('chai').expect;
const pmtBuilder = require('../index');
const txs3000 = require('./resources/3000-txs');
const { getWtxids } = require('../tool/pmt-builder-utils');

describe('PMT Builder', () => {
it('should create a valid PMT, block with a single transaction', () => {
Expand Down Expand Up @@ -109,3 +110,54 @@ describe('PMT Builder', () => {
expect(() => pmtBuilder.buildPMT(blockTransactions, randomHash)).to.throw('Filtered hash provided is not part of the leaves');
});
});

describe('getWtxids', () => {
const transactions = {
// https://mempool.space/testnet/tx/2bf2c26cf756564f1d7f17f89882da32f20d694d9f2c8f04962c1116ccdf0bcf
'2bf2c26cf756564f1d7f17f89882da32f20d694d9f2c8f04962c1116ccdf0bcf': {
rawTx: '02000000039a6075be99f85280c302d61054923f1b7f8a00718f3b7469bcf8e2a1cfebed06000000006b483045022100d087ca67b092c59ac07e81717c2ca62ff2952c4b42762168136b1bee326d6b9e022002c3b07f08a4ed5805b1185bd022e840d89fa952c583c6a5a76f998e0e962dfc012102e0b5d0549e132e3fdae7b6d11ad5ea15eb3e452681dd148867d85f7f83bc61cbfdffffffe68340cfb309aaacdb8bbe279b87e568c97416b42a8c57b72fb0fe125cf53d11000000006b48304502210097012cb79a48d914e10c9640e41d27b6db65dcedece5312ada04fc000b2e63bf022018e46e94271821006a2d2609b81ba44e0c1f91456e6b07438db23cc263451a00012102e0b5d0549e132e3fdae7b6d11ad5ea15eb3e452681dd148867d85f7f83bc61cbfdffffff460e61cdee3e4db4ab5c38c9d7254c77f93760b4c9ba43c64b136905726eece0000000006b4830450221008b893aea2f2222ea8bbc25ef65fee8a68c78d3833aeb9f8bf107800ac5152ddc022001259c70925c240da7dc50bdbc6f0a0b762d6db43458fee7deed9a61511a358d012102e0b5d0549e132e3fdae7b6d11ad5ea15eb3e452681dd148867d85f7f83bc61cbfdffffff0212520200000000001976a914bafc55fad94b8aa8ff1b94c28ecc0a47c46c7b1688ac90ab1e000000000017a9141eb5d0b64f652150d42cb0ccbc08ec710cc35218875fb44a00',
wtxid: '2bf2c26cf756564f1d7f17f89882da32f20d694d9f2c8f04962c1116ccdf0bcf',
},
// https://mempool.space/testnet/tx/d53d1f237239aebbd2cb16f9c1f85e42cde93e02b1fb2d247114a0db2e0e30d0
'd53d1f237239aebbd2cb16f9c1f85e42cde93e02b1fb2d247114a0db2e0e30d0': {
rawTx: '02000000000101fc11ed2e485faf6370461ec09b61e1d65d3e4f40dba26a6926356ec859436fac0100000023220020f8e08a83ad7e3ce13df880cdea97aba6d145b6244ead9f8512b159ebea15be55ffffffff02e8a31e00000000001976a914cab5925c59a9a413f8d443000abcc5640bdf067588acc00130030000000017a9147214a88d8e15f5a09050bff1d1a85f19d6e3b820870500473044022042a5bb9a1eb34b56a16602a4b29e6f594d82cf68a7758e05ccb2556396574b9802202900553564e44ca4e4718cae91767db380d20f2e6d0dd7fbcd9599eee5713d5101473044022043d7fe49578ff16e8abfda7951bad1aeb9c0a93d5157367ea03e09101c0d3770022061270ce8edfa9dae3e5e135d8673dd16f6c646c480cca42a11fe83f2960833710100db6452210379d78dcae0be90715a088413c588da6a9381aae42e504f6e05c7b5204ed5bf3a2103d9d48cdc0fdf039d08371c64b1e86e1715e9898d4680595f1d4e3398dbdd9e9e2103df89bd3d49c1ebdef2e9b4e77c84e048dbbcf7c41735b073a68fdcf5d086bd2853ae670350cd00b27552210216c23b2ea8e4f11c3f9e22711addb1d16a93964796913830856b568cc3ea21d3210275562901dd8faae20de0a4166362a4f82188db77dbed4ca887422ea1ec185f1421034db69f2112f4fb1bb6141bf6e2bd6631f0484d0bd95b16767902c9fe219d4a6f53ae6800000000',
wtxid: '84606c5f6c896f75a4b5d38c756a33cf7c2cd5fa308fe93d708a3b04d281f34b',
},
};

const createTransactionsClientMock = () => {
const requestedTxids = [];

return {
requestedTxids,
getTxHex: async ({ txid }) => {
requestedTxids.push(txid);
return transactions[txid].rawTx;
},
};
};

it('should return the txid as the wtxid for a block without witness transactions', async () => {
const blockTxids = ['2bf2c26cf756564f1d7f17f89882da32f20d694d9f2c8f04962c1116ccdf0bcf'];
const targetTxId = blockTxids[0];
const transactionsClient = createTransactionsClientMock();

const result = await getWtxids(transactionsClient, blockTxids, targetTxId);

expect(result.targetWtxid).to.equal(transactions[targetTxId].wtxid);
expect(result.blockWtxids.length).to.equal(1);
expect(result.blockWtxids).to.be.an('array').that.includes(transactions[targetTxId].wtxid);
});

it('should return the witness hash for a block with witness transactions', async () => {
const blockTxids = ['d53d1f237239aebbd2cb16f9c1f85e42cde93e02b1fb2d247114a0db2e0e30d0'];
const targetTxId = blockTxids[0];
const transactionsClient = createTransactionsClientMock();

const result = await getWtxids(transactionsClient, blockTxids, targetTxId);

expect(result.targetWtxid).to.equal(transactions[targetTxId].wtxid);
expect(result.blockWtxids.length).to.equal(1);
expect(result.blockWtxids).to.be.an('array').that.includes(transactions[targetTxId].wtxid);
});
});
27 changes: 15 additions & 12 deletions tool/getInformationReadyForRegisterBtcTransaction.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
const mempoolJS = require("@mempool/mempool.js");
const pmtBuilder = require("../index");
const { getWtxids, getTransactionWithRetry } = require("./pmt-builder-utils");

const getInformationReadyForRegisterBtcTransaction = async (transactionHash, network) => {

const { bitcoin: { blocks, transactions } } = mempoolJS({
const bitcoin = mempoolJS({
hostname: 'mempool.space',
network // 'testnet' | 'mainnet'
});

const transaction = await transactions.getTx({ txid: transactionHash });
const blockHash = transaction.status.block_hash;
const blockHeight = transaction.status.block_height;
const transactionsClient = bitcoin.bitcoin.transactions;
const transaction = await transactionsClient.getTx({ txid: transactionHash });

const blockTxids = await blocks.getBlockTxids({ hash: blockHash });
const rawBtcTransaction = await transactions.getTxHex({ txid: transactionHash });
const blockHash = transaction.status.block_hash;
const blocksClient = bitcoin.bitcoin.blocks;
const blockTxids = await blocksClient.getBlockTxids({ hash: blockHash });

const resultPmt = pmtBuilder.buildPMT(blockTxids, transactionHash);

const pmt = resultPmt.hex;
const blockHeight = transaction.status.block_height;

const { blockWtxids, targetWtxid } = await getWtxids(transactionsClient, blockTxids, transactionHash);
const resultPmtConsideringWitness = pmtBuilder.buildPMT(blockWtxids, targetWtxid);
const pmtConsideringWitness = resultPmtConsideringWitness.hex;
const rawTargetBtcTransaction = await getTransactionWithRetry(transactionsClient, transactionHash);

const informationReadyForRegisterBtcTransaction = {
tx: `0x${rawBtcTransaction}`,
tx: `0x${rawTargetBtcTransaction}`,
height: blockHeight,
pmt: `0x${pmt}`,
pmtConsideringWitness: `0x${pmtConsideringWitness}`,
};

return informationReadyForRegisterBtcTransaction;

};

(async () => {
try {

const network = process.argv[2];
const transactionHash = process.argv[3];

Expand All @@ -43,4 +47,3 @@ const getInformationReadyForRegisterBtcTransaction = async (transactionHash, net
console.log(e);
}
})();

68 changes: 8 additions & 60 deletions tool/getInformationReadyForRegisterCoinbaseBtcTransaction.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
const bitcoinJs = require('bitcoinjs-lib');
const merkleLib = require('merkle-lib');
const mempoolJS = require("@mempool/mempool.js");
const { sleep, getTransactionWithRetry, REQUEST_DELAY_MS } = require("./pmt-builder-utils");
const pmtBuilder = require("../index");

const REQUEST_DELAY_MS = 200; // Delay between individual TX detail requests (200ms = 0.2 seconds)
const MAX_RETRIES = 5; // Max retries for a failed request
const RETRY_DELAY_FACTOR_MS = 1000; // Base delay for retries (1000ms = 1 second)

let blocksClient;
let transactionsClient;

Expand All @@ -23,57 +20,15 @@ function clearProgress() {
process.stdout.write('\x1b[2K\r'); // ANSI escape code to clear the current line
}

/**
* Fetches transaction details with retry logic in case of rate limiting errors.
*
* This function attempts to retrieve transaction details for a given transaction ID
* using the specified transaction API. If a rate-limiting error is encountered
* (e.g., HTTP 429 status or related error messages), it retries the request with
* exponential backoff until the maximum retry attempts are exhausted.
*
* @param {string} txId - The transaction ID for which details need to be fetched.
* @param {number} [retries=0] - The current retry attempt count. Defaults to 0.
* @returns {Promise<Object|null>} - A promise that resolves to the transaction details if
* the operation is successful, or `null` if all retries fail or an unexpected error occurs.
* @throws {Error} - If the function encounters an unrecoverable error other than rate limiting.
*/
const getTransactionWithRetry = async (txId, retries = 0) => {
try {
return await transactionsClient.getTxHex({txid: txId});
} catch (error) {
// mempool.js might wrap the error, so we check for common indicators of 429
const isRateLimitError = error.response && error.response.status === 429;

// Sometimes the error message might contain clues if status is not directly 429
const isMempoolSpecificRateLimit = error.message && error.message.includes('Too Many Requests');

if ((isRateLimitError || isMempoolSpecificRateLimit) && retries < MAX_RETRIES) {
const delay = RETRY_DELAY_FACTOR_MS * Math.pow(2, retries); // Exponential backoff
console.warn(`Rate limit hit for ${txId}. Retrying in ${delay / 1000} seconds... (Attempt ${retries + 1}/${MAX_RETRIES})`);
await sleep(delay);
return getTransactionWithRetry(txId, retries + 1);
} else {
console.error(`Error fetching details for txid ${txId} after ${retries} retries:`, error.message);
return null;
}
}
};

/**
* Sleeps for a given number of milliseconds.
* @param {number} ms - The number of milliseconds to sleep.
* @returns {Promise<void>} A Promise that resolves after the specified delay.
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

/**
* Retrieves all transaction objects in a block by their transaction IDs.
* @param {Object} transactionsClient - Client instance used to fetch transaction data (must provide `getTxHex`).
* @param {string[]} txIds - An array of txIds.
* @returns {Promise<string[]>} An array of transactions.
*/
const getAllTxs = async (txIds) => {
const getAllTxs = async (transactionsClient, txIds) => {
if (txIds.length === 0) {
console.log("No transactions found in the block.");
return [];
Expand All @@ -91,19 +46,12 @@ const getAllTxs = async (txIds) => {
// Update progress before each request
updateProgress(i + 1, totalTxs);

// mempool.js getTx method returns an object that directly contains 'wtxid'
const tx = await getTransactionWithRetry(txId);

if (tx) {
txs.push(bitcoinJs.Transaction.fromHex(tx));
} else {
console.error(`Failed to fetch transaction details for txId: ${txId}. It might not exist or is malformed.`);
throw new Error('No wtxid found for txId: ' + txId);
}
const tx = await getTransactionWithRetry(transactionsClient, txId);
txs.push(bitcoinJs.Transaction.fromHex(tx));

// Apply throttling delay, but not after the very last request
if (i < txIds.length - 1) {
await sleep(REQUEST_DELAY_MS);
await sleep();
}
}

Expand Down Expand Up @@ -134,15 +82,15 @@ const getInformationReadyForRegisterCoinbaseBtcTransaction = async (transactionH
const blockTxIds = await blocksClient.getBlockTxids({ hash: blockHash });

const coinbaseTxId = blockTxIds[0];
const rawCoinbaseBtcTx = await transactionsClient.getTxHex({ txid: coinbaseTxId });
const rawCoinbaseBtcTx = await getTransactionWithRetry(transactionsClient, coinbaseTxId);
const coinbaseTx = bitcoinJs.Transaction.fromHex(rawCoinbaseBtcTx);

// Hack to get the coinbase transaction hash without witness data
const coinbaseTxWithoutWitness = bitcoinJs.Transaction.fromBuffer(coinbaseTx.__toBuffer(undefined, undefined, false));
const coinbaseTxHashWithoutWitness = coinbaseTxWithoutWitness.getId();

const witnessReservedValue = coinbaseTx.ins[0].witness[0].toString('hex');
const txs = await getAllTxs(blockTxIds);
const txs = await getAllTxs(transactionsClient, blockTxIds);

// Calculate witnessRoot
const hashesWithWitness = txs.map( x => Buffer.from(x.getHash(true)));
Expand Down
Loading
Loading