UNPKG

bitcore-node

Version:

A blockchain indexing node with extended capabilities using bitcore

540 lines (497 loc) 19.9 kB
import supertest from 'supertest'; import { expect } from 'chai'; import app from '../../src/routes'; import { BitcoinBlockStorage } from '../../src/models/block'; import { TransactionStorage } from '../../src/models/transaction'; import { intAfterHelper, intBeforeHelper } from '../helpers/integration'; import { expectObjectToHaveProps, minutesAgo, resetDatabase } from '../helpers'; import sinon from 'sinon'; import { ChainStateProvider } from '../../src/providers/chain-state'; import { CoinStorage } from '../../src/models/coin'; const request = supertest(app); async function addBlocks( blocks: { chain: 'BTC' | 'BCH'; height: number; hash?: string; time?: Date; transactions?: { txid?: string; fee: number; size: number; coinbase?: boolean; inputs?: number[]; outputs?: number[]; }[] }[] ) { for (const block of blocks) { const { chain, height } = block; const hash = block.hash || '2c07decae68f74d6ac20184cce0216388ea66f0068cde511bb9c51f0691539a8'; const transactions = block.transactions || []; const time = block.time || new Date('2025-07-07T17:16:38.002Z'); await BitcoinBlockStorage.collection.insertOne({ network: 'regtest', chain: chain, hash: hash, bits: 545259519, height: height, merkleRoot: '760a46b4f94ab17350a3ed299546fb5648c025ad9bd22271be38cf075c9cf3f4', nextBlockHash: '47bab8f788e3bd8d3caca2a5e054e912982a0e6dfb873a7578beb8fac90eb87d', nonce: 0, previousBlockHash: '0a60c6e93a931e9b342a6c258bada673784610fdd2504cc7c6795555ef7e53ea', processed: true, reward: 1250000000, size: 214, time: time, timeNormalized: time, transactionCount: 1, version: 805306368 }); for (const tx of transactions) { const { fee, size } = tx; const inputs = tx.inputs || []; const outputs = tx.outputs || []; const txid = tx.txid || 'da848d4c5a9d690259f5fddb6c5ca0fb0e52bc4a8ac472d3784a2de834cf448e'; const coinbase = tx.coinbase!!; await TransactionStorage.collection.insertOne({ chain: chain, network: 'regtest', txid: txid, blockHash: hash, blockHeight: height, blockTime: new Date('2025-07-07T17:38:02.000Z'), blockTimeNormalized: new Date('2025-07-07T17:38:02.000Z'), coinbase: coinbase, fee: fee, inputCount: inputs.length || 1, outputCount: outputs.length || 1, locktime: 0, size: size, value: 10_000_000, wallets: [] }); for (const input of inputs) { await CoinStorage.collection.insertOne({ chain: chain, network: 'regtest', value: input, mintTxid: '52e76c33561b0fc31ecf56e101c4f582d85e385381f3da3e5f5aabdb1b939f90', spentTxid: txid, spentHeight: height, mintHeight: height - 1, mintIndex: 0, script: Buffer.from('aiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPk'), coinbase: coinbase, address: 'bcrt1qxxm47l2d6hrl8e9w9rq6w9klxav5c9e76jehw8', wallets: [] }); } for (let i = 0; i < outputs.length; i++) { const output = outputs[i]; await CoinStorage.collection.insertOne({ chain: chain, network: 'regtest', value: output, mintTxid: txid, spentTxid: 'c9d06466adaf5322f619c603fddb8a325cb6cdfcb9dffaa4e1919e896b2b98d7', spentHeight: -2, mintHeight: height, mintIndex: i, script: Buffer.from('aiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPk'), coinbase: coinbase, address: 'bcrt1qxxm47l2d6hrl8e9w9rq6w9klxav5c9e76jehw8', wallets: [] }); } } } } describe('Routes', function() { let sandbox; const tipHeight = 103; before(async function() { this.timeout(15000); await intBeforeHelper(); await resetDatabase(); await addBlocks([ { chain: 'BTC', height: 99, time: minutesAgo(50) }, { chain: 'BTC', height: 100, hash: '4fedb28fb20b5dcfe4588857ac10c38c6d67e8267e35478d8bcca468c9114bbe', time: minutesAgo(40), transactions: [ { txid: '3c683a2deac83349d0da065baafc326a42f0f0630199cedd34beb52d8d16d11c', fee: 0, size: 133, coinbase: true, outputs: [ 5000000000, 0 ] }, { txid: 'f1bc9ab3ee4c44d304b06cb09ee1c323b072d1967f3e1c3d1bd1067eae07bd25', fee: 20000, size: 1056, inputs: [130000], outputs: [100000, 10000 ] }, { txid: '85cb924a14f354aae71afa503e057e570290c769b0fec10d149c7ea55d100f94', fee: 20000, size: 1056, inputs: [130000], outputs: [100000, 10000 ] }, { txid: 'a4aa0cce47e70df51407ba864be9d667b71ecb2b5ee01d48b1bb29ba32436ed2', fee: 25000, size: 1056, inputs: [135000], outputs: [100000, 10000 ] }, { txid: '86950d79deed75bcbe4e6345f6e87390a02477cfc8492e3d93702b5396ea746d', fee: 30000, size: 1056, inputs: [140000], outputs: [100000, 10000 ] }, { txid: '4541c61085876bbe91ed82468c46d9a5aa2df0e14b1833c1c1cd241f2f143bd6', fee: 35000, size: 1056, inputs: [100000, 45000 ], outputs: [ 100000 , 10000 ]}, ] }, { chain: 'BTC', height: 101, time: minutesAgo(30), transactions: [ { fee: 0, size: 133, coinbase: true } ] }, { chain: 'BTC', height: 102, time: minutesAgo(20) }, { chain: 'BTC', height: tipHeight, time: minutesAgo(10), transactions: [ { fee: 0, size: 133, coinbase: true }, { fee: 9000, size: 1056 }, { fee: 10000, size: 1056 }, { fee: 11000, size: 1056 }, ] }, { chain: 'BCH', height: 100, transactions: [ { fee: 0, size: 133, coinbase: true }, { fee: 2000, size: 1056 }, { fee: 2000, size: 1056 }, { fee: 2500, size: 1056 }, { fee: 3000, size: 1056 }, { fee: 3500, size: 1056 } ] }, { chain: 'BCH', height: 101 }, { chain: 'BCH', height: 102 } ]); }); beforeEach(async () => { sandbox = sinon.createSandbox(); }); after(async () => intAfterHelper()); afterEach(async () => { sandbox.restore(); }); it('should respond with a 404 status code for an unknown path', done => { request.get('/unknown').expect(404, done); }); describe('Block', function() { const testBlock = (block: any) => { const BlockProps = { chain: 'string', network: 'string', hash: 'string', height: 'number', version: 'number', size: 'number', merkleRoot: 'string', time: 'string', timeNormalized: 'string', nonce: 'number', bits: 'number', previousBlockHash: 'string', nextBlockHash: 'string', reward: 'number', transactionCount: 'number' }; expectObjectToHaveProps(block, BlockProps); expect(block.chain).to.not.equal(''); expect(block.network).to.not.equal(''); expect(block.hash).to.have.length(64); expect(block.merkleRoot).to.have.length(64); expect(block.height).to.be.at.least(0); expect(block.nonce).to.be.at.least(0); expect(block.bits).to.be.at.least(0); } it('should get blocks on BTC regtest', done => { request.get('/api/BTC/regtest/block').expect(200, (err, res) => { if (err) console.error(err); const blocks = res.body; for (const block of blocks) { expect(block).to.include({chain: 'BTC', network: 'regtest'}); testBlock(block); } done(); }); }); it('should get blocks after 101 on BTC regtest', done => { request.get(`/api/BTC/regtest/block?sinceBlock=101`).expect(200, (err, res) => { if (err) console.error(err); const blocks = res.body; for (const block of blocks) { expect(block.height).to.be.greaterThan(101); expect(block).to.include({chain: 'BTC', network: 'regtest'}); testBlock(block); } done(); }); }); it('should get 3 blocks with limit=3 on BTC regtest', done => { request.get(`/api/BTC/regtest/block?limit=3`).expect(200, (err, res) => { if (err) console.error(err); const blocks = res.body; expect(blocks.length).to.equal(3); for (const block of blocks) { expect(block).to.include({chain: 'BTC', network: 'regtest'}); testBlock(block); } done(); }); }); it('should respond with a 200 code for block tip and return expected data', done => { request .get('/api/BTC/regtest/block/tip') .expect(200, (err, res) => { if (err) console.error(err); const block = res.body; expect(block).to.include({chain: 'BTC', network: 'regtest', height: tipHeight}); testBlock(block); done(); }); }); it('should get block by height on BTC', done => { request.get('/api/BTC/regtest/block/101').expect(200, (err, res) => { if (err) console.error(err); const block = res.body; expect(block).to.include( {chain: 'BTC', network: 'regtest', height: 101, confirmations: tipHeight - 101 + 1} ); testBlock(block); done(); }); }); it('should get block by height on BCH', done => { request.get('/api/BCH/regtest/block/101').expect(200, (err, res) => { if (err) console.error(err); const block = res.body; expect(block).to.include({chain: 'BCH', network: 'regtest', height: 101}); testBlock(block); done(); }); }); const testCoins = ( chain: string, network: string, blockHeight: number, txids: string[], inputs: string[], outputs: string[], ) => { const coinProps = { chain: 'string', network: 'string', coinbase: 'boolean', mintIndex: 'number', spentTxid: 'string', mintTxid: 'string', spentHeight: 'number', mintHeight: 'number', address: 'string', script: 'string', value: 'number', confirmations: 'number' }; // expect a transaction input for every transaction except the mined/coinbase transaction expect(inputs.length).to.be.at.least(txids.length - 1); // every transaction must have an output by definition expect(outputs.length).to.be.at.least(txids.length); for (const input of inputs) { expect(input).to.include({chain, network, spentHeight: blockHeight}); expectObjectToHaveProps(input, coinProps); } for (const output of outputs) { expect(output).to.include({chain, network, mintHeight: blockHeight }); expectObjectToHaveProps(output, coinProps); } } let block100Hash; it('should fetch block 100 and save hash for other tests', done => { expect(block100Hash).to.be.undefined; request.get('/api/BTC/regtest/block/100').expect(200, (err, res) => { if (err) console.error(err); const block = res.body; block100Hash = block.hash; testBlock(block); done(); }) }) it('should get coins by block hash', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/0/1`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); it('should get coins by block hash and limit coins to 3', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/3/1`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; expect(txids.length).to.equal(3); testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); let pg1txids; it('should get coins by block hash and seperate into 2 pages (page 1)', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/3/1`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; pg1txids = txids; testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); it('should get coins by block hash and seperate into 2 pages (page 2)', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/3/2`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; expect(pg1txids).to.be.an.instanceof(Array); for (const txid of txids) { expect(pg1txids).to.not.contain(txid); } testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); let numTxsBlock100; it('should get number of transactions from block 100 for other tests', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/500/1`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; numTxsBlock100 = txids.length; // the following tests assume block 100 has at least 6 transactions expect(numTxsBlock100).to.be.at.least(6); testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); it('should get coins by block hash and handle coin limit higher than number of coins', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/500/1`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; expect(txids.length).to.equal(numTxsBlock100); testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); it('should get all coin data if no limit is specified (:limit == 0) on page 1', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/0/1`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; expect(txids.length).to.equal(numTxsBlock100); testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); it('should get all coin data if no limit is specified (:limit == 0) on page 2', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/0/2`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; expect(txids.length).to.equal(numTxsBlock100); testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); it('should skip all coins if :limit > num coins and :pgnum = 2', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/500/2`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; expect(txids.length).to.equal(0); testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); it('should recieve 0 coins if requesting a page that is too high', done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/${numTxsBlock100 - 1}/3`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; expect(txids).to.be.empty; expect(inputs).to.be.empty; expect(outputs).to.be.empty; done(); }); }); // Test route paging of coins when remainder == i (1..3) for (let i = 1; i <= 3; i++) { it(`should recieve partial pages with remainder ${i}`, done => { request.get(`/api/BTC/regtest/block/${block100Hash}/coins/${numTxsBlock100 - i}/2`).expect(200, (err, res) => { if (err) console.error(err); const { txids, inputs, outputs } = res.body; expect(txids).to.have.length(i); testCoins('BTC', 'regtest', 100, txids, inputs, outputs); done(); }); }); } it('should get blocks before 20 minutes ago', done => { request.get(`/api/BTC/regtest/block/before-time/${minutesAgo(20)}`).expect(200, (err, res) => { if (err) console.error(err); const block = res.body; const { timeNormalized } = block; expect(new Date(timeNormalized).getTime()).to.be.lessThan(minutesAgo(20).getTime()); expect(block).to.include({chain: 'BTC', network: 'regtest'}) testBlock(block); done(); }); }); it('should calculate fee data (total, mean, median, and mode) for block correctly', done => { const spy = sandbox.spy(ChainStateProvider, 'getBlockFee'); request.get('/api/BTC/regtest/block/100/fee').expect(200, (err, res) => { if (err) console.error(err); expect(spy.calledOnce).to.be.true; const { feeTotal, mean, median, mode } = res.body; // transaction data is defined in before function expect(feeTotal).to.equal(20000 + 20000 + 25000 + 30000 + 35000); expect(mean).to.equal((20000 / 1056 + 20000 / 1056 + 25000 / 1056 + 30000 / 1056 + 35000 / 1056) / 5); expect(median).to.equal(25000 / 1056); expect(mode).to.equal(20000 / 1056); done(); }); }); it('should cache fee data', done => { const spy = sandbox.spy(ChainStateProvider, 'getBlockFee'); request.get('/api/BTC/regtest/block/100/fee').expect(200, (err, res) => { if (err) console.error(err); expect(spy.notCalled).to.be.true; const { feeTotal, mean, median, mode } = res.body; // transaction data is defined in before function expect(feeTotal).to.equal(20000 + 20000 + 25000 + 30000 + 35000); expect(mean).to.equal((20000 / 1056 + 20000 / 1056 + 25000 / 1056 + 30000 / 1056 + 35000 / 1056) / 5); expect(median).to.equal(25000 / 1056); expect(mode).to.equal(20000 / 1056); done(); }); }); it('should calculate fee data on BCH', done => { request.get('/api/BCH/regtest/block/100/fee').expect(200, (err, res) => { if (err) console.error(err); const { feeTotal, mean, median, mode } = res.body; // transaction data is defined in before function expect(feeTotal).to.equal(2000 + 2000 + 2500 + 3000 + 3500); expect(mean).to.equal((2000 / 1056 + 2000 / 1056 + 2500 / 1056 + 3000 / 1056 + 3500 / 1056) / 5); expect(median).to.equal(2500 / 1056); expect(mode).to.equal(2000 / 1056); done(); }); }); it('should calculate tip fee data', done => { request.get('/api/BTC/regtest/block/tip/fee').expect(200, (err, res) => { if (err) console.error(err); const { feeTotal, mean, median, mode } = res.body; // transaction data is defined in before function expect(feeTotal).to.equal(9000 + 10000 + 11000); expect(mean).to.equal((9000 / 1056 + 10000 / 1056 + 11000 / 1056) / 3); expect(median).to.equal(10000 / 1056); expect(mode).to.equal(9000 / 1056); done(); }); }); it('should calculate fee data of block with only coinbase transaction as 0', done => { request.get('/api/BTC/regtest/block/101/fee').expect(200, (err, res) => { if (err) console.error(err); const { feeTotal, mean, median, mode } = res.body; expect(feeTotal).to.equal(0); expect(mean).to.equal(0); expect(median).to.equal(0); expect(mode).to.equal(0); done(); }); }); }); });