diff --git a/bitcore-test.config.json b/bitcore-test.config.json index 91171859a4b..4c5b3a739d5 100644 --- a/bitcore-test.config.json +++ b/bitcore-test.config.json @@ -5,6 +5,11 @@ "wallets": { "allowCreationBeforeCompleteSync": true } + }, + "socket": { + "bwsKeys": [ + "036df8264584a6398718e88a5bf4dc816c52e5037303934d555dba0ccaa65fbe3f" + ] } }, "chains": { diff --git a/packages/bitcore-node/src/providers/chain-state/index.ts b/packages/bitcore-node/src/providers/chain-state/index.ts index 4d90ac7e1b8..1b99c3808cf 100644 --- a/packages/bitcore-node/src/providers/chain-state/index.ts +++ b/packages/bitcore-node/src/providers/chain-state/index.ts @@ -35,10 +35,6 @@ class ChainStateProxy implements IChainStateProvider { return services[chain][network]; } - streamAddressUtxos(params: StreamAddressUtxosParams) { - return this.get(params).streamAddressUtxos(params); - } - streamAddressTransactions(params: StreamAddressUtxosParams) { return this.get(params).streamAddressTransactions(params); } diff --git a/packages/bitcore-node/src/providers/chain-state/internal/internal.ts b/packages/bitcore-node/src/providers/chain-state/internal/internal.ts index fe64e70262a..7660dd03abb 100644 --- a/packages/bitcore-node/src/providers/chain-state/internal/internal.ts +++ b/packages/bitcore-node/src/providers/chain-state/internal/internal.ts @@ -75,13 +75,6 @@ export class InternalStateProvider implements IChainStateService { return query; } - streamAddressUtxos(params: StreamAddressUtxosParams) { - const { req, res, args } = params; - const { limit, since } = args; - const query = this.getAddressQuery(params); - Storage.apiStreamingFind(CoinStorage, query, { limit, since, paging: '_id' }, req!, res!); - } - async streamAddressTransactions(params: StreamAddressUtxosParams) { const { req, res, args } = params; const { limit, since } = args; diff --git a/packages/bitcore-node/src/routes/api/address.ts b/packages/bitcore-node/src/routes/api/address.ts index ab21467466b..e7965f19b17 100644 --- a/packages/bitcore-node/src/routes/api/address.ts +++ b/packages/bitcore-node/src/routes/api/address.ts @@ -24,25 +24,7 @@ async function streamCoins(req: Request, res) { } } -router.get('/:address', function (req: Request, res) { - try { - let { chain, network, address } = req.params; - let { unspent, limit = 10, since } = req.query; - let payload = { - chain, - network, - address, - req, - res, - args: { unspent, limit, since } - } as StreamAddressUtxosParams; - return ChainStateProvider.streamAddressUtxos(payload); - } catch (err: any) { - logger.error('Error getting address: %o', err.stack || err.message || err); - return res.status(500).send(err.message || err); - } -}); - +router.get('/:address', streamCoins); router.get('/:address/txs', streamCoins); router.get('/:address/coins', streamCoins); diff --git a/packages/bitcore-node/src/types/namespaces/ChainStateProvider.ts b/packages/bitcore-node/src/types/namespaces/ChainStateProvider.ts index 6c3ff39ac3b..25cc3238938 100644 --- a/packages/bitcore-node/src/types/namespaces/ChainStateProvider.ts +++ b/packages/bitcore-node/src/types/namespaces/ChainStateProvider.ts @@ -200,7 +200,6 @@ export interface IChainStateService { getWalletBalanceAtTime( params: GetWalletBalanceAtTimeParams ): Promise; - streamAddressUtxos(params: StreamAddressUtxosParams): any; streamAddressTransactions(params: StreamAddressUtxosParams): any; streamTransactions(params: StreamTransactionsParams): any; getAuthhead(params: StreamTransactionParams): Promise; diff --git a/packages/bitcore-node/test/helpers/index.ts b/packages/bitcore-node/test/helpers/index.ts index 225101f10c8..87d8920d155 100644 --- a/packages/bitcore-node/test/helpers/index.ts +++ b/packages/bitcore-node/test/helpers/index.ts @@ -11,6 +11,7 @@ import { WalletStorage } from '../../src/models/wallet'; import { WalletAddressStorage } from '../../src/models/walletAddress'; import { Storage } from '../../src/services/storage'; import { expect } from 'chai'; +import { randomBytes } from 'crypto'; export async function resetDatabase() { console.log('Resetting database'); @@ -83,5 +84,29 @@ export function expectObjectToHaveProps(obj: any, props: Record) expect(obj[key]).to.be.a(props[key]); } }; +export function testCoin (coin) { + expect(coin, 'coin is undefined').to.exist; + expect(coin, 'coin is not an object').to.be.an('object'); + expect(coin, 'coin test').to.have.property('chain').that.is.a('string', `coin.chain is not a string`); + expect(coin, 'coin test').to.have.property('network').that.is.a('string', 'coin.network is not a string'); + expect(coin, 'coin test').to.have.property('mintIndex').that.is.a('number', 'coin.mintIndex is not a number'); + expect(coin, 'coin test').to.have.property('mintTxid').that.is.a('string', 'coin.mintTxid is not a string'); + expect(coin, 'coin test').to.have.property('address').that.is.a('string', 'coin.address is not a string'); + expect(coin, 'coin test').to.have.property('coinbase').that.is.a('boolean', 'coin.coinbase is not a boolean'); + expect(coin, 'coin test').to.have.property('mintHeight').that.is.a('number', 'coin.mintHeight is not a number'); + expect(coin, 'coin test').to.have.property('script'); + expect(coin, 'coin test').to.have.property('spentHeight').that.is.a('number', 'coin.spentHeight is not a number'); + expect(coin, 'coin test').to.have.property('value').that.is.a('number', 'coin.value is not a number'); + expect(coin, 'coin test').to.have.property('spentTxid').that.is.a('string', 'coin.spentTxid is not a string'); + if ('confirmations' in coin) { + expect(coin.confirmations, 'coin.confirmations is not a number').to.be.a('number'); + } +} export const minutesAgo = (minutes: number): Date => new Date(Date.now() - 1000 * 60 * minutes); + +export const randomHex = (characters: number): string => + randomBytes(Math.ceil(characters / 2.0)).toString('hex').slice(0, characters); + +export const mongoIdRegex = /^[a-f0-9]{24}$/; +export const base58Regex = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/; \ No newline at end of file diff --git a/packages/bitcore-node/test/integration/routes.spec.ts b/packages/bitcore-node/test/integration/routes.spec.ts deleted file mode 100644 index 70399d803de..00000000000 --- a/packages/bitcore-node/test/integration/routes.spec.ts +++ /dev/null @@ -1,539 +0,0 @@ -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(); - }); - }); - }); -}); diff --git a/packages/bitcore-node/test/integration/routes/address.spec.ts b/packages/bitcore-node/test/integration/routes/address.spec.ts new file mode 100644 index 00000000000..48470808c4a --- /dev/null +++ b/packages/bitcore-node/test/integration/routes/address.spec.ts @@ -0,0 +1,309 @@ +import { describe } from 'mocha'; +import app from '../../../src/routes'; +import supertest from 'supertest'; +import { intAfterHelper, intBeforeHelper } from '../../helpers/integration'; +import { resetDatabase, testCoin } from '../../helpers'; +import { CoinStorage, ICoin } from '../../../src/models/coin'; +import { expect } from 'chai'; + +const request = supertest(app); + + +const address1Coins: ICoin[] = [ + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "0ef2258370c1a2f7ab66615c0cc13bce45fd6a787e5e0bd3ba382ef5a1abf813", + mintHeight: 111, + spentHeight: -2, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000112800, + confirmations: -1, + wallets: [] + }, + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "68a32aaf3fdd37d5ad5b7ed85b482a49254e98fd908636a4fd886f2bd80fbde5", + mintHeight: 107, + spentHeight: -2, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000000000, + confirmations: -1, + wallets: [] + }, + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "b2dce546b533ab15798f074e17818a42d96ce2388ac4120854b15fbab2679e04", + mintHeight: 109, + spentHeight: -2, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000000000, + confirmations: -1, + wallets: [] + }, + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "6460e3a0ced11db7046b92a49098471c2f665d28eaeae1b710ade423a68a525d", + mintHeight: 108, + spentHeight: -2, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000000000, + confirmations: -1, + wallets: [] + }, + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "7b0eab0d74163bae5bf0e053a64519aa85aca68a3ff9457db6dec69e3420ec22", + mintHeight: 107, + spentHeight: 118, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000000000, + confirmations: -1, + wallets: [] + }, + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "aff0eb4844ff64043465971224be92f3e80e23e86e302f76316f31b287d612f6", + mintHeight: 106, + spentHeight: -2, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000000000, + confirmations: -1, + wallets: [] + }, + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "91c1449248394e297d8ef93185f09d77494c626c0d845607fc6b59976891fa7d", + mintHeight: 105, + spentHeight: -2, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000000000, + confirmations: -1, + wallets: [] + }, + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "3f8df18d023c25db19969822f8b52a51932dfd73a94bea04655b90539d082cf8", + mintHeight: 104, + spentHeight: 120, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000000000, + confirmations: -1, + wallets: [] + }, + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "fce3efb3a3ce16f98be6063a0f041dae231d6df6ae2a49895d0dc2a48f6c6835", + mintHeight: 103, + spentHeight: -2, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000000000, + confirmations: -1, + wallets: [] + }, + { + chain: "BTC", + network: "regtest", + coinbase: true, + mintIndex: 0, + spentTxid: "", + mintTxid: "a1f319579d76a67e251f7f53c6f6fabf2e1c21eef7e1aaf6a6733153f1c17309", + mintHeight: 102, + spentHeight: -2, + address: "bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8", + script: Buffer.from("0014ecc5b1bc62312765e5a28bf21160706245f3bdf4", "hex"), + value: 5000000000, + confirmations: -1, + wallets: [] + } +]; + +const address1Balance = { unconfirmed: 0, confirmed: 0, balance: 0 }; + +describe('Address Routes', function () { + before(async function() { + this.timeout(15000); + await intBeforeHelper(); + await resetDatabase(); + await CoinStorage.collection.insertMany(address1Coins); + for (const coin of address1Coins) { + if (coin.spentHeight > 0) + continue; + address1Balance.balance += coin.value; + if (coin.mintHeight >= 0) + address1Balance.confirmed += coin.value; + else + address1Balance.unconfirmed += coin.value; + } + }) + + after(async () => intAfterHelper()); + + it('should get address coins /api/BTC/regtest/address/:address', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8') + .expect(200, (err, res) => { + if (err) console.error(err); + const coins = res.body; + for (const coin of coins) { + testCoin(coin); + } + done(); + }); + }); + + it('should get address coins /api/BTC/regtest/address/:address and limit', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8?limit=5') + .expect(200, (err, res) => { + if (err) console.error(err); + const coins = res.body; + expect(coins.length).to.equal(5); + for (const coin of coins) { + testCoin(coin); + } + done(); + }); + }); + + it('should get address coins /api/BTC/regtest/address/:address, and filter for unspent', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8?limit=500&unspent=true') + .expect(200, (err, res) => { + if (err) console.error(err); + const coins = res.body; + for (const coin of coins) { + expect(coin.spentHeight).to.be.lessThan(0); + testCoin(coin); + } + done(); + }); + }); + + it('should get address coins /api/BTC/regtest/address/:address/coins', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8/coins') + .expect(200, (err, res) => { + if (err) console.error(err); + const coins = res.body; + for (const coin of coins) { + testCoin(coin); + } + done(); + }); + }); + + it('should get address coins /api/BTC/regtest/address/:address/coins and limit', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8/coins?limit=5') + .expect(200, (err, res) => { + if (err) console.error(err); + const coins = res.body; + expect(coins.length).to.equal(5); + for (const coin of coins) { + testCoin(coin); + } + done(); + }); + }); + + it('should get address coins /api/BTC/regtest/address/:address/coins and filter for unspent', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8/coins?limit=5') + .expect(200, (err, res) => { + if (err) console.error(err); + const coins = res.body; + expect(coins.length).to.equal(5); + for (const coin of coins) { + testCoin(coin); + } + done(); + }); + }); + + it('should get address coins /api/BTC/regtest/address/:address/txs', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8/txs') + .expect(200, (err, res) => { + if (err) console.error(err); + const coins = res.body; + for (const coin of coins) { + testCoin(coin); + } + done(); + }); + }); + + it('should get address coins /api/BTC/regtest/address/:address/txs and limit', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8/txs?limit=5') + .expect(200, (err, res) => { + if (err) console.error(err); + const coins = res.body; + expect(coins.length).to.equal(5); + for (const coin of coins) { + testCoin(coin); + } + done(); + }); + }); + + it('should get address coins /api/BTC/regtest/address/:address/txs and filter for unspent', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8/txs?limit=500&unspent=true') + .expect(200, (err, res) => { + if (err) console.error(err); + const coins = res.body; + for (const coin of coins) { + testCoin(coin); + } + done(); + }); + }); + + it('should get address balance', done => { + request.get('/api/BTC/regtest/address/bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8/balance') + .expect(200, (err, res) => { + if (err) console.error(err); + expect(res.body.balance).to.equal(address1Balance.balance); + expect(res.body.confirmed).to.equal(address1Balance.confirmed); + expect(res.body.unconfirmed).to.equal(address1Balance.unconfirmed); + done(); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcore-node/test/integration/routes/block.spec.ts b/packages/bitcore-node/test/integration/routes/block.spec.ts new file mode 100644 index 00000000000..ffe1ba4cb77 --- /dev/null +++ b/packages/bitcore-node/test/integration/routes/block.spec.ts @@ -0,0 +1,533 @@ +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('Block 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(); + }); + + 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(); + }); + }); +}); diff --git a/packages/bitcore-node/test/integration/routes/other.spec.ts b/packages/bitcore-node/test/integration/routes/other.spec.ts new file mode 100644 index 00000000000..f4ed1b07e4c --- /dev/null +++ b/packages/bitcore-node/test/integration/routes/other.spec.ts @@ -0,0 +1,14 @@ +import supertest from 'supertest'; +import app from '../../../src/routes'; + +const request = supertest(app); + +describe('Routes', function() { + before(async function() { + this.timeout(15000); + }); + + it('should respond with a 404 status code for an unknown path', done => { + request.get('/unknown').expect(404, done); + }); +}); diff --git a/packages/bitcore-node/test/integration/routes/stats.spec.ts b/packages/bitcore-node/test/integration/routes/stats.spec.ts new file mode 100644 index 00000000000..a3a2ab11370 --- /dev/null +++ b/packages/bitcore-node/test/integration/routes/stats.spec.ts @@ -0,0 +1,151 @@ +import supertest from 'supertest' +import app from '../../../src/routes' +import { expect } from 'chai'; +import { intAfterHelper, intBeforeHelper } from '../../helpers/integration'; +import { resetDatabase } from '../../helpers'; +import { BitcoinBlockStorage, IBtcBlock } from '../../../src/models/block'; +import { randomBytes } from 'crypto'; + +const requests = supertest(app); + +function randomInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + + +const days = 40, blocksPerDay = 5; +const blocks: IBtcBlock[] = []; +const hashes: string[] = []; +for (let i = 0; i < blocksPerDay; i++) + hashes.push(randomBytes(32).toString('hex')); +for (let day = 0; day < days; day++) { + const date = new Date(); + date.setUTCDate(date.getUTCDate() - day); + date.setUTCHours(0, 0, 0, 0); + + for (let i = 0; i < blocksPerDay; i++) { + blocks.push({ + chain: 'BTC', + network: 'regtest', + hash: hashes[i], + height: day * blocksPerDay + i, + bits: 545259519, + merkleRoot: randomBytes(32).toString('hex'), + nextBlockHash: hashes[i+1], + nonce: randomInt(0, 100), + previousBlockHash: hashes[i-1], + processed: true, + reward: 5000000000, + size: randomInt(200, 4000), + time: new Date(date.getTime() + i * 1000 * 60 * 10), + timeNormalized: new Date(date.getTime() + i * 1000 * 60 * 10), + transactionCount: randomInt(1, 1000), + version: 536870912 + }); + } +} + +const expectedDailyTransactions = new Map(); +for (let day = 0; day < days; day++) { + let dailyTransactionCount = 0; + for (let i = 0; i < blocksPerDay; i++) { + const block = blocks[day * blocksPerDay + i]; + dailyTransactionCount += block.transactionCount; + } + const date = blocks[day * blocksPerDay].time.toISOString().slice(0, 10); + expectedDailyTransactions.set(date, dailyTransactionCount ); +} + +describe('Stats Routes', function() { + before(async function() { + this.timeout(15000); + await intBeforeHelper(); + await resetDatabase(); + await BitcoinBlockStorage.collection.insertMany(blocks); + }); + + after(async function() { + await intAfterHelper(); + }) + + it('should get daily-transactions', done => { + requests.get('/api/BTC/regtest/stats/daily-transactions') + .expect(200, (err, res) => { + if (err) console.error(err); + const { chain, network, results } = res.body; + expect(chain).to.equal('BTC'); + expect(network).to.equal('regtest'); + expect(results.length).to.be.approximately(30, 1); + for (const result of results) { + const { date, transactionCount } = result; + expect(expectedDailyTransactions.get(date)).to.equal(transactionCount); + } + done(); + }); + }); + + it('should get daily-transactions from yesterday', done => { + const yesterday = new Date(); + yesterday.setUTCDate(yesterday.getUTCDate() - 1); + yesterday.setUTCHours(0, 0, 0, 0); + requests.get(`/api/BTC/regtest/stats/daily-transactions?startDate=${yesterday}`) + .expect(200, (err, res) => { + if (err) console.error(err); + const { chain, network, results } = res.body; + expect(chain).to.equal('BTC'); + expect(network).to.equal('regtest'); + expect(results.length).to.be.approximately(1, 1); + for (const result of results) { + const { date, transactionCount } = result; + expect(expectedDailyTransactions.get(date)).to.equal(transactionCount); + } + done(); + }); + }); + + + it('should get daily-transactions from this week', done => { + const yesterday = new Date(); + yesterday.setUTCDate(yesterday.getUTCDate() - 7); + yesterday.setUTCHours(0, 0, 0, 0); + requests.get(`/api/BTC/regtest/stats/daily-transactions?startDate=${yesterday}`) + .expect(200, (err, res) => { + if (err) console.error(err); + const { chain, network, results } = res.body; + expect(chain).to.equal('BTC'); + expect(network).to.equal('regtest'); + expect(results.length).to.be.approximately(7, 1); + for (const result of results) { + const { date, transactionCount } = result; + expect(expectedDailyTransactions.get(date)).to.equal(transactionCount); + expect(new Date(date)).to.be.at.least(yesterday); + } + done(); + }); + }); + + + it('should get daily-transactions from a week', done => { + const startDate = new Date(); + startDate.setUTCDate(startDate.getUTCDate() - 7 - 25); + startDate.setUTCHours(0, 0, 0, 0); + const endDate = new Date(); + endDate.setUTCDate(startDate.getUTCDate() - 25); + endDate.setUTCHours(0, 0, 0, 0); + requests.get(`/api/BTC/regtest/stats/daily-transactions?startDate=${startDate}&endDate=${endDate}`) + .expect(200, (err, res) => { + if (err) console.error(err); + const { chain, network, results } = res.body; + expect(chain).to.equal('BTC'); + expect(network).to.equal('regtest'); + expect(results.length).to.be.approximately(7, 1); + for (const result of results) { + const { date, transactionCount } = result; + expect(expectedDailyTransactions.get(date)).to.equal(transactionCount); + expect(new Date(date)).to.be.at.least(startDate); + expect(new Date(date)).to.be.at.most(endDate); + } + done(); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcore-node/test/integration/routes/tx.spec.ts b/packages/bitcore-node/test/integration/routes/tx.spec.ts new file mode 100644 index 00000000000..46598b2d350 --- /dev/null +++ b/packages/bitcore-node/test/integration/routes/tx.spec.ts @@ -0,0 +1,456 @@ +import supertest from 'supertest'; +import app from '../../../src/routes' +import { describe } from 'mocha'; +import { intAfterHelper, intBeforeHelper } from '../../helpers/integration'; +import { resetDatabase, testCoin } from '../../helpers'; +import { expect } from 'chai'; +import { ITransaction, TransactionStorage } from '../../../src/models/transaction'; +import { CoinStorage, ICoin } from '../../../src/models/coin'; + +const request = supertest(app); + +const transactions = [ + { + network: 'regtest', + txid: '8313f0b9645c64834e017029e7d3aecd27a3d4c68e4c47d3b5b46f342d1dcf52', + chain: 'BTC', + blockHash: '6a8fea1b3b598ecdf3cd3d117d4c406d06a7e540d29b5c3a47e208b3cdea8050', + blockHeight: 111, + blockTime: new Date('2025-08-28T14:46:51.000Z'), + blockTimeNormalized: new Date('2025-08-28T14:46:51.000Z'), + coinbase: false, + fee: 14100, + inputCount: 1, + locktime: 110, + outputCount: 2, + size: 113, + value: 4999985900, + wallets: [] + }, + { + chain: 'BTC', + txid: '517dc566fb3a82121840eecdcce4636fb776b6b64ccc4f29f74b870e6bcc44b0', + network: 'regtest', + blockHash: '6a8fea1b3b598ecdf3cd3d117d4c406d06a7e540d29b5c3a47e208b3cdea8050', + blockHeight: 111, + blockTime: new Date('2025-08-28T14:46:51.000Z'), + blockTimeNormalized: new Date('2025-08-28T14:46:51.000Z'), + coinbase: false, + fee: 14100, + inputCount: 1, + locktime: 110, + outputCount: 2, + size: 113, + value: 4999985900, + wallets: [] + }, + { + network: 'regtest', + chain: 'BTC', + txid: '969f2a9bf11f040248f2860ae4d1d3e876c1195bcc1b0bf9ff0a475fca915f7b', + blockHash: '6a8fea1b3b598ecdf3cd3d117d4c406d06a7e540d29b5c3a47e208b3cdea8050', + blockHeight: 111, + blockTime: new Date('2025-08-28T14:46:51.000Z'), + blockTimeNormalized: new Date('2025-08-28T14:46:51.000Z'), + coinbase: false, + fee: 14100, + inputCount: 1, + locktime: 110, + outputCount: 2, + size: 113, + value: 4999985900, + wallets: [] + }, + { + chain: 'BTC', + txid: '0e62f0e3f97a410a8c362cdaf2d7ab5dda9ab10e628b2f7d18184d50269c4b5d', + network: 'regtest', + blockHash: '6a8fea1b3b598ecdf3cd3d117d4c406d06a7e540d29b5c3a47e208b3cdea8050', + blockHeight: 112, + blockTime: new Date('2025-08-28T14:46:51.000Z'), + blockTimeNormalized: new Date('2025-08-28T14:46:51.000Z'), + coinbase: false, + fee: 14100, + inputCount: 1, + locktime: 110, + outputCount: 2, + size: 113, + value: 4999985900, + wallets: [] + }, + { + chain: 'BTC', + txid: '064a49a48f05ef621c16d5e2688f1f10bb249f96dacc91d6de9907b4690196f9', + network: 'regtest', + blockHash: '6a8fea1b3b598ecdf3cd3d117d4c406d06a7e540d29b5c3a47e208b3cdea8050', + blockHeight: 113, + blockTime: new Date('2025-08-28T14:46:51.000Z'), + blockTimeNormalized: new Date('2025-08-28T14:46:51.000Z'), + coinbase: false, + fee: 14100, + inputCount: 1, + locktime: 110, + outputCount: 2, + size: 113, + value: 4999985900, + wallets: [] + }, + { + chain: 'BTC', + network: 'regtest', + txid: '9d12f47632fc55a7b8d4e533c2f6fcbec01e08bd17939267342c9f3a28b282dc', + blockHash: '6a8fea1b3b598ecdf3cd3d117d4c406d06a7e540d29b5c3a47e208b3cdea8050', + blockHeight: 113, + blockTime: new Date('2025-08-28T14:46:51.000Z'), + blockTimeNormalized: new Date('2025-08-28T14:46:51.000Z'), + coinbase: false, + fee: 14100, + inputCount: 1, + locktime: 110, + outputCount: 2, + size: 113, + value: 4999985900, + wallets: [] + }, + { + chain: 'BTC', + txid: '2595e5efa5455c96ff376412172ab61c20c27a7260377c5b95913f1767c2d8d2', + network: 'regtest', + blockHash: '6a8fea1b3b598ecdf3cd3d117d4c406d06a7e540d29b5c3a47e208b3cdea8050', + blockHeight: 114, + blockTime: new Date('2025-08-28T14:46:51.000Z'), + blockTimeNormalized: new Date('2025-08-28T14:46:51.000Z'), + coinbase: false, + fee: 14100, + inputCount: 1, + locktime: 110, + outputCount: 2, + size: 113, + value: 4999985900, + wallets: [] + }, + { + chain: 'BTC', + network: 'regtest', + txid: '29567b4e313d9051164adb16346025b658db4959e9168cbf8f1b08a88b754897', + blockHash: '6a8fea1b3b598ecdf3cd3d117d4c406d06a7e540d29b5c3a47e208b3cdea8050', + blockHeight: 115, + blockTime: new Date('2025-08-28T14:46:51.000Z'), + blockTimeNormalized: new Date('2025-08-28T14:46:51.000Z'), + coinbase: false, + fee: 14100, + inputCount: 1, + locktime: 110, + outputCount: 2, + size: 113, + value: 4999985900, + wallets: [] + } +]; + +const coins: ICoin[] = [ + { + chain: 'BTC', + network: 'regtest', + coinbase: true, + mintIndex: 0, + spentTxid: '517dc566fb3a82121840eecdcce4636fb776b6b64ccc4f29f74b870e6bcc44b0', + mintTxid: 'd3667993af738c3db4033dc462ab95f9916da455c628bbf71488484e2b6803c3', + mintHeight: 5, + spentHeight: 111, + address: 'bcrt1qanzmr0rzxynktedz30epzcrsvfzl8005ppz0d8', + script: Buffer.from('0014ecc5b1bc62312765e5a28bf21160706245f3bdf4', 'hex'), + value: 5000000000, + confirmations: 1, + sequenceNumber: 4294967293, + wallets: [] + }, + { + chain: 'BTC', + network: 'regtest', + coinbase: false, + mintIndex: 0, + spentTxid: '', + mintTxid: '517dc566fb3a82121840eecdcce4636fb776b6b64ccc4f29f74b870e6bcc44b0', + mintHeight: 111, + spentHeight: -2, + address: 'bcrt1qhx4uesqfc48plrfw6u8z39068rm0palteu4lqv', + script: Buffer.from('0014b9abccc009c54e1f8d2ed70e2895fa38f6f0f7eb', 'hex'), + value: 100000, + confirmations: 1, + sequenceNumber: 4294967295, + wallets: [] + }, + { + chain: 'BTC', + network: 'regtest', + coinbase: false, + mintIndex: 1, + spentTxid: '', + mintTxid: '517dc566fb3a82121840eecdcce4636fb776b6b64ccc4f29f74b870e6bcc44b0', + mintHeight: 111, + spentHeight: -2, + address: 'bcrt1qc29l5h3uh33zgugw582kj0r9r2y6zna58gnkes', + script: Buffer.from('0014c28bfa5e3cbc6224710ea1d5693c651a89a14fb4', 'hex'), + value: 4999885900, + confirmations: 1, + sequenceNumber: 4294967295, + wallets: [] + }, + { + mintIndex: 0, + network: 'regtest', + mintTxid: '30ef4d50173b431c2649643a4c452f99c3feaf39246866ed403f459cc27c5913', + chain: 'BTC', + address: 'bcrt1qgw85g3zr0emah2wx2fvs9pxcg95xgslk9yffhs', + coinbase: false, + mintHeight: 113, + script: Buffer.from('0014c28bfa5e3cbc6224710ea1d5693c651a89a14fb4', 'hex'), + spentHeight: 113, + value: 31598971800, + wallets: [], + sequenceNumber: 4294967293, + spentTxid: '018811ac64369081011c93266652fe7907fcde5ede10f7400005018459858781' + }, + { + network: 'regtest', + mintIndex: 0, + chain: 'BTC', + mintTxid: 'a7639933badf18bd7f2cb76ed7b88efeb6aefef317f1ee769dd81737a6e62523', + address: 'bcrt1qzjfr6a043d93tje9s0s46apex7u8jp8zr3m9fk', + coinbase: false, + mintHeight: 113, + spentTxid: '', + script: Buffer.from('0014b9abccc009c54e1f8d2ed70e2895fa38f6f0f7eb', 'hex'), + spentHeight: -2, + value: 10000000000, + wallets: [] + }, + { + network: 'regtest', + mintIndex: 1, + chain: 'BTC', + mintTxid: 'a7639933badf18bd7f2cb76ed7b88efeb6aefef317f1ee769dd81737a6e62523', + address: 'bcrt1q5x28tlz8fw9gk8um2c8rj48hv6qypul3ztfr4k', + coinbase: false, + mintHeight: 113, + script: Buffer.from('0014b9abccc009c54e1f8d2ed70e2895fa38f6f0f7eb', 'hex'), + spentHeight: 113, + value: 11598943600, + wallets: [], + sequenceNumber: 4294967293, + spentTxid: 'e532e34a93d11b792c117ec92058a2115feb8cbc98fdfd0073f1ed26731152c4' + }, + { + mintIndex: 0, + mintTxid: 'e532e34a93d11b792c117ec92058a2115feb8cbc98fdfd0073f1ed26731152c4', + chain: 'BTC', + network: 'regtest', + spentTxid: '', + address: 'bcrt1qzjfr6a043d93tje9s0s46apex7u8jp8zr3m9fk', + coinbase: false, + mintHeight: 113, + script: Buffer.from('0014b9abccc009c54e1f8d2ed70e2895fa38f6f0f7eb', 'hex'), + spentHeight: -2, + value: 10000000000, + wallets: [] + }, + { + network: 'regtest', + mintIndex: 1, + chain: 'BTC', + mintTxid: 'e532e34a93d11b792c117ec92058a2115feb8cbc98fdfd0073f1ed26731152c4', + address: 'bcrt1qudtnxthw4y286g5q6p364zuknxq7ts30vfrwsn', + coinbase: false, + mintHeight: 113, + spentTxid: '', + script: Buffer.from('0014b9abccc009c54e1f8d2ed70e2895fa38f6f0f7eb', 'hex'), + spentHeight: -2, + value: 1598929500, + wallets: [] + }, + { + mintIndex: 1, + mintTxid: '018811ac64369081011c93266652fe7907fcde5ede10f7400005018459858781', + chain: 'BTC', + network: 'regtest', + address: 'bcrt1qrezp2ha82ra8mnhnd8uge3wsve37ccquek233e', + coinbase: false, + mintHeight: 113, + script: Buffer.from('0014b9abccc009c54e1f8d2ed70e2895fa38f6f0f7eb', 'hex'), + spentHeight: 113, + value: 21598957700, + wallets: [], + sequenceNumber: 4294967293, + spentTxid: 'a7639933badf18bd7f2cb76ed7b88efeb6aefef317f1ee769dd81737a6e62523' + }, + { + network: 'regtest', + mintIndex: 0, + chain: 'BTC', + mintTxid: '018811ac64369081011c93266652fe7907fcde5ede10f7400005018459858781', + address: 'bcrt1qzjfr6a043d93tje9s0s46apex7u8jp8zr3m9fk', + coinbase: false, + spentTxid: '', + mintHeight: 113, + script: Buffer.from('0014b9abccc009c54e1f8d2ed70e2895fa38f6f0f7eb', 'hex'), + spentHeight: -2, + value: 10000000000, + wallets: [] + } +]; + +describe('Tx Routes', function() { + before(async () => { + await intBeforeHelper(); + await resetDatabase(); + + await TransactionStorage.collection.insertMany(transactions); + await CoinStorage.collection.insertMany(coins); + }); + + after(async() => { + await intAfterHelper(); + }); + + function testTransaction(transaction) { + expect(transaction).to.be.an('object'); + expect(transaction).to.have.property('txid').that.is.a('string'); + expect(transaction).to.have.property('chain').that.is.a('string'); + expect(transaction).to.have.property('network').that.is.a('string'); + expect(transaction).to.have.property('blockHash').that.is.a('string'); + expect(transaction).to.have.property('blockHeight').that.is.a('number'); + expect(transaction).to.have.property('blockTime'); + expect(new Date(transaction.blockTime)).to.be.a('date'); + expect(transaction).to.have.property('blockTimeNormalized'); + expect(new Date(transaction.blockTimeNormalized)).to.be.a('date'); + expect(transaction).to.have.property('coinbase').that.is.a('boolean'); + expect(transaction).to.have.property('fee').that.is.a('number'); + expect(transaction).to.have.property('inputCount').that.is.a('number'); + expect(transaction).to.have.property('outputCount').that.is.a('number'); + expect(transaction).to.have.property('locktime').that.is.a('number'); + expect(transaction).to.have.property('size').that.is.a('number'); + expect(transaction).to.have.property('value').that.is.a('number'); + + if ('confirmations' in transaction) { + expect(transaction.confirmations).to.be.a('number'); + } + + if (!transaction.coinbase) { + expect(transaction.inputCount).to.be.at.least(1); + } + } + + it('should get transaction on /api/BTC/regtest/tx?blockHeight=113', done => { + request.get('/api/BTC/regtest/tx?blockHeight=113') + .expect(200, (err, res) => { + if (err) console.error(err); + const transactions = res.body; + for (const transaction of transactions) { + testTransaction(transaction); + expect(transaction.chain).to.equal('BTC'); + expect(transaction.network).to.equal('regtest'); + expect(transaction.blockHeight).to.equal(113); + } + done(); + }); + }); + + it('should get transaction on /api/BTC/regtest/tx?blockHash=[blockHash]', done => { + const blockHash = '6a8fea1b3b598ecdf3cd3d117d4c406d06a7e540d29b5c3a47e208b3cdea8050'; + request.get(`/api/BTC/regtest/tx?blockHash=${blockHash}`) + .expect(200, (err, res) => { + if (err) console.error(err); + const transactions = res.body; + for (const transaction of transactions) { + testTransaction(transaction); + expect(transaction.chain).to.equal('BTC'); + expect(transaction.network).to.equal('regtest'); + expect(transaction.blockHash).to.equal(blockHash); + } + done(); + }); + }); + + it('should get transaction on /api/BTC/regtest/tx/:txId', done => { + const txid = '0e62f0e3f97a410a8c362cdaf2d7ab5dda9ab10e628b2f7d18184d50269c4b5d'; // defined in transactions array + request.get(`/api/BTC/regtest/tx/${txid}`) + .expect(200, (err, res) => { + if (err) console.error(err); + const transaction = res.body; + testTransaction(transaction); + expect(transaction.txid).to.equal(txid); + expect(transaction.chain).to.equal('BTC'); + expect(transaction.network).to.equal('regtest'); + done(); + }); + }); + + it('should get transaction with inputs and outputs on /api/BTC/regtest/tx/:txId/populated', done => { + const txid = '517dc566fb3a82121840eecdcce4636fb776b6b64ccc4f29f74b870e6bcc44b0'; // defined in transactions array + request.get(`/api/BTC/regtest/tx/${txid}/populated`) + .expect(200, (err, res) => { + if (err) console.error(err); + const transaction: Partial = res.body; + const { inputs, outputs } = res.body.coins; + + testTransaction(transaction); + expect(transaction.txid).to.equal(txid); + expect(transaction.chain).to.equal('BTC'); + expect(transaction.network).to.equal('regtest'); + + for (const coin of inputs) { + testCoin(coin); + expect(coin.chain).to.equal('BTC'); + expect(coin.network).to.equal('regtest'); + expect(coin.spentTxid).to.equal(txid); + } + for (const coin of outputs) { + testCoin(coin); + expect(coin.chain).to.equal('BTC'); + expect(coin.network).to.equal('regtest'); + expect(coin.mintTxid).to.equal(txid); + } + done(); + }); + }); + + it('should get on /api/BTC/regtest/tx/:txId/authhead', done => { + request.get(`/api/BTC/regtest/tx/30ef4d50173b431c2649643a4c452f99c3feaf39246866ed403f459cc27c5913/authhead`) + .expect(200, (err, res) => { + if (err) console.error(err); + const { chain, network, authbase, identityOutputs } = res.body; + expect(chain).to.equal('BTC'); + expect(network).to.equal('regtest'); + expect(authbase).to.exist.and.to.be.a('string'); + for (const coin of identityOutputs) { + testCoin(coin); + expect(coin.chain).to.equal('BTC'); + expect(coin.network).to.equal('regtest'); + expect(coin.spentHeight).to.be.at.most(-1); + } + done(); + }) + }); + + it('should get transaction inputs and outputs on /api/BTC/regtest/tx/:txId/coins', done => { + const txid = '0e62f0e3f97a410a8c362cdaf2d7ab5dda9ab10e628b2f7d18184d50269c4b5d'; // defined in transactions array + request.get(`/api/BTC/regtest/tx/${txid}/coins`) + .expect(200, (err, res) => { + if (err) console.error(err); + const { inputs, outputs } = res.body; + for (const coin of inputs) { + testCoin(coin); + expect(coin.chain).to.equal('BTC'); + expect(coin.network).to.equal('regtest'); + expect(coin.spentTxid).to.equal(txid); + } + for (const coin of outputs) { + testCoin(coin); + expect(coin.chain).to.equal('BTC'); + expect(coin.network).to.equal('regtest'); + } + done(); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcore-node/test/integration/routes/valid.spec.ts b/packages/bitcore-node/test/integration/routes/valid.spec.ts new file mode 100644 index 00000000000..a6bfac8a204 --- /dev/null +++ b/packages/bitcore-node/test/integration/routes/valid.spec.ts @@ -0,0 +1,76 @@ +import supertest from 'supertest'; +import { expect } from 'chai'; +import app from '../../../src/routes'; + +const request = supertest(app); +describe('Validate Route', function() { + before(async function() { + + }); + + it('should detect valid block height', done => { + request.get('/api/BTC/regtest/valid/100000') + .expect(200, (err, res) => { + if (err) console.error(err); + const { isValid, type } = res.body; + expect(isValid).to.be.true; + expect(type).to.equal('blockOrTx'); + done(); + }); + }); + + it('should detect valid block hash', done => { + request.get('/api/BTC/regtest/valid/4fedb28fb20b5dcfe4588857ac10c38c6d67e8267e35478d8bcca468c9114bbe') + .expect(200, (err, res) => { + if (err) console.error(err); + const { isValid, type } = res.body; + expect(isValid).to.be.true; + expect(type).to.equal('blockOrTx'); + done(); + }); + }); + + it('should detect invalid block hash when length < 64', done => { + request.get('/api/BTC/regtest/valid/4fedb28fb20b5dcfe4588857ac10c38c6d67e8267e35478d8bcca468c9114bb') + .expect(200, (err, res) => { + if (err) console.error(err); + const { isValid, type } = res.body; + expect(isValid).to.be.false; + expect(type).to.equal('invalid'); + done(); + }); + }); + + it('should detect invalid block hash when length > 64', done => { + request.get('/api/BTC/regtest/valid/4fedb28fb20b5dcfe4588857ac10c38c6d67e8267e35478d8bcca468c9114bbee') + .expect(200, (err, res) => { + if (err) console.error(err); + const { isValid, type } = res.body; + expect(isValid).to.be.false; + expect(type).to.equal('invalid'); + done(); + }); + }); + + it('should detect valid bitcoin address', done => { + request.get('/api/BTC/regtest/valid/bcrt1qqf8mswxuh4fgv27e47lekxxt0jgp373zg85jk4') + .expect(200, (err, res) => { + if (err) console.error(err); + const { isValid, type } = res.body; + expect(isValid).to.be.true; + expect(type).to.equal('addr'); + done(); + }); + }); + + it('should detect invalid bitcoin address', done => { + request.get('/api/BTC/regtest/valid/bcrt1qqf8mswxuh4fgv27e47lekxxt0jgp373zg85jk44') + .expect(200, (err, res) => { + if (err) console.error(err); + const { isValid, type } = res.body; + expect(isValid).to.be.false; + expect(type).to.equal('invalid'); + done(); + }); + }); +}); \ No newline at end of file diff --git a/packages/bitcore-node/test/integration/routes/wallet.spec.ts b/packages/bitcore-node/test/integration/routes/wallet.spec.ts new file mode 100644 index 00000000000..e9a347e9d0d --- /dev/null +++ b/packages/bitcore-node/test/integration/routes/wallet.spec.ts @@ -0,0 +1,690 @@ +import supertest from 'supertest'; +import sinon from 'sinon'; +import app from '../../../src/routes'; +import { expect } from 'chai'; +import { intAfterHelper, intBeforeHelper } from '../../helpers/integration'; +import { base58Regex, minutesAgo, mongoIdRegex, randomHex, resetDatabase, testCoin } from '../../helpers'; +import { ChainStateProvider } from '../../../src/providers/chain-state'; +import { WalletStorage } from '../../../src/models/wallet'; +import { CoinStorage, ICoin } from '../../../src/models/coin'; +import { TransactionStorage } from '../../../src/models/transaction'; +import { BitcoinBlockStorage } from '../../../src/models/block'; +import { MongoBound } from '../../../src/models/base'; +import { WalletAddressStorage } from '../../../src/models/walletAddress'; + +const { PrivateKey, PublicKey, Address, crypto } = require('bitcore-lib'); +const secp256k1 = require('secp256k1'); + +const request = supertest(app); + +const privKey = new PrivateKey(); +const pubKey = PublicKey(privKey); + +const address = Address(PrivateKey().toPublicKey(), 'regtest').toString(); +const missingAddress1 = Address(PrivateKey().toPublicKey(), 'regtest').toString(); +const missingAddress2 = Address(PrivateKey().toPublicKey(), 'regtest').toString(); +const address2 = Address(PrivateKey().toPublicKey(), 'regtest').toString(); + +const bwsPrivKey = new PrivateKey('3711033b85a260d21cd469e7d93e27f04c31c21f13001053f1c074f7abbe6e75'); + +const wallet = { + chain: 'BTC', + network: 'regtest', + name: 'user', + pubKey: pubKey.toString(), + path: 'm/84/1h/0h/0/0', + singleAddress: 'bcrt1qzun74zp996s2najfar32t6j5dmyj5052s4vdq7' +}; + +function testWalletEquivalence(wallet1, doc) { + const { chain, network, name, pubKey, path, singleAddress } = doc; + + expect(chain).to.equal(wallet1.chain); + expect(network).to.equal(wallet1.network); + expect(name).to.equal(wallet1.name); + expect(pubKey).to.equal(wallet1.pubKey); + expect(path).to.equal(wallet1.path); + expect(singleAddress).to.equal(wallet1.singleAddress); +} + +function testWalletTransaction(tx) { + expect(tx).to.be.an('object'); + expect(tx).to.have.property('id').that.is.a('string').with.length.greaterThan(0); + expect(tx.id).to.match(mongoIdRegex); + + expect(tx).to.have.property('txid').that.is.a('string').with.length(64); + expect(tx.txid).to.match(/^[a-f0-9]{64}$/); + + expect(tx).to.have.property('fee').that.is.a('number'); + expect(tx).to.have.property('size').that.is.a('number').and.to.be.greaterThan(0); + + expect(tx).to.have.property('category').that.is.a('string'); + expect(['receive', 'send', 'move']).to.include(tx.category); + + expect(tx).to.have.property('satoshis').that.is.a('number'); + if (tx.category === 'receive') { + expect(tx.satoshis).to.be.at.least(0); + } else { + expect(tx.satoshis).to.be.at.most(0); + } + + expect(tx).to.have.property('height').that.is.a('number').and.to.be.at.least(0); + + expect(tx).to.have.property('address').that.is.a('string').with.length.greaterThan(0); + expect(tx.address).to.match(base58Regex); + + expect(tx).to.have.property('outputIndex').that.is.a('number').and.to.be.at.least(0); +} + +function getSignature(privateKey, method: 'GET' | 'POST' | 'reprocess', url: string, body={}) { + const message = [method, url, JSON.stringify(body)].join('|'); + const messageHash = crypto.Hash.sha256sha256(Buffer.from(message)); + return Buffer.from(secp256k1.ecdsaSign(messageHash, privateKey.toBuffer()).signature).toString('hex'); +} +async function addTransaction(params: { + senderAddress: string, + recieverAddress: string, + value: number, + fee?: number +}) { + const { senderAddress, recieverAddress, value, fee=0 } = params; + await addMultiIOTransaction({senderAddresses: [senderAddress], recipients: [{ address: recieverAddress, value }], fee}) +} +async function addMultiIOTransaction(params: { + senderAddresses: string[] | 'coinbase', + recipients: { address: string, value: number }[], + fee?: number + }) { + const { senderAddresses, recipients, fee = 0 } = params; + const chain = 'BTC'; + const network = 'regtest'; + const txid = randomHex(64); + const inputs: MongoBound[] = []; + let inputsTotalValue = 0; + // in the case of coins there are no input coins but the input count needs to be 1 + let inputCount; + let outputCount = 2; + const coinbase = senderAddresses === 'coinbase' || senderAddresses[0] === 'coinbase'; + + const tip = await BitcoinBlockStorage.getLocalTip({ chain, network }); + expect(tip, 'addTransaction assumes block exists to add transactions to').to.exist; + if (!tip) { + return; + } + + if (coinbase) { + inputCount = 1; + inputsTotalValue = recipients[0].value; + + // insert coinbase utxos + await CoinStorage.collection.insertMany([ + { + chain: chain, + network: network, + mintTxid: txid, + mintIndex: 0, + coinbase: true, + mintHeight: tip.height, + value: recipients[0].value, + spentHeight: -2, + spentTxid: '', + address: recipients[0].address, + wallets: [], + script: Buffer.from('aiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPk='), + }, + { + chain: chain, + network: network, + mintTxid: txid, + mintIndex: 1, + coinbase: true, + mintHeight: tip.height, + value: 0, + spentHeight: -2, + spentTxid: '', + address: 'false', + wallets: [], + script: Buffer.from('ABQUkj119YtLFcslg+FddDk3uHkE4g=='), + } + ]); + } else { + let totalValueSent = 0; + for (const recipient of recipients) { + totalValueSent += recipient.value; + } + let changeAddress; + for (const senderAddress of senderAddresses) { + const remainingValue = totalValueSent - inputsTotalValue; + const smallestSufficientUtxo: MongoBound | null = await CoinStorage.collection.findOne( + { + address: senderAddress, + spentHeight: -2, + value: { $gte: remainingValue } + }, + { sort: { value: 1 }} + ); + + if (smallestSufficientUtxo) { + inputs.push(smallestSufficientUtxo); + inputCount++; + inputsTotalValue += smallestSufficientUtxo.value; + } else { + const utxos: MongoBound[] = [...await CoinStorage.collection + .find({ chain, network, address: senderAddress, spentHeight: -2 }) + .sort({ value: 1 }) + .toArray()]; + + do { + const utxo: MongoBound | undefined = utxos.pop(); + if (utxo) { + inputs.push(utxo); + inputsTotalValue += utxo.value; + inputCount++; + } else { + break; + } + } while (inputsTotalValue < totalValueSent); + } + if (inputsTotalValue >= totalValueSent + fee) { + changeAddress = senderAddress; + break; + } + } + expect(inputsTotalValue, "Not enough funds to create transaction").to.be.at.least(totalValueSent + fee); + + // update spent utxos, prevents future usage + await CoinStorage.collection.bulkWrite( + inputs.map(({ _id }) => { + return { + updateOne: { + filter: { chain, network, _id }, + update: { $set: { spentTxid: txid, spentHeight: tip.height }} + } + } + }) + ); + + // add new utxos + await CoinStorage.collection.insertMany([ + // utxos for every recipient + ...recipients.map(recipient => ({ + chain: chain, + network: network, + mintTxid: txid, + mintIndex: 0, + coinbase: false, + mintHeight: tip.height, + value: recipient.value, + spentHeight: -2, + spentTxid: '', + address: recipient.address, + wallets: [], + script: Buffer.from('ABT2FdLqYRcZotGH4hBg/uUcL0lwUA=='), + }) + ), + // change utxo + { + chain: chain, + network: network, + mintTxid: txid, + mintIndex: 1, + coinbase: false, + mintHeight: tip.height, + value: inputsTotalValue - totalValueSent - fee, + spentHeight: -2, + spentTxid: '', + address: changeAddress, + wallets: [], + script: Buffer.from('ABT2FdLqYRcZotGH4hBg/uUcL0lwUA=='), + } + ]); + } + + // add new transaction + await TransactionStorage.collection.insertOne({ + txid: txid, + chain: chain, + network: network, + blockHash: tip.hash, + blockTime: tip.time, + blockHeight: tip.height, + blockTimeNormalized: tip.timeNormalized, + fee, + value: inputsTotalValue - fee, + coinbase, + inputCount, + outputCount, + wallets: [], + locktime: 0, + size: 100 + }); + + // add transaction to block transaction count + await BitcoinBlockStorage.collection.updateOne( + { height: tip.height }, + { $inc: { totalTransactions: 1 } } + ); +} + +async function addBlock(params?: { + time?: Date +}) { + let { time } = params || {}; + const chain = 'BTC'; + const network = 'regtest'; + const tip = await BitcoinBlockStorage.getLocalTip({ chain, network }); + const hash = randomHex(64); + if (!time) + time = tip ? new Date(tip.time.getTime() + 1000 * 60 * 10) : new Date(); + await BitcoinBlockStorage.collection.insertOne({ + network: 'regtest', + chain: chain, + hash: hash, + bits: 545259519, + height: tip ? tip.height + 1 : 0, + merkleRoot: '760a46b4f94ab17350a3ed299546fb5648c025ad9bd22271be38cf075c9cf3f4', + nextBlockHash: '', + nonce: 0, + previousBlockHash: (tip) ? tip.hash : '0000000000000000000000000000000000000000000000000000000000000000', + processed: true, + reward: 1250000000, + size: 214, + time: time, + timeNormalized: time, + transactionCount: 0, + version: 805306368 + }); + + if (tip) { + await BitcoinBlockStorage.collection.updateOne({ hash: tip.hash }, { $set: { nextBlockHash: hash } }) + } +} + +describe('Wallet Routes', function() { + let sandbox; + let firstBlockTime = minutesAgo(60); + const addressBalanceAtFirstBlock = 500_000; + const missingValue1 = 400_000; + const addressBalanceAtSecondBlock = addressBalanceAtFirstBlock + 500_000 - missingValue1; + + before(async function() { + this.timeout(15000); + await intBeforeHelper(); + await resetDatabase(); + await addBlock({ time: firstBlockTime }); + await addTransaction({ senderAddress: 'coinbase', recieverAddress: address, value: addressBalanceAtFirstBlock }); + await addBlock(); + await addTransaction({ senderAddress: 'coinbase', recieverAddress: address, value: 500_000 }); + for (let i = 0; i < 3; i++) { + await addTransaction({ senderAddress: address, recieverAddress: missingAddress1, value: missingValue1 / 4 }); + } + await addTransaction({ senderAddress: address, recieverAddress: missingAddress2, value: missingValue1 / 4 }); + await addBlock(); + await addTransaction({ senderAddress: 'coinbase', recieverAddress: address, value: 100_000 }); + await addMultiIOTransaction({ senderAddresses: [missingAddress1, missingAddress2, address], recipients: [{ address: address2, value: 500_000 }]}) + }); + + beforeEach(function() { + sandbox = sinon.createSandbox(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + after(async function() { + await intAfterHelper(); + }); + + const updateWallet = async (params: { privKey, pubKey, address }) => { + const { privKey, pubKey, address } = params; + + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}`; + const body = [{ address: address }]; + const chainStateUpdateWalletSpy = sandbox.spy(ChainStateProvider, 'updateWallet'); + const walletAddressStorageUpdateCoinsSpy = sandbox.spy(WalletAddressStorage, 'updateCoins'); + + await request.post(url) + .set('x-signature', getSignature(privKey, 'POST', url, body)) + .send(body); + expect(chainStateUpdateWalletSpy.calledOnce).to.be.true; + expect(walletAddressStorageUpdateCoinsSpy.calledOnce).to.be.true; + } + + it('should have empty wallets db at start', done => { + WalletStorage.collection.findOne({}).then(doc => { + expect(doc).to.be.null; + done(); + }).catch(done); + }); + + it('should not have wallet before it is created', done => { + const spy = sandbox.spy(ChainStateProvider, 'createWallet'); + request.get(`/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}`) + .expect(404, (err, res) => { + if (err) console.error(err); + expect(res.text).to.include('Wallet not found'); + // test to make sure sinon is working and not giving genuine positives in other tests + expect(spy.notCalled).to.be.true; + done(); + }); + }); + + it('should create a new wallet', done => { + const spy = sandbox.spy(ChainStateProvider, 'createWallet'); + request.post(`/api/${wallet.chain}/${wallet.network}/wallet`) + .send(wallet) + .expect(200, (err, res) => { + if (err) console.error(err); + expect(spy.calledOnce).to.be.true; + testWalletEquivalence(wallet, res.body); + done(); + }); + }); + + it('should not add wallet twice', done => { + // const createSpy = sandbox.spy(ChainStateProvider, 'createWallet'); + const getSpy = sandbox.spy(ChainStateProvider, 'getWallet'); + request.post(`/api/${wallet.chain}/${wallet.network}/wallet`) + .send(wallet) + .expect(200, (err, res) => { + if (err) console.error(err); + // expect(createSpy.notCalled).to.be.true; + expect(getSpy.calledOnce).to.be.true; + expect(res.text).to.equal('Wallet already exists'); + done(); + }); + }); + + it('should get wallet initial balance and it should be empty', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/balance`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + const { confirmed, unconfirmed, balance } = res.body; + expect(confirmed).to.equal(0); + expect(unconfirmed).to.equal(0); + expect(balance).to.equal(0); + done(); + }); + }); + + it('should have document in wallets', done => { + WalletStorage.collection.findOne(wallet).then(doc => { + testWalletEquivalence(wallet, doc); + done(); + }).catch(done); + }); + + it('should get wallet initial utxos and it should be empty', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/utxos`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + expect(res.body).to.deep.equal([]); + done(); + }); + }); + + it('should check wallet before it exists', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/check`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + const { lastAddress, sum } = res.body; + expect(sum).to.equal(0) + expect(lastAddress).to.be.undefined; + done(); + }); + }); + + it('should update wallet', done => { + updateWallet({ privKey, pubKey, address }).then(done); + }); + + it('should get address after update', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/addresses`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + expect(res.body).to.deep.include({ address: address }); + done(); + }); + }); + + it('should check wallet', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/check`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + const { lastAddress, sum } = res.body; + expect(lastAddress).to.equal(address); + + const expectedSum = Buffer.from(address).reduce((tot, cur) => (tot + cur) % Number.MAX_SAFE_INTEGER); + expect(sum).to.equal(expectedSum); + expect(sum).to.be.greaterThan(0); + done(); + }); + }); + + it('should get wallet', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + testWalletEquivalence(wallet, res.body); + expect(res.body._id).to.exist.and.to.be.a('string').with.length(24); + done(); + }); + }); + + it('should get new wallet balance before time (first block only)', done => { + const fiveMinutesAfterFirstBlock = new Date(firstBlockTime.getTime() + 1000 * 60 * 5) + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/balance/${fiveMinutesAfterFirstBlock.toISOString()}`; + (url); + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + const { confirmed, unconfirmed, balance } = res.body; + expect(confirmed).to.be.greaterThan(0).and.to.equal(addressBalanceAtFirstBlock); + expect(unconfirmed).to.equal(0); + expect(balance).to.greaterThan(0).and.to.equal(addressBalanceAtFirstBlock); + done(); + }); + }); + + it('should get new wallet balance before time (second block and down)', done => { + const fiveMinutesAfterSecondBlock = new Date(firstBlockTime.getTime() + 1000 * 60 * 15) + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/balance/${fiveMinutesAfterSecondBlock.toISOString()}`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + const { confirmed, unconfirmed, balance } = res.body; + expect(confirmed).to.be.greaterThan(0).and.to.equal(addressBalanceAtSecondBlock); + expect(unconfirmed).to.equal(0); + expect(balance).to.greaterThan(0).and.to.equal(addressBalanceAtSecondBlock); + done(); + }); + }); + + it('should get wallet added transactions', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/transactions`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + const transactions = res.text.split('\n').slice(0, -1).map(line => JSON.parse(line)); + for (const tx of transactions) { + testWalletTransaction(tx); + } + done(); + }) + }); + + { + let balance1; + it('should get new wallet balance', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/balance`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, async (err, res) => { + if (err) console.error(err); + const { confirmed, unconfirmed, balance } = res.body; + expect(confirmed).to.be.greaterThan(0); + expect(unconfirmed).to.equal(0); + expect(balance).to.greaterThan(0); + balance1 = balance; + done(); + }); + }); + + it('should get wallet added utxos', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/utxos`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + let balance = 0; + for (const utxo of res.body) { + expect(utxo.chain).to.equal('BTC'); + expect(utxo.network).to.equal('regtest'); + balance += utxo.value; + testCoin(utxo); + } + expect(balance).to.equal(balance1); + done(); + }); + }); + + it('should handle block updating (1/4): adding block and transactions directly to mongodb', done => { + const value = 10_000; + const fee = 100; + addBlock() + .then(async () => { + await addTransaction({ senderAddress: address, recieverAddress: missingAddress1, value, fee }); + done(); + }); + }); + + it('should handle block updating (2/4): updating and reprocessing address via api', done => { + const url = `/api/BTC/regtest/wallet/${pubKey}` + const body = [{ address: address }]; + + request.post(url) + .set('x-signature', getSignature(privKey, 'POST', url, body)) + .set('x-reprocess', getSignature(bwsPrivKey, 'reprocess', '/addAddresses' + pubKey, body)) + .send(body) + .expect(200, done); + }); + + it('should handle block updating (3/4): get blocks utxos', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/utxos`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + let balance = 0; + for (const utxo of res.body) { + expect(utxo.chain).to.equal('BTC'); + expect(utxo.network).to.equal('regtest'); + balance += utxo.value; + testCoin(utxo); + } + done(); + }); + }); + + it('should handle block updating (4/4): get balance', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/balance`; + const value = 10_000; + const fee = 100; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + const { confirmed, unconfirmed, balance } = res.body; + expect(balance1 - balance).to.equal(value + fee); + expect(balance1 - confirmed).to.equal(value + fee); + expect(unconfirmed).to.be.at.least(0); + done(); + }); + }); + }; + + it('should get address after update and reprocess', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/addresses`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + expect(res.body).to.deep.include({ address: address }); + done(); + }); + }); + + it('should check wallet after update and reprocess', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/check`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + const { lastAddress, sum } = res.body; + expect(lastAddress).to.equal(address); + + const expectedSum = Buffer.from(address).reduce((tot, cur) => (tot + cur) % Number.MAX_SAFE_INTEGER); + expect(sum).to.equal(expectedSum); + expect(sum).to.be.greaterThan(0); + done(); + }); + }); + + it('should get wallet missing addresses', done => { + const url = `/api/${wallet.chain}/${wallet.network}/wallet/${pubKey}/addresses/missing`; + request.get(url) + .set('x-signature', getSignature(privKey, 'GET', url)) + .expect(200, (err, res) => { + if (err) console.error(err); + const lines = res.text.split('\n').slice(0, -1); + const data = JSON.parse(lines.at(-1)); + + let expectedTotal = 0; + const expectedMissingAddresses: string[] = []; + + interface MissingAddressCoin { + value: number; + address: string; + expected: string; + wallets: []; + _id: string[]; + }; + + const coinData: MissingAddressCoin[] = JSON.parse(lines[0]).missing; + for (const coin of coinData) { + expectedTotal += coin.value; + expect(Address.isValid(coin.address, 'regtest')).to.be.true; + expect(coin._id).to.match(mongoIdRegex); + expect(coin.expected).to.match(mongoIdRegex); + expect(coin.wallets).to.exist.and.to.be.an('array'); + expect(coin.value).to.be.at.least(0); + expect(coinData.filter(c => c._id === coin._id).length).to.equal(1); + expectedMissingAddresses.push(coin.address); + } + + const { allMissingAddresses, totalMissingValue } = data; + expect(allMissingAddresses).to.include(missingAddress1); + expect(allMissingAddresses).to.include(missingAddress2); + expect(allMissingAddresses).to.deep.equal(expectedMissingAddresses); + expect(totalMissingValue).to.equal(expectedTotal); + expect(totalMissingValue).to.equal(missingValue1); + done(); + }) + }); +}); \ No newline at end of file