From 3ad87362ded673b6fa97150dc75383d82d8f0c41 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 1 Mar 2023 13:50:30 +0100 Subject: [PATCH 01/15] Add Tendermint 0.37 docker --- scripts/tendermint/all_start.sh | 1 + scripts/tendermint/all_stop.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/tendermint/all_start.sh b/scripts/tendermint/all_start.sh index f68135cb6c..49f9501c87 100755 --- a/scripts/tendermint/all_start.sh +++ b/scripts/tendermint/all_start.sh @@ -6,6 +6,7 @@ command -v shellcheck >/dev/null && shellcheck "$0" declare -a TM_VERSIONS TM_VERSIONS[34]=v0.34.19 TM_VERSIONS[35]=v0.35.6 +TM_VERSIONS[37]=v0.37.0-rc2 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/scripts/tendermint/all_stop.sh b/scripts/tendermint/all_stop.sh index 136dfd054f..f9f0e017d4 100755 --- a/scripts/tendermint/all_stop.sh +++ b/scripts/tendermint/all_stop.sh @@ -5,6 +5,7 @@ command -v shellcheck >/dev/null && shellcheck "$0" declare -a TM_VERSIONS TM_VERSIONS[34]=v0.34.19 TM_VERSIONS[35]=v0.35.6 +TM_VERSIONS[37]=v0.37.0-rc2 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" From 80194b3be9622e50319d671f3c2aed18ae0e1636 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 1 Mar 2023 13:51:11 +0100 Subject: [PATCH 02/15] Add Tendermint 37 testing instance --- packages/tendermint-rpc/src/testutil.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/tendermint-rpc/src/testutil.spec.ts b/packages/tendermint-rpc/src/testutil.spec.ts index c724a631a5..bcbd0dda2b 100644 --- a/packages/tendermint-rpc/src/testutil.spec.ts +++ b/packages/tendermint-rpc/src/testutil.spec.ts @@ -62,6 +62,19 @@ export const tendermintInstances = { appVersion: 1, }, }, + 37: { + url: "localhost:11137", + version: "0.37.x", + blockTime: 500, + expected: { + chainId: /^dockerchain$/, + version: /^$/, // Unfortunately we don't get info here + appCreator: "Cosmoshi Netowoko", + p2pVersion: 8, + blockVersion: 11, + appVersion: 1, + }, + }, }; export const defaultInstance: TendermintInstance = tendermintInstances[34]; From 81b7449770bc24de22478ebe0d80d74121cfe9f9 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 1 Mar 2023 13:57:47 +0100 Subject: [PATCH 03/15] Add `Tendermint37Client` --- CHANGELOG.md | 2 + packages/tendermint-rpc/src/index.ts | 2 + .../src/tendermint37/adaptor/index.ts | 13 + .../src/tendermint37/adaptor/requests.ts | 185 ++++ .../tendermint37/adaptor/responses.spec.ts | 124 +++ .../src/tendermint37/adaptor/responses.ts | 915 ++++++++++++++++++ .../src/tendermint37/adaptor/types.ts | 62 ++ .../src/tendermint37/encodings.spec.ts | 97 ++ .../src/tendermint37/encodings.ts | 198 ++++ .../src/tendermint37/hasher.spec.ts | 91 ++ .../tendermint-rpc/src/tendermint37/hasher.ts | 80 ++ .../tendermint-rpc/src/tendermint37/index.ts | 79 ++ .../src/tendermint37/requests.spec.ts | 41 + .../src/tendermint37/requests.ts | 208 ++++ .../src/tendermint37/responses.ts | 389 ++++++++ .../tendermint37/tendermint37client.spec.ts | 866 +++++++++++++++++ .../src/tendermint37/tendermint37client.ts | 356 +++++++ 17 files changed, 3708 insertions(+) create mode 100644 packages/tendermint-rpc/src/tendermint37/adaptor/index.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/adaptor/requests.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/adaptor/responses.spec.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/adaptor/responses.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/adaptor/types.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/encodings.spec.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/encodings.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/hasher.spec.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/hasher.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/index.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/requests.spec.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/requests.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/responses.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts create mode 100644 packages/tendermint-rpc/src/tendermint37/tendermint37client.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bdb018257..f0ca36df49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,8 +29,10 @@ and this project adheres to [#1329]: https://github.com/cosmos/cosmjs/pull/1329 ### Added + - @cosmjs/stargate: Add `granteeGrants` and `granterGrants` queries to `AuthzExtension` ([#1308]). +- @cosmjs/tendermint-rpc: Add `Tendermint37Client` [#1308]: https://github.com/cosmos/cosmjs/pull/1308 diff --git a/packages/tendermint-rpc/src/index.ts b/packages/tendermint-rpc/src/index.ts index a1556d15df..7849709a66 100644 --- a/packages/tendermint-rpc/src/index.ts +++ b/packages/tendermint-rpc/src/index.ts @@ -99,6 +99,8 @@ export { Tendermint34Client } from "./tendermint34"; // See https://github.com/cosmos/cosmjs/issues/1225 for more context. // export * as tendermint35 from "./tendermint35"; // export { Tendermint35Client } from "./tendermint35"; +export * as tendermint37 from "./tendermint37"; +export { Tendermint37Client } from "./tendermint37"; export { BlockIdFlag, CommitSignature, diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts new file mode 100644 index 0000000000..0fa5031aaa --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts @@ -0,0 +1,13 @@ +import { hashBlock, hashTx } from "../hasher"; +import { Params } from "./requests"; +import { Responses } from "./responses"; +import { Adaptor } from "./types"; + +export { Decoder, Encoder, Params, Responses } from "./types"; + +export const adaptor37: Adaptor = { + params: Params, + responses: Responses, + hashTx: hashTx, + hashBlock: hashBlock, +}; diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/requests.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/requests.ts new file mode 100644 index 0000000000..52a06a2932 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/requests.ts @@ -0,0 +1,185 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { toBase64, toHex } from "@cosmjs/encoding"; +import { JsonRpcRequest } from "@cosmjs/json-rpc"; + +import { smallIntToApi } from "../../inthelpers"; +import { createJsonRpcRequest } from "../../jsonrpc"; +import { assertNotEmpty, may } from "../encodings"; +import * as requests from "../requests"; + +interface HeightParam { + readonly height?: number; +} +interface RpcHeightParam { + readonly height?: string; +} +function encodeHeightParam(param: HeightParam): RpcHeightParam { + return { + height: may(smallIntToApi, param.height), + }; +} + +interface RpcBlockchainRequestParams { + readonly minHeight?: string; + readonly maxHeight?: string; +} + +function encodeBlockchainRequestParams(param: requests.BlockchainRequestParams): RpcBlockchainRequestParams { + return { + minHeight: may(smallIntToApi, param.minHeight), + maxHeight: may(smallIntToApi, param.maxHeight), + }; +} + +interface RpcBlockSearchParams { + readonly query: string; + readonly page?: string; + readonly per_page?: string; + readonly order_by?: string; +} +function encodeBlockSearchParams(params: requests.BlockSearchParams): RpcBlockSearchParams { + return { + query: params.query, + page: may(smallIntToApi, params.page), + per_page: may(smallIntToApi, params.per_page), + order_by: params.order_by, + }; +} + +interface RpcAbciQueryParams { + readonly path: string; + /** hex encoded */ + readonly data: string; + readonly height?: string; + readonly prove?: boolean; +} + +function encodeAbciQueryParams(params: requests.AbciQueryParams): RpcAbciQueryParams { + return { + path: assertNotEmpty(params.path), + data: toHex(params.data), + height: may(smallIntToApi, params.height), + prove: params.prove, + }; +} + +interface RpcBroadcastTxParams { + /** base64 encoded */ + readonly tx: string; +} +function encodeBroadcastTxParams(params: requests.BroadcastTxParams): RpcBroadcastTxParams { + return { + tx: toBase64(assertNotEmpty(params.tx)), + }; +} + +interface RpcTxParams { + /** hex encoded */ + readonly hash: string; + readonly prove?: boolean; +} +function encodeTxParams(params: requests.TxParams): RpcTxParams { + return { + hash: toHex(assertNotEmpty(params.hash)), + prove: params.prove, + }; +} + +interface RpcTxSearchParams { + readonly query: string; + readonly prove?: boolean; + readonly page?: string; + readonly per_page?: string; + readonly order_by?: string; +} +function encodeTxSearchParams(params: requests.TxSearchParams): RpcTxSearchParams { + return { + query: params.query, + prove: params.prove, + page: may(smallIntToApi, params.page), + per_page: may(smallIntToApi, params.per_page), + order_by: params.order_by, + }; +} + +interface RpcValidatorsParams { + readonly height?: string; + readonly page?: string; + readonly per_page?: string; +} +function encodeValidatorsParams(params: requests.ValidatorsParams): RpcValidatorsParams { + return { + height: may(smallIntToApi, params.height), + page: may(smallIntToApi, params.page), + per_page: may(smallIntToApi, params.per_page), + }; +} + +export class Params { + public static encodeAbciInfo(req: requests.AbciInfoRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method); + } + + public static encodeAbciQuery(req: requests.AbciQueryRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeAbciQueryParams(req.params)); + } + + public static encodeBlock(req: requests.BlockRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeHeightParam(req.params)); + } + + public static encodeBlockchain(req: requests.BlockchainRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeBlockchainRequestParams(req.params)); + } + + public static encodeBlockResults(req: requests.BlockResultsRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeHeightParam(req.params)); + } + + public static encodeBlockSearch(req: requests.BlockSearchRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeBlockSearchParams(req.params)); + } + + public static encodeBroadcastTx(req: requests.BroadcastTxRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeBroadcastTxParams(req.params)); + } + + public static encodeCommit(req: requests.CommitRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeHeightParam(req.params)); + } + + public static encodeGenesis(req: requests.GenesisRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method); + } + + public static encodeHealth(req: requests.HealthRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method); + } + + public static encodeNumUnconfirmedTxs(req: requests.NumUnconfirmedTxsRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method); + } + + public static encodeStatus(req: requests.StatusRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method); + } + + public static encodeSubscribe(req: requests.SubscribeRequest): JsonRpcRequest { + const eventTag = { key: "tm.event", value: req.query.type }; + const query = requests.buildQuery({ tags: [eventTag], raw: req.query.raw }); + return createJsonRpcRequest("subscribe", { query: query }); + } + + public static encodeTx(req: requests.TxRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeTxParams(req.params)); + } + + // TODO: encode params for query string??? + public static encodeTxSearch(req: requests.TxSearchRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeTxSearchParams(req.params)); + } + + public static encodeValidators(req: requests.ValidatorsRequest): JsonRpcRequest { + return createJsonRpcRequest(req.method, encodeValidatorsParams(req.params)); + } +} diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/responses.spec.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/responses.spec.ts new file mode 100644 index 0000000000..c85b753328 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/responses.spec.ts @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { fromBase64, fromHex } from "@cosmjs/encoding"; + +import { decodeEvent, decodeValidatorGenesis, decodeValidatorInfo, decodeValidatorUpdate } from "./responses"; + +describe("Adaptor Responses", () => { + describe("decodeEvent", () => { + it("works with attributes", () => { + // from https://rpc.mainnet-1.tgrade.confio.run/tx?hash=0x2C44715748022DB2FB5F40105383719BFCFCEE51DBC02FF4088BE3F5924CD7BF + const event = decodeEvent({ + type: "coin_spent", + attributes: [ + { key: "foo", value: "123" }, + { key: "bar", value: "456" }, + ], + }); + expect(event.type).toEqual("coin_spent"); + expect(event.attributes).toEqual([ + { key: "foo", value: "123" }, + { key: "bar", value: "456" }, + ]); + }); + + it("works with no attribute", () => { + const event = decodeEvent({ + type: "cosmos.module.EmittedEvent", + }); + expect(event.type).toEqual("cosmos.module.EmittedEvent"); + expect(event.attributes).toEqual([]); + }); + }); + + describe("decodeValidatorGenesis", () => { + it("works for genesis format", () => { + // from https://raw.githubusercontent.com/cosmos/mainnet/master/genesis.json + const validator = decodeValidatorGenesis({ + address: "A03DC128D38DB0BC5F18AE1872F1CB2E1FD41157", + name: "真本聪&IOSG", + power: "169980", + pub_key: { + type: "tendermint/PubKeyEd25519", + value: "2BX6Zuj8RmdJAkD1BAg6KB0v04liyM7jBdwOGIb9F9Q=", + }, + }); + expect(validator).toEqual({ + address: fromHex("A03DC128D38DB0BC5F18AE1872F1CB2E1FD41157"), + votingPower: BigInt(169980), + pubkey: { + algorithm: "ed25519", + data: fromBase64("2BX6Zuj8RmdJAkD1BAg6KB0v04liyM7jBdwOGIb9F9Q="), + }, + }); + }); + }); + + describe("decodeValidatorUpdate", () => { + it("works for block results format", () => { + // from https://rpc.cosmos.network/block_results?height=10539773 + const update = decodeValidatorUpdate({ + pub_key: { + Sum: { + type: "tendermint.crypto.PublicKey_Ed25519", + value: { + ed25519: "0kNlxBMpm+5WtfHIG1xsWatOXTKPLtmSqn3EiEIDZeI=", + }, + }, + }, + power: "11418237", + }); + expect(update).toEqual({ + pubkey: { + algorithm: "ed25519", + data: fromBase64("0kNlxBMpm+5WtfHIG1xsWatOXTKPLtmSqn3EiEIDZeI="), + }, + votingPower: BigInt(11418237), + }); + }); + + it("works for block results format without voting power", () => { + // from https://rpc.cosmos.network/block_results?height=10883046 + const update = decodeValidatorUpdate({ + pub_key: { + Sum: { + type: "tendermint.crypto.PublicKey_Ed25519", + value: { + ed25519: "HjSC7VkhKih6xMhudlqfaFE8ZZnP8RKJPv4iqR7RhcE=", + }, + }, + }, + }); + expect(update).toEqual({ + pubkey: { + algorithm: "ed25519", + data: fromBase64("HjSC7VkhKih6xMhudlqfaFE8ZZnP8RKJPv4iqR7RhcE="), + }, + votingPower: BigInt(0), + }); + }); + }); + + describe("decodeValidatorInfo", () => { + it("works for validators format", () => { + // from https://rpc.cosmos.network/validators?height=10601034 + const info = decodeValidatorInfo({ + address: "AC2D56057CD84765E6FBE318979093E8E44AA18F", + pub_key: { + type: "tendermint/PubKeyEd25519", + value: "0kNlxBMpm+5WtfHIG1xsWatOXTKPLtmSqn3EiEIDZeI=", + }, + voting_power: "11228980", + proposer_priority: "62870960", + }); + expect(info).toEqual({ + address: fromHex("AC2D56057CD84765E6FBE318979093E8E44AA18F"), + pubkey: { + algorithm: "ed25519", + data: fromBase64("0kNlxBMpm+5WtfHIG1xsWatOXTKPLtmSqn3EiEIDZeI="), + }, + votingPower: BigInt(11228980), + proposerPriority: 62870960, + }); + }); + }); +}); diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/responses.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/responses.ts new file mode 100644 index 0000000000..05aecedf8e --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/responses.ts @@ -0,0 +1,915 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { fromBase64, fromHex } from "@cosmjs/encoding"; +import { JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; +import { assert } from "@cosmjs/utils"; + +import { DateWithNanoseconds, fromRfc3339WithNanoseconds } from "../../dates"; +import { apiToBigInt, apiToSmallInt } from "../../inthelpers"; +import { SubscriptionEvent } from "../../rpcclients"; +import { BlockIdFlag, CommitSignature, ValidatorPubkey } from "../../types"; +import { + assertArray, + assertBoolean, + assertNotEmpty, + assertNumber, + assertObject, + assertSet, + assertString, + dictionaryToStringMap, + may, +} from "../encodings"; +import { hashTx } from "../hasher"; +import * as responses from "../responses"; + +interface AbciInfoResult { + readonly response: RpcAbciInfoResponse; +} + +interface RpcAbciInfoResponse { + readonly data?: string; + readonly last_block_height?: string; + /** base64 encoded */ + readonly last_block_app_hash?: string; +} + +function decodeAbciInfo(data: RpcAbciInfoResponse): responses.AbciInfoResponse { + return { + data: data.data, + lastBlockHeight: may(apiToSmallInt, data.last_block_height), + lastBlockAppHash: may(fromBase64, data.last_block_app_hash), + }; +} + +interface AbciQueryResult { + readonly response: RpcAbciQueryResponse; +} + +export interface RpcProofOp { + readonly type: string; + /** base64 encoded */ + readonly key: string; + /** base64 encoded */ + readonly data: string; +} + +export interface RpcQueryProof { + readonly ops: readonly RpcProofOp[]; +} + +function decodeQueryProof(data: RpcQueryProof): responses.QueryProof { + return { + ops: data.ops.map((op) => ({ + type: op.type, + key: fromBase64(op.key), + data: fromBase64(op.data), + })), + }; +} + +interface RpcAbciQueryResponse { + /** + * Base64 encoded + * + * This can be null since this is a byte slice and due to + * https://github.com/tendermint/tendermint/blob/v0.35.7/abci/types/result.go#L53 + */ + readonly key?: string | null; + /** + * Base64 encoded + * + * This can be null since this is a byte slice and due to + * https://github.com/tendermint/tendermint/blob/v0.35.7/abci/types/result.go#L53 + */ + readonly value?: string | null; + readonly proofOps?: RpcQueryProof | null; + readonly height?: string; + readonly index?: string; + readonly code?: string; // only for errors + readonly codespace?: string; + readonly log?: string; + readonly info?: string; +} + +function decodeAbciQuery(data: RpcAbciQueryResponse): responses.AbciQueryResponse { + return { + key: fromBase64(assertString(data.key ?? "")), + value: fromBase64(assertString(data.value ?? "")), + proof: may(decodeQueryProof, data.proofOps), + height: may(apiToSmallInt, data.height), + code: may(apiToSmallInt, data.code), + codespace: assertString(data.codespace ?? ""), + index: may(apiToSmallInt, data.index), + log: data.log, + info: assertString(data.info ?? ""), + }; +} + +/** + * EventAttribute from Tendermint. In 0.35 the type of key and value was changed + * from bytes to string, such that no base64 encoding is used anymore. + */ +interface RpcEventAttribute { + readonly key: string; + readonly value?: string; +} + +function decodeEventAttribute(attribute: RpcEventAttribute): responses.EventAttribute { + return { + key: assertNotEmpty(attribute.key), + value: attribute.value ?? "", + }; +} + +function decodeAttributes(attributes: readonly RpcEventAttribute[]): responses.EventAttribute[] { + return assertArray(attributes).map(decodeEventAttribute); +} + +interface RpcEvent { + readonly type: string; + /** Can be omitted (see https://github.com/cosmos/cosmjs/pull/1198) */ + readonly attributes?: readonly RpcEventAttribute[]; +} + +export function decodeEvent(event: RpcEvent): responses.Event { + return { + type: event.type, + attributes: event.attributes ? decodeAttributes(event.attributes) : [], + }; +} + +function decodeEvents(events: readonly RpcEvent[]): readonly responses.Event[] { + return assertArray(events).map(decodeEvent); +} + +interface RpcTxData { + readonly codespace?: string; + readonly code?: number; + readonly log?: string; + /** base64 encoded */ + readonly data?: string; + readonly events?: readonly RpcEvent[]; + readonly gas_wanted?: string; + readonly gas_used?: string; +} + +function decodeTxData(data: RpcTxData): responses.TxData { + return { + code: apiToSmallInt(assertNumber(data.code ?? 0)), + codespace: data.codespace, + log: data.log, + data: may(fromBase64, data.data), + events: data.events ? decodeEvents(data.events) : [], + gasWanted: apiToSmallInt(data.gas_wanted ?? "0"), + gasUsed: apiToSmallInt(data.gas_used ?? "0"), + }; +} + +type RpcPubkey = + | { + readonly type: string; + /** base64 encoded */ + readonly value: string; + } + | { + // See: https://github.com/cosmos/cosmjs/issues/1142 + readonly Sum: { + readonly type: string; + readonly value: { + /** base64 encoded */ + [algorithm: string]: string; + }; + }; + }; + +function decodePubkey(data: RpcPubkey): ValidatorPubkey { + if ("Sum" in data) { + // we don't need to check type because we're checking algorithm + const [[algorithm, value]] = Object.entries(data.Sum.value); + assert(algorithm === "ed25519" || algorithm === "secp256k1", `unknown pubkey type: ${algorithm}`); + return { + algorithm, + data: fromBase64(assertNotEmpty(value)), + }; + } else { + switch (data.type) { + // go-amino special code + case "tendermint/PubKeyEd25519": + return { + algorithm: "ed25519", + data: fromBase64(assertNotEmpty(data.value)), + }; + case "tendermint/PubKeySecp256k1": + return { + algorithm: "secp256k1", + data: fromBase64(assertNotEmpty(data.value)), + }; + default: + throw new Error(`unknown pubkey type: ${data.type}`); + } + } +} + +interface RpcBlockParams { + readonly max_bytes: string; + readonly max_gas: string; +} + +/** + * Note: we do not parse block.time_iota_ms for now because of this CHANGELOG entry + * + * > Add time_iota_ms to block's consensus parameters (not exposed to the application) + * https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0310 + */ +function decodeBlockParams(data: RpcBlockParams): responses.BlockParams { + return { + maxBytes: apiToSmallInt(assertNotEmpty(data.max_bytes)), + maxGas: apiToSmallInt(assertNotEmpty(data.max_gas)), + }; +} + +interface RpcEvidenceParams { + readonly max_age_num_blocks: string; + readonly max_age_duration: string; +} + +function decodeEvidenceParams(data: RpcEvidenceParams): responses.EvidenceParams { + return { + maxAgeNumBlocks: apiToSmallInt(assertNotEmpty(data.max_age_num_blocks)), + maxAgeDuration: apiToSmallInt(assertNotEmpty(data.max_age_duration)), + }; +} + +/** + * Example data: + * { + * "block": { + * "max_bytes": "22020096", + * "max_gas": "-1", + * "time_iota_ms": "1000" + * }, + * "evidence": { + * "max_age_num_blocks": "100000", + * "max_age_duration": "172800000000000" + * }, + * "validator": { + * "pub_key_types": [ + * "ed25519" + * ] + * } + * } + */ +interface RpcConsensusParams { + readonly block: RpcBlockParams; + readonly evidence: RpcEvidenceParams; +} + +function decodeConsensusParams(data: RpcConsensusParams): responses.ConsensusParams { + return { + block: decodeBlockParams(assertObject(data.block)), + evidence: decodeEvidenceParams(assertObject(data.evidence)), + }; +} + +// for block results +interface RpcValidatorUpdate { + readonly pub_key: RpcPubkey; + // When omitted, this means zero (see https://github.com/cosmos/cosmjs/issues/1177#issuecomment-1160115080) + readonly power?: string; +} + +export function decodeValidatorUpdate(data: RpcValidatorUpdate): responses.ValidatorUpdate { + return { + pubkey: decodePubkey(assertObject(data.pub_key)), + votingPower: apiToBigInt(data.power ?? "0"), + }; +} + +interface RpcBlockResultsResponse { + readonly height: string; + readonly txs_results: readonly RpcTxData[] | null; + readonly begin_block_events: readonly RpcEvent[] | null; + readonly end_block_events: readonly RpcEvent[] | null; + readonly validator_updates: readonly RpcValidatorUpdate[] | null; + readonly consensus_param_updates: RpcConsensusParams | null; +} + +function decodeBlockResults(data: RpcBlockResultsResponse): responses.BlockResultsResponse { + return { + height: apiToSmallInt(assertNotEmpty(data.height)), + results: (data.txs_results || []).map(decodeTxData), + validatorUpdates: (data.validator_updates || []).map(decodeValidatorUpdate), + consensusUpdates: may(decodeConsensusParams, data.consensus_param_updates), + beginBlockEvents: decodeEvents(data.begin_block_events || []), + endBlockEvents: decodeEvents(data.end_block_events || []), + }; +} + +interface RpcBlockId { + /** hex encoded */ + readonly hash: string; + readonly parts: { + readonly total: number; + /** hex encoded */ + readonly hash: string; + }; +} + +function decodeBlockId(data: RpcBlockId): responses.BlockId { + return { + hash: fromHex(assertNotEmpty(data.hash)), + parts: { + total: assertNotEmpty(data.parts.total), + hash: fromHex(assertNotEmpty(data.parts.hash)), + }, + }; +} + +interface RpcBlockVersion { + readonly block: string; + readonly app?: string; +} + +function decodeBlockVersion(data: RpcBlockVersion): responses.Version { + return { + block: apiToSmallInt(data.block), + app: apiToSmallInt(data.app ?? 0), + }; +} + +interface RpcHeader { + readonly version: RpcBlockVersion; + readonly chain_id: string; + readonly height: string; + readonly time: string; + + readonly last_block_id: RpcBlockId; + + /** hex encoded */ + readonly last_commit_hash: string; + /** hex encoded */ + readonly data_hash: string; + + /** hex encoded */ + readonly validators_hash: string; + /** hex encoded */ + readonly next_validators_hash: string; + /** hex encoded */ + readonly consensus_hash: string; + /** hex encoded */ + readonly app_hash: string; + /** hex encoded */ + readonly last_results_hash: string; + + /** hex encoded */ + readonly evidence_hash: string; + /** hex encoded */ + readonly proposer_address: string; +} + +function decodeHeader(data: RpcHeader): responses.Header { + return { + version: decodeBlockVersion(data.version), + chainId: assertNotEmpty(data.chain_id), + height: apiToSmallInt(assertNotEmpty(data.height)), + time: fromRfc3339WithNanoseconds(assertNotEmpty(data.time)), + + // When there is no last block ID (i.e. this block's height is 1), we get an empty structure like this: + // { hash: '', parts: { total: 0, hash: '' } } + lastBlockId: data.last_block_id.hash ? decodeBlockId(data.last_block_id) : null, + + lastCommitHash: fromHex(assertSet(data.last_commit_hash)), + dataHash: fromHex(assertSet(data.data_hash)), + + validatorsHash: fromHex(assertSet(data.validators_hash)), + nextValidatorsHash: fromHex(assertSet(data.next_validators_hash)), + consensusHash: fromHex(assertSet(data.consensus_hash)), + appHash: fromHex(assertSet(data.app_hash)), + lastResultsHash: fromHex(assertSet(data.last_results_hash)), + + evidenceHash: fromHex(assertSet(data.evidence_hash)), + proposerAddress: fromHex(assertNotEmpty(data.proposer_address)), + }; +} + +interface RpcBlockMeta { + readonly block_id: RpcBlockId; + readonly block_size: string; + readonly header: RpcHeader; + readonly num_txs: string; +} + +function decodeBlockMeta(data: RpcBlockMeta): responses.BlockMeta { + return { + blockId: decodeBlockId(data.block_id), + blockSize: apiToSmallInt(assertNotEmpty(data.block_size)), + header: decodeHeader(data.header), + numTxs: apiToSmallInt(assertNotEmpty(data.num_txs)), + }; +} + +interface RpcBlockchainResponse { + readonly last_height: string; + readonly block_metas: readonly RpcBlockMeta[]; +} + +function decodeBlockchain(data: RpcBlockchainResponse): responses.BlockchainResponse { + return { + lastHeight: apiToSmallInt(assertNotEmpty(data.last_height)), + blockMetas: assertArray(data.block_metas).map(decodeBlockMeta), + }; +} + +interface RpcBroadcastTxSyncResponse extends RpcTxData { + /** hex encoded */ + readonly hash: string; +} + +function decodeBroadcastTxSync(data: RpcBroadcastTxSyncResponse): responses.BroadcastTxSyncResponse { + return { + ...decodeTxData(data), + hash: fromHex(assertNotEmpty(data.hash)), + }; +} + +interface RpcBroadcastTxCommitResponse { + readonly height: string; + /** hex encoded */ + readonly hash: string; + readonly check_tx: RpcTxData; + readonly deliver_tx?: RpcTxData; +} + +function decodeBroadcastTxCommit(data: RpcBroadcastTxCommitResponse): responses.BroadcastTxCommitResponse { + return { + height: apiToSmallInt(data.height), + hash: fromHex(assertNotEmpty(data.hash)), + checkTx: decodeTxData(assertObject(data.check_tx)), + deliverTx: may(decodeTxData, data.deliver_tx), + }; +} + +function decodeBlockIdFlag(blockIdFlag: number): BlockIdFlag { + assert(blockIdFlag in BlockIdFlag); + return blockIdFlag; +} + +type RpcSignature = { + readonly block_id_flag: number; + /** hex encoded */ + readonly validator_address: string; + readonly timestamp: string; + /** + * Base64 encoded signature. + * There are cases when this is not set, see https://github.com/cosmos/cosmjs/issues/704#issuecomment-797122415. + */ + readonly signature: string | null; +}; + +/** + * In some cases a timestamp is optional and set to the value 0 in Go. + * This can lead to strings like "0001-01-01T00:00:00Z" (see https://github.com/cosmos/cosmjs/issues/704#issuecomment-797122415). + * This decoder tries to clean up such encoding from the API and turn them + * into undefined values. + */ +function decodeOptionalTime(timestamp: string): DateWithNanoseconds | undefined { + const nonZeroTime = timestamp && !timestamp.startsWith("0001-01-01"); + return nonZeroTime ? fromRfc3339WithNanoseconds(timestamp) : undefined; +} + +function decodeCommitSignature(data: RpcSignature): CommitSignature { + return { + blockIdFlag: decodeBlockIdFlag(data.block_id_flag), + validatorAddress: data.validator_address ? fromHex(data.validator_address) : undefined, + timestamp: decodeOptionalTime(data.timestamp), + signature: data.signature ? fromBase64(data.signature) : undefined, + }; +} + +interface RpcCommit { + readonly block_id: RpcBlockId; + readonly height: string; + readonly round: string; + readonly signatures: readonly RpcSignature[]; +} + +function decodeCommit(data: RpcCommit): responses.Commit { + return { + blockId: decodeBlockId(assertObject(data.block_id)), + height: apiToSmallInt(assertNotEmpty(data.height)), + round: apiToSmallInt(data.round), + signatures: assertArray(data.signatures).map(decodeCommitSignature), + }; +} + +interface RpcCommitResponse { + readonly signed_header: { + readonly header: RpcHeader; + readonly commit: RpcCommit; + }; + readonly canonical: boolean; +} + +function decodeCommitResponse(data: RpcCommitResponse): responses.CommitResponse { + return { + canonical: assertBoolean(data.canonical), + header: decodeHeader(data.signed_header.header), + commit: decodeCommit(data.signed_header.commit), + }; +} + +interface RpcValidatorGenesis { + /** hex-encoded */ + readonly address: string; + readonly pub_key: RpcPubkey; + readonly power: string; + readonly name?: string; +} + +export function decodeValidatorGenesis(data: RpcValidatorGenesis): responses.Validator { + return { + address: fromHex(assertNotEmpty(data.address)), + pubkey: decodePubkey(assertObject(data.pub_key)), + votingPower: apiToBigInt(assertNotEmpty(data.power)), + }; +} + +interface RpcGenesisResponse { + readonly genesis_time: string; + readonly chain_id: string; + readonly consensus_params: RpcConsensusParams; + // The validators key is used to specify a set of validators for testnets or PoA blockchains. + // PoS blockchains use the app_state.genutil.gentxs field to stake and bond a number of validators in the first block. + readonly validators?: readonly RpcValidatorGenesis[]; + /** hex encoded */ + readonly app_hash: string; + readonly app_state: Record | undefined; +} + +interface GenesisResult { + readonly genesis: RpcGenesisResponse; +} + +function decodeGenesis(data: RpcGenesisResponse): responses.GenesisResponse { + return { + genesisTime: fromRfc3339WithNanoseconds(assertNotEmpty(data.genesis_time)), + chainId: assertNotEmpty(data.chain_id), + consensusParams: decodeConsensusParams(data.consensus_params), + validators: data.validators ? assertArray(data.validators).map(decodeValidatorGenesis) : [], + appHash: fromHex(assertSet(data.app_hash)), // empty string in kvstore app + appState: data.app_state, + }; +} + +// this is in status +interface RpcValidatorInfo { + /** hex encoded */ + readonly address: string; + readonly pub_key: RpcPubkey; + readonly voting_power: string; + readonly proposer_priority?: string; +} + +export function decodeValidatorInfo(data: RpcValidatorInfo): responses.Validator { + return { + pubkey: decodePubkey(assertObject(data.pub_key)), + votingPower: apiToBigInt(assertNotEmpty(data.voting_power)), + address: fromHex(assertNotEmpty(data.address)), + proposerPriority: data.proposer_priority ? apiToSmallInt(data.proposer_priority) : undefined, + }; +} + +interface RpcNodeInfo { + /** hex encoded */ + readonly id: string; + /** IP and port */ + readonly listen_addr: string; + readonly network: string; + readonly version: string; + readonly channels: string; // ??? + readonly moniker: string; + readonly protocol_version: { + readonly p2p: string; + readonly block: string; + readonly app: string; + }; + /** + * Additional information. E.g. + * { + * "tx_index": "on", + * "rpc_address":"tcp://0.0.0.0:26657" + * } + */ + readonly other: Record; +} + +function decodeNodeInfo(data: RpcNodeInfo): responses.NodeInfo { + return { + id: fromHex(assertNotEmpty(data.id)), + listenAddr: assertNotEmpty(data.listen_addr), + network: assertNotEmpty(data.network), + version: assertString(data.version), // Can be empty (https://github.com/cosmos/cosmos-sdk/issues/7963) + channels: assertNotEmpty(data.channels), + moniker: assertNotEmpty(data.moniker), + other: dictionaryToStringMap(data.other), + protocolVersion: { + app: apiToSmallInt(assertNotEmpty(data.protocol_version.app)), + block: apiToSmallInt(assertNotEmpty(data.protocol_version.block)), + p2p: apiToSmallInt(assertNotEmpty(data.protocol_version.p2p)), + }, + }; +} + +interface RpcSyncInfo { + /** hex encoded */ + readonly latest_block_hash: string; + /** hex encoded */ + readonly latest_app_hash: string; + readonly latest_block_height: string; + readonly latest_block_time: string; + readonly catching_up: boolean; +} + +function decodeSyncInfo(data: RpcSyncInfo): responses.SyncInfo { + return { + latestBlockHash: fromHex(assertNotEmpty(data.latest_block_hash)), + latestAppHash: fromHex(assertNotEmpty(data.latest_app_hash)), + latestBlockTime: fromRfc3339WithNanoseconds(assertNotEmpty(data.latest_block_time)), + latestBlockHeight: apiToSmallInt(assertNotEmpty(data.latest_block_height)), + catchingUp: assertBoolean(data.catching_up), + }; +} + +interface RpcStatusResponse { + readonly node_info: RpcNodeInfo; + readonly sync_info: RpcSyncInfo; + readonly validator_info: RpcValidatorInfo; +} + +function decodeStatus(data: RpcStatusResponse): responses.StatusResponse { + return { + nodeInfo: decodeNodeInfo(data.node_info), + syncInfo: decodeSyncInfo(data.sync_info), + validatorInfo: decodeValidatorInfo(data.validator_info), + }; +} + +/** + * Example data: + * { + * "root_hash": "10A1A17D5F818099B5CAB5B91733A3CC27C0DB6CE2D571AC27FB970C314308BB", + * "data": "ZVlERVhDV2lVNEUwPXhTUjc4Tmp2QkNVSg==", + * "proof": { + * "total": "1", + * "index": "0", + * "leaf_hash": "EKGhfV+BgJm1yrW5FzOjzCfA22zi1XGsJ/uXDDFDCLs=", + * "aunts": [] + * } + * } + */ +interface RpcTxProof { + /** base64 encoded */ + readonly data: string; + /** hex encoded */ + readonly root_hash: string; + readonly proof: { + readonly total: string; + readonly index: string; + /** base64 encoded */ + readonly leaf_hash: string; + /** base64 encoded */ + readonly aunts: readonly string[]; + }; +} + +function decodeTxProof(data: RpcTxProof): responses.TxProof { + return { + data: fromBase64(assertNotEmpty(data.data)), + rootHash: fromHex(assertNotEmpty(data.root_hash)), + proof: { + total: apiToSmallInt(assertNotEmpty(data.proof.total)), + index: apiToSmallInt(assertNotEmpty(data.proof.index)), + leafHash: fromBase64(assertNotEmpty(data.proof.leaf_hash)), + aunts: assertArray(data.proof.aunts).map(fromBase64), + }, + }; +} + +interface RpcTxResponse { + /** Raw tx bytes, base64 encoded */ + readonly tx: string; + readonly tx_result: RpcTxData; + readonly height: string; + readonly index: number; + /** hex encoded */ + readonly hash: string; + readonly proof?: RpcTxProof; +} + +function decodeTxResponse(data: RpcTxResponse): responses.TxResponse { + return { + tx: fromBase64(assertNotEmpty(data.tx)), + result: decodeTxData(assertObject(data.tx_result)), + height: apiToSmallInt(assertNotEmpty(data.height)), + index: apiToSmallInt(assertNumber(data.index)), + hash: fromHex(assertNotEmpty(data.hash)), + proof: may(decodeTxProof, data.proof), + }; +} + +interface RpcTxSearchResponse { + readonly txs: readonly RpcTxResponse[]; + readonly total_count: string; +} + +function decodeTxSearch(data: RpcTxSearchResponse): responses.TxSearchResponse { + return { + totalCount: apiToSmallInt(assertNotEmpty(data.total_count)), + txs: assertArray(data.txs).map(decodeTxResponse), + }; +} + +interface RpcTxEvent { + /** Raw tx bytes, base64 encoded */ + readonly tx: string; + readonly result: RpcTxData; + readonly height: string; +} + +function decodeTxEvent(data: RpcTxEvent): responses.TxEvent { + const tx = fromBase64(assertNotEmpty(data.tx)); + return { + tx: tx, + hash: hashTx(tx), + result: decodeTxData(data.result), + height: apiToSmallInt(assertNotEmpty(data.height)), + }; +} + +interface RpcValidatorsResponse { + readonly block_height: string; + readonly validators: readonly RpcValidatorInfo[]; + readonly count: string; + readonly total: string; +} + +function decodeValidators(data: RpcValidatorsResponse): responses.ValidatorsResponse { + return { + blockHeight: apiToSmallInt(assertNotEmpty(data.block_height)), + validators: assertArray(data.validators).map(decodeValidatorInfo), + count: apiToSmallInt(assertNotEmpty(data.count)), + total: apiToSmallInt(assertNotEmpty(data.total)), + }; +} + +// We lost track on how the evidence structure actually looks like. +// This is any now and passed to the caller untouched. +type RpcEvidence = any; + +interface RpcBlock { + readonly header: RpcHeader; + readonly last_commit: RpcCommit; + readonly data: { + /** Raw tx bytes, base64 encoded */ + readonly txs?: readonly string[]; + }; + // It's currently unclear why the deep nesting is requied. + // See https://github.com/tendermint/tendermint/issues/7697. + readonly evidence?: { + readonly evidence?: readonly RpcEvidence[]; + }; +} + +function decodeBlock(data: RpcBlock): responses.Block { + return { + header: decodeHeader(assertObject(data.header)), + // For the block at height 1, last commit is not set. This is represented in an empty object like this: + // { height: '0', round: 0, block_id: { hash: '', parts: [Object] }, signatures: [] } + lastCommit: data.last_commit.block_id.hash ? decodeCommit(assertObject(data.last_commit)) : null, + txs: data.data.txs ? assertArray(data.data.txs).map(fromBase64) : [], + // Lift up .evidence.evidence to just .evidence + // See https://github.com/tendermint/tendermint/issues/7697 + evidence: data.evidence?.evidence ?? [], + }; +} + +interface RpcBlockResponse { + readonly block_id: RpcBlockId; + readonly block: RpcBlock; +} + +function decodeBlockResponse(data: RpcBlockResponse): responses.BlockResponse { + return { + blockId: decodeBlockId(data.block_id), + block: decodeBlock(data.block), + }; +} + +interface RpcBlockSearchResponse { + readonly blocks: readonly RpcBlockResponse[]; + readonly total_count: string; +} + +function decodeBlockSearch(data: RpcBlockSearchResponse): responses.BlockSearchResponse { + return { + totalCount: apiToSmallInt(assertNotEmpty(data.total_count)), + blocks: assertArray(data.blocks).map(decodeBlockResponse), + }; +} + +interface RpcNumUnconfirmedTxsResponse { + readonly total: string; + readonly total_bytes: string; +} + +function decodeNumUnconfirmedTxs(data: RpcNumUnconfirmedTxsResponse): responses.NumUnconfirmedTxsResponse { + return { + total: apiToSmallInt(assertNotEmpty(data.total)), + totalBytes: apiToSmallInt(assertNotEmpty(data.total_bytes)), + }; +} + +export class Responses { + public static decodeAbciInfo(response: JsonRpcSuccessResponse): responses.AbciInfoResponse { + return decodeAbciInfo(assertObject((response.result as AbciInfoResult).response)); + } + + public static decodeAbciQuery(response: JsonRpcSuccessResponse): responses.AbciQueryResponse { + return decodeAbciQuery(assertObject((response.result as AbciQueryResult).response)); + } + + public static decodeBlock(response: JsonRpcSuccessResponse): responses.BlockResponse { + return decodeBlockResponse(response.result as RpcBlockResponse); + } + + public static decodeBlockResults(response: JsonRpcSuccessResponse): responses.BlockResultsResponse { + return decodeBlockResults(response.result as RpcBlockResultsResponse); + } + + public static decodeBlockSearch(response: JsonRpcSuccessResponse): responses.BlockSearchResponse { + return decodeBlockSearch(response.result as RpcBlockSearchResponse); + } + + public static decodeBlockchain(response: JsonRpcSuccessResponse): responses.BlockchainResponse { + return decodeBlockchain(response.result as RpcBlockchainResponse); + } + + public static decodeBroadcastTxSync(response: JsonRpcSuccessResponse): responses.BroadcastTxSyncResponse { + return decodeBroadcastTxSync(response.result as RpcBroadcastTxSyncResponse); + } + + public static decodeBroadcastTxAsync(response: JsonRpcSuccessResponse): responses.BroadcastTxAsyncResponse { + return Responses.decodeBroadcastTxSync(response); + } + + public static decodeBroadcastTxCommit( + response: JsonRpcSuccessResponse, + ): responses.BroadcastTxCommitResponse { + return decodeBroadcastTxCommit(response.result as RpcBroadcastTxCommitResponse); + } + + public static decodeCommit(response: JsonRpcSuccessResponse): responses.CommitResponse { + return decodeCommitResponse(response.result as RpcCommitResponse); + } + + public static decodeGenesis(response: JsonRpcSuccessResponse): responses.GenesisResponse { + return decodeGenesis(assertObject((response.result as GenesisResult).genesis)); + } + + public static decodeHealth(): responses.HealthResponse { + return null; + } + + public static decodeNumUnconfirmedTxs( + response: JsonRpcSuccessResponse, + ): responses.NumUnconfirmedTxsResponse { + return decodeNumUnconfirmedTxs(response.result as RpcNumUnconfirmedTxsResponse); + } + + public static decodeStatus(response: JsonRpcSuccessResponse): responses.StatusResponse { + return decodeStatus(response.result as RpcStatusResponse); + } + + public static decodeNewBlockEvent(event: SubscriptionEvent): responses.NewBlockEvent { + return decodeBlock(event.data.value.block as RpcBlock); + } + + public static decodeNewBlockHeaderEvent(event: SubscriptionEvent): responses.NewBlockHeaderEvent { + return decodeHeader(event.data.value.header as RpcHeader); + } + + public static decodeTxEvent(event: SubscriptionEvent): responses.TxEvent { + return decodeTxEvent(event.data.value.TxResult as RpcTxEvent); + } + + public static decodeTx(response: JsonRpcSuccessResponse): responses.TxResponse { + return decodeTxResponse(response.result as RpcTxResponse); + } + + public static decodeTxSearch(response: JsonRpcSuccessResponse): responses.TxSearchResponse { + return decodeTxSearch(response.result as RpcTxSearchResponse); + } + + public static decodeValidators(response: JsonRpcSuccessResponse): responses.ValidatorsResponse { + return decodeValidators(response.result as RpcValidatorsResponse); + } +} diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts new file mode 100644 index 0000000000..1558df6d8e --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts @@ -0,0 +1,62 @@ +import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; + +import { SubscriptionEvent } from "../../rpcclients"; +import * as requests from "../requests"; +import * as responses from "../responses"; + +export interface Adaptor { + readonly params: Params; + readonly responses: Responses; + readonly hashTx: (tx: Uint8Array) => Uint8Array; + readonly hashBlock: (header: responses.Header) => Uint8Array; +} + +// Encoder is a generic that matches all methods of Params +export type Encoder = (req: T) => JsonRpcRequest; + +// Decoder is a generic that matches all methods of Responses +export type Decoder = (res: JsonRpcSuccessResponse) => T; + +export interface Params { + readonly encodeAbciInfo: (req: requests.AbciInfoRequest) => JsonRpcRequest; + readonly encodeAbciQuery: (req: requests.AbciQueryRequest) => JsonRpcRequest; + readonly encodeBlock: (req: requests.BlockRequest) => JsonRpcRequest; + readonly encodeBlockchain: (req: requests.BlockchainRequest) => JsonRpcRequest; + readonly encodeBlockResults: (req: requests.BlockResultsRequest) => JsonRpcRequest; + readonly encodeBlockSearch: (req: requests.BlockSearchRequest) => JsonRpcRequest; + readonly encodeBroadcastTx: (req: requests.BroadcastTxRequest) => JsonRpcRequest; + readonly encodeCommit: (req: requests.CommitRequest) => JsonRpcRequest; + readonly encodeGenesis: (req: requests.GenesisRequest) => JsonRpcRequest; + readonly encodeHealth: (req: requests.HealthRequest) => JsonRpcRequest; + readonly encodeNumUnconfirmedTxs: (req: requests.NumUnconfirmedTxsRequest) => JsonRpcRequest; + readonly encodeStatus: (req: requests.StatusRequest) => JsonRpcRequest; + readonly encodeSubscribe: (req: requests.SubscribeRequest) => JsonRpcRequest; + readonly encodeTx: (req: requests.TxRequest) => JsonRpcRequest; + readonly encodeTxSearch: (req: requests.TxSearchRequest) => JsonRpcRequest; + readonly encodeValidators: (req: requests.ValidatorsRequest) => JsonRpcRequest; +} + +export interface Responses { + readonly decodeAbciInfo: (response: JsonRpcSuccessResponse) => responses.AbciInfoResponse; + readonly decodeAbciQuery: (response: JsonRpcSuccessResponse) => responses.AbciQueryResponse; + readonly decodeBlock: (response: JsonRpcSuccessResponse) => responses.BlockResponse; + readonly decodeBlockResults: (response: JsonRpcSuccessResponse) => responses.BlockResultsResponse; + readonly decodeBlockSearch: (response: JsonRpcSuccessResponse) => responses.BlockSearchResponse; + readonly decodeBlockchain: (response: JsonRpcSuccessResponse) => responses.BlockchainResponse; + readonly decodeBroadcastTxSync: (response: JsonRpcSuccessResponse) => responses.BroadcastTxSyncResponse; + readonly decodeBroadcastTxAsync: (response: JsonRpcSuccessResponse) => responses.BroadcastTxAsyncResponse; + readonly decodeBroadcastTxCommit: (response: JsonRpcSuccessResponse) => responses.BroadcastTxCommitResponse; + readonly decodeCommit: (response: JsonRpcSuccessResponse) => responses.CommitResponse; + readonly decodeGenesis: (response: JsonRpcSuccessResponse) => responses.GenesisResponse; + readonly decodeHealth: (response: JsonRpcSuccessResponse) => responses.HealthResponse; + readonly decodeNumUnconfirmedTxs: (response: JsonRpcSuccessResponse) => responses.NumUnconfirmedTxsResponse; + readonly decodeStatus: (response: JsonRpcSuccessResponse) => responses.StatusResponse; + readonly decodeTx: (response: JsonRpcSuccessResponse) => responses.TxResponse; + readonly decodeTxSearch: (response: JsonRpcSuccessResponse) => responses.TxSearchResponse; + readonly decodeValidators: (response: JsonRpcSuccessResponse) => responses.ValidatorsResponse; + + // events + readonly decodeNewBlockEvent: (response: SubscriptionEvent) => responses.NewBlockEvent; + readonly decodeNewBlockHeaderEvent: (response: SubscriptionEvent) => responses.NewBlockHeaderEvent; + readonly decodeTxEvent: (response: SubscriptionEvent) => responses.TxEvent; +} diff --git a/packages/tendermint-rpc/src/tendermint37/encodings.spec.ts b/packages/tendermint-rpc/src/tendermint37/encodings.spec.ts new file mode 100644 index 0000000000..b4921d3b73 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/encodings.spec.ts @@ -0,0 +1,97 @@ +import { ReadonlyDate } from "readonly-date"; + +import { + encodeBlockId, + encodeBytes, + encodeString, + encodeTime, + encodeUvarint, + encodeVersion, +} from "./encodings"; + +describe("encodings", () => { + describe("encodeString", () => { + it("works", () => { + expect(encodeString("")).toEqual(Uint8Array.from([0])); + const str = "hello iov"; + expect(encodeString(str)).toEqual( + Uint8Array.from([str.length, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x69, 0x6f, 0x76]), + ); + }); + }); + + describe("encodeUvarint", () => { + it("works", () => { + expect(encodeUvarint(0)).toEqual(Uint8Array.from([0])); + expect(encodeUvarint(1)).toEqual(Uint8Array.from([1])); + expect(encodeUvarint(127)).toEqual(Uint8Array.from([127])); + expect(encodeUvarint(128)).toEqual(Uint8Array.from([128, 1])); + expect(encodeUvarint(255)).toEqual(Uint8Array.from([255, 1])); + expect(encodeUvarint(256)).toEqual(Uint8Array.from([128, 2])); + }); + }); + + describe("encodeTime", () => { + it("works", () => { + const readonlyDateWithNanoseconds = new ReadonlyDate(1464109200); + (readonlyDateWithNanoseconds as any).nanoseconds = 666666; + expect(encodeTime(readonlyDateWithNanoseconds)).toEqual( + Uint8Array.from([0x08, 173, 174, 89, 0x10, 170, 220, 215, 95]), + ); + }); + }); + + describe("encodeBytes", () => { + it("works", () => { + expect(encodeBytes(Uint8Array.from([]))).toEqual(Uint8Array.from([])); + const uint8Array = Uint8Array.from([1, 2, 3, 4, 5, 6, 7]); + expect(encodeBytes(uint8Array)).toEqual(Uint8Array.from([uint8Array.length, 1, 2, 3, 4, 5, 6, 7])); + }); + }); + + describe("encodeVersion", () => { + it("works", () => { + const version = { + block: 666666, + app: 200, + }; + expect(encodeVersion(version)).toEqual(Uint8Array.from([0x08, 170, 216, 40, 0x10, 200, 1])); + }); + }); + + describe("encodeBlockId", () => { + it("works", () => { + const blockId = { + hash: Uint8Array.from([1, 2, 3, 4, 5, 6, 7]), + parts: { + total: 88, + hash: Uint8Array.from([8, 9, 10, 11, 12]), + }, + }; + expect(encodeBlockId(blockId)).toEqual( + Uint8Array.from([ + 0x0a, + blockId.hash.length, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 0x12, + 9, + 0x08, + 88, + 0x12, + 5, + 8, + 9, + 10, + 11, + 12, + ]), + ); + }); + }); +}); diff --git a/packages/tendermint-rpc/src/tendermint37/encodings.ts b/packages/tendermint-rpc/src/tendermint37/encodings.ts new file mode 100644 index 0000000000..037b506898 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/encodings.ts @@ -0,0 +1,198 @@ +import { toUtf8 } from "@cosmjs/encoding"; + +import { ReadonlyDateWithNanoseconds } from "../dates"; +import { BlockId, Version } from "./responses"; + +/** + * A runtime checker that ensures a given value is set (i.e. not undefined or null) + * + * This is used when you want to verify that data at runtime matches the expected type. + */ +export function assertSet(value: T): T { + if ((value as unknown) === undefined) { + throw new Error("Value must not be undefined"); + } + + if ((value as unknown) === null) { + throw new Error("Value must not be null"); + } + + return value; +} + +/** + * A runtime checker that ensures a given value is a boolean + * + * This is used when you want to verify that data at runtime matches the expected type. + * This implies assertSet. + */ +export function assertBoolean(value: boolean): boolean { + assertSet(value); + if (typeof (value as unknown) !== "boolean") { + throw new Error("Value must be a boolean"); + } + return value; +} + +/** + * A runtime checker that ensures a given value is a string. + * + * This is used when you want to verify that data at runtime matches the expected type. + * This implies assertSet. + */ +export function assertString(value: string): string { + assertSet(value); + if (typeof (value as unknown) !== "string") { + throw new Error("Value must be a string"); + } + return value; +} + +/** + * A runtime checker that ensures a given value is a number + * + * This is used when you want to verify that data at runtime matches the expected type. + * This implies assertSet. + */ +export function assertNumber(value: number): number { + assertSet(value); + if (typeof (value as unknown) !== "number") { + throw new Error("Value must be a number"); + } + return value; +} + +/** + * A runtime checker that ensures a given value is an array + * + * This is used when you want to verify that data at runtime matches the expected type. + * This implies assertSet. + */ +export function assertArray(value: readonly T[]): readonly T[] { + assertSet(value); + if (!Array.isArray(value as unknown)) { + throw new Error("Value must be a an array"); + } + return value; +} + +/** + * A runtime checker that ensures a given value is an object in the sense of JSON + * (an unordered collection of key–value pairs where the keys are strings) + * + * This is used when you want to verify that data at runtime matches the expected type. + * This implies assertSet. + */ +export function assertObject(value: T): T { + assertSet(value); + if (typeof (value as unknown) !== "object") { + throw new Error("Value must be an object"); + } + + // Exclude special kind of objects like Array, Date or Uint8Array + // Object.prototype.toString() returns a specified value: + // http://www.ecma-international.org/ecma-262/7.0/index.html#sec-object.prototype.tostring + if (Object.prototype.toString.call(value) !== "[object Object]") { + throw new Error("Value must be a simple object"); + } + + return value; +} + +interface Lengther { + readonly length: number; +} + +/** + * Throws an error if value matches the empty value for the + * given type (array/string of length 0, number of value 0, ...) + * + * Otherwise returns the value. + * + * This implies assertSet + */ +export function assertNotEmpty(value: T): T { + assertSet(value); + + if (typeof value === "number" && value === 0) { + throw new Error("must provide a non-zero value"); + } else if ((value as any as Lengther).length === 0) { + throw new Error("must provide a non-empty value"); + } + return value; +} + +// may will run the transform if value is defined, otherwise returns undefined +export function may(transform: (val: T) => U, value: T | null | undefined): U | undefined { + return value === undefined || value === null ? undefined : transform(value); +} + +export function dictionaryToStringMap(obj: Record): Map { + const out = new Map(); + for (const key of Object.keys(obj)) { + const value = obj[key]; + if (typeof value !== "string") { + throw new Error("Found dictionary value of type other than string"); + } + out.set(key, value); + } + return out; +} + +// Encodings needed for hashing block headers +// Several of these functions are inspired by https://github.com/nomic-io/js-tendermint/blob/tendermint-0.30/src/ + +// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L193-L195 +export function encodeString(s: string): Uint8Array { + const utf8 = toUtf8(s); + return Uint8Array.from([utf8.length, ...utf8]); +} + +// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L79-L87 +export function encodeUvarint(n: number): Uint8Array { + return n >= 0x80 + ? // eslint-disable-next-line no-bitwise + Uint8Array.from([(n & 0xff) | 0x80, ...encodeUvarint(n >> 7)]) + : // eslint-disable-next-line no-bitwise + Uint8Array.from([n & 0xff]); +} + +// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L134-L178 +export function encodeTime(time: ReadonlyDateWithNanoseconds): Uint8Array { + const milliseconds = time.getTime(); + const seconds = Math.floor(milliseconds / 1000); + const secondsArray = seconds ? [0x08, ...encodeUvarint(seconds)] : new Uint8Array(); + const nanoseconds = (time.nanoseconds || 0) + (milliseconds % 1000) * 1e6; + const nanosecondsArray = nanoseconds ? [0x10, ...encodeUvarint(nanoseconds)] : new Uint8Array(); + return Uint8Array.from([...secondsArray, ...nanosecondsArray]); +} + +// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L180-L187 +export function encodeBytes(bytes: Uint8Array): Uint8Array { + // Since we're only dealing with short byte arrays we don't need a full VarBuffer implementation yet + if (bytes.length >= 0x80) throw new Error("Not implemented for byte arrays of length 128 or more"); + return bytes.length ? Uint8Array.from([bytes.length, ...bytes]) : new Uint8Array(); +} + +export function encodeVersion(version: Version): Uint8Array { + const blockArray = version.block + ? Uint8Array.from([0x08, ...encodeUvarint(version.block)]) + : new Uint8Array(); + const appArray = version.app ? Uint8Array.from([0x10, ...encodeUvarint(version.app)]) : new Uint8Array(); + return Uint8Array.from([...blockArray, ...appArray]); +} + +export function encodeBlockId(blockId: BlockId): Uint8Array { + return Uint8Array.from([ + 0x0a, + blockId.hash.length, + ...blockId.hash, + 0x12, + blockId.parts.hash.length + 4, + 0x08, + blockId.parts.total, + 0x12, + blockId.parts.hash.length, + ...blockId.parts.hash, + ]); +} diff --git a/packages/tendermint-rpc/src/tendermint37/hasher.spec.ts b/packages/tendermint-rpc/src/tendermint37/hasher.spec.ts new file mode 100644 index 0000000000..c66448757c --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/hasher.spec.ts @@ -0,0 +1,91 @@ +import { fromBase64, fromHex } from "@cosmjs/encoding"; +import { ReadonlyDate } from "readonly-date"; + +import { ReadonlyDateWithNanoseconds } from "../dates"; +import { hashBlock, hashTx } from "./hasher"; + +describe("Hasher", () => { + it("creates transaction hash equal to local test", () => { + // This was taken from a result from /tx_search of some random test transaction + // curl "http://localhost:11127/tx_search?query=\"tx.hash='5CB2CF94A1097A4BC19258BC2353C3E76102B6D528458BE45C855DC5563C1DB2'\"" + const txId = fromHex("5CB2CF94A1097A4BC19258BC2353C3E76102B6D528458BE45C855DC5563C1DB2"); + const txData = fromBase64("YUpxZDY2NURaUDMxPWd2TzBPdnNrVWFWYg=="); + expect(hashTx(txData)).toEqual(txId); + }); + + it("creates block hash equal to local test for empty block", () => { + // This was taken from a result from /block of some random empty block + // curl "http://localhost:11133/block" + const blockId = fromHex("153C484DCBC33633F0616BC019388C93DEA94F7880627976F2BFE83749E062F7"); + const time = new ReadonlyDate("2020-06-23T13:54:15.4638668Z"); + (time as any).nanoseconds = 866800; + const blockData = { + version: { + block: 10, + app: 1, + }, + chainId: "test-chain-2A5rwi", + height: 7795, + time: time as ReadonlyDateWithNanoseconds, + + lastBlockId: { + hash: fromHex("1EC48444E64E7B96585BA518613612E52B976E3DA2F2222B9CD4D1602656C96F"), + parts: { + total: 1, + hash: fromHex("D4E6F1B0EE08D0438C9BB8455D7D3F2FC1883C32D66F7C69C4A0F093B073F6D2"), + }, + }, + + lastCommitHash: fromHex("BA6A5EEA6687ACA8EE4FFE4F5D40EA073CB7397A5336309C3EC824805AF9723E"), + dataHash: fromHex(""), + + validatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"), + nextValidatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"), + consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"), + appHash: fromHex("8801000000000000"), + lastResultsHash: fromHex(""), + + evidenceHash: fromHex(""), + proposerAddress: fromHex("614F305502F65C01114F9B8711D9A0AB0AC369F4"), + }; + expect(hashBlock(blockData)).toEqual(blockId); + }); + + it("creates block hash equal to local test for block with a transaction", () => { + // This was taken from a result from /block of some random block with a transaction + // curl "http://localhost:11133/block?height=13575" + const blockId = fromHex("FF2995AF1F38B9A584077E53B5E144778718FB86539A51886A2C55F730403373"); + const time = new ReadonlyDate("2020-06-23T15:34:12.3232688Z"); + (time as any).nanoseconds = 268800; + const blockData = { + version: { + block: 10, + app: 1, + }, + chainId: "test-chain-2A5rwi", + height: 13575, + time: time as ReadonlyDateWithNanoseconds, + + lastBlockId: { + hash: fromHex("046D5441FC4D008FCDBF9F3DD5DC25CF00883763E44CF4FAF3923FB5FEA42D8F"), + parts: { + total: 1, + hash: fromHex("02E4715343625093C717638EAC67FB3A4B24CCC8DA610E0CB324D705E68FEF7B"), + }, + }, + + lastCommitHash: fromHex("AA2B807F3B0ACC866AB58D90C2D0FC70B6C860CFAC440590B4F590CDC178A207"), + dataHash: fromHex("56782879F526889734BA65375CD92A9152C7114B2C91B2D2AD8464FF69E884AA"), + + validatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"), + nextValidatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"), + consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"), + appHash: fromHex("CC02000000000000"), + lastResultsHash: fromHex("6E340B9CFFB37A989CA544E6BB780A2C78901D3FB33738768511A30617AFA01D"), + + evidenceHash: fromHex(""), + proposerAddress: fromHex("614F305502F65C01114F9B8711D9A0AB0AC369F4"), + }; + expect(hashBlock(blockData)).toEqual(blockId); + }); +}); diff --git a/packages/tendermint-rpc/src/tendermint37/hasher.ts b/packages/tendermint-rpc/src/tendermint37/hasher.ts new file mode 100644 index 0000000000..a02d2f814f --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/hasher.ts @@ -0,0 +1,80 @@ +import { Sha256, sha256 } from "@cosmjs/crypto"; + +import { + encodeBlockId, + encodeBytes, + encodeString, + encodeTime, + encodeUvarint, + encodeVersion, +} from "./encodings"; +import { Header } from "./responses"; + +// hash is sha256 +// https://github.com/tendermint/tendermint/blob/master/UPGRADING.md#v0260 +export function hashTx(tx: Uint8Array): Uint8Array { + return sha256(tx); +} + +function getSplitPoint(n: number): number { + if (n < 1) throw new Error("Cannot split an empty tree"); + const largestPowerOf2 = 2 ** Math.floor(Math.log2(n)); + return largestPowerOf2 < n ? largestPowerOf2 : largestPowerOf2 / 2; +} + +function hashLeaf(leaf: Uint8Array): Uint8Array { + const hash = new Sha256(Uint8Array.from([0])); + hash.update(leaf); + return hash.digest(); +} + +function hashInner(left: Uint8Array, right: Uint8Array): Uint8Array { + const hash = new Sha256(Uint8Array.from([1])); + hash.update(left); + hash.update(right); + return hash.digest(); +} + +// See https://github.com/tendermint/tendermint/blob/v0.31.8/docs/spec/blockchain/encoding.md#merkleroot +// Note: the hashes input may not actually be hashes, especially before a recursive call +function hashTree(hashes: readonly Uint8Array[]): Uint8Array { + switch (hashes.length) { + case 0: + throw new Error("Cannot hash empty tree"); + case 1: + return hashLeaf(hashes[0]); + default: { + const slicePoint = getSplitPoint(hashes.length); + const left = hashTree(hashes.slice(0, slicePoint)); + const right = hashTree(hashes.slice(slicePoint)); + return hashInner(left, right); + } + } +} + +export function hashBlock(header: Header): Uint8Array { + if (!header.lastBlockId) { + throw new Error( + "Hashing a block header with no last block ID (i.e. header at height 1) is not supported. If you need this, contributions are welcome. Please add documentation and test vectors for this case.", + ); + } + + const encodedFields: readonly Uint8Array[] = [ + encodeVersion(header.version), + encodeString(header.chainId), + encodeUvarint(header.height), + encodeTime(header.time), + encodeBlockId(header.lastBlockId), + + encodeBytes(header.lastCommitHash), + encodeBytes(header.dataHash), + encodeBytes(header.validatorsHash), + encodeBytes(header.nextValidatorsHash), + encodeBytes(header.consensusHash), + encodeBytes(header.appHash), + encodeBytes(header.lastResultsHash), + encodeBytes(header.evidenceHash), + encodeBytes(header.proposerAddress), + ]; + return hashTree(encodedFields); +} diff --git a/packages/tendermint-rpc/src/tendermint37/index.ts b/packages/tendermint-rpc/src/tendermint37/index.ts new file mode 100644 index 0000000000..87931eb179 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/index.ts @@ -0,0 +1,79 @@ +// Note: all exports in this module are publicly available via +// `import { tendermint37 } from "@cosmjs/tendermint-rpc"` + +export { + AbciInfoRequest, + AbciQueryParams, + AbciQueryRequest, + BlockchainRequest, + BlockRequest, + BlockResultsRequest, + BlockSearchParams, + BlockSearchRequest, + BroadcastTxParams, + BroadcastTxRequest, + CommitRequest, + GenesisRequest, + HealthRequest, + Method, + NumUnconfirmedTxsRequest, + QueryTag, + Request, + StatusRequest, + SubscriptionEventType, + TxParams, + TxRequest, + TxSearchParams, + TxSearchRequest, + ValidatorsParams, + ValidatorsRequest, +} from "./requests"; +export { + AbciInfoResponse, + AbciQueryResponse, + EventAttribute as Attribute, + Block, + BlockchainResponse, + BlockGossipParams, + BlockId, + BlockMeta, + BlockParams, + BlockResponse, + BlockResultsResponse, + BlockSearchResponse, + BroadcastTxAsyncResponse, + BroadcastTxCommitResponse, + broadcastTxCommitSuccess, + BroadcastTxSyncResponse, + broadcastTxSyncSuccess, + Commit, + CommitResponse, + ConsensusParams, + Event, + Evidence, + EvidenceParams, + GenesisResponse, + Header, + HealthResponse, + NewBlockEvent, + NewBlockHeaderEvent, + NodeInfo, + NumUnconfirmedTxsResponse, + ProofOp, + QueryProof, + Response, + StatusResponse, + SyncInfo, + TxData, + TxEvent, + TxProof, + TxResponse, + TxSearchResponse, + TxSizeParams, + Validator, + ValidatorsResponse, + Version, + Vote, + VoteType, +} from "./responses"; +export { Tendermint37Client } from "./tendermint37client"; diff --git a/packages/tendermint-rpc/src/tendermint37/requests.spec.ts b/packages/tendermint-rpc/src/tendermint37/requests.spec.ts new file mode 100644 index 0000000000..f2134ea8d6 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/requests.spec.ts @@ -0,0 +1,41 @@ +import { buildQuery } from "./requests"; + +describe("Requests", () => { + describe("buildQuery", () => { + it("works for no input", () => { + const query = buildQuery({}); + expect(query).toEqual(""); + }); + + it("works for one tags", () => { + const query = buildQuery({ tags: [{ key: "abc", value: "def" }] }); + expect(query).toEqual("abc='def'"); + }); + + it("works for two tags", () => { + const query = buildQuery({ + tags: [ + { key: "k", value: "9" }, + { key: "L", value: "7" }, + ], + }); + expect(query).toEqual("k='9' AND L='7'"); + }); + + it("works for raw input", () => { + const query = buildQuery({ raw: "aabbCCDD" }); + expect(query).toEqual("aabbCCDD"); + }); + + it("works for mixed input", () => { + const query = buildQuery({ + tags: [ + { key: "k", value: "9" }, + { key: "L", value: "7" }, + ], + raw: "aabbCCDD", + }); + expect(query).toEqual("k='9' AND L='7' AND aabbCCDD"); + }); + }); +}); diff --git a/packages/tendermint-rpc/src/tendermint37/requests.ts b/packages/tendermint-rpc/src/tendermint37/requests.ts new file mode 100644 index 0000000000..e4f131c5c0 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/requests.ts @@ -0,0 +1,208 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +/** + * RPC methods as documented in https://docs.tendermint.com/master/rpc/ + * + * Enum raw value must match the spelling in the "shell" example call (snake_case) + */ +export enum Method { + AbciInfo = "abci_info", + AbciQuery = "abci_query", + Block = "block", + /** Get block headers for minHeight <= height <= maxHeight. */ + Blockchain = "blockchain", + BlockResults = "block_results", + BlockSearch = "block_search", + BroadcastTxAsync = "broadcast_tx_async", + BroadcastTxSync = "broadcast_tx_sync", + BroadcastTxCommit = "broadcast_tx_commit", + Commit = "commit", + Genesis = "genesis", + Health = "health", + NumUnconfirmedTxs = "num_unconfirmed_txs", + Status = "status", + Subscribe = "subscribe", + Tx = "tx", + TxSearch = "tx_search", + Validators = "validators", + Unsubscribe = "unsubscribe", +} + +export type Request = + | AbciInfoRequest + | AbciQueryRequest + | BlockRequest + | BlockSearchRequest + | BlockchainRequest + | BlockResultsRequest + | BroadcastTxRequest + | CommitRequest + | GenesisRequest + | HealthRequest + | NumUnconfirmedTxsRequest + | StatusRequest + | TxRequest + | TxSearchRequest + | ValidatorsRequest; + +/** + * Raw values must match the tendermint event name + * + * @see https://godoc.org/github.com/tendermint/tendermint/types#pkg-constants + */ +export enum SubscriptionEventType { + NewBlock = "NewBlock", + NewBlockHeader = "NewBlockHeader", + Tx = "Tx", +} + +export interface AbciInfoRequest { + readonly method: Method.AbciInfo; +} + +export interface AbciQueryRequest { + readonly method: Method.AbciQuery; + readonly params: AbciQueryParams; +} + +export interface AbciQueryParams { + readonly path: string; + readonly data: Uint8Array; + readonly height?: number; + /** + * A flag that defines if proofs are included in the response or not. + * + * Internally this is mapped to the old inverse name `trusted` for Tendermint < 0.26. + * Starting with Tendermint 0.26, the default value changed from true to false. + */ + readonly prove?: boolean; +} + +export interface BlockRequest { + readonly method: Method.Block; + readonly params: { + readonly height?: number; + }; +} + +export interface BlockchainRequest { + readonly method: Method.Blockchain; + readonly params: BlockchainRequestParams; +} + +export interface BlockchainRequestParams { + readonly minHeight?: number; + readonly maxHeight?: number; +} + +export interface BlockResultsRequest { + readonly method: Method.BlockResults; + readonly params: { + readonly height?: number; + }; +} + +export interface BlockSearchRequest { + readonly method: Method.BlockSearch; + readonly params: BlockSearchParams; +} + +export interface BlockSearchParams { + readonly query: string; + readonly page?: number; + readonly per_page?: number; + readonly order_by?: string; +} + +export interface BroadcastTxRequest { + readonly method: Method.BroadcastTxAsync | Method.BroadcastTxSync | Method.BroadcastTxCommit; + readonly params: BroadcastTxParams; +} + +export interface BroadcastTxParams { + readonly tx: Uint8Array; +} + +export interface CommitRequest { + readonly method: Method.Commit; + readonly params: { + readonly height?: number; + }; +} + +export interface GenesisRequest { + readonly method: Method.Genesis; +} + +export interface HealthRequest { + readonly method: Method.Health; +} + +export interface NumUnconfirmedTxsRequest { + readonly method: Method.NumUnconfirmedTxs; +} + +export interface StatusRequest { + readonly method: Method.Status; +} + +export interface SubscribeRequest { + readonly method: Method.Subscribe; + readonly query: { + readonly type: SubscriptionEventType; + readonly raw?: string; + }; +} + +export interface QueryTag { + readonly key: string; + readonly value: string; +} + +export interface TxRequest { + readonly method: Method.Tx; + readonly params: TxParams; +} + +export interface TxParams { + readonly hash: Uint8Array; + readonly prove?: boolean; +} + +// TODO: clarify this type +export interface TxSearchRequest { + readonly method: Method.TxSearch; + readonly params: TxSearchParams; +} + +export interface TxSearchParams { + readonly query: string; + readonly prove?: boolean; + readonly page?: number; + readonly per_page?: number; + readonly order_by?: string; +} + +export interface ValidatorsRequest { + readonly method: Method.Validators; + readonly params: ValidatorsParams; +} + +export interface ValidatorsParams { + readonly height?: number; + readonly page?: number; + readonly per_page?: number; +} + +export interface BuildQueryComponents { + readonly tags?: readonly QueryTag[]; + readonly raw?: string; +} + +export function buildQuery(components: BuildQueryComponents): string { + const tags = components.tags ? components.tags : []; + const tagComponents = tags.map((tag) => `${tag.key}='${tag.value}'`); + const rawComponents = components.raw ? [components.raw] : []; + + return [...tagComponents, ...rawComponents].join(" AND "); +} diff --git a/packages/tendermint-rpc/src/tendermint37/responses.ts b/packages/tendermint-rpc/src/tendermint37/responses.ts new file mode 100644 index 0000000000..66eb464c77 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/responses.ts @@ -0,0 +1,389 @@ +import { ReadonlyDate } from "readonly-date"; + +import { ReadonlyDateWithNanoseconds } from "../dates"; +import { CommitSignature, ValidatorPubkey } from "../types"; + +export type Response = + | AbciInfoResponse + | AbciQueryResponse + | BlockResponse + | BlockResultsResponse + | BlockSearchResponse + | BlockchainResponse + | BroadcastTxAsyncResponse + | BroadcastTxSyncResponse + | BroadcastTxCommitResponse + | CommitResponse + | GenesisResponse + | HealthResponse + | NumUnconfirmedTxsResponse + | StatusResponse + | TxResponse + | TxSearchResponse + | ValidatorsResponse; + +export interface AbciInfoResponse { + readonly data?: string; + readonly lastBlockHeight?: number; + readonly lastBlockAppHash?: Uint8Array; +} + +export interface ProofOp { + readonly type: string; + readonly key: Uint8Array; + readonly data: Uint8Array; +} + +export interface QueryProof { + readonly ops: readonly ProofOp[]; +} + +export interface AbciQueryResponse { + readonly key: Uint8Array; + readonly value: Uint8Array; + readonly proof?: QueryProof; + readonly height?: number; + readonly index?: number; + readonly code?: number; // non-falsy for errors + readonly codespace: string; + readonly log?: string; + readonly info: string; +} + +export interface BlockResponse { + readonly blockId: BlockId; + readonly block: Block; +} + +export interface BlockResultsResponse { + readonly height: number; + readonly results: readonly TxData[]; + readonly validatorUpdates: readonly ValidatorUpdate[]; + readonly consensusUpdates?: ConsensusParams; + readonly beginBlockEvents: readonly Event[]; + readonly endBlockEvents: readonly Event[]; +} + +export interface BlockSearchResponse { + readonly blocks: readonly BlockResponse[]; + readonly totalCount: number; +} + +export interface BlockchainResponse { + readonly lastHeight: number; + readonly blockMetas: readonly BlockMeta[]; +} + +/** + * No transaction data in here because RPC method BroadcastTxAsync "returns right away, with no response" + */ +export interface BroadcastTxAsyncResponse { + readonly hash: Uint8Array; +} + +export interface BroadcastTxSyncResponse extends TxData { + readonly hash: Uint8Array; +} + +/** + * Returns true iff transaction made it successfully into the transaction pool + */ +export function broadcastTxSyncSuccess(res: BroadcastTxSyncResponse): boolean { + // code must be 0 on success + return res.code === 0; +} + +export interface BroadcastTxCommitResponse { + readonly height: number; + readonly hash: Uint8Array; + readonly checkTx: TxData; + readonly deliverTx?: TxData; +} + +/** + * Returns true iff transaction made it successfully into a block + * (i.e. success in `check_tx` and `deliver_tx` field) + */ +export function broadcastTxCommitSuccess(response: BroadcastTxCommitResponse): boolean { + // code must be 0 on success + // deliverTx may be present but empty on failure + return response.checkTx.code === 0 && !!response.deliverTx && response.deliverTx.code === 0; +} + +export interface CommitResponse { + readonly header: Header; + readonly commit: Commit; + readonly canonical: boolean; +} + +export interface GenesisResponse { + readonly genesisTime: ReadonlyDate; + readonly chainId: string; + readonly consensusParams: ConsensusParams; + readonly validators: readonly Validator[]; + readonly appHash: Uint8Array; + readonly appState: Record | undefined; +} + +export type HealthResponse = null; + +export interface NumUnconfirmedTxsResponse { + readonly total: number; + readonly totalBytes: number; +} + +export interface StatusResponse { + readonly nodeInfo: NodeInfo; + readonly syncInfo: SyncInfo; + readonly validatorInfo: Validator; +} + +/** + * A transaction from RPC calls like search. + * + * Try to keep this compatible to TxEvent + */ +export interface TxResponse { + readonly tx: Uint8Array; + readonly hash: Uint8Array; + readonly height: number; + readonly index: number; + readonly result: TxData; + readonly proof?: TxProof; +} + +export interface TxSearchResponse { + readonly txs: readonly TxResponse[]; + readonly totalCount: number; +} + +export interface ValidatorsResponse { + readonly blockHeight: number; + readonly validators: readonly Validator[]; + readonly count: number; + readonly total: number; +} + +// Events + +export interface NewBlockEvent extends Block {} + +export interface NewBlockHeaderEvent extends Header {} + +export interface TxEvent { + readonly tx: Uint8Array; + readonly hash: Uint8Array; + readonly height: number; + readonly result: TxData; +} + +// Helper items used above + +/** + * An event attribute. + * + * In 0.35 the type of key and value was changed + * from bytes to string, such that no base64 encoding is used anymore. + */ +export interface EventAttribute { + readonly key: string; + readonly value: string; +} + +export interface Event { + readonly type: string; + readonly attributes: readonly EventAttribute[]; +} + +export interface TxData { + readonly code: number; + readonly codespace?: string; + readonly log?: string; + readonly data?: Uint8Array; + readonly events: readonly Event[]; + readonly gasWanted: number; + readonly gasUsed: number; +} + +export interface TxProof { + readonly data: Uint8Array; + readonly rootHash: Uint8Array; + readonly proof: { + readonly total: number; + readonly index: number; + readonly leafHash: Uint8Array; + readonly aunts: readonly Uint8Array[]; + }; +} + +export interface BlockMeta { + readonly blockId: BlockId; + readonly blockSize: number; + readonly header: Header; + readonly numTxs: number; +} + +export interface BlockId { + readonly hash: Uint8Array; + readonly parts: { + readonly total: number; + readonly hash: Uint8Array; + }; +} + +export interface Block { + readonly header: Header; + /** + * For the block at height 1, last commit is not set. + */ + readonly lastCommit: Commit | null; + readonly txs: readonly Uint8Array[]; + readonly evidence: readonly Evidence[]; +} + +/** + * We lost track on how the evidence structure actually looks like. + * This is any now and passed to the caller untouched. + * + * See also https://github.com/cosmos/cosmjs/issues/980. + */ +export type Evidence = any; + +export interface Commit { + readonly blockId: BlockId; + readonly height: number; + readonly round: number; + readonly signatures: readonly CommitSignature[]; +} + +/** + * raw values from https://github.com/tendermint/tendermint/blob/dfa9a9a30a666132425b29454e90a472aa579a48/types/vote.go#L44 + */ +export enum VoteType { + PreVote = 1, + PreCommit = 2, +} + +export interface Vote { + readonly type: VoteType; + readonly validatorAddress: Uint8Array; + readonly validatorIndex: number; + readonly height: number; + readonly round: number; + readonly timestamp: ReadonlyDate; + readonly blockId: BlockId; + readonly signature: Uint8Array; +} + +export interface Version { + readonly block: number; + readonly app: number; +} + +// https://github.com/tendermint/tendermint/blob/v0.31.8/docs/spec/blockchain/blockchain.md +export interface Header { + // basic block info + readonly version: Version; + readonly chainId: string; + readonly height: number; + readonly time: ReadonlyDateWithNanoseconds; + + /** + * Block ID of the previous block. This can be `null` when the currect block is height 1. + */ + readonly lastBlockId: BlockId | null; + + /** + * Hashes of block data. + * + * This is `sha256("")` for height 1 🤷‍ + */ + readonly lastCommitHash: Uint8Array; + /** + * This is `sha256("")` as long as there is no data 🤷‍ + */ + readonly dataHash: Uint8Array; + + // hashes from the app output from the prev block + readonly validatorsHash: Uint8Array; + readonly nextValidatorsHash: Uint8Array; + readonly consensusHash: Uint8Array; + /** + * This can be an empty string for height 1 and turn into "0000000000000000" later on 🤷‍ + */ + readonly appHash: Uint8Array; + /** + * This is `sha256("")` as long as there is no data 🤷‍ + */ + readonly lastResultsHash: Uint8Array; + + // consensus info + /** + * This is `sha256("")` as long as there is no data 🤷‍ + */ + readonly evidenceHash: Uint8Array; + readonly proposerAddress: Uint8Array; +} + +export interface NodeInfo { + readonly id: Uint8Array; + /** IP and port */ + readonly listenAddr: string; + readonly network: string; + /** + * The Tendermint version. Can be empty (see https://github.com/cosmos/cosmos-sdk/issues/7963). + */ + readonly version: string; + readonly channels: string; // ??? + readonly moniker: string; + readonly other: Map; + readonly protocolVersion: { + readonly p2p: number; + readonly block: number; + readonly app: number; + }; +} + +export interface SyncInfo { + readonly latestBlockHash: Uint8Array; + readonly latestAppHash: Uint8Array; + readonly latestBlockHeight: number; + readonly latestBlockTime: ReadonlyDate; + readonly catchingUp: boolean; +} + +export interface Validator { + readonly address: Uint8Array; + readonly pubkey?: ValidatorPubkey; + readonly votingPower: bigint; + readonly proposerPriority?: number; +} + +export interface ValidatorUpdate { + readonly pubkey: ValidatorPubkey; + readonly votingPower: bigint; +} + +export interface ConsensusParams { + readonly block: BlockParams; + readonly evidence: EvidenceParams; +} + +export interface BlockParams { + readonly maxBytes: number; + readonly maxGas: number; +} + +export interface TxSizeParams { + readonly maxBytes: number; + readonly maxGas: number; +} + +export interface BlockGossipParams { + readonly blockPartSizeBytes: number; +} + +export interface EvidenceParams { + readonly maxAgeNumBlocks: number; + readonly maxAgeDuration: number; +} diff --git a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts new file mode 100644 index 0000000000..d141e1faa5 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts @@ -0,0 +1,866 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { toAscii } from "@cosmjs/encoding"; +import { firstEvent, toListPromise } from "@cosmjs/stream"; +import { sleep } from "@cosmjs/utils"; +import { ReadonlyDate } from "readonly-date"; +import { Stream } from "xstream"; + +import { HttpClient, RpcClient, WebsocketClient } from "../rpcclients"; +import { + buildKvTx, + ExpectedValues, + nonNegativeIntegerMatcher, + pendingWithoutTendermint, + randomString, + tendermintEnabled, + tendermintInstances, + tendermintSearchIndexUpdated, +} from "../testutil.spec"; +import { adaptor37 } from "./adaptor"; +import { buildQuery } from "./requests"; +import * as responses from "./responses"; +import { Tendermint37Client } from "./tendermint37client"; + +function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues): void { + describe("create", () => { + it("can auto-discover Tendermint version and communicate", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + const info = await client.abciInfo(); + expect(info).toBeTruthy(); + client.disconnect(); + }); + + it("can connect to Tendermint with known version", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + expect(await client.abciInfo()).toBeTruthy(); + client.disconnect(); + }); + }); + + it("can get genesis", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + const genesis = await client.genesis(); + expect(genesis).toBeTruthy(); + client.disconnect(); + }); + + describe("broadcastTxCommit", () => { + it("can broadcast a transaction", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + const tx = buildKvTx(randomString(), randomString()); + + const response = await client.broadcastTxCommit({ tx: tx }); + expect(response.height).toBeGreaterThan(2); + expect(response.hash).toBeTruthy(); + // verify success + expect(response.checkTx.code).toBeFalsy(); + expect(response.deliverTx).toBeTruthy(); + if (response.deliverTx) { + expect(response.deliverTx.code).toBeFalsy(); + } + + client.disconnect(); + }); + }); + + describe("broadcastTxSync", () => { + it("can broadcast a transaction", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + const tx = buildKvTx(randomString(), randomString()); + + const response = await client.broadcastTxSync({ tx: tx }); + expect(response.hash.length).toEqual(32); + // verify success + expect(response.code).toBeFalsy(); + expect(response.codespace).toBeFalsy(); + + client.disconnect(); + }); + }); + + describe("broadcastTxAsync", () => { + it("can broadcast a transaction", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + const tx = buildKvTx(randomString(), randomString()); + + const response = await client.broadcastTxAsync({ tx: tx }); + expect(response.hash.length).toEqual(32); + + client.disconnect(); + }); + }); + + it("gets the same tx hash from backend as calculated locally", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + const tx = buildKvTx(randomString(), randomString()); + const calculatedTxHash = adaptor37.hashTx(tx); + + const response = await client.broadcastTxCommit({ tx: tx }); + expect(response.hash).toEqual(calculatedTxHash); + + client.disconnect(); + }); + + describe("abciQuery", () => { + it("can query the state", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const key = randomString(); + const value = randomString(); + await client.broadcastTxCommit({ tx: buildKvTx(key, value) }); + + const binKey = toAscii(key); + const binValue = toAscii(value); + const queryParams = { path: "/key", data: binKey, prove: true }; + const response = await client.abciQuery(queryParams); + expect(response.key).toEqual(binKey); + expect(response.value).toEqual(binValue); + expect(response.code).toEqual(0); + expect(response.codespace).toEqual(""); + expect(response.index).toEqual(-1); + expect(response.proof).toBeUndefined(); + expect(response.log).toEqual("exists"); + expect(response.info).toEqual(""); + expect(response.height).toMatch(nonNegativeIntegerMatcher); + + client.disconnect(); + }); + }); + + it("can get a commit", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + const response = await client.commit(4); + + expect(response).toBeTruthy(); + expect(response.commit.signatures.length).toBeGreaterThanOrEqual(1); + expect(response.commit.signatures[0].blockIdFlag).toEqual(2); + expect(response.commit.signatures[0].validatorAddress?.length).toEqual(20); + expect(response.commit.signatures[0].timestamp).toBeInstanceOf(Date); + expect(response.commit.signatures[0].signature?.length).toEqual(64); + + client.disconnect(); + }); + + it("can get validators", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + const response = await client.validators({}); + + expect(response).toBeTruthy(); + expect(response.blockHeight).toBeGreaterThanOrEqual(1); + expect(response.count).toBeGreaterThanOrEqual(1); + expect(response.total).toBeGreaterThanOrEqual(1); + expect(response.validators.length).toBeGreaterThanOrEqual(1); + expect(response.validators[0].address.length).toEqual(20); + expect(response.validators[0].pubkey).toBeDefined(); + expect(response.validators[0].votingPower).toBeGreaterThanOrEqual(0); + expect(response.validators[0].proposerPriority).toBeGreaterThanOrEqual(0); + + client.disconnect(); + }); + + it("can get all validators", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + const response = await client.validatorsAll(); + + expect(response).toBeTruthy(); + expect(response.blockHeight).toBeGreaterThanOrEqual(1); + expect(response.count).toBeGreaterThanOrEqual(1); + expect(response.total).toBeGreaterThanOrEqual(1); + expect(response.validators.length).toBeGreaterThanOrEqual(1); + expect(response.validators[0].address.length).toEqual(20); + expect(response.validators[0].pubkey).toBeDefined(); + expect(response.validators[0].votingPower).toBeGreaterThanOrEqual(0); + expect(response.validators[0].proposerPriority).toBeGreaterThanOrEqual(0); + + client.disconnect(); + }); + + it("can call a bunch of methods", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + expect(await client.block()).toBeTruthy(); + expect(await client.genesis()).toBeTruthy(); + expect(await client.health()).toBeNull(); + + client.disconnect(); + }); + + describe("status", () => { + it("works", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const status = await client.status(); + + // node info + expect(status.nodeInfo.version).toMatch(expected.version); + expect(status.nodeInfo.protocolVersion).toEqual({ + p2p: expected.p2pVersion, + block: expected.blockVersion, + app: expected.appVersion, + }); + expect(status.nodeInfo.network).toMatch(expected.chainId); + expect(status.nodeInfo.other.size).toBeGreaterThanOrEqual(2); + expect(status.nodeInfo.other.get("tx_index")).toEqual("on"); + + // sync info + expect(status.syncInfo.catchingUp).toEqual(false); + expect(status.syncInfo.latestBlockHeight).toBeGreaterThanOrEqual(1); + + // validator info + expect(status.validatorInfo.pubkey).toBeTruthy(); + expect(status.validatorInfo.votingPower).toBeGreaterThan(0); + + client.disconnect(); + }); + }); + + describe("numUnconfirmedTxs", () => { + it("works", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const response = await client.numUnconfirmedTxs(); + + expect(response.total).toBeGreaterThanOrEqual(0); + expect(response.totalBytes).toBeGreaterThanOrEqual(0); + + client.disconnect(); + }); + }); + + describe("blockResults", () => { + it("works", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const height = 3; + const results = await client.blockResults(height); + expect(results.height).toEqual(height); + expect(results.results).toEqual([]); + expect(results.beginBlockEvents).toEqual([]); + expect(results.endBlockEvents).toEqual([]); + + client.disconnect(); + }); + }); + + describe("blockSearch", () => { + beforeAll(async () => { + if (tendermintEnabled()) { + const client = await Tendermint37Client.create(rpcFactory()); + + // eslint-disable-next-line no-inner-declarations + async function sendTx(): Promise { + const tx = buildKvTx(randomString(), randomString()); + + 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 blockSearch results", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const query = buildQuery({ raw: "block.height >= 1 AND block.height <= 3" }); + + // expect one page of results + const s1 = await client.blockSearch({ query: query, page: 1, per_page: 2 }); + expect(s1.totalCount).toEqual(3); + expect(s1.blocks.length).toEqual(2); + + // second page + const s2 = await client.blockSearch({ query: query, page: 2, per_page: 2 }); + expect(s2.totalCount).toEqual(3); + expect(s2.blocks.length).toEqual(1); + + client.disconnect(); + }); + + it("can get all search results in one call", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const query = buildQuery({ raw: "block.height >= 1 AND block.height <= 3" }); + + const sall = await client.blockSearchAll({ query: query, per_page: 2 }); + expect(sall.totalCount).toEqual(3); + expect(sall.blocks.length).toEqual(3); + // make sure there are in order from lowest to highest height + const [b1, b2, b3] = sall.blocks; + expect(b2.block.header.height).toEqual(b1.block.header.height + 1); + expect(b3.block.header.height).toEqual(b2.block.header.height + 1); + + client.disconnect(); + }); + }); + + describe("blockchain", () => { + it("returns latest in descending order by default", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + // 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).toBeGreaterThanOrEqual(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 = await Tendermint37Client.create(rpcFactory()); + + const height = (await client.status()).syncInfo.latestBlockHeight; + const blockchain = await client.blockchain(undefined, height - 1); + expect(blockchain.lastHeight).toBeGreaterThanOrEqual(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("works with maxHeight in the future", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const height = (await client.status()).syncInfo.latestBlockHeight; + const blockchain = await client.blockchain(undefined, height + 20); + expect(blockchain.lastHeight).toBeGreaterThanOrEqual(height); + expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(3); + expect(blockchain.blockMetas[0].header.height).toEqual(blockchain.lastHeight); + expect(blockchain.blockMetas[1].header.height).toEqual(blockchain.lastHeight - 1); + expect(blockchain.blockMetas[2].header.height).toEqual(blockchain.lastHeight - 2); + + client.disconnect(); + }); + + it("can limit by minHeight and maxHeight", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const height = (await client.status()).syncInfo.latestBlockHeight; + const blockchain = await client.blockchain(height - 2, height - 1); + expect(blockchain.lastHeight).toBeGreaterThanOrEqual(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 = await Tendermint37Client.create(rpcFactory()); + + const height = (await client.status()).syncInfo.latestBlockHeight; + const blockchain = await client.blockchain(height - 1, height - 1); + + expect(blockchain.lastHeight).toBeGreaterThanOrEqual(height); + expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(1); + const meta = blockchain.blockMetas[0]; + + expect(meta.blockId).toEqual(jasmine.objectContaining({})); + expect(meta.blockSize).toBeInstanceOf(Number); + expect(meta.header).toEqual( + jasmine.objectContaining({ + version: { + block: expected.blockVersion, + app: expected.appVersion, + }, + chainId: jasmine.stringMatching(expected.chainId), + }), + ); + expect(meta.numTxs).toBeInstanceOf(Number); + + client.disconnect(); + }); + }); + + describe("tx", () => { + it("can query a tx properly", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + 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(); + }); + }); + + describe("txSearch", () => { + const key = randomString(); + + beforeAll(async () => { + if (tendermintEnabled()) { + const client = await Tendermint37Client.create(rpcFactory()); + + // 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("returns transactions in descending order by default", async () => { + // NOTE: The Tendermint docs states the default ordering is "desc". Until + // 0.35 it was actually "asc" but from 0.35 on it is "desc". + // Docs: https://docs.tendermint.com/master/rpc/#/Info/tx_search + // Code 0.34: https://github.com/tendermint/tendermint/blob/v0.34.10/rpc/core/tx.go#L89 + // Code 0.35: https://github.com/tendermint/tendermint/blob/v0.35.6/internal/rpc/core/tx.go#L93 + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + + const result = await client.txSearch({ query: query }); + + expect(result.totalCount).toEqual(3); + result.txs.slice(1).reduce((lastHeight, { height }) => { + expect(height).toBeLessThanOrEqual(lastHeight); + return height; + }, result.txs[0].height); + + client.disconnect(); + }); + + it("can set the order", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + + const result1 = await client.txSearch({ query: query, order_by: "desc" }); + const result2 = await client.txSearch({ query: query, order_by: "asc" }); + + expect(result1.totalCount).toEqual(result2.totalCount); + expect([...result1.txs].reverse()).toEqual(result2.txs); + + client.disconnect(); + }); + + it("can paginate over txSearch results", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + 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 = await Tendermint37Client.create(rpcFactory()); + + 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 highest to lowest height + const [tx1, tx2, tx3] = sall.txs; + expect(tx2.height).toBeLessThan(tx1.height); + expect(tx3.height).toBeLessThan(tx2.height); + + client.disconnect(); + }); + }); +} + +function websocketTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues): void { + it("can subscribe to block header events", (done) => { + pendingWithoutTendermint(); + + const testStart = ReadonlyDate.now(); + + (async () => { + const events: responses.NewBlockHeaderEvent[] = []; + const client = await Tendermint37Client.create(rpcFactory()); + const stream = client.subscribeNewBlockHeader(); + expect(stream).toBeTruthy(); + const subscription = stream.subscribe({ + next: (event) => { + expect(event.chainId).toMatch(expected.chainId); + expect(event.height).toBeGreaterThan(0); + // seems that tendermint just guarantees within the last second for timestamp + expect(event.time.getTime()).toBeGreaterThan(testStart - 1000); + // Tendermint clock is sometimes ahead of test clock. Add 10ms tolerance + expect(event.time.getTime()).toBeLessThanOrEqual(ReadonlyDate.now() + 10); + expect(event.lastBlockId).toBeTruthy(); + + // merkle roots for proofs + expect(event.appHash).toBeTruthy(); + expect(event.consensusHash).toBeTruthy(); + expect(event.dataHash).toBeTruthy(); + expect(event.evidenceHash).toBeTruthy(); + expect(event.lastCommitHash).toBeTruthy(); + expect(event.lastResultsHash).toBeTruthy(); + expect(event.validatorsHash).toBeTruthy(); + + events.push(event); + + if (events.length === 2) { + subscription.unsubscribe(); + expect(events.length).toEqual(2); + expect(events[1].chainId).toEqual(events[0].chainId); + expect(events[1].height).toEqual(events[0].height + 1); + expect(events[1].time.getTime()).toBeGreaterThan(events[0].time.getTime()); + + expect(events[1].appHash).toEqual(events[0].appHash); + expect(events[1].consensusHash).toEqual(events[0].consensusHash); + expect(events[1].dataHash).toEqual(events[0].dataHash); + expect(events[1].evidenceHash).toEqual(events[0].evidenceHash); + expect(events[1].lastCommitHash).not.toEqual(events[0].lastCommitHash); + // This test is flaky. Not sure what to test here. + // expect(events[1].lastResultsHash).not.toEqual(events[0].lastResultsHash); + expect(events[1].validatorsHash).toEqual(events[0].validatorsHash); + + client.disconnect(); + done(); + } + }, + error: done.fail, + complete: () => done.fail("Stream completed before we are done"), + }); + })().catch(done.fail); + }); + + it("can subscribe to block events", async () => { + pendingWithoutTendermint(); + + const testStart = ReadonlyDate.now(); + + const transactionData1 = buildKvTx(randomString(), randomString()); + const transactionData2 = buildKvTx(randomString(), randomString()); + + const events: responses.NewBlockEvent[] = []; + const client = await Tendermint37Client.create(rpcFactory()); + const stream = client.subscribeNewBlock(); + const subscription = stream.subscribe({ + next: (event) => { + expect(event.header.chainId).toMatch(expected.chainId); + 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); + // Tendermint clock is sometimes ahead of test clock. Add 10ms tolerance + expect(event.header.time.getTime()).toBeLessThanOrEqual(ReadonlyDate.now() + 10); + expect(event.header.lastBlockId).toBeTruthy(); + + // merkle roots for proofs + expect(event.header.appHash).toBeTruthy(); + expect(event.header.consensusHash).toBeTruthy(); + expect(event.header.dataHash).toBeTruthy(); + expect(event.header.evidenceHash).toBeTruthy(); + expect(event.header.lastCommitHash).toBeTruthy(); + expect(event.header.lastResultsHash).toBeTruthy(); + expect(event.header.validatorsHash).toBeTruthy(); + + events.push(event); + }, + error: fail, + }); + + await client.broadcastTxCommit({ tx: transactionData1 }); + await client.broadcastTxCommit({ tx: transactionData2 }); + + // wait for events to be processed + await sleep(100); + + // Stop listening for new blocks + subscription.unsubscribe(); + + // We don't know exactly in which block the transactions are added. So we look into those + // with txs. + const eventsWithTx = events.filter((e) => e.txs.length > 0); + + expect(eventsWithTx.length).toEqual(2); + // Block body + expect(eventsWithTx[0].txs.length).toEqual(1); + expect(eventsWithTx[0].txs[0]).toEqual(transactionData1); + expect(eventsWithTx[1].txs.length).toEqual(1); + expect(eventsWithTx[1].txs[0]).toEqual(transactionData2); + // Block header + expect(eventsWithTx[1].header.height).toBeGreaterThan(eventsWithTx[0].header.height); + expect(eventsWithTx[1].header.chainId).toEqual(eventsWithTx[0].header.chainId); + expect(eventsWithTx[1].header.time.getTime()).toBeGreaterThan(eventsWithTx[0].header.time.getTime()); + expect(eventsWithTx[1].header.appHash).not.toEqual(eventsWithTx[0].header.appHash); + expect(eventsWithTx[1].header.validatorsHash).toEqual(eventsWithTx[0].header.validatorsHash); + + client.disconnect(); + }); + + it("can subscribe to transaction events", async () => { + pendingWithoutTendermint(); + + const events: responses.TxEvent[] = []; + const client = await Tendermint37Client.create(rpcFactory()); + const stream = client.subscribeTx(); + const subscription = stream.subscribe({ + next: (event) => { + expect(event.height).toBeGreaterThan(0); + expect(event.result).toBeTruthy(); + expect(event.result.events.length).toBeGreaterThanOrEqual(1); + + events.push(event); + + if (events.length === 2) { + subscription.unsubscribe(); + } + }, + error: fail, + }); + + const transactionData1 = buildKvTx(randomString(), randomString()); + const transactionData2 = buildKvTx(randomString(), randomString()); + + await client.broadcastTxCommit({ tx: transactionData1 }); + await client.broadcastTxCommit({ tx: transactionData2 }); + + // wait for events to be processed + await sleep(50); + + expect(events.length).toEqual(2); + // Meta + expect(events[1].height).toBeGreaterThan(events[0].height); + expect(events[1].result.events).not.toEqual(events[0].result.events); + // Content + expect(events[0].tx).toEqual(transactionData1); + expect(events[1].tx).toEqual(transactionData2); + + client.disconnect(); + }); + + it("can subscribe to transaction events filtered by creator", async () => { + pendingWithoutTendermint(); + + const transactionData1 = buildKvTx(randomString(), randomString()); + const transactionData2 = buildKvTx(randomString(), randomString()); + + const events: responses.TxEvent[] = []; + const client = await Tendermint37Client.create(rpcFactory()); + const query = buildQuery({ tags: [{ key: "app.creator", value: expected.appCreator }] }); + const stream = client.subscribeTx(query); + expect(stream).toBeTruthy(); + const subscription = stream.subscribe({ + next: (event) => { + expect(event.height).toBeGreaterThan(0); + expect(event.result).toBeTruthy(); + expect(event.result.events.length).toBeGreaterThanOrEqual(1); + events.push(event); + + if (events.length === 2) { + subscription.unsubscribe(); + } + }, + error: fail, + }); + + await client.broadcastTxCommit({ tx: transactionData1 }); + await client.broadcastTxCommit({ tx: transactionData2 }); + + // wait for events to be processed + await sleep(50); + + expect(events.length).toEqual(2); + // Meta + expect(events[1].height).toBeGreaterThan(events[0].height); + expect(events[1].result.events).not.toEqual(events[0].result.events); + // Content + expect(events[0].tx).toEqual(transactionData1); + expect(events[1].tx).toEqual(transactionData2); + + client.disconnect(); + }); + + it("can unsubscribe and re-subscribe to the same stream", async () => { + pendingWithoutTendermint(); + + const client = await Tendermint37Client.create(rpcFactory()); + const stream = client.subscribeNewBlockHeader(); + + const event1 = await firstEvent(stream); + expect(event1.height).toBeGreaterThanOrEqual(1); + expect(event1.time.getTime()).toBeGreaterThanOrEqual(1); + + // No sleep: producer will not be stopped in the meantime + + const event2 = await firstEvent(stream); + expect(event2.height).toBeGreaterThan(event1.height); + expect(event2.time.getTime()).toBeGreaterThan(event1.time.getTime()); + + // Very short sleep: just enough to schedule asynchronous producer stopping + await sleep(5); + + const event3 = await firstEvent(stream); + expect(event3.height).toBeGreaterThan(event2.height); + expect(event3.time.getTime()).toBeGreaterThan(event2.time.getTime()); + + // Proper sleep: enough to finish unsubscribing at over the network + await sleep(100); + + const event4 = await firstEvent(stream); + expect(event4.height).toBeGreaterThan(event3.height); + expect(event4.time.getTime()).toBeGreaterThan(event3.time.getTime()); + + client.disconnect(); + }); + + it("can subscribe twice", async () => { + pendingWithoutTendermint(); + + const client = await Tendermint37Client.create(rpcFactory()); + const stream1 = client.subscribeNewBlockHeader(); + const stream2 = client.subscribeNewBlockHeader(); + + const events = await toListPromise(Stream.merge(stream1, stream2), 4); + + expect(new Set(events.map((e) => e.height)).size).toEqual(2); + + client.disconnect(); + }); +} + +describe("Tendermint37Client", () => { + const { url, expected } = tendermintInstances[37]; + + it("can connect to a given url", async () => { + pendingWithoutTendermint(); + + // default connection + { + const client = await Tendermint37Client.connect(url); + const info = await client.abciInfo(); + expect(info).toBeTruthy(); + client.disconnect(); + } + + // http connection + { + const client = await Tendermint37Client.connect("http://" + url); + const info = await client.abciInfo(); + expect(info).toBeTruthy(); + client.disconnect(); + } + + // ws connection + { + const client = await Tendermint37Client.connect("ws://" + url); + const info = await client.abciInfo(); + expect(info).toBeTruthy(); + client.disconnect(); + } + }); + + describe("With HttpClient", () => { + defaultTestSuite(() => new HttpClient(url), expected); + }); + + describe("With WebsocketClient", () => { + // don't print out WebSocket errors if marked pending + const onError = process.env.TENDERMINT_ENABLED ? console.error : () => 0; + const factory = (): WebsocketClient => new WebsocketClient(url, onError); + defaultTestSuite(factory, expected); + websocketTestSuite(factory, expected); + }); +}); diff --git a/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts new file mode 100644 index 0000000000..cdbbf164a9 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts @@ -0,0 +1,356 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Stream } from "xstream"; + +import { createJsonRpcRequest } from "../jsonrpc"; +import { + HttpClient, + HttpEndpoint, + instanceOfRpcStreamingClient, + RpcClient, + SubscriptionEvent, + WebsocketClient, +} from "../rpcclients"; +import { adaptor37, Decoder, Encoder, Params, Responses } from "./adaptor"; +import * as requests from "./requests"; +import * as responses from "./responses"; + +export class Tendermint37Client { + /** + * Creates a new Tendermint client for the given endpoint. + * + * Uses HTTP when the URL schema is http or https. Uses WebSockets otherwise. + */ + public static async connect(endpoint: string | HttpEndpoint): Promise { + if (typeof endpoint === "object") { + return Tendermint37Client.create(new HttpClient(endpoint)); + } else { + const useHttp = endpoint.startsWith("http://") || endpoint.startsWith("https://"); + const rpcClient = useHttp ? new HttpClient(endpoint) : new WebsocketClient(endpoint); + return Tendermint37Client.create(rpcClient); + } + } + + /** + * Creates a new Tendermint client given an RPC client. + */ + public static async create(rpcClient: RpcClient): Promise { + // For some very strange reason I don't understand, tests start to fail on some systems + // (our CI) when skipping the status call before doing other queries. Sleeping a little + // while did not help. Thus we query the version as a way to say "hi" to the backend, + // even in cases where we don't use the result. + const _version = await this.detectVersion(rpcClient); + return new Tendermint37Client(rpcClient); + } + + private static async detectVersion(client: RpcClient): Promise { + const req = createJsonRpcRequest(requests.Method.Status); + const response = await client.execute(req); + const result = response.result; + + if (!result || !result.node_info) { + throw new Error("Unrecognized format for status response"); + } + + const version = result.node_info.version; + if (typeof version !== "string") { + throw new Error("Unrecognized version format: must be string"); + } + return version; + } + + private readonly client: RpcClient; + private readonly p: Params; + private readonly r: Responses; + + /** + * Use `Tendermint37Client.connect` or `Tendermint37Client.create` to create an instance. + */ + private constructor(client: RpcClient) { + this.client = client; + this.p = adaptor37.params; + this.r = adaptor37.responses; + } + + public disconnect(): void { + this.client.disconnect(); + } + + public async abciInfo(): Promise { + const query: requests.AbciInfoRequest = { method: requests.Method.AbciInfo }; + return this.doCall(query, this.p.encodeAbciInfo, this.r.decodeAbciInfo); + } + + public async abciQuery(params: requests.AbciQueryParams): Promise { + const query: requests.AbciQueryRequest = { params: params, method: requests.Method.AbciQuery }; + return this.doCall(query, this.p.encodeAbciQuery, this.r.decodeAbciQuery); + } + + public async block(height?: number): Promise { + const query: requests.BlockRequest = { method: requests.Method.Block, params: { height: height } }; + return this.doCall(query, this.p.encodeBlock, this.r.decodeBlock); + } + + public async blockResults(height?: number): Promise { + const query: requests.BlockResultsRequest = { + method: requests.Method.BlockResults, + params: { height: height }, + }; + return this.doCall(query, this.p.encodeBlockResults, this.r.decodeBlockResults); + } + + /** + * Search for events that are in a block. + * + * NOTE + * This method will error on any node that is running a Tendermint version lower than 0.34.9. + * + * @see https://docs.tendermint.com/master/rpc/#/Info/block_search + */ + public async blockSearch(params: requests.BlockSearchParams): Promise { + const query: requests.BlockSearchRequest = { params: params, method: requests.Method.BlockSearch }; + const resp = await this.doCall(query, this.p.encodeBlockSearch, this.r.decodeBlockSearch); + return { + ...resp, + // make sure we sort by height, as tendermint may be sorting by string value of the height + blocks: [...resp.blocks].sort((a, b) => a.block.header.height - b.block.header.height), + }; + } + + // this should paginate through all blockSearch options to ensure it returns all results. + // starts with page 1 or whatever was provided (eg. to start on page 7) + // + // NOTE + // This method will error on any node that is running a Tendermint version lower than 0.34.9. + public async blockSearchAll(params: requests.BlockSearchParams): Promise { + let page = params.page || 1; + const blocks: responses.BlockResponse[] = []; + let done = false; + + while (!done) { + const resp = await this.blockSearch({ ...params, page: page }); + blocks.push(...resp.blocks); + if (blocks.length < resp.totalCount) { + page++; + } else { + done = true; + } + } + // make sure we sort by height, as tendermint may be sorting by string value of the height + // and the earlier items may be in a higher page than the later items + blocks.sort((a, b) => a.block.header.height - b.block.header.height); + + return { + totalCount: blocks.length, + blocks: blocks, + }; + } + + /** + * 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 = { + method: requests.Method.Blockchain, + params: { + minHeight: minHeight, + maxHeight: maxHeight, + }, + }; + return this.doCall(query, this.p.encodeBlockchain, this.r.decodeBlockchain); + } + + /** + * Broadcast transaction to mempool and wait for response + * + * @see https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_sync + */ + public async broadcastTxSync( + params: requests.BroadcastTxParams, + ): Promise { + const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxSync }; + return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxSync); + } + + /** + * Broadcast transaction to mempool and do not wait for result + * + * @see https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_async + */ + public async broadcastTxAsync( + params: requests.BroadcastTxParams, + ): Promise { + const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxAsync }; + return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxAsync); + } + + /** + * Broadcast transaction to mempool and wait for block + * + * @see https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_commit + */ + public async broadcastTxCommit( + params: requests.BroadcastTxParams, + ): Promise { + const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxCommit }; + return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxCommit); + } + + public async commit(height?: number): Promise { + const query: requests.CommitRequest = { method: requests.Method.Commit, params: { height: height } }; + return this.doCall(query, this.p.encodeCommit, this.r.decodeCommit); + } + + public async genesis(): Promise { + const query: requests.GenesisRequest = { method: requests.Method.Genesis }; + return this.doCall(query, this.p.encodeGenesis, this.r.decodeGenesis); + } + + public async health(): Promise { + const query: requests.HealthRequest = { method: requests.Method.Health }; + return this.doCall(query, this.p.encodeHealth, this.r.decodeHealth); + } + + public async numUnconfirmedTxs(): Promise { + const query: requests.NumUnconfirmedTxsRequest = { method: requests.Method.NumUnconfirmedTxs }; + return this.doCall(query, this.p.encodeNumUnconfirmedTxs, this.r.decodeNumUnconfirmedTxs); + } + + public async status(): Promise { + const query: requests.StatusRequest = { method: requests.Method.Status }; + return this.doCall(query, this.p.encodeStatus, this.r.decodeStatus); + } + + public subscribeNewBlock(): Stream { + const request: requests.SubscribeRequest = { + method: requests.Method.Subscribe, + query: { type: requests.SubscriptionEventType.NewBlock }, + }; + return this.subscribe(request, this.r.decodeNewBlockEvent); + } + + public subscribeNewBlockHeader(): Stream { + const request: requests.SubscribeRequest = { + method: requests.Method.Subscribe, + query: { type: requests.SubscriptionEventType.NewBlockHeader }, + }; + return this.subscribe(request, this.r.decodeNewBlockHeaderEvent); + } + + public subscribeTx(query?: string): Stream { + const request: requests.SubscribeRequest = { + method: requests.Method.Subscribe, + query: { + type: requests.SubscriptionEventType.Tx, + raw: query, + }, + }; + return this.subscribe(request, this.r.decodeTxEvent); + } + + /** + * Get a single transaction by hash + * + * @see https://docs.tendermint.com/master/rpc/#/Info/tx + */ + public async tx(params: requests.TxParams): Promise { + const query: requests.TxRequest = { params: params, method: requests.Method.Tx }; + return this.doCall(query, this.p.encodeTx, this.r.decodeTx); + } + + /** + * Search for transactions that are in a block + * + * @see https://docs.tendermint.com/master/rpc/#/Info/tx_search + */ + public async txSearch(params: requests.TxSearchParams): Promise { + const query: requests.TxSearchRequest = { params: params, method: requests.Method.TxSearch }; + return this.doCall(query, this.p.encodeTxSearch, this.r.decodeTxSearch); + } + + // this should paginate through all txSearch options to ensure it returns all results. + // starts with page 1 or whatever was provided (eg. to start on page 7) + public async txSearchAll(params: requests.TxSearchParams): Promise { + let page = params.page || 1; + const txs: responses.TxResponse[] = []; + let done = false; + + while (!done) { + const resp = await this.txSearch({ ...params, page: page }); + txs.push(...resp.txs); + if (txs.length < resp.totalCount) { + page++; + } else { + done = true; + } + } + + return { + totalCount: txs.length, + txs: txs, + }; + } + + public async validators(params: requests.ValidatorsParams): Promise { + const query: requests.ValidatorsRequest = { + method: requests.Method.Validators, + params: params, + }; + return this.doCall(query, this.p.encodeValidators, this.r.decodeValidators); + } + + public async validatorsAll(height?: number): Promise { + const validators: responses.Validator[] = []; + let page = 1; + let done = false; + let blockHeight = height; + + while (!done) { + const response = await this.validators({ + per_page: 50, + height: blockHeight, + page: page, + }); + validators.push(...response.validators); + blockHeight = blockHeight || response.blockHeight; + if (validators.length < response.total) { + page++; + } else { + done = true; + } + } + + return { + // NOTE: Default value is for type safety but this should always be set + blockHeight: blockHeight ?? 0, + count: validators.length, + total: validators.length, + validators: validators, + }; + } + + // doCall is a helper to handle the encode/call/decode logic + private async doCall( + request: T, + encode: Encoder, + decode: Decoder, + ): Promise { + const req = encode(request); + const result = await this.client.execute(req); + return decode(result); + } + + private subscribe(request: requests.SubscribeRequest, decode: (e: SubscriptionEvent) => T): Stream { + if (!instanceOfRpcStreamingClient(this.client)) { + throw new Error("This RPC client type cannot subscribe to events"); + } + + const req = this.p.encodeSubscribe(request); + const eventStream = this.client.listen(req); + return eventStream.map((event) => { + return decode(event); + }); + } +} From a14c9eb563107c38a94999df5262af6e320fec75 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 1 Mar 2023 17:23:38 +0100 Subject: [PATCH 04/15] Switch to cometbft/cometbft:v0.37.0-rc3 --- scripts/tendermint/all_start.sh | 24 ++++++++++++++++-------- scripts/tendermint/all_stop.sh | 7 +------ scripts/tendermint/start.sh | 10 +++++----- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/scripts/tendermint/all_start.sh b/scripts/tendermint/all_start.sh index 49f9501c87..38aeaa0c69 100755 --- a/scripts/tendermint/all_start.sh +++ b/scripts/tendermint/all_start.sh @@ -2,19 +2,27 @@ set -o errexit -o nounset -o pipefail command -v shellcheck >/dev/null && shellcheck "$0" -# Find latest patch releases at https://hub.docker.com/r/tendermint/tendermint/tags/ -declare -a TM_VERSIONS -TM_VERSIONS[34]=v0.34.19 -TM_VERSIONS[35]=v0.35.6 -TM_VERSIONS[37]=v0.37.0-rc2 +# Find latest patch releases at +# - https://hub.docker.com/r/tendermint/tendermint/tags/ +# - https://hub.docker.com/r/cometbft/cometbft/tags/ +declare -a TM_IMAGES +TM_IMAGES[34]="tendermint/tendermint:v0.34.19" +TM_IMAGES[35]="tendermint/tendermint:v0.35.6" +TM_IMAGES[37]="cometbft/cometbft:v0.37.0-rc3" + +declare -a TM_ROOTS +TM_ROOTS[34]="/tendermint" +TM_ROOTS[35]="/tendermint" +TM_ROOTS[37]="/cometbft" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -for KEY in "${!TM_VERSIONS[@]}"; do - export TENDERMINT_VERSION="${TM_VERSIONS[$KEY]}" +for KEY in "${!TM_IMAGES[@]}"; do + export TENDERMINT_IMAGE="${TM_IMAGES[$KEY]}" + export TENDERMINT_ROOT="${TM_ROOTS[$KEY]}" export TENDERMINT_PORT="111$KEY" export TENDERMINT_NAME="tendermint-$KEY" - echo "Starting $TENDERMINT_NAME ($TENDERMINT_VERSION) on port $TENDERMINT_PORT ..." + echo "Starting $TENDERMINT_NAME ($TENDERMINT_IMAGE) on port $TENDERMINT_PORT ..." "$SCRIPT_DIR/start.sh" done diff --git a/scripts/tendermint/all_stop.sh b/scripts/tendermint/all_stop.sh index f9f0e017d4..46246195ce 100755 --- a/scripts/tendermint/all_stop.sh +++ b/scripts/tendermint/all_stop.sh @@ -2,14 +2,9 @@ set -o errexit -o nounset -o pipefail command -v shellcheck >/dev/null && shellcheck "$0" -declare -a TM_VERSIONS -TM_VERSIONS[34]=v0.34.19 -TM_VERSIONS[35]=v0.35.6 -TM_VERSIONS[37]=v0.37.0-rc2 - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -for KEY in "${!TM_VERSIONS[@]}"; do +for KEY in 34 35 37; do export TENDERMINT_NAME="tendermint-$KEY" echo "Stopping $TENDERMINT_NAME ..." diff --git a/scripts/tendermint/start.sh b/scripts/tendermint/start.sh index 00ae26e2e7..e871df8ddd 100755 --- a/scripts/tendermint/start.sh +++ b/scripts/tendermint/start.sh @@ -6,7 +6,7 @@ gnused="$(command -v gsed || echo sed)" # Tendermint settings must be specified # Choose version from https://hub.docker.com/r/tendermint/tendermint/tags/ -for SETTING in "TENDERMINT_VERSION" "TENDERMINT_PORT" "TENDERMINT_NAME"; do +for SETTING in "TENDERMINT_IMAGE" "TENDERMINT_PORT" "TENDERMINT_NAME"; do if test -z "$(eval echo "\$$SETTING")"; then echo "\$$SETTING must be set when running this script" exit 1 @@ -20,8 +20,8 @@ LOGFILE="$TMP_DIR/tendermint.log" docker run --rm \ --user="$UID" \ - -v "${TMP_DIR}:/tendermint" \ - "tendermint/tendermint:${TENDERMINT_VERSION}" \ + -v "${TMP_DIR}:${TENDERMINT_ROOT}" \ + "${TENDERMINT_IMAGE}" \ init validator # make sure we allow cors origins, only possible by modifying the config file @@ -36,11 +36,11 @@ docker run --rm \ docker run --rm \ --user="$UID" \ --name "$TENDERMINT_NAME" \ - -p "${TENDERMINT_PORT}:26657" -v "${TMP_DIR}:/tendermint" \ + -p "${TENDERMINT_PORT}:26657" -v "${TMP_DIR}:${TENDERMINT_ROOT}" \ -e "TM_TX_INDEX_INDEX_ALL_KEYS=true" \ -e "PROXY_APP=kvstore" \ -e "LOG_LEVEL=state:info,rpc:info,*:error" \ - "tendermint/tendermint:${TENDERMINT_VERSION}" node \ + "${TENDERMINT_IMAGE}" node \ --rpc.laddr=tcp://0.0.0.0:26657 \ >"$LOGFILE" 2>&1 & From ff282983409601faac49e58c7ddf17a8bfc0a016 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 1 Mar 2023 17:23:58 +0100 Subject: [PATCH 05/15] Adapt testing code --- .../src/tendermint34/tendermint34client.spec.ts | 8 ++++---- .../src/tendermint37/tendermint37client.spec.ts | 10 ++++++---- packages/tendermint-rpc/src/testutil.spec.ts | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts index 1daa834f8d..8f41e8137d 100644 --- a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts @@ -501,13 +501,13 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); - const s = await client.txSearch({ query: query }); + const result = await client.txSearch({ query: query }); - expect(s.totalCount).toEqual(3); - s.txs.slice(1).reduce((lastHeight, { height }) => { + expect(result.totalCount).toEqual(3); + result.txs.slice(1).reduce((lastHeight, { height }) => { expect(height).toBeGreaterThanOrEqual(lastHeight); return height; - }, s.txs[0].height); + }, result.txs[0].height); client.disconnect(); }); diff --git a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts index d141e1faa5..c37d124773 100644 --- a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts @@ -493,12 +493,14 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) } }); - it("returns transactions in descending order by default", async () => { + it("returns transactions in ascending order by default", async () => { // NOTE: The Tendermint docs states the default ordering is "desc". Until // 0.35 it was actually "asc" but from 0.35 on it is "desc". + // Then it was changed back to "asc" in 0.37. // Docs: https://docs.tendermint.com/master/rpc/#/Info/tx_search // Code 0.34: https://github.com/tendermint/tendermint/blob/v0.34.10/rpc/core/tx.go#L89 // Code 0.35: https://github.com/tendermint/tendermint/blob/v0.35.6/internal/rpc/core/tx.go#L93 + // Code 0.37: https://github.com/cometbft/cometbft/blob/v0.37.0-rc3/rpc/core/tx.go#L87 pendingWithoutTendermint(); const client = await Tendermint37Client.create(rpcFactory()); @@ -508,7 +510,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) expect(result.totalCount).toEqual(3); result.txs.slice(1).reduce((lastHeight, { height }) => { - expect(height).toBeLessThanOrEqual(lastHeight); + expect(height).toBeGreaterThanOrEqual(lastHeight); return height; }, result.txs[0].height); @@ -560,8 +562,8 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) expect(sall.txs.length).toEqual(3); // make sure there are in order from highest to lowest height const [tx1, tx2, tx3] = sall.txs; - expect(tx2.height).toBeLessThan(tx1.height); - expect(tx3.height).toBeLessThan(tx2.height); + expect(tx2.height).toBeGreaterThan(tx1.height); + expect(tx3.height).toBeGreaterThan(tx2.height); client.disconnect(); }); diff --git a/packages/tendermint-rpc/src/testutil.spec.ts b/packages/tendermint-rpc/src/testutil.spec.ts index bcbd0dda2b..8bfd2146d8 100644 --- a/packages/tendermint-rpc/src/testutil.spec.ts +++ b/packages/tendermint-rpc/src/testutil.spec.ts @@ -68,7 +68,7 @@ export const tendermintInstances = { blockTime: 500, expected: { chainId: /^dockerchain$/, - version: /^$/, // Unfortunately we don't get info here + version: /^0\.37\.0-alpha\.3$/, appCreator: "Cosmoshi Netowoko", p2pVersion: 8, blockVersion: 11, From 1cc4fbe032a0d2792a6f7f93739e61f0ef27c03b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 1 Mar 2023 17:57:05 +0100 Subject: [PATCH 06/15] Remove Tendermint 0.35 client --- CHANGELOG.md | 4 +- packages/tendermint-rpc/src/index.ts | 4 - .../src/tendermint35/adaptor/index.ts | 13 - .../src/tendermint35/adaptor/requests.ts | 185 ---- .../tendermint35/adaptor/responses.spec.ts | 124 --- .../src/tendermint35/adaptor/responses.ts | 915 ------------------ .../src/tendermint35/adaptor/types.ts | 62 -- .../src/tendermint35/encodings.spec.ts | 97 -- .../src/tendermint35/encodings.ts | 198 ---- .../src/tendermint35/hasher.spec.ts | 91 -- .../tendermint-rpc/src/tendermint35/hasher.ts | 80 -- .../tendermint-rpc/src/tendermint35/index.ts | 79 -- .../src/tendermint35/requests.spec.ts | 41 - .../src/tendermint35/requests.ts | 208 ---- .../src/tendermint35/responses.ts | 389 -------- .../tendermint35/tendermint35client.spec.ts | 866 ----------------- .../src/tendermint35/tendermint35client.ts | 361 ------- packages/tendermint-rpc/src/testutil.spec.ts | 13 - scripts/tendermint/all_start.sh | 2 - scripts/tendermint/all_stop.sh | 2 +- 20 files changed, 4 insertions(+), 3730 deletions(-) delete mode 100644 packages/tendermint-rpc/src/tendermint35/adaptor/index.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/adaptor/requests.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/adaptor/responses.spec.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/adaptor/responses.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/adaptor/types.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/encodings.spec.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/encodings.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/hasher.spec.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/hasher.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/index.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/requests.spec.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/requests.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/responses.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/tendermint35client.spec.ts delete mode 100644 packages/tendermint-rpc/src/tendermint35/tendermint35client.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ca36df49..3596ebde46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,9 +32,11 @@ and this project adheres to - @cosmjs/stargate: Add `granteeGrants` and `granterGrants` queries to `AuthzExtension` ([#1308]). -- @cosmjs/tendermint-rpc: Add `Tendermint37Client` +- @cosmjs/tendermint-rpc: Add new `Tendermint37Client` and remove unused + `Tendermint35Client` ([#1376]). [#1308]: https://github.com/cosmos/cosmjs/pull/1308 +[#1376]: https://github.com/cosmos/cosmjs/pull/1376 ## [0.29.5] - 2022-12-07 diff --git a/packages/tendermint-rpc/src/index.ts b/packages/tendermint-rpc/src/index.ts index 7849709a66..757012a3b5 100644 --- a/packages/tendermint-rpc/src/index.ts +++ b/packages/tendermint-rpc/src/index.ts @@ -95,10 +95,6 @@ export { } from "./tendermint34"; export * as tendermint34 from "./tendermint34"; export { Tendermint34Client } from "./tendermint34"; -// Tendermint 0.35 support is not public. The implementation may break or be removed at any point in time. -// See https://github.com/cosmos/cosmjs/issues/1225 for more context. -// export * as tendermint35 from "./tendermint35"; -// export { Tendermint35Client } from "./tendermint35"; export * as tendermint37 from "./tendermint37"; export { Tendermint37Client } from "./tendermint37"; export { diff --git a/packages/tendermint-rpc/src/tendermint35/adaptor/index.ts b/packages/tendermint-rpc/src/tendermint35/adaptor/index.ts deleted file mode 100644 index 9f5061ff84..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/adaptor/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { hashBlock, hashTx } from "../hasher"; -import { Params } from "./requests"; -import { Responses } from "./responses"; -import { Adaptor } from "./types"; - -export { Decoder, Encoder, Params, Responses } from "./types"; - -export const adaptor35: Adaptor = { - params: Params, - responses: Responses, - hashTx: hashTx, - hashBlock: hashBlock, -}; diff --git a/packages/tendermint-rpc/src/tendermint35/adaptor/requests.ts b/packages/tendermint-rpc/src/tendermint35/adaptor/requests.ts deleted file mode 100644 index 52a06a2932..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/adaptor/requests.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { toBase64, toHex } from "@cosmjs/encoding"; -import { JsonRpcRequest } from "@cosmjs/json-rpc"; - -import { smallIntToApi } from "../../inthelpers"; -import { createJsonRpcRequest } from "../../jsonrpc"; -import { assertNotEmpty, may } from "../encodings"; -import * as requests from "../requests"; - -interface HeightParam { - readonly height?: number; -} -interface RpcHeightParam { - readonly height?: string; -} -function encodeHeightParam(param: HeightParam): RpcHeightParam { - return { - height: may(smallIntToApi, param.height), - }; -} - -interface RpcBlockchainRequestParams { - readonly minHeight?: string; - readonly maxHeight?: string; -} - -function encodeBlockchainRequestParams(param: requests.BlockchainRequestParams): RpcBlockchainRequestParams { - return { - minHeight: may(smallIntToApi, param.minHeight), - maxHeight: may(smallIntToApi, param.maxHeight), - }; -} - -interface RpcBlockSearchParams { - readonly query: string; - readonly page?: string; - readonly per_page?: string; - readonly order_by?: string; -} -function encodeBlockSearchParams(params: requests.BlockSearchParams): RpcBlockSearchParams { - return { - query: params.query, - page: may(smallIntToApi, params.page), - per_page: may(smallIntToApi, params.per_page), - order_by: params.order_by, - }; -} - -interface RpcAbciQueryParams { - readonly path: string; - /** hex encoded */ - readonly data: string; - readonly height?: string; - readonly prove?: boolean; -} - -function encodeAbciQueryParams(params: requests.AbciQueryParams): RpcAbciQueryParams { - return { - path: assertNotEmpty(params.path), - data: toHex(params.data), - height: may(smallIntToApi, params.height), - prove: params.prove, - }; -} - -interface RpcBroadcastTxParams { - /** base64 encoded */ - readonly tx: string; -} -function encodeBroadcastTxParams(params: requests.BroadcastTxParams): RpcBroadcastTxParams { - return { - tx: toBase64(assertNotEmpty(params.tx)), - }; -} - -interface RpcTxParams { - /** hex encoded */ - readonly hash: string; - readonly prove?: boolean; -} -function encodeTxParams(params: requests.TxParams): RpcTxParams { - return { - hash: toHex(assertNotEmpty(params.hash)), - prove: params.prove, - }; -} - -interface RpcTxSearchParams { - readonly query: string; - readonly prove?: boolean; - readonly page?: string; - readonly per_page?: string; - readonly order_by?: string; -} -function encodeTxSearchParams(params: requests.TxSearchParams): RpcTxSearchParams { - return { - query: params.query, - prove: params.prove, - page: may(smallIntToApi, params.page), - per_page: may(smallIntToApi, params.per_page), - order_by: params.order_by, - }; -} - -interface RpcValidatorsParams { - readonly height?: string; - readonly page?: string; - readonly per_page?: string; -} -function encodeValidatorsParams(params: requests.ValidatorsParams): RpcValidatorsParams { - return { - height: may(smallIntToApi, params.height), - page: may(smallIntToApi, params.page), - per_page: may(smallIntToApi, params.per_page), - }; -} - -export class Params { - public static encodeAbciInfo(req: requests.AbciInfoRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method); - } - - public static encodeAbciQuery(req: requests.AbciQueryRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeAbciQueryParams(req.params)); - } - - public static encodeBlock(req: requests.BlockRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeHeightParam(req.params)); - } - - public static encodeBlockchain(req: requests.BlockchainRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeBlockchainRequestParams(req.params)); - } - - public static encodeBlockResults(req: requests.BlockResultsRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeHeightParam(req.params)); - } - - public static encodeBlockSearch(req: requests.BlockSearchRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeBlockSearchParams(req.params)); - } - - public static encodeBroadcastTx(req: requests.BroadcastTxRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeBroadcastTxParams(req.params)); - } - - public static encodeCommit(req: requests.CommitRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeHeightParam(req.params)); - } - - public static encodeGenesis(req: requests.GenesisRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method); - } - - public static encodeHealth(req: requests.HealthRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method); - } - - public static encodeNumUnconfirmedTxs(req: requests.NumUnconfirmedTxsRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method); - } - - public static encodeStatus(req: requests.StatusRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method); - } - - public static encodeSubscribe(req: requests.SubscribeRequest): JsonRpcRequest { - const eventTag = { key: "tm.event", value: req.query.type }; - const query = requests.buildQuery({ tags: [eventTag], raw: req.query.raw }); - return createJsonRpcRequest("subscribe", { query: query }); - } - - public static encodeTx(req: requests.TxRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeTxParams(req.params)); - } - - // TODO: encode params for query string??? - public static encodeTxSearch(req: requests.TxSearchRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeTxSearchParams(req.params)); - } - - public static encodeValidators(req: requests.ValidatorsRequest): JsonRpcRequest { - return createJsonRpcRequest(req.method, encodeValidatorsParams(req.params)); - } -} diff --git a/packages/tendermint-rpc/src/tendermint35/adaptor/responses.spec.ts b/packages/tendermint-rpc/src/tendermint35/adaptor/responses.spec.ts deleted file mode 100644 index c85b753328..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/adaptor/responses.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { fromBase64, fromHex } from "@cosmjs/encoding"; - -import { decodeEvent, decodeValidatorGenesis, decodeValidatorInfo, decodeValidatorUpdate } from "./responses"; - -describe("Adaptor Responses", () => { - describe("decodeEvent", () => { - it("works with attributes", () => { - // from https://rpc.mainnet-1.tgrade.confio.run/tx?hash=0x2C44715748022DB2FB5F40105383719BFCFCEE51DBC02FF4088BE3F5924CD7BF - const event = decodeEvent({ - type: "coin_spent", - attributes: [ - { key: "foo", value: "123" }, - { key: "bar", value: "456" }, - ], - }); - expect(event.type).toEqual("coin_spent"); - expect(event.attributes).toEqual([ - { key: "foo", value: "123" }, - { key: "bar", value: "456" }, - ]); - }); - - it("works with no attribute", () => { - const event = decodeEvent({ - type: "cosmos.module.EmittedEvent", - }); - expect(event.type).toEqual("cosmos.module.EmittedEvent"); - expect(event.attributes).toEqual([]); - }); - }); - - describe("decodeValidatorGenesis", () => { - it("works for genesis format", () => { - // from https://raw.githubusercontent.com/cosmos/mainnet/master/genesis.json - const validator = decodeValidatorGenesis({ - address: "A03DC128D38DB0BC5F18AE1872F1CB2E1FD41157", - name: "真本聪&IOSG", - power: "169980", - pub_key: { - type: "tendermint/PubKeyEd25519", - value: "2BX6Zuj8RmdJAkD1BAg6KB0v04liyM7jBdwOGIb9F9Q=", - }, - }); - expect(validator).toEqual({ - address: fromHex("A03DC128D38DB0BC5F18AE1872F1CB2E1FD41157"), - votingPower: BigInt(169980), - pubkey: { - algorithm: "ed25519", - data: fromBase64("2BX6Zuj8RmdJAkD1BAg6KB0v04liyM7jBdwOGIb9F9Q="), - }, - }); - }); - }); - - describe("decodeValidatorUpdate", () => { - it("works for block results format", () => { - // from https://rpc.cosmos.network/block_results?height=10539773 - const update = decodeValidatorUpdate({ - pub_key: { - Sum: { - type: "tendermint.crypto.PublicKey_Ed25519", - value: { - ed25519: "0kNlxBMpm+5WtfHIG1xsWatOXTKPLtmSqn3EiEIDZeI=", - }, - }, - }, - power: "11418237", - }); - expect(update).toEqual({ - pubkey: { - algorithm: "ed25519", - data: fromBase64("0kNlxBMpm+5WtfHIG1xsWatOXTKPLtmSqn3EiEIDZeI="), - }, - votingPower: BigInt(11418237), - }); - }); - - it("works for block results format without voting power", () => { - // from https://rpc.cosmos.network/block_results?height=10883046 - const update = decodeValidatorUpdate({ - pub_key: { - Sum: { - type: "tendermint.crypto.PublicKey_Ed25519", - value: { - ed25519: "HjSC7VkhKih6xMhudlqfaFE8ZZnP8RKJPv4iqR7RhcE=", - }, - }, - }, - }); - expect(update).toEqual({ - pubkey: { - algorithm: "ed25519", - data: fromBase64("HjSC7VkhKih6xMhudlqfaFE8ZZnP8RKJPv4iqR7RhcE="), - }, - votingPower: BigInt(0), - }); - }); - }); - - describe("decodeValidatorInfo", () => { - it("works for validators format", () => { - // from https://rpc.cosmos.network/validators?height=10601034 - const info = decodeValidatorInfo({ - address: "AC2D56057CD84765E6FBE318979093E8E44AA18F", - pub_key: { - type: "tendermint/PubKeyEd25519", - value: "0kNlxBMpm+5WtfHIG1xsWatOXTKPLtmSqn3EiEIDZeI=", - }, - voting_power: "11228980", - proposer_priority: "62870960", - }); - expect(info).toEqual({ - address: fromHex("AC2D56057CD84765E6FBE318979093E8E44AA18F"), - pubkey: { - algorithm: "ed25519", - data: fromBase64("0kNlxBMpm+5WtfHIG1xsWatOXTKPLtmSqn3EiEIDZeI="), - }, - votingPower: BigInt(11228980), - proposerPriority: 62870960, - }); - }); - }); -}); diff --git a/packages/tendermint-rpc/src/tendermint35/adaptor/responses.ts b/packages/tendermint-rpc/src/tendermint35/adaptor/responses.ts deleted file mode 100644 index 05aecedf8e..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/adaptor/responses.ts +++ /dev/null @@ -1,915 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { fromBase64, fromHex } from "@cosmjs/encoding"; -import { JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; -import { assert } from "@cosmjs/utils"; - -import { DateWithNanoseconds, fromRfc3339WithNanoseconds } from "../../dates"; -import { apiToBigInt, apiToSmallInt } from "../../inthelpers"; -import { SubscriptionEvent } from "../../rpcclients"; -import { BlockIdFlag, CommitSignature, ValidatorPubkey } from "../../types"; -import { - assertArray, - assertBoolean, - assertNotEmpty, - assertNumber, - assertObject, - assertSet, - assertString, - dictionaryToStringMap, - may, -} from "../encodings"; -import { hashTx } from "../hasher"; -import * as responses from "../responses"; - -interface AbciInfoResult { - readonly response: RpcAbciInfoResponse; -} - -interface RpcAbciInfoResponse { - readonly data?: string; - readonly last_block_height?: string; - /** base64 encoded */ - readonly last_block_app_hash?: string; -} - -function decodeAbciInfo(data: RpcAbciInfoResponse): responses.AbciInfoResponse { - return { - data: data.data, - lastBlockHeight: may(apiToSmallInt, data.last_block_height), - lastBlockAppHash: may(fromBase64, data.last_block_app_hash), - }; -} - -interface AbciQueryResult { - readonly response: RpcAbciQueryResponse; -} - -export interface RpcProofOp { - readonly type: string; - /** base64 encoded */ - readonly key: string; - /** base64 encoded */ - readonly data: string; -} - -export interface RpcQueryProof { - readonly ops: readonly RpcProofOp[]; -} - -function decodeQueryProof(data: RpcQueryProof): responses.QueryProof { - return { - ops: data.ops.map((op) => ({ - type: op.type, - key: fromBase64(op.key), - data: fromBase64(op.data), - })), - }; -} - -interface RpcAbciQueryResponse { - /** - * Base64 encoded - * - * This can be null since this is a byte slice and due to - * https://github.com/tendermint/tendermint/blob/v0.35.7/abci/types/result.go#L53 - */ - readonly key?: string | null; - /** - * Base64 encoded - * - * This can be null since this is a byte slice and due to - * https://github.com/tendermint/tendermint/blob/v0.35.7/abci/types/result.go#L53 - */ - readonly value?: string | null; - readonly proofOps?: RpcQueryProof | null; - readonly height?: string; - readonly index?: string; - readonly code?: string; // only for errors - readonly codespace?: string; - readonly log?: string; - readonly info?: string; -} - -function decodeAbciQuery(data: RpcAbciQueryResponse): responses.AbciQueryResponse { - return { - key: fromBase64(assertString(data.key ?? "")), - value: fromBase64(assertString(data.value ?? "")), - proof: may(decodeQueryProof, data.proofOps), - height: may(apiToSmallInt, data.height), - code: may(apiToSmallInt, data.code), - codespace: assertString(data.codespace ?? ""), - index: may(apiToSmallInt, data.index), - log: data.log, - info: assertString(data.info ?? ""), - }; -} - -/** - * EventAttribute from Tendermint. In 0.35 the type of key and value was changed - * from bytes to string, such that no base64 encoding is used anymore. - */ -interface RpcEventAttribute { - readonly key: string; - readonly value?: string; -} - -function decodeEventAttribute(attribute: RpcEventAttribute): responses.EventAttribute { - return { - key: assertNotEmpty(attribute.key), - value: attribute.value ?? "", - }; -} - -function decodeAttributes(attributes: readonly RpcEventAttribute[]): responses.EventAttribute[] { - return assertArray(attributes).map(decodeEventAttribute); -} - -interface RpcEvent { - readonly type: string; - /** Can be omitted (see https://github.com/cosmos/cosmjs/pull/1198) */ - readonly attributes?: readonly RpcEventAttribute[]; -} - -export function decodeEvent(event: RpcEvent): responses.Event { - return { - type: event.type, - attributes: event.attributes ? decodeAttributes(event.attributes) : [], - }; -} - -function decodeEvents(events: readonly RpcEvent[]): readonly responses.Event[] { - return assertArray(events).map(decodeEvent); -} - -interface RpcTxData { - readonly codespace?: string; - readonly code?: number; - readonly log?: string; - /** base64 encoded */ - readonly data?: string; - readonly events?: readonly RpcEvent[]; - readonly gas_wanted?: string; - readonly gas_used?: string; -} - -function decodeTxData(data: RpcTxData): responses.TxData { - return { - code: apiToSmallInt(assertNumber(data.code ?? 0)), - codespace: data.codespace, - log: data.log, - data: may(fromBase64, data.data), - events: data.events ? decodeEvents(data.events) : [], - gasWanted: apiToSmallInt(data.gas_wanted ?? "0"), - gasUsed: apiToSmallInt(data.gas_used ?? "0"), - }; -} - -type RpcPubkey = - | { - readonly type: string; - /** base64 encoded */ - readonly value: string; - } - | { - // See: https://github.com/cosmos/cosmjs/issues/1142 - readonly Sum: { - readonly type: string; - readonly value: { - /** base64 encoded */ - [algorithm: string]: string; - }; - }; - }; - -function decodePubkey(data: RpcPubkey): ValidatorPubkey { - if ("Sum" in data) { - // we don't need to check type because we're checking algorithm - const [[algorithm, value]] = Object.entries(data.Sum.value); - assert(algorithm === "ed25519" || algorithm === "secp256k1", `unknown pubkey type: ${algorithm}`); - return { - algorithm, - data: fromBase64(assertNotEmpty(value)), - }; - } else { - switch (data.type) { - // go-amino special code - case "tendermint/PubKeyEd25519": - return { - algorithm: "ed25519", - data: fromBase64(assertNotEmpty(data.value)), - }; - case "tendermint/PubKeySecp256k1": - return { - algorithm: "secp256k1", - data: fromBase64(assertNotEmpty(data.value)), - }; - default: - throw new Error(`unknown pubkey type: ${data.type}`); - } - } -} - -interface RpcBlockParams { - readonly max_bytes: string; - readonly max_gas: string; -} - -/** - * Note: we do not parse block.time_iota_ms for now because of this CHANGELOG entry - * - * > Add time_iota_ms to block's consensus parameters (not exposed to the application) - * https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0310 - */ -function decodeBlockParams(data: RpcBlockParams): responses.BlockParams { - return { - maxBytes: apiToSmallInt(assertNotEmpty(data.max_bytes)), - maxGas: apiToSmallInt(assertNotEmpty(data.max_gas)), - }; -} - -interface RpcEvidenceParams { - readonly max_age_num_blocks: string; - readonly max_age_duration: string; -} - -function decodeEvidenceParams(data: RpcEvidenceParams): responses.EvidenceParams { - return { - maxAgeNumBlocks: apiToSmallInt(assertNotEmpty(data.max_age_num_blocks)), - maxAgeDuration: apiToSmallInt(assertNotEmpty(data.max_age_duration)), - }; -} - -/** - * Example data: - * { - * "block": { - * "max_bytes": "22020096", - * "max_gas": "-1", - * "time_iota_ms": "1000" - * }, - * "evidence": { - * "max_age_num_blocks": "100000", - * "max_age_duration": "172800000000000" - * }, - * "validator": { - * "pub_key_types": [ - * "ed25519" - * ] - * } - * } - */ -interface RpcConsensusParams { - readonly block: RpcBlockParams; - readonly evidence: RpcEvidenceParams; -} - -function decodeConsensusParams(data: RpcConsensusParams): responses.ConsensusParams { - return { - block: decodeBlockParams(assertObject(data.block)), - evidence: decodeEvidenceParams(assertObject(data.evidence)), - }; -} - -// for block results -interface RpcValidatorUpdate { - readonly pub_key: RpcPubkey; - // When omitted, this means zero (see https://github.com/cosmos/cosmjs/issues/1177#issuecomment-1160115080) - readonly power?: string; -} - -export function decodeValidatorUpdate(data: RpcValidatorUpdate): responses.ValidatorUpdate { - return { - pubkey: decodePubkey(assertObject(data.pub_key)), - votingPower: apiToBigInt(data.power ?? "0"), - }; -} - -interface RpcBlockResultsResponse { - readonly height: string; - readonly txs_results: readonly RpcTxData[] | null; - readonly begin_block_events: readonly RpcEvent[] | null; - readonly end_block_events: readonly RpcEvent[] | null; - readonly validator_updates: readonly RpcValidatorUpdate[] | null; - readonly consensus_param_updates: RpcConsensusParams | null; -} - -function decodeBlockResults(data: RpcBlockResultsResponse): responses.BlockResultsResponse { - return { - height: apiToSmallInt(assertNotEmpty(data.height)), - results: (data.txs_results || []).map(decodeTxData), - validatorUpdates: (data.validator_updates || []).map(decodeValidatorUpdate), - consensusUpdates: may(decodeConsensusParams, data.consensus_param_updates), - beginBlockEvents: decodeEvents(data.begin_block_events || []), - endBlockEvents: decodeEvents(data.end_block_events || []), - }; -} - -interface RpcBlockId { - /** hex encoded */ - readonly hash: string; - readonly parts: { - readonly total: number; - /** hex encoded */ - readonly hash: string; - }; -} - -function decodeBlockId(data: RpcBlockId): responses.BlockId { - return { - hash: fromHex(assertNotEmpty(data.hash)), - parts: { - total: assertNotEmpty(data.parts.total), - hash: fromHex(assertNotEmpty(data.parts.hash)), - }, - }; -} - -interface RpcBlockVersion { - readonly block: string; - readonly app?: string; -} - -function decodeBlockVersion(data: RpcBlockVersion): responses.Version { - return { - block: apiToSmallInt(data.block), - app: apiToSmallInt(data.app ?? 0), - }; -} - -interface RpcHeader { - readonly version: RpcBlockVersion; - readonly chain_id: string; - readonly height: string; - readonly time: string; - - readonly last_block_id: RpcBlockId; - - /** hex encoded */ - readonly last_commit_hash: string; - /** hex encoded */ - readonly data_hash: string; - - /** hex encoded */ - readonly validators_hash: string; - /** hex encoded */ - readonly next_validators_hash: string; - /** hex encoded */ - readonly consensus_hash: string; - /** hex encoded */ - readonly app_hash: string; - /** hex encoded */ - readonly last_results_hash: string; - - /** hex encoded */ - readonly evidence_hash: string; - /** hex encoded */ - readonly proposer_address: string; -} - -function decodeHeader(data: RpcHeader): responses.Header { - return { - version: decodeBlockVersion(data.version), - chainId: assertNotEmpty(data.chain_id), - height: apiToSmallInt(assertNotEmpty(data.height)), - time: fromRfc3339WithNanoseconds(assertNotEmpty(data.time)), - - // When there is no last block ID (i.e. this block's height is 1), we get an empty structure like this: - // { hash: '', parts: { total: 0, hash: '' } } - lastBlockId: data.last_block_id.hash ? decodeBlockId(data.last_block_id) : null, - - lastCommitHash: fromHex(assertSet(data.last_commit_hash)), - dataHash: fromHex(assertSet(data.data_hash)), - - validatorsHash: fromHex(assertSet(data.validators_hash)), - nextValidatorsHash: fromHex(assertSet(data.next_validators_hash)), - consensusHash: fromHex(assertSet(data.consensus_hash)), - appHash: fromHex(assertSet(data.app_hash)), - lastResultsHash: fromHex(assertSet(data.last_results_hash)), - - evidenceHash: fromHex(assertSet(data.evidence_hash)), - proposerAddress: fromHex(assertNotEmpty(data.proposer_address)), - }; -} - -interface RpcBlockMeta { - readonly block_id: RpcBlockId; - readonly block_size: string; - readonly header: RpcHeader; - readonly num_txs: string; -} - -function decodeBlockMeta(data: RpcBlockMeta): responses.BlockMeta { - return { - blockId: decodeBlockId(data.block_id), - blockSize: apiToSmallInt(assertNotEmpty(data.block_size)), - header: decodeHeader(data.header), - numTxs: apiToSmallInt(assertNotEmpty(data.num_txs)), - }; -} - -interface RpcBlockchainResponse { - readonly last_height: string; - readonly block_metas: readonly RpcBlockMeta[]; -} - -function decodeBlockchain(data: RpcBlockchainResponse): responses.BlockchainResponse { - return { - lastHeight: apiToSmallInt(assertNotEmpty(data.last_height)), - blockMetas: assertArray(data.block_metas).map(decodeBlockMeta), - }; -} - -interface RpcBroadcastTxSyncResponse extends RpcTxData { - /** hex encoded */ - readonly hash: string; -} - -function decodeBroadcastTxSync(data: RpcBroadcastTxSyncResponse): responses.BroadcastTxSyncResponse { - return { - ...decodeTxData(data), - hash: fromHex(assertNotEmpty(data.hash)), - }; -} - -interface RpcBroadcastTxCommitResponse { - readonly height: string; - /** hex encoded */ - readonly hash: string; - readonly check_tx: RpcTxData; - readonly deliver_tx?: RpcTxData; -} - -function decodeBroadcastTxCommit(data: RpcBroadcastTxCommitResponse): responses.BroadcastTxCommitResponse { - return { - height: apiToSmallInt(data.height), - hash: fromHex(assertNotEmpty(data.hash)), - checkTx: decodeTxData(assertObject(data.check_tx)), - deliverTx: may(decodeTxData, data.deliver_tx), - }; -} - -function decodeBlockIdFlag(blockIdFlag: number): BlockIdFlag { - assert(blockIdFlag in BlockIdFlag); - return blockIdFlag; -} - -type RpcSignature = { - readonly block_id_flag: number; - /** hex encoded */ - readonly validator_address: string; - readonly timestamp: string; - /** - * Base64 encoded signature. - * There are cases when this is not set, see https://github.com/cosmos/cosmjs/issues/704#issuecomment-797122415. - */ - readonly signature: string | null; -}; - -/** - * In some cases a timestamp is optional and set to the value 0 in Go. - * This can lead to strings like "0001-01-01T00:00:00Z" (see https://github.com/cosmos/cosmjs/issues/704#issuecomment-797122415). - * This decoder tries to clean up such encoding from the API and turn them - * into undefined values. - */ -function decodeOptionalTime(timestamp: string): DateWithNanoseconds | undefined { - const nonZeroTime = timestamp && !timestamp.startsWith("0001-01-01"); - return nonZeroTime ? fromRfc3339WithNanoseconds(timestamp) : undefined; -} - -function decodeCommitSignature(data: RpcSignature): CommitSignature { - return { - blockIdFlag: decodeBlockIdFlag(data.block_id_flag), - validatorAddress: data.validator_address ? fromHex(data.validator_address) : undefined, - timestamp: decodeOptionalTime(data.timestamp), - signature: data.signature ? fromBase64(data.signature) : undefined, - }; -} - -interface RpcCommit { - readonly block_id: RpcBlockId; - readonly height: string; - readonly round: string; - readonly signatures: readonly RpcSignature[]; -} - -function decodeCommit(data: RpcCommit): responses.Commit { - return { - blockId: decodeBlockId(assertObject(data.block_id)), - height: apiToSmallInt(assertNotEmpty(data.height)), - round: apiToSmallInt(data.round), - signatures: assertArray(data.signatures).map(decodeCommitSignature), - }; -} - -interface RpcCommitResponse { - readonly signed_header: { - readonly header: RpcHeader; - readonly commit: RpcCommit; - }; - readonly canonical: boolean; -} - -function decodeCommitResponse(data: RpcCommitResponse): responses.CommitResponse { - return { - canonical: assertBoolean(data.canonical), - header: decodeHeader(data.signed_header.header), - commit: decodeCommit(data.signed_header.commit), - }; -} - -interface RpcValidatorGenesis { - /** hex-encoded */ - readonly address: string; - readonly pub_key: RpcPubkey; - readonly power: string; - readonly name?: string; -} - -export function decodeValidatorGenesis(data: RpcValidatorGenesis): responses.Validator { - return { - address: fromHex(assertNotEmpty(data.address)), - pubkey: decodePubkey(assertObject(data.pub_key)), - votingPower: apiToBigInt(assertNotEmpty(data.power)), - }; -} - -interface RpcGenesisResponse { - readonly genesis_time: string; - readonly chain_id: string; - readonly consensus_params: RpcConsensusParams; - // The validators key is used to specify a set of validators for testnets or PoA blockchains. - // PoS blockchains use the app_state.genutil.gentxs field to stake and bond a number of validators in the first block. - readonly validators?: readonly RpcValidatorGenesis[]; - /** hex encoded */ - readonly app_hash: string; - readonly app_state: Record | undefined; -} - -interface GenesisResult { - readonly genesis: RpcGenesisResponse; -} - -function decodeGenesis(data: RpcGenesisResponse): responses.GenesisResponse { - return { - genesisTime: fromRfc3339WithNanoseconds(assertNotEmpty(data.genesis_time)), - chainId: assertNotEmpty(data.chain_id), - consensusParams: decodeConsensusParams(data.consensus_params), - validators: data.validators ? assertArray(data.validators).map(decodeValidatorGenesis) : [], - appHash: fromHex(assertSet(data.app_hash)), // empty string in kvstore app - appState: data.app_state, - }; -} - -// this is in status -interface RpcValidatorInfo { - /** hex encoded */ - readonly address: string; - readonly pub_key: RpcPubkey; - readonly voting_power: string; - readonly proposer_priority?: string; -} - -export function decodeValidatorInfo(data: RpcValidatorInfo): responses.Validator { - return { - pubkey: decodePubkey(assertObject(data.pub_key)), - votingPower: apiToBigInt(assertNotEmpty(data.voting_power)), - address: fromHex(assertNotEmpty(data.address)), - proposerPriority: data.proposer_priority ? apiToSmallInt(data.proposer_priority) : undefined, - }; -} - -interface RpcNodeInfo { - /** hex encoded */ - readonly id: string; - /** IP and port */ - readonly listen_addr: string; - readonly network: string; - readonly version: string; - readonly channels: string; // ??? - readonly moniker: string; - readonly protocol_version: { - readonly p2p: string; - readonly block: string; - readonly app: string; - }; - /** - * Additional information. E.g. - * { - * "tx_index": "on", - * "rpc_address":"tcp://0.0.0.0:26657" - * } - */ - readonly other: Record; -} - -function decodeNodeInfo(data: RpcNodeInfo): responses.NodeInfo { - return { - id: fromHex(assertNotEmpty(data.id)), - listenAddr: assertNotEmpty(data.listen_addr), - network: assertNotEmpty(data.network), - version: assertString(data.version), // Can be empty (https://github.com/cosmos/cosmos-sdk/issues/7963) - channels: assertNotEmpty(data.channels), - moniker: assertNotEmpty(data.moniker), - other: dictionaryToStringMap(data.other), - protocolVersion: { - app: apiToSmallInt(assertNotEmpty(data.protocol_version.app)), - block: apiToSmallInt(assertNotEmpty(data.protocol_version.block)), - p2p: apiToSmallInt(assertNotEmpty(data.protocol_version.p2p)), - }, - }; -} - -interface RpcSyncInfo { - /** hex encoded */ - readonly latest_block_hash: string; - /** hex encoded */ - readonly latest_app_hash: string; - readonly latest_block_height: string; - readonly latest_block_time: string; - readonly catching_up: boolean; -} - -function decodeSyncInfo(data: RpcSyncInfo): responses.SyncInfo { - return { - latestBlockHash: fromHex(assertNotEmpty(data.latest_block_hash)), - latestAppHash: fromHex(assertNotEmpty(data.latest_app_hash)), - latestBlockTime: fromRfc3339WithNanoseconds(assertNotEmpty(data.latest_block_time)), - latestBlockHeight: apiToSmallInt(assertNotEmpty(data.latest_block_height)), - catchingUp: assertBoolean(data.catching_up), - }; -} - -interface RpcStatusResponse { - readonly node_info: RpcNodeInfo; - readonly sync_info: RpcSyncInfo; - readonly validator_info: RpcValidatorInfo; -} - -function decodeStatus(data: RpcStatusResponse): responses.StatusResponse { - return { - nodeInfo: decodeNodeInfo(data.node_info), - syncInfo: decodeSyncInfo(data.sync_info), - validatorInfo: decodeValidatorInfo(data.validator_info), - }; -} - -/** - * Example data: - * { - * "root_hash": "10A1A17D5F818099B5CAB5B91733A3CC27C0DB6CE2D571AC27FB970C314308BB", - * "data": "ZVlERVhDV2lVNEUwPXhTUjc4Tmp2QkNVSg==", - * "proof": { - * "total": "1", - * "index": "0", - * "leaf_hash": "EKGhfV+BgJm1yrW5FzOjzCfA22zi1XGsJ/uXDDFDCLs=", - * "aunts": [] - * } - * } - */ -interface RpcTxProof { - /** base64 encoded */ - readonly data: string; - /** hex encoded */ - readonly root_hash: string; - readonly proof: { - readonly total: string; - readonly index: string; - /** base64 encoded */ - readonly leaf_hash: string; - /** base64 encoded */ - readonly aunts: readonly string[]; - }; -} - -function decodeTxProof(data: RpcTxProof): responses.TxProof { - return { - data: fromBase64(assertNotEmpty(data.data)), - rootHash: fromHex(assertNotEmpty(data.root_hash)), - proof: { - total: apiToSmallInt(assertNotEmpty(data.proof.total)), - index: apiToSmallInt(assertNotEmpty(data.proof.index)), - leafHash: fromBase64(assertNotEmpty(data.proof.leaf_hash)), - aunts: assertArray(data.proof.aunts).map(fromBase64), - }, - }; -} - -interface RpcTxResponse { - /** Raw tx bytes, base64 encoded */ - readonly tx: string; - readonly tx_result: RpcTxData; - readonly height: string; - readonly index: number; - /** hex encoded */ - readonly hash: string; - readonly proof?: RpcTxProof; -} - -function decodeTxResponse(data: RpcTxResponse): responses.TxResponse { - return { - tx: fromBase64(assertNotEmpty(data.tx)), - result: decodeTxData(assertObject(data.tx_result)), - height: apiToSmallInt(assertNotEmpty(data.height)), - index: apiToSmallInt(assertNumber(data.index)), - hash: fromHex(assertNotEmpty(data.hash)), - proof: may(decodeTxProof, data.proof), - }; -} - -interface RpcTxSearchResponse { - readonly txs: readonly RpcTxResponse[]; - readonly total_count: string; -} - -function decodeTxSearch(data: RpcTxSearchResponse): responses.TxSearchResponse { - return { - totalCount: apiToSmallInt(assertNotEmpty(data.total_count)), - txs: assertArray(data.txs).map(decodeTxResponse), - }; -} - -interface RpcTxEvent { - /** Raw tx bytes, base64 encoded */ - readonly tx: string; - readonly result: RpcTxData; - readonly height: string; -} - -function decodeTxEvent(data: RpcTxEvent): responses.TxEvent { - const tx = fromBase64(assertNotEmpty(data.tx)); - return { - tx: tx, - hash: hashTx(tx), - result: decodeTxData(data.result), - height: apiToSmallInt(assertNotEmpty(data.height)), - }; -} - -interface RpcValidatorsResponse { - readonly block_height: string; - readonly validators: readonly RpcValidatorInfo[]; - readonly count: string; - readonly total: string; -} - -function decodeValidators(data: RpcValidatorsResponse): responses.ValidatorsResponse { - return { - blockHeight: apiToSmallInt(assertNotEmpty(data.block_height)), - validators: assertArray(data.validators).map(decodeValidatorInfo), - count: apiToSmallInt(assertNotEmpty(data.count)), - total: apiToSmallInt(assertNotEmpty(data.total)), - }; -} - -// We lost track on how the evidence structure actually looks like. -// This is any now and passed to the caller untouched. -type RpcEvidence = any; - -interface RpcBlock { - readonly header: RpcHeader; - readonly last_commit: RpcCommit; - readonly data: { - /** Raw tx bytes, base64 encoded */ - readonly txs?: readonly string[]; - }; - // It's currently unclear why the deep nesting is requied. - // See https://github.com/tendermint/tendermint/issues/7697. - readonly evidence?: { - readonly evidence?: readonly RpcEvidence[]; - }; -} - -function decodeBlock(data: RpcBlock): responses.Block { - return { - header: decodeHeader(assertObject(data.header)), - // For the block at height 1, last commit is not set. This is represented in an empty object like this: - // { height: '0', round: 0, block_id: { hash: '', parts: [Object] }, signatures: [] } - lastCommit: data.last_commit.block_id.hash ? decodeCommit(assertObject(data.last_commit)) : null, - txs: data.data.txs ? assertArray(data.data.txs).map(fromBase64) : [], - // Lift up .evidence.evidence to just .evidence - // See https://github.com/tendermint/tendermint/issues/7697 - evidence: data.evidence?.evidence ?? [], - }; -} - -interface RpcBlockResponse { - readonly block_id: RpcBlockId; - readonly block: RpcBlock; -} - -function decodeBlockResponse(data: RpcBlockResponse): responses.BlockResponse { - return { - blockId: decodeBlockId(data.block_id), - block: decodeBlock(data.block), - }; -} - -interface RpcBlockSearchResponse { - readonly blocks: readonly RpcBlockResponse[]; - readonly total_count: string; -} - -function decodeBlockSearch(data: RpcBlockSearchResponse): responses.BlockSearchResponse { - return { - totalCount: apiToSmallInt(assertNotEmpty(data.total_count)), - blocks: assertArray(data.blocks).map(decodeBlockResponse), - }; -} - -interface RpcNumUnconfirmedTxsResponse { - readonly total: string; - readonly total_bytes: string; -} - -function decodeNumUnconfirmedTxs(data: RpcNumUnconfirmedTxsResponse): responses.NumUnconfirmedTxsResponse { - return { - total: apiToSmallInt(assertNotEmpty(data.total)), - totalBytes: apiToSmallInt(assertNotEmpty(data.total_bytes)), - }; -} - -export class Responses { - public static decodeAbciInfo(response: JsonRpcSuccessResponse): responses.AbciInfoResponse { - return decodeAbciInfo(assertObject((response.result as AbciInfoResult).response)); - } - - public static decodeAbciQuery(response: JsonRpcSuccessResponse): responses.AbciQueryResponse { - return decodeAbciQuery(assertObject((response.result as AbciQueryResult).response)); - } - - public static decodeBlock(response: JsonRpcSuccessResponse): responses.BlockResponse { - return decodeBlockResponse(response.result as RpcBlockResponse); - } - - public static decodeBlockResults(response: JsonRpcSuccessResponse): responses.BlockResultsResponse { - return decodeBlockResults(response.result as RpcBlockResultsResponse); - } - - public static decodeBlockSearch(response: JsonRpcSuccessResponse): responses.BlockSearchResponse { - return decodeBlockSearch(response.result as RpcBlockSearchResponse); - } - - public static decodeBlockchain(response: JsonRpcSuccessResponse): responses.BlockchainResponse { - return decodeBlockchain(response.result as RpcBlockchainResponse); - } - - public static decodeBroadcastTxSync(response: JsonRpcSuccessResponse): responses.BroadcastTxSyncResponse { - return decodeBroadcastTxSync(response.result as RpcBroadcastTxSyncResponse); - } - - public static decodeBroadcastTxAsync(response: JsonRpcSuccessResponse): responses.BroadcastTxAsyncResponse { - return Responses.decodeBroadcastTxSync(response); - } - - public static decodeBroadcastTxCommit( - response: JsonRpcSuccessResponse, - ): responses.BroadcastTxCommitResponse { - return decodeBroadcastTxCommit(response.result as RpcBroadcastTxCommitResponse); - } - - public static decodeCommit(response: JsonRpcSuccessResponse): responses.CommitResponse { - return decodeCommitResponse(response.result as RpcCommitResponse); - } - - public static decodeGenesis(response: JsonRpcSuccessResponse): responses.GenesisResponse { - return decodeGenesis(assertObject((response.result as GenesisResult).genesis)); - } - - public static decodeHealth(): responses.HealthResponse { - return null; - } - - public static decodeNumUnconfirmedTxs( - response: JsonRpcSuccessResponse, - ): responses.NumUnconfirmedTxsResponse { - return decodeNumUnconfirmedTxs(response.result as RpcNumUnconfirmedTxsResponse); - } - - public static decodeStatus(response: JsonRpcSuccessResponse): responses.StatusResponse { - return decodeStatus(response.result as RpcStatusResponse); - } - - public static decodeNewBlockEvent(event: SubscriptionEvent): responses.NewBlockEvent { - return decodeBlock(event.data.value.block as RpcBlock); - } - - public static decodeNewBlockHeaderEvent(event: SubscriptionEvent): responses.NewBlockHeaderEvent { - return decodeHeader(event.data.value.header as RpcHeader); - } - - public static decodeTxEvent(event: SubscriptionEvent): responses.TxEvent { - return decodeTxEvent(event.data.value.TxResult as RpcTxEvent); - } - - public static decodeTx(response: JsonRpcSuccessResponse): responses.TxResponse { - return decodeTxResponse(response.result as RpcTxResponse); - } - - public static decodeTxSearch(response: JsonRpcSuccessResponse): responses.TxSearchResponse { - return decodeTxSearch(response.result as RpcTxSearchResponse); - } - - public static decodeValidators(response: JsonRpcSuccessResponse): responses.ValidatorsResponse { - return decodeValidators(response.result as RpcValidatorsResponse); - } -} diff --git a/packages/tendermint-rpc/src/tendermint35/adaptor/types.ts b/packages/tendermint-rpc/src/tendermint35/adaptor/types.ts deleted file mode 100644 index 1558df6d8e..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/adaptor/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; - -import { SubscriptionEvent } from "../../rpcclients"; -import * as requests from "../requests"; -import * as responses from "../responses"; - -export interface Adaptor { - readonly params: Params; - readonly responses: Responses; - readonly hashTx: (tx: Uint8Array) => Uint8Array; - readonly hashBlock: (header: responses.Header) => Uint8Array; -} - -// Encoder is a generic that matches all methods of Params -export type Encoder = (req: T) => JsonRpcRequest; - -// Decoder is a generic that matches all methods of Responses -export type Decoder = (res: JsonRpcSuccessResponse) => T; - -export interface Params { - readonly encodeAbciInfo: (req: requests.AbciInfoRequest) => JsonRpcRequest; - readonly encodeAbciQuery: (req: requests.AbciQueryRequest) => JsonRpcRequest; - readonly encodeBlock: (req: requests.BlockRequest) => JsonRpcRequest; - readonly encodeBlockchain: (req: requests.BlockchainRequest) => JsonRpcRequest; - readonly encodeBlockResults: (req: requests.BlockResultsRequest) => JsonRpcRequest; - readonly encodeBlockSearch: (req: requests.BlockSearchRequest) => JsonRpcRequest; - readonly encodeBroadcastTx: (req: requests.BroadcastTxRequest) => JsonRpcRequest; - readonly encodeCommit: (req: requests.CommitRequest) => JsonRpcRequest; - readonly encodeGenesis: (req: requests.GenesisRequest) => JsonRpcRequest; - readonly encodeHealth: (req: requests.HealthRequest) => JsonRpcRequest; - readonly encodeNumUnconfirmedTxs: (req: requests.NumUnconfirmedTxsRequest) => JsonRpcRequest; - readonly encodeStatus: (req: requests.StatusRequest) => JsonRpcRequest; - readonly encodeSubscribe: (req: requests.SubscribeRequest) => JsonRpcRequest; - readonly encodeTx: (req: requests.TxRequest) => JsonRpcRequest; - readonly encodeTxSearch: (req: requests.TxSearchRequest) => JsonRpcRequest; - readonly encodeValidators: (req: requests.ValidatorsRequest) => JsonRpcRequest; -} - -export interface Responses { - readonly decodeAbciInfo: (response: JsonRpcSuccessResponse) => responses.AbciInfoResponse; - readonly decodeAbciQuery: (response: JsonRpcSuccessResponse) => responses.AbciQueryResponse; - readonly decodeBlock: (response: JsonRpcSuccessResponse) => responses.BlockResponse; - readonly decodeBlockResults: (response: JsonRpcSuccessResponse) => responses.BlockResultsResponse; - readonly decodeBlockSearch: (response: JsonRpcSuccessResponse) => responses.BlockSearchResponse; - readonly decodeBlockchain: (response: JsonRpcSuccessResponse) => responses.BlockchainResponse; - readonly decodeBroadcastTxSync: (response: JsonRpcSuccessResponse) => responses.BroadcastTxSyncResponse; - readonly decodeBroadcastTxAsync: (response: JsonRpcSuccessResponse) => responses.BroadcastTxAsyncResponse; - readonly decodeBroadcastTxCommit: (response: JsonRpcSuccessResponse) => responses.BroadcastTxCommitResponse; - readonly decodeCommit: (response: JsonRpcSuccessResponse) => responses.CommitResponse; - readonly decodeGenesis: (response: JsonRpcSuccessResponse) => responses.GenesisResponse; - readonly decodeHealth: (response: JsonRpcSuccessResponse) => responses.HealthResponse; - readonly decodeNumUnconfirmedTxs: (response: JsonRpcSuccessResponse) => responses.NumUnconfirmedTxsResponse; - readonly decodeStatus: (response: JsonRpcSuccessResponse) => responses.StatusResponse; - readonly decodeTx: (response: JsonRpcSuccessResponse) => responses.TxResponse; - readonly decodeTxSearch: (response: JsonRpcSuccessResponse) => responses.TxSearchResponse; - readonly decodeValidators: (response: JsonRpcSuccessResponse) => responses.ValidatorsResponse; - - // events - readonly decodeNewBlockEvent: (response: SubscriptionEvent) => responses.NewBlockEvent; - readonly decodeNewBlockHeaderEvent: (response: SubscriptionEvent) => responses.NewBlockHeaderEvent; - readonly decodeTxEvent: (response: SubscriptionEvent) => responses.TxEvent; -} diff --git a/packages/tendermint-rpc/src/tendermint35/encodings.spec.ts b/packages/tendermint-rpc/src/tendermint35/encodings.spec.ts deleted file mode 100644 index b4921d3b73..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/encodings.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ReadonlyDate } from "readonly-date"; - -import { - encodeBlockId, - encodeBytes, - encodeString, - encodeTime, - encodeUvarint, - encodeVersion, -} from "./encodings"; - -describe("encodings", () => { - describe("encodeString", () => { - it("works", () => { - expect(encodeString("")).toEqual(Uint8Array.from([0])); - const str = "hello iov"; - expect(encodeString(str)).toEqual( - Uint8Array.from([str.length, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x69, 0x6f, 0x76]), - ); - }); - }); - - describe("encodeUvarint", () => { - it("works", () => { - expect(encodeUvarint(0)).toEqual(Uint8Array.from([0])); - expect(encodeUvarint(1)).toEqual(Uint8Array.from([1])); - expect(encodeUvarint(127)).toEqual(Uint8Array.from([127])); - expect(encodeUvarint(128)).toEqual(Uint8Array.from([128, 1])); - expect(encodeUvarint(255)).toEqual(Uint8Array.from([255, 1])); - expect(encodeUvarint(256)).toEqual(Uint8Array.from([128, 2])); - }); - }); - - describe("encodeTime", () => { - it("works", () => { - const readonlyDateWithNanoseconds = new ReadonlyDate(1464109200); - (readonlyDateWithNanoseconds as any).nanoseconds = 666666; - expect(encodeTime(readonlyDateWithNanoseconds)).toEqual( - Uint8Array.from([0x08, 173, 174, 89, 0x10, 170, 220, 215, 95]), - ); - }); - }); - - describe("encodeBytes", () => { - it("works", () => { - expect(encodeBytes(Uint8Array.from([]))).toEqual(Uint8Array.from([])); - const uint8Array = Uint8Array.from([1, 2, 3, 4, 5, 6, 7]); - expect(encodeBytes(uint8Array)).toEqual(Uint8Array.from([uint8Array.length, 1, 2, 3, 4, 5, 6, 7])); - }); - }); - - describe("encodeVersion", () => { - it("works", () => { - const version = { - block: 666666, - app: 200, - }; - expect(encodeVersion(version)).toEqual(Uint8Array.from([0x08, 170, 216, 40, 0x10, 200, 1])); - }); - }); - - describe("encodeBlockId", () => { - it("works", () => { - const blockId = { - hash: Uint8Array.from([1, 2, 3, 4, 5, 6, 7]), - parts: { - total: 88, - hash: Uint8Array.from([8, 9, 10, 11, 12]), - }, - }; - expect(encodeBlockId(blockId)).toEqual( - Uint8Array.from([ - 0x0a, - blockId.hash.length, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 0x12, - 9, - 0x08, - 88, - 0x12, - 5, - 8, - 9, - 10, - 11, - 12, - ]), - ); - }); - }); -}); diff --git a/packages/tendermint-rpc/src/tendermint35/encodings.ts b/packages/tendermint-rpc/src/tendermint35/encodings.ts deleted file mode 100644 index 037b506898..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/encodings.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { toUtf8 } from "@cosmjs/encoding"; - -import { ReadonlyDateWithNanoseconds } from "../dates"; -import { BlockId, Version } from "./responses"; - -/** - * A runtime checker that ensures a given value is set (i.e. not undefined or null) - * - * This is used when you want to verify that data at runtime matches the expected type. - */ -export function assertSet(value: T): T { - if ((value as unknown) === undefined) { - throw new Error("Value must not be undefined"); - } - - if ((value as unknown) === null) { - throw new Error("Value must not be null"); - } - - return value; -} - -/** - * A runtime checker that ensures a given value is a boolean - * - * This is used when you want to verify that data at runtime matches the expected type. - * This implies assertSet. - */ -export function assertBoolean(value: boolean): boolean { - assertSet(value); - if (typeof (value as unknown) !== "boolean") { - throw new Error("Value must be a boolean"); - } - return value; -} - -/** - * A runtime checker that ensures a given value is a string. - * - * This is used when you want to verify that data at runtime matches the expected type. - * This implies assertSet. - */ -export function assertString(value: string): string { - assertSet(value); - if (typeof (value as unknown) !== "string") { - throw new Error("Value must be a string"); - } - return value; -} - -/** - * A runtime checker that ensures a given value is a number - * - * This is used when you want to verify that data at runtime matches the expected type. - * This implies assertSet. - */ -export function assertNumber(value: number): number { - assertSet(value); - if (typeof (value as unknown) !== "number") { - throw new Error("Value must be a number"); - } - return value; -} - -/** - * A runtime checker that ensures a given value is an array - * - * This is used when you want to verify that data at runtime matches the expected type. - * This implies assertSet. - */ -export function assertArray(value: readonly T[]): readonly T[] { - assertSet(value); - if (!Array.isArray(value as unknown)) { - throw new Error("Value must be a an array"); - } - return value; -} - -/** - * A runtime checker that ensures a given value is an object in the sense of JSON - * (an unordered collection of key–value pairs where the keys are strings) - * - * This is used when you want to verify that data at runtime matches the expected type. - * This implies assertSet. - */ -export function assertObject(value: T): T { - assertSet(value); - if (typeof (value as unknown) !== "object") { - throw new Error("Value must be an object"); - } - - // Exclude special kind of objects like Array, Date or Uint8Array - // Object.prototype.toString() returns a specified value: - // http://www.ecma-international.org/ecma-262/7.0/index.html#sec-object.prototype.tostring - if (Object.prototype.toString.call(value) !== "[object Object]") { - throw new Error("Value must be a simple object"); - } - - return value; -} - -interface Lengther { - readonly length: number; -} - -/** - * Throws an error if value matches the empty value for the - * given type (array/string of length 0, number of value 0, ...) - * - * Otherwise returns the value. - * - * This implies assertSet - */ -export function assertNotEmpty(value: T): T { - assertSet(value); - - if (typeof value === "number" && value === 0) { - throw new Error("must provide a non-zero value"); - } else if ((value as any as Lengther).length === 0) { - throw new Error("must provide a non-empty value"); - } - return value; -} - -// may will run the transform if value is defined, otherwise returns undefined -export function may(transform: (val: T) => U, value: T | null | undefined): U | undefined { - return value === undefined || value === null ? undefined : transform(value); -} - -export function dictionaryToStringMap(obj: Record): Map { - const out = new Map(); - for (const key of Object.keys(obj)) { - const value = obj[key]; - if (typeof value !== "string") { - throw new Error("Found dictionary value of type other than string"); - } - out.set(key, value); - } - return out; -} - -// Encodings needed for hashing block headers -// Several of these functions are inspired by https://github.com/nomic-io/js-tendermint/blob/tendermint-0.30/src/ - -// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L193-L195 -export function encodeString(s: string): Uint8Array { - const utf8 = toUtf8(s); - return Uint8Array.from([utf8.length, ...utf8]); -} - -// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L79-L87 -export function encodeUvarint(n: number): Uint8Array { - return n >= 0x80 - ? // eslint-disable-next-line no-bitwise - Uint8Array.from([(n & 0xff) | 0x80, ...encodeUvarint(n >> 7)]) - : // eslint-disable-next-line no-bitwise - Uint8Array.from([n & 0xff]); -} - -// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L134-L178 -export function encodeTime(time: ReadonlyDateWithNanoseconds): Uint8Array { - const milliseconds = time.getTime(); - const seconds = Math.floor(milliseconds / 1000); - const secondsArray = seconds ? [0x08, ...encodeUvarint(seconds)] : new Uint8Array(); - const nanoseconds = (time.nanoseconds || 0) + (milliseconds % 1000) * 1e6; - const nanosecondsArray = nanoseconds ? [0x10, ...encodeUvarint(nanoseconds)] : new Uint8Array(); - return Uint8Array.from([...secondsArray, ...nanosecondsArray]); -} - -// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L180-L187 -export function encodeBytes(bytes: Uint8Array): Uint8Array { - // Since we're only dealing with short byte arrays we don't need a full VarBuffer implementation yet - if (bytes.length >= 0x80) throw new Error("Not implemented for byte arrays of length 128 or more"); - return bytes.length ? Uint8Array.from([bytes.length, ...bytes]) : new Uint8Array(); -} - -export function encodeVersion(version: Version): Uint8Array { - const blockArray = version.block - ? Uint8Array.from([0x08, ...encodeUvarint(version.block)]) - : new Uint8Array(); - const appArray = version.app ? Uint8Array.from([0x10, ...encodeUvarint(version.app)]) : new Uint8Array(); - return Uint8Array.from([...blockArray, ...appArray]); -} - -export function encodeBlockId(blockId: BlockId): Uint8Array { - return Uint8Array.from([ - 0x0a, - blockId.hash.length, - ...blockId.hash, - 0x12, - blockId.parts.hash.length + 4, - 0x08, - blockId.parts.total, - 0x12, - blockId.parts.hash.length, - ...blockId.parts.hash, - ]); -} diff --git a/packages/tendermint-rpc/src/tendermint35/hasher.spec.ts b/packages/tendermint-rpc/src/tendermint35/hasher.spec.ts deleted file mode 100644 index c66448757c..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/hasher.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { fromBase64, fromHex } from "@cosmjs/encoding"; -import { ReadonlyDate } from "readonly-date"; - -import { ReadonlyDateWithNanoseconds } from "../dates"; -import { hashBlock, hashTx } from "./hasher"; - -describe("Hasher", () => { - it("creates transaction hash equal to local test", () => { - // This was taken from a result from /tx_search of some random test transaction - // curl "http://localhost:11127/tx_search?query=\"tx.hash='5CB2CF94A1097A4BC19258BC2353C3E76102B6D528458BE45C855DC5563C1DB2'\"" - const txId = fromHex("5CB2CF94A1097A4BC19258BC2353C3E76102B6D528458BE45C855DC5563C1DB2"); - const txData = fromBase64("YUpxZDY2NURaUDMxPWd2TzBPdnNrVWFWYg=="); - expect(hashTx(txData)).toEqual(txId); - }); - - it("creates block hash equal to local test for empty block", () => { - // This was taken from a result from /block of some random empty block - // curl "http://localhost:11133/block" - const blockId = fromHex("153C484DCBC33633F0616BC019388C93DEA94F7880627976F2BFE83749E062F7"); - const time = new ReadonlyDate("2020-06-23T13:54:15.4638668Z"); - (time as any).nanoseconds = 866800; - const blockData = { - version: { - block: 10, - app: 1, - }, - chainId: "test-chain-2A5rwi", - height: 7795, - time: time as ReadonlyDateWithNanoseconds, - - lastBlockId: { - hash: fromHex("1EC48444E64E7B96585BA518613612E52B976E3DA2F2222B9CD4D1602656C96F"), - parts: { - total: 1, - hash: fromHex("D4E6F1B0EE08D0438C9BB8455D7D3F2FC1883C32D66F7C69C4A0F093B073F6D2"), - }, - }, - - lastCommitHash: fromHex("BA6A5EEA6687ACA8EE4FFE4F5D40EA073CB7397A5336309C3EC824805AF9723E"), - dataHash: fromHex(""), - - validatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"), - nextValidatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"), - consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"), - appHash: fromHex("8801000000000000"), - lastResultsHash: fromHex(""), - - evidenceHash: fromHex(""), - proposerAddress: fromHex("614F305502F65C01114F9B8711D9A0AB0AC369F4"), - }; - expect(hashBlock(blockData)).toEqual(blockId); - }); - - it("creates block hash equal to local test for block with a transaction", () => { - // This was taken from a result from /block of some random block with a transaction - // curl "http://localhost:11133/block?height=13575" - const blockId = fromHex("FF2995AF1F38B9A584077E53B5E144778718FB86539A51886A2C55F730403373"); - const time = new ReadonlyDate("2020-06-23T15:34:12.3232688Z"); - (time as any).nanoseconds = 268800; - const blockData = { - version: { - block: 10, - app: 1, - }, - chainId: "test-chain-2A5rwi", - height: 13575, - time: time as ReadonlyDateWithNanoseconds, - - lastBlockId: { - hash: fromHex("046D5441FC4D008FCDBF9F3DD5DC25CF00883763E44CF4FAF3923FB5FEA42D8F"), - parts: { - total: 1, - hash: fromHex("02E4715343625093C717638EAC67FB3A4B24CCC8DA610E0CB324D705E68FEF7B"), - }, - }, - - lastCommitHash: fromHex("AA2B807F3B0ACC866AB58D90C2D0FC70B6C860CFAC440590B4F590CDC178A207"), - dataHash: fromHex("56782879F526889734BA65375CD92A9152C7114B2C91B2D2AD8464FF69E884AA"), - - validatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"), - nextValidatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"), - consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"), - appHash: fromHex("CC02000000000000"), - lastResultsHash: fromHex("6E340B9CFFB37A989CA544E6BB780A2C78901D3FB33738768511A30617AFA01D"), - - evidenceHash: fromHex(""), - proposerAddress: fromHex("614F305502F65C01114F9B8711D9A0AB0AC369F4"), - }; - expect(hashBlock(blockData)).toEqual(blockId); - }); -}); diff --git a/packages/tendermint-rpc/src/tendermint35/hasher.ts b/packages/tendermint-rpc/src/tendermint35/hasher.ts deleted file mode 100644 index a02d2f814f..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/hasher.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Sha256, sha256 } from "@cosmjs/crypto"; - -import { - encodeBlockId, - encodeBytes, - encodeString, - encodeTime, - encodeUvarint, - encodeVersion, -} from "./encodings"; -import { Header } from "./responses"; - -// hash is sha256 -// https://github.com/tendermint/tendermint/blob/master/UPGRADING.md#v0260 -export function hashTx(tx: Uint8Array): Uint8Array { - return sha256(tx); -} - -function getSplitPoint(n: number): number { - if (n < 1) throw new Error("Cannot split an empty tree"); - const largestPowerOf2 = 2 ** Math.floor(Math.log2(n)); - return largestPowerOf2 < n ? largestPowerOf2 : largestPowerOf2 / 2; -} - -function hashLeaf(leaf: Uint8Array): Uint8Array { - const hash = new Sha256(Uint8Array.from([0])); - hash.update(leaf); - return hash.digest(); -} - -function hashInner(left: Uint8Array, right: Uint8Array): Uint8Array { - const hash = new Sha256(Uint8Array.from([1])); - hash.update(left); - hash.update(right); - return hash.digest(); -} - -// See https://github.com/tendermint/tendermint/blob/v0.31.8/docs/spec/blockchain/encoding.md#merkleroot -// Note: the hashes input may not actually be hashes, especially before a recursive call -function hashTree(hashes: readonly Uint8Array[]): Uint8Array { - switch (hashes.length) { - case 0: - throw new Error("Cannot hash empty tree"); - case 1: - return hashLeaf(hashes[0]); - default: { - const slicePoint = getSplitPoint(hashes.length); - const left = hashTree(hashes.slice(0, slicePoint)); - const right = hashTree(hashes.slice(slicePoint)); - return hashInner(left, right); - } - } -} - -export function hashBlock(header: Header): Uint8Array { - if (!header.lastBlockId) { - throw new Error( - "Hashing a block header with no last block ID (i.e. header at height 1) is not supported. If you need this, contributions are welcome. Please add documentation and test vectors for this case.", - ); - } - - const encodedFields: readonly Uint8Array[] = [ - encodeVersion(header.version), - encodeString(header.chainId), - encodeUvarint(header.height), - encodeTime(header.time), - encodeBlockId(header.lastBlockId), - - encodeBytes(header.lastCommitHash), - encodeBytes(header.dataHash), - encodeBytes(header.validatorsHash), - encodeBytes(header.nextValidatorsHash), - encodeBytes(header.consensusHash), - encodeBytes(header.appHash), - encodeBytes(header.lastResultsHash), - encodeBytes(header.evidenceHash), - encodeBytes(header.proposerAddress), - ]; - return hashTree(encodedFields); -} diff --git a/packages/tendermint-rpc/src/tendermint35/index.ts b/packages/tendermint-rpc/src/tendermint35/index.ts deleted file mode 100644 index 468b2e98ba..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Note: all exports in this module are publicly available via -// `import { tendermint35 } from "@cosmjs/tendermint-rpc"` - -export { - AbciInfoRequest, - AbciQueryParams, - AbciQueryRequest, - BlockchainRequest, - BlockRequest, - BlockResultsRequest, - BlockSearchParams, - BlockSearchRequest, - BroadcastTxParams, - BroadcastTxRequest, - CommitRequest, - GenesisRequest, - HealthRequest, - Method, - NumUnconfirmedTxsRequest, - QueryTag, - Request, - StatusRequest, - SubscriptionEventType, - TxParams, - TxRequest, - TxSearchParams, - TxSearchRequest, - ValidatorsParams, - ValidatorsRequest, -} from "./requests"; -export { - AbciInfoResponse, - AbciQueryResponse, - EventAttribute as Attribute, - Block, - BlockchainResponse, - BlockGossipParams, - BlockId, - BlockMeta, - BlockParams, - BlockResponse, - BlockResultsResponse, - BlockSearchResponse, - BroadcastTxAsyncResponse, - BroadcastTxCommitResponse, - broadcastTxCommitSuccess, - BroadcastTxSyncResponse, - broadcastTxSyncSuccess, - Commit, - CommitResponse, - ConsensusParams, - Event, - Evidence, - EvidenceParams, - GenesisResponse, - Header, - HealthResponse, - NewBlockEvent, - NewBlockHeaderEvent, - NodeInfo, - NumUnconfirmedTxsResponse, - ProofOp, - QueryProof, - Response, - StatusResponse, - SyncInfo, - TxData, - TxEvent, - TxProof, - TxResponse, - TxSearchResponse, - TxSizeParams, - Validator, - ValidatorsResponse, - Version, - Vote, - VoteType, -} from "./responses"; -export { Tendermint35Client } from "./tendermint35client"; diff --git a/packages/tendermint-rpc/src/tendermint35/requests.spec.ts b/packages/tendermint-rpc/src/tendermint35/requests.spec.ts deleted file mode 100644 index f2134ea8d6..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/requests.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { buildQuery } from "./requests"; - -describe("Requests", () => { - describe("buildQuery", () => { - it("works for no input", () => { - const query = buildQuery({}); - expect(query).toEqual(""); - }); - - it("works for one tags", () => { - const query = buildQuery({ tags: [{ key: "abc", value: "def" }] }); - expect(query).toEqual("abc='def'"); - }); - - it("works for two tags", () => { - const query = buildQuery({ - tags: [ - { key: "k", value: "9" }, - { key: "L", value: "7" }, - ], - }); - expect(query).toEqual("k='9' AND L='7'"); - }); - - it("works for raw input", () => { - const query = buildQuery({ raw: "aabbCCDD" }); - expect(query).toEqual("aabbCCDD"); - }); - - it("works for mixed input", () => { - const query = buildQuery({ - tags: [ - { key: "k", value: "9" }, - { key: "L", value: "7" }, - ], - raw: "aabbCCDD", - }); - expect(query).toEqual("k='9' AND L='7' AND aabbCCDD"); - }); - }); -}); diff --git a/packages/tendermint-rpc/src/tendermint35/requests.ts b/packages/tendermint-rpc/src/tendermint35/requests.ts deleted file mode 100644 index e4f131c5c0..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/requests.ts +++ /dev/null @@ -1,208 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - -/** - * RPC methods as documented in https://docs.tendermint.com/master/rpc/ - * - * Enum raw value must match the spelling in the "shell" example call (snake_case) - */ -export enum Method { - AbciInfo = "abci_info", - AbciQuery = "abci_query", - Block = "block", - /** Get block headers for minHeight <= height <= maxHeight. */ - Blockchain = "blockchain", - BlockResults = "block_results", - BlockSearch = "block_search", - BroadcastTxAsync = "broadcast_tx_async", - BroadcastTxSync = "broadcast_tx_sync", - BroadcastTxCommit = "broadcast_tx_commit", - Commit = "commit", - Genesis = "genesis", - Health = "health", - NumUnconfirmedTxs = "num_unconfirmed_txs", - Status = "status", - Subscribe = "subscribe", - Tx = "tx", - TxSearch = "tx_search", - Validators = "validators", - Unsubscribe = "unsubscribe", -} - -export type Request = - | AbciInfoRequest - | AbciQueryRequest - | BlockRequest - | BlockSearchRequest - | BlockchainRequest - | BlockResultsRequest - | BroadcastTxRequest - | CommitRequest - | GenesisRequest - | HealthRequest - | NumUnconfirmedTxsRequest - | StatusRequest - | TxRequest - | TxSearchRequest - | ValidatorsRequest; - -/** - * Raw values must match the tendermint event name - * - * @see https://godoc.org/github.com/tendermint/tendermint/types#pkg-constants - */ -export enum SubscriptionEventType { - NewBlock = "NewBlock", - NewBlockHeader = "NewBlockHeader", - Tx = "Tx", -} - -export interface AbciInfoRequest { - readonly method: Method.AbciInfo; -} - -export interface AbciQueryRequest { - readonly method: Method.AbciQuery; - readonly params: AbciQueryParams; -} - -export interface AbciQueryParams { - readonly path: string; - readonly data: Uint8Array; - readonly height?: number; - /** - * A flag that defines if proofs are included in the response or not. - * - * Internally this is mapped to the old inverse name `trusted` for Tendermint < 0.26. - * Starting with Tendermint 0.26, the default value changed from true to false. - */ - readonly prove?: boolean; -} - -export interface BlockRequest { - readonly method: Method.Block; - readonly params: { - readonly height?: number; - }; -} - -export interface BlockchainRequest { - readonly method: Method.Blockchain; - readonly params: BlockchainRequestParams; -} - -export interface BlockchainRequestParams { - readonly minHeight?: number; - readonly maxHeight?: number; -} - -export interface BlockResultsRequest { - readonly method: Method.BlockResults; - readonly params: { - readonly height?: number; - }; -} - -export interface BlockSearchRequest { - readonly method: Method.BlockSearch; - readonly params: BlockSearchParams; -} - -export interface BlockSearchParams { - readonly query: string; - readonly page?: number; - readonly per_page?: number; - readonly order_by?: string; -} - -export interface BroadcastTxRequest { - readonly method: Method.BroadcastTxAsync | Method.BroadcastTxSync | Method.BroadcastTxCommit; - readonly params: BroadcastTxParams; -} - -export interface BroadcastTxParams { - readonly tx: Uint8Array; -} - -export interface CommitRequest { - readonly method: Method.Commit; - readonly params: { - readonly height?: number; - }; -} - -export interface GenesisRequest { - readonly method: Method.Genesis; -} - -export interface HealthRequest { - readonly method: Method.Health; -} - -export interface NumUnconfirmedTxsRequest { - readonly method: Method.NumUnconfirmedTxs; -} - -export interface StatusRequest { - readonly method: Method.Status; -} - -export interface SubscribeRequest { - readonly method: Method.Subscribe; - readonly query: { - readonly type: SubscriptionEventType; - readonly raw?: string; - }; -} - -export interface QueryTag { - readonly key: string; - readonly value: string; -} - -export interface TxRequest { - readonly method: Method.Tx; - readonly params: TxParams; -} - -export interface TxParams { - readonly hash: Uint8Array; - readonly prove?: boolean; -} - -// TODO: clarify this type -export interface TxSearchRequest { - readonly method: Method.TxSearch; - readonly params: TxSearchParams; -} - -export interface TxSearchParams { - readonly query: string; - readonly prove?: boolean; - readonly page?: number; - readonly per_page?: number; - readonly order_by?: string; -} - -export interface ValidatorsRequest { - readonly method: Method.Validators; - readonly params: ValidatorsParams; -} - -export interface ValidatorsParams { - readonly height?: number; - readonly page?: number; - readonly per_page?: number; -} - -export interface BuildQueryComponents { - readonly tags?: readonly QueryTag[]; - readonly raw?: string; -} - -export function buildQuery(components: BuildQueryComponents): string { - const tags = components.tags ? components.tags : []; - const tagComponents = tags.map((tag) => `${tag.key}='${tag.value}'`); - const rawComponents = components.raw ? [components.raw] : []; - - return [...tagComponents, ...rawComponents].join(" AND "); -} diff --git a/packages/tendermint-rpc/src/tendermint35/responses.ts b/packages/tendermint-rpc/src/tendermint35/responses.ts deleted file mode 100644 index 66eb464c77..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/responses.ts +++ /dev/null @@ -1,389 +0,0 @@ -import { ReadonlyDate } from "readonly-date"; - -import { ReadonlyDateWithNanoseconds } from "../dates"; -import { CommitSignature, ValidatorPubkey } from "../types"; - -export type Response = - | AbciInfoResponse - | AbciQueryResponse - | BlockResponse - | BlockResultsResponse - | BlockSearchResponse - | BlockchainResponse - | BroadcastTxAsyncResponse - | BroadcastTxSyncResponse - | BroadcastTxCommitResponse - | CommitResponse - | GenesisResponse - | HealthResponse - | NumUnconfirmedTxsResponse - | StatusResponse - | TxResponse - | TxSearchResponse - | ValidatorsResponse; - -export interface AbciInfoResponse { - readonly data?: string; - readonly lastBlockHeight?: number; - readonly lastBlockAppHash?: Uint8Array; -} - -export interface ProofOp { - readonly type: string; - readonly key: Uint8Array; - readonly data: Uint8Array; -} - -export interface QueryProof { - readonly ops: readonly ProofOp[]; -} - -export interface AbciQueryResponse { - readonly key: Uint8Array; - readonly value: Uint8Array; - readonly proof?: QueryProof; - readonly height?: number; - readonly index?: number; - readonly code?: number; // non-falsy for errors - readonly codespace: string; - readonly log?: string; - readonly info: string; -} - -export interface BlockResponse { - readonly blockId: BlockId; - readonly block: Block; -} - -export interface BlockResultsResponse { - readonly height: number; - readonly results: readonly TxData[]; - readonly validatorUpdates: readonly ValidatorUpdate[]; - readonly consensusUpdates?: ConsensusParams; - readonly beginBlockEvents: readonly Event[]; - readonly endBlockEvents: readonly Event[]; -} - -export interface BlockSearchResponse { - readonly blocks: readonly BlockResponse[]; - readonly totalCount: number; -} - -export interface BlockchainResponse { - readonly lastHeight: number; - readonly blockMetas: readonly BlockMeta[]; -} - -/** - * No transaction data in here because RPC method BroadcastTxAsync "returns right away, with no response" - */ -export interface BroadcastTxAsyncResponse { - readonly hash: Uint8Array; -} - -export interface BroadcastTxSyncResponse extends TxData { - readonly hash: Uint8Array; -} - -/** - * Returns true iff transaction made it successfully into the transaction pool - */ -export function broadcastTxSyncSuccess(res: BroadcastTxSyncResponse): boolean { - // code must be 0 on success - return res.code === 0; -} - -export interface BroadcastTxCommitResponse { - readonly height: number; - readonly hash: Uint8Array; - readonly checkTx: TxData; - readonly deliverTx?: TxData; -} - -/** - * Returns true iff transaction made it successfully into a block - * (i.e. success in `check_tx` and `deliver_tx` field) - */ -export function broadcastTxCommitSuccess(response: BroadcastTxCommitResponse): boolean { - // code must be 0 on success - // deliverTx may be present but empty on failure - return response.checkTx.code === 0 && !!response.deliverTx && response.deliverTx.code === 0; -} - -export interface CommitResponse { - readonly header: Header; - readonly commit: Commit; - readonly canonical: boolean; -} - -export interface GenesisResponse { - readonly genesisTime: ReadonlyDate; - readonly chainId: string; - readonly consensusParams: ConsensusParams; - readonly validators: readonly Validator[]; - readonly appHash: Uint8Array; - readonly appState: Record | undefined; -} - -export type HealthResponse = null; - -export interface NumUnconfirmedTxsResponse { - readonly total: number; - readonly totalBytes: number; -} - -export interface StatusResponse { - readonly nodeInfo: NodeInfo; - readonly syncInfo: SyncInfo; - readonly validatorInfo: Validator; -} - -/** - * A transaction from RPC calls like search. - * - * Try to keep this compatible to TxEvent - */ -export interface TxResponse { - readonly tx: Uint8Array; - readonly hash: Uint8Array; - readonly height: number; - readonly index: number; - readonly result: TxData; - readonly proof?: TxProof; -} - -export interface TxSearchResponse { - readonly txs: readonly TxResponse[]; - readonly totalCount: number; -} - -export interface ValidatorsResponse { - readonly blockHeight: number; - readonly validators: readonly Validator[]; - readonly count: number; - readonly total: number; -} - -// Events - -export interface NewBlockEvent extends Block {} - -export interface NewBlockHeaderEvent extends Header {} - -export interface TxEvent { - readonly tx: Uint8Array; - readonly hash: Uint8Array; - readonly height: number; - readonly result: TxData; -} - -// Helper items used above - -/** - * An event attribute. - * - * In 0.35 the type of key and value was changed - * from bytes to string, such that no base64 encoding is used anymore. - */ -export interface EventAttribute { - readonly key: string; - readonly value: string; -} - -export interface Event { - readonly type: string; - readonly attributes: readonly EventAttribute[]; -} - -export interface TxData { - readonly code: number; - readonly codespace?: string; - readonly log?: string; - readonly data?: Uint8Array; - readonly events: readonly Event[]; - readonly gasWanted: number; - readonly gasUsed: number; -} - -export interface TxProof { - readonly data: Uint8Array; - readonly rootHash: Uint8Array; - readonly proof: { - readonly total: number; - readonly index: number; - readonly leafHash: Uint8Array; - readonly aunts: readonly Uint8Array[]; - }; -} - -export interface BlockMeta { - readonly blockId: BlockId; - readonly blockSize: number; - readonly header: Header; - readonly numTxs: number; -} - -export interface BlockId { - readonly hash: Uint8Array; - readonly parts: { - readonly total: number; - readonly hash: Uint8Array; - }; -} - -export interface Block { - readonly header: Header; - /** - * For the block at height 1, last commit is not set. - */ - readonly lastCommit: Commit | null; - readonly txs: readonly Uint8Array[]; - readonly evidence: readonly Evidence[]; -} - -/** - * We lost track on how the evidence structure actually looks like. - * This is any now and passed to the caller untouched. - * - * See also https://github.com/cosmos/cosmjs/issues/980. - */ -export type Evidence = any; - -export interface Commit { - readonly blockId: BlockId; - readonly height: number; - readonly round: number; - readonly signatures: readonly CommitSignature[]; -} - -/** - * raw values from https://github.com/tendermint/tendermint/blob/dfa9a9a30a666132425b29454e90a472aa579a48/types/vote.go#L44 - */ -export enum VoteType { - PreVote = 1, - PreCommit = 2, -} - -export interface Vote { - readonly type: VoteType; - readonly validatorAddress: Uint8Array; - readonly validatorIndex: number; - readonly height: number; - readonly round: number; - readonly timestamp: ReadonlyDate; - readonly blockId: BlockId; - readonly signature: Uint8Array; -} - -export interface Version { - readonly block: number; - readonly app: number; -} - -// https://github.com/tendermint/tendermint/blob/v0.31.8/docs/spec/blockchain/blockchain.md -export interface Header { - // basic block info - readonly version: Version; - readonly chainId: string; - readonly height: number; - readonly time: ReadonlyDateWithNanoseconds; - - /** - * Block ID of the previous block. This can be `null` when the currect block is height 1. - */ - readonly lastBlockId: BlockId | null; - - /** - * Hashes of block data. - * - * This is `sha256("")` for height 1 🤷‍ - */ - readonly lastCommitHash: Uint8Array; - /** - * This is `sha256("")` as long as there is no data 🤷‍ - */ - readonly dataHash: Uint8Array; - - // hashes from the app output from the prev block - readonly validatorsHash: Uint8Array; - readonly nextValidatorsHash: Uint8Array; - readonly consensusHash: Uint8Array; - /** - * This can be an empty string for height 1 and turn into "0000000000000000" later on 🤷‍ - */ - readonly appHash: Uint8Array; - /** - * This is `sha256("")` as long as there is no data 🤷‍ - */ - readonly lastResultsHash: Uint8Array; - - // consensus info - /** - * This is `sha256("")` as long as there is no data 🤷‍ - */ - readonly evidenceHash: Uint8Array; - readonly proposerAddress: Uint8Array; -} - -export interface NodeInfo { - readonly id: Uint8Array; - /** IP and port */ - readonly listenAddr: string; - readonly network: string; - /** - * The Tendermint version. Can be empty (see https://github.com/cosmos/cosmos-sdk/issues/7963). - */ - readonly version: string; - readonly channels: string; // ??? - readonly moniker: string; - readonly other: Map; - readonly protocolVersion: { - readonly p2p: number; - readonly block: number; - readonly app: number; - }; -} - -export interface SyncInfo { - readonly latestBlockHash: Uint8Array; - readonly latestAppHash: Uint8Array; - readonly latestBlockHeight: number; - readonly latestBlockTime: ReadonlyDate; - readonly catchingUp: boolean; -} - -export interface Validator { - readonly address: Uint8Array; - readonly pubkey?: ValidatorPubkey; - readonly votingPower: bigint; - readonly proposerPriority?: number; -} - -export interface ValidatorUpdate { - readonly pubkey: ValidatorPubkey; - readonly votingPower: bigint; -} - -export interface ConsensusParams { - readonly block: BlockParams; - readonly evidence: EvidenceParams; -} - -export interface BlockParams { - readonly maxBytes: number; - readonly maxGas: number; -} - -export interface TxSizeParams { - readonly maxBytes: number; - readonly maxGas: number; -} - -export interface BlockGossipParams { - readonly blockPartSizeBytes: number; -} - -export interface EvidenceParams { - readonly maxAgeNumBlocks: number; - readonly maxAgeDuration: number; -} diff --git a/packages/tendermint-rpc/src/tendermint35/tendermint35client.spec.ts b/packages/tendermint-rpc/src/tendermint35/tendermint35client.spec.ts deleted file mode 100644 index d46b95cc88..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/tendermint35client.spec.ts +++ /dev/null @@ -1,866 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { toAscii } from "@cosmjs/encoding"; -import { firstEvent, toListPromise } from "@cosmjs/stream"; -import { sleep } from "@cosmjs/utils"; -import { ReadonlyDate } from "readonly-date"; -import { Stream } from "xstream"; - -import { HttpClient, RpcClient, WebsocketClient } from "../rpcclients"; -import { - buildKvTx, - ExpectedValues, - nonNegativeIntegerMatcher, - pendingWithoutTendermint, - randomString, - tendermintEnabled, - tendermintInstances, - tendermintSearchIndexUpdated, -} from "../testutil.spec"; -import { adaptor35 } from "./adaptor"; -import { buildQuery } from "./requests"; -import * as responses from "./responses"; -import { Tendermint35Client } from "./tendermint35client"; - -function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues): void { - describe("create", () => { - it("can auto-discover Tendermint version and communicate", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - const info = await client.abciInfo(); - expect(info).toBeTruthy(); - client.disconnect(); - }); - - it("can connect to Tendermint with known version", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - expect(await client.abciInfo()).toBeTruthy(); - client.disconnect(); - }); - }); - - it("can get genesis", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - const genesis = await client.genesis(); - expect(genesis).toBeTruthy(); - client.disconnect(); - }); - - describe("broadcastTxCommit", () => { - it("can broadcast a transaction", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - const tx = buildKvTx(randomString(), randomString()); - - const response = await client.broadcastTxCommit({ tx: tx }); - expect(response.height).toBeGreaterThan(2); - expect(response.hash).toBeTruthy(); - // verify success - expect(response.checkTx.code).toBeFalsy(); - expect(response.deliverTx).toBeTruthy(); - if (response.deliverTx) { - expect(response.deliverTx.code).toBeFalsy(); - } - - client.disconnect(); - }); - }); - - describe("broadcastTxSync", () => { - it("can broadcast a transaction", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - const tx = buildKvTx(randomString(), randomString()); - - const response = await client.broadcastTxSync({ tx: tx }); - expect(response.hash.length).toEqual(32); - // verify success - expect(response.code).toBeFalsy(); - expect(response.codespace).toBeFalsy(); - - client.disconnect(); - }); - }); - - describe("broadcastTxAsync", () => { - it("can broadcast a transaction", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - const tx = buildKvTx(randomString(), randomString()); - - const response = await client.broadcastTxAsync({ tx: tx }); - expect(response.hash.length).toEqual(32); - - client.disconnect(); - }); - }); - - it("gets the same tx hash from backend as calculated locally", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - const tx = buildKvTx(randomString(), randomString()); - const calculatedTxHash = adaptor35.hashTx(tx); - - const response = await client.broadcastTxCommit({ tx: tx }); - expect(response.hash).toEqual(calculatedTxHash); - - client.disconnect(); - }); - - describe("abciQuery", () => { - it("can query the state", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const key = randomString(); - const value = randomString(); - await client.broadcastTxCommit({ tx: buildKvTx(key, value) }); - - const binKey = toAscii(key); - const binValue = toAscii(value); - const queryParams = { path: "/key", data: binKey, prove: true }; - const response = await client.abciQuery(queryParams); - expect(response.key).toEqual(binKey); - expect(response.value).toEqual(binValue); - expect(response.code).toEqual(0); - expect(response.codespace).toEqual(""); - expect(response.index).toEqual(-1); - expect(response.proof).toBeUndefined(); - expect(response.log).toEqual("exists"); - expect(response.info).toEqual(""); - expect(response.height).toMatch(nonNegativeIntegerMatcher); - - client.disconnect(); - }); - }); - - it("can get a commit", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - const response = await client.commit(4); - - expect(response).toBeTruthy(); - expect(response.commit.signatures.length).toBeGreaterThanOrEqual(1); - expect(response.commit.signatures[0].blockIdFlag).toEqual(2); - expect(response.commit.signatures[0].validatorAddress?.length).toEqual(20); - expect(response.commit.signatures[0].timestamp).toBeInstanceOf(Date); - expect(response.commit.signatures[0].signature?.length).toEqual(64); - - client.disconnect(); - }); - - it("can get validators", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - const response = await client.validators({}); - - expect(response).toBeTruthy(); - expect(response.blockHeight).toBeGreaterThanOrEqual(1); - expect(response.count).toBeGreaterThanOrEqual(1); - expect(response.total).toBeGreaterThanOrEqual(1); - expect(response.validators.length).toBeGreaterThanOrEqual(1); - expect(response.validators[0].address.length).toEqual(20); - expect(response.validators[0].pubkey).toBeDefined(); - expect(response.validators[0].votingPower).toBeGreaterThanOrEqual(0); - expect(response.validators[0].proposerPriority).toBeGreaterThanOrEqual(0); - - client.disconnect(); - }); - - it("can get all validators", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - const response = await client.validatorsAll(); - - expect(response).toBeTruthy(); - expect(response.blockHeight).toBeGreaterThanOrEqual(1); - expect(response.count).toBeGreaterThanOrEqual(1); - expect(response.total).toBeGreaterThanOrEqual(1); - expect(response.validators.length).toBeGreaterThanOrEqual(1); - expect(response.validators[0].address.length).toEqual(20); - expect(response.validators[0].pubkey).toBeDefined(); - expect(response.validators[0].votingPower).toBeGreaterThanOrEqual(0); - expect(response.validators[0].proposerPriority).toBeGreaterThanOrEqual(0); - - client.disconnect(); - }); - - it("can call a bunch of methods", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - expect(await client.block()).toBeTruthy(); - expect(await client.genesis()).toBeTruthy(); - expect(await client.health()).toBeNull(); - - client.disconnect(); - }); - - describe("status", () => { - it("works", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const status = await client.status(); - - // node info - expect(status.nodeInfo.version).toMatch(expected.version); - expect(status.nodeInfo.protocolVersion).toEqual({ - p2p: expected.p2pVersion, - block: expected.blockVersion, - app: expected.appVersion, - }); - expect(status.nodeInfo.network).toMatch(expected.chainId); - expect(status.nodeInfo.other.size).toBeGreaterThanOrEqual(2); - expect(status.nodeInfo.other.get("tx_index")).toEqual("on"); - - // sync info - expect(status.syncInfo.catchingUp).toEqual(false); - expect(status.syncInfo.latestBlockHeight).toBeGreaterThanOrEqual(1); - - // validator info - expect(status.validatorInfo.pubkey).toBeTruthy(); - expect(status.validatorInfo.votingPower).toBeGreaterThan(0); - - client.disconnect(); - }); - }); - - describe("numUnconfirmedTxs", () => { - it("works", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const response = await client.numUnconfirmedTxs(); - - expect(response.total).toBeGreaterThanOrEqual(0); - expect(response.totalBytes).toBeGreaterThanOrEqual(0); - - client.disconnect(); - }); - }); - - describe("blockResults", () => { - it("works", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const height = 3; - const results = await client.blockResults(height); - expect(results.height).toEqual(height); - expect(results.results).toEqual([]); - expect(results.beginBlockEvents).toEqual([]); - expect(results.endBlockEvents).toEqual([]); - - client.disconnect(); - }); - }); - - describe("blockSearch", () => { - beforeAll(async () => { - if (tendermintEnabled()) { - const client = await Tendermint35Client.create(rpcFactory()); - - // eslint-disable-next-line no-inner-declarations - async function sendTx(): Promise { - const tx = buildKvTx(randomString(), randomString()); - - 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 blockSearch results", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const query = buildQuery({ raw: "block.height >= 1 AND block.height <= 3" }); - - // expect one page of results - const s1 = await client.blockSearch({ query: query, page: 1, per_page: 2 }); - expect(s1.totalCount).toEqual(3); - expect(s1.blocks.length).toEqual(2); - - // second page - const s2 = await client.blockSearch({ query: query, page: 2, per_page: 2 }); - expect(s2.totalCount).toEqual(3); - expect(s2.blocks.length).toEqual(1); - - client.disconnect(); - }); - - it("can get all search results in one call", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const query = buildQuery({ raw: "block.height >= 1 AND block.height <= 3" }); - - const sall = await client.blockSearchAll({ query: query, per_page: 2 }); - expect(sall.totalCount).toEqual(3); - expect(sall.blocks.length).toEqual(3); - // make sure there are in order from lowest to highest height - const [b1, b2, b3] = sall.blocks; - expect(b2.block.header.height).toEqual(b1.block.header.height + 1); - expect(b3.block.header.height).toEqual(b2.block.header.height + 1); - - client.disconnect(); - }); - }); - - describe("blockchain", () => { - it("returns latest in descending order by default", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - // 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).toBeGreaterThanOrEqual(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 = await Tendermint35Client.create(rpcFactory()); - - const height = (await client.status()).syncInfo.latestBlockHeight; - const blockchain = await client.blockchain(undefined, height - 1); - expect(blockchain.lastHeight).toBeGreaterThanOrEqual(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("works with maxHeight in the future", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const height = (await client.status()).syncInfo.latestBlockHeight; - const blockchain = await client.blockchain(undefined, height + 20); - expect(blockchain.lastHeight).toBeGreaterThanOrEqual(height); - expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(3); - expect(blockchain.blockMetas[0].header.height).toEqual(blockchain.lastHeight); - expect(blockchain.blockMetas[1].header.height).toEqual(blockchain.lastHeight - 1); - expect(blockchain.blockMetas[2].header.height).toEqual(blockchain.lastHeight - 2); - - client.disconnect(); - }); - - it("can limit by minHeight and maxHeight", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const height = (await client.status()).syncInfo.latestBlockHeight; - const blockchain = await client.blockchain(height - 2, height - 1); - expect(blockchain.lastHeight).toBeGreaterThanOrEqual(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 = await Tendermint35Client.create(rpcFactory()); - - const height = (await client.status()).syncInfo.latestBlockHeight; - const blockchain = await client.blockchain(height - 1, height - 1); - - expect(blockchain.lastHeight).toBeGreaterThanOrEqual(height); - expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(1); - const meta = blockchain.blockMetas[0]; - - expect(meta.blockId).toEqual(jasmine.objectContaining({})); - expect(meta.blockSize).toBeInstanceOf(Number); - expect(meta.header).toEqual( - jasmine.objectContaining({ - version: { - block: expected.blockVersion, - app: expected.appVersion, - }, - chainId: jasmine.stringMatching(expected.chainId), - }), - ); - expect(meta.numTxs).toBeInstanceOf(Number); - - client.disconnect(); - }); - }); - - describe("tx", () => { - it("can query a tx properly", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - 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(); - }); - }); - - describe("txSearch", () => { - const key = randomString(); - - beforeAll(async () => { - if (tendermintEnabled()) { - const client = await Tendermint35Client.create(rpcFactory()); - - // 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("returns transactions in descending order by default", async () => { - // NOTE: The Tendermint docs states the default ordering is "desc". Until - // 0.35 it was actually "asc" but from 0.35 on it is "desc". - // Docs: https://docs.tendermint.com/master/rpc/#/Info/tx_search - // Code 0.34: https://github.com/tendermint/tendermint/blob/v0.34.10/rpc/core/tx.go#L89 - // Code 0.35: https://github.com/tendermint/tendermint/blob/v0.35.6/internal/rpc/core/tx.go#L93 - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); - - const result = await client.txSearch({ query: query }); - - expect(result.totalCount).toEqual(3); - result.txs.slice(1).reduce((lastHeight, { height }) => { - expect(height).toBeLessThanOrEqual(lastHeight); - return height; - }, result.txs[0].height); - - client.disconnect(); - }); - - it("can set the order", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); - - const result1 = await client.txSearch({ query: query, order_by: "desc" }); - const result2 = await client.txSearch({ query: query, order_by: "asc" }); - - expect(result1.totalCount).toEqual(result2.totalCount); - expect([...result1.txs].reverse()).toEqual(result2.txs); - - client.disconnect(); - }); - - it("can paginate over txSearch results", async () => { - pendingWithoutTendermint(); - const client = await Tendermint35Client.create(rpcFactory()); - - 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 = await Tendermint35Client.create(rpcFactory()); - - 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 highest to lowest height - const [tx1, tx2, tx3] = sall.txs; - expect(tx2.height).toBeLessThan(tx1.height); - expect(tx3.height).toBeLessThan(tx2.height); - - client.disconnect(); - }); - }); -} - -function websocketTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues): void { - it("can subscribe to block header events", (done) => { - pendingWithoutTendermint(); - - const testStart = ReadonlyDate.now(); - - (async () => { - const events: responses.NewBlockHeaderEvent[] = []; - const client = await Tendermint35Client.create(rpcFactory()); - const stream = client.subscribeNewBlockHeader(); - expect(stream).toBeTruthy(); - const subscription = stream.subscribe({ - next: (event) => { - expect(event.chainId).toMatch(expected.chainId); - expect(event.height).toBeGreaterThan(0); - // seems that tendermint just guarantees within the last second for timestamp - expect(event.time.getTime()).toBeGreaterThan(testStart - 1000); - // Tendermint clock is sometimes ahead of test clock. Add 10ms tolerance - expect(event.time.getTime()).toBeLessThanOrEqual(ReadonlyDate.now() + 10); - expect(event.lastBlockId).toBeTruthy(); - - // merkle roots for proofs - expect(event.appHash).toBeTruthy(); - expect(event.consensusHash).toBeTruthy(); - expect(event.dataHash).toBeTruthy(); - expect(event.evidenceHash).toBeTruthy(); - expect(event.lastCommitHash).toBeTruthy(); - expect(event.lastResultsHash).toBeTruthy(); - expect(event.validatorsHash).toBeTruthy(); - - events.push(event); - - if (events.length === 2) { - subscription.unsubscribe(); - expect(events.length).toEqual(2); - expect(events[1].chainId).toEqual(events[0].chainId); - expect(events[1].height).toEqual(events[0].height + 1); - expect(events[1].time.getTime()).toBeGreaterThan(events[0].time.getTime()); - - expect(events[1].appHash).toEqual(events[0].appHash); - expect(events[1].consensusHash).toEqual(events[0].consensusHash); - expect(events[1].dataHash).toEqual(events[0].dataHash); - expect(events[1].evidenceHash).toEqual(events[0].evidenceHash); - expect(events[1].lastCommitHash).not.toEqual(events[0].lastCommitHash); - // This test is flaky. Not sure what to test here. - // expect(events[1].lastResultsHash).not.toEqual(events[0].lastResultsHash); - expect(events[1].validatorsHash).toEqual(events[0].validatorsHash); - - client.disconnect(); - done(); - } - }, - error: done.fail, - complete: () => done.fail("Stream completed before we are done"), - }); - })().catch(done.fail); - }); - - it("can subscribe to block events", async () => { - pendingWithoutTendermint(); - - const testStart = ReadonlyDate.now(); - - const transactionData1 = buildKvTx(randomString(), randomString()); - const transactionData2 = buildKvTx(randomString(), randomString()); - - const events: responses.NewBlockEvent[] = []; - const client = await Tendermint35Client.create(rpcFactory()); - const stream = client.subscribeNewBlock(); - const subscription = stream.subscribe({ - next: (event) => { - expect(event.header.chainId).toMatch(expected.chainId); - 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); - // Tendermint clock is sometimes ahead of test clock. Add 10ms tolerance - expect(event.header.time.getTime()).toBeLessThanOrEqual(ReadonlyDate.now() + 10); - expect(event.header.lastBlockId).toBeTruthy(); - - // merkle roots for proofs - expect(event.header.appHash).toBeTruthy(); - expect(event.header.consensusHash).toBeTruthy(); - expect(event.header.dataHash).toBeTruthy(); - expect(event.header.evidenceHash).toBeTruthy(); - expect(event.header.lastCommitHash).toBeTruthy(); - expect(event.header.lastResultsHash).toBeTruthy(); - expect(event.header.validatorsHash).toBeTruthy(); - - events.push(event); - }, - error: fail, - }); - - await client.broadcastTxCommit({ tx: transactionData1 }); - await client.broadcastTxCommit({ tx: transactionData2 }); - - // wait for events to be processed - await sleep(100); - - // Stop listening for new blocks - subscription.unsubscribe(); - - // We don't know exactly in which block the transactions are added. So we look into those - // with txs. - const eventsWithTx = events.filter((e) => e.txs.length > 0); - - expect(eventsWithTx.length).toEqual(2); - // Block body - expect(eventsWithTx[0].txs.length).toEqual(1); - expect(eventsWithTx[0].txs[0]).toEqual(transactionData1); - expect(eventsWithTx[1].txs.length).toEqual(1); - expect(eventsWithTx[1].txs[0]).toEqual(transactionData2); - // Block header - expect(eventsWithTx[1].header.height).toBeGreaterThan(eventsWithTx[0].header.height); - expect(eventsWithTx[1].header.chainId).toEqual(eventsWithTx[0].header.chainId); - expect(eventsWithTx[1].header.time.getTime()).toBeGreaterThan(eventsWithTx[0].header.time.getTime()); - expect(eventsWithTx[1].header.appHash).not.toEqual(eventsWithTx[0].header.appHash); - expect(eventsWithTx[1].header.validatorsHash).toEqual(eventsWithTx[0].header.validatorsHash); - - client.disconnect(); - }); - - it("can subscribe to transaction events", async () => { - pendingWithoutTendermint(); - - const events: responses.TxEvent[] = []; - const client = await Tendermint35Client.create(rpcFactory()); - const stream = client.subscribeTx(); - const subscription = stream.subscribe({ - next: (event) => { - expect(event.height).toBeGreaterThan(0); - expect(event.result).toBeTruthy(); - expect(event.result.events.length).toBeGreaterThanOrEqual(1); - - events.push(event); - - if (events.length === 2) { - subscription.unsubscribe(); - } - }, - error: fail, - }); - - const transactionData1 = buildKvTx(randomString(), randomString()); - const transactionData2 = buildKvTx(randomString(), randomString()); - - await client.broadcastTxCommit({ tx: transactionData1 }); - await client.broadcastTxCommit({ tx: transactionData2 }); - - // wait for events to be processed - await sleep(50); - - expect(events.length).toEqual(2); - // Meta - expect(events[1].height).toBeGreaterThan(events[0].height); - expect(events[1].result.events).not.toEqual(events[0].result.events); - // Content - expect(events[0].tx).toEqual(transactionData1); - expect(events[1].tx).toEqual(transactionData2); - - client.disconnect(); - }); - - it("can subscribe to transaction events filtered by creator", async () => { - pendingWithoutTendermint(); - - const transactionData1 = buildKvTx(randomString(), randomString()); - const transactionData2 = buildKvTx(randomString(), randomString()); - - const events: responses.TxEvent[] = []; - const client = await Tendermint35Client.create(rpcFactory()); - const query = buildQuery({ tags: [{ key: "app.creator", value: expected.appCreator }] }); - const stream = client.subscribeTx(query); - expect(stream).toBeTruthy(); - const subscription = stream.subscribe({ - next: (event) => { - expect(event.height).toBeGreaterThan(0); - expect(event.result).toBeTruthy(); - expect(event.result.events.length).toBeGreaterThanOrEqual(1); - events.push(event); - - if (events.length === 2) { - subscription.unsubscribe(); - } - }, - error: fail, - }); - - await client.broadcastTxCommit({ tx: transactionData1 }); - await client.broadcastTxCommit({ tx: transactionData2 }); - - // wait for events to be processed - await sleep(50); - - expect(events.length).toEqual(2); - // Meta - expect(events[1].height).toBeGreaterThan(events[0].height); - expect(events[1].result.events).not.toEqual(events[0].result.events); - // Content - expect(events[0].tx).toEqual(transactionData1); - expect(events[1].tx).toEqual(transactionData2); - - client.disconnect(); - }); - - it("can unsubscribe and re-subscribe to the same stream", async () => { - pendingWithoutTendermint(); - - const client = await Tendermint35Client.create(rpcFactory()); - const stream = client.subscribeNewBlockHeader(); - - const event1 = await firstEvent(stream); - expect(event1.height).toBeGreaterThanOrEqual(1); - expect(event1.time.getTime()).toBeGreaterThanOrEqual(1); - - // No sleep: producer will not be stopped in the meantime - - const event2 = await firstEvent(stream); - expect(event2.height).toBeGreaterThan(event1.height); - expect(event2.time.getTime()).toBeGreaterThan(event1.time.getTime()); - - // Very short sleep: just enough to schedule asynchronous producer stopping - await sleep(5); - - const event3 = await firstEvent(stream); - expect(event3.height).toBeGreaterThan(event2.height); - expect(event3.time.getTime()).toBeGreaterThan(event2.time.getTime()); - - // Proper sleep: enough to finish unsubscribing at over the network - await sleep(100); - - const event4 = await firstEvent(stream); - expect(event4.height).toBeGreaterThan(event3.height); - expect(event4.time.getTime()).toBeGreaterThan(event3.time.getTime()); - - client.disconnect(); - }); - - it("can subscribe twice", async () => { - pendingWithoutTendermint(); - - const client = await Tendermint35Client.create(rpcFactory()); - const stream1 = client.subscribeNewBlockHeader(); - const stream2 = client.subscribeNewBlockHeader(); - - const events = await toListPromise(Stream.merge(stream1, stream2), 4); - - expect(new Set(events.map((e) => e.height)).size).toEqual(2); - - client.disconnect(); - }); -} - -describe("Tendermint35Client", () => { - const { url, expected } = tendermintInstances[35]; - - it("can connect to a given url", async () => { - pendingWithoutTendermint(); - - // default connection - { - const client = await Tendermint35Client.connect(url); - const info = await client.abciInfo(); - expect(info).toBeTruthy(); - client.disconnect(); - } - - // http connection - { - const client = await Tendermint35Client.connect("http://" + url); - const info = await client.abciInfo(); - expect(info).toBeTruthy(); - client.disconnect(); - } - - // ws connection - { - const client = await Tendermint35Client.connect("ws://" + url); - const info = await client.abciInfo(); - expect(info).toBeTruthy(); - client.disconnect(); - } - }); - - describe("With HttpClient", () => { - defaultTestSuite(() => new HttpClient(url), expected); - }); - - describe("With WebsocketClient", () => { - // don't print out WebSocket errors if marked pending - const onError = process.env.TENDERMINT_ENABLED ? console.error : () => 0; - const factory = (): WebsocketClient => new WebsocketClient(url, onError); - defaultTestSuite(factory, expected); - websocketTestSuite(factory, expected); - }); -}); diff --git a/packages/tendermint-rpc/src/tendermint35/tendermint35client.ts b/packages/tendermint-rpc/src/tendermint35/tendermint35client.ts deleted file mode 100644 index 108966203e..0000000000 --- a/packages/tendermint-rpc/src/tendermint35/tendermint35client.ts +++ /dev/null @@ -1,361 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { Stream } from "xstream"; - -import { createJsonRpcRequest } from "../jsonrpc"; -import { - HttpClient, - HttpEndpoint, - instanceOfRpcStreamingClient, - RpcClient, - SubscriptionEvent, - WebsocketClient, -} from "../rpcclients"; -import { adaptor35, Decoder, Encoder, Params, Responses } from "./adaptor"; -import * as requests from "./requests"; -import * as responses from "./responses"; - -/** - * Please note the Tendermint 0.35 client is currently not exported and may break or be removed at any point in time. - * - * @see https://github.com/cosmos/cosmjs/issues/1225 - */ -export class Tendermint35Client { - /** - * Creates a new Tendermint client for the given endpoint. - * - * Uses HTTP when the URL schema is http or https. Uses WebSockets otherwise. - */ - public static async connect(endpoint: string | HttpEndpoint): Promise { - if (typeof endpoint === "object") { - return Tendermint35Client.create(new HttpClient(endpoint)); - } else { - const useHttp = endpoint.startsWith("http://") || endpoint.startsWith("https://"); - const rpcClient = useHttp ? new HttpClient(endpoint) : new WebsocketClient(endpoint); - return Tendermint35Client.create(rpcClient); - } - } - - /** - * Creates a new Tendermint client given an RPC client. - */ - public static async create(rpcClient: RpcClient): Promise { - // For some very strange reason I don't understand, tests start to fail on some systems - // (our CI) when skipping the status call before doing other queries. Sleeping a little - // while did not help. Thus we query the version as a way to say "hi" to the backend, - // even in cases where we don't use the result. - const _version = await this.detectVersion(rpcClient); - return new Tendermint35Client(rpcClient); - } - - private static async detectVersion(client: RpcClient): Promise { - const req = createJsonRpcRequest(requests.Method.Status); - const response = await client.execute(req); - const result = response.result; - - if (!result || !result.node_info) { - throw new Error("Unrecognized format for status response"); - } - - const version = result.node_info.version; - if (typeof version !== "string") { - throw new Error("Unrecognized version format: must be string"); - } - return version; - } - - private readonly client: RpcClient; - private readonly p: Params; - private readonly r: Responses; - - /** - * Use `Tendermint34Client.connect` or `Tendermint34Client.create` to create an instance. - */ - private constructor(client: RpcClient) { - this.client = client; - this.p = adaptor35.params; - this.r = adaptor35.responses; - } - - public disconnect(): void { - this.client.disconnect(); - } - - public async abciInfo(): Promise { - const query: requests.AbciInfoRequest = { method: requests.Method.AbciInfo }; - return this.doCall(query, this.p.encodeAbciInfo, this.r.decodeAbciInfo); - } - - public async abciQuery(params: requests.AbciQueryParams): Promise { - const query: requests.AbciQueryRequest = { params: params, method: requests.Method.AbciQuery }; - return this.doCall(query, this.p.encodeAbciQuery, this.r.decodeAbciQuery); - } - - public async block(height?: number): Promise { - const query: requests.BlockRequest = { method: requests.Method.Block, params: { height: height } }; - return this.doCall(query, this.p.encodeBlock, this.r.decodeBlock); - } - - public async blockResults(height?: number): Promise { - const query: requests.BlockResultsRequest = { - method: requests.Method.BlockResults, - params: { height: height }, - }; - return this.doCall(query, this.p.encodeBlockResults, this.r.decodeBlockResults); - } - - /** - * Search for events that are in a block. - * - * NOTE - * This method will error on any node that is running a Tendermint version lower than 0.34.9. - * - * @see https://docs.tendermint.com/master/rpc/#/Info/block_search - */ - public async blockSearch(params: requests.BlockSearchParams): Promise { - const query: requests.BlockSearchRequest = { params: params, method: requests.Method.BlockSearch }; - const resp = await this.doCall(query, this.p.encodeBlockSearch, this.r.decodeBlockSearch); - return { - ...resp, - // make sure we sort by height, as tendermint may be sorting by string value of the height - blocks: [...resp.blocks].sort((a, b) => a.block.header.height - b.block.header.height), - }; - } - - // this should paginate through all blockSearch options to ensure it returns all results. - // starts with page 1 or whatever was provided (eg. to start on page 7) - // - // NOTE - // This method will error on any node that is running a Tendermint version lower than 0.34.9. - public async blockSearchAll(params: requests.BlockSearchParams): Promise { - let page = params.page || 1; - const blocks: responses.BlockResponse[] = []; - let done = false; - - while (!done) { - const resp = await this.blockSearch({ ...params, page: page }); - blocks.push(...resp.blocks); - if (blocks.length < resp.totalCount) { - page++; - } else { - done = true; - } - } - // make sure we sort by height, as tendermint may be sorting by string value of the height - // and the earlier items may be in a higher page than the later items - blocks.sort((a, b) => a.block.header.height - b.block.header.height); - - return { - totalCount: blocks.length, - blocks: blocks, - }; - } - - /** - * 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 = { - method: requests.Method.Blockchain, - params: { - minHeight: minHeight, - maxHeight: maxHeight, - }, - }; - return this.doCall(query, this.p.encodeBlockchain, this.r.decodeBlockchain); - } - - /** - * Broadcast transaction to mempool and wait for response - * - * @see https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_sync - */ - public async broadcastTxSync( - params: requests.BroadcastTxParams, - ): Promise { - const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxSync }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxSync); - } - - /** - * Broadcast transaction to mempool and do not wait for result - * - * @see https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_async - */ - public async broadcastTxAsync( - params: requests.BroadcastTxParams, - ): Promise { - const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxAsync }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxAsync); - } - - /** - * Broadcast transaction to mempool and wait for block - * - * @see https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_commit - */ - public async broadcastTxCommit( - params: requests.BroadcastTxParams, - ): Promise { - const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxCommit }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxCommit); - } - - public async commit(height?: number): Promise { - const query: requests.CommitRequest = { method: requests.Method.Commit, params: { height: height } }; - return this.doCall(query, this.p.encodeCommit, this.r.decodeCommit); - } - - public async genesis(): Promise { - const query: requests.GenesisRequest = { method: requests.Method.Genesis }; - return this.doCall(query, this.p.encodeGenesis, this.r.decodeGenesis); - } - - public async health(): Promise { - const query: requests.HealthRequest = { method: requests.Method.Health }; - return this.doCall(query, this.p.encodeHealth, this.r.decodeHealth); - } - - public async numUnconfirmedTxs(): Promise { - const query: requests.NumUnconfirmedTxsRequest = { method: requests.Method.NumUnconfirmedTxs }; - return this.doCall(query, this.p.encodeNumUnconfirmedTxs, this.r.decodeNumUnconfirmedTxs); - } - - public async status(): Promise { - const query: requests.StatusRequest = { method: requests.Method.Status }; - return this.doCall(query, this.p.encodeStatus, this.r.decodeStatus); - } - - public subscribeNewBlock(): Stream { - const request: requests.SubscribeRequest = { - method: requests.Method.Subscribe, - query: { type: requests.SubscriptionEventType.NewBlock }, - }; - return this.subscribe(request, this.r.decodeNewBlockEvent); - } - - public subscribeNewBlockHeader(): Stream { - const request: requests.SubscribeRequest = { - method: requests.Method.Subscribe, - query: { type: requests.SubscriptionEventType.NewBlockHeader }, - }; - return this.subscribe(request, this.r.decodeNewBlockHeaderEvent); - } - - public subscribeTx(query?: string): Stream { - const request: requests.SubscribeRequest = { - method: requests.Method.Subscribe, - query: { - type: requests.SubscriptionEventType.Tx, - raw: query, - }, - }; - return this.subscribe(request, this.r.decodeTxEvent); - } - - /** - * Get a single transaction by hash - * - * @see https://docs.tendermint.com/master/rpc/#/Info/tx - */ - public async tx(params: requests.TxParams): Promise { - const query: requests.TxRequest = { params: params, method: requests.Method.Tx }; - return this.doCall(query, this.p.encodeTx, this.r.decodeTx); - } - - /** - * Search for transactions that are in a block - * - * @see https://docs.tendermint.com/master/rpc/#/Info/tx_search - */ - public async txSearch(params: requests.TxSearchParams): Promise { - const query: requests.TxSearchRequest = { params: params, method: requests.Method.TxSearch }; - return this.doCall(query, this.p.encodeTxSearch, this.r.decodeTxSearch); - } - - // this should paginate through all txSearch options to ensure it returns all results. - // starts with page 1 or whatever was provided (eg. to start on page 7) - public async txSearchAll(params: requests.TxSearchParams): Promise { - let page = params.page || 1; - const txs: responses.TxResponse[] = []; - let done = false; - - while (!done) { - const resp = await this.txSearch({ ...params, page: page }); - txs.push(...resp.txs); - if (txs.length < resp.totalCount) { - page++; - } else { - done = true; - } - } - - return { - totalCount: txs.length, - txs: txs, - }; - } - - public async validators(params: requests.ValidatorsParams): Promise { - const query: requests.ValidatorsRequest = { - method: requests.Method.Validators, - params: params, - }; - return this.doCall(query, this.p.encodeValidators, this.r.decodeValidators); - } - - public async validatorsAll(height?: number): Promise { - const validators: responses.Validator[] = []; - let page = 1; - let done = false; - let blockHeight = height; - - while (!done) { - const response = await this.validators({ - per_page: 50, - height: blockHeight, - page: page, - }); - validators.push(...response.validators); - blockHeight = blockHeight || response.blockHeight; - if (validators.length < response.total) { - page++; - } else { - done = true; - } - } - - return { - // NOTE: Default value is for type safety but this should always be set - blockHeight: blockHeight ?? 0, - count: validators.length, - total: validators.length, - validators: validators, - }; - } - - // doCall is a helper to handle the encode/call/decode logic - private async doCall( - request: T, - encode: Encoder, - decode: Decoder, - ): Promise { - const req = encode(request); - const result = await this.client.execute(req); - return decode(result); - } - - private subscribe(request: requests.SubscribeRequest, decode: (e: SubscriptionEvent) => T): Stream { - if (!instanceOfRpcStreamingClient(this.client)) { - throw new Error("This RPC client type cannot subscribe to events"); - } - - const req = this.p.encodeSubscribe(request); - const eventStream = this.client.listen(req); - return eventStream.map((event) => { - return decode(event); - }); - } -} diff --git a/packages/tendermint-rpc/src/testutil.spec.ts b/packages/tendermint-rpc/src/testutil.spec.ts index 8bfd2146d8..3b699c67b5 100644 --- a/packages/tendermint-rpc/src/testutil.spec.ts +++ b/packages/tendermint-rpc/src/testutil.spec.ts @@ -49,19 +49,6 @@ export const tendermintInstances = { appVersion: 1, }, }, - 35: { - url: "localhost:11135", - version: "0.35.x", - blockTime: 500, - expected: { - chainId: /^dockerchain$/, - version: /^$/, // Unfortunately we don't get info here - appCreator: "Cosmoshi Netowoko", - p2pVersion: 8, - blockVersion: 11, - appVersion: 1, - }, - }, 37: { url: "localhost:11137", version: "0.37.x", diff --git a/scripts/tendermint/all_start.sh b/scripts/tendermint/all_start.sh index 38aeaa0c69..5c691cd829 100755 --- a/scripts/tendermint/all_start.sh +++ b/scripts/tendermint/all_start.sh @@ -7,12 +7,10 @@ command -v shellcheck >/dev/null && shellcheck "$0" # - https://hub.docker.com/r/cometbft/cometbft/tags/ declare -a TM_IMAGES TM_IMAGES[34]="tendermint/tendermint:v0.34.19" -TM_IMAGES[35]="tendermint/tendermint:v0.35.6" TM_IMAGES[37]="cometbft/cometbft:v0.37.0-rc3" declare -a TM_ROOTS TM_ROOTS[34]="/tendermint" -TM_ROOTS[35]="/tendermint" TM_ROOTS[37]="/cometbft" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/scripts/tendermint/all_stop.sh b/scripts/tendermint/all_stop.sh index 46246195ce..a2988c4fb5 100755 --- a/scripts/tendermint/all_stop.sh +++ b/scripts/tendermint/all_stop.sh @@ -4,7 +4,7 @@ command -v shellcheck >/dev/null && shellcheck "$0" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -for KEY in 34 35 37; do +for KEY in 34 37; do export TENDERMINT_NAME="tendermint-$KEY" echo "Stopping $TENDERMINT_NAME ..." From b9ca8cb38f9f535b31f0def4d3ce4c8a3170dd14 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 2 Mar 2023 09:49:53 +0100 Subject: [PATCH 07/15] Add tx search tests --- .../tendermint34/tendermint34client.spec.ts | 110 ++++++++++++------ .../tendermint37/tendermint37client.spec.ts | 110 ++++++++++++------ 2 files changed, 154 insertions(+), 66 deletions(-) diff --git a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts index 8f41e8137d..cf2d861cdc 100644 --- a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { toAscii } from "@cosmjs/encoding"; +import { toAscii, toHex } from "@cosmjs/encoding"; import { firstEvent, toListPromise } from "@cosmjs/stream"; -import { sleep } from "@cosmjs/utils"; +import { assert, sleep } from "@cosmjs/utils"; import { ReadonlyDate } from "readonly-date"; import { Stream } from "xstream"; @@ -435,25 +435,6 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) 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); @@ -464,25 +445,28 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) }); describe("txSearch", () => { - const key = randomString(); + const txKey = randomString(); // a key used for multiple transactions + let tx1: Uint8Array | undefined; + let broadcast1: responses.BroadcastTxCommitResponse | undefined; beforeAll(async () => { if (tendermintEnabled()) { const client = await Tendermint34Client.create(rpcFactory()); // eslint-disable-next-line no-inner-declarations - async function sendTx(): Promise { + async function sendTx(): Promise<[Uint8Array, responses.BroadcastTxCommitResponse]> { const me = randomString(); - const tx = buildKvTx(key, me); + const tx = buildKvTx(txKey, 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); + expect(txRes.hash.length).toEqual(32); + return [tx, txRes]; } // send 3 txs - await sendTx(); + [tx1, broadcast1] = await sendTx(); await sendTx(); await sendTx(); @@ -492,6 +476,67 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) } }); + it("finds a single tx by hash", async () => { + pendingWithoutTendermint(); + assert(tx1 && broadcast1); + const client = await Tendermint34Client.create(rpcFactory()); + + const result = await client.txSearch({ query: `tx.hash='${toHex(broadcast1.hash)}'` }); + expect(result.totalCount).toEqual(1); + expect(result.txs[0]).toEqual({ + hash: broadcast1.hash, + height: broadcast1.height, + index: 0, + tx: tx1, + result: broadcast1.deliverTx!, + proof: undefined, + }); + + client.disconnect(); + }); + + it("finds a single tx by tags", async () => { + pendingWithoutTendermint(); + const client = await Tendermint34Client.create(rpcFactory()); + + const txKey2 = randomString(); + const txValue2 = randomString(); + const tx = buildKvTx(txKey2, txValue2); + + const txRes = await client.broadcastTxCommit({ tx: tx }); + expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true); + + // txSearch - you must enable the indexer when running + // tendermint, else you get empty results + const query = buildQuery({ tags: [{ key: "app.key", value: txKey2 }] }); + + const search = await client.txSearch({ query: query, page: 1, per_page: 30 }); + // should find the tx + expect(search.totalCount).toEqual(1); + // should return same info as querying directly, + // except without the proof + expect(search.txs[0]).toEqual({ + hash: txRes.hash, + height: txRes.height, + index: 0, + tx: tx, + result: txRes.deliverTx!, + proof: undefined, + }); + + // Ensure txSearchAll works as well. This should be moved in a dedicated "txSearchAll" test block. + const searchAll = await client.txSearchAll({ query: query }); + expect(searchAll.totalCount).toEqual(1); + expect(searchAll.txs[0]).toEqual({ + hash: txRes.hash, + height: txRes.height, + index: 0, + tx: tx, + result: txRes.deliverTx!, + proof: undefined, + }); + }); + it("returns transactions in ascending order by default", async () => { // NOTE: The Tendermint docs claim the default ordering is "desc" but it is actually "asc" // Docs: https://docs.tendermint.com/master/rpc/#/Info/tx_search @@ -499,7 +544,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint34Client.create(rpcFactory()); - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + const query = buildQuery({ tags: [{ key: "app.key", value: txKey }] }); const result = await client.txSearch({ query: query }); @@ -516,7 +561,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint34Client.create(rpcFactory()); - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + const query = buildQuery({ tags: [{ key: "app.key", value: txKey }] }); const s1 = await client.txSearch({ query: query, order_by: "desc" }); const s2 = await client.txSearch({ query: query, order_by: "asc" }); @@ -531,7 +576,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint34Client.create(rpcFactory()); - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + const query = buildQuery({ tags: [{ key: "app.key", value: txKey }] }); // expect one page of results const s1 = await client.txSearch({ query: query, page: 1, per_page: 2 }); @@ -550,15 +595,14 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint34Client.create(rpcFactory()); - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + const query = buildQuery({ tags: [{ key: "app.key", value: txKey }] }); 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); + expect(sall.txs[1].height).toEqual(sall.txs[0].height + 1); + expect(sall.txs[2].height).toEqual(sall.txs[1].height + 1); client.disconnect(); }); diff --git a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts index c37d124773..d43b1d11ac 100644 --- a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { toAscii } from "@cosmjs/encoding"; +import { toAscii, toHex } from "@cosmjs/encoding"; import { firstEvent, toListPromise } from "@cosmjs/stream"; -import { sleep } from "@cosmjs/utils"; +import { assert, sleep } from "@cosmjs/utils"; import { ReadonlyDate } from "readonly-date"; import { Stream } from "xstream"; @@ -436,25 +436,6 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) 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); @@ -465,25 +446,28 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) }); describe("txSearch", () => { - const key = randomString(); + const txKey = randomString(); // a key used for multiple transactions + let tx1: Uint8Array | undefined; + let broadcast1: responses.BroadcastTxCommitResponse | undefined; beforeAll(async () => { if (tendermintEnabled()) { const client = await Tendermint37Client.create(rpcFactory()); // eslint-disable-next-line no-inner-declarations - async function sendTx(): Promise { + async function sendTx(): Promise<[Uint8Array, responses.BroadcastTxCommitResponse]> { const me = randomString(); - const tx = buildKvTx(key, me); + const tx = buildKvTx(txKey, 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); + expect(txRes.hash.length).toEqual(32); + return [tx, txRes]; } // send 3 txs - await sendTx(); + [tx1, broadcast1] = await sendTx(); await sendTx(); await sendTx(); @@ -493,6 +477,67 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) } }); + it("finds a single tx by hash", async () => { + pendingWithoutTendermint(); + assert(tx1 && broadcast1); + const client = await Tendermint37Client.create(rpcFactory()); + + const result = await client.txSearch({ query: `tx.hash='${toHex(broadcast1.hash)}'` }); + expect(result.totalCount).toEqual(1); + expect(result.txs[0]).toEqual({ + hash: broadcast1.hash, + height: broadcast1.height, + index: 0, + tx: tx1, + result: broadcast1.deliverTx!, + proof: undefined, + }); + + client.disconnect(); + }); + + it("finds a single tx by tags", async () => { + pendingWithoutTendermint(); + const client = await Tendermint37Client.create(rpcFactory()); + + const txKey2 = randomString(); + const txValue2 = randomString(); + const tx = buildKvTx(txKey2, txValue2); + + const txRes = await client.broadcastTxCommit({ tx: tx }); + expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true); + + // txSearch - you must enable the indexer when running + // tendermint, else you get empty results + const query = buildQuery({ tags: [{ key: "app.key", value: txKey2 }] }); + + const search = await client.txSearch({ query: query, page: 1, per_page: 30 }); + // should find the tx + expect(search.totalCount).toEqual(1); + // should return same info as querying directly, + // except without the proof + expect(search.txs[0]).toEqual({ + hash: txRes.hash, + height: txRes.height, + index: 0, + tx: tx, + result: txRes.deliverTx!, + proof: undefined, + }); + + // Ensure txSearchAll works as well. This should be moved in a dedicated "txSearchAll" test block. + const searchAll = await client.txSearchAll({ query: query }); + expect(searchAll.totalCount).toEqual(1); + expect(searchAll.txs[0]).toEqual({ + hash: txRes.hash, + height: txRes.height, + index: 0, + tx: tx, + result: txRes.deliverTx!, + proof: undefined, + }); + }); + it("returns transactions in ascending order by default", async () => { // NOTE: The Tendermint docs states the default ordering is "desc". Until // 0.35 it was actually "asc" but from 0.35 on it is "desc". @@ -504,7 +549,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint37Client.create(rpcFactory()); - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + const query = buildQuery({ tags: [{ key: "app.key", value: txKey }] }); const result = await client.txSearch({ query: query }); @@ -521,7 +566,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint37Client.create(rpcFactory()); - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + const query = buildQuery({ tags: [{ key: "app.key", value: txKey }] }); const result1 = await client.txSearch({ query: query, order_by: "desc" }); const result2 = await client.txSearch({ query: query, order_by: "asc" }); @@ -536,7 +581,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint37Client.create(rpcFactory()); - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + const query = buildQuery({ tags: [{ key: "app.key", value: txKey }] }); // expect one page of results const s1 = await client.txSearch({ query: query, page: 1, per_page: 2 }); @@ -555,15 +600,14 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint37Client.create(rpcFactory()); - const query = buildQuery({ tags: [{ key: "app.key", value: key }] }); + const query = buildQuery({ tags: [{ key: "app.key", value: txKey }] }); 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 highest to lowest height - const [tx1, tx2, tx3] = sall.txs; - expect(tx2.height).toBeGreaterThan(tx1.height); - expect(tx3.height).toBeGreaterThan(tx2.height); + expect(sall.txs[1].height).toEqual(sall.txs[0].height + 1); + expect(sall.txs[2].height).toEqual(sall.txs[1].height + 1); client.disconnect(); }); From e6bf27f844ed7ae0dfb08e6ea8b84ff9b68241ef Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 2 Mar 2023 13:35:02 +0100 Subject: [PATCH 08/15] Harden "can limit by maxHeight" test --- .../tendermint-rpc/src/tendermint34/tendermint34client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts index cf2d861cdc..5298e12f78 100644 --- a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts @@ -344,7 +344,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) const height = (await client.status()).syncInfo.latestBlockHeight; const blockchain = await client.blockchain(undefined, height - 1); - expect(blockchain.lastHeight).toEqual(height); + expect(blockchain.lastHeight).toBeGreaterThanOrEqual(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); From 3d0546a5820cd9bcf8766b09a0bb91106d457b4f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 2 Mar 2023 13:35:23 +0100 Subject: [PATCH 09/15] Use base64 encoding for tx query --- packages/tendermint-rpc/src/tendermint37/adaptor/requests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/requests.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/requests.ts index 52a06a2932..ae2bd709d8 100644 --- a/packages/tendermint-rpc/src/tendermint37/adaptor/requests.ts +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/requests.ts @@ -74,13 +74,13 @@ function encodeBroadcastTxParams(params: requests.BroadcastTxParams): RpcBroadca } interface RpcTxParams { - /** hex encoded */ + /** base64 encoded */ readonly hash: string; readonly prove?: boolean; } function encodeTxParams(params: requests.TxParams): RpcTxParams { return { - hash: toHex(assertNotEmpty(params.hash)), + hash: toBase64(assertNotEmpty(params.hash)), prove: params.prove, }; } From 3daa7b6c4aa6be09c593d6b4fc8e82681cc98e0f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 2 Mar 2023 14:10:52 +0100 Subject: [PATCH 10/15] Rename `fromTendermint34Event` to `fromTendermintEvent` and let it support both Tendermint 0.34 and 0.37 events as input --- CHANGELOG.md | 2 ++ packages/cosmwasm-stargate/src/cosmwasmclient.ts | 4 ++-- packages/stargate/src/events.ts | 10 +++++----- packages/stargate/src/index.ts | 2 +- packages/stargate/src/stargateclient.ts | 4 ++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3596ebde46..c5d0b2f85d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to - @cosmjs/proto-signing: Remove `fromJSON`/`toJSON` from `TsProtoGeneratedType` such that generated types are not required to generate those anymore. The methods were provided by ts-proto but we never needed them. ([#1329]) +- @cosmjs/stargate: Rename `fromTendermint34Event` to `fromTendermintEvent` and + let it support both Tendermint 0.34 and 0.37 events as input. [#1002]: https://github.com/cosmos/cosmjs/issues/1002 [#1240]: https://github.com/cosmos/cosmjs/pull/1240 diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index ab357d9aa4..158d3eab7f 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -10,7 +10,7 @@ import { BroadcastTxError, Coin, DeliverTxResponse, - fromTendermint34Event, + fromTendermintEvent, IndexedTx, isSearchByHeightQuery, isSearchBySentFromOrToQuery, @@ -464,7 +464,7 @@ export class CosmWasmClient { height: tx.height, hash: toHex(tx.hash).toUpperCase(), code: tx.result.code, - events: tx.result.events.map(fromTendermint34Event), + events: tx.result.events.map(fromTendermintEvent), rawLog: tx.result.log || "", tx: tx.tx, gasUsed: tx.result.gasUsed, diff --git a/packages/stargate/src/events.ts b/packages/stargate/src/events.ts index b4b70cce29..57f12aaa47 100644 --- a/packages/stargate/src/events.ts +++ b/packages/stargate/src/events.ts @@ -1,5 +1,5 @@ import { fromUtf8 } from "@cosmjs/encoding"; -import { tendermint34 } from "@cosmjs/tendermint-rpc"; +import { tendermint34, tendermint37 } from "@cosmjs/tendermint-rpc"; /** * An event attribute. @@ -30,16 +30,16 @@ export interface Event { } /** - * Takes a Tendemrint 0.34 event with binary encoded key and value + * Takes a Tendermint 0.34 or 0.37 event with binary encoded key and value * and converts it into an `Event` with string attributes. */ -export function fromTendermint34Event(event: tendermint34.Event): Event { +export function fromTendermintEvent(event: tendermint34.Event | tendermint37.Event): Event { return { type: event.type, attributes: event.attributes.map( (attr): Attribute => ({ - key: fromUtf8(attr.key, true), - value: fromUtf8(attr.value, true), + key: typeof attr.key == "string" ? attr.key : fromUtf8(attr.key, true), + value: typeof attr.value == "string" ? attr.value : fromUtf8(attr.value, true), }), ), }; diff --git a/packages/stargate/src/index.ts b/packages/stargate/src/index.ts index fab188f774..62560972de 100644 --- a/packages/stargate/src/index.ts +++ b/packages/stargate/src/index.ts @@ -1,6 +1,6 @@ export { Account, accountFromAny, AccountParser } from "./accounts"; export { AminoConverter, AminoConverters, AminoTypes } from "./aminotypes"; -export { Attribute, Event, fromTendermint34Event } from "./events"; +export { Attribute, Event, fromTendermintEvent } from "./events"; export { calculateFee, GasPrice } from "./fee"; export * as logs from "./logs"; export { diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 410af11198..400f59ffc9 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -10,7 +10,7 @@ import { QueryDelegatorDelegationsResponse } from "cosmjs-types/cosmos/staking/v import { DelegationResponse } from "cosmjs-types/cosmos/staking/v1beta1/staking"; import { Account, accountFromAny, AccountParser } from "./accounts"; -import { Event, fromTendermint34Event } from "./events"; +import { Event, fromTendermintEvent } from "./events"; import { AuthExtension, BankExtension, @@ -471,7 +471,7 @@ export class StargateClient { height: tx.height, hash: toHex(tx.hash).toUpperCase(), code: tx.result.code, - events: tx.result.events.map(fromTendermint34Event), + events: tx.result.events.map(fromTendermintEvent), rawLog: tx.result.log || "", tx: tx.tx, gasUsed: tx.result.gasUsed, From a808a8218c877185cc2c80511c70914bfcbc9b4e Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 2 Mar 2023 14:22:35 +0100 Subject: [PATCH 11/15] Add TendermintClient --- CHANGELOG.md | 5 +- .../cosmwasm-stargate/src/cosmwasmclient.ts | 17 ++++--- .../src/signingcosmwasmclient.ts | 4 +- .../stargate/src/queryclient/queryclient.ts | 46 +++++++++---------- .../stargate/src/signingstargateclient.ts | 4 +- packages/stargate/src/stargateclient.ts | 17 ++++--- packages/tendermint-rpc/src/index.ts | 1 + .../tendermint-rpc/src/tendermintclient.ts | 13 ++++++ 8 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 packages/tendermint-rpc/src/tendermintclient.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d0b2f85d..4b76adeb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,10 @@ and this project adheres to - @cosmjs/stargate: Add `granteeGrants` and `granterGrants` queries to `AuthzExtension` ([#1308]). - @cosmjs/tendermint-rpc: Add new `Tendermint37Client` and remove unused - `Tendermint35Client` ([#1376]). + `Tendermint35Client`; Add `TendermintClient` as a union type for + `Tendermint34Client` or `Tendermint37Client` and + `isTendermint34Client`/`isTendermint37Client` to get the specific type + ([#1376]). [#1308]: https://github.com/cosmos/cosmjs/pull/1308 [#1376]: https://github.com/cosmos/cosmjs/pull/1376 diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index 158d3eab7f..866daf4fa3 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -25,7 +25,12 @@ import { TimeoutError, TxExtension, } from "@cosmjs/stargate"; -import { HttpEndpoint, Tendermint34Client, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc"; +import { + HttpEndpoint, + Tendermint34Client, + TendermintClient, + toRfc3339WithNanoseconds, +} from "@cosmjs/tendermint-rpc"; import { assert, sleep } from "@cosmjs/utils"; import { CodeInfoResponse, @@ -77,14 +82,14 @@ export interface ContractCodeHistoryEntry { /** Use for testing only */ export interface PrivateCosmWasmClient { - readonly tmClient: Tendermint34Client | undefined; + readonly tmClient: TendermintClient | undefined; readonly queryClient: | (QueryClient & AuthExtension & BankExtension & TxExtension & WasmExtension) | undefined; } export class CosmWasmClient { - private readonly tmClient: Tendermint34Client | undefined; + private readonly tmClient: TendermintClient | undefined; private readonly queryClient: | (QueryClient & AuthExtension & BankExtension & TxExtension & WasmExtension) | undefined; @@ -96,7 +101,7 @@ export class CosmWasmClient { return new CosmWasmClient(tmClient); } - protected constructor(tmClient: Tendermint34Client | undefined) { + protected constructor(tmClient: TendermintClient | undefined) { if (tmClient) { this.tmClient = tmClient; this.queryClient = QueryClient.withExtensions( @@ -109,11 +114,11 @@ export class CosmWasmClient { } } - protected getTmClient(): Tendermint34Client | undefined { + protected getTmClient(): TendermintClient | undefined { return this.tmClient; } - protected forceGetTmClient(): Tendermint34Client { + protected forceGetTmClient(): TendermintClient { if (!this.tmClient) { throw new Error( "Tendermint client not available. You cannot use online functionality in offline mode.", diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index 5d03812a71..e3843404fe 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -31,7 +31,7 @@ import { SignerData, StdFee, } from "@cosmjs/stargate"; -import { HttpEndpoint, Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { HttpEndpoint, Tendermint34Client, TendermintClient } from "@cosmjs/tendermint-rpc"; import { assert, assertDefined } from "@cosmjs/utils"; import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx"; import { MsgDelegate, MsgUndelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; @@ -209,7 +209,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { } protected constructor( - tmClient: Tendermint34Client | undefined, + tmClient: TendermintClient | undefined, signer: OfflineSigner, options: SigningCosmWasmClientOptions, ) { diff --git a/packages/stargate/src/queryclient/queryclient.ts b/packages/stargate/src/queryclient/queryclient.ts index 6664443316..aaa000a904 100644 --- a/packages/stargate/src/queryclient/queryclient.ts +++ b/packages/stargate/src/queryclient/queryclient.ts @@ -2,7 +2,7 @@ import { iavlSpec, ics23, tendermintSpec, verifyExistence, verifyNonExistence } from "@confio/ics23"; import { toAscii, toHex } from "@cosmjs/encoding"; import { firstEvent } from "@cosmjs/stream"; -import { tendermint34, Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { tendermint34, TendermintClient } from "@cosmjs/tendermint-rpc"; import { arrayContentEquals, assert, assertDefined, isNonNullObject, sleep } from "@cosmjs/utils"; import { ProofOps } from "cosmjs-types/tendermint/crypto/proof"; import { Stream } from "xstream"; @@ -45,24 +45,24 @@ export interface QueryAbciResponse { export class QueryClient { /** Constructs a QueryClient with 0 extensions */ - public static withExtensions(tmClient: Tendermint34Client): QueryClient; + public static withExtensions(tmClient: TendermintClient): QueryClient; /** Constructs a QueryClient with 1 extension */ public static withExtensions( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, ): QueryClient & A; /** Constructs a QueryClient with 2 extensions */ public static withExtensions( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, ): QueryClient & A & B; /** Constructs a QueryClient with 3 extensions */ public static withExtensions( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -70,7 +70,7 @@ export class QueryClient { /** Constructs a QueryClient with 4 extensions */ public static withExtensions( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -85,7 +85,7 @@ export class QueryClient { D extends object, E extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -102,7 +102,7 @@ export class QueryClient { E extends object, F extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -121,7 +121,7 @@ export class QueryClient { F extends object, G extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -142,7 +142,7 @@ export class QueryClient { G extends object, H extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -165,7 +165,7 @@ export class QueryClient { H extends object, I extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -190,7 +190,7 @@ export class QueryClient { I extends object, J extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -217,7 +217,7 @@ export class QueryClient { J extends object, K extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -246,7 +246,7 @@ export class QueryClient { K extends object, L extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -277,7 +277,7 @@ export class QueryClient { L extends object, M extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -310,7 +310,7 @@ export class QueryClient { M extends object, N extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -345,7 +345,7 @@ export class QueryClient { N extends object, O extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -382,7 +382,7 @@ export class QueryClient { O extends object, P extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -421,7 +421,7 @@ export class QueryClient { P extends object, Q extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -462,7 +462,7 @@ export class QueryClient { Q extends object, R extends object, >( - tmClient: Tendermint34Client, + tmClient: TendermintClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -484,7 +484,7 @@ export class QueryClient { ): QueryClient & A & B & C & D & E & F & G & H & I & J & K & L & M & N & O & P & Q & R; public static withExtensions( - tmClient: Tendermint34Client, + tmClient: TendermintClient, ...extensionSetups: Array> ): any { const client = new QueryClient(tmClient); @@ -506,9 +506,9 @@ export class QueryClient { return client; } - private readonly tmClient: Tendermint34Client; + private readonly tmClient: TendermintClient; - public constructor(tmClient: Tendermint34Client) { + public constructor(tmClient: TendermintClient) { this.tmClient = tmClient; } diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index ed89c7e93c..73fa8143f0 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -12,7 +12,7 @@ import { Registry, TxBodyEncodeObject, } from "@cosmjs/proto-signing"; -import { HttpEndpoint, Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { HttpEndpoint, Tendermint34Client, TendermintClient } from "@cosmjs/tendermint-rpc"; import { assert, assertDefined } from "@cosmjs/utils"; import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx"; @@ -140,7 +140,7 @@ export class SigningStargateClient extends StargateClient { } protected constructor( - tmClient: Tendermint34Client | undefined, + tmClient: TendermintClient | undefined, signer: OfflineSigner, options: SigningStargateClientOptions, ) { diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 400f59ffc9..ddcf763ab6 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -2,7 +2,12 @@ import { addCoins } from "@cosmjs/amino"; import { toHex } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; -import { HttpEndpoint, Tendermint34Client, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc"; +import { + HttpEndpoint, + Tendermint34Client, + TendermintClient, + toRfc3339WithNanoseconds, +} from "@cosmjs/tendermint-rpc"; import { assert, sleep } from "@cosmjs/utils"; import { MsgData } from "cosmjs-types/cosmos/base/abci/v1beta1/abci"; import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; @@ -171,7 +176,7 @@ export class BroadcastTxError extends Error { /** Use for testing only */ export interface PrivateStargateClient { - readonly tmClient: Tendermint34Client | undefined; + readonly tmClient: TendermintClient | undefined; } export interface StargateClientOptions { @@ -179,7 +184,7 @@ export interface StargateClientOptions { } export class StargateClient { - private readonly tmClient: Tendermint34Client | undefined; + private readonly tmClient: TendermintClient | undefined; private readonly queryClient: | (QueryClient & AuthExtension & BankExtension & StakingExtension & TxExtension) | undefined; @@ -194,7 +199,7 @@ export class StargateClient { return new StargateClient(tmClient, options); } - protected constructor(tmClient: Tendermint34Client | undefined, options: StargateClientOptions) { + protected constructor(tmClient: TendermintClient | undefined, options: StargateClientOptions) { if (tmClient) { this.tmClient = tmClient; this.queryClient = QueryClient.withExtensions( @@ -209,11 +214,11 @@ export class StargateClient { this.accountParser = accountParser; } - protected getTmClient(): Tendermint34Client | undefined { + protected getTmClient(): TendermintClient | undefined { return this.tmClient; } - protected forceGetTmClient(): Tendermint34Client { + protected forceGetTmClient(): TendermintClient { if (!this.tmClient) { throw new Error( "Tendermint client not available. You cannot use online functionality in offline mode.", diff --git a/packages/tendermint-rpc/src/index.ts b/packages/tendermint-rpc/src/index.ts index 757012a3b5..5b645eff44 100644 --- a/packages/tendermint-rpc/src/index.ts +++ b/packages/tendermint-rpc/src/index.ts @@ -97,6 +97,7 @@ export * as tendermint34 from "./tendermint34"; export { Tendermint34Client } from "./tendermint34"; export * as tendermint37 from "./tendermint37"; export { Tendermint37Client } from "./tendermint37"; +export { isTendermint34Client, isTendermint37Client, TendermintClient } from "./tendermintclient"; export { BlockIdFlag, CommitSignature, diff --git a/packages/tendermint-rpc/src/tendermintclient.ts b/packages/tendermint-rpc/src/tendermintclient.ts new file mode 100644 index 0000000000..3d07558bd4 --- /dev/null +++ b/packages/tendermint-rpc/src/tendermintclient.ts @@ -0,0 +1,13 @@ +import { Tendermint34Client } from "./tendermint34"; +import { Tendermint37Client } from "./tendermint37"; + +/** A TendermintClient is either a Tendermint34Client or a Tendermint37Client */ +export type TendermintClient = Tendermint34Client | Tendermint37Client; + +export function isTendermint34Client(client: TendermintClient): client is Tendermint34Client { + return client instanceof Tendermint34Client; +} + +export function isTendermint37Client(client: TendermintClient): client is Tendermint37Client { + return client instanceof Tendermint37Client; +} From dd350298137afbf283c9d9943985997429ad52d6 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 2 Mar 2023 14:49:34 +0100 Subject: [PATCH 12/15] Sleep between broadcast and search --- .../tendermint-rpc/src/tendermint34/tendermint34client.spec.ts | 1 + .../tendermint-rpc/src/tendermint37/tendermint37client.spec.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts index 5298e12f78..11c5d1428d 100644 --- a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts @@ -505,6 +505,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) const txRes = await client.broadcastTxCommit({ tx: tx }); expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true); + await tendermintSearchIndexUpdated(); // txSearch - you must enable the indexer when running // tendermint, else you get empty results diff --git a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts index d43b1d11ac..73458aa645 100644 --- a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts @@ -506,6 +506,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) const txRes = await client.broadcastTxCommit({ tx: tx }); expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true); + await tendermintSearchIndexUpdated(); // txSearch - you must enable the indexer when running // tendermint, else you get empty results From 7f8934cbe50ac90a06101289d08911de403b40d8 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 2 Mar 2023 17:29:51 +0100 Subject: [PATCH 13/15] Allow creating higher level clients with a given Tendermint client --- CHANGELOG.md | 6 ++++++ .../cosmwasm-stargate/src/cosmwasmclient.ts | 14 ++++++++++++++ .../src/signingcosmwasmclient.ts | 18 ++++++++++++++++++ packages/stargate/src/signingstargateclient.ts | 18 ++++++++++++++++++ packages/stargate/src/stargateclient.ts | 17 +++++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b76adeb5d..752ffcfc76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,12 @@ and this project adheres to `Tendermint34Client` or `Tendermint37Client` and `isTendermint34Client`/`isTendermint37Client` to get the specific type ([#1376]). +- @cosmjs/stargate: Add constructors `StargateClient.create` and + `SigningStargateClient.createWithSigner` to construct with a given Tendermint + client ([#1376]). +- @cosmjs/cosmwasm-stargate: Add constructors `CosmWasmClient.create` and + `SigningCosmWasmClient.createWithSigner` to construct with a given Tendermint + client ([#1376]). [#1308]: https://github.com/cosmos/cosmjs/pull/1308 [#1376]: https://github.com/cosmos/cosmjs/pull/1376 diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index 866daf4fa3..97c362008b 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -96,8 +96,22 @@ export class CosmWasmClient { private readonly codesCache = new Map(); private chainId: string | undefined; + /** + * Creates an instance by connecting to the given Tendermint RPC endpoint. + * + * For now this uses the Tendermint 0.34 client. If you need Tendermint 0.37 + * support, see `create`. + */ public static async connect(endpoint: string | HttpEndpoint): Promise { const tmClient = await Tendermint34Client.connect(endpoint); + return CosmWasmClient.create(tmClient); + } + + /** + * Creates an instance from a manually created Tendermint client. + * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. + */ + public static async create(tmClient: TendermintClient): Promise { return new CosmWasmClient(tmClient); } diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index e3843404fe..6cdad85257 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -183,12 +183,30 @@ export class SigningCosmWasmClient extends CosmWasmClient { private readonly aminoTypes: AminoTypes; private readonly gasPrice: GasPrice | undefined; + /** + * Creates an instance by connecting to the given Tendermint RPC endpoint. + * + * For now this uses the Tendermint 0.34 client. If you need Tendermint 0.37 + * support, see `createWithSigner`. + */ public static async connectWithSigner( endpoint: string | HttpEndpoint, signer: OfflineSigner, options: SigningCosmWasmClientOptions = {}, ): Promise { const tmClient = await Tendermint34Client.connect(endpoint); + return SigningCosmWasmClient.createWithSigner(tmClient, signer, options); + } + + /** + * Creates an instance from a manually created Tendermint client. + * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. + */ + public static async createWithSigner( + tmClient: TendermintClient, + signer: OfflineSigner, + options: SigningCosmWasmClientOptions = {}, + ): Promise { return new SigningCosmWasmClient(tmClient, signer, options); } diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index 73fa8143f0..a1efb39bef 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -114,12 +114,30 @@ export class SigningStargateClient extends StargateClient { private readonly aminoTypes: AminoTypes; private readonly gasPrice: GasPrice | undefined; + /** + * Creates an instance by connecting to the given Tendermint RPC endpoint. + * + * For now this uses the Tendermint 0.34 client. If you need Tendermint 0.37 + * support, see `createWithSigner`. + */ public static async connectWithSigner( endpoint: string | HttpEndpoint, signer: OfflineSigner, options: SigningStargateClientOptions = {}, ): Promise { const tmClient = await Tendermint34Client.connect(endpoint); + return SigningStargateClient.createWithSigner(tmClient, signer, options); + } + + /** + * Creates an instance from a manually created Tendermint client. + * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. + */ + public static async createWithSigner( + tmClient: TendermintClient, + signer: OfflineSigner, + options: SigningStargateClientOptions = {}, + ): Promise { return new SigningStargateClient(tmClient, signer, options); } diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index ddcf763ab6..c81ca8e256 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -191,11 +191,28 @@ export class StargateClient { private chainId: string | undefined; private readonly accountParser: AccountParser; + /** + * Creates an instance by connecting to the given Tendermint RPC endpoint. + * + * For now this uses the Tendermint 0.34 client. If you need Tendermint 0.37 + * support, see `create`. + */ public static async connect( endpoint: string | HttpEndpoint, options: StargateClientOptions = {}, ): Promise { const tmClient = await Tendermint34Client.connect(endpoint); + return StargateClient.create(tmClient, options); + } + + /** + * Creates an instance from a manually created Tendermint client. + * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. + */ + public static async create( + tmClient: TendermintClient, + options: StargateClientOptions = {}, + ): Promise { return new StargateClient(tmClient, options); } From 113158137e3eeedec8d00546b15d4cfd00e5e942 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 6 Mar 2023 11:33:38 +0100 Subject: [PATCH 14/15] Add example --- packages/cli/examples/tendermint0.37.ts | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/cli/examples/tendermint0.37.ts diff --git a/packages/cli/examples/tendermint0.37.ts b/packages/cli/examples/tendermint0.37.ts new file mode 100644 index 0000000000..3035a3cea5 --- /dev/null +++ b/packages/cli/examples/tendermint0.37.ts @@ -0,0 +1,38 @@ +import { coins, makeCosmoshubPath } from "@cosmjs/amino"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { assertIsDeliverTxSuccess, calculateFee, GasPrice, SigningStargateClient } from "@cosmjs/stargate"; +import { Tendermint37Client } from "@cosmjs/tendermint-rpc"; + +// Network config +const prefix = "wasm"; +const rpcEndpoint = "http://146.190.50.102:26657"; // or 137.184.83.82:26657 +const gasPrice = GasPrice.fromString("0.001stake"); + +// Wallet wasm16jd84xm6yerfaafvtp7s6tpetdqkpu6wxumszp +const mnemonic = "royal next favorite duck plastic august rent knee strong weather father opinion"; +const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: prefix }); +const [account] = await wallet.getAccounts(); +console.log("Signer address:", account.address); + +// Setup client. In contrast to most other examples out there, we create the Tendermint client +// explicitly. Otherwise the 0.34 client will be used. +const tmClient = await Tendermint37Client.connect(rpcEndpoint); +const client = await SigningStargateClient.createWithSigner(tmClient, wallet, { gasPrice: gasPrice }); + +// Get my balance +const balance = await client.getAllBalances(account.address); +console.log("Balance:", balance); + +// Send a transaction +const recipient = "wasm142u9fgcjdlycfcez3lw8x6x5h7rfjlnfaallkd"; +const result = await client.sendTokens( + account.address, + recipient, + coins(1, "stake"), + 1.5, // In the current testnet the default multiplier of 1.3 is not sufficient 🤷‍♂️ + "Have fun with this gift", +); +assertIsDeliverTxSuccess(result); +console.log("Successfully broadcasted:", result); + +client.disconnect(); From d107fdeddd6a4543a8b01173cf99ce0aa618a518 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 6 Mar 2023 11:39:01 +0100 Subject: [PATCH 15/15] Log Tendermint version in example --- packages/cli/examples/tendermint0.37.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/examples/tendermint0.37.ts b/packages/cli/examples/tendermint0.37.ts index 3035a3cea5..65200a2598 100644 --- a/packages/cli/examples/tendermint0.37.ts +++ b/packages/cli/examples/tendermint0.37.ts @@ -17,6 +17,8 @@ console.log("Signer address:", account.address); // Setup client. In contrast to most other examples out there, we create the Tendermint client // explicitly. Otherwise the 0.34 client will be used. const tmClient = await Tendermint37Client.connect(rpcEndpoint); +const version = (await tmClient.status()).nodeInfo.version; +console.log("Tendermint version:", version); const client = await SigningStargateClient.createWithSigner(tmClient, wallet, { gasPrice: gasPrice }); // Get my balance