319 lines
9.3 KiB
TypeScript
319 lines
9.3 KiB
TypeScript
import { isNonNullObject } from "@cosmjs/utils";
|
|
import axios, { AxiosError, AxiosInstance } from "axios";
|
|
|
|
import { Coin } from "./coins";
|
|
import { CosmosSdkTx, StdTx } from "./types";
|
|
|
|
export interface CosmosSdkAccount {
|
|
/** Bech32 account address */
|
|
readonly address: string;
|
|
readonly coins: ReadonlyArray<Coin>;
|
|
/** Bech32 encoded pubkey */
|
|
readonly public_key: string;
|
|
readonly account_number: number;
|
|
readonly sequence: number;
|
|
}
|
|
|
|
interface NodeInfo {
|
|
readonly protocol_version: {
|
|
readonly p2p: string;
|
|
readonly block: string;
|
|
readonly app: string;
|
|
};
|
|
readonly id: string;
|
|
readonly listen_addr: string;
|
|
readonly network: string;
|
|
readonly version: string;
|
|
readonly channels: string;
|
|
readonly moniker: string;
|
|
readonly other: {
|
|
readonly tx_index: string;
|
|
readonly rpc_address: string;
|
|
};
|
|
}
|
|
|
|
interface ApplicationVersion {
|
|
readonly name: string;
|
|
readonly server_name: string;
|
|
readonly client_name: string;
|
|
readonly version: string;
|
|
readonly commit: string;
|
|
readonly build_tags: string;
|
|
readonly go: string;
|
|
}
|
|
|
|
export interface NodeInfoResponse {
|
|
readonly node_info: NodeInfo;
|
|
readonly application_version: ApplicationVersion;
|
|
}
|
|
|
|
interface BlockId {
|
|
readonly hash: string;
|
|
// TODO: here we also have this
|
|
// parts: {
|
|
// total: '1',
|
|
// hash: '7AF200C78FBF9236944E1AB270F4045CD60972B7C265E3A9DA42973397572931'
|
|
// }
|
|
}
|
|
|
|
interface BlockHeader {
|
|
readonly version: {
|
|
readonly block: string;
|
|
readonly app: string;
|
|
};
|
|
readonly height: string;
|
|
readonly chain_id: string;
|
|
/** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */
|
|
readonly time: string;
|
|
readonly last_commit_hash: string;
|
|
readonly last_block_id: BlockId;
|
|
/** Can be empty */
|
|
readonly data_hash: string;
|
|
readonly validators_hash: string;
|
|
readonly next_validators_hash: string;
|
|
readonly consensus_hash: string;
|
|
readonly app_hash: string;
|
|
/** Can be empty */
|
|
readonly last_results_hash: string;
|
|
/** Can be empty */
|
|
readonly evidence_hash: string;
|
|
readonly proposer_address: string;
|
|
}
|
|
|
|
interface Block {
|
|
readonly header: BlockHeader;
|
|
readonly data: {
|
|
/** Array of base64 encoded transactions */
|
|
readonly txs: ReadonlyArray<string> | null;
|
|
};
|
|
}
|
|
|
|
export interface BlockResponse {
|
|
readonly block_id: BlockId;
|
|
readonly block: Block;
|
|
}
|
|
|
|
export interface AuthAccountsResponse {
|
|
readonly height: string;
|
|
readonly result: {
|
|
readonly type: "cosmos-sdk/Account";
|
|
readonly value: CosmosSdkAccount;
|
|
};
|
|
}
|
|
|
|
export interface TxsResponse {
|
|
readonly height: string;
|
|
readonly txhash: string;
|
|
/** 🤷♂️ */
|
|
readonly codespace?: string;
|
|
/** Falsy when transaction execution succeeded. Contains error code on error. */
|
|
readonly code?: number;
|
|
readonly raw_log: string;
|
|
readonly logs?: object;
|
|
readonly tx: CosmosSdkTx;
|
|
/** The gas limit as set by the user */
|
|
readonly gas_wanted?: string;
|
|
/** The gas used by the execution */
|
|
readonly gas_used?: string;
|
|
readonly timestamp: string;
|
|
}
|
|
|
|
export interface SearchTxsResponse {
|
|
readonly total_count: string;
|
|
readonly count: string;
|
|
readonly page_number: string;
|
|
readonly page_total: string;
|
|
readonly limit: string;
|
|
readonly txs: readonly TxsResponse[];
|
|
}
|
|
|
|
export interface PostTxsResponse {
|
|
readonly height: string;
|
|
readonly txhash: string;
|
|
readonly code?: number;
|
|
/**
|
|
* The result data of the execution (hex encoded).
|
|
*
|
|
* @see https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/types/result.go#L101
|
|
*/
|
|
readonly data?: string;
|
|
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;
|
|
}
|
|
|
|
/** A reponse from the /txs/encode endpoint */
|
|
export interface EncodeTxResponse {
|
|
/** base64-encoded amino-binary encoded representation */
|
|
readonly tx: string;
|
|
}
|
|
|
|
/**
|
|
* The mode used to send transaction
|
|
*
|
|
* @see https://cosmos.network/rpc/#/Transactions/post_txs
|
|
*/
|
|
export enum BroadcastMode {
|
|
/** Return after tx commit */
|
|
Block = "block",
|
|
/** Return afer CheckTx */
|
|
Sync = "sync",
|
|
/** Return right away */
|
|
Async = "async",
|
|
}
|
|
|
|
// We want to get message data from 500 errors
|
|
// https://stackoverflow.com/questions/56577124/how-to-handle-500-error-message-with-axios
|
|
// this should be chained to catch one error and throw a more informative one
|
|
function parseAxiosError(err: AxiosError): never {
|
|
// use the error message sent from server, not default 500 msg
|
|
if (err.response?.data) {
|
|
let errorText: string;
|
|
const data = err.response.data;
|
|
// expect { error: string }, but otherwise dump
|
|
if (data.error && typeof data.error === "string") {
|
|
errorText = data.error;
|
|
} else if (typeof data === "string") {
|
|
errorText = data;
|
|
} else {
|
|
errorText = JSON.stringify(data);
|
|
}
|
|
throw new Error(`${errorText} (HTTP ${err.response.status})`);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export class RestClient {
|
|
private readonly client: AxiosInstance;
|
|
private readonly broadcastMode: BroadcastMode;
|
|
|
|
/**
|
|
* Creates a new client to interact with a Cosmos SDK light client daemon.
|
|
* This class tries to be a direct mapping onto the API. Some basic decoding and normalizatin is done
|
|
* but things like caching are done at a higher level.
|
|
*
|
|
* When building apps, you should not need to use this class directly. If you do, this indicates a missing feature
|
|
* in higher level components. Feel free to raise an issue in this case.
|
|
*
|
|
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
|
|
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
|
|
*/
|
|
public constructor(apiUrl: string, broadcastMode = BroadcastMode.Block) {
|
|
const headers = {
|
|
post: { "Content-Type": "application/json" },
|
|
};
|
|
this.client = axios.create({
|
|
baseURL: apiUrl,
|
|
headers: headers,
|
|
});
|
|
this.broadcastMode = broadcastMode;
|
|
}
|
|
|
|
public async get(path: string): Promise<any> {
|
|
const { data } = await this.client.get(path).catch(parseAxiosError);
|
|
if (data === null) {
|
|
throw new Error("Received null response from server");
|
|
}
|
|
return data;
|
|
}
|
|
|
|
public async post(path: string, params: any): Promise<any> {
|
|
if (!isNonNullObject(params)) throw new Error("Got unexpected type of params. Expected object.");
|
|
const { data } = await this.client.post(path, params).catch(parseAxiosError);
|
|
if (data === null) {
|
|
throw new Error("Received null response from server");
|
|
}
|
|
return data;
|
|
}
|
|
|
|
// The /auth endpoints
|
|
|
|
public async authAccounts(address: string): Promise<AuthAccountsResponse> {
|
|
const path = `/auth/accounts/${address}`;
|
|
const responseData = await this.get(path);
|
|
if (responseData.result.type !== "cosmos-sdk/Account") {
|
|
throw new Error("Unexpected response data format");
|
|
}
|
|
return responseData as AuthAccountsResponse;
|
|
}
|
|
|
|
// The /blocks endpoints
|
|
|
|
public async blocksLatest(): Promise<BlockResponse> {
|
|
const responseData = await this.get("/blocks/latest");
|
|
if (!responseData.block) {
|
|
throw new Error("Unexpected response data format");
|
|
}
|
|
return responseData as BlockResponse;
|
|
}
|
|
|
|
public async blocks(height: number): Promise<BlockResponse> {
|
|
const responseData = await this.get(`/blocks/${height}`);
|
|
if (!responseData.block) {
|
|
throw new Error("Unexpected response data format");
|
|
}
|
|
return responseData as BlockResponse;
|
|
}
|
|
|
|
// The /node_info endpoint
|
|
|
|
public async nodeInfo(): Promise<NodeInfoResponse> {
|
|
const responseData = await this.get("/node_info");
|
|
if (!responseData.node_info) {
|
|
throw new Error("Unexpected response data format");
|
|
}
|
|
return responseData as NodeInfoResponse;
|
|
}
|
|
|
|
// The /txs endpoints
|
|
|
|
public async txById(id: string): Promise<TxsResponse> {
|
|
const responseData = await this.get(`/txs/${id}`);
|
|
if (!responseData.tx) {
|
|
throw new Error("Unexpected response data format");
|
|
}
|
|
return responseData as TxsResponse;
|
|
}
|
|
|
|
public async txsQuery(query: string): Promise<SearchTxsResponse> {
|
|
const responseData = await this.get(`/txs?${query}`);
|
|
if (!responseData.txs) {
|
|
throw new Error("Unexpected response data format");
|
|
}
|
|
return responseData as SearchTxsResponse;
|
|
}
|
|
|
|
/** returns the amino-encoding of the transaction performed by the server */
|
|
public async encodeTx(tx: CosmosSdkTx): Promise<EncodeTxResponse> {
|
|
const responseData = await this.post("/txs/encode", tx);
|
|
if (!responseData.tx) {
|
|
throw new Error("Unexpected response data format");
|
|
}
|
|
return responseData as EncodeTxResponse;
|
|
}
|
|
|
|
/**
|
|
* Broadcasts a signed transaction to into the transaction pool.
|
|
* Depending on the RestClient's broadcast mode, this might or might
|
|
* wait for checkTx or deliverTx to be executed before returning.
|
|
*
|
|
* @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container)
|
|
*/
|
|
public async postTx(tx: StdTx): Promise<PostTxsResponse> {
|
|
const params = {
|
|
tx: tx,
|
|
mode: this.broadcastMode,
|
|
};
|
|
const responseData = await this.post("/txs", params);
|
|
if (!responseData.txhash) {
|
|
throw new Error("Unexpected response data format");
|
|
}
|
|
return responseData as PostTxsResponse;
|
|
}
|
|
}
|