From e173576667602b93ab98d2f75df8a4d08f113e4b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 17 Aug 2020 13:13:35 +0200 Subject: [PATCH 1/5] Create blocks for Tendermint client tests --- packages/tendermint-rpc/src/client.spec.ts | 218 ++++++++++++--------- 1 file changed, 123 insertions(+), 95 deletions(-) diff --git a/packages/tendermint-rpc/src/client.spec.ts b/packages/tendermint-rpc/src/client.spec.ts index c7466c95..da9eff42 100644 --- a/packages/tendermint-rpc/src/client.spec.ts +++ b/packages/tendermint-rpc/src/client.spec.ts @@ -14,8 +14,12 @@ import * as responses from "./responses"; import { HttpClient, RpcClient, WebsocketClient } from "./rpcclients"; import { TxBytes } from "./types"; +function tendermintEnabled(): boolean { + return !!process.env.TENDERMINT_ENABLED; +} + function pendingWithoutTendermint(): void { - if (!process.env.TENDERMINT_ENABLED) { + if (!tendermintEnabled()) { pending("Set TENDERMINT_ENABLED to enable tendermint-based tests"); } } @@ -124,118 +128,142 @@ function defaultTestSuite(rpcFactory: () => RpcClient, adaptor: Adaptor): void { client.disconnect(); }); - it("can call status", async () => { - pendingWithoutTendermint(); - const client = new Client(rpcFactory(), adaptor); + describe("status", () => { + it("works", async () => { + pendingWithoutTendermint(); + const client = new Client(rpcFactory(), adaptor); - const status = await client.status(); - expect(status.nodeInfo.other.size).toBeGreaterThanOrEqual(2); - expect(status.nodeInfo.other.get("tx_index")).toEqual("on"); - expect(status.validatorInfo.pubkey).toBeTruthy(); - expect(status.validatorInfo.votingPower).toBeGreaterThan(0); - expect(status.syncInfo.catchingUp).toEqual(false); - expect(status.syncInfo.latestBlockHeight).toBeGreaterThanOrEqual(1); + const status = await client.status(); + expect(status.nodeInfo.other.size).toBeGreaterThanOrEqual(2); + expect(status.nodeInfo.other.get("tx_index")).toEqual("on"); + expect(status.validatorInfo.pubkey).toBeTruthy(); + expect(status.validatorInfo.votingPower).toBeGreaterThan(0); + expect(status.syncInfo.catchingUp).toEqual(false); + expect(status.syncInfo.latestBlockHeight).toBeGreaterThanOrEqual(1); - client.disconnect(); + client.disconnect(); + }); }); - it("can query a tx properly", async () => { - pendingWithoutTendermint(); - const client = new Client(rpcFactory(), adaptor); + describe("tx", () => { + it("can query a tx properly", async () => { + pendingWithoutTendermint(); + const client = new Client(rpcFactory(), adaptor); - const find = randomString(); - const me = randomString(); - const tx = buildKvTx(find, me); - - const txRes = await client.broadcastTxCommit({ tx: tx }); - expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true); - expect(txRes.height).toBeTruthy(); - const height: number = txRes.height || 0; // || 0 for type system - expect(txRes.hash.length).not.toEqual(0); - const hash = txRes.hash; - - await tendermintSearchIndexUpdated(); - - // find by hash - does it match? - const r = await client.tx({ hash: hash, prove: true }); - // both values come from rpc, so same type (Buffer/Uint8Array) - expect(r.hash).toEqual(hash); - // force the type when comparing to locally generated value - expect(r.tx).toEqual(tx); - expect(r.height).toEqual(height); - expect(r.proof).toBeTruthy(); - - // txSearch - you must enable the indexer when running - // tendermint, else you get empty results - const query = buildQuery({ tags: [{ key: "app.key", value: find }] }); - - const s = await client.txSearch({ query: query, page: 1, per_page: 30 }); - // should find the tx - expect(s.totalCount).toEqual(1); - // should return same info as querying directly, - // except without the proof - expect(s.txs[0]).toEqual({ ...r, proof: undefined }); - - // ensure txSearchAll works as well - const sall = await client.txSearchAll({ query: query }); - // should find the tx - expect(sall.totalCount).toEqual(1); - // should return same info as querying directly, - // except without the proof - expect(sall.txs[0]).toEqual({ ...r, proof: undefined }); - - // and let's query the block itself to see this transaction - const block = await client.block(height); - expect(block.block.txs.length).toEqual(1); - expect(block.block.txs[0]).toEqual(tx); - - client.disconnect(); - }); - - it("can paginate over txSearch results", async () => { - pendingWithoutTendermint(); - const client = new Client(rpcFactory(), adaptor); - - const find = randomString(); - const query = buildQuery({ tags: [{ key: "app.key", value: find }] }); - - async function sendTx(): Promise { + const find = randomString(); const me = randomString(); const tx = buildKvTx(find, me); const txRes = await client.broadcastTxCommit({ tx: tx }); expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true); expect(txRes.height).toBeTruthy(); + const height: number = txRes.height || 0; // || 0 for type system expect(txRes.hash.length).not.toEqual(0); - } + const hash = txRes.hash; - // send 3 txs - await sendTx(); - await sendTx(); - await sendTx(); + await tendermintSearchIndexUpdated(); - await tendermintSearchIndexUpdated(); + // find by hash - does it match? + const r = await client.tx({ hash: hash, prove: true }); + // both values come from rpc, so same type (Buffer/Uint8Array) + expect(r.hash).toEqual(hash); + // force the type when comparing to locally generated value + expect(r.tx).toEqual(tx); + expect(r.height).toEqual(height); + expect(r.proof).toBeTruthy(); - // expect one page of results - const s1 = await client.txSearch({ query: query, page: 1, per_page: 2 }); - expect(s1.totalCount).toEqual(3); - expect(s1.txs.length).toEqual(2); + // txSearch - you must enable the indexer when running + // tendermint, else you get empty results + const query = buildQuery({ tags: [{ key: "app.key", value: find }] }); - // second page - const s2 = await client.txSearch({ query: query, page: 2, per_page: 2 }); - expect(s2.totalCount).toEqual(3); - expect(s2.txs.length).toEqual(1); + const s = await client.txSearch({ query: query, page: 1, per_page: 30 }); + // should find the tx + expect(s.totalCount).toEqual(1); + // should return same info as querying directly, + // except without the proof + expect(s.txs[0]).toEqual({ ...r, proof: undefined }); - // and all together now - const sall = await client.txSearchAll({ query: query, per_page: 2 }); - expect(sall.totalCount).toEqual(3); - expect(sall.txs.length).toEqual(3); - // make sure there are in order from lowest to highest height - const [tx1, tx2, tx3] = sall.txs; - expect(tx2.height).toEqual(tx1.height + 1); - expect(tx3.height).toEqual(tx2.height + 1); + // ensure txSearchAll works as well + const sall = await client.txSearchAll({ query: query }); + // should find the tx + expect(sall.totalCount).toEqual(1); + // should return same info as querying directly, + // except without the proof + expect(sall.txs[0]).toEqual({ ...r, proof: undefined }); - client.disconnect(); + // and let's query the block itself to see this transaction + const block = await client.block(height); + expect(block.block.txs.length).toEqual(1); + expect(block.block.txs[0]).toEqual(tx); + + client.disconnect(); + }); + }); + + describe("txSearch", () => { + const key = randomString(); + + beforeAll(async () => { + if (tendermintEnabled()) { + const client = new Client(rpcFactory(), adaptor); + + // eslint-disable-next-line no-inner-declarations + async function sendTx(): Promise { + const me = randomString(); + const tx = buildKvTx(key, me); + + const txRes = await client.broadcastTxCommit({ tx: tx }); + expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true); + expect(txRes.height).toBeTruthy(); + expect(txRes.hash.length).not.toEqual(0); + } + + // send 3 txs + await sendTx(); + await sendTx(); + await sendTx(); + + client.disconnect(); + + await tendermintSearchIndexUpdated(); + } + }); + + it("can paginate over txSearch results", async () => { + pendingWithoutTendermint(); + const client = new Client(rpcFactory(), adaptor); + + const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + + // expect one page of results + const s1 = await client.txSearch({ query: query, page: 1, per_page: 2 }); + expect(s1.totalCount).toEqual(3); + expect(s1.txs.length).toEqual(2); + + // second page + const s2 = await client.txSearch({ query: query, page: 2, per_page: 2 }); + expect(s2.totalCount).toEqual(3); + expect(s2.txs.length).toEqual(1); + + client.disconnect(); + }); + + it("can get all search results in one call", async () => { + pendingWithoutTendermint(); + const client = new Client(rpcFactory(), adaptor); + + const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + + const sall = await client.txSearchAll({ query: query, per_page: 2 }); + expect(sall.totalCount).toEqual(3); + expect(sall.txs.length).toEqual(3); + // make sure there are in order from lowest to highest height + const [tx1, tx2, tx3] = sall.txs; + expect(tx2.height).toEqual(tx1.height + 1); + expect(tx3.height).toEqual(tx2.height + 1); + + client.disconnect(); + }); }); } From 9955ec0dbece6105286fed4661bce64672c7cb51 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 17 Aug 2020 13:44:16 +0200 Subject: [PATCH 2/5] Add better docs and test for Client.blockchain --- packages/tendermint-rpc/src/client.spec.ts | 80 ++++++++++++++++++- packages/tendermint-rpc/src/client.ts | 3 + packages/tendermint-rpc/src/requests.ts | 2 + packages/tendermint-rpc/src/responses.ts | 2 + packages/tendermint-rpc/src/testutil.spec.ts | 1 + packages/tendermint-rpc/src/v0-33/requests.ts | 1 + packages/tendermint-rpc/types/client.d.ts | 3 + packages/tendermint-rpc/types/requests.d.ts | 1 + 8 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 packages/tendermint-rpc/src/testutil.spec.ts diff --git a/packages/tendermint-rpc/src/client.spec.ts b/packages/tendermint-rpc/src/client.spec.ts index da9eff42..8aa7d510 100644 --- a/packages/tendermint-rpc/src/client.spec.ts +++ b/packages/tendermint-rpc/src/client.spec.ts @@ -12,6 +12,7 @@ import { tendermintInstances } from "./config.spec"; import { buildQuery } from "./requests"; import * as responses from "./responses"; import { HttpClient, RpcClient, WebsocketClient } from "./rpcclients"; +import { chainIdMatcher } from "./testutil.spec"; import { TxBytes } from "./types"; function tendermintEnabled(): boolean { @@ -145,6 +146,81 @@ function defaultTestSuite(rpcFactory: () => RpcClient, adaptor: Adaptor): void { }); }); + describe("blockchain", () => { + it("returns latest in descending order by default", async () => { + pendingWithoutTendermint(); + const client = new Client(rpcFactory(), adaptor); + + // Run in parallel to increase chance there is no block between the calls + const [status, blockchain] = await Promise.all([client.status(), client.blockchain()]); + const height = status.syncInfo.latestBlockHeight; + + expect(blockchain.lastHeight).toEqual(height); + expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(3); + expect(blockchain.blockMetas[0].header.height).toEqual(height); + expect(blockchain.blockMetas[1].header.height).toEqual(height - 1); + expect(blockchain.blockMetas[2].header.height).toEqual(height - 2); + + client.disconnect(); + }); + + it("can limit by maxHeight", async () => { + pendingWithoutTendermint(); + const client = new Client(rpcFactory(), adaptor); + + const height = (await client.status()).syncInfo.latestBlockHeight; + const blockchain = await client.blockchain(undefined, height - 1); + expect(blockchain.lastHeight).toEqual(height); + expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(2); + expect(blockchain.blockMetas[0].header.height).toEqual(height - 1); // upper limit included + expect(blockchain.blockMetas[1].header.height).toEqual(height - 2); + + client.disconnect(); + }); + + it("can limit by minHeight and maxHeight", async () => { + pendingWithoutTendermint(); + const client = new Client(rpcFactory(), adaptor); + + const height = (await client.status()).syncInfo.latestBlockHeight; + const blockchain = await client.blockchain(height - 2, height - 1); + expect(blockchain.lastHeight).toEqual(height); + expect(blockchain.blockMetas.length).toEqual(2); + expect(blockchain.blockMetas[0].header.height).toEqual(height - 1); // upper limit included + expect(blockchain.blockMetas[1].header.height).toEqual(height - 2); // lower limit included + + client.disconnect(); + }); + + it("contains all the info", async () => { + pendingWithoutTendermint(); + const client = new Client(rpcFactory(), adaptor); + + const height = (await client.status()).syncInfo.latestBlockHeight; + const blockchain = await client.blockchain(height - 1, height - 1); + + expect(blockchain.lastHeight).toEqual(height); + expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(1); + const meta = blockchain.blockMetas[0]; + + // TODO: check all the fields + expect(meta).toEqual({ + blockId: jasmine.objectContaining({}), + // block_size: jasmine.stringMatching(nonNegativeIntegerMatcher), + // num_txs: jasmine.stringMatching(nonNegativeIntegerMatcher), + header: jasmine.objectContaining({ + version: { + block: 10, + app: 1, + }, + chainId: jasmine.stringMatching(chainIdMatcher), + }), + }); + + client.disconnect(); + }); + }); + describe("tx", () => { it("can query a tx properly", async () => { pendingWithoutTendermint(); @@ -280,7 +356,7 @@ function websocketTestSuite(rpcFactory: () => RpcClient, adaptor: Adaptor, appCr expect(stream).toBeTruthy(); const subscription = stream.subscribe({ next: (event) => { - expect(event.chainId).toMatch(/^[-a-zA-Z0-9]{3,30}$/); + expect(event.chainId).toMatch(chainIdMatcher); expect(event.height).toBeGreaterThan(0); // seems that tendermint just guarantees within the last second for timestamp expect(event.time.getTime()).toBeGreaterThan(testStart - 1000); @@ -337,7 +413,7 @@ function websocketTestSuite(rpcFactory: () => RpcClient, adaptor: Adaptor, appCr const stream = client.subscribeNewBlock(); const subscription = stream.subscribe({ next: (event) => { - expect(event.header.chainId).toMatch(/^[-a-zA-Z0-9]{3,30}$/); + expect(event.header.chainId).toMatch(chainIdMatcher); expect(event.header.height).toBeGreaterThan(0); // seems that tendermint just guarantees within the last second for timestamp expect(event.header.time.getTime()).toBeGreaterThan(testStart - 1000); diff --git a/packages/tendermint-rpc/src/client.ts b/packages/tendermint-rpc/src/client.ts index 23fd42ec..9bda89ea 100644 --- a/packages/tendermint-rpc/src/client.ts +++ b/packages/tendermint-rpc/src/client.ts @@ -74,6 +74,9 @@ export class Client { return this.doCall(query, this.p.encodeBlockResults, this.r.decodeBlockResults); } + /** + * Get block headers for minHeight <= height <= maxHeight. + */ public async blockchain(minHeight?: number, maxHeight?: number): Promise { const query: requests.BlockchainRequest = { method: requests.Method.Blockchain, diff --git a/packages/tendermint-rpc/src/requests.ts b/packages/tendermint-rpc/src/requests.ts index 21ba5648..bb6b1b83 100644 --- a/packages/tendermint-rpc/src/requests.ts +++ b/packages/tendermint-rpc/src/requests.ts @@ -10,6 +10,7 @@ export enum Method { AbciInfo = "abci_info", AbciQuery = "abci_query", Block = "block", + /** Get block headers for minHeight <= height <= maxHeight. */ Blockchain = "blockchain", BlockResults = "block_results", BroadcastTxAsync = "broadcast_tx_async", @@ -84,6 +85,7 @@ export interface BlockchainRequest { readonly method: Method.Blockchain; readonly params: BlockchainRequestParams; } + export interface BlockchainRequestParams { readonly minHeight?: number; readonly maxHeight?: number; diff --git a/packages/tendermint-rpc/src/responses.ts b/packages/tendermint-rpc/src/responses.ts index 1eb25b6f..bfbde872 100644 --- a/packages/tendermint-rpc/src/responses.ts +++ b/packages/tendermint-rpc/src/responses.ts @@ -186,6 +186,8 @@ export interface TxProof { export interface BlockMeta { readonly blockId: BlockId; readonly header: Header; + // TODO: Add blockSize (e.g "block_size": "471") + // TODO: Add numTxs (e.g "num_txs": "0") } export interface BlockId { diff --git a/packages/tendermint-rpc/src/testutil.spec.ts b/packages/tendermint-rpc/src/testutil.spec.ts new file mode 100644 index 00000000..de7ecb19 --- /dev/null +++ b/packages/tendermint-rpc/src/testutil.spec.ts @@ -0,0 +1 @@ +export const chainIdMatcher = /^[-a-zA-Z0-9]{3,30}$/; diff --git a/packages/tendermint-rpc/src/v0-33/requests.ts b/packages/tendermint-rpc/src/v0-33/requests.ts index f3c1dc7a..0e6b71c3 100644 --- a/packages/tendermint-rpc/src/v0-33/requests.ts +++ b/packages/tendermint-rpc/src/v0-33/requests.ts @@ -22,6 +22,7 @@ interface RpcBlockchainRequestParams { readonly minHeight?: IntegerString; readonly maxHeight?: IntegerString; } + function encodeBlockchainRequestParams(param: requests.BlockchainRequestParams): RpcBlockchainRequestParams { return { minHeight: may(Integer.encode, param.minHeight), diff --git a/packages/tendermint-rpc/types/client.d.ts b/packages/tendermint-rpc/types/client.d.ts index 550db050..5844ad64 100644 --- a/packages/tendermint-rpc/types/client.d.ts +++ b/packages/tendermint-rpc/types/client.d.ts @@ -15,6 +15,9 @@ export declare class Client { abciQuery(params: requests.AbciQueryParams): Promise; block(height?: number): Promise; blockResults(height?: number): Promise; + /** + * Get block headers for minHeight <= height <= maxHeight. + */ blockchain(minHeight?: number, maxHeight?: number): Promise; /** * Broadcast transaction to mempool and wait for response diff --git a/packages/tendermint-rpc/types/requests.d.ts b/packages/tendermint-rpc/types/requests.d.ts index f17fa18d..c8c8f312 100644 --- a/packages/tendermint-rpc/types/requests.d.ts +++ b/packages/tendermint-rpc/types/requests.d.ts @@ -8,6 +8,7 @@ export declare enum Method { AbciInfo = "abci_info", AbciQuery = "abci_query", Block = "block", + /** Get block headers for minHeight <= height <= maxHeight. */ Blockchain = "blockchain", BlockResults = "block_results", BroadcastTxAsync = "broadcast_tx_async", From b94d130c84ce02bf218dcc8b487b43cfe19b4ec5 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 18 Aug 2020 09:15:08 +0200 Subject: [PATCH 3/5] Improve docs to highlight query+filter --- packages/tendermint-rpc/src/client.ts | 5 ++++- packages/tendermint-rpc/types/client.d.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/tendermint-rpc/src/client.ts b/packages/tendermint-rpc/src/client.ts index 9bda89ea..b5845a75 100644 --- a/packages/tendermint-rpc/src/client.ts +++ b/packages/tendermint-rpc/src/client.ts @@ -75,7 +75,10 @@ export class Client { } /** - * Get block headers for minHeight <= height <= maxHeight. + * Queries block headers filtered by minHeight <= height <= maxHeight. + * + * @param minHeight The minimum height to be included in the result. Defaults to 0. + * @param maxHeight The maximum height to be included in the result. Defaults to infinity. */ public async blockchain(minHeight?: number, maxHeight?: number): Promise { const query: requests.BlockchainRequest = { diff --git a/packages/tendermint-rpc/types/client.d.ts b/packages/tendermint-rpc/types/client.d.ts index 5844ad64..92606f94 100644 --- a/packages/tendermint-rpc/types/client.d.ts +++ b/packages/tendermint-rpc/types/client.d.ts @@ -16,7 +16,10 @@ export declare class Client { block(height?: number): Promise; blockResults(height?: number): Promise; /** - * Get block headers for minHeight <= height <= maxHeight. + * Queries block headers filtered by minHeight <= height <= maxHeight. + * + * @param minHeight The minimum height to be included in the result. Defaults to 0. + * @param maxHeight The maximum height to be included in the result. Defaults to infinity. */ blockchain(minHeight?: number, maxHeight?: number): Promise; /** From 9591c225127a7b5a5301e16e0107c9e97f4756f3 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 18 Aug 2020 09:15:21 +0200 Subject: [PATCH 4/5] Add future height test --- packages/tendermint-rpc/src/client.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/tendermint-rpc/src/client.spec.ts b/packages/tendermint-rpc/src/client.spec.ts index 8aa7d510..2d44e61d 100644 --- a/packages/tendermint-rpc/src/client.spec.ts +++ b/packages/tendermint-rpc/src/client.spec.ts @@ -178,6 +178,20 @@ function defaultTestSuite(rpcFactory: () => RpcClient, adaptor: Adaptor): void { client.disconnect(); }); + it("works with maxHeight in the future", async () => { + pendingWithoutTendermint(); + const client = new Client(rpcFactory(), adaptor); + + const height = (await client.status()).syncInfo.latestBlockHeight; + const blockchain = await client.blockchain(undefined, height + 20); + expect(blockchain.lastHeight).toEqual(height); + expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(2); + expect(blockchain.blockMetas[0].header.height).toEqual(height); + expect(blockchain.blockMetas[1].header.height).toEqual(height - 1); + + client.disconnect(); + }); + it("can limit by minHeight and maxHeight", async () => { pendingWithoutTendermint(); const client = new Client(rpcFactory(), adaptor); From 541559e3291f0d800e6166ecb869a74227caeaf4 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 18 Aug 2020 09:15:36 +0200 Subject: [PATCH 5/5] Remove obsolete client.blockchain(2, 4) test --- packages/tendermint-rpc/src/client.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/tendermint-rpc/src/client.spec.ts b/packages/tendermint-rpc/src/client.spec.ts index 2d44e61d..ff4703f0 100644 --- a/packages/tendermint-rpc/src/client.spec.ts +++ b/packages/tendermint-rpc/src/client.spec.ts @@ -119,7 +119,6 @@ function defaultTestSuite(rpcFactory: () => RpcClient, adaptor: Adaptor): void { const client = new Client(rpcFactory(), adaptor); expect(await client.block()).toBeTruthy(); - expect(await client.blockchain(2, 4)).toBeTruthy(); expect(await client.blockResults(3)).toBeTruthy(); expect(await client.commit(4)).toBeTruthy(); expect(await client.genesis()).toBeTruthy();