cosmjs-util/packages/sdk/src/restclient.ts
2020-02-05 18:22:20 +01:00

297 lines
9.2 KiB
TypeScript

import { Encoding } from "@iov/encoding";
import axios, { AxiosInstance } from "axios";
import { AminoTx, BaseAccount, CodeInfo, ContractInfo, isAminoStdTx, StdTx, WasmData } from "./types";
const { fromBase64, fromUtf8, toHex, toUtf8 } = Encoding;
interface NodeInfo {
readonly network: string;
}
interface NodeInfoResponse {
readonly node_info: NodeInfo;
}
interface BlockMeta {
readonly header: {
readonly height: number;
readonly time: string;
readonly num_txs: number;
};
readonly block_id: {
readonly hash: string;
};
}
interface Block {
readonly header: {
readonly height: number;
};
}
interface BlocksResponse {
readonly block_meta: BlockMeta;
readonly block: Block;
}
interface AuthAccountsResponse {
readonly result: {
readonly value: BaseAccount;
};
}
// Currently all wasm query responses return json-encoded strings...
// later deprecate this and use the specific types for result
// (assuming it is inlined, no second parse needed)
type WasmResponse = WasmSuccess | WasmError;
interface WasmSuccess {
readonly height: string;
readonly result: string;
}
interface WasmError {
readonly error: string;
}
export interface TxsResponse {
readonly height: string;
readonly txhash: string;
readonly raw_log: string;
readonly tx: AminoTx;
}
interface SearchTxsResponse {
readonly total_count: string;
readonly count: string;
readonly page_number: string;
readonly page_total: string;
readonly limit: string;
readonly txs: readonly TxsResponse[];
}
interface PostTxsParams {}
export interface PostTxsResponse {
readonly height: string;
readonly txhash: string;
readonly code?: number;
readonly raw_log?: string;
/** The same as `raw_log` but deserialized? */
readonly logs?: object;
/** The gas limit as set by the user */
readonly gas_wanted?: string;
/** The gas used by the execution */
readonly gas_used?: string;
}
interface EncodeTxResponse {
// base64-encoded amino-binary encoded representation
readonly tx: string;
}
type RestClientResponse =
| NodeInfoResponse
| BlocksResponse
| AuthAccountsResponse
| TxsResponse
| SearchTxsResponse
| PostTxsResponse
| EncodeTxResponse
| WasmResponse;
type BroadcastMode = "block" | "sync" | "async";
function isWasmError(resp: WasmResponse): resp is WasmError {
return (resp as WasmError).error !== undefined;
}
function parseWasmResponse(response: WasmResponse): any {
if (isWasmError(response)) {
throw new Error(response.error);
}
return JSON.parse(response.result);
}
export class RestClient {
private readonly client: AxiosInstance;
// From https://cosmos.network/rpc/#/ICS0/post_txs
// The supported broadcast modes include "block"(return after tx commit), "sync"(return afer CheckTx) and "async"(return right away).
private readonly mode: BroadcastMode;
public constructor(url: string, mode: BroadcastMode = "block") {
const headers = {
post: { "Content-Type": "application/json" },
};
this.client = axios.create({
baseURL: url,
headers: headers,
});
this.mode = mode;
}
public async get(path: string): Promise<RestClientResponse> {
const { data } = await this.client.get(path);
if (data === null) {
throw new Error("Received null response from server");
}
return data;
}
public async post(path: string, params: PostTxsParams): Promise<RestClientResponse> {
const { data } = await this.client.post(path, params);
if (data === null) {
throw new Error("Received null response from server");
}
return data;
}
public async nodeInfo(): Promise<NodeInfoResponse> {
const responseData = await this.get("/node_info");
if (!(responseData as any).node_info) {
throw new Error("Unexpected response data format");
}
return responseData as NodeInfoResponse;
}
public async blocksLatest(): Promise<BlocksResponse> {
const responseData = await this.get("/blocks/latest");
if (!(responseData as any).block) {
throw new Error("Unexpected response data format");
}
return responseData as BlocksResponse;
}
public async blocks(height: number): Promise<BlocksResponse> {
const responseData = await this.get(`/blocks/${height}`);
if (!(responseData as any).block) {
throw new Error("Unexpected response data format");
}
return responseData as BlocksResponse;
}
/** returns the amino-encoding of the transaction performed by the server */
public async encodeTx(stdTx: StdTx): Promise<Uint8Array> {
const tx = { type: "cosmos-sdk/StdTx", value: stdTx };
const responseData = await this.post("/txs/encode", tx);
if (!(responseData as any).tx) {
throw new Error("Unexpected response data format");
}
return Encoding.fromBase64((responseData as EncodeTxResponse).tx);
}
public async authAccounts(address: string, height?: string): Promise<AuthAccountsResponse> {
const path =
height === undefined ? `/auth/accounts/${address}` : `/auth/accounts/${address}?tx.height=${height}`;
const responseData = await this.get(path);
if ((responseData as any).result.type !== "cosmos-sdk/Account") {
throw new Error("Unexpected response data format");
}
return responseData as AuthAccountsResponse;
}
public async txs(query: string): Promise<SearchTxsResponse> {
const responseData = await this.get(`/txs?${query}`);
if (!(responseData as any).txs) {
throw new Error("Unexpected response data format");
}
return responseData as SearchTxsResponse;
}
public async txsById(id: string): Promise<TxsResponse> {
const responseData = await this.get(`/txs/${id}`);
if (!(responseData as any).tx) {
throw new Error("Unexpected response data format");
}
return responseData as TxsResponse;
}
// tx must be JSON encoded StdTx (no wrapper)
public async postTx(tx: Uint8Array): Promise<PostTxsResponse> {
// TODO: check this is StdTx
const decoded = JSON.parse(fromUtf8(tx));
if (!isAminoStdTx(decoded)) {
throw new Error("Must be json encoded StdTx");
}
const params = {
tx: decoded,
mode: this.mode,
};
const responseData = await this.post("/txs", params);
if (!(responseData as any).txhash) {
throw new Error("Unexpected response data format");
}
return responseData as PostTxsResponse;
}
// wasm rest queries are listed here: https://github.com/cosmwasm/wasmd/blob/master/x/wasm/client/rest/query.go#L19-L27
public async listCodeInfo(): Promise<readonly CodeInfo[]> {
const path = `/wasm/code`;
const responseData = await this.get(path);
// answer may be null (empty array)
return parseWasmResponse(responseData as WasmResponse) || [];
}
// this will download the original wasm bytecode by code id
// throws error if no code with this id
public async getCode(id: number): Promise<Uint8Array> {
// TODO: broken currently
const path = `/wasm/code/${id}`;
const responseData = await this.get(path);
const { code } = parseWasmResponse(responseData as WasmResponse);
return fromBase64(code);
}
public async listContractAddresses(): Promise<readonly string[]> {
const path = `/wasm/contract`;
const responseData = await this.get(path);
// answer may be null (go's encoding of empty array)
const addresses: string[] | null = parseWasmResponse(responseData as WasmResponse);
return addresses || [];
}
// throws error if no contract at this address
public async getContractInfo(address: string): Promise<ContractInfo> {
const path = `/wasm/contract/${address}`;
const responseData = await this.get(path);
// rest server returns null if no data for the address
const info: ContractInfo | null = parseWasmResponse(responseData as WasmResponse);
if (!info) {
throw new Error(`No contract with address ${address}`);
}
return info;
}
// Returns all contract state.
// This is an empty array if no such contract, or contract has no data.
public async getAllContractState(address: string): Promise<readonly WasmData[]> {
const path = `/wasm/contract/${address}/state`;
const responseData = await this.get(path);
return parseWasmResponse(responseData as WasmResponse);
}
// Returns the data at the key if present (unknown decoded json),
// or null if no data at this (contract address, key) pair
public async queryContractRaw(address: string, key: Uint8Array): Promise<unknown | null> {
const hexKey = toHex(key);
const path = `/wasm/contract/${address}/raw/${hexKey}?encoding=hex`;
const responseData = await this.get(path);
const data: readonly WasmData[] = parseWasmResponse(responseData as WasmResponse);
return data.length === 0 ? null : data[0].val;
}
// Makes a "smart query" on the contract, returns response verbatim (json.RawMessage)
// Throws error if no such contract or invalid query format
public async queryContractSmart(address: string, query: object): Promise<unknown> {
const encoded = toHex(toUtf8(JSON.stringify(query)));
const path = `/wasm/contract/${address}/smart/${encoded}?encoding=hex`;
const responseData = (await this.get(path)) as WasmResponse;
if (isWasmError(responseData)) {
throw new Error(responseData.error);
}
// no extra parse here
return responseData.result;
}
}