Create Tendermint34Client

This commit is contained in:
Simon Warta 2021-02-23 13:04:09 +01:00
parent 1bef41ee94
commit 8eb40f4eef
17 changed files with 3200 additions and 0 deletions

View File

@ -54,6 +54,9 @@
`MsgFundCommunityPool`, `MsgSetWithdrawAddress`, `MsgWithdrawDelegatorReward`,
`MsgWithdrawValidatorCommission` and type checker helper functions.
- @cosmjs/utils: Added `assertDefinedAndNotNull`.
- @cosmjs/tendermint-rpc: The new `Tendermint34Client` is a copy of the old
`Client` but without the automatic version detection. Its usage is encouraged
over `Client` if you connect to a Tendermint 0.34 backend.
### Changed

View File

@ -80,3 +80,5 @@ export {
} from "./dates";
export { HttpClient, WebsocketClient } from "./rpcclients"; // TODO: Why do we export those outside of this package?
export { BlockIdFlag, CommitSignature, ValidatorEd25519Pubkey, ValidatorPubkey } from "./types";
export * as tendermint34 from "./tendermint34";
export { Tendermint34Client } from "./tendermint34";

View File

@ -0,0 +1,58 @@
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<T extends requests.Request> = (req: T) => JsonRpcRequest;
// Decoder is a generic that matches all methods of Responses
export type Decoder<T extends responses.Response> = (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 encodeBroadcastTx: (req: requests.BroadcastTxRequest) => JsonRpcRequest;
readonly encodeCommit: (req: requests.CommitRequest) => JsonRpcRequest;
readonly encodeGenesis: (req: requests.GenesisRequest) => JsonRpcRequest;
readonly encodeHealth: (req: requests.HealthRequest) => 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 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 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;
}

View File

@ -0,0 +1,2 @@
/* eslint-disable @typescript-eslint/naming-convention */
export { v0_34 as adaptor34 } from "./v0-34";

View File

@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { Adaptor } from "../../adaptor";
import { hashBlock, hashTx } from "../../hasher";
import { Params } from "./requests";
import { Responses } from "./responses";
export const v0_34: Adaptor = {
params: Params,
responses: Responses,
hashTx: hashTx,
hashBlock: hashBlock,
};

View File

@ -0,0 +1,159 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { toBase64, toHex } from "@cosmjs/encoding";
import { JsonRpcRequest } from "@cosmjs/json-rpc";
import { createJsonRpcRequest } from "../../../jsonrpc";
import { assertNotEmpty, Integer, 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(Integer.encode, param.height),
};
}
interface RpcBlockchainRequestParams {
readonly minHeight?: string;
readonly maxHeight?: string;
}
function encodeBlockchainRequestParams(param: requests.BlockchainRequestParams): RpcBlockchainRequestParams {
return {
minHeight: may(Integer.encode, param.minHeight),
maxHeight: may(Integer.encode, param.maxHeight),
};
}
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(Integer.encode, 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;
}
function encodeTxSearchParams(params: requests.TxSearchParams): RpcTxSearchParams {
return {
query: params.query,
prove: params.prove,
page: may(Integer.encode, params.page),
per_page: may(Integer.encode, params.per_page),
};
}
interface RpcValidatorsParams {
readonly height?: string;
readonly page?: string;
readonly per_page?: string;
}
function encodeValidatorsParams(params: requests.ValidatorsParams): RpcValidatorsParams {
return {
height: may(Integer.encode, params.height),
page: may(Integer.encode, params.page),
per_page: may(Integer.encode, 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 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 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));
}
}

View File

@ -0,0 +1,831 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { fromBase64, fromHex } from "@cosmjs/encoding";
import { JsonRpcSuccessResponse } from "@cosmjs/json-rpc";
import { assert } from "@cosmjs/utils";
import { fromRfc3339WithNanoseconds } from "../../../dates";
import { SubscriptionEvent } from "../../../rpcclients";
import { BlockIdFlag, CommitSignature, ValidatorPubkey } from "../../../types";
import {
assertArray,
assertBoolean,
assertNotEmpty,
assertNumber,
assertObject,
assertSet,
assertString,
dictionaryToStringMap,
Integer,
may,
optional,
} 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(Integer.parse, 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 */
readonly key: string;
/** base64 encoded */
readonly value?: string;
readonly proofOps?: RpcQueryProof;
readonly height?: string;
readonly index?: string;
readonly code?: string; // only for errors
readonly log?: string;
}
function decodeAbciQuery(data: RpcAbciQueryResponse): responses.AbciQueryResponse {
return {
key: fromBase64(optional(data.key, "")),
value: fromBase64(optional(data.value, "")),
proof: may(decodeQueryProof, data.proofOps),
height: may(Integer.parse, data.height),
code: may(Integer.parse, data.code),
index: may(Integer.parse, data.index),
log: data.log,
};
}
interface RpcAttribute {
/** base64 encoded */
readonly key: string;
/** base64 encoded */
readonly value: string;
}
function decodeAttribute(attribute: RpcAttribute): responses.Attribute {
return {
key: fromBase64(assertNotEmpty(attribute.key)),
value: fromBase64(optional(attribute.value, "")),
};
}
function decodeAttributes(attributes: readonly RpcAttribute[]): responses.Attribute[] {
return assertArray(attributes).map(decodeAttribute);
}
interface RpcEvent {
readonly type: string;
readonly attributes: readonly RpcAttribute[];
}
function decodeEvent(event: RpcEvent): responses.Event {
return {
type: event.type,
attributes: decodeAttributes(event.attributes),
};
}
function decodeEvents(events: readonly RpcEvent[]): readonly responses.Event[] {
return assertArray(events).map(decodeEvent);
}
interface RpcTxData {
readonly code?: number;
readonly log?: string;
/** base64 encoded */
readonly data?: string;
readonly events: readonly RpcEvent[];
}
function decodeTxData(data: RpcTxData): responses.TxData {
return {
data: may(fromBase64, data.data),
log: data.log,
code: Integer.parse(assertNumber(optional<number>(data.code, 0))),
events: decodeEvents(data.events),
};
}
// yes, a different format for status and dump consensus state
interface RpcPubkey {
readonly type: string;
/** base64 encoded */
readonly value: string;
}
function decodePubkey(data: RpcPubkey): ValidatorPubkey {
if (data.type === "tendermint/PubKeyEd25519") {
// go-amino special code
return {
algorithm: "ed25519",
data: fromBase64(assertNotEmpty(data.value)),
};
}
throw new Error(`unknown pubkey type: ${data.type}`);
}
// for evidence, block results, etc.
interface RpcValidatorUpdate {
/** hex encoded */
readonly address: string;
readonly pub_key: RpcPubkey;
readonly voting_power: string;
readonly proposer_priority: string;
}
function decodeValidatorUpdate(data: RpcValidatorUpdate): responses.Validator {
return {
pubkey: decodePubkey(assertObject(data.pub_key)),
votingPower: Integer.parse(assertNotEmpty(data.voting_power)),
address: fromHex(assertNotEmpty(data.address)),
proposerPriority: Integer.parse(data.proposer_priority),
};
}
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: Integer.parse(assertNotEmpty(data.max_bytes)),
maxGas: Integer.parse(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: Integer.parse(assertNotEmpty(data.max_age_num_blocks)),
maxAgeDuration: Integer.parse(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)),
};
}
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: Integer.parse(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: string;
/** hex encoded */
readonly hash: string;
};
}
function decodeBlockId(data: RpcBlockId): responses.BlockId {
return {
hash: fromHex(assertNotEmpty(data.hash)),
parts: {
total: Integer.parse(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: Integer.parse(data.block),
app: Integer.parse(data.app ?? 0),
};
}
interface RpcHeader {
readonly version: RpcBlockVersion;
readonly chain_id: string;
readonly height: string;
readonly time: string;
readonly num_txs: string;
readonly total_txs: 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: Integer.parse(assertNotEmpty(data.height)),
time: fromRfc3339WithNanoseconds(assertNotEmpty(data.time)),
lastBlockId: decodeBlockId(data.last_block_id),
lastCommitHash: fromHex(assertNotEmpty(data.last_commit_hash)),
dataHash: fromHex(assertSet(data.data_hash)),
validatorsHash: fromHex(assertNotEmpty(data.validators_hash)),
nextValidatorsHash: fromHex(assertNotEmpty(data.next_validators_hash)),
consensusHash: fromHex(assertNotEmpty(data.consensus_hash)),
appHash: fromHex(assertNotEmpty(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 header: RpcHeader;
}
function decodeBlockMeta(data: RpcBlockMeta): responses.BlockMeta {
return {
blockId: decodeBlockId(data.block_id),
header: decodeHeader(data.header),
};
}
interface RpcBlockchainResponse {
readonly last_height: string;
readonly block_metas: readonly RpcBlockMeta[];
}
function decodeBlockchain(data: RpcBlockchainResponse): responses.BlockchainResponse {
return {
lastHeight: Integer.parse(assertNotEmpty(data.last_height)),
blockMetas: assertArray(data.block_metas).map(decodeBlockMeta),
};
}
interface RpcBroadcastTxSyncResponse extends RpcTxData {
/** hex encoded */
readonly hash: string;
}
function decodeBroadcastTxSync(data: RpcBroadcastTxSyncResponse): responses.BroadcastTxSyncResponse {
return {
...decodeTxData(data),
hash: fromHex(assertNotEmpty(data.hash)),
};
}
interface RpcBroadcastTxCommitResponse {
readonly height: string;
/** hex encoded */
readonly hash: string;
readonly check_tx: RpcTxData;
readonly deliver_tx?: RpcTxData;
}
function decodeBroadcastTxCommit(data: RpcBroadcastTxCommitResponse): responses.BroadcastTxCommitResponse {
return {
height: Integer.parse(data.height),
hash: fromHex(assertNotEmpty(data.hash)),
checkTx: decodeTxData(assertObject(data.check_tx)),
deliverTx: may(decodeTxData, data.deliver_tx),
};
}
function decodeBlockIdFlag(blockIdFlag: number): BlockIdFlag {
assert(blockIdFlag in BlockIdFlag);
return blockIdFlag;
}
type RpcSignature = {
readonly block_id_flag: number;
/** hex encoded */
readonly validator_address: string;
readonly timestamp: string;
/** bae64 encoded */
readonly signature: string;
};
function decodeCommitSignature(data: RpcSignature): CommitSignature {
return {
blockIdFlag: decodeBlockIdFlag(data.block_id_flag),
validatorAddress: fromHex(data.validator_address),
timestamp: fromRfc3339WithNanoseconds(assertNotEmpty(data.timestamp)),
signature: fromBase64(assertNotEmpty(data.signature)),
};
}
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: Integer.parse(assertNotEmpty(data.height)),
round: Integer.parse(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;
}
function decodeValidatorGenesis(data: RpcValidatorGenesis): responses.Validator {
return {
address: fromHex(assertNotEmpty(data.address)),
pubkey: decodePubkey(assertObject(data.pub_key)),
votingPower: Integer.parse(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<string, unknown> | 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;
}
function decodeValidatorInfo(data: RpcValidatorInfo): responses.Validator {
return {
pubkey: decodePubkey(assertObject(data.pub_key)),
votingPower: Integer.parse(assertNotEmpty(data.voting_power)),
address: fromHex(assertNotEmpty(data.address)),
};
}
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<string, unknown>;
}
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: Integer.parse(assertNotEmpty(data.protocol_version.app)),
block: Integer.parse(assertNotEmpty(data.protocol_version.block)),
p2p: Integer.parse(assertNotEmpty(data.protocol_version.p2p)),
},
};
}
interface RpcSyncInfo {
/** hex encoded */
readonly latest_block_hash: string;
/** hex encoded */
readonly latest_app_hash: string;
readonly latest_block_height: string;
readonly latest_block_time: string;
readonly catching_up: boolean;
}
function decodeSyncInfo(data: RpcSyncInfo): responses.SyncInfo {
return {
latestBlockHash: fromHex(assertNotEmpty(data.latest_block_hash)),
latestAppHash: fromHex(assertNotEmpty(data.latest_app_hash)),
latestBlockTime: fromRfc3339WithNanoseconds(assertNotEmpty(data.latest_block_time)),
latestBlockHeight: Integer.parse(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: Integer.parse(assertNotEmpty(data.proof.total)),
index: Integer.parse(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: Integer.parse(assertNotEmpty(data.height)),
index: Integer.parse(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: Integer.parse(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;
/** Not set since Tendermint 0.34 */
readonly index?: number;
}
function decodeTxEvent(data: RpcTxEvent): responses.TxEvent {
const tx = fromBase64(assertNotEmpty(data.tx));
return {
tx: tx,
hash: hashTx(tx),
result: decodeTxData(data.result),
height: Integer.parse(assertNotEmpty(data.height)),
index: may(Integer.parse, data.index),
};
}
interface RpcValidatorsResponse {
readonly block_height: string;
readonly validators: readonly RpcValidatorUpdate[];
readonly count: string;
readonly total: string;
}
function decodeValidators(data: RpcValidatorsResponse): responses.ValidatorsResponse {
return {
blockHeight: Integer.parse(assertNotEmpty(data.block_height)),
validators: assertArray(data.validators).map(decodeValidatorUpdate),
count: Integer.parse(assertNotEmpty(data.count)),
total: Integer.parse(assertNotEmpty(data.total)),
};
}
interface RpcEvidence {
readonly type: string;
readonly validator: RpcValidatorUpdate;
readonly height: string;
readonly time: string;
readonly totalVotingPower: string;
}
function decodeEvidence(data: RpcEvidence): responses.Evidence {
return {
type: assertNotEmpty(data.type),
height: Integer.parse(assertNotEmpty(data.height)),
time: Integer.parse(assertNotEmpty(data.time)),
totalVotingPower: Integer.parse(assertNotEmpty(data.totalVotingPower)),
validator: decodeValidatorUpdate(data.validator),
};
}
function decodeEvidences(ev: readonly RpcEvidence[]): readonly responses.Evidence[] {
return assertArray(ev).map(decodeEvidence);
}
interface RpcBlock {
readonly header: RpcHeader;
readonly last_commit: RpcCommit;
readonly data: {
/** Raw tx bytes, base64 encoded */
readonly txs?: readonly string[];
};
readonly evidence?: {
readonly evidence?: readonly RpcEvidence[];
};
}
function decodeBlock(data: RpcBlock): responses.Block {
return {
header: decodeHeader(assertObject(data.header)),
lastCommit: decodeCommit(assertObject(data.last_commit)),
txs: data.data.txs ? assertArray(data.data.txs).map(fromBase64) : [],
evidence: data.evidence && may(decodeEvidences, 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),
};
}
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 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 this.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 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);
}
}

View File

@ -0,0 +1,90 @@
import { ReadonlyDate } from "readonly-date";
import { encodeBlockId, encodeBytes, encodeInt, encodeString, encodeTime, 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("encodeInt", () => {
it("works", () => {
expect(encodeInt(0)).toEqual(Uint8Array.from([0]));
expect(encodeInt(1)).toEqual(Uint8Array.from([1]));
expect(encodeInt(127)).toEqual(Uint8Array.from([127]));
expect(encodeInt(128)).toEqual(Uint8Array.from([128, 1]));
expect(encodeInt(255)).toEqual(Uint8Array.from([255, 1]));
expect(encodeInt(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,
]),
);
});
});
});

View File

@ -0,0 +1,210 @@
import { toUtf8 } from "@cosmjs/encoding";
import { Int53 } from "@cosmjs/math";
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<T>(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<T>(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 keyvalue 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<T>(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<T>(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;
}
// optional uses the value or provides a default
export function optional<T>(value: T | null | undefined, fallback: T): T {
return value === undefined || value === null ? fallback : value;
}
// may will run the transform if value is defined, otherwise returns undefined
export function may<T, U>(transform: (val: T) => U, value: T | null | undefined): U | undefined {
return value === undefined || value === null ? undefined : transform(value);
}
export function dictionaryToStringMap(obj: Record<string, unknown>): Map<string, string> {
const out = new Map<string, string>();
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;
}
export class Integer {
public static parse(input: string | number): number {
const asInt = typeof input === "number" ? new Int53(input) : Int53.fromString(input);
return asInt.toNumber();
}
public static encode(num: number): string {
return new Int53(num).toString();
}
}
// 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 encodeInt(n: number): Uint8Array {
// eslint-disable-next-line no-bitwise
return n >= 0x80 ? Uint8Array.from([(n & 0xff) | 0x80, ...encodeInt(n >> 7)]) : 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, ...encodeInt(seconds)] : new Uint8Array();
const nanoseconds = (time.nanoseconds || 0) + (milliseconds % 1000) * 1e6;
const nanosecondsArray = nanoseconds ? [0x10, ...encodeInt(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, ...encodeInt(version.block)]) : new Uint8Array();
const appArray = version.app ? Uint8Array.from([0x10, ...encodeInt(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,
]);
}

View File

@ -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);
});
});

View File

@ -0,0 +1,67 @@
import { Sha256, sha256 } from "@cosmjs/crypto";
import { encodeBlockId, encodeBytes, encodeInt, encodeString, encodeTime, 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 {
const encodedFields: readonly Uint8Array[] = [
encodeVersion(header.version),
encodeString(header.chainId),
encodeInt(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);
}

View File

@ -0,0 +1,74 @@
// Note: all exports in this module are publicly available via
// `import { tendermint34 } from "@cosnjs/tendermint-rpc"`
export { Tendermint34Client } from "./tendermint34client";
export {
AbciInfoRequest,
AbciQueryParams,
AbciQueryRequest,
BlockRequest,
BlockchainRequest,
BlockResultsRequest,
BroadcastTxRequest,
BroadcastTxParams,
CommitRequest,
GenesisRequest,
HealthRequest,
Method,
Request,
QueryTag,
StatusRequest,
SubscriptionEventType,
TxParams,
TxRequest,
TxSearchParams,
TxSearchRequest,
ValidatorsRequest,
ValidatorsParams,
} from "./requests";
export {
AbciInfoResponse,
AbciQueryResponse,
Attribute,
Block,
BlockchainResponse,
BlockGossipParams,
BlockId,
BlockMeta,
BlockParams,
BlockResponse,
BlockResultsResponse,
BroadcastTxAsyncResponse,
BroadcastTxCommitResponse,
broadcastTxCommitSuccess,
BroadcastTxSyncResponse,
broadcastTxSyncSuccess,
Commit,
CommitResponse,
ConsensusParams,
Event,
Evidence,
EvidenceParams,
GenesisResponse,
Header,
HealthResponse,
NewBlockEvent,
NewBlockHeaderEvent,
NodeInfo,
ProofOp,
QueryProof,
Response,
StatusResponse,
SyncInfo,
TxData,
TxEvent,
TxProof,
TxResponse,
TxSearchResponse,
TxSizeParams,
Validator,
ValidatorsResponse,
Version,
Vote,
VoteType,
} from "./responses";

View File

@ -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");
});
});
});

View File

@ -0,0 +1,184 @@
/* 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",
BroadcastTxAsync = "broadcast_tx_async",
BroadcastTxSync = "broadcast_tx_sync",
BroadcastTxCommit = "broadcast_tx_commit",
Commit = "commit",
Genesis = "genesis",
Health = "health",
Status = "status",
Subscribe = "subscribe",
Tx = "tx",
TxSearch = "tx_search",
Validators = "validators",
Unsubscribe = "unsubscribe",
}
export type Request =
| AbciInfoRequest
| AbciQueryRequest
| BlockRequest
| BlockchainRequest
| BlockResultsRequest
| BroadcastTxRequest
| CommitRequest
| GenesisRequest
| HealthRequest
| 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 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 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;
}
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 ");
}

View File

@ -0,0 +1,337 @@
import { ReadonlyDate } from "readonly-date";
import { ReadonlyDateWithNanoseconds } from "../dates";
import { CommitSignature, ValidatorPubkey } from "../types";
export type Response =
| AbciInfoResponse
| AbciQueryResponse
| BlockResponse
| BlockResultsResponse
| BlockchainResponse
| BroadcastTxAsyncResponse
| BroadcastTxSyncResponse
| BroadcastTxCommitResponse
| CommitResponse
| GenesisResponse
| HealthResponse
| 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 log?: string;
}
export interface BlockResponse {
readonly blockId: BlockId;
readonly block: Block;
}
export interface BlockResultsResponse {
readonly height: number;
readonly results: readonly TxData[];
readonly validatorUpdates: readonly Validator[];
readonly consensusUpdates?: ConsensusParams;
readonly beginBlockEvents: readonly Event[];
readonly endBlockEvents: readonly Event[];
}
export interface BlockchainResponse {
readonly lastHeight: number;
readonly blockMetas: readonly BlockMeta[];
}
/** No data in here because RPC method BroadcastTxAsync "returns right away, with no response" */
export interface BroadcastTxAsyncResponse {}
export interface BroadcastTxSyncResponse extends TxData {
readonly hash: Uint8Array;
}
/**
* Returns true iff transaction made it sucessfully into the transaction pool
*/
export function broadcastTxSyncSuccess(res: BroadcastTxSyncResponse): boolean {
// code must be 0 on success
return res.code === 0;
}
export interface BroadcastTxCommitResponse {
readonly height: number;
readonly hash: Uint8Array;
readonly checkTx: TxData;
readonly deliverTx?: TxData;
}
/**
* Returns true iff transaction made it successfully into a block
* (i.e. success in `check_tx` and `deliver_tx` field)
*/
export function broadcastTxCommitSuccess(response: BroadcastTxCommitResponse): boolean {
// code must be 0 on success
// deliverTx may be present but empty on failure
return response.checkTx.code === 0 && !!response.deliverTx && response.deliverTx.code === 0;
}
export interface CommitResponse {
readonly header: Header;
readonly commit: Commit;
readonly canonical: boolean;
}
export interface GenesisResponse {
readonly genesisTime: ReadonlyDate;
readonly chainId: string;
readonly consensusParams: ConsensusParams;
readonly validators: readonly Validator[];
readonly appHash: Uint8Array;
readonly appState: Record<string, unknown> | undefined;
}
export type HealthResponse = null;
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;
/** @deprecated this value is not set in Tendermint 0.34+ */
readonly index?: number;
readonly result: TxData;
}
// Helper items used above
/** An event attribute */
export interface Attribute {
readonly key: Uint8Array;
readonly value: Uint8Array;
}
export interface Event {
readonly type: string;
readonly attributes: readonly Attribute[];
}
export interface TxData {
readonly code: number;
readonly log?: string;
readonly data?: Uint8Array;
readonly events: readonly Event[];
// readonly fees?: any;
}
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 header: Header;
// TODO: Add blockSize (e.g "block_size": "471")
// TODO: Add numTxs (e.g "num_txs": "0")
}
export interface BlockId {
readonly hash: Uint8Array;
readonly parts: {
readonly total: number;
readonly hash: Uint8Array;
};
}
export interface Block {
readonly header: Header;
readonly lastCommit: Commit;
readonly txs: readonly Uint8Array[];
readonly evidence?: readonly Evidence[];
}
export interface Evidence {
readonly type: string;
readonly validator: Validator;
readonly height: number;
readonly time: number;
readonly totalVotingPower: number;
}
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;
// prev block info
readonly lastBlockId: BlockId;
// hashes of block data
readonly lastCommitHash: Uint8Array;
readonly dataHash: Uint8Array; // empty when number of transaction is 0
// hashes from the app output from the prev block
readonly validatorsHash: Uint8Array;
readonly nextValidatorsHash: Uint8Array;
readonly consensusHash: Uint8Array;
readonly appHash: Uint8Array;
readonly lastResultsHash: Uint8Array;
// consensus info
readonly evidenceHash: Uint8Array;
readonly proposerAddress: Uint8Array;
}
export interface NodeInfo {
readonly id: Uint8Array;
/** IP and port */
readonly listenAddr: string;
readonly network: string;
readonly version: string;
readonly channels: string; // ???
readonly moniker: string;
readonly other: Map<string, string>;
readonly protocolVersion: {
readonly p2p: number;
readonly block: number;
readonly app: number;
};
}
export interface SyncInfo {
readonly latestBlockHash: Uint8Array;
readonly latestAppHash: Uint8Array;
readonly latestBlockHeight: number;
readonly latestBlockTime: ReadonlyDate;
readonly catchingUp: boolean;
}
export interface Validator {
readonly address: Uint8Array;
readonly pubkey?: ValidatorPubkey;
readonly votingPower: number;
readonly proposerPriority?: number;
}
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;
}

View File

@ -0,0 +1,727 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { toAscii } from "@cosmjs/encoding";
import { firstEvent, toListPromise } from "@cosmjs/stream";
import { sleep } from "@cosmjs/utils";
import { ReadonlyDate } from "readonly-date";
import { Stream } from "xstream";
import { ExpectedValues, tendermintInstances } from "../config.spec";
import { HttpClient, RpcClient, WebsocketClient } from "../rpcclients";
import { chainIdMatcher } from "../testutil.spec";
import { adaptor34 } from "./adaptors";
import { buildQuery } from "./requests";
import * as responses from "./responses";
import { Tendermint34Client } from "./tendermint34client";
function tendermintEnabled(): boolean {
return !!process.env.TENDERMINT_ENABLED;
}
function pendingWithoutTendermint(): void {
if (!tendermintEnabled()) {
pending("Set TENDERMINT_ENABLED to enable tendermint-based tests");
}
}
async function tendermintSearchIndexUpdated(): Promise<void> {
// Tendermint needs some time before a committed transaction is found in search
return sleep(75);
}
function buildKvTx(k: string, v: string): Uint8Array {
return toAscii(`${k}=${v}`);
}
function randomString(): string {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from({ length: 12 })
.map(() => alphabet[Math.floor(Math.random() * alphabet.length)])
.join("");
}
function defaultTestSuite(rpcFactory: () => RpcClient, expected: ExpectedValues): void {
describe("create", () => {
it("can auto-discover Tendermint version and communicate", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.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 Tendermint34Client.create(rpcFactory());
expect(await client.abciInfo()).toBeTruthy();
client.disconnect();
});
});
it("can get genesis", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
const genesis = await client.genesis();
expect(genesis).toBeTruthy();
client.disconnect();
});
it("can broadcast a transaction", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.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();
});
it("gets the same tx hash from backend as calculated locally", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
const tx = buildKvTx(randomString(), randomString());
const calculatedTxHash = adaptor34.hashTx(tx);
const response = await client.broadcastTxCommit({ tx: tx });
expect(response.hash).toEqual(calculatedTxHash);
client.disconnect();
});
it("can query the state", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.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).toBeFalsy();
client.disconnect();
});
it("can get a commit", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.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 Tendermint34Client.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 Tendermint34Client.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 Tendermint34Client.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 Tendermint34Client.create(rpcFactory());
const status = await client.status();
// node info
expect(status.nodeInfo.version).toEqual(expected.version);
expect(status.nodeInfo.protocolVersion).toEqual({
p2p: expected.p2pVersion,
block: expected.blockVersion,
app: expected.appVersion,
});
expect(status.nodeInfo.network).toMatch(chainIdMatcher);
expect(status.nodeInfo.other.size).toBeGreaterThanOrEqual(2);
expect(status.nodeInfo.other.get("tx_index")).toEqual("on");
// sync info
expect(status.syncInfo.catchingUp).toEqual(false);
expect(status.syncInfo.latestBlockHeight).toBeGreaterThanOrEqual(1);
// validator info
expect(status.validatorInfo.pubkey).toBeTruthy();
expect(status.validatorInfo.votingPower).toBeGreaterThan(0);
client.disconnect();
});
});
describe("blockResults", () => {
it("works", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.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("blockchain", () => {
it("returns latest in descending order by default", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.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).toEqual(height);
expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(3);
expect(blockchain.blockMetas[0].header.height).toEqual(height);
expect(blockchain.blockMetas[1].header.height).toEqual(height - 1);
expect(blockchain.blockMetas[2].header.height).toEqual(height - 2);
client.disconnect();
});
it("can limit by maxHeight", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
const height = (await client.status()).syncInfo.latestBlockHeight;
const blockchain = await client.blockchain(undefined, height - 1);
expect(blockchain.lastHeight).toEqual(height);
expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(2);
expect(blockchain.blockMetas[0].header.height).toEqual(height - 1); // upper limit included
expect(blockchain.blockMetas[1].header.height).toEqual(height - 2);
client.disconnect();
});
it("works with maxHeight in the future", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
const height = (await client.status()).syncInfo.latestBlockHeight;
const blockchain = await client.blockchain(undefined, height + 20);
expect(blockchain.lastHeight).toEqual(height);
expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(2);
expect(blockchain.blockMetas[0].header.height).toEqual(height);
expect(blockchain.blockMetas[1].header.height).toEqual(height - 1);
client.disconnect();
});
it("can limit by minHeight and maxHeight", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
const height = (await client.status()).syncInfo.latestBlockHeight;
const blockchain = await client.blockchain(height - 2, height - 1);
expect(blockchain.lastHeight).toEqual(height);
expect(blockchain.blockMetas.length).toEqual(2);
expect(blockchain.blockMetas[0].header.height).toEqual(height - 1); // upper limit included
expect(blockchain.blockMetas[1].header.height).toEqual(height - 2); // lower limit included
client.disconnect();
});
it("contains all the info", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
const height = (await client.status()).syncInfo.latestBlockHeight;
const blockchain = await client.blockchain(height - 1, height - 1);
expect(blockchain.lastHeight).toEqual(height);
expect(blockchain.blockMetas.length).toBeGreaterThanOrEqual(1);
const meta = blockchain.blockMetas[0];
// TODO: check all the fields
expect(meta).toEqual({
blockId: jasmine.objectContaining({}),
// block_size: jasmine.stringMatching(nonNegativeIntegerMatcher),
// num_txs: jasmine.stringMatching(nonNegativeIntegerMatcher),
header: jasmine.objectContaining({
version: {
block: expected.blockVersion,
app: expected.appVersion,
},
chainId: jasmine.stringMatching(chainIdMatcher),
}),
});
client.disconnect();
});
});
describe("tx", () => {
it("can query a tx properly", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
const find = randomString();
const me = randomString();
const tx = buildKvTx(find, me);
const txRes = await client.broadcastTxCommit({ tx: tx });
expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true);
expect(txRes.height).toBeTruthy();
const height: number = txRes.height || 0; // || 0 for type system
expect(txRes.hash.length).not.toEqual(0);
const hash = txRes.hash;
await tendermintSearchIndexUpdated();
// find by hash - does it match?
const r = await client.tx({ hash: hash, prove: true });
// both values come from rpc, so same type (Buffer/Uint8Array)
expect(r.hash).toEqual(hash);
// force the type when comparing to locally generated value
expect(r.tx).toEqual(tx);
expect(r.height).toEqual(height);
expect(r.proof).toBeTruthy();
// txSearch - you must enable the indexer when running
// tendermint, else you get empty results
const query = buildQuery({ tags: [{ key: "app.key", value: find }] });
const s = await client.txSearch({ query: query, page: 1, per_page: 30 });
// should find the tx
expect(s.totalCount).toEqual(1);
// should return same info as querying directly,
// except without the proof
expect(s.txs[0]).toEqual({ ...r, proof: undefined });
// ensure txSearchAll works as well
const sall = await client.txSearchAll({ query: query });
// should find the tx
expect(sall.totalCount).toEqual(1);
// should return same info as querying directly,
// except without the proof
expect(sall.txs[0]).toEqual({ ...r, proof: undefined });
// and let's query the block itself to see this transaction
const block = await client.block(height);
expect(block.block.txs.length).toEqual(1);
expect(block.block.txs[0]).toEqual(tx);
client.disconnect();
});
});
describe("txSearch", () => {
const key = randomString();
beforeAll(async () => {
if (tendermintEnabled()) {
const client = await Tendermint34Client.create(rpcFactory());
// eslint-disable-next-line no-inner-declarations
async function sendTx(): Promise<void> {
const me = randomString();
const tx = buildKvTx(key, me);
const txRes = await client.broadcastTxCommit({ tx: tx });
expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true);
expect(txRes.height).toBeTruthy();
expect(txRes.hash.length).not.toEqual(0);
}
// send 3 txs
await sendTx();
await sendTx();
await sendTx();
client.disconnect();
await tendermintSearchIndexUpdated();
}
});
it("can paginate over txSearch results", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
const query = buildQuery({ tags: [{ key: "app.key", value: key }] });
// expect one page of results
const s1 = await client.txSearch({ query: query, page: 1, per_page: 2 });
expect(s1.totalCount).toEqual(3);
expect(s1.txs.length).toEqual(2);
// second page
const s2 = await client.txSearch({ query: query, page: 2, per_page: 2 });
expect(s2.totalCount).toEqual(3);
expect(s2.txs.length).toEqual(1);
client.disconnect();
});
it("can get all search results in one call", async () => {
pendingWithoutTendermint();
const client = await Tendermint34Client.create(rpcFactory());
const query = buildQuery({ tags: [{ key: "app.key", value: key }] });
const sall = await client.txSearchAll({ query: query, per_page: 2 });
expect(sall.totalCount).toEqual(3);
expect(sall.txs.length).toEqual(3);
// make sure there are in order from lowest to highest height
const [tx1, tx2, tx3] = sall.txs;
expect(tx2.height).toEqual(tx1.height + 1);
expect(tx3.height).toEqual(tx2.height + 1);
client.disconnect();
});
});
}
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 Tendermint34Client.create(rpcFactory());
const stream = client.subscribeNewBlockHeader();
expect(stream).toBeTruthy();
const subscription = stream.subscribe({
next: (event) => {
expect(event.chainId).toMatch(chainIdMatcher);
expect(event.height).toBeGreaterThan(0);
// seems that tendermint just guarantees within the last second for timestamp
expect(event.time.getTime()).toBeGreaterThan(testStart - 1000);
// 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);
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 Tendermint34Client.create(rpcFactory());
const stream = client.subscribeNewBlock();
const subscription = stream.subscribe({
next: (event) => {
expect(event.header.chainId).toMatch(chainIdMatcher);
expect(event.header.height).toBeGreaterThan(0);
// seems that tendermint just guarantees within the last second for timestamp
expect(event.header.time.getTime()).toBeGreaterThan(testStart - 1000);
// 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);
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(100);
expect(events.length).toEqual(2);
// Block header
expect(events[1].header.height).toEqual(events[0].header.height + 1);
expect(events[1].header.chainId).toEqual(events[0].header.chainId);
expect(events[1].header.time.getTime()).toBeGreaterThan(events[0].header.time.getTime());
expect(events[1].header.appHash).not.toEqual(events[0].header.appHash);
expect(events[1].header.validatorsHash).toEqual(events[0].header.validatorsHash);
// Block body
expect(events[0].txs.length).toEqual(1);
expect(events[1].txs.length).toEqual(1);
expect(events[0].txs[0]).toEqual(transactionData1);
expect(events[1].txs[0]).toEqual(transactionData2);
client.disconnect();
});
it("can subscribe to transaction events", async () => {
pendingWithoutTendermint();
const events: responses.TxEvent[] = [];
const client = await Tendermint34Client.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(100);
expect(events.length).toEqual(2);
// Meta
expect(events[1].height).toEqual(events[0].height + 1);
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 Tendermint34Client.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(100);
expect(events.length).toEqual(2);
// Meta
expect(events[1].height).toEqual(events[0].height + 1);
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 Tendermint34Client.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 Tendermint34Client.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("Tendermint34Client", () => {
const { url, expected } = tendermintInstances[1];
it("can connect to a given url", async () => {
pendingWithoutTendermint();
// default connection
{
const client = await Tendermint34Client.connect(url);
const info = await client.abciInfo();
expect(info).toBeTruthy();
client.disconnect();
}
// http connection
{
const client = await Tendermint34Client.connect("http://" + url);
const info = await client.abciInfo();
expect(info).toBeTruthy();
client.disconnect();
}
// ws connection
{
const client = await Tendermint34Client.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);
});
});

View File

@ -0,0 +1,312 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { Stream } from "xstream";
import { createJsonRpcRequest } from "../jsonrpc";
import {
HttpClient,
instanceOfRpcStreamingClient,
RpcClient,
SubscriptionEvent,
WebsocketClient,
} from "../rpcclients";
import { Decoder, Encoder, Params, Responses } from "./adaptor";
import { adaptor34 } from "./adaptors";
import * as requests from "./requests";
import * as responses from "./responses";
export class Tendermint34Client {
/**
* Creates a new Tendermint client for the given endpoint.
*
* Uses HTTP when the URL schema is http or https. Uses WebSockets otherwise.
*
* If the adaptor is not set an auto-detection is attempted.
*/
public static async connect(url: string): Promise<Tendermint34Client> {
const useHttp = url.startsWith("http://") || url.startsWith("https://");
const rpcClient = useHttp ? new HttpClient(url) : new WebsocketClient(url);
return Tendermint34Client.create(rpcClient);
}
/**
* Creates a new Tendermint client given an RPC client.
*
* If the adaptor is not set an auto-detection is attempted.
*/
public static async create(rpcClient: RpcClient): Promise<Tendermint34Client> {
// For some very strange reason I don't understand, tests start to fail on some systems
// (our CI) when skipping the status call before doing other queries. Sleeping a little
// while did not help. Thus we query the version as a way to say "hi" to the backend,
// even in cases where we don't use the result.
const _version = await this.detectVersion(rpcClient);
return new Tendermint34Client(rpcClient);
}
private static async detectVersion(client: RpcClient): Promise<string> {
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 `Client.connect` or `Client.create` to create an instance.
*/
private constructor(client: RpcClient) {
this.client = client;
this.p = adaptor34.params;
this.r = adaptor34.responses;
}
public disconnect(): void {
this.client.disconnect();
}
public async abciInfo(): Promise<responses.AbciInfoResponse> {
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<responses.AbciQueryResponse> {
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<responses.BlockResponse> {
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<responses.BlockResultsResponse> {
const query: requests.BlockResultsRequest = {
method: requests.Method.BlockResults,
params: { height: height },
};
return this.doCall(query, this.p.encodeBlockResults, this.r.decodeBlockResults);
}
/**
* 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<responses.BlockchainResponse> {
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<responses.BroadcastTxSyncResponse> {
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<responses.BroadcastTxAsyncResponse> {
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<responses.BroadcastTxCommitResponse> {
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<responses.CommitResponse> {
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<responses.GenesisResponse> {
const query: requests.GenesisRequest = { method: requests.Method.Genesis };
return this.doCall(query, this.p.encodeGenesis, this.r.decodeGenesis);
}
public async health(): Promise<responses.HealthResponse> {
const query: requests.HealthRequest = { method: requests.Method.Health };
return this.doCall(query, this.p.encodeHealth, this.r.decodeHealth);
}
public async status(): Promise<responses.StatusResponse> {
const query: requests.StatusRequest = { method: requests.Method.Status };
return this.doCall(query, this.p.encodeStatus, this.r.decodeStatus);
}
public subscribeNewBlock(): Stream<responses.NewBlockEvent> {
const request: requests.SubscribeRequest = {
method: requests.Method.Subscribe,
query: { type: requests.SubscriptionEventType.NewBlock },
};
return this.subscribe(request, this.r.decodeNewBlockEvent);
}
public subscribeNewBlockHeader(): Stream<responses.NewBlockHeaderEvent> {
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<responses.TxEvent> {
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<responses.TxResponse> {
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<responses.TxSearchResponse> {
const query: requests.TxSearchRequest = { params: params, method: requests.Method.TxSearch };
const resp = await this.doCall(query, this.p.encodeTxSearch, this.r.decodeTxSearch);
return {
...resp,
// make sure we sort by height, as tendermint may be sorting by string value of the height
txs: [...resp.txs].sort((a, b) => a.height - b.height),
};
}
// 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<responses.TxSearchResponse> {
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;
}
}
// 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
txs.sort((a, b) => a.height - b.height);
return {
totalCount: txs.length,
txs: txs,
};
}
public async validators(params: requests.ValidatorsParams): Promise<responses.ValidatorsResponse> {
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<responses.ValidatorsResponse> {
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<T extends requests.Request, U extends responses.Response>(
request: T,
encode: Encoder<T>,
decode: Decoder<U>,
): Promise<U> {
const req = encode(request);
const result = await this.client.execute(req);
return decode(result);
}
private subscribe<T>(request: requests.SubscribeRequest, decode: (e: SubscriptionEvent) => T): Stream<T> {
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<T>((event) => {
return decode(event);
});
}
}