From f3f23f8c9d8cb6185ec036468997809f869b6973 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 16 Aug 2023 17:48:08 +0200 Subject: [PATCH 1/9] Add Tendermint 0.38 backend --- packages/tendermint-rpc/src/testutil.spec.ts | 13 +++++++++++++ scripts/tendermint/all_start.sh | 2 ++ scripts/tendermint/all_stop.sh | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/tendermint-rpc/src/testutil.spec.ts b/packages/tendermint-rpc/src/testutil.spec.ts index 3b699c67b5..97ba2c85df 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, }, }, + 38: { + url: "localhost:11138", + version: "0.38.x", + blockTime: 500, + expected: { + chainId: /^dockerchain$/, + version: /^0\.38\.0-rc3$/, + appCreator: "Cosmoshi Netowoko", + p2pVersion: 8, + blockVersion: 11, + appVersion: 1, + }, + }, }; export const defaultInstance: TendermintInstance = tendermintInstances[34]; diff --git a/scripts/tendermint/all_start.sh b/scripts/tendermint/all_start.sh index 5c691cd829..0b7e16224f 100755 --- a/scripts/tendermint/all_start.sh +++ b/scripts/tendermint/all_start.sh @@ -8,10 +8,12 @@ command -v shellcheck >/dev/null && shellcheck "$0" declare -a TM_IMAGES TM_IMAGES[34]="tendermint/tendermint:v0.34.19" TM_IMAGES[37]="cometbft/cometbft:v0.37.0-rc3" +TM_IMAGES[38]="cometbft/cometbft:v0.38.0-rc3" declare -a TM_ROOTS TM_ROOTS[34]="/tendermint" TM_ROOTS[37]="/cometbft" +TM_ROOTS[38]="/cometbft" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/scripts/tendermint/all_stop.sh b/scripts/tendermint/all_stop.sh index a2988c4fb5..9258bf2064 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 37; do +for KEY in 34 37 38; do export TENDERMINT_NAME="tendermint-$KEY" echo "Stopping $TENDERMINT_NAME ..." From 3b1047aafdbf67927b985e3112ec201175a36b86 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 16 Aug 2023 17:49:21 +0200 Subject: [PATCH 2/9] Test Tendermint37Client against Tendermint 0.38 backend --- .../tendermint-rpc/src/tendermint37/tendermint37client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts index 9389ffa995..dfda881417 100644 --- a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts @@ -880,7 +880,7 @@ function websocketTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValue } describe("Tendermint37Client", () => { - const { url, expected } = tendermintInstances[37]; + const { url, expected } = tendermintInstances[38]; it("can connect to a given url", async () => { pendingWithoutTendermint(); From b7410530fdb3571d267b0ac2c7ae2487e2a02969 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 24 Aug 2023 16:28:52 +0200 Subject: [PATCH 3/9] Avoid running Tendermint37Client against 0.38 backend --- .../tendermint-rpc/src/tendermint37/tendermint37client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts index dfda881417..9389ffa995 100644 --- a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts @@ -880,7 +880,7 @@ function websocketTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValue } describe("Tendermint37Client", () => { - const { url, expected } = tendermintInstances[38]; + const { url, expected } = tendermintInstances[37]; it("can connect to a given url", async () => { pendingWithoutTendermint(); From 3cffb629a1cd8c92f8dd4bc50a5ff4a33128a989 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 24 Aug 2023 16:29:08 +0200 Subject: [PATCH 4/9] Create Comet38Client --- .../src/comet38/adaptor/index.ts | 13 + .../src/comet38/adaptor/requests.ts | 185 ++++ .../src/comet38/adaptor/responses.spec.ts | 124 +++ .../src/comet38/adaptor/responses.ts | 934 ++++++++++++++++++ .../src/comet38/adaptor/types.ts | 62 ++ .../src/comet38/comet38client.spec.ts | 924 +++++++++++++++++ .../src/comet38/comet38client.ts | 358 +++++++ .../src/comet38/encodings.spec.ts | 97 ++ .../tendermint-rpc/src/comet38/encodings.ts | 198 ++++ .../tendermint-rpc/src/comet38/hasher.spec.ts | 91 ++ packages/tendermint-rpc/src/comet38/hasher.ts | 80 ++ packages/tendermint-rpc/src/comet38/index.ts | 79 ++ .../src/comet38/requests.spec.ts | 41 + .../tendermint-rpc/src/comet38/requests.ts | 208 ++++ .../tendermint-rpc/src/comet38/responses.ts | 395 ++++++++ packages/tendermint-rpc/src/index.ts | 2 + 16 files changed, 3791 insertions(+) create mode 100644 packages/tendermint-rpc/src/comet38/adaptor/index.ts create mode 100644 packages/tendermint-rpc/src/comet38/adaptor/requests.ts create mode 100644 packages/tendermint-rpc/src/comet38/adaptor/responses.spec.ts create mode 100644 packages/tendermint-rpc/src/comet38/adaptor/responses.ts create mode 100644 packages/tendermint-rpc/src/comet38/adaptor/types.ts create mode 100644 packages/tendermint-rpc/src/comet38/comet38client.spec.ts create mode 100644 packages/tendermint-rpc/src/comet38/comet38client.ts create mode 100644 packages/tendermint-rpc/src/comet38/encodings.spec.ts create mode 100644 packages/tendermint-rpc/src/comet38/encodings.ts create mode 100644 packages/tendermint-rpc/src/comet38/hasher.spec.ts create mode 100644 packages/tendermint-rpc/src/comet38/hasher.ts create mode 100644 packages/tendermint-rpc/src/comet38/index.ts create mode 100644 packages/tendermint-rpc/src/comet38/requests.spec.ts create mode 100644 packages/tendermint-rpc/src/comet38/requests.ts create mode 100644 packages/tendermint-rpc/src/comet38/responses.ts diff --git a/packages/tendermint-rpc/src/comet38/adaptor/index.ts b/packages/tendermint-rpc/src/comet38/adaptor/index.ts new file mode 100644 index 0000000000..ef16c695f2 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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 adaptor38: Adaptor = { + params: Params, + responses: Responses, + hashTx: hashTx, + hashBlock: hashBlock, +}; diff --git a/packages/tendermint-rpc/src/comet38/adaptor/requests.ts b/packages/tendermint-rpc/src/comet38/adaptor/requests.ts new file mode 100644 index 0000000000..ae2bd709d8 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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 { + /** base64 encoded */ + readonly hash: string; + readonly prove?: boolean; +} +function encodeTxParams(params: requests.TxParams): RpcTxParams { + return { + hash: toBase64(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/comet38/adaptor/responses.spec.ts b/packages/tendermint-rpc/src/comet38/adaptor/responses.spec.ts new file mode 100644 index 0000000000..c85b753328 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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/comet38/adaptor/responses.ts b/packages/tendermint-rpc/src/comet38/adaptor/responses.ts new file mode 100644 index 0000000000..0791539f40 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/adaptor/responses.ts @@ -0,0 +1,934 @@ +/* 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 tx_result?: RpcTxData; +} + +function decodeBroadcastTxCommit(data: RpcBroadcastTxCommitResponse): responses.BroadcastTxCommitResponse { + const txResult = data.tx_result ? decodeTxData(data.tx_result) : undefined; + return { + height: apiToSmallInt(data.height), + hash: fromHex(assertNotEmpty(data.hash)), + checkTx: decodeTxData(assertObject(data.check_tx)), + deliverTx: txResult, + txResult: txResult, + }; +} + +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 earliest_app_hash: string; + /** hex encoded */ + readonly earliest_block_hash: string; + readonly earliest_block_height: string; + readonly earliest_block_time: string; + /** 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 { + const earliestBlockHeight = data.earliest_block_height + ? apiToSmallInt(data.earliest_block_height) + : undefined; + const earliestBlockTime = data.earliest_block_time + ? fromRfc3339WithNanoseconds(data.earliest_block_time) + : undefined; + + return { + earliestAppHash: data.earliest_app_hash ? fromHex(data.earliest_app_hash) : undefined, + earliestBlockHash: data.earliest_block_hash ? fromHex(data.earliest_block_hash) : undefined, + earliestBlockHeight: earliestBlockHeight || undefined, + earliestBlockTime: earliestBlockTime?.getTime() ? earliestBlockTime : undefined, + 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/comet38/adaptor/types.ts b/packages/tendermint-rpc/src/comet38/adaptor/types.ts new file mode 100644 index 0000000000..1558df6d8e --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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/comet38/comet38client.spec.ts b/packages/tendermint-rpc/src/comet38/comet38client.spec.ts new file mode 100644 index 0000000000..3c83e13076 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/comet38client.spec.ts @@ -0,0 +1,924 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { toAscii, toHex } from "@cosmjs/encoding"; +import { firstEvent, toListPromise } from "@cosmjs/stream"; +import { assert, 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 { adaptor38 } from "./adaptor"; +import { Comet38Client } from "./comet38client"; +import { buildQuery } from "./requests"; +import * as responses from "./responses"; + +function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues): void { + describe("create", () => { + it("can auto-discover Tendermint version and communicate", async () => { + pendingWithoutTendermint(); + const client = await Comet38Client.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 Comet38Client.create(rpcFactory()); + expect(await client.abciInfo()).toBeTruthy(); + client.disconnect(); + }); + }); + + it("can get genesis", async () => { + pendingWithoutTendermint(); + const client = await Comet38Client.create(rpcFactory()); + const genesis = await client.genesis(); + expect(genesis).toBeTruthy(); + client.disconnect(); + }); + + describe("broadcastTxCommit", () => { + it("can broadcast a transaction", async () => { + pendingWithoutTendermint(); + const client = await Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.create(rpcFactory()); + const tx = buildKvTx(randomString(), randomString()); + const calculatedTxHash = adaptor38.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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); + expect(status.syncInfo.latestBlockTime).toBeInstanceOf(Date); + if (status.syncInfo.earliestBlockHeight) { + expect(status.syncInfo.earliestBlockHeight).toBeGreaterThanOrEqual(1); + expect(status.syncInfo.earliestBlockHeight).toBeLessThanOrEqual(status.syncInfo.latestBlockHeight); + } + if (status.syncInfo.earliestBlockTime) { + expect(status.syncInfo.earliestBlockTime).toBeInstanceOf(Date); + expect(status.syncInfo.earliestBlockTime.getTime()).toBeLessThanOrEqual( + status.syncInfo.latestBlockTime.getTime(), + ); + } + + // validator info + expect(status.validatorInfo.pubkey).toBeTruthy(); + expect(status.validatorInfo.votingPower).toBeGreaterThan(0); + + client.disconnect(); + }); + }); + + describe("numUnconfirmedTxs", () => { + it("works", async () => { + pendingWithoutTendermint(); + const client = await Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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(); + + // 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 txKey = randomString(); // a key used for multiple transactions + let tx1: Uint8Array | undefined; + let broadcast1: responses.BroadcastTxCommitResponse | undefined; + + beforeAll(async () => { + if (tendermintEnabled()) { + const client = await Comet38Client.create(rpcFactory()); + + // eslint-disable-next-line no-inner-declarations + async function sendTx(): Promise<[Uint8Array, responses.BroadcastTxCommitResponse]> { + const me = randomString(); + 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).toEqual(32); + return [tx, txRes]; + } + + // send 3 txs + [tx1, broadcast1] = await sendTx(); + await sendTx(); + await sendTx(); + + client.disconnect(); + + await tendermintSearchIndexUpdated(); + } + }); + + it("finds a single tx by hash", async () => { + pendingWithoutTendermint(); + assert(tx1 && broadcast1); + const client = await Comet38Client.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 Comet38Client.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); + await tendermintSearchIndexUpdated(); + + // 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". + // 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 Comet38Client.create(rpcFactory()); + + const query = buildQuery({ tags: [{ key: "app.key", value: txKey }] }); + + const result = await client.txSearch({ query: query }); + + expect(result.totalCount).toEqual(3); + result.txs.slice(1).reduce((lastHeight, { height }) => { + expect(height).toBeGreaterThanOrEqual(lastHeight); + return height; + }, result.txs[0].height); + + client.disconnect(); + }); + + it("can set the order", async () => { + pendingWithoutTendermint(); + const client = await Comet38Client.create(rpcFactory()); + + 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" }); + + 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 Comet38Client.create(rpcFactory()); + + 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 }); + 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 Comet38Client.create(rpcFactory()); + + 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 + expect(sall.txs[1].height).toEqual(sall.txs[0].height + 1); + expect(sall.txs[2].height).toEqual(sall.txs[1].height + 1); + + 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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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 Comet38Client.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("Comet38Client", () => { + const { url, expected } = tendermintInstances[38]; + + it("can connect to a given url", async () => { + pendingWithoutTendermint(); + + // default connection + { + const client = await Comet38Client.connect(url); + const info = await client.abciInfo(); + expect(info).toBeTruthy(); + client.disconnect(); + } + + // http connection + { + const client = await Comet38Client.connect("http://" + url); + const info = await client.abciInfo(); + expect(info).toBeTruthy(); + client.disconnect(); + } + + // ws connection + { + const client = await Comet38Client.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/comet38/comet38client.ts b/packages/tendermint-rpc/src/comet38/comet38client.ts new file mode 100644 index 0000000000..f6c4bc88de --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/comet38client.ts @@ -0,0 +1,358 @@ +import { Stream } from "xstream"; + +import { createJsonRpcRequest } from "../jsonrpc"; +import { + HttpClient, + HttpEndpoint, + instanceOfRpcStreamingClient, + RpcClient, + SubscriptionEvent, + WebsocketClient, +} from "../rpcclients"; +import { adaptor38, Decoder, Encoder, Params, Responses } from "./adaptor"; +import * as requests from "./requests"; +import * as responses from "./responses"; + +export class Comet38Client { + /** + * 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 { + let rpcClient: RpcClient; + if (typeof endpoint === "object") { + rpcClient = new HttpClient(endpoint); + } else { + const useHttp = endpoint.startsWith("http://") || endpoint.startsWith("https://"); + rpcClient = useHttp ? new HttpClient(endpoint) : new WebsocketClient(endpoint); + } + + // 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 Comet38Client.create(rpcClient); + } + + /** + * Creates a new Tendermint client given an RPC client. + */ + public static async create(rpcClient: RpcClient): Promise { + return new Comet38Client(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 = adaptor38.params; + this.r = adaptor38.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/comet38/encodings.spec.ts b/packages/tendermint-rpc/src/comet38/encodings.spec.ts new file mode 100644 index 0000000000..b4921d3b73 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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/comet38/encodings.ts b/packages/tendermint-rpc/src/comet38/encodings.ts new file mode 100644 index 0000000000..037b506898 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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/comet38/hasher.spec.ts b/packages/tendermint-rpc/src/comet38/hasher.spec.ts new file mode 100644 index 0000000000..c66448757c --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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/comet38/hasher.ts b/packages/tendermint-rpc/src/comet38/hasher.ts new file mode 100644 index 0000000000..a02d2f814f --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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/comet38/index.ts b/packages/tendermint-rpc/src/comet38/index.ts new file mode 100644 index 0000000000..9dacdf0faf --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/index.ts @@ -0,0 +1,79 @@ +// Note: all exports in this module are publicly available via +// `import { comet38 } from "@cosmjs/tendermint-rpc"` + +export { Comet38Client } from "./comet38client"; +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"; diff --git a/packages/tendermint-rpc/src/comet38/requests.spec.ts b/packages/tendermint-rpc/src/comet38/requests.spec.ts new file mode 100644 index 0000000000..f2134ea8d6 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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/comet38/requests.ts b/packages/tendermint-rpc/src/comet38/requests.ts new file mode 100644 index 0000000000..e4f131c5c0 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/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/comet38/responses.ts b/packages/tendermint-rpc/src/comet38/responses.ts new file mode 100644 index 0000000000..5d7f83f1c5 --- /dev/null +++ b/packages/tendermint-rpc/src/comet38/responses.ts @@ -0,0 +1,395 @@ +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; + /** @deprecated use txResult. Containes the same data as txResult for now */ + readonly deliverTx?: TxData; + readonly txResult?: 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 earliestAppHash?: Uint8Array; + readonly earliestBlockHash?: Uint8Array; + readonly earliestBlockHeight?: number; + readonly earliestBlockTime?: ReadonlyDate; + 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/index.ts b/packages/tendermint-rpc/src/index.ts index 5b645eff44..4c1e3026cd 100644 --- a/packages/tendermint-rpc/src/index.ts +++ b/packages/tendermint-rpc/src/index.ts @@ -15,6 +15,8 @@ export { // The public Tendermint34Client.create constructor allows manually choosing an RpcClient. // This is currently the only way to switch to the HttpBatchClient (which may become default at some point). // Due to this API, we make RPC client implementations public. +export * as comet38 from "./comet38"; +export { Comet38Client } from "./comet38"; export { HttpBatchClient, HttpBatchClientOptions, From 4adfcb0f6af524b1cffab98608d8a83c69eef1bf Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 24 Aug 2023 16:32:57 +0200 Subject: [PATCH 5/9] Remove hashTx/hashBlock from Adaptor abstraction --- packages/tendermint-rpc/src/comet38/adaptor/index.ts | 3 --- packages/tendermint-rpc/src/comet38/adaptor/types.ts | 2 -- packages/tendermint-rpc/src/comet38/comet38client.spec.ts | 4 ++-- packages/tendermint-rpc/src/tendermint34/adaptor/index.ts | 3 --- packages/tendermint-rpc/src/tendermint34/adaptor/types.ts | 2 -- .../src/tendermint34/tendermint34client.spec.ts | 4 ++-- packages/tendermint-rpc/src/tendermint37/adaptor/index.ts | 3 --- packages/tendermint-rpc/src/tendermint37/adaptor/types.ts | 2 -- .../src/tendermint37/tendermint37client.spec.ts | 4 ++-- 9 files changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/tendermint-rpc/src/comet38/adaptor/index.ts b/packages/tendermint-rpc/src/comet38/adaptor/index.ts index ef16c695f2..f6e2998492 100644 --- a/packages/tendermint-rpc/src/comet38/adaptor/index.ts +++ b/packages/tendermint-rpc/src/comet38/adaptor/index.ts @@ -1,4 +1,3 @@ -import { hashBlock, hashTx } from "../hasher"; import { Params } from "./requests"; import { Responses } from "./responses"; import { Adaptor } from "./types"; @@ -8,6 +7,4 @@ export { Decoder, Encoder, Params, Responses } from "./types"; export const adaptor38: Adaptor = { params: Params, responses: Responses, - hashTx: hashTx, - hashBlock: hashBlock, }; diff --git a/packages/tendermint-rpc/src/comet38/adaptor/types.ts b/packages/tendermint-rpc/src/comet38/adaptor/types.ts index 1558df6d8e..1ce9ea1c17 100644 --- a/packages/tendermint-rpc/src/comet38/adaptor/types.ts +++ b/packages/tendermint-rpc/src/comet38/adaptor/types.ts @@ -7,8 +7,6 @@ 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 diff --git a/packages/tendermint-rpc/src/comet38/comet38client.spec.ts b/packages/tendermint-rpc/src/comet38/comet38client.spec.ts index 3c83e13076..c949036a4b 100644 --- a/packages/tendermint-rpc/src/comet38/comet38client.spec.ts +++ b/packages/tendermint-rpc/src/comet38/comet38client.spec.ts @@ -16,8 +16,8 @@ import { tendermintInstances, tendermintSearchIndexUpdated, } from "../testutil.spec"; -import { adaptor38 } from "./adaptor"; import { Comet38Client } from "./comet38client"; +import { hashTx } from "./hasher"; import { buildQuery } from "./requests"; import * as responses from "./responses"; @@ -100,7 +100,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Comet38Client.create(rpcFactory()); const tx = buildKvTx(randomString(), randomString()); - const calculatedTxHash = adaptor38.hashTx(tx); + const calculatedTxHash = hashTx(tx); const response = await client.broadcastTxCommit({ tx: tx }); expect(response.hash).toEqual(calculatedTxHash); diff --git a/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts b/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts index 4ef0b8cd4e..5e63cb3d39 100644 --- a/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts +++ b/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts @@ -1,4 +1,3 @@ -import { hashBlock, hashTx } from "../hasher"; import { Params } from "./requests"; import { Responses } from "./responses"; import { Adaptor } from "./types"; @@ -8,6 +7,4 @@ export { Decoder, Encoder, Params, Responses } from "./types"; export const adaptor34: Adaptor = { params: Params, responses: Responses, - hashTx: hashTx, - hashBlock: hashBlock, }; diff --git a/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts b/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts index 1558df6d8e..1ce9ea1c17 100644 --- a/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts +++ b/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts @@ -7,8 +7,6 @@ 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 diff --git a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts index a3d64e5e77..eca45de830 100644 --- a/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint34/tendermint34client.spec.ts @@ -16,7 +16,7 @@ import { tendermintInstances, tendermintSearchIndexUpdated, } from "../testutil.spec"; -import { adaptor34 } from "./adaptor"; +import { hashTx } from "./hasher"; import { buildQuery } from "./requests"; import * as responses from "./responses"; import { Tendermint34Client } from "./tendermint34client"; @@ -100,7 +100,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint34Client.create(rpcFactory()); const tx = buildKvTx(randomString(), randomString()); - const calculatedTxHash = adaptor34.hashTx(tx); + const calculatedTxHash = hashTx(tx); const response = await client.broadcastTxCommit({ tx: tx }); expect(response.hash).toEqual(calculatedTxHash); diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts index 0fa5031aaa..86739c6611 100644 --- a/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts @@ -1,4 +1,3 @@ -import { hashBlock, hashTx } from "../hasher"; import { Params } from "./requests"; import { Responses } from "./responses"; import { Adaptor } from "./types"; @@ -8,6 +7,4 @@ 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/types.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts index 1558df6d8e..1ce9ea1c17 100644 --- a/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts @@ -7,8 +7,6 @@ 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 diff --git a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts index 9389ffa995..36732b31e7 100644 --- a/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.spec.ts @@ -16,7 +16,7 @@ import { tendermintInstances, tendermintSearchIndexUpdated, } from "../testutil.spec"; -import { adaptor37 } from "./adaptor"; +import { hashTx } from "./hasher"; import { buildQuery } from "./requests"; import * as responses from "./responses"; import { Tendermint37Client } from "./tendermint37client"; @@ -100,7 +100,7 @@ function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues) pendingWithoutTendermint(); const client = await Tendermint37Client.create(rpcFactory()); const tx = buildKvTx(randomString(), randomString()); - const calculatedTxHash = adaptor37.hashTx(tx); + const calculatedTxHash = hashTx(tx); const response = await client.broadcastTxCommit({ tx: tx }); expect(response.hash).toEqual(calculatedTxHash); From 3124187177ef2269915e68bd5a90fe6f02214725 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 24 Aug 2023 16:46:19 +0200 Subject: [PATCH 6/9] Remove Adaptor abstractions --- CHANGELOG.md | 5 ++ .../src/comet38/adaptor/index.ts | 13 ++--- .../src/comet38/adaptor/responses.spec.ts | 2 +- .../src/comet38/adaptor/types.ts | 50 ------------------- .../src/comet38/comet38client.ts | 48 ++++++++---------- .../src/tendermint34/adaptor/index.ts | 13 ++--- .../tendermint34/adaptor/responses.spec.ts | 2 +- .../src/tendermint34/adaptor/types.ts | 50 ------------------- .../src/tendermint34/tendermint34client.ts | 48 ++++++++---------- .../src/tendermint37/adaptor/index.ts | 13 ++--- .../tendermint37/adaptor/responses.spec.ts | 2 +- .../src/tendermint37/adaptor/types.ts | 50 ------------------- .../src/tendermint37/tendermint37client.ts | 48 ++++++++---------- 13 files changed, 83 insertions(+), 261 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 351861b0c4..c842224023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to ## [Unreleased] +### Changed + +- @cosmjs/tendermint-rpc: Remove `Adaptor` abstractions which are not needed + anymore by haing a dedicated client for each backend. + ## [0.31.1] - 2023-08-21 ### Fixed diff --git a/packages/tendermint-rpc/src/comet38/adaptor/index.ts b/packages/tendermint-rpc/src/comet38/adaptor/index.ts index f6e2998492..d592b38acb 100644 --- a/packages/tendermint-rpc/src/comet38/adaptor/index.ts +++ b/packages/tendermint-rpc/src/comet38/adaptor/index.ts @@ -1,10 +1,3 @@ -import { Params } from "./requests"; -import { Responses } from "./responses"; -import { Adaptor } from "./types"; - -export { Decoder, Encoder, Params, Responses } from "./types"; - -export const adaptor38: Adaptor = { - params: Params, - responses: Responses, -}; +export { Params } from "./requests"; +export { Responses } from "./responses"; +export { Decoder, Encoder } from "./types"; diff --git a/packages/tendermint-rpc/src/comet38/adaptor/responses.spec.ts b/packages/tendermint-rpc/src/comet38/adaptor/responses.spec.ts index c85b753328..38baf246c9 100644 --- a/packages/tendermint-rpc/src/comet38/adaptor/responses.spec.ts +++ b/packages/tendermint-rpc/src/comet38/adaptor/responses.spec.ts @@ -3,7 +3,7 @@ import { fromBase64, fromHex } from "@cosmjs/encoding"; import { decodeEvent, decodeValidatorGenesis, decodeValidatorInfo, decodeValidatorUpdate } from "./responses"; -describe("Adaptor Responses", () => { +describe("Responses", () => { describe("decodeEvent", () => { it("works with attributes", () => { // from https://rpc.mainnet-1.tgrade.confio.run/tx?hash=0x2C44715748022DB2FB5F40105383719BFCFCEE51DBC02FF4088BE3F5924CD7BF diff --git a/packages/tendermint-rpc/src/comet38/adaptor/types.ts b/packages/tendermint-rpc/src/comet38/adaptor/types.ts index 1ce9ea1c17..f9bf1d11ab 100644 --- a/packages/tendermint-rpc/src/comet38/adaptor/types.ts +++ b/packages/tendermint-rpc/src/comet38/adaptor/types.ts @@ -1,60 +1,10 @@ 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; -} - // 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/comet38/comet38client.ts b/packages/tendermint-rpc/src/comet38/comet38client.ts index f6c4bc88de..f8037aa240 100644 --- a/packages/tendermint-rpc/src/comet38/comet38client.ts +++ b/packages/tendermint-rpc/src/comet38/comet38client.ts @@ -9,7 +9,7 @@ import { SubscriptionEvent, WebsocketClient, } from "../rpcclients"; -import { adaptor38, Decoder, Encoder, Params, Responses } from "./adaptor"; +import { Decoder, Encoder, Params, Responses } from "./adaptor"; import * as requests from "./requests"; import * as responses from "./responses"; @@ -61,16 +61,12 @@ export class Comet38Client { } 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 = adaptor38.params; - this.r = adaptor38.responses; } public disconnect(): void { @@ -79,17 +75,17 @@ export class Comet38Client { public async abciInfo(): Promise { const query: requests.AbciInfoRequest = { method: requests.Method.AbciInfo }; - return this.doCall(query, this.p.encodeAbciInfo, this.r.decodeAbciInfo); + return this.doCall(query, Params.encodeAbciInfo, Responses.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); + return this.doCall(query, Params.encodeAbciQuery, Responses.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); + return this.doCall(query, Params.encodeBlock, Responses.decodeBlock); } public async blockResults(height?: number): Promise { @@ -97,7 +93,7 @@ export class Comet38Client { method: requests.Method.BlockResults, params: { height: height }, }; - return this.doCall(query, this.p.encodeBlockResults, this.r.decodeBlockResults); + return this.doCall(query, Params.encodeBlockResults, Responses.decodeBlockResults); } /** @@ -110,7 +106,7 @@ export class Comet38Client { */ 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); + const resp = await this.doCall(query, Params.encodeBlockSearch, Responses.decodeBlockSearch); return { ...resp, // make sure we sort by height, as tendermint may be sorting by string value of the height @@ -161,7 +157,7 @@ export class Comet38Client { maxHeight: maxHeight, }, }; - return this.doCall(query, this.p.encodeBlockchain, this.r.decodeBlockchain); + return this.doCall(query, Params.encodeBlockchain, Responses.decodeBlockchain); } /** @@ -173,7 +169,7 @@ export class Comet38Client { params: requests.BroadcastTxParams, ): Promise { const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxSync }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxSync); + return this.doCall(query, Params.encodeBroadcastTx, Responses.decodeBroadcastTxSync); } /** @@ -185,7 +181,7 @@ export class Comet38Client { params: requests.BroadcastTxParams, ): Promise { const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxAsync }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxAsync); + return this.doCall(query, Params.encodeBroadcastTx, Responses.decodeBroadcastTxAsync); } /** @@ -197,32 +193,32 @@ export class Comet38Client { params: requests.BroadcastTxParams, ): Promise { const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxCommit }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxCommit); + return this.doCall(query, Params.encodeBroadcastTx, Responses.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); + return this.doCall(query, Params.encodeCommit, Responses.decodeCommit); } public async genesis(): Promise { const query: requests.GenesisRequest = { method: requests.Method.Genesis }; - return this.doCall(query, this.p.encodeGenesis, this.r.decodeGenesis); + return this.doCall(query, Params.encodeGenesis, Responses.decodeGenesis); } public async health(): Promise { const query: requests.HealthRequest = { method: requests.Method.Health }; - return this.doCall(query, this.p.encodeHealth, this.r.decodeHealth); + return this.doCall(query, Params.encodeHealth, Responses.decodeHealth); } public async numUnconfirmedTxs(): Promise { const query: requests.NumUnconfirmedTxsRequest = { method: requests.Method.NumUnconfirmedTxs }; - return this.doCall(query, this.p.encodeNumUnconfirmedTxs, this.r.decodeNumUnconfirmedTxs); + return this.doCall(query, Params.encodeNumUnconfirmedTxs, Responses.decodeNumUnconfirmedTxs); } public async status(): Promise { const query: requests.StatusRequest = { method: requests.Method.Status }; - return this.doCall(query, this.p.encodeStatus, this.r.decodeStatus); + return this.doCall(query, Params.encodeStatus, Responses.decodeStatus); } public subscribeNewBlock(): Stream { @@ -230,7 +226,7 @@ export class Comet38Client { method: requests.Method.Subscribe, query: { type: requests.SubscriptionEventType.NewBlock }, }; - return this.subscribe(request, this.r.decodeNewBlockEvent); + return this.subscribe(request, Responses.decodeNewBlockEvent); } public subscribeNewBlockHeader(): Stream { @@ -238,7 +234,7 @@ export class Comet38Client { method: requests.Method.Subscribe, query: { type: requests.SubscriptionEventType.NewBlockHeader }, }; - return this.subscribe(request, this.r.decodeNewBlockHeaderEvent); + return this.subscribe(request, Responses.decodeNewBlockHeaderEvent); } public subscribeTx(query?: string): Stream { @@ -249,7 +245,7 @@ export class Comet38Client { raw: query, }, }; - return this.subscribe(request, this.r.decodeTxEvent); + return this.subscribe(request, Responses.decodeTxEvent); } /** @@ -259,7 +255,7 @@ export class Comet38Client { */ 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); + return this.doCall(query, Params.encodeTx, Responses.decodeTx); } /** @@ -269,7 +265,7 @@ export class Comet38Client { */ 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); + return this.doCall(query, Params.encodeTxSearch, Responses.decodeTxSearch); } // this should paginate through all txSearch options to ensure it returns all results. @@ -300,7 +296,7 @@ export class Comet38Client { method: requests.Method.Validators, params: params, }; - return this.doCall(query, this.p.encodeValidators, this.r.decodeValidators); + return this.doCall(query, Params.encodeValidators, Responses.decodeValidators); } public async validatorsAll(height?: number): Promise { @@ -349,7 +345,7 @@ export class Comet38Client { throw new Error("This RPC client type cannot subscribe to events"); } - const req = this.p.encodeSubscribe(request); + const req = Params.encodeSubscribe(request); const eventStream = this.client.listen(req); return eventStream.map((event) => { return decode(event); diff --git a/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts b/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts index 5e63cb3d39..d592b38acb 100644 --- a/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts +++ b/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts @@ -1,10 +1,3 @@ -import { Params } from "./requests"; -import { Responses } from "./responses"; -import { Adaptor } from "./types"; - -export { Decoder, Encoder, Params, Responses } from "./types"; - -export const adaptor34: Adaptor = { - params: Params, - responses: Responses, -}; +export { Params } from "./requests"; +export { Responses } from "./responses"; +export { Decoder, Encoder } from "./types"; diff --git a/packages/tendermint-rpc/src/tendermint34/adaptor/responses.spec.ts b/packages/tendermint-rpc/src/tendermint34/adaptor/responses.spec.ts index 7de09654d0..056cdc3d02 100644 --- a/packages/tendermint-rpc/src/tendermint34/adaptor/responses.spec.ts +++ b/packages/tendermint-rpc/src/tendermint34/adaptor/responses.spec.ts @@ -3,7 +3,7 @@ import { fromBase64, fromHex, toUtf8 } from "@cosmjs/encoding"; import { decodeEvent, decodeValidatorGenesis, decodeValidatorInfo, decodeValidatorUpdate } from "./responses"; -describe("Adaptor Responses", () => { +describe("Responses", () => { describe("decodeEvent", () => { it("works with attributes", () => { // from https://rpc.mainnet-1.tgrade.confio.run/tx?hash=0x2C44715748022DB2FB5F40105383719BFCFCEE51DBC02FF4088BE3F5924CD7BF diff --git a/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts b/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts index 1ce9ea1c17..f9bf1d11ab 100644 --- a/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts +++ b/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts @@ -1,60 +1,10 @@ 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; -} - // 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/tendermint34/tendermint34client.ts b/packages/tendermint-rpc/src/tendermint34/tendermint34client.ts index c0538bfbba..701960de43 100644 --- a/packages/tendermint-rpc/src/tendermint34/tendermint34client.ts +++ b/packages/tendermint-rpc/src/tendermint34/tendermint34client.ts @@ -9,7 +9,7 @@ import { SubscriptionEvent, WebsocketClient, } from "../rpcclients"; -import { adaptor34, Decoder, Encoder, Params, Responses } from "./adaptor"; +import { Decoder, Encoder, Params, Responses } from "./adaptor"; import * as requests from "./requests"; import * as responses from "./responses"; @@ -61,16 +61,12 @@ export class Tendermint34Client { } 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 = adaptor34.params; - this.r = adaptor34.responses; } public disconnect(): void { @@ -79,17 +75,17 @@ export class Tendermint34Client { public async abciInfo(): Promise { const query: requests.AbciInfoRequest = { method: requests.Method.AbciInfo }; - return this.doCall(query, this.p.encodeAbciInfo, this.r.decodeAbciInfo); + return this.doCall(query, Params.encodeAbciInfo, Responses.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); + return this.doCall(query, Params.encodeAbciQuery, Responses.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); + return this.doCall(query, Params.encodeBlock, Responses.decodeBlock); } public async blockResults(height?: number): Promise { @@ -97,7 +93,7 @@ export class Tendermint34Client { method: requests.Method.BlockResults, params: { height: height }, }; - return this.doCall(query, this.p.encodeBlockResults, this.r.decodeBlockResults); + return this.doCall(query, Params.encodeBlockResults, Responses.decodeBlockResults); } /** @@ -110,7 +106,7 @@ export class Tendermint34Client { */ 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); + const resp = await this.doCall(query, Params.encodeBlockSearch, Responses.decodeBlockSearch); return { ...resp, // make sure we sort by height, as tendermint may be sorting by string value of the height @@ -161,7 +157,7 @@ export class Tendermint34Client { maxHeight: maxHeight, }, }; - return this.doCall(query, this.p.encodeBlockchain, this.r.decodeBlockchain); + return this.doCall(query, Params.encodeBlockchain, Responses.decodeBlockchain); } /** @@ -173,7 +169,7 @@ export class Tendermint34Client { params: requests.BroadcastTxParams, ): Promise { const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxSync }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxSync); + return this.doCall(query, Params.encodeBroadcastTx, Responses.decodeBroadcastTxSync); } /** @@ -185,7 +181,7 @@ export class Tendermint34Client { params: requests.BroadcastTxParams, ): Promise { const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxAsync }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxAsync); + return this.doCall(query, Params.encodeBroadcastTx, Responses.decodeBroadcastTxAsync); } /** @@ -197,32 +193,32 @@ export class Tendermint34Client { params: requests.BroadcastTxParams, ): Promise { const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxCommit }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxCommit); + return this.doCall(query, Params.encodeBroadcastTx, Responses.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); + return this.doCall(query, Params.encodeCommit, Responses.decodeCommit); } public async genesis(): Promise { const query: requests.GenesisRequest = { method: requests.Method.Genesis }; - return this.doCall(query, this.p.encodeGenesis, this.r.decodeGenesis); + return this.doCall(query, Params.encodeGenesis, Responses.decodeGenesis); } public async health(): Promise { const query: requests.HealthRequest = { method: requests.Method.Health }; - return this.doCall(query, this.p.encodeHealth, this.r.decodeHealth); + return this.doCall(query, Params.encodeHealth, Responses.decodeHealth); } public async numUnconfirmedTxs(): Promise { const query: requests.NumUnconfirmedTxsRequest = { method: requests.Method.NumUnconfirmedTxs }; - return this.doCall(query, this.p.encodeNumUnconfirmedTxs, this.r.decodeNumUnconfirmedTxs); + return this.doCall(query, Params.encodeNumUnconfirmedTxs, Responses.decodeNumUnconfirmedTxs); } public async status(): Promise { const query: requests.StatusRequest = { method: requests.Method.Status }; - return this.doCall(query, this.p.encodeStatus, this.r.decodeStatus); + return this.doCall(query, Params.encodeStatus, Responses.decodeStatus); } public subscribeNewBlock(): Stream { @@ -230,7 +226,7 @@ export class Tendermint34Client { method: requests.Method.Subscribe, query: { type: requests.SubscriptionEventType.NewBlock }, }; - return this.subscribe(request, this.r.decodeNewBlockEvent); + return this.subscribe(request, Responses.decodeNewBlockEvent); } public subscribeNewBlockHeader(): Stream { @@ -238,7 +234,7 @@ export class Tendermint34Client { method: requests.Method.Subscribe, query: { type: requests.SubscriptionEventType.NewBlockHeader }, }; - return this.subscribe(request, this.r.decodeNewBlockHeaderEvent); + return this.subscribe(request, Responses.decodeNewBlockHeaderEvent); } public subscribeTx(query?: string): Stream { @@ -249,7 +245,7 @@ export class Tendermint34Client { raw: query, }, }; - return this.subscribe(request, this.r.decodeTxEvent); + return this.subscribe(request, Responses.decodeTxEvent); } /** @@ -259,7 +255,7 @@ export class Tendermint34Client { */ 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); + return this.doCall(query, Params.encodeTx, Responses.decodeTx); } /** @@ -269,7 +265,7 @@ export class Tendermint34Client { */ 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); + return this.doCall(query, Params.encodeTxSearch, Responses.decodeTxSearch); } // this should paginate through all txSearch options to ensure it returns all results. @@ -300,7 +296,7 @@ export class Tendermint34Client { method: requests.Method.Validators, params: params, }; - return this.doCall(query, this.p.encodeValidators, this.r.decodeValidators); + return this.doCall(query, Params.encodeValidators, Responses.decodeValidators); } public async validatorsAll(height?: number): Promise { @@ -349,7 +345,7 @@ export class Tendermint34Client { throw new Error("This RPC client type cannot subscribe to events"); } - const req = this.p.encodeSubscribe(request); + const req = Params.encodeSubscribe(request); const eventStream = this.client.listen(req); return eventStream.map((event) => { return decode(event); diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts index 86739c6611..d592b38acb 100644 --- a/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts @@ -1,10 +1,3 @@ -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, -}; +export { Params } from "./requests"; +export { Responses } from "./responses"; +export { Decoder, Encoder } from "./types"; diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/responses.spec.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/responses.spec.ts index c85b753328..38baf246c9 100644 --- a/packages/tendermint-rpc/src/tendermint37/adaptor/responses.spec.ts +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/responses.spec.ts @@ -3,7 +3,7 @@ import { fromBase64, fromHex } from "@cosmjs/encoding"; import { decodeEvent, decodeValidatorGenesis, decodeValidatorInfo, decodeValidatorUpdate } from "./responses"; -describe("Adaptor Responses", () => { +describe("Responses", () => { describe("decodeEvent", () => { it("works with attributes", () => { // from https://rpc.mainnet-1.tgrade.confio.run/tx?hash=0x2C44715748022DB2FB5F40105383719BFCFCEE51DBC02FF4088BE3F5924CD7BF diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts index 1ce9ea1c17..f9bf1d11ab 100644 --- a/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts @@ -1,60 +1,10 @@ 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; -} - // 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/tendermint37client.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts index 9bbbc3117c..9d89708e2a 100644 --- a/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts @@ -9,7 +9,7 @@ import { SubscriptionEvent, WebsocketClient, } from "../rpcclients"; -import { adaptor37, Decoder, Encoder, Params, Responses } from "./adaptor"; +import { Decoder, Encoder, Params, Responses } from "./adaptor"; import * as requests from "./requests"; import * as responses from "./responses"; @@ -61,16 +61,12 @@ export class Tendermint37Client { } 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 { @@ -79,17 +75,17 @@ export class Tendermint37Client { public async abciInfo(): Promise { const query: requests.AbciInfoRequest = { method: requests.Method.AbciInfo }; - return this.doCall(query, this.p.encodeAbciInfo, this.r.decodeAbciInfo); + return this.doCall(query, Params.encodeAbciInfo, Responses.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); + return this.doCall(query, Params.encodeAbciQuery, Responses.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); + return this.doCall(query, Params.encodeBlock, Responses.decodeBlock); } public async blockResults(height?: number): Promise { @@ -97,7 +93,7 @@ export class Tendermint37Client { method: requests.Method.BlockResults, params: { height: height }, }; - return this.doCall(query, this.p.encodeBlockResults, this.r.decodeBlockResults); + return this.doCall(query, Params.encodeBlockResults, Responses.decodeBlockResults); } /** @@ -110,7 +106,7 @@ export class Tendermint37Client { */ 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); + const resp = await this.doCall(query, Params.encodeBlockSearch, Responses.decodeBlockSearch); return { ...resp, // make sure we sort by height, as tendermint may be sorting by string value of the height @@ -161,7 +157,7 @@ export class Tendermint37Client { maxHeight: maxHeight, }, }; - return this.doCall(query, this.p.encodeBlockchain, this.r.decodeBlockchain); + return this.doCall(query, Params.encodeBlockchain, Responses.decodeBlockchain); } /** @@ -173,7 +169,7 @@ export class Tendermint37Client { params: requests.BroadcastTxParams, ): Promise { const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxSync }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxSync); + return this.doCall(query, Params.encodeBroadcastTx, Responses.decodeBroadcastTxSync); } /** @@ -185,7 +181,7 @@ export class Tendermint37Client { params: requests.BroadcastTxParams, ): Promise { const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxAsync }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxAsync); + return this.doCall(query, Params.encodeBroadcastTx, Responses.decodeBroadcastTxAsync); } /** @@ -197,32 +193,32 @@ export class Tendermint37Client { params: requests.BroadcastTxParams, ): Promise { const query: requests.BroadcastTxRequest = { params: params, method: requests.Method.BroadcastTxCommit }; - return this.doCall(query, this.p.encodeBroadcastTx, this.r.decodeBroadcastTxCommit); + return this.doCall(query, Params.encodeBroadcastTx, Responses.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); + return this.doCall(query, Params.encodeCommit, Responses.decodeCommit); } public async genesis(): Promise { const query: requests.GenesisRequest = { method: requests.Method.Genesis }; - return this.doCall(query, this.p.encodeGenesis, this.r.decodeGenesis); + return this.doCall(query, Params.encodeGenesis, Responses.decodeGenesis); } public async health(): Promise { const query: requests.HealthRequest = { method: requests.Method.Health }; - return this.doCall(query, this.p.encodeHealth, this.r.decodeHealth); + return this.doCall(query, Params.encodeHealth, Responses.decodeHealth); } public async numUnconfirmedTxs(): Promise { const query: requests.NumUnconfirmedTxsRequest = { method: requests.Method.NumUnconfirmedTxs }; - return this.doCall(query, this.p.encodeNumUnconfirmedTxs, this.r.decodeNumUnconfirmedTxs); + return this.doCall(query, Params.encodeNumUnconfirmedTxs, Responses.decodeNumUnconfirmedTxs); } public async status(): Promise { const query: requests.StatusRequest = { method: requests.Method.Status }; - return this.doCall(query, this.p.encodeStatus, this.r.decodeStatus); + return this.doCall(query, Params.encodeStatus, Responses.decodeStatus); } public subscribeNewBlock(): Stream { @@ -230,7 +226,7 @@ export class Tendermint37Client { method: requests.Method.Subscribe, query: { type: requests.SubscriptionEventType.NewBlock }, }; - return this.subscribe(request, this.r.decodeNewBlockEvent); + return this.subscribe(request, Responses.decodeNewBlockEvent); } public subscribeNewBlockHeader(): Stream { @@ -238,7 +234,7 @@ export class Tendermint37Client { method: requests.Method.Subscribe, query: { type: requests.SubscriptionEventType.NewBlockHeader }, }; - return this.subscribe(request, this.r.decodeNewBlockHeaderEvent); + return this.subscribe(request, Responses.decodeNewBlockHeaderEvent); } public subscribeTx(query?: string): Stream { @@ -249,7 +245,7 @@ export class Tendermint37Client { raw: query, }, }; - return this.subscribe(request, this.r.decodeTxEvent); + return this.subscribe(request, Responses.decodeTxEvent); } /** @@ -259,7 +255,7 @@ export class Tendermint37Client { */ 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); + return this.doCall(query, Params.encodeTx, Responses.decodeTx); } /** @@ -269,7 +265,7 @@ export class Tendermint37Client { */ 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); + return this.doCall(query, Params.encodeTxSearch, Responses.decodeTxSearch); } // this should paginate through all txSearch options to ensure it returns all results. @@ -300,7 +296,7 @@ export class Tendermint37Client { method: requests.Method.Validators, params: params, }; - return this.doCall(query, this.p.encodeValidators, this.r.decodeValidators); + return this.doCall(query, Params.encodeValidators, Responses.decodeValidators); } public async validatorsAll(height?: number): Promise { @@ -349,7 +345,7 @@ export class Tendermint37Client { throw new Error("This RPC client type cannot subscribe to events"); } - const req = this.p.encodeSubscribe(request); + const req = Params.encodeSubscribe(request); const eventStream = this.client.listen(req); return eventStream.map((event) => { return decode(event); From ec26b0a883c1b6af6c4fe2996a5a39476dd04b29 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 24 Aug 2023 17:09:18 +0200 Subject: [PATCH 7/9] Add connectComet for auto-detecting the right client --- CHANGELOG.md | 8 ++++ packages/tendermint-rpc/src/index.ts | 9 ++++- .../tendermint-rpc/src/tendermintclient.ts | 40 +++++++++++++++++-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c842224023..e7d2198e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ and this project adheres to - @cosmjs/tendermint-rpc: Remove `Adaptor` abstractions which are not needed anymore by haing a dedicated client for each backend. +- @cosmjs/tendermint-rpc: Add + `CometClient = Tendermint34Client | Tendermint37Client | Comet38Client` and + `connectComet` for auto-detecting the right client for a provided endpoint. + +### Deprecated + +- @cosmjs/tendermint-rpc: `CometClient` should be used instead of + `TendermintClient`. ## [0.31.1] - 2023-08-21 diff --git a/packages/tendermint-rpc/src/index.ts b/packages/tendermint-rpc/src/index.ts index 4c1e3026cd..7188450fa4 100644 --- a/packages/tendermint-rpc/src/index.ts +++ b/packages/tendermint-rpc/src/index.ts @@ -99,7 +99,14 @@ 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 { + CometClient, + connectComet, + isComet38Client, + isTendermint34Client, + isTendermint37Client, + TendermintClient, +} from "./tendermintclient"; export { BlockIdFlag, CommitSignature, diff --git a/packages/tendermint-rpc/src/tendermintclient.ts b/packages/tendermint-rpc/src/tendermintclient.ts index 3d07558bd4..db932a8788 100644 --- a/packages/tendermint-rpc/src/tendermintclient.ts +++ b/packages/tendermint-rpc/src/tendermintclient.ts @@ -1,13 +1,47 @@ +import { Comet38Client } from "./comet38"; +import { HttpEndpoint } from "./rpcclients"; import { Tendermint34Client } from "./tendermint34"; import { Tendermint37Client } from "./tendermint37"; -/** A TendermintClient is either a Tendermint34Client or a Tendermint37Client */ +/** + * A TendermintClient is either a Tendermint34Client or a Tendermint37Client + * + * @deprecated use `CometClient` + */ export type TendermintClient = Tendermint34Client | Tendermint37Client; -export function isTendermint34Client(client: TendermintClient): client is Tendermint34Client { +/** A CometClient is either a Tendermint34Client, Tendermint37Client or a Comet38Client */ +export type CometClient = Tendermint34Client | Tendermint37Client | Comet38Client; + +export function isTendermint34Client(client: CometClient): client is Tendermint34Client { return client instanceof Tendermint34Client; } -export function isTendermint37Client(client: TendermintClient): client is Tendermint37Client { +export function isTendermint37Client(client: CometClient): client is Tendermint37Client { return client instanceof Tendermint37Client; } + +export function isComet38Client(client: CometClient): client is Comet38Client { + return client instanceof Comet38Client; +} + +/** + * Auto-detects the version of the backend and uses a suitable client. + */ +export async function connectComet(endpoint: string | HttpEndpoint): Promise { + // Tendermint/CometBFT 0.34/0.37/0.38 auto-detection. Starting with 0.37 we seem to get reliable versions again 🎉 + // Using 0.34 as the fallback. + let out: CometClient; + const tm37Client = await Tendermint37Client.connect(endpoint); + const version = (await tm37Client.status()).nodeInfo.version; + if (version.startsWith("0.37.")) { + out = tm37Client; + } else if (version.startsWith("0.38.")) { + tm37Client.disconnect(); + out = await Comet38Client.connect(endpoint); + } else { + tm37Client.disconnect(); + out = await Tendermint34Client.connect(endpoint); + } + return out; +} From 99217b1437f518270e3b7f9485177018643d7dee Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 24 Aug 2023 17:10:57 +0200 Subject: [PATCH 8/9] Use CometClient --- .../src/cosmwasmclient.spec.ts | 2 +- .../cosmwasm-stargate/src/cosmwasmclient.ts | 55 ++++++------------ .../src/signingcosmwasmclient.ts | 30 +++------- .../stargate/src/queryclient/queryclient.ts | 58 +++++++++---------- packages/stargate/src/stargateclient.spec.ts | 2 +- packages/stargate/src/stargateclient.ts | 55 ++++++------------ 6 files changed, 74 insertions(+), 128 deletions(-) diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts index 08a08b6ff3..ac1b6f244c 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts @@ -60,7 +60,7 @@ describe("CosmWasmClient", () => { pendingWithoutWasmd(); const client = await CosmWasmClient.connect(wasmd.endpoint); const openedClient = client as unknown as PrivateCosmWasmClient; - const getCodeSpy = spyOn(openedClient.tmClient!, "status").and.callThrough(); + const getCodeSpy = spyOn(openedClient.cometClient!, "status").and.callThrough(); expect(await client.getChainId()).toEqual(wasmd.chainId); // from network expect(await client.getChainId()).toEqual(wasmd.chainId); // from cache diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index 4fba5edc55..7c31dd3e99 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -21,13 +21,7 @@ import { TimeoutError, TxExtension, } from "@cosmjs/stargate"; -import { - HttpEndpoint, - Tendermint34Client, - Tendermint37Client, - TendermintClient, - toRfc3339WithNanoseconds, -} from "@cosmjs/tendermint-rpc"; +import { CometClient, connectComet, HttpEndpoint, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc"; import { assert, sleep } from "@cosmjs/utils"; import { TxMsgData } from "cosmjs-types/cosmos/base/abci/v1beta1/abci"; import { @@ -81,14 +75,14 @@ export interface ContractCodeHistoryEntry { /** Use for testing only */ export interface PrivateCosmWasmClient { - readonly tmClient: TendermintClient | undefined; + readonly cometClient: CometClient | undefined; readonly queryClient: | (QueryClient & AuthExtension & BankExtension & TxExtension & WasmExtension) | undefined; } export class CosmWasmClient { - private readonly tmClient: TendermintClient | undefined; + private readonly cometClient: CometClient | undefined; private readonly queryClient: | (QueryClient & AuthExtension & BankExtension & TxExtension & WasmExtension) | undefined; @@ -102,34 +96,23 @@ export class CosmWasmClient { * To set the Tendermint client explicitly, use `create`. */ public static async connect(endpoint: string | HttpEndpoint): Promise { - // Tendermint/CometBFT 0.34/0.37 auto-detection. Starting with 0.37 we seem to get reliable versions again 🎉 - // Using 0.34 as the fallback. - let tmClient: TendermintClient; - const tm37Client = await Tendermint37Client.connect(endpoint); - const version = (await tm37Client.status()).nodeInfo.version; - if (version.startsWith("0.37.")) { - tmClient = tm37Client; - } else { - tm37Client.disconnect(); - tmClient = await Tendermint34Client.connect(endpoint); - } - - return CosmWasmClient.create(tmClient); + const cometClient = await connectComet(endpoint); + return CosmWasmClient.create(cometClient); } /** * 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); + public static async create(cometClient: CometClient): Promise { + return new CosmWasmClient(cometClient); } - protected constructor(tmClient: TendermintClient | undefined) { - if (tmClient) { - this.tmClient = tmClient; + protected constructor(cometClient: CometClient | undefined) { + if (cometClient) { + this.cometClient = cometClient; this.queryClient = QueryClient.withExtensions( - tmClient, + cometClient, setupAuthExtension, setupBankExtension, setupWasmExtension, @@ -138,17 +121,15 @@ export class CosmWasmClient { } } - protected getTmClient(): TendermintClient | undefined { - return this.tmClient; + protected getTmClient(): CometClient | undefined { + return this.cometClient; } - protected forceGetTmClient(): TendermintClient { - if (!this.tmClient) { - throw new Error( - "Tendermint client not available. You cannot use online functionality in offline mode.", - ); + protected forceGetTmClient(): CometClient { + if (!this.cometClient) { + throw new Error("Comet client not available. You cannot use online functionality in offline mode."); } - return this.tmClient; + return this.cometClient; } protected getQueryClient(): @@ -244,7 +225,7 @@ export class CosmWasmClient { } public disconnect(): void { - if (this.tmClient) this.tmClient.disconnect(); + if (this.cometClient) this.cometClient.disconnect(); } /** diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index a5d8ca3772..c1670d5ef2 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -31,12 +31,7 @@ import { SignerData, StdFee, } from "@cosmjs/stargate"; -import { - HttpEndpoint, - Tendermint34Client, - Tendermint37Client, - TendermintClient, -} from "@cosmjs/tendermint-rpc"; +import { CometClient, connectComet, HttpEndpoint } 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"; @@ -196,19 +191,8 @@ export class SigningCosmWasmClient extends CosmWasmClient { signer: OfflineSigner, options: SigningCosmWasmClientOptions = {}, ): Promise { - // Tendermint/CometBFT 0.34/0.37 auto-detection. Starting with 0.37 we seem to get reliable versions again 🎉 - // Using 0.34 as the fallback. - let tmClient: TendermintClient; - const tm37Client = await Tendermint37Client.connect(endpoint); - const version = (await tm37Client.status()).nodeInfo.version; - if (version.startsWith("0.37.")) { - tmClient = tm37Client; - } else { - tm37Client.disconnect(); - tmClient = await Tendermint34Client.connect(endpoint); - } - - return SigningCosmWasmClient.createWithSigner(tmClient, signer, options); + const cometClient = await connectComet(endpoint); + return SigningCosmWasmClient.createWithSigner(cometClient, signer, options); } /** @@ -216,11 +200,11 @@ export class SigningCosmWasmClient extends CosmWasmClient { * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. */ public static async createWithSigner( - tmClient: TendermintClient, + cometClient: CometClient, signer: OfflineSigner, options: SigningCosmWasmClientOptions = {}, ): Promise { - return new SigningCosmWasmClient(tmClient, signer, options); + return new SigningCosmWasmClient(cometClient, signer, options); } /** @@ -240,11 +224,11 @@ export class SigningCosmWasmClient extends CosmWasmClient { } protected constructor( - tmClient: TendermintClient | undefined, + cometClient: CometClient | undefined, signer: OfflineSigner, options: SigningCosmWasmClientOptions, ) { - super(tmClient); + super(cometClient); const { registry = new Registry([...defaultStargateTypes, ...wasmTypes]), aminoTypes = new AminoTypes({ diff --git a/packages/stargate/src/queryclient/queryclient.ts b/packages/stargate/src/queryclient/queryclient.ts index fed504ec5c..e839666cb0 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, TendermintClient } from "@cosmjs/tendermint-rpc"; +import { CometClient, tendermint34 } 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: TendermintClient): QueryClient; + public static withExtensions(cometClient: CometClient): QueryClient; /** Constructs a QueryClient with 1 extension */ public static withExtensions( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, ): QueryClient & A; /** Constructs a QueryClient with 2 extensions */ public static withExtensions( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, ): QueryClient & A & B; /** Constructs a QueryClient with 3 extensions */ public static withExtensions( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -70,7 +70,7 @@ export class QueryClient { /** Constructs a QueryClient with 4 extensions */ public static withExtensions( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -85,7 +85,7 @@ export class QueryClient { D extends object, E extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -102,7 +102,7 @@ export class QueryClient { E extends object, F extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -121,7 +121,7 @@ export class QueryClient { F extends object, G extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -142,7 +142,7 @@ export class QueryClient { G extends object, H extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -165,7 +165,7 @@ export class QueryClient { H extends object, I extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -190,7 +190,7 @@ export class QueryClient { I extends object, J extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -217,7 +217,7 @@ export class QueryClient { J extends object, K extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -246,7 +246,7 @@ export class QueryClient { K extends object, L extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -277,7 +277,7 @@ export class QueryClient { L extends object, M extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -310,7 +310,7 @@ export class QueryClient { M extends object, N extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -345,7 +345,7 @@ export class QueryClient { N extends object, O extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -382,7 +382,7 @@ export class QueryClient { O extends object, P extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -421,7 +421,7 @@ export class QueryClient { P extends object, Q extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -462,7 +462,7 @@ export class QueryClient { Q extends object, R extends object, >( - tmClient: TendermintClient, + cometClient: CometClient, setupExtensionA: QueryExtensionSetup, setupExtensionB: QueryExtensionSetup, setupExtensionC: QueryExtensionSetup, @@ -484,10 +484,10 @@ 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: TendermintClient, + cometClient: CometClient, ...extensionSetups: Array> ): any { - const client = new QueryClient(tmClient); + const client = new QueryClient(cometClient); const extensions = extensionSetups.map((setupExtension) => setupExtension(client)); for (const extension of extensions) { assert(isNonNullObject(extension), `Extension must be a non-null object`); @@ -506,10 +506,10 @@ export class QueryClient { return client; } - private readonly tmClient: TendermintClient; + private readonly cometClient: CometClient; - public constructor(tmClient: TendermintClient) { - this.tmClient = tmClient; + public constructor(cometClient: CometClient) { + this.cometClient = cometClient; } /** @@ -557,7 +557,7 @@ export class QueryClient { queryKey: Uint8Array, desiredHeight?: number, ): Promise { - const { key, value, height, proof, code, log } = await this.tmClient.abciQuery({ + const { key, value, height, proof, code, log } = await this.cometClient.abciQuery({ // we need the StoreKey for the module, not the module name // https://github.com/cosmos/cosmos-sdk/blob/8cab43c8120fec5200c3459cbf4a92017bb6f287/x/auth/types/keys.go#L12 path: `/store/${store}/key`, @@ -608,7 +608,7 @@ export class QueryClient { request: Uint8Array, desiredHeight?: number, ): Promise { - const response = await this.tmClient.abciQuery({ + const response = await this.cometClient.abciQuery({ path: path, data: request, prove: false, @@ -641,7 +641,7 @@ export class QueryClient { let nextHeader: tendermint34.Header | undefined; let headersSubscription: Stream | undefined; try { - headersSubscription = this.tmClient.subscribeNewBlockHeader(); + headersSubscription = this.cometClient.subscribeNewBlockHeader(); } catch { // Ignore exception caused by non-WebSocket Tendermint clients } @@ -656,7 +656,7 @@ export class QueryClient { while (!nextHeader) { // start from current height to avoid backend error for minHeight in the future - const correctHeader = (await this.tmClient.blockchain(height, searchHeight)).blockMetas + const correctHeader = (await this.cometClient.blockchain(height, searchHeight)).blockMetas .map((meta) => meta.header) .find((h) => h.height === searchHeight); if (correctHeader) { diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index 52b97eb80c..2ae554465f 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -98,7 +98,7 @@ describe("StargateClient", () => { pendingWithoutSimapp(); const client = await StargateClient.connect(simapp.tendermintUrl); const openedClient = client as unknown as PrivateStargateClient; - const getCodeSpy = spyOn(openedClient.tmClient!, "status").and.callThrough(); + const getCodeSpy = spyOn(openedClient.cometClient!, "status").and.callThrough(); expect(await client.getChainId()).toEqual(simapp.chainId); // from network expect(await client.getChainId()).toEqual(simapp.chainId); // from cache diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index e463a2e326..69ff4582d3 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -2,13 +2,7 @@ import { addCoins } from "@cosmjs/amino"; import { toHex } from "@cosmjs/encoding"; import { Uint53 } from "@cosmjs/math"; -import { - HttpEndpoint, - Tendermint34Client, - Tendermint37Client, - TendermintClient, - toRfc3339WithNanoseconds, -} from "@cosmjs/tendermint-rpc"; +import { CometClient, connectComet, HttpEndpoint, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc"; import { assert, sleep } from "@cosmjs/utils"; import { MsgData, TxMsgData } from "cosmjs-types/cosmos/base/abci/v1beta1/abci"; import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; @@ -188,7 +182,7 @@ export class BroadcastTxError extends Error { /** Use for testing only */ export interface PrivateStargateClient { - readonly tmClient: TendermintClient | undefined; + readonly cometClient: CometClient | undefined; } export interface StargateClientOptions { @@ -196,7 +190,7 @@ export interface StargateClientOptions { } export class StargateClient { - private readonly tmClient: TendermintClient | undefined; + private readonly cometClient: CometClient | undefined; private readonly queryClient: | (QueryClient & AuthExtension & BankExtension & StakingExtension & TxExtension) | undefined; @@ -213,19 +207,8 @@ export class StargateClient { endpoint: string | HttpEndpoint, options: StargateClientOptions = {}, ): Promise { - // Tendermint/CometBFT 0.34/0.37 auto-detection. Starting with 0.37 we seem to get reliable versions again 🎉 - // Using 0.34 as the fallback. - let tmClient: TendermintClient; - const tm37Client = await Tendermint37Client.connect(endpoint); - const version = (await tm37Client.status()).nodeInfo.version; - if (version.startsWith("0.37.")) { - tmClient = tm37Client; - } else { - tm37Client.disconnect(); - tmClient = await Tendermint34Client.connect(endpoint); - } - - return StargateClient.create(tmClient, options); + const cometClient = await connectComet(endpoint); + return StargateClient.create(cometClient, options); } /** @@ -233,17 +216,17 @@ export class StargateClient { * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. */ public static async create( - tmClient: TendermintClient, + cometClient: CometClient, options: StargateClientOptions = {}, ): Promise { - return new StargateClient(tmClient, options); + return new StargateClient(cometClient, options); } - protected constructor(tmClient: TendermintClient | undefined, options: StargateClientOptions) { - if (tmClient) { - this.tmClient = tmClient; + protected constructor(cometClient: CometClient | undefined, options: StargateClientOptions) { + if (cometClient) { + this.cometClient = cometClient; this.queryClient = QueryClient.withExtensions( - tmClient, + cometClient, setupAuthExtension, setupBankExtension, setupStakingExtension, @@ -254,17 +237,15 @@ export class StargateClient { this.accountParser = accountParser; } - protected getTmClient(): TendermintClient | undefined { - return this.tmClient; + protected getTmClient(): CometClient | undefined { + return this.cometClient; } - protected forceGetTmClient(): TendermintClient { - if (!this.tmClient) { - throw new Error( - "Tendermint client not available. You cannot use online functionality in offline mode.", - ); + protected forceGetTmClient(): CometClient { + if (!this.cometClient) { + throw new Error("Comet client not available. You cannot use online functionality in offline mode."); } - return this.tmClient; + return this.cometClient; } protected getQueryClient(): @@ -414,7 +395,7 @@ export class StargateClient { } public disconnect(): void { - if (this.tmClient) this.tmClient.disconnect(); + if (this.cometClient) this.cometClient.disconnect(); } /** From efdf1983e6a1fa0d70b1c14bcd3fba46c919efae Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 24 Aug 2023 17:40:36 +0200 Subject: [PATCH 9/9] Move Decoder, Encoder out of adaptor --- packages/tendermint-rpc/src/comet38/adaptor/index.ts | 1 - packages/tendermint-rpc/src/comet38/adaptor/types.ts | 10 ---------- packages/tendermint-rpc/src/comet38/comet38client.ts | 9 ++++++++- .../tendermint-rpc/src/tendermint34/adaptor/index.ts | 1 - .../tendermint-rpc/src/tendermint34/adaptor/types.ts | 10 ---------- .../src/tendermint34/tendermint34client.ts | 9 ++++++++- .../tendermint-rpc/src/tendermint37/adaptor/index.ts | 1 - .../tendermint-rpc/src/tendermint37/adaptor/types.ts | 10 ---------- .../src/tendermint37/tendermint37client.ts | 9 ++++++++- 9 files changed, 24 insertions(+), 36 deletions(-) delete mode 100644 packages/tendermint-rpc/src/comet38/adaptor/types.ts delete mode 100644 packages/tendermint-rpc/src/tendermint34/adaptor/types.ts delete mode 100644 packages/tendermint-rpc/src/tendermint37/adaptor/types.ts diff --git a/packages/tendermint-rpc/src/comet38/adaptor/index.ts b/packages/tendermint-rpc/src/comet38/adaptor/index.ts index d592b38acb..38e924802f 100644 --- a/packages/tendermint-rpc/src/comet38/adaptor/index.ts +++ b/packages/tendermint-rpc/src/comet38/adaptor/index.ts @@ -1,3 +1,2 @@ export { Params } from "./requests"; export { Responses } from "./responses"; -export { Decoder, Encoder } from "./types"; diff --git a/packages/tendermint-rpc/src/comet38/adaptor/types.ts b/packages/tendermint-rpc/src/comet38/adaptor/types.ts deleted file mode 100644 index f9bf1d11ab..0000000000 --- a/packages/tendermint-rpc/src/comet38/adaptor/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; - -import * as requests from "../requests"; -import * as responses from "../responses"; - -// 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; diff --git a/packages/tendermint-rpc/src/comet38/comet38client.ts b/packages/tendermint-rpc/src/comet38/comet38client.ts index f8037aa240..2d20513fac 100644 --- a/packages/tendermint-rpc/src/comet38/comet38client.ts +++ b/packages/tendermint-rpc/src/comet38/comet38client.ts @@ -1,3 +1,4 @@ +import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; import { Stream } from "xstream"; import { createJsonRpcRequest } from "../jsonrpc"; @@ -9,10 +10,16 @@ import { SubscriptionEvent, WebsocketClient, } from "../rpcclients"; -import { Decoder, Encoder, Params, Responses } from "./adaptor"; +import { Params, Responses } from "./adaptor"; import * as requests from "./requests"; import * as responses from "./responses"; +// Encoder is a generic that matches all methods of Params +type Encoder = (req: T) => JsonRpcRequest; + +// Decoder is a generic that matches all methods of Responses +type Decoder = (res: JsonRpcSuccessResponse) => T; + export class Comet38Client { /** * Creates a new Tendermint client for the given endpoint. diff --git a/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts b/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts index d592b38acb..38e924802f 100644 --- a/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts +++ b/packages/tendermint-rpc/src/tendermint34/adaptor/index.ts @@ -1,3 +1,2 @@ export { Params } from "./requests"; export { Responses } from "./responses"; -export { Decoder, Encoder } from "./types"; diff --git a/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts b/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts deleted file mode 100644 index f9bf1d11ab..0000000000 --- a/packages/tendermint-rpc/src/tendermint34/adaptor/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; - -import * as requests from "../requests"; -import * as responses from "../responses"; - -// 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; diff --git a/packages/tendermint-rpc/src/tendermint34/tendermint34client.ts b/packages/tendermint-rpc/src/tendermint34/tendermint34client.ts index 701960de43..6704a6f066 100644 --- a/packages/tendermint-rpc/src/tendermint34/tendermint34client.ts +++ b/packages/tendermint-rpc/src/tendermint34/tendermint34client.ts @@ -1,3 +1,4 @@ +import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; import { Stream } from "xstream"; import { createJsonRpcRequest } from "../jsonrpc"; @@ -9,10 +10,16 @@ import { SubscriptionEvent, WebsocketClient, } from "../rpcclients"; -import { Decoder, Encoder, Params, Responses } from "./adaptor"; +import { Params, Responses } from "./adaptor"; import * as requests from "./requests"; import * as responses from "./responses"; +// Encoder is a generic that matches all methods of Params +type Encoder = (req: T) => JsonRpcRequest; + +// Decoder is a generic that matches all methods of Responses +type Decoder = (res: JsonRpcSuccessResponse) => T; + export class Tendermint34Client { /** * Creates a new Tendermint client for the given endpoint. diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts index d592b38acb..38e924802f 100644 --- a/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts +++ b/packages/tendermint-rpc/src/tendermint37/adaptor/index.ts @@ -1,3 +1,2 @@ export { Params } from "./requests"; export { Responses } from "./responses"; -export { Decoder, Encoder } from "./types"; diff --git a/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts b/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts deleted file mode 100644 index f9bf1d11ab..0000000000 --- a/packages/tendermint-rpc/src/tendermint37/adaptor/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; - -import * as requests from "../requests"; -import * as responses from "../responses"; - -// 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; diff --git a/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts b/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts index 9d89708e2a..3d55949720 100644 --- a/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts +++ b/packages/tendermint-rpc/src/tendermint37/tendermint37client.ts @@ -1,3 +1,4 @@ +import { JsonRpcRequest, JsonRpcSuccessResponse } from "@cosmjs/json-rpc"; import { Stream } from "xstream"; import { createJsonRpcRequest } from "../jsonrpc"; @@ -9,10 +10,16 @@ import { SubscriptionEvent, WebsocketClient, } from "../rpcclients"; -import { Decoder, Encoder, Params, Responses } from "./adaptor"; +import { Params, Responses } from "./adaptor"; import * as requests from "./requests"; import * as responses from "./responses"; +// Encoder is a generic that matches all methods of Params +type Encoder = (req: T) => JsonRpcRequest; + +// Decoder is a generic that matches all methods of Responses +type Decoder = (res: JsonRpcSuccessResponse) => T; + export class Tendermint37Client { /** * Creates a new Tendermint client for the given endpoint.