tendermint-rpc: Fork package from @iov-one

This commit is contained in:
willclarktech 2020-06-15 16:08:26 +01:00
parent 0c52fc46e4
commit 8e8f29f041
No known key found for this signature in database
GPG Key ID: 551A86E2E398ADF7
115 changed files with 5811 additions and 0 deletions

View File

@ -0,0 +1 @@
../../.eslintignore

3
packages/tendermint-rpc/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/
dist/
docs/

View File

@ -0,0 +1,86 @@
# @iov/tendermint-rpc
[![npm version](https://img.shields.io/npm/v/@iov/tendermint-rpc.svg)](https://www.npmjs.com/package/@iov/tendermint-rpc)
This package provides a type-safe wrapper around
[Tendermint RPC](https://docs.tendermint.com/master/rpc/). Notably, all binary
data is passed in and out as Uint8Array, and this module is reponsible for the
hex/base64 encoding/decoding depending on the field and version of Tendermint.
Also handles converting numbers to and from strings.
## Getting started
The simplest possible use of the module is to assume it does everything
automatically, and call:
```ts
import { Client } from "@iov/tendermint-rpc";
const client = await Client.connect(
"ws://rpc-private-a-x-exchangenet.iov.one:16657",
);
const genesis = await client.genesis();
const status = await client.status();
```
A query to the ABCI application looks like this:
```ts
const results = await client.abciQuery({
path: "/tokens?prefix",
data: new Uint8Array([]),
});
```
## Supported Tendermint versions
| IOV-Core version | Supported Tendermint versions |
| ---------------- | ----------------------------- |
| 1.1 | 0.31.x 0.32.x |
| 1.0 | 0.31.x |
| 0.15 0.17 | 0.29.x 0.31.x |
| 0.12 0.14 | 0.25.x, 0.27.x 0.29.x |
| 0.11 | 0.25.x, 0.27.x |
| 0.9 0.10 | 0.20.x, 0.21.x, 0.25.x |
| 0.1 0.8 | 0.20.x, 0.21.x |
Support for Tendermint versions is determined by demand for IOV's own products.
Please let us know if you need support for other versions of Tendermint or need
long term support for one specific Tendermint version.
## Code Overview
The main entry point is the
[Client](https://iov-one.github.io/iov-core-docs/latest/iov-tendermint-rpc/classes/client.html).
The connection to the blockchain is defined by a flexible
[RpcClient](https://iov-one.github.io/iov-core-docs/latest/iov-tendermint-rpc/interfaces/rpcclient.html)
interface, with implementations for HTTP (POST) and WebSockets. You can add your
own connection type or just wrap one with custom retry rules, error handling,
etc. The RPC client is just responsible for sending JSON-RPC requests and
returning the responses.
The actual domain knowledge is embedded in the
[Adaptor](https://iov-one.github.io/iov-core-docs/latest/iov-tendermint-rpc/interfaces/adaptor.html),
which defines a class for encoding
[Params](https://iov-one.github.io/iov-core-docs/latest/iov-tendermint-rpc/classes/params.html)
and another for decoding
[Responses](https://iov-one.github.io/iov-core-docs/latest/iov-tendermint-rpc/classes/responses.html).
The Tendermint version-specific functionality is implemented in global objects
(like e.g.
[v0_31](https://iov-one.github.io/iov-core-docs/latest/iov-tendermint-rpc/globals.html#v0_31)).
This knowledge is mainly for those who want to add support for new versions,
which should be added to the
[auto-detect method](https://iov-one.github.io/iov-core-docs/latest/iov-tendermint-rpc/classes/client.html#detectversion).
## API Documentation
[https://iov-one.github.io/iov-core-docs/latest/iov-tendermint-rpc/](https://iov-one.github.io/iov-core-docs/latest/iov-tendermint-rpc/)
## License
This package is part of the IOV-Core repository, licensed under the Apache
License 2.0 (see
[NOTICE](https://github.com/iov-one/iov-core/blob/master/NOTICE) and
[LICENSE](https://github.com/iov-one/iov-core/blob/master/LICENSE)).

View File

@ -0,0 +1,26 @@
#!/usr/bin/env node
require("source-map-support").install();
const defaultSpecReporterConfig = require("../../jasmine-spec-reporter.config.json");
// setup Jasmine
const Jasmine = require("jasmine");
const jasmine = new Jasmine();
jasmine.loadConfig({
spec_dir: "build",
spec_files: ["**/*.spec.js"],
helpers: [],
random: false,
seed: null,
stopSpecOnExpectationFailure: false,
});
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 1000;
// setup reporter
const { SpecReporter } = require("jasmine-spec-reporter");
const reporter = new SpecReporter({ ...defaultSpecReporterConfig });
// initialize and execute
jasmine.env.clearReporters();
jasmine.addReporter(reporter);
jasmine.execute();

View File

@ -0,0 +1,45 @@
module.exports = function (config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: ".",
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ["jasmine"],
// list of files / patterns to load in the browser
files: ["dist/web/tests.js"],
client: {
jasmine: {
random: false,
timeoutInterval: 15000,
},
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["progress", "kjhtml"],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["Firefox"],
// Keep brower open for debugging. This is overridden by yarn scripts
singleRun: false,
});
};

View File

@ -0,0 +1 @@
Directory used to trigger lerna package updates for all packages

View File

@ -0,0 +1,53 @@
{
"name": "@iov/tendermint-rpc",
"version": "2.3.2",
"description": "Codec to encode/decode bns transactions and state objects",
"author": "IOV SAS <admin@iov.one>",
"license": "Apache-2.0",
"main": "build/index.js",
"types": "types/index.d.ts",
"files": [
"build/",
"types/",
"*.md",
"!*.spec.*",
"!**/testdata/"
],
"repository": {
"type": "git",
"url": "https://github.com/iov-one/iov-core/tree/master/packages/iov-tendermint-rpc"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"docs": "shx rm -rf docs && typedoc --options typedoc.js",
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\" && tslint -t verbose --project .",
"format": "prettier --write --loglevel warn \"./src/**/*.ts\"",
"format-text": "prettier --write --prose-wrap always --print-width 80 \"./*.md\"",
"test-node": "node jasmine-testrunner.js",
"test-edge": "yarn pack-web && karma start --single-run --browsers Edge",
"test-firefox": "yarn pack-web && karma start --single-run --browsers Firefox",
"test-chrome": "yarn pack-web && karma start --single-run --browsers ChromeHeadless",
"test-safari": "yarn pack-web && karma start --single-run --browsers Safari",
"test": "yarn build-or-skip && yarn test-node",
"move-types": "shx rm -r ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts && shx rm ./types/**/*.spec.d.ts",
"format-types": "prettier --write --loglevel warn \"./types/**/*.d.ts\"",
"build": "shx rm -rf ./build && tsc && yarn move-types && yarn format-types",
"build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build",
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
},
"dependencies": {
"@iov/crypto": "^2.3.2",
"@iov/encoding": "^2.3.2",
"@iov/jsonrpc": "^2.3.2",
"@iov/socket": "^2.3.2",
"axios": "^0.19.0",
"readonly-date": "^1.0.0",
"type-tagger": "^1.0.0",
"xstream": "^11.10.0"
},
"devDependencies": {
"@iov/utils": "^2.3.2"
}
}

View File

@ -0,0 +1,59 @@
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@iov/jsonrpc";
import * as requests from "./requests";
import * as responses from "./responses";
import { SubscriptionEvent } from "./rpcclients";
import { BlockHash, TxBytes, TxHash } from "./types";
export interface Adaptor {
readonly params: Params;
readonly responses: Responses;
readonly hashTx: (tx: TxBytes) => TxHash;
readonly hashBlock: (header: responses.Header) => BlockHash;
}
// 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,21 @@
// This module exposes translators for multiple tendermint versions
// Pick a version that matches the server to properly encode the data types
import { Adaptor } from "./adaptor";
import { v0_31 } from "./v0-31";
import { v0_32 } from "./v0-32";
/**
* Returns an Adaptor implementation for a given tendermint version.
* Throws when version is not supported.
*
* @param version full Tendermint version string, e.g. "0.20.1"
*/
export function adaptorForVersion(version: string): Adaptor {
if (version.startsWith("0.31.")) {
return v0_31;
} else if (version.startsWith("0.32.")) {
return v0_32;
} else {
throw new Error(`Unsupported tendermint version: ${version}`);
}
}

View File

@ -0,0 +1,548 @@
// tslint:disable:readonly-array
import { toAscii } from "@iov/encoding";
import { firstEvent, toListPromise } from "@iov/stream";
import { sleep } from "@iov/utils";
import { ReadonlyDate } from "readonly-date";
import { Stream } from "xstream";
import { Adaptor } from "./adaptor";
import { adaptorForVersion } from "./adaptorforversion";
import { Client } from "./client";
import { tendermintInstances } from "./config.spec";
import { buildQuery } from "./requests";
import * as responses from "./responses";
import { HttpClient, RpcClient, WebsocketClient } from "./rpcclients";
import { TxBytes } from "./types";
function pendingWithoutTendermint(): void {
if (!process.env.TENDERMINT_ENABLED) {
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(50);
}
function buildKvTx(k: string, v: string): TxBytes {
return toAscii(`${k}=${v}`) as TxBytes;
}
function randomString(): string {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from({ length: 12 })
.map(() => alphabet[Math.floor(Math.random() * alphabet.length)])
.join("");
}
function defaultTestSuite(rpcFactory: () => RpcClient, adaptor: Adaptor): void {
it("can connect to tendermint with known version", async () => {
pendingWithoutTendermint();
const client = new Client(rpcFactory(), adaptor);
expect(await client.abciInfo()).toBeTruthy();
client.disconnect();
});
it("can auto-discover tendermint version and connect", async () => {
pendingWithoutTendermint();
const client = await Client.detectVersion(rpcFactory());
const info = await client.abciInfo();
expect(info).toBeTruthy();
client.disconnect();
});
it("can post a transaction", async () => {
pendingWithoutTendermint();
const client = new Client(rpcFactory(), adaptor);
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 = new Client(rpcFactory(), adaptor);
const tx = buildKvTx(randomString(), randomString());
const calculatedTxHash = adaptor.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 = new Client(rpcFactory(), adaptor);
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 call a bunch of methods", async () => {
pendingWithoutTendermint();
const client = new Client(rpcFactory(), adaptor);
expect(await client.block()).toBeTruthy();
expect(await client.blockchain(2, 4)).toBeTruthy();
expect(await client.blockResults(3)).toBeTruthy();
expect(await client.commit(4)).toBeTruthy();
expect(await client.genesis()).toBeTruthy();
expect(await client.health()).toBeNull();
expect(await client.validators()).toBeTruthy();
client.disconnect();
});
it("can call status", async () => {
pendingWithoutTendermint();
const client = new Client(rpcFactory(), adaptor);
const status = await client.status();
expect(status.nodeInfo.other.size).toBeGreaterThanOrEqual(2);
expect(status.nodeInfo.other.get("tx_index")).toEqual("on");
expect(status.validatorInfo.pubkey).toBeTruthy();
expect(status.validatorInfo.votingPower).toBeGreaterThan(0);
expect(status.syncInfo.catchingUp).toEqual(false);
expect(status.syncInfo.latestBlockHeight).toBeGreaterThanOrEqual(1);
client.disconnect();
});
it("can query a tx properly", async () => {
pendingWithoutTendermint();
const client = new Client(rpcFactory(), adaptor);
const find = randomString();
const me = randomString();
const tx = buildKvTx(find, me);
const txRes = await client.broadcastTxCommit({ tx: tx });
expect(responses.broadcastTxCommitSuccess(txRes)).toEqual(true);
expect(txRes.height).toBeTruthy();
const height: number = txRes.height || 0; // || 0 for type system
expect(txRes.hash.length).not.toEqual(0);
const hash = txRes.hash;
await tendermintSearchIndexUpdated();
// find by hash - does it match?
const r = await client.tx({ hash: hash, prove: true });
// both values come from rpc, so same type (Buffer/Uint8Array)
expect(r.hash).toEqual(hash);
// force the type when comparing to locally generated value
expect(r.tx).toEqual(tx);
expect(r.height).toEqual(height);
expect(r.proof).toBeTruthy();
// txSearch - you must enable the indexer when running
// tendermint, else you get empty results
const query = buildQuery({ tags: [{ key: "app.key", value: find }] });
// eslint-disable-next-line @typescript-eslint/camelcase
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.blockMeta.header.numTxs).toEqual(1);
expect(block.block.txs.length).toEqual(1);
expect(block.block.txs[0]).toEqual(tx);
client.disconnect();
});
it("can paginate over txSearch results", async () => {
pendingWithoutTendermint();
const client = new Client(rpcFactory(), adaptor);
const find = randomString();
const query = buildQuery({ tags: [{ key: "app.key", value: find }] });
async function sendTx(): Promise<void> {
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();
expect(txRes.hash.length).not.toEqual(0);
}
// send 3 txs
await sendTx();
await sendTx();
await sendTx();
await tendermintSearchIndexUpdated();
// expect one page of results
// eslint-disable-next-line @typescript-eslint/camelcase
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
// eslint-disable-next-line @typescript-eslint/camelcase
const s2 = await client.txSearch({ query: query, page: 2, per_page: 2 });
expect(s2.totalCount).toEqual(3);
expect(s2.txs.length).toEqual(1);
// and all together now
// eslint-disable-next-line @typescript-eslint/camelcase
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, adaptor: Adaptor, appCreator: string): void {
it("can subscribe to block header events", (done) => {
pendingWithoutTendermint();
const testStart = ReadonlyDate.now();
(async () => {
const events: responses.NewBlockHeaderEvent[] = [];
const client = new Client(rpcFactory(), adaptor);
const stream = client.subscribeNewBlockHeader();
expect(stream).toBeTruthy();
const subscription = stream.subscribe({
next: (event) => {
expect(event.chainId).toMatch(/^[-a-zA-Z0-9]{3,30}$/);
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.numTxs).toEqual(0);
expect(event.lastBlockId).toBeTruthy();
expect(event.totalTxs).toBeGreaterThan(0);
// 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].totalTxs).toEqual(events[0].totalTxs);
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 = new Client(rpcFactory(), adaptor);
const stream = client.subscribeNewBlock();
const subscription = stream.subscribe({
next: (event) => {
expect(event.header.chainId).toMatch(/^[-a-zA-Z0-9]{3,30}$/);
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.numTxs).toEqual(1);
expect(event.header.lastBlockId).toBeTruthy();
expect(event.header.totalTxs).toBeGreaterThan(0);
// 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.totalTxs).toEqual(events[0].header.totalTxs + 1);
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 = new Client(rpcFactory(), adaptor);
const stream = client.subscribeTx();
const subscription = stream.subscribe({
next: (event) => {
expect(event.height).toBeGreaterThan(0);
expect(event.index).toEqual(0);
expect(event.result).toBeTruthy();
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);
if (events[1].result.tags && events[0].result.tags) {
expect(events[1].result.tags).not.toEqual(events[0].result.tags);
}
if (events[1].result.events && events[0].result.events) {
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 = new Client(rpcFactory(), adaptor);
const query = buildQuery({ tags: [{ key: "app.creator", value: appCreator }] });
const stream = client.subscribeTx(query);
expect(stream).toBeTruthy();
const subscription = stream.subscribe({
next: (event) => {
expect(event.height).toBeGreaterThan(0);
expect(event.index).toEqual(0);
expect(event.result).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);
// Meta
expect(events[1].height).toEqual(events[0].height + 1);
if (events[1].result.tags && events[0].result.tags) {
expect(events[1].result.tags).not.toEqual(events[0].result.tags);
}
if (events[1].result.events && events[0].result.events) {
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 = new Client(rpcFactory(), adaptor);
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 = new Client(rpcFactory(), adaptor);
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();
});
}
for (const { url, version, appCreator } of tendermintInstances) {
describe(`Client ${version}`, () => {
it("can connect to a given url", async () => {
pendingWithoutTendermint();
// default connection
{
const client = await Client.connect(url);
const info = await client.abciInfo();
expect(info).toBeTruthy();
client.disconnect();
}
// http connection
{
const client = await Client.connect("http://" + url);
const info = await client.abciInfo();
expect(info).toBeTruthy();
client.disconnect();
}
// ws connection
{
const client = await Client.connect("ws://" + url);
const info = await client.abciInfo();
expect(info).toBeTruthy();
client.disconnect();
}
});
describe("With HttpClient", () => {
const adaptor = adaptorForVersion(version);
defaultTestSuite(() => new HttpClient(url), adaptor);
});
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);
const adaptor = adaptorForVersion(version);
defaultTestSuite(factory, adaptor);
websocketTestSuite(factory, adaptor, appCreator);
});
});
}

View File

@ -0,0 +1,253 @@
import { Stream } from "xstream";
import { Adaptor, Decoder, Encoder, Params, Responses } from "./adaptor";
import { adaptorForVersion } from "./adaptorforversion";
import { createJsonRpcRequest } from "./jsonrpc";
import * as requests from "./requests";
import * as responses from "./responses";
import {
HttpClient,
instanceOfRpcStreamingClient,
RpcClient,
SubscriptionEvent,
WebsocketClient,
} from "./rpcclients";
export class Client {
public static async connect(url: string): Promise<Client> {
const useHttp = url.startsWith("http://") || url.startsWith("https://");
const client = useHttp ? new HttpClient(url) : new WebsocketClient(url);
return this.detectVersion(client);
}
public static async detectVersion(client: RpcClient): Promise<Client> {
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 new Client(client, adaptorForVersion(version));
}
private readonly client: RpcClient;
private readonly p: Params;
private readonly r: Responses;
public constructor(client: RpcClient, adaptor: Adaptor) {
this.client = client;
this.p = adaptor.params;
this.r = adaptor.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);
}
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?: requests.QueryString): 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;
// tslint:disable-next-line:readonly-array
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(height?: number): Promise<responses.ValidatorsResponse> {
const query: requests.ValidatorsRequest = {
method: requests.Method.Validators,
params: { height: height },
};
return this.doCall(query, this.p.encodeValidators, this.r.decodeValidators);
}
// 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);
});
}
}

View File

@ -0,0 +1,32 @@
export interface TendermintInstance {
readonly url: string;
readonly version: string;
readonly appCreator: string;
}
/**
* Tendermint instances to be tested.
*
* Testing different versions: as a convention, the minor version number is encoded
* in the port 111<version>, e.g. Tendermint 0.21.0 runs on port 11121. To start
* a specific version use:
* TENDERMINT_VERSION=0.29.2 TENDERMINT_PORT=11129 ./scripts/tendermint/start.sh
*
* When more than 1 instances of tendermint are running, stop them manually:
* docker container ls | grep tendermint/tendermint
* docker container kill <container id from 1st column>
*/
export const tendermintInstances: readonly TendermintInstance[] = [
{
url: "localhost:11131",
version: "0.31.x",
appCreator: "Cosmoshi Netowoko",
},
{
url: "localhost:11132",
version: "0.32.x",
appCreator: "Cosmoshi Netowoko",
},
];
export const defaultInstance: TendermintInstance = tendermintInstances[0];

View File

@ -0,0 +1,91 @@
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);
// tslint:disable-next-line:no-object-mutation
(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,231 @@
import { fromBase64, fromHex, fromRfc3339, Int53, toBase64, toHex, toUtf8 } from "@iov/encoding";
import { As } from "type-tagger";
import { BlockId, ReadonlyDateWithNanoseconds, Version } from "./responses";
export type Base64String = string & As<"base64">;
export type HexString = string & As<"hex">;
export type IntegerString = string & As<"integer">;
export type DateTimeString = string & As<"datetime">;
/**
* 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 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: any): Map<string, string> {
const out = new Map<string, string>();
for (const key of Object.keys(obj)) {
const value: unknown = 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: IntegerString | number): number {
const asInt = typeof input === "number" ? new Int53(input) : Int53.fromString(input);
return asInt.toNumber();
}
public static encode(num: number): IntegerString {
return new Int53(num).toString() as IntegerString;
}
}
export class Base64 {
public static encode(data: Uint8Array): Base64String {
return toBase64(data) as Base64String;
}
public static decode(base64String: Base64String): Uint8Array {
return fromBase64(base64String);
}
}
export class DateTime {
public static decode(dateTimeString: DateTimeString): ReadonlyDateWithNanoseconds {
const readonlyDate = fromRfc3339(dateTimeString);
const nanosecondsMatch = dateTimeString.match(/\.(\d+)Z$/);
const nanoseconds = nanosecondsMatch ? nanosecondsMatch[1].slice(3) : "";
// tslint:disable-next-line:no-object-mutation
(readonlyDate as any).nanoseconds = parseInt(nanoseconds.padEnd(6, "0"), 10);
return readonlyDate as ReadonlyDateWithNanoseconds;
}
}
export class Hex {
public static encode(data: Uint8Array): HexString {
return toHex(data) as HexString;
}
public static decode(hexString: HexString): Uint8Array {
return fromHex(hexString);
}
}
// 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 {
// tslint: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,40 @@
// exported to access version-specific hashing
export { v0_31 } from "./v0-31";
export { v0_32 } from "./v0-32";
export { Client } from "./client";
export {
AbciInfoRequest,
AbciQueryParams,
AbciQueryRequest,
BlockRequest,
BlockchainRequest,
BlockResultsRequest,
BroadcastTxRequest,
BroadcastTxParams,
CommitRequest,
GenesisRequest,
HealthRequest,
Method,
Request,
QueryString,
QueryTag,
StatusRequest,
SubscriptionEventType,
TxParams,
TxRequest,
TxSearchParams,
TxSearchRequest,
ValidatorsRequest,
} from "./requests";
export * from "./responses";
export { HttpClient, WebsocketClient } from "./rpcclients"; // TODO: Why do we export those outside of this package?
export {
IpPortString,
TxBytes,
TxHash,
ValidatorEd25519Pubkey,
ValidatorEd25519Signature,
ValidatorPubkey,
ValidatorSignature,
} from "./types";

View File

@ -0,0 +1,25 @@
import { createJsonRpcRequest } from "./jsonrpc";
describe("jsonrpc", () => {
describe("createJsonRpcRequest", () => {
it("generates proper object with correct method", () => {
const request = createJsonRpcRequest("do_something");
expect(request.jsonrpc).toEqual("2.0");
expect(request.id).toMatch(/^[a-zA-Z0-9]{12}$/);
expect(request.method).toEqual("do_something");
});
it("generates distinct IDs", () => {
const request1 = createJsonRpcRequest("foo");
const request2 = createJsonRpcRequest("foo");
expect(request2.id).not.toEqual(request1.id);
});
it("copies params", () => {
const params = { foo: "bar" };
const request = createJsonRpcRequest("some_method", params);
expect(request.params).toEqual(params);
expect(request.params).not.toBe(params);
});
});
});

View File

@ -0,0 +1,25 @@
import { JsonRpcRequest } from "@iov/jsonrpc";
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
/** generates a random alphanumeric character */
function randomChar(): string {
return alphabet[Math.floor(Math.random() * alphabet.length)];
}
function randomId(): string {
return Array.from({ length: 12 })
.map(() => randomChar())
.join("");
}
/** Creates a JSON-RPC request with random ID */
export function createJsonRpcRequest(method: string, params?: {}): JsonRpcRequest {
const paramsCopy = params ? { ...params } : {};
return {
jsonrpc: "2.0",
id: randomId(),
method: method,
params: paramsCopy,
};
}

View File

@ -0,0 +1,41 @@
import { buildQuery, QueryString } 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" as QueryString });
expect(query).toEqual("aabbCCDD");
});
it("works for mixed input", () => {
const query = buildQuery({
tags: [
{ key: "k", value: "9" },
{ key: "L", value: "7" },
],
raw: "aabbCCDD" as QueryString,
});
expect(query).toEqual("k='9' AND L='7' AND aabbCCDD");
});
});
});

View File

@ -0,0 +1,179 @@
import { As } from "type-tagger";
/**
* 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",
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?: QueryString;
};
}
export type QueryString = string & As<"query">;
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: QueryString;
readonly prove?: boolean;
readonly page?: number;
readonly per_page?: number;
}
export interface ValidatorsRequest {
readonly method: Method.Validators;
readonly params: {
readonly height?: number;
};
}
export interface BuildQueryComponents {
readonly tags?: readonly QueryTag[];
readonly raw?: QueryString;
}
export function buildQuery(components: BuildQueryComponents): QueryString {
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 ") as QueryString;
}

View File

@ -0,0 +1,331 @@
import { ReadonlyDate } from "readonly-date";
import { IpPortString, TxBytes, TxHash, ValidatorPubkey, ValidatorSignature } 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 AbciQueryResponse {
readonly key: Uint8Array;
readonly value: Uint8Array;
readonly height?: number;
readonly index?: number;
readonly code?: number; // non-falsy for errors
readonly log?: string;
}
export interface BlockResponse {
readonly blockMeta: BlockMeta;
readonly block: Block;
}
export interface BlockResultsResponse {
readonly height: number;
readonly results: readonly TxData[];
readonly endBlock: {
readonly validatorUpdates: readonly Validator[];
readonly consensusUpdates?: ConsensusParams;
readonly tags?: readonly Tag[];
};
}
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: TxHash;
}
/**
* 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: TxHash;
readonly checkTx: TxData;
readonly deliverTx?: TxData;
}
/**
* Returns true iff transaction made it sucessfully into a block
* (i.e. sucess in `check_tx` and `deliver_tx` field)
*/
export function broadcastTxCommitSuccess(res: BroadcastTxCommitResponse): boolean {
// code must be 0 on success
// deliverTx may be present but empty on failure
return res.checkTx.code === 0 && !!res.deliverTx && res.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: {} | 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: TxBytes;
readonly hash: TxHash;
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 results: readonly Validator[];
}
// Events
export interface NewBlockEvent extends Block {}
export interface NewBlockHeaderEvent extends Header {}
export interface TxEvent {
readonly tx: TxBytes;
readonly hash: TxHash;
readonly height: number;
readonly index: number;
readonly result: TxData;
}
export const getTxEventHeight = (event: TxEvent): number => event.height;
export const getHeaderEventHeight = (event: NewBlockHeaderEvent): number => event.height;
export const getBlockEventHeight = (event: NewBlockEvent): number => event.header.height;
// Helper items used above
export interface Tag {
readonly key: Uint8Array;
readonly value: Uint8Array;
}
export interface Event {
readonly type: string;
readonly attributes: readonly Tag[];
}
export interface TxData {
readonly code: number;
readonly log?: string;
readonly data?: Uint8Array;
readonly tags?: readonly Tag[];
readonly events?: readonly Event[];
// readonly fees?: any;
}
export interface TxProof {
readonly data: Uint8Array;
readonly rootHash: Uint8Array;
readonly proof: {
readonly total: number;
readonly index: number;
/** Optional because does not exist in Tendermint 0.25.x */
readonly leafHash?: Uint8Array;
readonly aunts: readonly Uint8Array[];
};
}
export interface BlockMeta {
readonly blockId: BlockId;
readonly header: Header;
}
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 precommits: readonly Vote[];
}
/**
* 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: ValidatorSignature;
}
export interface Version {
readonly block: number;
readonly app: number;
}
export interface ReadonlyDateWithNanoseconds extends ReadonlyDate {
/* Nanoseconds after the time stored in a vanilla ReadonlyDate (millisecond granularity) */
readonly nanoseconds?: 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;
readonly numTxs: number;
readonly totalTxs: number;
// 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;
readonly listenAddr: IpPortString;
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;
}
// this is in status
export interface Validator {
readonly address?: Uint8Array;
readonly pubkey: ValidatorPubkey;
readonly votingPower: number;
readonly accum?: number;
readonly name?: string;
}
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 maxAge: number;
}

View File

@ -0,0 +1,33 @@
import { defaultInstance } from "../config.spec";
import { createJsonRpcRequest } from "../jsonrpc";
import { Method } from "../requests";
import { HttpClient } from "./httpclient";
function pendingWithoutTendermint(): void {
if (!process.env.TENDERMINT_ENABLED) {
pending("Set TENDERMINT_ENABLED to enable tendermint rpc tests");
}
}
describe("HttpClient", () => {
const tendermintUrl = defaultInstance.url;
it("can make a simple call", async () => {
pendingWithoutTendermint();
const client = new HttpClient(tendermintUrl);
const healthResponse = await client.execute(createJsonRpcRequest(Method.Health));
expect(healthResponse.result).toEqual({});
const statusResponse = await client.execute(createJsonRpcRequest(Method.Status));
expect(statusResponse.result).toBeTruthy();
expect(statusResponse.result.node_info).toBeTruthy();
await client
.execute(createJsonRpcRequest("no-such-method"))
.then(() => fail("must not resolve"))
.catch((error) => expect(error).toBeTruthy());
client.disconnect();
});
});

View File

@ -0,0 +1,57 @@
import {
isJsonRpcErrorResponse,
JsonRpcRequest,
JsonRpcSuccessResponse,
parseJsonRpcResponse,
} from "@iov/jsonrpc";
import axios from "axios";
import { hasProtocol, RpcClient } from "./rpcclient";
// Global symbols in some environments
// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
declare const fetch: any | undefined;
function filterBadStatus(res: any): any {
if (res.status >= 400) {
throw new Error(`Bad status on response: ${res.status}`);
}
return res;
}
/**
* Helper to work around missing CORS support in Tendermint (https://github.com/tendermint/tendermint/pull/2800)
*
* For some reason, fetch does not complain about missing server-side CORS support.
*/
async function http(method: "POST", url: string, request?: any): Promise<any> {
if (typeof fetch !== "undefined") {
const body = request ? JSON.stringify(request) : undefined;
return fetch(url, { method: method, body: body })
.then(filterBadStatus)
.then((res: any) => res.json());
} else {
return axios.request({ url: url, method: method, data: request }).then((res) => res.data);
}
}
export class HttpClient implements RpcClient {
protected readonly url: string;
public constructor(url = "http://localhost:46657") {
// accept host.name:port and assume http protocol
this.url = hasProtocol(url) ? url : "http://" + url;
}
public disconnect(): void {
// nothing to be done
}
public async execute(request: JsonRpcRequest): Promise<JsonRpcSuccessResponse> {
const response = parseJsonRpcResponse(await http("POST", this.url, request));
if (isJsonRpcErrorResponse(response)) {
throw new Error(JSON.stringify(response.error));
}
return response;
}
}

View File

@ -0,0 +1,5 @@
// This folder contains Tendermint-specific RPC clients
export { instanceOfRpcStreamingClient, RpcClient, RpcStreamingClient, SubscriptionEvent } from "./rpcclient";
export { HttpClient } from "./httpclient";
export { WebsocketClient } from "./websocketclient";

View File

@ -0,0 +1,44 @@
import { defaultInstance } from "../config.spec";
import { createJsonRpcRequest } from "../jsonrpc";
import { Method } from "../requests";
import { HttpClient } from "./httpclient";
import { instanceOfRpcStreamingClient } from "./rpcclient";
import { WebsocketClient } from "./websocketclient";
function pendingWithoutTendermint(): void {
if (!process.env.TENDERMINT_ENABLED) {
pending("Set TENDERMINT_ENABLED to enable tendermint rpc tests");
}
}
describe("RpcClient", () => {
const tendermintUrl = defaultInstance.url;
it("has working instanceOfRpcStreamingClient()", async () => {
pendingWithoutTendermint();
const httpClient = new HttpClient(tendermintUrl);
const wsClient = new WebsocketClient(tendermintUrl);
expect(instanceOfRpcStreamingClient(httpClient)).toEqual(false);
expect(instanceOfRpcStreamingClient(wsClient)).toEqual(true);
httpClient.disconnect();
await wsClient.connected();
wsClient.disconnect();
});
it("should also work with trailing slashes", async () => {
pendingWithoutTendermint();
const statusRequest = createJsonRpcRequest(Method.Status);
const httpClient = new HttpClient(tendermintUrl + "/");
expect(await httpClient.execute(statusRequest)).toBeDefined();
httpClient.disconnect();
const wsClient = new WebsocketClient(tendermintUrl + "/");
expect(await wsClient.execute(statusRequest)).toBeDefined();
wsClient.disconnect();
});
});

View File

@ -0,0 +1,36 @@
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@iov/jsonrpc";
import { Stream } from "xstream";
/**
* An event emitted from Tendermint after subscribing via RPC.
*
* These events are passed as the `result` of JSON-RPC responses, which is kind
* of hacky because it breaks the idea that exactly one JSON-RPC response belongs
* to each JSON-RPC request. But this is how subscriptions work in Tendermint.
*/
export interface SubscriptionEvent {
readonly query: string;
readonly data: {
readonly type: string;
readonly value: any;
};
}
export interface RpcClient {
readonly execute: (request: JsonRpcRequest) => Promise<JsonRpcSuccessResponse>;
readonly disconnect: () => void;
}
export interface RpcStreamingClient extends RpcClient {
readonly listen: (request: JsonRpcRequest) => Stream<SubscriptionEvent>;
}
export function instanceOfRpcStreamingClient(client: RpcClient): client is RpcStreamingClient {
return typeof (client as any).listen === "function";
}
// Helpers for all RPC clients
export function hasProtocol(url: string): boolean {
return url.search("://") !== -1;
}

View File

@ -0,0 +1,210 @@
import { toListPromise } from "@iov/stream";
import { Stream } from "xstream";
import { defaultInstance } from "../config.spec";
import { Integer } from "../encodings";
import { createJsonRpcRequest } from "../jsonrpc";
import { Method } from "../requests";
import { SubscriptionEvent } from "./rpcclient";
import { WebsocketClient } from "./websocketclient";
function pendingWithoutTendermint(): void {
if (!process.env.TENDERMINT_ENABLED) {
pending("Set TENDERMINT_ENABLED to enable tendermint rpc tests");
}
}
describe("WebsocketClient", () => {
const tendermintUrl = defaultInstance.url;
it("can make a simple call", async () => {
pendingWithoutTendermint();
const client = new WebsocketClient(tendermintUrl);
const healthResponse = await client.execute(createJsonRpcRequest(Method.Health));
expect(healthResponse.result).toEqual({});
const statusResponse = await client.execute(createJsonRpcRequest(Method.Status));
expect(statusResponse.result).toBeTruthy();
expect(statusResponse.result.node_info).toBeTruthy();
await client
.execute(createJsonRpcRequest("no-such-method"))
.then(() => fail("must not resolve"))
.catch((error) => expect(error).toBeTruthy());
client.disconnect();
});
it("can listen to events", (done) => {
pendingWithoutTendermint();
const client = new WebsocketClient(tendermintUrl);
const query = "tm.event='NewBlockHeader'";
const req = createJsonRpcRequest("subscribe", { query: query });
const headers = client.listen(req);
// tslint:disable-next-line:readonly-array
const events: SubscriptionEvent[] = [];
const sub = headers.subscribe({
error: done.fail,
complete: () => done.fail("subscription should not complete"),
next: (evt: SubscriptionEvent) => {
events.push(evt);
expect(evt.query).toEqual(query);
if (events.length === 2) {
// make sure they are consequtive heights
const height = (i: number): number => Integer.parse(events[i].data.value.header.height);
expect(height(1)).toEqual(height(0) + 1);
sub.unsubscribe();
// wait 1.5s and check we did not get more events
setTimeout(() => {
expect(events.length).toEqual(2);
client.disconnect();
done();
}, 1500);
}
},
});
});
it("can listen to the same query twice", async () => {
pendingWithoutTendermint();
const client = new WebsocketClient(tendermintUrl);
const newBlockHeaderQuery = "tm.event='NewBlockHeader'";
// we need two requests with unique IDs
const request1 = createJsonRpcRequest("subscribe", { query: newBlockHeaderQuery });
const request2 = createJsonRpcRequest("subscribe", { query: newBlockHeaderQuery });
const stream1 = client.listen(request1);
const stream2 = client.listen(request2);
const eventHeights = await toListPromise(
Stream.merge(stream1, stream2).map((event) => {
// height is string or number, depending on Tendermint version. But we don't care in this case
return event.data.value.header.height;
}),
4,
);
expect(new Set(eventHeights).size).toEqual(2);
client.disconnect();
});
it("can execute commands while listening to events", (done) => {
pendingWithoutTendermint();
const client = new WebsocketClient(tendermintUrl);
const query = "tm.event='NewBlockHeader'";
const req = createJsonRpcRequest("subscribe", { query: query });
const headers = client.listen(req);
// tslint:disable-next-line:readonly-array
const events: SubscriptionEvent[] = [];
const sub = headers.subscribe({
error: done.fail,
complete: () => done.fail("subscription should not complete"),
next: (evt: SubscriptionEvent) => {
events.push(evt);
expect(evt.query).toEqual(query);
if (events.length === 2) {
sub.unsubscribe();
// wait 1.5s and check we did not get more events
setTimeout(() => {
expect(events.length).toEqual(2);
client.disconnect();
done();
}, 1500);
}
},
});
client
.execute(createJsonRpcRequest(Method.Status))
.then((startusResponse) => expect(startusResponse).toBeTruthy())
.catch(done.fail);
});
it("can end event listening by disconnecting", (done) => {
pendingWithoutTendermint();
const client = new WebsocketClient(tendermintUrl);
const query = "tm.event='NewBlockHeader'";
const req = createJsonRpcRequest("subscribe", { query: query });
const headers = client.listen(req);
// tslint:disable-next-line:readonly-array
const receivedEvents: SubscriptionEvent[] = [];
setTimeout(() => client.disconnect(), 1500);
headers.subscribe({
error: done.fail,
next: (event: SubscriptionEvent) => receivedEvents.push(event),
complete: () => {
expect(receivedEvents.length).toEqual(1);
done();
},
});
});
it("fails when executing on a disconnected client", async () => {
pendingWithoutTendermint();
const client = new WebsocketClient(tendermintUrl);
// dummy command to ensure client is connected
await client.execute(createJsonRpcRequest(Method.Health));
client.disconnect();
await client
.execute(createJsonRpcRequest(Method.Health))
.then(() => fail("must not resolve"))
.catch((error) => expect(error).toMatch(/socket has disconnected/i));
});
it("fails when listening to a disconnected client", (done) => {
pendingWithoutTendermint();
// async and done does not work together with pending() in Jasmine 2.8
(async () => {
const client = new WebsocketClient(tendermintUrl);
// dummy command to ensure client is connected
await client.execute(createJsonRpcRequest(Method.Health));
client.disconnect();
const query = "tm.event='NewBlockHeader'";
const req = createJsonRpcRequest("subscribe", { query: query });
expect(() => client.listen(req).subscribe({})).toThrowError(/socket has disconnected/i);
done();
})().catch(done.fail);
});
it("cannot listen to simple requests", async () => {
pendingWithoutTendermint();
const client = new WebsocketClient(tendermintUrl);
const req = createJsonRpcRequest(Method.Health);
expect(() => client.listen(req)).toThrowError(/request method must be "subscribe"/i);
await client.connected();
client.disconnect();
});
});

View File

@ -0,0 +1,212 @@
/* tslint:disable:readonly-keyword readonly-array no-object-mutation */
import {
isJsonRpcErrorResponse,
JsonRpcId,
JsonRpcRequest,
JsonRpcResponse,
JsonRpcSuccessResponse,
parseJsonRpcResponse,
} from "@iov/jsonrpc";
import { ConnectionStatus, ReconnectingSocket, SocketWrapperMessageEvent } from "@iov/socket";
import { firstEvent } from "@iov/stream";
import { Listener, Producer, Stream, Subscription } from "xstream";
import { hasProtocol, RpcStreamingClient, SubscriptionEvent } from "./rpcclient";
function defaultErrorHandler(error: any): never {
throw error;
}
function toJsonRpcResponse(message: SocketWrapperMessageEvent): JsonRpcResponse {
// this should never happen, but I want an alert if it does
if (message.type !== "message") {
throw new Error(`Unexcepted message type on websocket: ${message.type}`);
}
const jsonRpcEvent = parseJsonRpcResponse(JSON.parse(message.data));
return jsonRpcEvent;
}
class RpcEventProducer implements Producer<SubscriptionEvent> {
private readonly request: JsonRpcRequest;
private readonly socket: ReconnectingSocket;
private running = false;
private subscriptions: Subscription[] = [];
public constructor(request: JsonRpcRequest, socket: ReconnectingSocket) {
this.request = request;
this.socket = socket;
}
/**
* Implementation of Producer.start
*/
public start(listener: Listener<SubscriptionEvent>): void {
if (this.running) {
throw Error("Already started. Please stop first before restarting.");
}
this.running = true;
this.connectToClient(listener);
this.socket.queueRequest(JSON.stringify(this.request));
}
/**
* Implementation of Producer.stop
*
* Called by the stream when the stream's last listener stopped listening
* or when the producer completed.
*/
public stop(): void {
this.running = false;
// Tell the server we are done in order to save resources. We cannot wait for the result.
// This may fail when socket connection is not open, thus ignore errors in queueRequest
const endRequest: JsonRpcRequest = { ...this.request, method: "unsubscribe" };
try {
this.socket.queueRequest(JSON.stringify(endRequest));
} catch (error) {
if (error instanceof Error && error.message.match(/socket has disconnected/i)) {
// ignore
} else {
throw error;
}
}
}
protected connectToClient(listener: Listener<SubscriptionEvent>): void {
const responseStream = this.socket.events.map(toJsonRpcResponse);
// this should unsubscribe itself, so doesn't need to be removed explicitly
const idSubscription = responseStream
.filter((response) => response.id === this.request.id)
.subscribe({
next: (response) => {
if (isJsonRpcErrorResponse(response)) {
this.closeSubscriptions();
listener.error(JSON.stringify(response.error));
}
idSubscription.unsubscribe();
},
});
// this will fire on a response (success or error)
// Tendermint adds an "#event" suffix for events that follow a previous subscription
// https://github.com/tendermint/tendermint/blob/v0.23.0/rpc/core/events.go#L107
const idEventSubscription = responseStream
.filter((response) => response.id === `${this.request.id}#event`)
.subscribe({
next: (response) => {
if (isJsonRpcErrorResponse(response)) {
this.closeSubscriptions();
listener.error(JSON.stringify(response.error));
} else {
listener.next(response.result as SubscriptionEvent);
}
},
});
// this will fire in case the websocket disconnects cleanly
const nonResponseSubscription = responseStream.subscribe({
error: (error) => {
this.closeSubscriptions();
listener.error(error);
},
complete: () => {
this.closeSubscriptions();
listener.complete();
},
});
this.subscriptions.push(idSubscription, idEventSubscription, nonResponseSubscription);
}
protected closeSubscriptions(): void {
for (const subscription of this.subscriptions) {
subscription.unsubscribe();
}
// clear unused subscriptions
this.subscriptions = [];
}
}
export class WebsocketClient implements RpcStreamingClient {
private readonly url: string;
private readonly socket: ReconnectingSocket;
/** Same events as in socket.events but in the format we need */
private readonly jsonRpcResponseStream: Stream<JsonRpcResponse>;
// Lazily create streams and use the same stream when listening to the same query twice.
//
// Creating streams is cheap since producer is not started as long as nobody listens to events. Thus this
// map is never cleared and there is no need to do so. But unsubscribe all the subscriptions!
private readonly subscriptionStreams = new Map<string, Stream<SubscriptionEvent>>();
public constructor(baseUrl = "ws://localhost:46657", onError: (err: any) => void = defaultErrorHandler) {
// accept host.name:port and assume ws protocol
// make sure we don't end up with ...//websocket
const path = baseUrl.endsWith("/") ? "websocket" : "/websocket";
const cleanBaseUrl = hasProtocol(baseUrl) ? baseUrl : "ws://" + baseUrl;
this.url = cleanBaseUrl + path;
this.socket = new ReconnectingSocket(this.url);
const errorSubscription = this.socket.events.subscribe({
error: (error) => {
onError(error);
errorSubscription.unsubscribe();
},
});
this.jsonRpcResponseStream = this.socket.events.map(toJsonRpcResponse);
this.socket.connect();
}
public async execute(request: JsonRpcRequest): Promise<JsonRpcSuccessResponse> {
const pendingResponse = this.responseForRequestId(request.id);
this.socket.queueRequest(JSON.stringify(request));
const response = await pendingResponse;
if (isJsonRpcErrorResponse(response)) {
throw new Error(JSON.stringify(response.error));
}
return response;
}
public listen(request: JsonRpcRequest): Stream<SubscriptionEvent> {
if (request.method !== "subscribe") {
throw new Error(`Request method must be "subscribe" to start event listening`);
}
const query = (request.params as any).query;
if (typeof query !== "string") {
throw new Error("request.params.query must be a string");
}
if (!this.subscriptionStreams.has(query)) {
const producer = new RpcEventProducer(request, this.socket);
const stream = Stream.create(producer);
this.subscriptionStreams.set(query, stream);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.subscriptionStreams.get(query)!;
}
/**
* Resolves as soon as websocket is connected. execute() queues requests automatically,
* so this should be required for testing purposes only.
*/
public async connected(): Promise<void> {
await this.socket.connectionStatus.waitFor(ConnectionStatus.Connected);
}
public disconnect(): void {
this.socket.disconnect();
}
protected async responseForRequestId(id: JsonRpcId): Promise<JsonRpcResponse> {
return firstEvent(this.jsonRpcResponseStream.filter((r) => r.id === id));
}
}

View File

@ -0,0 +1,41 @@
// Types in this file are exported outside of the @iov/tendermint-rpc package,
// e.g. as part of a request or response
import { As } from "type-tagger";
/**
* Merkle root
*/
export type BlockHash = Uint8Array & As<"block-hash">;
/** Raw transaction bytes */
export type TxBytes = Uint8Array & As<"tx-bytes">;
/**
* A raw tendermint transaction hash, currently 20 bytes
*/
export type TxHash = Uint8Array & As<"tx-hash">;
export type IpPortString = string & As<"ipport">;
export interface ValidatorEd25519Pubkey {
readonly algorithm: "ed25519";
readonly data: Uint8Array;
}
/**
* Union type for different possible pubkeys.
* Currently only Ed25519 supported.
*/
export type ValidatorPubkey = ValidatorEd25519Pubkey;
export interface ValidatorEd25519Signature {
readonly algorithm: "ed25519";
readonly data: Uint8Array;
}
/**
* Union type for different possible voting signatures.
* Currently only Ed25519 supported.
*/
export type ValidatorSignature = ValidatorEd25519Signature;

View File

@ -0,0 +1,98 @@
import { fromBase64, fromHex } from "@iov/encoding";
import { ReadonlyDate } from "readonly-date";
import { ReadonlyDateWithNanoseconds } from "../responses";
import { TxBytes } from "../types";
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==") as TxBytes;
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:11131/block"
const blockId = fromHex("5B5D3F7E77A4BD6CB6067947E478BC3BD493DD24A981535F0ADEBDAAA0498480");
const time = new ReadonlyDate("2019-09-19T10:41:24.898178746Z");
// tslint:disable-next-line:no-object-mutation
(time as any).nanoseconds = 178746;
const blockData = {
version: {
block: 10,
app: 1,
},
chainId: "test-chain-RRlV24",
height: 2195,
time: time as ReadonlyDateWithNanoseconds,
numTxs: 0,
totalTxs: 20,
lastBlockId: {
hash: fromHex("1D38C4FE5C1D8C3CC1F47602BF107C9B269BA7DA3514DEDF958F5A33AB75C06B"),
parts: {
total: 1,
hash: fromHex("C441341B7D846DDA6AF72F83DF68C9AF93665FE5280B136CA29C7411D280DAEC"),
},
},
lastCommitHash: fromHex("0C5EEF7AE1275337BFAA173F57799AA90830E74AFF3FB03D1F579DA37BCAEAB1"),
dataHash: fromHex(""),
validatorsHash: fromHex("44D7D0BE3C70B58DA87696102E3A52E5C9FA98A717E56D02987DA8CAE86F03F4"),
nextValidatorsHash: fromHex("44D7D0BE3C70B58DA87696102E3A52E5C9FA98A717E56D02987DA8CAE86F03F4"),
consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"),
appHash: fromHex("2800000000000000"),
lastResultsHash: fromHex(""),
evidenceHash: fromHex(""),
proposerAddress: fromHex("057B8C349E591579EDFCC0E5D5402E3076E99675"),
};
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:11131/block?height=5940"
const blockId = fromHex("1C4777AFBBA49E15D031A830E62E7BE986823938732B872C02B8A3D16BD3163B");
const time = new ReadonlyDate("2019-09-24T10:51:28.240847497Z");
// tslint:disable-next-line:no-object-mutation
(time as any).nanoseconds = 847497;
const blockData = {
version: {
block: 10,
app: 1,
},
chainId: "test-chain-lY9FO6",
height: 5940,
time: time as ReadonlyDateWithNanoseconds,
numTxs: 1,
totalTxs: 61,
lastBlockId: {
hash: fromHex("D2983E6AEEFC55E0A46565CD2274CCD21CB013F5602B0C35A423A99D1120DB13"),
parts: {
total: 1,
hash: fromHex("AA55D7F92AD3A9CFDA8C5E45F95B03AEF9FB38AB984FD762E5CE20791324369D"),
},
},
lastCommitHash: fromHex("5DBFFDBE41878AEB947176D3E0B0DC70850B0A61F8B709ED132FEA59664DFCE5"),
dataHash: fromHex("90FE1A62418F68B411915EEF6792B134693D9D0148432BA661D91213B0CCD15A"),
validatorsHash: fromHex("0A4647900ED90CC605E851BBB4946D7B9D1830F293BC87F3CE16AEFF4E4C77E2"),
nextValidatorsHash: fromHex("0A4647900ED90CC605E851BBB4946D7B9D1830F293BC87F3CE16AEFF4E4C77E2"),
consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"),
appHash: fromHex("7800000000000000"),
lastResultsHash: fromHex("6E340B9CFFB37A989CA544E6BB780A2C78901D3FB33738768511A30617AFA01D"),
evidenceHash: fromHex(""),
proposerAddress: fromHex("6BCBB90987613FE15D3DEFA4920E9F98425698FF"),
};
expect(hashBlock(blockData)).toEqual(blockId);
});
});

View File

@ -0,0 +1,71 @@
import { Sha256 } from "@iov/crypto";
import { encodeBlockId, encodeBytes, encodeInt, encodeString, encodeTime, encodeVersion } from "../encodings";
import { Header } from "../responses";
import { BlockHash, TxBytes, TxHash } from "../types";
// hash is sha256
// https://github.com/tendermint/tendermint/blob/master/UPGRADING.md#v0260
export function hashTx(tx: TxBytes): TxHash {
const hash = new Sha256(tx).digest();
return hash as TxHash;
}
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): BlockHash {
const encodedFields: readonly Uint8Array[] = [
encodeVersion(header.version),
encodeString(header.chainId),
encodeInt(header.height),
encodeTime(header.time),
encodeInt(header.numTxs),
encodeInt(header.totalTxs),
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) as BlockHash;
}

View File

@ -0,0 +1,12 @@
import { Adaptor } from "../adaptor";
import { hashBlock, hashTx } from "./hasher";
import { Params } from "./requests";
import { Responses } from "./responses";
// tslint:disable-next-line:variable-name
export const v0_31: Adaptor = {
params: Params,
responses: Responses,
hashTx: hashTx,
hashBlock: hashBlock,
};

View File

@ -0,0 +1,142 @@
import { toHex } from "@iov/encoding";
import { JsonRpcRequest } from "@iov/jsonrpc";
import { assertNotEmpty, Base64, Base64String, HexString, Integer, IntegerString, may } from "../encodings";
import { createJsonRpcRequest } from "../jsonrpc";
import * as requests from "../requests";
interface HeightParam {
readonly height?: number;
}
interface RpcHeightParam {
readonly height?: IntegerString;
}
function encodeHeightParam(param: HeightParam): RpcHeightParam {
return {
height: may(Integer.encode, param.height),
};
}
interface RpcBlockchainRequestParams {
readonly minHeight?: IntegerString;
readonly maxHeight?: IntegerString;
}
function encodeBlockchainRequestParams(param: requests.BlockchainRequestParams): RpcBlockchainRequestParams {
return {
minHeight: may(Integer.encode, param.minHeight),
maxHeight: may(Integer.encode, param.maxHeight),
};
}
interface RpcAbciQueryParams {
readonly path: string;
readonly data: HexString;
readonly height?: string;
readonly prove?: boolean;
}
function encodeAbciQueryParams(params: requests.AbciQueryParams): RpcAbciQueryParams {
return {
path: assertNotEmpty(params.path),
data: toHex(params.data) as HexString,
height: may(Integer.encode, params.height),
prove: params.prove,
};
}
interface RpcBroadcastTxParams {
readonly tx: Base64String;
}
function encodeBroadcastTxParams(params: requests.BroadcastTxParams): RpcBroadcastTxParams {
return {
tx: Base64.encode(assertNotEmpty(params.tx)),
};
}
interface RpcTxParams {
readonly hash: Base64String;
readonly prove?: boolean;
}
function encodeTxParams(params: requests.TxParams): RpcTxParams {
return {
hash: Base64.encode(assertNotEmpty(params.hash)),
prove: params.prove,
};
}
interface RpcTxSearchParams {
readonly query: requests.QueryString;
readonly prove?: boolean;
readonly page?: IntegerString;
readonly per_page?: IntegerString;
}
function encodeTxSearchParams(params: requests.TxSearchParams): RpcTxSearchParams {
return {
query: params.query,
prove: params.prove,
page: may(Integer.encode, params.page),
// eslint-disable-next-line @typescript-eslint/camelcase
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, encodeHeightParam(req.params));
}
}

View File

@ -0,0 +1,773 @@
import { fromHex } from "@iov/encoding";
import { JsonRpcSuccessResponse } from "@iov/jsonrpc";
import {
assertArray,
assertBoolean,
assertNotEmpty,
assertNumber,
assertObject,
assertSet,
Base64,
Base64String,
DateTime,
DateTimeString,
dictionaryToStringMap,
Hex,
HexString,
Integer,
IntegerString,
may,
optional,
} from "../encodings";
import * as responses from "../responses";
import { SubscriptionEvent } from "../rpcclients";
import { IpPortString, TxBytes, TxHash, ValidatorPubkey, ValidatorSignature } from "../types";
import { hashTx } from "./hasher";
interface AbciInfoResult {
readonly response: RpcAbciInfoResponse;
}
interface RpcAbciInfoResponse {
readonly data?: string;
readonly last_block_height?: IntegerString;
readonly last_block_app_hash?: Base64String;
}
function decodeAbciInfo(data: RpcAbciInfoResponse): responses.AbciInfoResponse {
return {
data: data.data,
lastBlockHeight: may(Integer.parse, data.last_block_height),
lastBlockAppHash: may(Base64.decode, data.last_block_app_hash),
};
}
interface AbciQueryResult {
readonly response: RpcAbciQueryResponse;
}
interface RpcAbciQueryResponse {
readonly key: Base64String;
readonly value?: Base64String;
readonly proof?: Base64String;
readonly height?: IntegerString;
readonly index?: IntegerString;
readonly code?: IntegerString; // only for errors
readonly log?: string;
}
function decodeAbciQuery(data: RpcAbciQueryResponse): responses.AbciQueryResponse {
return {
key: Base64.decode(optional(data.key, "" as Base64String)),
value: Base64.decode(optional(data.value, "" as Base64String)),
// proof: may(Base64.decode, data.proof),
height: may(Integer.parse, data.height),
code: may(Integer.parse, data.code),
index: may(Integer.parse, data.index),
log: data.log,
};
}
interface RpcTag {
readonly key: Base64String;
readonly value: Base64String;
}
function decodeTag(tag: RpcTag): responses.Tag {
return {
key: Base64.decode(assertNotEmpty(tag.key)),
value: Base64.decode(assertNotEmpty(tag.value)),
};
}
function decodeTags(tags: readonly RpcTag[]): readonly responses.Tag[] {
return assertArray(tags).map(decodeTag);
}
interface RpcTxData {
readonly code?: number;
readonly log?: string;
readonly data?: Base64String;
readonly tags?: readonly RpcTag[];
}
function decodeTxData(data: RpcTxData): responses.TxData {
return {
data: may(Base64.decode, data.data),
log: data.log,
code: Integer.parse(assertNumber(optional<number>(data.code, 0))),
tags: may(decodeTags, data.tags),
};
}
// yes, a different format for status and dump consensus state
interface RpcPubkey {
readonly type: string;
readonly value: Base64String;
}
function decodePubkey(data: RpcPubkey): ValidatorPubkey {
if (data.type === "tendermint/PubKeyEd25519") {
// go-amino special code
return {
algorithm: "ed25519",
data: Base64.decode(assertNotEmpty(data.value)),
};
}
throw new Error(`unknown pubkey type: ${data.type}`);
}
// for evidence, block results, etc.
interface RpcValidatorUpdate {
readonly address: HexString;
readonly pub_key: RpcPubkey;
readonly voting_power: IntegerString;
}
function decodeValidatorUpdate(data: RpcValidatorUpdate): responses.Validator {
return {
pubkey: decodePubkey(assertObject(data.pub_key)),
votingPower: Integer.parse(assertNotEmpty(data.voting_power)),
address: Hex.decode(assertNotEmpty(data.address)),
};
}
interface RpcBlockParams {
readonly max_bytes: IntegerString;
readonly max_gas: IntegerString;
}
/**
* 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: IntegerString;
}
function decodeEvidenceParams(data: RpcEvidenceParams): responses.EvidenceParams {
return {
maxAge: Integer.parse(assertNotEmpty(data.max_age)),
};
}
/**
* Example data:
* {
* "block": {
* "max_bytes": "22020096",
* "max_gas": "-1",
* "time_iota_ms": "1000"
* },
* "evidence": {
* "max_age": "100000"
* },
* "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: IntegerString;
readonly results: {
readonly DeliverTx: readonly RpcTxData[];
readonly EndBlock: {
readonly validator_updates?: readonly RpcValidatorUpdate[];
readonly consensus_param_updates?: RpcConsensusParams;
readonly tags?: readonly RpcTag[];
};
};
}
function decodeBlockResults(data: RpcBlockResultsResponse): responses.BlockResultsResponse {
const res = optional(data.results.DeliverTx, [] as readonly RpcTxData[]);
const end = data.results.EndBlock;
const validators = optional(end.validator_updates, [] as readonly RpcValidatorUpdate[]);
return {
height: Integer.parse(assertNotEmpty(data.height)),
results: assertArray(res).map(decodeTxData),
endBlock: {
validatorUpdates: assertArray(validators).map(decodeValidatorUpdate),
consensusUpdates: may(decodeConsensusParams, end.consensus_param_updates),
tags: may(decodeTags, end.tags),
},
};
}
interface RpcBlockId {
readonly hash: HexString;
readonly parts: {
readonly total: IntegerString;
readonly hash: HexString;
};
}
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: IntegerString;
readonly app: IntegerString;
}
function decodeBlockVersion(data: RpcBlockVersion): responses.Version {
return {
block: Integer.parse(data.block),
app: Integer.parse(data.app),
};
}
interface RpcHeader {
readonly version: RpcBlockVersion;
readonly chain_id: string;
readonly height: IntegerString;
readonly time: DateTimeString;
readonly num_txs: IntegerString;
readonly total_txs: IntegerString;
readonly last_block_id: RpcBlockId;
readonly last_commit_hash: HexString;
readonly data_hash: HexString;
readonly validators_hash: HexString;
readonly next_validators_hash: HexString;
readonly consensus_hash: HexString;
readonly app_hash: HexString;
readonly last_results_hash: HexString;
readonly evidence_hash: HexString;
readonly proposer_address: HexString;
}
function decodeHeader(data: RpcHeader): responses.Header {
return {
version: decodeBlockVersion(data.version),
chainId: assertNotEmpty(data.chain_id),
height: Integer.parse(assertNotEmpty(data.height)),
time: DateTime.decode(assertNotEmpty(data.time)),
numTxs: Integer.parse(assertNotEmpty(data.num_txs)),
totalTxs: Integer.parse(assertNotEmpty(data.total_txs)),
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: IntegerString;
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 {
readonly hash: HexString;
}
function decodeBroadcastTxSync(data: RpcBroadcastTxSyncResponse): responses.BroadcastTxSyncResponse {
return {
...decodeTxData(data),
hash: fromHex(assertNotEmpty(data.hash)) as TxHash,
};
}
interface RpcBroadcastTxCommitResponse {
readonly height?: IntegerString;
readonly hash: HexString;
readonly check_tx: RpcTxData;
readonly deliver_tx?: RpcTxData;
}
function decodeBroadcastTxCommit(data: RpcBroadcastTxCommitResponse): responses.BroadcastTxCommitResponse {
return {
height: may(Integer.parse, data.height),
hash: fromHex(assertNotEmpty(data.hash)) as TxHash,
checkTx: decodeTxData(assertObject(data.check_tx)),
deliverTx: may(decodeTxData, data.deliver_tx),
};
}
type RpcSignature = Base64String;
function decodeSignature(data: RpcSignature): ValidatorSignature {
return {
algorithm: "ed25519",
data: Base64.decode(assertNotEmpty(data)),
};
}
interface RpcVote {
readonly type: number;
readonly validator_address: HexString;
readonly validator_index: IntegerString;
readonly height: IntegerString;
readonly round: IntegerString;
readonly timestamp: DateTimeString;
readonly block_id: RpcBlockId;
readonly signature: RpcSignature;
}
function decodeVote(data: RpcVote): responses.Vote {
return {
type: Integer.parse(assertNumber(data.type)),
validatorAddress: fromHex(assertNotEmpty(data.validator_address)),
validatorIndex: Integer.parse(assertNotEmpty(data.validator_index)),
height: Integer.parse(assertNotEmpty(data.height)),
round: Integer.parse(assertNotEmpty(data.round)),
timestamp: DateTime.decode(assertNotEmpty(data.timestamp)),
blockId: decodeBlockId(assertObject(data.block_id)),
signature: decodeSignature(assertNotEmpty(data.signature)),
};
}
interface RpcCommit {
readonly block_id: RpcBlockId;
readonly precommits: readonly RpcVote[];
}
function decodeCommit(data: RpcCommit): responses.Commit {
return {
blockId: decodeBlockId(assertObject(data.block_id)),
precommits: assertArray(data.precommits).map(decodeVote),
};
}
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 {
readonly pub_key: RpcPubkey;
readonly power: IntegerString;
readonly name?: string;
}
function decodeValidatorGenesis(data: RpcValidatorGenesis): responses.Validator {
return {
pubkey: decodePubkey(assertObject(data.pub_key)),
votingPower: Integer.parse(assertNotEmpty(data.power)),
name: data.name,
};
}
interface RpcGenesisResponse {
readonly genesis_time: DateTimeString;
readonly chain_id: string;
readonly consensus_params: RpcConsensusParams;
readonly validators: readonly RpcValidatorGenesis[];
readonly app_hash: HexString;
readonly app_state: {} | undefined;
}
interface GenesisResult {
readonly genesis: RpcGenesisResponse;
}
function decodeGenesis(data: RpcGenesisResponse): responses.GenesisResponse {
return {
genesisTime: DateTime.decode(assertNotEmpty(data.genesis_time)),
chainId: assertNotEmpty(data.chain_id),
consensusParams: decodeConsensusParams(data.consensus_params),
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 {
readonly address: HexString;
readonly pub_key: RpcPubkey;
readonly voting_power: IntegerString;
}
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 {
readonly id: HexString;
readonly listen_addr: IpPortString;
readonly network: string;
readonly version: string;
readonly channels: string; // ???
readonly moniker: string;
readonly protocol_version: {
readonly p2p: IntegerString;
readonly block: IntegerString;
readonly app: IntegerString;
};
/**
* Additional information. E.g.
* {
* "tx_index": "on",
* "rpc_address":"tcp://0.0.0.0:26657"
* }
*/
readonly other: object;
}
function decodeNodeInfo(data: RpcNodeInfo): responses.NodeInfo {
return {
id: fromHex(assertNotEmpty(data.id)),
listenAddr: assertNotEmpty(data.listen_addr),
network: assertNotEmpty(data.network),
version: assertNotEmpty(data.version),
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 {
readonly latest_block_hash: HexString;
readonly latest_app_hash: HexString;
readonly latest_block_height: IntegerString;
readonly latest_block_time: DateTimeString;
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: DateTime.decode(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:
* {
* "RootHash": "10A1A17D5F818099B5CAB5B91733A3CC27C0DB6CE2D571AC27FB970C314308BB",
* "Data": "ZVlERVhDV2lVNEUwPXhTUjc4Tmp2QkNVSg==",
* "Proof": {
* "total": "1",
* "index": "0",
* "leaf_hash": "EKGhfV+BgJm1yrW5FzOjzCfA22zi1XGsJ/uXDDFDCLs=",
* "aunts": []
* }
* }
*/
interface RpcTxProof {
readonly Data: Base64String;
readonly RootHash: HexString;
readonly Proof: {
readonly total: IntegerString;
readonly index: IntegerString;
readonly leaf_hash: Base64String;
readonly aunts: readonly Base64String[];
};
}
function decodeTxProof(data: RpcTxProof): responses.TxProof {
return {
data: Base64.decode(assertNotEmpty(data.Data)),
rootHash: fromHex(assertNotEmpty(data.RootHash)),
proof: {
total: Integer.parse(assertNotEmpty(data.Proof.total)),
index: Integer.parse(assertNotEmpty(data.Proof.index)),
leafHash: Base64.decode(assertNotEmpty(data.Proof.leaf_hash)),
aunts: assertArray(data.Proof.aunts).map(Base64.decode),
},
};
}
interface RpcTxResponse {
readonly tx: Base64String;
readonly tx_result: RpcTxData;
readonly height: IntegerString;
readonly index: number;
readonly hash: HexString;
readonly proof?: RpcTxProof;
}
function decodeTxResponse(data: RpcTxResponse): responses.TxResponse {
return {
tx: Base64.decode(assertNotEmpty(data.tx)) as TxBytes,
result: decodeTxData(assertObject(data.tx_result)),
height: Integer.parse(assertNotEmpty(data.height)),
index: Integer.parse(assertNumber(data.index)),
hash: fromHex(assertNotEmpty(data.hash)) as TxHash,
proof: may(decodeTxProof, data.proof),
};
}
interface RpcTxSearchResponse {
readonly txs: readonly RpcTxResponse[];
readonly total_count: IntegerString;
}
function decodeTxSearch(data: RpcTxSearchResponse): responses.TxSearchResponse {
return {
totalCount: Integer.parse(assertNotEmpty(data.total_count)),
txs: assertArray(data.txs).map(decodeTxResponse),
};
}
interface RpcTxEvent {
readonly tx: Base64String;
readonly result: RpcTxData;
readonly height: IntegerString;
readonly index: number;
}
function decodeTxEvent(data: RpcTxEvent): responses.TxEvent {
const tx = Base64.decode(assertNotEmpty(data.tx)) as TxBytes;
return {
tx: tx,
hash: hashTx(tx),
result: decodeTxData(data.result),
height: Integer.parse(assertNotEmpty(data.height)),
index: Integer.parse(assertNumber(data.index)),
};
}
// for validators
interface RpcValidatorData extends RpcValidatorUpdate {
readonly accum?: IntegerString;
}
function decodeValidatorData(data: RpcValidatorData): responses.Validator {
return {
...decodeValidatorUpdate(data),
accum: may(Integer.parse, data.accum),
};
}
interface RpcValidatorsResponse {
readonly block_height: IntegerString;
readonly validators: readonly RpcValidatorData[];
}
function decodeValidators(data: RpcValidatorsResponse): responses.ValidatorsResponse {
return {
blockHeight: Integer.parse(assertNotEmpty(data.block_height)),
results: assertArray(data.validators).map(decodeValidatorData),
};
}
interface RpcEvidence {
readonly type: string;
readonly validator: RpcValidatorUpdate;
readonly height: IntegerString;
readonly time: IntegerString;
readonly totalVotingPower: IntegerString;
}
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: {
readonly txs?: readonly Base64String[];
};
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(Base64.decode) : [],
evidence: data.evidence && may(decodeEvidences, data.evidence.evidence),
};
}
interface RpcBlockResponse {
readonly block_meta: RpcBlockMeta;
readonly block: RpcBlock;
}
function decodeBlockResponse(data: RpcBlockResponse): responses.BlockResponse {
return {
blockMeta: decodeBlockMeta(data.block_meta),
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,98 @@
import { fromBase64, fromHex } from "@iov/encoding";
import { ReadonlyDate } from "readonly-date";
import { ReadonlyDateWithNanoseconds } from "../responses";
import { TxBytes } from "../types";
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==") as TxBytes;
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:11131/block"
const blockId = fromHex("5B5D3F7E77A4BD6CB6067947E478BC3BD493DD24A981535F0ADEBDAAA0498480");
const time = new ReadonlyDate("2019-09-19T10:41:24.898178746Z");
// tslint:disable-next-line:no-object-mutation
(time as any).nanoseconds = 178746;
const blockData = {
version: {
block: 10,
app: 1,
},
chainId: "test-chain-RRlV24",
height: 2195,
time: time as ReadonlyDateWithNanoseconds,
numTxs: 0,
totalTxs: 20,
lastBlockId: {
hash: fromHex("1D38C4FE5C1D8C3CC1F47602BF107C9B269BA7DA3514DEDF958F5A33AB75C06B"),
parts: {
total: 1,
hash: fromHex("C441341B7D846DDA6AF72F83DF68C9AF93665FE5280B136CA29C7411D280DAEC"),
},
},
lastCommitHash: fromHex("0C5EEF7AE1275337BFAA173F57799AA90830E74AFF3FB03D1F579DA37BCAEAB1"),
dataHash: fromHex(""),
validatorsHash: fromHex("44D7D0BE3C70B58DA87696102E3A52E5C9FA98A717E56D02987DA8CAE86F03F4"),
nextValidatorsHash: fromHex("44D7D0BE3C70B58DA87696102E3A52E5C9FA98A717E56D02987DA8CAE86F03F4"),
consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"),
appHash: fromHex("2800000000000000"),
lastResultsHash: fromHex(""),
evidenceHash: fromHex(""),
proposerAddress: fromHex("057B8C349E591579EDFCC0E5D5402E3076E99675"),
};
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:11131/block?height=5940"
const blockId = fromHex("1C4777AFBBA49E15D031A830E62E7BE986823938732B872C02B8A3D16BD3163B");
const time = new ReadonlyDate("2019-09-24T10:51:28.240847497Z");
// tslint:disable-next-line:no-object-mutation
(time as any).nanoseconds = 847497;
const blockData = {
version: {
block: 10,
app: 1,
},
chainId: "test-chain-lY9FO6",
height: 5940,
time: time as ReadonlyDateWithNanoseconds,
numTxs: 1,
totalTxs: 61,
lastBlockId: {
hash: fromHex("D2983E6AEEFC55E0A46565CD2274CCD21CB013F5602B0C35A423A99D1120DB13"),
parts: {
total: 1,
hash: fromHex("AA55D7F92AD3A9CFDA8C5E45F95B03AEF9FB38AB984FD762E5CE20791324369D"),
},
},
lastCommitHash: fromHex("5DBFFDBE41878AEB947176D3E0B0DC70850B0A61F8B709ED132FEA59664DFCE5"),
dataHash: fromHex("90FE1A62418F68B411915EEF6792B134693D9D0148432BA661D91213B0CCD15A"),
validatorsHash: fromHex("0A4647900ED90CC605E851BBB4946D7B9D1830F293BC87F3CE16AEFF4E4C77E2"),
nextValidatorsHash: fromHex("0A4647900ED90CC605E851BBB4946D7B9D1830F293BC87F3CE16AEFF4E4C77E2"),
consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"),
appHash: fromHex("7800000000000000"),
lastResultsHash: fromHex("6E340B9CFFB37A989CA544E6BB780A2C78901D3FB33738768511A30617AFA01D"),
evidenceHash: fromHex(""),
proposerAddress: fromHex("6BCBB90987613FE15D3DEFA4920E9F98425698FF"),
};
expect(hashBlock(blockData)).toEqual(blockId);
});
});

View File

@ -0,0 +1,71 @@
import { Sha256 } from "@iov/crypto";
import { encodeBlockId, encodeBytes, encodeInt, encodeString, encodeTime, encodeVersion } from "../encodings";
import { Header } from "../responses";
import { BlockHash, TxBytes, TxHash } from "../types";
// hash is sha256
// https://github.com/tendermint/tendermint/blob/master/UPGRADING.md#v0260
export function hashTx(tx: TxBytes): TxHash {
const hash = new Sha256(tx).digest();
return hash as TxHash;
}
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): BlockHash {
const encodedFields: readonly Uint8Array[] = [
encodeVersion(header.version),
encodeString(header.chainId),
encodeInt(header.height),
encodeTime(header.time),
encodeInt(header.numTxs),
encodeInt(header.totalTxs),
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) as BlockHash;
}

View File

@ -0,0 +1,12 @@
import { Adaptor } from "../adaptor";
import { hashBlock, hashTx } from "./hasher";
import { Params } from "./requests";
import { Responses } from "./responses";
// tslint:disable-next-line:variable-name
export const v0_32: Adaptor = {
params: Params,
responses: Responses,
hashTx: hashTx,
hashBlock: hashBlock,
};

View File

@ -0,0 +1,142 @@
import { toHex } from "@iov/encoding";
import { JsonRpcRequest } from "@iov/jsonrpc";
import { assertNotEmpty, Base64, Base64String, HexString, Integer, IntegerString, may } from "../encodings";
import { createJsonRpcRequest } from "../jsonrpc";
import * as requests from "../requests";
interface HeightParam {
readonly height?: number;
}
interface RpcHeightParam {
readonly height?: IntegerString;
}
function encodeHeightParam(param: HeightParam): RpcHeightParam {
return {
height: may(Integer.encode, param.height),
};
}
interface RpcBlockchainRequestParams {
readonly minHeight?: IntegerString;
readonly maxHeight?: IntegerString;
}
function encodeBlockchainRequestParams(param: requests.BlockchainRequestParams): RpcBlockchainRequestParams {
return {
minHeight: may(Integer.encode, param.minHeight),
maxHeight: may(Integer.encode, param.maxHeight),
};
}
interface RpcAbciQueryParams {
readonly path: string;
readonly data: HexString;
readonly height?: string;
readonly prove?: boolean;
}
function encodeAbciQueryParams(params: requests.AbciQueryParams): RpcAbciQueryParams {
return {
path: assertNotEmpty(params.path),
data: toHex(params.data) as HexString,
height: may(Integer.encode, params.height),
prove: params.prove,
};
}
interface RpcBroadcastTxParams {
readonly tx: Base64String;
}
function encodeBroadcastTxParams(params: requests.BroadcastTxParams): RpcBroadcastTxParams {
return {
tx: Base64.encode(assertNotEmpty(params.tx)),
};
}
interface RpcTxParams {
readonly hash: Base64String;
readonly prove?: boolean;
}
function encodeTxParams(params: requests.TxParams): RpcTxParams {
return {
hash: Base64.encode(assertNotEmpty(params.hash)),
prove: params.prove,
};
}
interface RpcTxSearchParams {
readonly query: requests.QueryString;
readonly prove?: boolean;
readonly page?: IntegerString;
readonly per_page?: IntegerString;
}
function encodeTxSearchParams(params: requests.TxSearchParams): RpcTxSearchParams {
return {
query: params.query,
prove: params.prove,
page: may(Integer.encode, params.page),
// eslint-disable-next-line @typescript-eslint/camelcase
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, encodeHeightParam(req.params));
}
}

View File

@ -0,0 +1,790 @@
import { fromHex } from "@iov/encoding";
import { JsonRpcSuccessResponse } from "@iov/jsonrpc";
import {
assertArray,
assertBoolean,
assertNotEmpty,
assertNumber,
assertObject,
assertSet,
Base64,
Base64String,
DateTime,
DateTimeString,
dictionaryToStringMap,
Hex,
HexString,
Integer,
IntegerString,
may,
optional,
} from "../encodings";
import * as responses from "../responses";
import { SubscriptionEvent } from "../rpcclients";
import { IpPortString, TxBytes, TxHash, ValidatorPubkey, ValidatorSignature } from "../types";
import { hashTx } from "./hasher";
interface AbciInfoResult {
readonly response: RpcAbciInfoResponse;
}
interface RpcAbciInfoResponse {
readonly data?: string;
readonly last_block_height?: IntegerString;
readonly last_block_app_hash?: Base64String;
}
function decodeAbciInfo(data: RpcAbciInfoResponse): responses.AbciInfoResponse {
return {
data: data.data,
lastBlockHeight: may(Integer.parse, data.last_block_height),
lastBlockAppHash: may(Base64.decode, data.last_block_app_hash),
};
}
interface AbciQueryResult {
readonly response: RpcAbciQueryResponse;
}
interface RpcAbciQueryResponse {
readonly key: Base64String;
readonly value?: Base64String;
readonly proof?: Base64String;
readonly height?: IntegerString;
readonly index?: IntegerString;
readonly code?: IntegerString; // only for errors
readonly log?: string;
}
function decodeAbciQuery(data: RpcAbciQueryResponse): responses.AbciQueryResponse {
return {
key: Base64.decode(optional(data.key, "" as Base64String)),
value: Base64.decode(optional(data.value, "" as Base64String)),
// proof: may(Base64.decode, data.proof),
height: may(Integer.parse, data.height),
code: may(Integer.parse, data.code),
index: may(Integer.parse, data.index),
log: data.log,
};
}
interface RpcTag {
readonly key: Base64String;
readonly value: Base64String;
}
function decodeTag(tag: RpcTag): responses.Tag {
return {
key: Base64.decode(assertNotEmpty(tag.key)),
value: Base64.decode(assertNotEmpty(tag.value)),
};
}
function decodeTags(tags: readonly RpcTag[]): readonly responses.Tag[] {
return assertArray(tags).map(decodeTag);
}
interface RpcEvent {
readonly type: string;
readonly attributes: readonly RpcTag[];
}
function decodeEvent(event: RpcEvent): responses.Event {
return {
type: event.type,
attributes: decodeTags(event.attributes),
};
}
function decodeEvents(events: readonly RpcEvent[]): readonly responses.Event[] {
return assertArray(events).map(decodeEvent);
}
interface RpcTxData {
readonly code?: number;
readonly log?: string;
readonly data?: Base64String;
readonly events?: readonly RpcEvent[];
}
function decodeTxData(data: RpcTxData): responses.TxData {
return {
data: may(Base64.decode, data.data),
log: data.log,
code: Integer.parse(assertNumber(optional<number>(data.code, 0))),
events: may(decodeEvents, data.events),
};
}
// yes, a different format for status and dump consensus state
interface RpcPubkey {
readonly type: string;
readonly value: Base64String;
}
function decodePubkey(data: RpcPubkey): ValidatorPubkey {
if (data.type === "tendermint/PubKeyEd25519") {
// go-amino special code
return {
algorithm: "ed25519",
data: Base64.decode(assertNotEmpty(data.value)),
};
}
throw new Error(`unknown pubkey type: ${data.type}`);
}
// for evidence, block results, etc.
interface RpcValidatorUpdate {
readonly address: HexString;
readonly pub_key: RpcPubkey;
readonly voting_power: IntegerString;
}
function decodeValidatorUpdate(data: RpcValidatorUpdate): responses.Validator {
return {
pubkey: decodePubkey(assertObject(data.pub_key)),
votingPower: Integer.parse(assertNotEmpty(data.voting_power)),
address: Hex.decode(assertNotEmpty(data.address)),
};
}
interface RpcBlockParams {
readonly max_bytes: IntegerString;
readonly max_gas: IntegerString;
}
/**
* 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: IntegerString;
}
function decodeEvidenceParams(data: RpcEvidenceParams): responses.EvidenceParams {
return {
maxAge: Integer.parse(assertNotEmpty(data.max_age)),
};
}
/**
* Example data:
* {
* "block": {
* "max_bytes": "22020096",
* "max_gas": "-1",
* "time_iota_ms": "1000"
* },
* "evidence": {
* "max_age": "100000"
* },
* "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: IntegerString;
readonly results: {
readonly deliver_tx: readonly RpcTxData[];
readonly end_block: {
readonly validator_updates?: readonly RpcValidatorUpdate[];
readonly consensus_param_updates?: RpcConsensusParams;
readonly tags?: readonly RpcTag[];
};
};
}
function decodeBlockResults(data: RpcBlockResultsResponse): responses.BlockResultsResponse {
const res = optional(data.results.deliver_tx, [] as readonly RpcTxData[]);
const end = data.results.end_block;
const validators = optional(end.validator_updates, [] as readonly RpcValidatorUpdate[]);
return {
height: Integer.parse(assertNotEmpty(data.height)),
results: assertArray(res).map(decodeTxData),
endBlock: {
validatorUpdates: assertArray(validators).map(decodeValidatorUpdate),
consensusUpdates: may(decodeConsensusParams, end.consensus_param_updates),
tags: may(decodeTags, end.tags),
},
};
}
interface RpcBlockId {
readonly hash: HexString;
readonly parts: {
readonly total: IntegerString;
readonly hash: HexString;
};
}
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: IntegerString;
readonly app: IntegerString;
}
function decodeBlockVersion(data: RpcBlockVersion): responses.Version {
return {
block: Integer.parse(data.block),
app: Integer.parse(data.app),
};
}
interface RpcHeader {
readonly version: RpcBlockVersion;
readonly chain_id: string;
readonly height: IntegerString;
readonly time: DateTimeString;
readonly num_txs: IntegerString;
readonly total_txs: IntegerString;
readonly last_block_id: RpcBlockId;
readonly last_commit_hash: HexString;
readonly data_hash: HexString;
readonly validators_hash: HexString;
readonly next_validators_hash: HexString;
readonly consensus_hash: HexString;
readonly app_hash: HexString;
readonly last_results_hash: HexString;
readonly evidence_hash: HexString;
readonly proposer_address: HexString;
}
function decodeHeader(data: RpcHeader): responses.Header {
return {
version: decodeBlockVersion(data.version),
chainId: assertNotEmpty(data.chain_id),
height: Integer.parse(assertNotEmpty(data.height)),
time: DateTime.decode(assertNotEmpty(data.time)),
numTxs: Integer.parse(assertNotEmpty(data.num_txs)),
totalTxs: Integer.parse(assertNotEmpty(data.total_txs)),
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: IntegerString;
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 {
readonly hash: HexString;
}
function decodeBroadcastTxSync(data: RpcBroadcastTxSyncResponse): responses.BroadcastTxSyncResponse {
return {
...decodeTxData(data),
hash: fromHex(assertNotEmpty(data.hash)) as TxHash,
};
}
interface RpcBroadcastTxCommitResponse {
readonly height?: IntegerString;
readonly hash: HexString;
readonly check_tx: RpcTxData;
readonly deliver_tx?: RpcTxData;
}
function decodeBroadcastTxCommit(data: RpcBroadcastTxCommitResponse): responses.BroadcastTxCommitResponse {
return {
height: may(Integer.parse, data.height),
hash: fromHex(assertNotEmpty(data.hash)) as TxHash,
checkTx: decodeTxData(assertObject(data.check_tx)),
deliverTx: may(decodeTxData, data.deliver_tx),
};
}
type RpcSignature = Base64String;
function decodeSignature(data: RpcSignature): ValidatorSignature {
return {
algorithm: "ed25519",
data: Base64.decode(assertNotEmpty(data)),
};
}
interface RpcVote {
readonly type: number;
readonly validator_address: HexString;
readonly validator_index: IntegerString;
readonly height: IntegerString;
readonly round: IntegerString;
readonly timestamp: DateTimeString;
readonly block_id: RpcBlockId;
readonly signature: RpcSignature;
}
function decodeVote(data: RpcVote): responses.Vote {
return {
type: Integer.parse(assertNumber(data.type)),
validatorAddress: fromHex(assertNotEmpty(data.validator_address)),
validatorIndex: Integer.parse(assertNotEmpty(data.validator_index)),
height: Integer.parse(assertNotEmpty(data.height)),
round: Integer.parse(assertNotEmpty(data.round)),
timestamp: DateTime.decode(assertNotEmpty(data.timestamp)),
blockId: decodeBlockId(assertObject(data.block_id)),
signature: decodeSignature(assertNotEmpty(data.signature)),
};
}
interface RpcCommit {
readonly block_id: RpcBlockId;
readonly precommits: readonly RpcVote[];
}
function decodeCommit(data: RpcCommit): responses.Commit {
return {
blockId: decodeBlockId(assertObject(data.block_id)),
precommits: assertArray(data.precommits).map(decodeVote),
};
}
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 {
readonly pub_key: RpcPubkey;
readonly power: IntegerString;
readonly name?: string;
}
function decodeValidatorGenesis(data: RpcValidatorGenesis): responses.Validator {
return {
pubkey: decodePubkey(assertObject(data.pub_key)),
votingPower: Integer.parse(assertNotEmpty(data.power)),
name: data.name,
};
}
interface RpcGenesisResponse {
readonly genesis_time: DateTimeString;
readonly chain_id: string;
readonly consensus_params: RpcConsensusParams;
readonly validators: readonly RpcValidatorGenesis[];
readonly app_hash: HexString;
readonly app_state: {} | undefined;
}
interface GenesisResult {
readonly genesis: RpcGenesisResponse;
}
function decodeGenesis(data: RpcGenesisResponse): responses.GenesisResponse {
return {
genesisTime: DateTime.decode(assertNotEmpty(data.genesis_time)),
chainId: assertNotEmpty(data.chain_id),
consensusParams: decodeConsensusParams(data.consensus_params),
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 {
readonly address: HexString;
readonly pub_key: RpcPubkey;
readonly voting_power: IntegerString;
}
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 {
readonly id: HexString;
readonly listen_addr: IpPortString;
readonly network: string;
readonly version: string;
readonly channels: string; // ???
readonly moniker: string;
readonly protocol_version: {
readonly p2p: IntegerString;
readonly block: IntegerString;
readonly app: IntegerString;
};
/**
* Additional information. E.g.
* {
* "tx_index": "on",
* "rpc_address":"tcp://0.0.0.0:26657"
* }
*/
readonly other: object;
}
function decodeNodeInfo(data: RpcNodeInfo): responses.NodeInfo {
return {
id: fromHex(assertNotEmpty(data.id)),
listenAddr: assertNotEmpty(data.listen_addr),
network: assertNotEmpty(data.network),
version: assertNotEmpty(data.version),
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 {
readonly latest_block_hash: HexString;
readonly latest_app_hash: HexString;
readonly latest_block_height: IntegerString;
readonly latest_block_time: DateTimeString;
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: DateTime.decode(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:
* {
* "RootHash": "10A1A17D5F818099B5CAB5B91733A3CC27C0DB6CE2D571AC27FB970C314308BB",
* "Data": "ZVlERVhDV2lVNEUwPXhTUjc4Tmp2QkNVSg==",
* "Proof": {
* "total": "1",
* "index": "0",
* "leaf_hash": "EKGhfV+BgJm1yrW5FzOjzCfA22zi1XGsJ/uXDDFDCLs=",
* "aunts": []
* }
* }
*/
interface RpcTxProof {
readonly Data: Base64String;
readonly RootHash: HexString;
readonly Proof: {
readonly total: IntegerString;
readonly index: IntegerString;
readonly leaf_hash: Base64String;
readonly aunts: readonly Base64String[];
};
}
function decodeTxProof(data: RpcTxProof): responses.TxProof {
return {
data: Base64.decode(assertNotEmpty(data.Data)),
rootHash: fromHex(assertNotEmpty(data.RootHash)),
proof: {
total: Integer.parse(assertNotEmpty(data.Proof.total)),
index: Integer.parse(assertNotEmpty(data.Proof.index)),
leafHash: Base64.decode(assertNotEmpty(data.Proof.leaf_hash)),
aunts: assertArray(data.Proof.aunts).map(Base64.decode),
},
};
}
interface RpcTxResponse {
readonly tx: Base64String;
readonly tx_result: RpcTxData;
readonly height: IntegerString;
readonly index: number;
readonly hash: HexString;
readonly proof?: RpcTxProof;
}
function decodeTxResponse(data: RpcTxResponse): responses.TxResponse {
return {
tx: Base64.decode(assertNotEmpty(data.tx)) as TxBytes,
result: decodeTxData(assertObject(data.tx_result)),
height: Integer.parse(assertNotEmpty(data.height)),
index: Integer.parse(assertNumber(data.index)),
hash: fromHex(assertNotEmpty(data.hash)) as TxHash,
proof: may(decodeTxProof, data.proof),
};
}
interface RpcTxSearchResponse {
readonly txs: readonly RpcTxResponse[];
readonly total_count: IntegerString;
}
function decodeTxSearch(data: RpcTxSearchResponse): responses.TxSearchResponse {
return {
totalCount: Integer.parse(assertNotEmpty(data.total_count)),
txs: assertArray(data.txs).map(decodeTxResponse),
};
}
interface RpcTxEvent {
readonly tx: Base64String;
readonly result: RpcTxData;
readonly height: IntegerString;
readonly index: number;
}
function decodeTxEvent(data: RpcTxEvent): responses.TxEvent {
const tx = Base64.decode(assertNotEmpty(data.tx)) as TxBytes;
return {
tx: tx,
hash: hashTx(tx),
result: decodeTxData(data.result),
height: Integer.parse(assertNotEmpty(data.height)),
index: Integer.parse(assertNumber(data.index)),
};
}
// for validators
interface RpcValidatorData extends RpcValidatorUpdate {
readonly accum?: IntegerString;
}
function decodeValidatorData(data: RpcValidatorData): responses.Validator {
return {
...decodeValidatorUpdate(data),
accum: may(Integer.parse, data.accum),
};
}
interface RpcValidatorsResponse {
readonly block_height: IntegerString;
readonly validators: readonly RpcValidatorData[];
}
function decodeValidators(data: RpcValidatorsResponse): responses.ValidatorsResponse {
return {
blockHeight: Integer.parse(assertNotEmpty(data.block_height)),
results: assertArray(data.validators).map(decodeValidatorData),
};
}
interface RpcEvidence {
readonly type: string;
readonly validator: RpcValidatorUpdate;
readonly height: IntegerString;
readonly time: IntegerString;
readonly totalVotingPower: IntegerString;
}
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: {
readonly txs?: readonly Base64String[];
};
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(Base64.decode) : [],
evidence: data.evidence && may(decodeEvidences, data.evidence.evidence),
};
}
interface RpcBlockResponse {
readonly block_meta: RpcBlockMeta;
readonly block: RpcBlock;
}
function decodeBlockResponse(data: RpcBlockResponse): responses.BlockResponse {
return {
blockMeta: decodeBlockMeta(data.block_meta),
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,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"declarationDir": "build/types",
"rootDir": "src"
},
"include": [
"src/**/*"
]
}

View File

@ -0,0 +1,3 @@
{
"extends": "../../tslint.json"
}

View File

@ -0,0 +1,14 @@
const packageJson = require("./package.json");
module.exports = {
src: ["./src"],
out: "docs",
exclude: "**/*.spec.ts",
target: "es6",
name: `${packageJson.name} Documentation`,
readme: "README.md",
mode: "file",
excludeExternals: true,
excludeNotExported: true,
excludePrivate: true,
};

View File

@ -0,0 +1,49 @@
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@iov/jsonrpc";
import * as requests from "./requests";
import * as responses from "./responses";
import { SubscriptionEvent } from "./rpcclients";
import { BlockHash, TxBytes, TxHash } from "./types";
export interface Adaptor {
readonly params: Params;
readonly responses: Responses;
readonly hashTx: (tx: TxBytes) => TxHash;
readonly hashBlock: (header: responses.Header) => BlockHash;
}
export declare type Encoder<T extends requests.Request> = (req: T) => JsonRpcRequest;
export declare 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;
readonly decodeNewBlockEvent: (response: SubscriptionEvent) => responses.NewBlockEvent;
readonly decodeNewBlockHeaderEvent: (response: SubscriptionEvent) => responses.NewBlockHeaderEvent;
readonly decodeTxEvent: (response: SubscriptionEvent) => responses.TxEvent;
}

View File

@ -0,0 +1,8 @@
import { Adaptor } from "./adaptor";
/**
* Returns an Adaptor implementation for a given tendermint version.
* Throws when version is not supported.
*
* @param version full Tendermint version string, e.g. "0.20.1"
*/
export declare function adaptorForVersion(version: string): Adaptor;

View File

@ -0,0 +1,60 @@
import { Stream } from "xstream";
import { Adaptor } from "./adaptor";
import * as requests from "./requests";
import * as responses from "./responses";
import { RpcClient } from "./rpcclients";
export declare class Client {
static connect(url: string): Promise<Client>;
static detectVersion(client: RpcClient): Promise<Client>;
private readonly client;
private readonly p;
private readonly r;
constructor(client: RpcClient, adaptor: Adaptor);
disconnect(): void;
abciInfo(): Promise<responses.AbciInfoResponse>;
abciQuery(params: requests.AbciQueryParams): Promise<responses.AbciQueryResponse>;
block(height?: number): Promise<responses.BlockResponse>;
blockResults(height?: number): Promise<responses.BlockResultsResponse>;
blockchain(minHeight?: number, maxHeight?: number): Promise<responses.BlockchainResponse>;
/**
* Broadcast transaction to mempool and wait for response
*
* @see https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_sync
*/
broadcastTxSync(params: requests.BroadcastTxParams): Promise<responses.BroadcastTxSyncResponse>;
/**
* Broadcast transaction to mempool and do not wait for result
*
* @see https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_async
*/
broadcastTxAsync(params: requests.BroadcastTxParams): Promise<responses.BroadcastTxAsyncResponse>;
/**
* Broadcast transaction to mempool and wait for block
*
* @see https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_commit
*/
broadcastTxCommit(params: requests.BroadcastTxParams): Promise<responses.BroadcastTxCommitResponse>;
commit(height?: number): Promise<responses.CommitResponse>;
genesis(): Promise<responses.GenesisResponse>;
health(): Promise<responses.HealthResponse>;
status(): Promise<responses.StatusResponse>;
subscribeNewBlock(): Stream<responses.NewBlockEvent>;
subscribeNewBlockHeader(): Stream<responses.NewBlockHeaderEvent>;
subscribeTx(query?: requests.QueryString): Stream<responses.TxEvent>;
/**
* Get a single transaction by hash
*
* @see https://docs.tendermint.com/master/rpc/#/Info/tx
*/
tx(params: requests.TxParams): Promise<responses.TxResponse>;
/**
* Search for transactions that are in a block
*
* @see https://docs.tendermint.com/master/rpc/#/Info/tx_search
*/
txSearch(params: requests.TxSearchParams): Promise<responses.TxSearchResponse>;
txSearchAll(params: requests.TxSearchParams): Promise<responses.TxSearchResponse>;
validators(height?: number): Promise<responses.ValidatorsResponse>;
private doCall;
private subscribe;
}

View File

@ -0,0 +1,74 @@
import { As } from "type-tagger";
import { BlockId, ReadonlyDateWithNanoseconds, Version } from "./responses";
export declare type Base64String = string & As<"base64">;
export declare type HexString = string & As<"hex">;
export declare type IntegerString = string & As<"integer">;
export declare type DateTimeString = string & As<"datetime">;
/**
* 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 declare function assertSet<T>(value: T): T;
/**
* 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 declare function assertBoolean(value: boolean): boolean;
/**
* 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 declare function assertNumber(value: number): number;
/**
* 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 declare function assertArray<T>(value: readonly T[]): readonly T[];
/**
* 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 declare function assertObject<T>(value: T): T;
/**
* 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 declare function assertNotEmpty<T>(value: T): T;
export declare function optional<T>(value: T | null | undefined, fallback: T): T;
export declare function may<T, U>(transform: (val: T) => U, value: T | null | undefined): U | undefined;
export declare function dictionaryToStringMap(obj: any): Map<string, string>;
export declare class Integer {
static parse(input: IntegerString | number): number;
static encode(num: number): IntegerString;
}
export declare class Base64 {
static encode(data: Uint8Array): Base64String;
static decode(base64String: Base64String): Uint8Array;
}
export declare class DateTime {
static decode(dateTimeString: DateTimeString): ReadonlyDateWithNanoseconds;
}
export declare class Hex {
static encode(data: Uint8Array): HexString;
static decode(hexString: HexString): Uint8Array;
}
export declare function encodeString(s: string): Uint8Array;
export declare function encodeInt(n: number): Uint8Array;
export declare function encodeTime(time: ReadonlyDateWithNanoseconds): Uint8Array;
export declare function encodeBytes(bytes: Uint8Array): Uint8Array;
export declare function encodeVersion(version: Version): Uint8Array;
export declare function encodeBlockId(blockId: BlockId): Uint8Array;

View File

@ -0,0 +1,38 @@
export { v0_31 } from "./v0-31";
export { v0_32 } from "./v0-32";
export { Client } from "./client";
export {
AbciInfoRequest,
AbciQueryParams,
AbciQueryRequest,
BlockRequest,
BlockchainRequest,
BlockResultsRequest,
BroadcastTxRequest,
BroadcastTxParams,
CommitRequest,
GenesisRequest,
HealthRequest,
Method,
Request,
QueryString,
QueryTag,
StatusRequest,
SubscriptionEventType,
TxParams,
TxRequest,
TxSearchParams,
TxSearchRequest,
ValidatorsRequest,
} from "./requests";
export * from "./responses";
export { HttpClient, WebsocketClient } from "./rpcclients";
export {
IpPortString,
TxBytes,
TxHash,
ValidatorEd25519Pubkey,
ValidatorEd25519Signature,
ValidatorPubkey,
ValidatorSignature,
} from "./types";

View File

@ -0,0 +1,3 @@
import { JsonRpcRequest } from "@iov/jsonrpc";
/** Creates a JSON-RPC request with random ID */
export declare function createJsonRpcRequest(method: string, params?: {}): JsonRpcRequest;

View File

@ -0,0 +1,151 @@
import { As } from "type-tagger";
/**
* 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 declare enum Method {
AbciInfo = "abci_info",
AbciQuery = "abci_query",
Block = "block",
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 declare 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 declare 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?: QueryString;
};
}
export declare type QueryString = string & As<"query">;
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;
}
export interface TxSearchRequest {
readonly method: Method.TxSearch;
readonly params: TxSearchParams;
}
export interface TxSearchParams {
readonly query: QueryString;
readonly prove?: boolean;
readonly page?: number;
readonly per_page?: number;
}
export interface ValidatorsRequest {
readonly method: Method.Validators;
readonly params: {
readonly height?: number;
};
}
export interface BuildQueryComponents {
readonly tags?: readonly QueryTag[];
readonly raw?: QueryString;
}
export declare function buildQuery(components: BuildQueryComponents): QueryString;

Some files were not shown because too many files have changed in this diff Show More