mirror of
https://github.com/cosmos/cosmjs.git
synced 2025-03-11 14:09:15 +00:00
Merge pull request #244 from CosmWasm/204-tendermint-rpc
Fork tendermint-rpc package
This commit is contained in:
commit
186cac31fa
2
NOTICE
2
NOTICE
@ -17,6 +17,8 @@ on 2020-06-05.
|
|||||||
The code in packages/encoding and packages/math was forked from https://github.com/iov-one/iov-core/tree/v2.3.2/packages/iov-encoding
|
The code in packages/encoding and packages/math was forked from https://github.com/iov-one/iov-core/tree/v2.3.2/packages/iov-encoding
|
||||||
on 2020-06-05.
|
on 2020-06-05.
|
||||||
|
|
||||||
|
The code in packages/tendermint-rpc and scripts/tendermint was forked from the folders packages/iov-tendermint-rpc and scripts/tendermint respectively of https://github.com/iov-one/iov-core at tag v2.5.0 on 2020-06-15.
|
||||||
|
|
||||||
Copyright 2018-2020 IOV SAS
|
Copyright 2018-2020 IOV SAS
|
||||||
Copyright 2020 Confio UO
|
Copyright 2020 Confio UO
|
||||||
Copyright 2020 Simon Warta
|
Copyright 2020 Simon Warta
|
||||||
|
1
packages/tendermint-rpc/.eslintignore
Symbolic link
1
packages/tendermint-rpc/.eslintignore
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../.eslintignore
|
3
packages/tendermint-rpc/.gitignore
vendored
Normal file
3
packages/tendermint-rpc/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
build/
|
||||||
|
dist/
|
||||||
|
docs/
|
15
packages/tendermint-rpc/README.md
Normal file
15
packages/tendermint-rpc/README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# @cosmjs/tendermint-rpc
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/@cosmjs/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.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This package is part of the cosmjs repository, licensed under the Apache License
|
||||||
|
2.0 (see [NOTICE](https://github.com/CosmWasm/cosmjs/blob/master/NOTICE) and
|
||||||
|
[LICENSE](https://github.com/CosmWasm/cosmjs/blob/master/LICENSE)).
|
26
packages/tendermint-rpc/jasmine-testrunner.js
Executable file
26
packages/tendermint-rpc/jasmine-testrunner.js
Executable 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();
|
45
packages/tendermint-rpc/karma.conf.js
Normal file
45
packages/tendermint-rpc/karma.conf.js
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
1
packages/tendermint-rpc/nonces/README.txt
Normal file
1
packages/tendermint-rpc/nonces/README.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Directory used to trigger lerna package updates for all packages
|
59
packages/tendermint-rpc/package.json
Normal file
59
packages/tendermint-rpc/package.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "@cosmjs/tendermint-rpc",
|
||||||
|
"version": "0.20.0",
|
||||||
|
"description": "Tendermint RPC clients",
|
||||||
|
"contributors": [
|
||||||
|
"IOV SAS <admin@iov.one>",
|
||||||
|
"Confio UO <hello@confio.tech>",
|
||||||
|
"Will Clark <willclarktech@users.noreply.github.com>"
|
||||||
|
],
|
||||||
|
"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/CosmWasm/cosmjs/tree/master/packages/tendermint-rpc"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"docs": "shx rm -rf docs && typedoc --options typedoc.js",
|
||||||
|
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\"",
|
||||||
|
"lint-fix": "eslint --max-warnings 0 \"**/*.{js,ts}\" --fix",
|
||||||
|
"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": {
|
||||||
|
"@cosmjs/crypto": "^0.20.0",
|
||||||
|
"@cosmjs/encoding": "^0.20.0",
|
||||||
|
"@cosmjs/math": "^0.20.0",
|
||||||
|
"@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": {
|
||||||
|
"@cosmjs/utils": "^0.20.0"
|
||||||
|
}
|
||||||
|
}
|
59
packages/tendermint-rpc/src/adaptor.ts
Normal file
59
packages/tendermint-rpc/src/adaptor.ts
Normal 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;
|
||||||
|
}
|
19
packages/tendermint-rpc/src/adaptorforversion.ts
Normal file
19
packages/tendermint-rpc/src/adaptorforversion.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
// 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_33 } from "./v0-33";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.33.")) {
|
||||||
|
return v0_33;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported tendermint version: ${version}`);
|
||||||
|
}
|
||||||
|
}
|
540
packages/tendermint-rpc/src/client.spec.ts
Normal file
540
packages/tendermint-rpc/src/client.spec.ts
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
import { toAscii } from "@cosmjs/encoding";
|
||||||
|
import { sleep } from "@cosmjs/utils";
|
||||||
|
import { firstEvent, toListPromise } from "@iov/stream";
|
||||||
|
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(75);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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.lastBlockId).toBeTruthy();
|
||||||
|
|
||||||
|
// merkle roots for proofs
|
||||||
|
expect(event.appHash).toBeTruthy();
|
||||||
|
expect(event.consensusHash).toBeTruthy();
|
||||||
|
expect(event.dataHash).toBeTruthy();
|
||||||
|
expect(event.evidenceHash).toBeTruthy();
|
||||||
|
expect(event.lastCommitHash).toBeTruthy();
|
||||||
|
expect(event.lastResultsHash).toBeTruthy();
|
||||||
|
expect(event.validatorsHash).toBeTruthy();
|
||||||
|
|
||||||
|
events.push(event);
|
||||||
|
|
||||||
|
if (events.length === 2) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
expect(events.length).toEqual(2);
|
||||||
|
expect(events[1].chainId).toEqual(events[0].chainId);
|
||||||
|
expect(events[1].height).toEqual(events[0].height + 1);
|
||||||
|
expect(events[1].time.getTime()).toBeGreaterThan(events[0].time.getTime());
|
||||||
|
|
||||||
|
expect(events[1].appHash).toEqual(events[0].appHash);
|
||||||
|
expect(events[1].consensusHash).toEqual(events[0].consensusHash);
|
||||||
|
expect(events[1].dataHash).toEqual(events[0].dataHash);
|
||||||
|
expect(events[1].evidenceHash).toEqual(events[0].evidenceHash);
|
||||||
|
expect(events[1].lastCommitHash).not.toEqual(events[0].lastCommitHash);
|
||||||
|
expect(events[1].lastResultsHash).not.toEqual(events[0].lastResultsHash);
|
||||||
|
expect(events[1].validatorsHash).toEqual(events[0].validatorsHash);
|
||||||
|
|
||||||
|
client.disconnect();
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: done.fail,
|
||||||
|
complete: () => done.fail("Stream completed before we are done"),
|
||||||
|
});
|
||||||
|
})().catch(done.fail);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can subscribe to block events", async () => {
|
||||||
|
pendingWithoutTendermint();
|
||||||
|
|
||||||
|
const testStart = ReadonlyDate.now();
|
||||||
|
|
||||||
|
const transactionData1 = buildKvTx(randomString(), randomString());
|
||||||
|
const transactionData2 = buildKvTx(randomString(), randomString());
|
||||||
|
|
||||||
|
const events: responses.NewBlockEvent[] = [];
|
||||||
|
const client = 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.lastBlockId).toBeTruthy();
|
||||||
|
|
||||||
|
// merkle roots for proofs
|
||||||
|
expect(event.header.appHash).toBeTruthy();
|
||||||
|
expect(event.header.consensusHash).toBeTruthy();
|
||||||
|
expect(event.header.dataHash).toBeTruthy();
|
||||||
|
expect(event.header.evidenceHash).toBeTruthy();
|
||||||
|
expect(event.header.lastCommitHash).toBeTruthy();
|
||||||
|
expect(event.header.lastResultsHash).toBeTruthy();
|
||||||
|
expect(event.header.validatorsHash).toBeTruthy();
|
||||||
|
|
||||||
|
events.push(event);
|
||||||
|
|
||||||
|
if (events.length === 2) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: fail,
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.broadcastTxCommit({ tx: transactionData1 });
|
||||||
|
await client.broadcastTxCommit({ tx: transactionData2 });
|
||||||
|
|
||||||
|
// wait for events to be processed
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
expect(events.length).toEqual(2);
|
||||||
|
// Block header
|
||||||
|
expect(events[1].header.height).toEqual(events[0].header.height + 1);
|
||||||
|
expect(events[1].header.chainId).toEqual(events[0].header.chainId);
|
||||||
|
expect(events[1].header.time.getTime()).toBeGreaterThan(events[0].header.time.getTime());
|
||||||
|
expect(events[1].header.appHash).not.toEqual(events[0].header.appHash);
|
||||||
|
expect(events[1].header.validatorsHash).toEqual(events[0].header.validatorsHash);
|
||||||
|
// Block body
|
||||||
|
expect(events[0].txs.length).toEqual(1);
|
||||||
|
expect(events[1].txs.length).toEqual(1);
|
||||||
|
expect(events[0].txs[0]).toEqual(transactionData1);
|
||||||
|
expect(events[1].txs[0]).toEqual(transactionData2);
|
||||||
|
|
||||||
|
client.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can subscribe to transaction events", async () => {
|
||||||
|
pendingWithoutTendermint();
|
||||||
|
|
||||||
|
const events: responses.TxEvent[] = [];
|
||||||
|
const client = 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
252
packages/tendermint-rpc/src/client.ts
Normal file
252
packages/tendermint-rpc/src/client.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
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;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
27
packages/tendermint-rpc/src/config.spec.ts
Normal file
27
packages/tendermint-rpc/src/config.spec.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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:11133",
|
||||||
|
version: "0.33.x",
|
||||||
|
appCreator: "Cosmoshi Netowoko",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const defaultInstance: TendermintInstance = tendermintInstances[0];
|
90
packages/tendermint-rpc/src/encodings.spec.ts
Normal file
90
packages/tendermint-rpc/src/encodings.spec.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { ReadonlyDate } from "readonly-date";
|
||||||
|
|
||||||
|
import { encodeBlockId, encodeBytes, encodeInt, encodeString, encodeTime, encodeVersion } from "./encodings";
|
||||||
|
|
||||||
|
describe("encodings", () => {
|
||||||
|
describe("encodeString", () => {
|
||||||
|
it("works", () => {
|
||||||
|
expect(encodeString("")).toEqual(Uint8Array.from([0]));
|
||||||
|
const str = "hello iov";
|
||||||
|
expect(encodeString(str)).toEqual(
|
||||||
|
Uint8Array.from([str.length, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x69, 0x6f, 0x76]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encodeInt", () => {
|
||||||
|
it("works", () => {
|
||||||
|
expect(encodeInt(0)).toEqual(Uint8Array.from([0]));
|
||||||
|
expect(encodeInt(1)).toEqual(Uint8Array.from([1]));
|
||||||
|
expect(encodeInt(127)).toEqual(Uint8Array.from([127]));
|
||||||
|
expect(encodeInt(128)).toEqual(Uint8Array.from([128, 1]));
|
||||||
|
expect(encodeInt(255)).toEqual(Uint8Array.from([255, 1]));
|
||||||
|
expect(encodeInt(256)).toEqual(Uint8Array.from([128, 2]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encodeTime", () => {
|
||||||
|
it("works", () => {
|
||||||
|
const readonlyDateWithNanoseconds = new ReadonlyDate(1464109200);
|
||||||
|
(readonlyDateWithNanoseconds as any).nanoseconds = 666666;
|
||||||
|
expect(encodeTime(readonlyDateWithNanoseconds)).toEqual(
|
||||||
|
Uint8Array.from([0x08, 173, 174, 89, 0x10, 170, 220, 215, 95]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encodeBytes", () => {
|
||||||
|
it("works", () => {
|
||||||
|
expect(encodeBytes(Uint8Array.from([]))).toEqual(Uint8Array.from([]));
|
||||||
|
const uint8Array = Uint8Array.from([1, 2, 3, 4, 5, 6, 7]);
|
||||||
|
expect(encodeBytes(uint8Array)).toEqual(Uint8Array.from([uint8Array.length, 1, 2, 3, 4, 5, 6, 7]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encodeVersion", () => {
|
||||||
|
it("works", () => {
|
||||||
|
const version = {
|
||||||
|
block: 666666,
|
||||||
|
app: 200,
|
||||||
|
};
|
||||||
|
expect(encodeVersion(version)).toEqual(Uint8Array.from([0x08, 170, 216, 40, 0x10, 200, 1]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encodeBlockId", () => {
|
||||||
|
it("works", () => {
|
||||||
|
const blockId = {
|
||||||
|
hash: Uint8Array.from([1, 2, 3, 4, 5, 6, 7]),
|
||||||
|
parts: {
|
||||||
|
total: 88,
|
||||||
|
hash: Uint8Array.from([8, 9, 10, 11, 12]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(encodeBlockId(blockId)).toEqual(
|
||||||
|
Uint8Array.from([
|
||||||
|
0x0a,
|
||||||
|
blockId.hash.length,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
0x12,
|
||||||
|
9,
|
||||||
|
0x08,
|
||||||
|
88,
|
||||||
|
0x12,
|
||||||
|
5,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
231
packages/tendermint-rpc/src/encodings.ts
Normal file
231
packages/tendermint-rpc/src/encodings.ts
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import { fromBase64, fromHex, fromRfc3339, toBase64, toHex, toUtf8 } from "@cosmjs/encoding";
|
||||||
|
import { Int53 } from "@cosmjs/math";
|
||||||
|
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 key–value pairs where the keys are strings)
|
||||||
|
*
|
||||||
|
* This is used when you want to verify that data at runtime matches the expected type.
|
||||||
|
* This implies assertSet.
|
||||||
|
*/
|
||||||
|
export function assertObject<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) : "";
|
||||||
|
(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 {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
return n >= 0x80 ? Uint8Array.from([(n & 0xff) | 0x80, ...encodeInt(n >> 7)]) : Uint8Array.from([n & 0xff]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L134-L178
|
||||||
|
export function encodeTime(time: ReadonlyDateWithNanoseconds): Uint8Array {
|
||||||
|
const milliseconds = time.getTime();
|
||||||
|
const seconds = Math.floor(milliseconds / 1000);
|
||||||
|
const secondsArray = seconds ? [0x08, ...encodeInt(seconds)] : new Uint8Array();
|
||||||
|
const nanoseconds = (time.nanoseconds || 0) + (milliseconds % 1000) * 1e6;
|
||||||
|
const nanosecondsArray = nanoseconds ? [0x10, ...encodeInt(nanoseconds)] : new Uint8Array();
|
||||||
|
return Uint8Array.from([...secondsArray, ...nanosecondsArray]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/tendermint/go-amino/blob/v0.15.0/encoder.go#L180-L187
|
||||||
|
export function encodeBytes(bytes: Uint8Array): Uint8Array {
|
||||||
|
// Since we're only dealing with short byte arrays we don't need a full VarBuffer implementation yet
|
||||||
|
if (bytes.length >= 0x80) throw new Error("Not implemented for byte arrays of length 128 or more");
|
||||||
|
return bytes.length ? Uint8Array.from([bytes.length, ...bytes]) : new Uint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeVersion(version: Version): Uint8Array {
|
||||||
|
const blockArray = version.block ? Uint8Array.from([0x08, ...encodeInt(version.block)]) : new Uint8Array();
|
||||||
|
const appArray = version.app ? Uint8Array.from([0x10, ...encodeInt(version.app)]) : new Uint8Array();
|
||||||
|
return Uint8Array.from([...blockArray, ...appArray]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeBlockId(blockId: BlockId): Uint8Array {
|
||||||
|
return Uint8Array.from([
|
||||||
|
0x0a,
|
||||||
|
blockId.hash.length,
|
||||||
|
...blockId.hash,
|
||||||
|
0x12,
|
||||||
|
blockId.parts.hash.length + 4,
|
||||||
|
0x08,
|
||||||
|
blockId.parts.total,
|
||||||
|
0x12,
|
||||||
|
blockId.parts.hash.length,
|
||||||
|
...blockId.parts.hash,
|
||||||
|
]);
|
||||||
|
}
|
40
packages/tendermint-rpc/src/index.ts
Normal file
40
packages/tendermint-rpc/src/index.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/camelcase */
|
||||||
|
// exported to access version-specific hashing
|
||||||
|
export { v0_33 } from "./v0-33";
|
||||||
|
|
||||||
|
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";
|
25
packages/tendermint-rpc/src/jsonrpc.spec.ts
Normal file
25
packages/tendermint-rpc/src/jsonrpc.spec.ts
Normal 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.toString()).toMatch(/^[0-9]{10,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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
28
packages/tendermint-rpc/src/jsonrpc.ts
Normal file
28
packages/tendermint-rpc/src/jsonrpc.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { JsonRpcRequest } from "@iov/jsonrpc";
|
||||||
|
|
||||||
|
const numbers = "0123456789";
|
||||||
|
|
||||||
|
/** generates a random numeric character */
|
||||||
|
function randomNumericChar(): string {
|
||||||
|
return numbers[Math.floor(Math.random() * numbers.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomId(): number {
|
||||||
|
return parseInt(
|
||||||
|
Array.from({ length: 12 })
|
||||||
|
.map(() => randomNumericChar())
|
||||||
|
.join(""),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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,
|
||||||
|
};
|
||||||
|
}
|
41
packages/tendermint-rpc/src/requests.spec.ts
Normal file
41
packages/tendermint-rpc/src/requests.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
179
packages/tendermint-rpc/src/requests.ts
Normal file
179
packages/tendermint-rpc/src/requests.ts
Normal 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;
|
||||||
|
}
|
329
packages/tendermint-rpc/src/responses.ts
Normal file
329
packages/tendermint-rpc/src/responses.ts
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
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 blockId: BlockId;
|
||||||
|
readonly block: Block;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockResultsResponse {
|
||||||
|
readonly height: number;
|
||||||
|
readonly results: readonly TxData[];
|
||||||
|
readonly validatorUpdates: readonly Validator[];
|
||||||
|
readonly consensusUpdates?: ConsensusParams;
|
||||||
|
readonly beginBlock?: readonly Tag[];
|
||||||
|
readonly endBlock?: 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 signatures: readonly ValidatorSignature[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
// 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 maxAgeNumBlocks: number;
|
||||||
|
readonly maxAgeDuration: number;
|
||||||
|
}
|
33
packages/tendermint-rpc/src/rpcclients/httpclient.spec.ts
Normal file
33
packages/tendermint-rpc/src/rpcclients/httpclient.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
57
packages/tendermint-rpc/src/rpcclients/httpclient.ts
Normal file
57
packages/tendermint-rpc/src/rpcclients/httpclient.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
5
packages/tendermint-rpc/src/rpcclients/index.ts
Normal file
5
packages/tendermint-rpc/src/rpcclients/index.ts
Normal 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";
|
44
packages/tendermint-rpc/src/rpcclients/rpcclient.spec.ts
Normal file
44
packages/tendermint-rpc/src/rpcclients/rpcclient.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
36
packages/tendermint-rpc/src/rpcclients/rpcclient.ts
Normal file
36
packages/tendermint-rpc/src/rpcclients/rpcclient.ts
Normal 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;
|
||||||
|
}
|
207
packages/tendermint-rpc/src/rpcclients/websocketclient.spec.ts
Normal file
207
packages/tendermint-rpc/src/rpcclients/websocketclient.spec.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
211
packages/tendermint-rpc/src/rpcclients/websocketclient.ts
Normal file
211
packages/tendermint-rpc/src/rpcclients/websocketclient.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
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)
|
||||||
|
.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)!.filter((response) => response.query !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
}
|
41
packages/tendermint-rpc/src/types.ts
Normal file
41
packages/tendermint-rpc/src/types.ts
Normal 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;
|
92
packages/tendermint-rpc/src/v0-33/hasher.spec.ts
Normal file
92
packages/tendermint-rpc/src/v0-33/hasher.spec.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { fromBase64, fromHex } from "@cosmjs/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:11133/block"
|
||||||
|
const blockId = fromHex("153C484DCBC33633F0616BC019388C93DEA94F7880627976F2BFE83749E062F7");
|
||||||
|
const time = new ReadonlyDate("2020-06-23T13:54:15.4638668Z");
|
||||||
|
(time as any).nanoseconds = 866800;
|
||||||
|
const blockData = {
|
||||||
|
version: {
|
||||||
|
block: 10,
|
||||||
|
app: 1,
|
||||||
|
},
|
||||||
|
chainId: "test-chain-2A5rwi",
|
||||||
|
height: 7795,
|
||||||
|
time: time as ReadonlyDateWithNanoseconds,
|
||||||
|
|
||||||
|
lastBlockId: {
|
||||||
|
hash: fromHex("1EC48444E64E7B96585BA518613612E52B976E3DA2F2222B9CD4D1602656C96F"),
|
||||||
|
parts: {
|
||||||
|
total: 1,
|
||||||
|
hash: fromHex("D4E6F1B0EE08D0438C9BB8455D7D3F2FC1883C32D66F7C69C4A0F093B073F6D2"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
lastCommitHash: fromHex("BA6A5EEA6687ACA8EE4FFE4F5D40EA073CB7397A5336309C3EC824805AF9723E"),
|
||||||
|
dataHash: fromHex(""),
|
||||||
|
|
||||||
|
validatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"),
|
||||||
|
nextValidatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"),
|
||||||
|
consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"),
|
||||||
|
appHash: fromHex("8801000000000000"),
|
||||||
|
lastResultsHash: fromHex(""),
|
||||||
|
|
||||||
|
evidenceHash: fromHex(""),
|
||||||
|
proposerAddress: fromHex("614F305502F65C01114F9B8711D9A0AB0AC369F4"),
|
||||||
|
};
|
||||||
|
expect(hashBlock(blockData)).toEqual(blockId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates block hash equal to local test for block with a transaction", () => {
|
||||||
|
// This was taken from a result from /block of some random block with a transaction
|
||||||
|
// curl "http://localhost:11133/block?height=13575"
|
||||||
|
const blockId = fromHex("FF2995AF1F38B9A584077E53B5E144778718FB86539A51886A2C55F730403373");
|
||||||
|
const time = new ReadonlyDate("2020-06-23T15:34:12.3232688Z");
|
||||||
|
(time as any).nanoseconds = 268800;
|
||||||
|
const blockData = {
|
||||||
|
version: {
|
||||||
|
block: 10,
|
||||||
|
app: 1,
|
||||||
|
},
|
||||||
|
chainId: "test-chain-2A5rwi",
|
||||||
|
height: 13575,
|
||||||
|
time: time as ReadonlyDateWithNanoseconds,
|
||||||
|
|
||||||
|
lastBlockId: {
|
||||||
|
hash: fromHex("046D5441FC4D008FCDBF9F3DD5DC25CF00883763E44CF4FAF3923FB5FEA42D8F"),
|
||||||
|
parts: {
|
||||||
|
total: 1,
|
||||||
|
hash: fromHex("02E4715343625093C717638EAC67FB3A4B24CCC8DA610E0CB324D705E68FEF7B"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
lastCommitHash: fromHex("AA2B807F3B0ACC866AB58D90C2D0FC70B6C860CFAC440590B4F590CDC178A207"),
|
||||||
|
dataHash: fromHex("56782879F526889734BA65375CD92A9152C7114B2C91B2D2AD8464FF69E884AA"),
|
||||||
|
|
||||||
|
validatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"),
|
||||||
|
nextValidatorsHash: fromHex("0BEEBC6AB3B7D4FE21E22B609CD4AEC7E121A42C07604FF1827651F0173745EB"),
|
||||||
|
consensusHash: fromHex("048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F"),
|
||||||
|
appHash: fromHex("CC02000000000000"),
|
||||||
|
lastResultsHash: fromHex("6E340B9CFFB37A989CA544E6BB780A2C78901D3FB33738768511A30617AFA01D"),
|
||||||
|
|
||||||
|
evidenceHash: fromHex(""),
|
||||||
|
proposerAddress: fromHex("614F305502F65C01114F9B8711D9A0AB0AC369F4"),
|
||||||
|
};
|
||||||
|
expect(hashBlock(blockData)).toEqual(blockId);
|
||||||
|
});
|
||||||
|
});
|
69
packages/tendermint-rpc/src/v0-33/hasher.ts
Normal file
69
packages/tendermint-rpc/src/v0-33/hasher.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { Sha256 } from "@cosmjs/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),
|
||||||
|
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;
|
||||||
|
}
|
12
packages/tendermint-rpc/src/v0-33/index.ts
Normal file
12
packages/tendermint-rpc/src/v0-33/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Adaptor } from "../adaptor";
|
||||||
|
import { hashBlock, hashTx } from "./hasher";
|
||||||
|
import { Params } from "./requests";
|
||||||
|
import { Responses } from "./responses";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||||
|
export const v0_33: Adaptor = {
|
||||||
|
params: Params,
|
||||||
|
responses: Responses,
|
||||||
|
hashTx: hashTx,
|
||||||
|
hashBlock: hashBlock,
|
||||||
|
};
|
142
packages/tendermint-rpc/src/v0-33/requests.ts
Normal file
142
packages/tendermint-rpc/src/v0-33/requests.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { toHex } from "@cosmjs/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));
|
||||||
|
}
|
||||||
|
}
|
767
packages/tendermint-rpc/src/v0-33/responses.ts
Normal file
767
packages/tendermint-rpc/src/v0-33/responses.ts
Normal file
@ -0,0 +1,767 @@
|
|||||||
|
import { fromHex } from "@cosmjs/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_num_blocks: IntegerString;
|
||||||
|
readonly max_age_duration: IntegerString;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeEvidenceParams(data: RpcEvidenceParams): responses.EvidenceParams {
|
||||||
|
return {
|
||||||
|
maxAgeNumBlocks: Integer.parse(assertNotEmpty(data.max_age_num_blocks)),
|
||||||
|
maxAgeDuration: Integer.parse(assertNotEmpty(data.max_age_duration)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example data:
|
||||||
|
* {
|
||||||
|
* "block": {
|
||||||
|
* "max_bytes": "22020096",
|
||||||
|
* "max_gas": "-1",
|
||||||
|
* "time_iota_ms": "1000"
|
||||||
|
* },
|
||||||
|
* "evidence": {
|
||||||
|
* "max_age_num_blocks": "100000",
|
||||||
|
* "max_age_duration": "172800000000000"
|
||||||
|
* },
|
||||||
|
* "validator": {
|
||||||
|
* "pub_key_types": [
|
||||||
|
* "ed25519"
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
interface RpcConsensusParams {
|
||||||
|
readonly block: RpcBlockParams;
|
||||||
|
readonly evidence: RpcEvidenceParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeConsensusParams(data: RpcConsensusParams): responses.ConsensusParams {
|
||||||
|
return {
|
||||||
|
block: decodeBlockParams(assertObject(data.block)),
|
||||||
|
evidence: decodeEvidenceParams(assertObject(data.evidence)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RpcBlockResultsResponse {
|
||||||
|
readonly height: IntegerString;
|
||||||
|
readonly txs_results: readonly RpcTxData[] | null;
|
||||||
|
readonly begin_block_events: null;
|
||||||
|
readonly end_block_events: null;
|
||||||
|
readonly validator_updates: null;
|
||||||
|
readonly consensus_param_updates: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeBlockResults(data: RpcBlockResultsResponse): responses.BlockResultsResponse {
|
||||||
|
const results = optional(data.txs_results, [] as readonly RpcTxData[]);
|
||||||
|
const validatorUpdates = optional(data.validator_updates, [] as readonly RpcValidatorUpdate[]);
|
||||||
|
return {
|
||||||
|
height: Integer.parse(assertNotEmpty(data.height)),
|
||||||
|
results: results.map(decodeTxData),
|
||||||
|
validatorUpdates: validatorUpdates.map(decodeValidatorUpdate),
|
||||||
|
consensusUpdates: may(decodeConsensusParams, data.consensus_param_updates),
|
||||||
|
beginBlock: may(decodeTags, data.begin_block_events),
|
||||||
|
endBlock: may(decodeTags, data.end_block_events),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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)),
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
readonly block_id_flag: number;
|
||||||
|
readonly validator_address: HexString;
|
||||||
|
readonly timestamp: DateTimeString;
|
||||||
|
readonly signature: Base64String;
|
||||||
|
};
|
||||||
|
|
||||||
|
function decodeSignature(data: RpcSignature): ValidatorSignature {
|
||||||
|
return {
|
||||||
|
algorithm: "ed25519",
|
||||||
|
data: Base64.decode(assertNotEmpty(data.signature)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RpcCommit {
|
||||||
|
readonly block_id: RpcBlockId;
|
||||||
|
readonly signatures: readonly RpcSignature[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeCommit(data: RpcCommit): responses.Commit {
|
||||||
|
return {
|
||||||
|
blockId: decodeBlockId(assertObject(data.block_id)),
|
||||||
|
signatures: assertArray(data.signatures).map(decodeSignature),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
* {
|
||||||
|
* "root_hash": "10A1A17D5F818099B5CAB5B91733A3CC27C0DB6CE2D571AC27FB970C314308BB",
|
||||||
|
* "data": "ZVlERVhDV2lVNEUwPXhTUjc4Tmp2QkNVSg==",
|
||||||
|
* "proof": {
|
||||||
|
* "total": "1",
|
||||||
|
* "index": "0",
|
||||||
|
* "leaf_hash": "EKGhfV+BgJm1yrW5FzOjzCfA22zi1XGsJ/uXDDFDCLs=",
|
||||||
|
* "aunts": []
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
interface RpcTxProof {
|
||||||
|
readonly data: Base64String;
|
||||||
|
readonly root_hash: 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.root_hash)),
|
||||||
|
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_id: RpcBlockId;
|
||||||
|
readonly block: RpcBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeBlockResponse(data: RpcBlockResponse): responses.BlockResponse {
|
||||||
|
return {
|
||||||
|
blockId: decodeBlockId(data.block_id),
|
||||||
|
block: decodeBlock(data.block),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Responses {
|
||||||
|
public static decodeAbciInfo(response: JsonRpcSuccessResponse): responses.AbciInfoResponse {
|
||||||
|
return decodeAbciInfo(assertObject((response.result as AbciInfoResult).response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeAbciQuery(response: JsonRpcSuccessResponse): responses.AbciQueryResponse {
|
||||||
|
return decodeAbciQuery(assertObject((response.result as AbciQueryResult).response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeBlock(response: JsonRpcSuccessResponse): responses.BlockResponse {
|
||||||
|
return decodeBlockResponse(response.result as RpcBlockResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeBlockResults(response: JsonRpcSuccessResponse): responses.BlockResultsResponse {
|
||||||
|
return decodeBlockResults(response.result as RpcBlockResultsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeBlockchain(response: JsonRpcSuccessResponse): responses.BlockchainResponse {
|
||||||
|
return decodeBlockchain(response.result as RpcBlockchainResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeBroadcastTxSync(response: JsonRpcSuccessResponse): responses.BroadcastTxSyncResponse {
|
||||||
|
return decodeBroadcastTxSync(response.result as RpcBroadcastTxSyncResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeBroadcastTxAsync(response: JsonRpcSuccessResponse): responses.BroadcastTxAsyncResponse {
|
||||||
|
return this.decodeBroadcastTxSync(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeBroadcastTxCommit(
|
||||||
|
response: JsonRpcSuccessResponse,
|
||||||
|
): responses.BroadcastTxCommitResponse {
|
||||||
|
return decodeBroadcastTxCommit(response.result as RpcBroadcastTxCommitResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeCommit(response: JsonRpcSuccessResponse): responses.CommitResponse {
|
||||||
|
return decodeCommitResponse(response.result as RpcCommitResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeGenesis(response: JsonRpcSuccessResponse): responses.GenesisResponse {
|
||||||
|
return decodeGenesis(assertObject((response.result as GenesisResult).genesis));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeHealth(): responses.HealthResponse {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeStatus(response: JsonRpcSuccessResponse): responses.StatusResponse {
|
||||||
|
return decodeStatus(response.result as RpcStatusResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeNewBlockEvent(event: SubscriptionEvent): responses.NewBlockEvent {
|
||||||
|
return decodeBlock(event.data.value.block as RpcBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeNewBlockHeaderEvent(event: SubscriptionEvent): responses.NewBlockHeaderEvent {
|
||||||
|
return decodeHeader(event.data.value.header as RpcHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeTxEvent(event: SubscriptionEvent): responses.TxEvent {
|
||||||
|
return decodeTxEvent(event.data.value.TxResult as RpcTxEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeTx(response: JsonRpcSuccessResponse): responses.TxResponse {
|
||||||
|
return decodeTxResponse(response.result as RpcTxResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeTxSearch(response: JsonRpcSuccessResponse): responses.TxSearchResponse {
|
||||||
|
return decodeTxSearch(response.result as RpcTxSearchResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decodeValidators(response: JsonRpcSuccessResponse): responses.ValidatorsResponse {
|
||||||
|
return decodeValidators(response.result as RpcValidatorsResponse);
|
||||||
|
}
|
||||||
|
}
|
12
packages/tendermint-rpc/tsconfig.json
Normal file
12
packages/tendermint-rpc/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "build",
|
||||||
|
"declarationDir": "build/types",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
14
packages/tendermint-rpc/typedoc.js
Normal file
14
packages/tendermint-rpc/typedoc.js
Normal 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,
|
||||||
|
};
|
49
packages/tendermint-rpc/types/adaptor.d.ts
vendored
Normal file
49
packages/tendermint-rpc/types/adaptor.d.ts
vendored
Normal 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;
|
||||||
|
}
|
8
packages/tendermint-rpc/types/adaptorforversion.d.ts
vendored
Normal file
8
packages/tendermint-rpc/types/adaptorforversion.d.ts
vendored
Normal 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;
|
60
packages/tendermint-rpc/types/client.d.ts
vendored
Normal file
60
packages/tendermint-rpc/types/client.d.ts
vendored
Normal 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;
|
||||||
|
}
|
74
packages/tendermint-rpc/types/encodings.d.ts
vendored
Normal file
74
packages/tendermint-rpc/types/encodings.d.ts
vendored
Normal 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 key–value pairs where the keys are strings)
|
||||||
|
*
|
||||||
|
* This is used when you want to verify that data at runtime matches the expected type.
|
||||||
|
* This implies assertSet.
|
||||||
|
*/
|
||||||
|
export 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;
|
37
packages/tendermint-rpc/types/index.d.ts
vendored
Normal file
37
packages/tendermint-rpc/types/index.d.ts
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export { v0_33 } from "./v0-33";
|
||||||
|
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";
|
3
packages/tendermint-rpc/types/jsonrpc.d.ts
vendored
Normal file
3
packages/tendermint-rpc/types/jsonrpc.d.ts
vendored
Normal 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;
|
151
packages/tendermint-rpc/types/requests.d.ts
vendored
Normal file
151
packages/tendermint-rpc/types/requests.d.ts
vendored
Normal 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;
|
260
packages/tendermint-rpc/types/responses.d.ts
vendored
Normal file
260
packages/tendermint-rpc/types/responses.d.ts
vendored
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import { ReadonlyDate } from "readonly-date";
|
||||||
|
import { IpPortString, TxBytes, TxHash, ValidatorPubkey, ValidatorSignature } from "./types";
|
||||||
|
export declare 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;
|
||||||
|
readonly log?: string;
|
||||||
|
}
|
||||||
|
export interface BlockResponse {
|
||||||
|
readonly blockId: BlockId;
|
||||||
|
readonly block: Block;
|
||||||
|
}
|
||||||
|
export interface BlockResultsResponse {
|
||||||
|
readonly height: number;
|
||||||
|
readonly results: readonly TxData[];
|
||||||
|
readonly validatorUpdates: readonly Validator[];
|
||||||
|
readonly consensusUpdates?: ConsensusParams;
|
||||||
|
readonly beginBlock?: readonly Tag[];
|
||||||
|
readonly endBlock?: 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 declare function broadcastTxSyncSuccess(res: BroadcastTxSyncResponse): boolean;
|
||||||
|
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 declare function broadcastTxCommitSuccess(res: BroadcastTxCommitResponse): boolean;
|
||||||
|
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 declare 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[];
|
||||||
|
}
|
||||||
|
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 declare const getTxEventHeight: (event: TxEvent) => number;
|
||||||
|
export declare const getHeaderEventHeight: (event: NewBlockHeaderEvent) => number;
|
||||||
|
export declare const getBlockEventHeight: (event: NewBlockEvent) => number;
|
||||||
|
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[];
|
||||||
|
}
|
||||||
|
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 signatures: readonly ValidatorSignature[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* raw values from https://github.com/tendermint/tendermint/blob/dfa9a9a30a666132425b29454e90a472aa579a48/types/vote.go#L44
|
||||||
|
*/
|
||||||
|
export declare 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 {
|
||||||
|
readonly nanoseconds?: number;
|
||||||
|
}
|
||||||
|
export interface Header {
|
||||||
|
readonly version: Version;
|
||||||
|
readonly chainId: string;
|
||||||
|
readonly height: number;
|
||||||
|
readonly time: ReadonlyDateWithNanoseconds;
|
||||||
|
readonly lastBlockId: BlockId;
|
||||||
|
readonly lastCommitHash: Uint8Array;
|
||||||
|
readonly dataHash: Uint8Array;
|
||||||
|
readonly validatorsHash: Uint8Array;
|
||||||
|
readonly nextValidatorsHash: Uint8Array;
|
||||||
|
readonly consensusHash: Uint8Array;
|
||||||
|
readonly appHash: Uint8Array;
|
||||||
|
readonly lastResultsHash: Uint8Array;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 maxAgeNumBlocks: number;
|
||||||
|
readonly maxAgeDuration: number;
|
||||||
|
}
|
8
packages/tendermint-rpc/types/rpcclients/httpclient.d.ts
vendored
Normal file
8
packages/tendermint-rpc/types/rpcclients/httpclient.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { JsonRpcRequest, JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||||
|
import { RpcClient } from "./rpcclient";
|
||||||
|
export declare class HttpClient implements RpcClient {
|
||||||
|
protected readonly url: string;
|
||||||
|
constructor(url?: string);
|
||||||
|
disconnect(): void;
|
||||||
|
execute(request: JsonRpcRequest): Promise<JsonRpcSuccessResponse>;
|
||||||
|
}
|
3
packages/tendermint-rpc/types/rpcclients/index.d.ts
vendored
Normal file
3
packages/tendermint-rpc/types/rpcclients/index.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { instanceOfRpcStreamingClient, RpcClient, RpcStreamingClient, SubscriptionEvent } from "./rpcclient";
|
||||||
|
export { HttpClient } from "./httpclient";
|
||||||
|
export { WebsocketClient } from "./websocketclient";
|
25
packages/tendermint-rpc/types/rpcclients/rpcclient.d.ts
vendored
Normal file
25
packages/tendermint-rpc/types/rpcclients/rpcclient.d.ts
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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 declare function instanceOfRpcStreamingClient(client: RpcClient): client is RpcStreamingClient;
|
||||||
|
export declare function hasProtocol(url: string): boolean;
|
20
packages/tendermint-rpc/types/rpcclients/websocketclient.d.ts
vendored
Normal file
20
packages/tendermint-rpc/types/rpcclients/websocketclient.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { JsonRpcId, JsonRpcRequest, JsonRpcResponse, JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||||
|
import { Stream } from "xstream";
|
||||||
|
import { RpcStreamingClient, SubscriptionEvent } from "./rpcclient";
|
||||||
|
export declare class WebsocketClient implements RpcStreamingClient {
|
||||||
|
private readonly url;
|
||||||
|
private readonly socket;
|
||||||
|
/** Same events as in socket.events but in the format we need */
|
||||||
|
private readonly jsonRpcResponseStream;
|
||||||
|
private readonly subscriptionStreams;
|
||||||
|
constructor(baseUrl?: string, onError?: (err: any) => void);
|
||||||
|
execute(request: JsonRpcRequest): Promise<JsonRpcSuccessResponse>;
|
||||||
|
listen(request: JsonRpcRequest): Stream<SubscriptionEvent>;
|
||||||
|
/**
|
||||||
|
* Resolves as soon as websocket is connected. execute() queues requests automatically,
|
||||||
|
* so this should be required for testing purposes only.
|
||||||
|
*/
|
||||||
|
connected(): Promise<void>;
|
||||||
|
disconnect(): void;
|
||||||
|
protected responseForRequestId(id: JsonRpcId): Promise<JsonRpcResponse>;
|
||||||
|
}
|
30
packages/tendermint-rpc/types/types.d.ts
vendored
Normal file
30
packages/tendermint-rpc/types/types.d.ts
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { As } from "type-tagger";
|
||||||
|
/**
|
||||||
|
* Merkle root
|
||||||
|
*/
|
||||||
|
export declare type BlockHash = Uint8Array & As<"block-hash">;
|
||||||
|
/** Raw transaction bytes */
|
||||||
|
export declare type TxBytes = Uint8Array & As<"tx-bytes">;
|
||||||
|
/**
|
||||||
|
* A raw tendermint transaction hash, currently 20 bytes
|
||||||
|
*/
|
||||||
|
export declare type TxHash = Uint8Array & As<"tx-hash">;
|
||||||
|
export declare 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 declare type ValidatorPubkey = ValidatorEd25519Pubkey;
|
||||||
|
export interface ValidatorEd25519Signature {
|
||||||
|
readonly algorithm: "ed25519";
|
||||||
|
readonly data: Uint8Array;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Union type for different possible voting signatures.
|
||||||
|
* Currently only Ed25519 supported.
|
||||||
|
*/
|
||||||
|
export declare type ValidatorSignature = ValidatorEd25519Signature;
|
4
packages/tendermint-rpc/types/v0-33/hasher.d.ts
vendored
Normal file
4
packages/tendermint-rpc/types/v0-33/hasher.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { Header } from "../responses";
|
||||||
|
import { BlockHash, TxBytes, TxHash } from "../types";
|
||||||
|
export declare function hashTx(tx: TxBytes): TxHash;
|
||||||
|
export declare function hashBlock(header: Header): BlockHash;
|
2
packages/tendermint-rpc/types/v0-33/index.d.ts
vendored
Normal file
2
packages/tendermint-rpc/types/v0-33/index.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { Adaptor } from "../adaptor";
|
||||||
|
export declare const v0_33: Adaptor;
|
18
packages/tendermint-rpc/types/v0-33/requests.d.ts
vendored
Normal file
18
packages/tendermint-rpc/types/v0-33/requests.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { JsonRpcRequest } from "@iov/jsonrpc";
|
||||||
|
import * as requests from "../requests";
|
||||||
|
export declare class Params {
|
||||||
|
static encodeAbciInfo(req: requests.AbciInfoRequest): JsonRpcRequest;
|
||||||
|
static encodeAbciQuery(req: requests.AbciQueryRequest): JsonRpcRequest;
|
||||||
|
static encodeBlock(req: requests.BlockRequest): JsonRpcRequest;
|
||||||
|
static encodeBlockchain(req: requests.BlockchainRequest): JsonRpcRequest;
|
||||||
|
static encodeBlockResults(req: requests.BlockResultsRequest): JsonRpcRequest;
|
||||||
|
static encodeBroadcastTx(req: requests.BroadcastTxRequest): JsonRpcRequest;
|
||||||
|
static encodeCommit(req: requests.CommitRequest): JsonRpcRequest;
|
||||||
|
static encodeGenesis(req: requests.GenesisRequest): JsonRpcRequest;
|
||||||
|
static encodeHealth(req: requests.HealthRequest): JsonRpcRequest;
|
||||||
|
static encodeStatus(req: requests.StatusRequest): JsonRpcRequest;
|
||||||
|
static encodeSubscribe(req: requests.SubscribeRequest): JsonRpcRequest;
|
||||||
|
static encodeTx(req: requests.TxRequest): JsonRpcRequest;
|
||||||
|
static encodeTxSearch(req: requests.TxSearchRequest): JsonRpcRequest;
|
||||||
|
static encodeValidators(req: requests.ValidatorsRequest): JsonRpcRequest;
|
||||||
|
}
|
23
packages/tendermint-rpc/types/v0-33/responses.d.ts
vendored
Normal file
23
packages/tendermint-rpc/types/v0-33/responses.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { JsonRpcSuccessResponse } from "@iov/jsonrpc";
|
||||||
|
import * as responses from "../responses";
|
||||||
|
import { SubscriptionEvent } from "../rpcclients";
|
||||||
|
export declare class Responses {
|
||||||
|
static decodeAbciInfo(response: JsonRpcSuccessResponse): responses.AbciInfoResponse;
|
||||||
|
static decodeAbciQuery(response: JsonRpcSuccessResponse): responses.AbciQueryResponse;
|
||||||
|
static decodeBlock(response: JsonRpcSuccessResponse): responses.BlockResponse;
|
||||||
|
static decodeBlockResults(response: JsonRpcSuccessResponse): responses.BlockResultsResponse;
|
||||||
|
static decodeBlockchain(response: JsonRpcSuccessResponse): responses.BlockchainResponse;
|
||||||
|
static decodeBroadcastTxSync(response: JsonRpcSuccessResponse): responses.BroadcastTxSyncResponse;
|
||||||
|
static decodeBroadcastTxAsync(response: JsonRpcSuccessResponse): responses.BroadcastTxAsyncResponse;
|
||||||
|
static decodeBroadcastTxCommit(response: JsonRpcSuccessResponse): responses.BroadcastTxCommitResponse;
|
||||||
|
static decodeCommit(response: JsonRpcSuccessResponse): responses.CommitResponse;
|
||||||
|
static decodeGenesis(response: JsonRpcSuccessResponse): responses.GenesisResponse;
|
||||||
|
static decodeHealth(): responses.HealthResponse;
|
||||||
|
static decodeStatus(response: JsonRpcSuccessResponse): responses.StatusResponse;
|
||||||
|
static decodeNewBlockEvent(event: SubscriptionEvent): responses.NewBlockEvent;
|
||||||
|
static decodeNewBlockHeaderEvent(event: SubscriptionEvent): responses.NewBlockHeaderEvent;
|
||||||
|
static decodeTxEvent(event: SubscriptionEvent): responses.TxEvent;
|
||||||
|
static decodeTx(response: JsonRpcSuccessResponse): responses.TxResponse;
|
||||||
|
static decodeTxSearch(response: JsonRpcSuccessResponse): responses.TxSearchResponse;
|
||||||
|
static decodeValidators(response: JsonRpcSuccessResponse): responses.ValidatorsResponse;
|
||||||
|
}
|
19
packages/tendermint-rpc/webpack.web.config.js
Normal file
19
packages/tendermint-rpc/webpack.web.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const glob = require("glob");
|
||||||
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
|
||||||
|
const target = "web";
|
||||||
|
const distdir = path.join(__dirname, "dist", "web");
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
// bundle used for Karma tests
|
||||||
|
target: target,
|
||||||
|
entry: glob.sync("./build/**/*.spec.js"),
|
||||||
|
output: {
|
||||||
|
path: distdir,
|
||||||
|
filename: "tests.js",
|
||||||
|
},
|
||||||
|
plugins: [new webpack.EnvironmentPlugin(["TENDERMINT_ENABLED"])],
|
||||||
|
},
|
||||||
|
];
|
18
scripts/tendermint/all_start.sh
Executable file
18
scripts/tendermint/all_start.sh
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -o errexit -o nounset -o pipefail
|
||||||
|
command -v shellcheck > /dev/null && shellcheck "$0"
|
||||||
|
|
||||||
|
# Find latest patch releases at https://hub.docker.com/r/tendermint/tendermint/tags/
|
||||||
|
declare -a TM_VERSIONS
|
||||||
|
TM_VERSIONS[33]=v0.33.5
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
for KEY in "${!TM_VERSIONS[@]}"; do
|
||||||
|
export TENDERMINT_VERSION="${TM_VERSIONS[$KEY]}"
|
||||||
|
export TENDERMINT_PORT="111$KEY"
|
||||||
|
export TENDERMINT_NAME="tendermint-$KEY"
|
||||||
|
|
||||||
|
echo "Starting $TENDERMINT_NAME ($TENDERMINT_VERSION) on port $TENDERMINT_PORT ..."
|
||||||
|
"$SCRIPT_DIR/start.sh"
|
||||||
|
done
|
15
scripts/tendermint/all_stop.sh
Executable file
15
scripts/tendermint/all_stop.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -o errexit -o nounset -o pipefail
|
||||||
|
command -v shellcheck > /dev/null && shellcheck "$0"
|
||||||
|
|
||||||
|
declare -a TM_VERSIONS
|
||||||
|
TM_VERSIONS[33]=v0.33.5
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
for KEY in "${!TM_VERSIONS[@]}"; do
|
||||||
|
export TENDERMINT_NAME="tendermint-$KEY"
|
||||||
|
|
||||||
|
echo "Stopping $TENDERMINT_NAME ..."
|
||||||
|
"$SCRIPT_DIR/stop.sh"
|
||||||
|
done
|
43
scripts/tendermint/start.sh
Executable file
43
scripts/tendermint/start.sh
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -o errexit -o nounset -o pipefail
|
||||||
|
command -v shellcheck > /dev/null && shellcheck "$0"
|
||||||
|
|
||||||
|
# Tendermint settings must be specified
|
||||||
|
# Choose version from https://hub.docker.com/r/tendermint/tendermint/tags/
|
||||||
|
for SETTING in "TENDERMINT_VERSION" "TENDERMINT_PORT" "TENDERMINT_NAME"
|
||||||
|
do
|
||||||
|
if test -z "$(eval echo "\$$SETTING")"
|
||||||
|
then
|
||||||
|
echo "\$$SETTING must be set when running this script"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tendermint.XXXXXXXXX")
|
||||||
|
chmod 777 "${TMP_DIR}"
|
||||||
|
echo "Using temporary dir $TMP_DIR"
|
||||||
|
LOGFILE="$TMP_DIR/tendermint.log"
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--user="$UID" \
|
||||||
|
-v "${TMP_DIR}:/tendermint" \
|
||||||
|
"tendermint/tendermint:${TENDERMINT_VERSION}" \
|
||||||
|
init
|
||||||
|
|
||||||
|
# make sure we allow cors origins, only possible by modifying the config file
|
||||||
|
# https://github.com/tendermint/tendermint/issues/3216
|
||||||
|
sed -ie 's/cors_allowed_origins.*$/cors_allowed_origins = ["*"]/' "${TMP_DIR}/config/config.toml"
|
||||||
|
|
||||||
|
# must enable tx index for search and subscribe
|
||||||
|
docker run --rm \
|
||||||
|
--user="$UID" \
|
||||||
|
--name "$TENDERMINT_NAME" \
|
||||||
|
-p "${TENDERMINT_PORT}:26657" -v "${TMP_DIR}:/tendermint" \
|
||||||
|
-e "TM_TX_INDEX_INDEX_ALL_KEYS=true" \
|
||||||
|
"tendermint/tendermint:${TENDERMINT_VERSION}" node \
|
||||||
|
--proxy_app=kvstore \
|
||||||
|
--rpc.laddr=tcp://0.0.0.0:26657 \
|
||||||
|
--log_level=state:info,rpc:info,*:error \
|
||||||
|
> "$LOGFILE" &
|
||||||
|
|
||||||
|
echo "Tendermint running and logging into $LOGFILE"
|
8
scripts/tendermint/stop.sh
Executable file
8
scripts/tendermint/stop.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -o errexit -o nounset -o pipefail
|
||||||
|
command -v shellcheck > /dev/null && shellcheck "$0"
|
||||||
|
|
||||||
|
NAME=${TENDERMINT_NAME:-tendermint-25}
|
||||||
|
|
||||||
|
echo "Killing container named '$NAME' ..."
|
||||||
|
docker container kill "$NAME"
|
55
yarn.lock
55
yarn.lock
@ -92,6 +92,42 @@
|
|||||||
unique-filename "^1.1.1"
|
unique-filename "^1.1.1"
|
||||||
which "^1.3.1"
|
which "^1.3.1"
|
||||||
|
|
||||||
|
"@iov/encoding@^2.3.2":
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@iov/encoding/-/encoding-2.3.2.tgz#4b37966af0345a6bc904bb58189dc1ea9d14ad9b"
|
||||||
|
integrity sha512-viioqo1flTkG4Oxb0PvoBXGozHq9fObAgAL4dRHJe9zmChE77EBX2Y5u0nabd2JwAhEbir56AtsrUe4dOrtd5w==
|
||||||
|
dependencies:
|
||||||
|
base64-js "^1.3.0"
|
||||||
|
bech32 "^1.1.4"
|
||||||
|
bn.js "^4.11.8"
|
||||||
|
readonly-date "^1.0.0"
|
||||||
|
|
||||||
|
"@iov/jsonrpc@^2.3.2":
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@iov/jsonrpc/-/jsonrpc-2.3.2.tgz#5cdfa56333741073cc00f17d54efb9f526b9705a"
|
||||||
|
integrity sha512-fPryTYZ4na1F/K0AF4eRjf1mwg97s8N/AgvELHyqpm7Bq9zvV0/ZBPvzjV1mqmPMfONx91qLIkpDguwmwEb8NA==
|
||||||
|
dependencies:
|
||||||
|
"@iov/encoding" "^2.3.2"
|
||||||
|
"@iov/stream" "^2.3.2"
|
||||||
|
xstream "^11.10.0"
|
||||||
|
|
||||||
|
"@iov/socket@^2.3.2":
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@iov/socket/-/socket-2.3.2.tgz#adc8ef389bafc5380e1c7415fb21f9a890d79195"
|
||||||
|
integrity sha512-LMIVGhYNvEdctHwjprVYv5QnDxXLNNZ9ASU0IRlsjHsMNR1Tcy+FsdqqQ5PJlW1r/r92dHLCfV9IKXucg/h8rQ==
|
||||||
|
dependencies:
|
||||||
|
"@iov/stream" "^2.3.2"
|
||||||
|
isomorphic-ws "^4.0.1"
|
||||||
|
ws "^6.2.0"
|
||||||
|
xstream "^11.10.0"
|
||||||
|
|
||||||
|
"@iov/stream@^2.3.2":
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@iov/stream/-/stream-2.3.2.tgz#472063f3a4fcd1e97de0ae99189f98b94825afac"
|
||||||
|
integrity sha512-nOq5OKwK2rWnyzXpGTAKZToCU0jmTjEC05owVzmBfw3+7hmjlExrGGbLncjcMWzbvp2VVpHc2l01Tr+qpzr8Vw==
|
||||||
|
dependencies:
|
||||||
|
xstream "^11.10.0"
|
||||||
|
|
||||||
"@koa/cors@^3.0.0":
|
"@koa/cors@^3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.0.0.tgz#df021b4df2dadf1e2b04d7c8ddf93ba2d42519cb"
|
resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.0.0.tgz#df021b4df2dadf1e2b04d7c8ddf93ba2d42519cb"
|
||||||
@ -4649,6 +4685,11 @@ isobject@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
|
resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
|
||||||
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
|
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
|
||||||
|
|
||||||
|
isomorphic-ws@^4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
|
||||||
|
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
|
||||||
|
|
||||||
isstream@~0.1.2:
|
isstream@~0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||||
@ -8078,6 +8119,13 @@ write@1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mkdirp "^0.5.1"
|
mkdirp "^0.5.1"
|
||||||
|
|
||||||
|
ws@^6.2.0:
|
||||||
|
version "6.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
|
||||||
|
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
|
||||||
|
dependencies:
|
||||||
|
async-limiter "~1.0.0"
|
||||||
|
|
||||||
ws@^7.1.2:
|
ws@^7.1.2:
|
||||||
version "7.3.0"
|
version "7.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
|
||||||
@ -8095,6 +8143,13 @@ xmlhttprequest-ssl@~1.5.4:
|
|||||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
|
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
|
||||||
integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
|
integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
|
||||||
|
|
||||||
|
xstream@^11.10.0:
|
||||||
|
version "11.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xstream/-/xstream-11.12.0.tgz#a85fded069cc8ac3aedc9538e5d94ab6d0ce845a"
|
||||||
|
integrity sha512-rceZqhyRPJdmDNh8hyFEnOacNrL4pTVkNZzoLvFqOVaIZHbM3bS15ycqI5V9eJXCRMfgEapwzcNzPjkIRUkv2Q==
|
||||||
|
dependencies:
|
||||||
|
symbol-observable "1.2.0"
|
||||||
|
|
||||||
xstream@^11.11.0:
|
xstream@^11.11.0:
|
||||||
version "11.11.0"
|
version "11.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/xstream/-/xstream-11.11.0.tgz#2c963cf0a6cb3eafecb57eaeae16c9850235b422"
|
resolved "https://registry.yarnpkg.com/xstream/-/xstream-11.11.0.tgz#2c963cf0a6cb3eafecb57eaeae16c9850235b422"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user