Merge pull request #119 from confio/rest-cleanup

RestClient cleanups; Pass min/max height filters to backend
This commit is contained in:
merge-when-green[bot] 2020-02-21 13:16:36 +00:00 committed by GitHub
commit 41db029f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 448 additions and 130 deletions

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/camelcase */
import { CosmWasmClient, findSequenceForSignedTx, TxsResponse, types } from "@cosmwasm/sdk";
import { CosmWasmClient, findSequenceForSignedTx, SearchTxFilter, TxsResponse, types } from "@cosmwasm/sdk";
import {
Account,
AccountQuery,
@ -257,7 +257,9 @@ export class CosmWasmConnection implements BlockchainConnection {
}
public async postTx(tx: PostableBytes): Promise<PostTxResponse> {
const { transactionHash, rawLog } = await this.cosmWasmClient.postTx(tx);
const txAsJson = JSON.parse(Encoding.fromUtf8(tx));
if (!types.isStdTx(txAsJson)) throw new Error("Postable bytes must contain a JSON encoded StdTx");
const { transactionHash, rawLog } = await this.cosmWasmClient.postTx(txAsJson);
const transactionId = transactionHash as TransactionId;
const firstEvent: BlockInfo = { state: TransactionState.Pending };
let blockInfoInterval: NodeJS.Timeout;
@ -298,8 +300,8 @@ export class CosmWasmConnection implements BlockchainConnection {
public async searchTx({
height,
id,
maxHeight: maxHeightOptional,
minHeight: minHeightOptional,
maxHeight,
minHeight,
sentFromOrTo,
signedBy,
tags,
@ -314,32 +316,20 @@ export class CosmWasmConnection implements BlockchainConnection {
);
}
const minHeight = minHeightOptional || 0;
const maxHeight = maxHeightOptional || Number.MAX_SAFE_INTEGER;
if (maxHeight < minHeight) return []; // optional optimization
const filter: SearchTxFilter = { minHeight: minHeight, maxHeight: maxHeight };
let txs: readonly TxsResponse[];
if (id) {
txs = await this.cosmWasmClient.searchTx({ id: id });
txs = await this.cosmWasmClient.searchTx({ id: id }, filter);
} else if (height) {
if (height < minHeight) return []; // optional optimization
if (height > maxHeight) return []; // optional optimization
txs = await this.cosmWasmClient.searchTx({ height: height });
txs = await this.cosmWasmClient.searchTx({ height: height }, filter);
} else if (sentFromOrTo) {
// TODO: pass minHeight/maxHeight to server once we have
// https://github.com/cosmwasm/wasmd/issues/73
txs = await this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo });
txs = await this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo }, filter);
} else {
throw new Error("Unsupported query");
}
const filtered = txs.filter(tx => {
const txHeight = parseInt(tx.height, 10);
return txHeight >= minHeight && txHeight <= maxHeight;
});
return filtered.map(tx => this.parseAndPopulateTxResponseUnsigned(tx));
return txs.map(tx => this.parseAndPopulateTxResponseUnsigned(tx));
}
public listenTx(

View File

@ -73,8 +73,8 @@ export declare class CosmWasmConnection implements BlockchainConnection {
searchTx({
height,
id,
maxHeight: maxHeightOptional,
minHeight: minHeightOptional,
maxHeight,
minHeight,
sentFromOrTo,
signedBy,
tags,

View File

@ -72,7 +72,7 @@ const signedTx: types.StdTx = {
memo: memo,
signatures: [signature],
}
const postResult = await client.postTx(marshalTx(signedTx));
const postResult = await client.postTx(signedTx);
```
## Extended helpers

View File

@ -47,7 +47,7 @@ const instantiateContract = async (initClient: RestClient, initPen: Secp256k1Pen
memo: memo,
signatures: [signature],
};
const result = await initClient.postTx(marshalTx(signedTx));
const result = await initClient.postTx(signedTx);
if (result.code) {
throw new Error(`Failed tx: (${result.code}): ${result.raw_log}`)
}
@ -78,7 +78,7 @@ const executeContract = async (execClient: RestClient, execPen: Secp256k1Pen, co
memo: memo,
signatures: [signature],
};
const result = await execClient.postTx(marshalTx(signedTx));
const result = await execClient.postTx(signedTx);
if (result.code) {
throw new Error(`Failed tx: (${result.code}): ${result.raw_log}`)
}

View File

@ -4,7 +4,7 @@ import { assert, sleep } from "@iov/utils";
import { ReadonlyDate } from "readonly-date";
import { CosmWasmClient } from "./cosmwasmclient";
import { makeSignBytes, marshalTx } from "./encoding";
import { makeSignBytes } from "./encoding";
import { findAttribute } from "./logs";
import { Secp256k1Pen } from "./pen";
import { RestClient } from "./restclient";
@ -198,7 +198,7 @@ describe("CosmWasmClient", () => {
memo: memo,
signatures: [signature],
};
const { logs, transactionHash } = await client.postTx(marshalTx(signedTx));
const { logs, transactionHash } = await client.postTx(signedTx);
const amountAttr = findAttribute(logs, "transfer", "amount");
expect(amountAttr.value).toEqual("1234567ucosm");
expect(transactionHash).toMatch(/^[0-9A-F]{64}$/);
@ -309,6 +309,87 @@ describe("CosmWasmClient", () => {
}),
);
});
it("can search by ID and filter by minHeight", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new CosmWasmClient(httpUrl);
const query = { id: posted.hash };
{
const result = await client.searchTx(query, { minHeight: 0 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: posted.height - 1 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: posted.height });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: posted.height + 1 });
expect(result.length).toEqual(0);
}
});
it("can search by recipient and filter by minHeight", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new CosmWasmClient(httpUrl);
const query = { sentFromOrTo: posted.recipient };
{
const result = await client.searchTx(query, { minHeight: 0 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: posted.height - 1 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: posted.height });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: posted.height + 1 });
expect(result.length).toEqual(0);
}
});
it("can search by recipient and filter by maxHeight", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new CosmWasmClient(httpUrl);
const query = { sentFromOrTo: posted.recipient };
{
const result = await client.searchTx(query, { maxHeight: 9999999999999 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { maxHeight: posted.height + 1 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { maxHeight: posted.height });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { maxHeight: posted.height - 1 });
expect(result.length).toEqual(0);
}
});
});
describe("queryContractRaw", () => {

View File

@ -3,7 +3,7 @@ import { Encoding } from "@iov/encoding";
import { Log, parseLogs } from "./logs";
import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient";
import { CosmosSdkAccount, CosmosSdkTx } from "./types";
import { CosmosSdkAccount, CosmosSdkTx, StdTx } from "./types";
export interface GetNonceResult {
readonly accountNumber: number;
@ -43,6 +43,11 @@ function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySen
return (query as SearchBySentFromOrToQuery).sentFromOrTo !== undefined;
}
export interface SearchTxFilter {
readonly minHeight?: number;
readonly maxHeight?: number;
}
export class CosmWasmClient {
protected readonly restClient: RestClient;
@ -104,28 +109,47 @@ export class CosmWasmClient {
}
}
public async searchTx(query: SearchTxQuery): Promise<readonly TxsResponse[]> {
// TODO: we need proper pagination support
function limited(originalQuery: string): string {
return `${originalQuery}&limit=75`;
public async searchTx(query: SearchTxQuery, filter: SearchTxFilter = {}): Promise<readonly TxsResponse[]> {
const minHeight = filter.minHeight || 0;
const maxHeight = filter.maxHeight || Number.MAX_SAFE_INTEGER;
if (maxHeight < minHeight) return []; // optional optimization
function withFilters(originalQuery: string): string {
return `${originalQuery}&tx.minheight=${minHeight}&tx.maxheight=${maxHeight}`;
}
let txs: readonly TxsResponse[];
if (isSearchByIdQuery(query)) {
return (await this.restClient.txs(`tx.hash=${query.id}`)).txs;
txs = await this.txsQuery(`tx.hash=${query.id}`);
} else if (isSearchByHeightQuery(query)) {
return (await this.restClient.txs(`tx.height=${query.height}`)).txs;
// optional optimization to avoid network request
if (query.height < minHeight || query.height > maxHeight) {
txs = [];
} else {
txs = await this.txsQuery(`tx.height=${query.height}`);
}
} else if (isSearchBySentFromOrToQuery(query)) {
// We cannot get both in one request (see https://github.com/cosmos/gaia/issues/75)
const sent = (await this.restClient.txs(limited(`message.sender=${query.sentFromOrTo}`))).txs;
const received = (await this.restClient.txs(limited(`transfer.recipient=${query.sentFromOrTo}`))).txs;
const sent = await this.txsQuery(withFilters(`message.sender=${query.sentFromOrTo}`));
const received = await this.txsQuery(withFilters(`transfer.recipient=${query.sentFromOrTo}`));
const sentHashes = sent.map(t => t.txhash);
return [...sent, ...received.filter(t => !sentHashes.includes(t.txhash))];
txs = [...sent, ...received.filter(t => !sentHashes.includes(t.txhash))];
} else {
throw new Error("Unknown query type");
}
// backend sometimes messes up with min/max height filtering
const filtered = txs.filter(tx => {
const txHeight = parseInt(tx.height, 10);
return txHeight >= minHeight && txHeight <= maxHeight;
});
return filtered;
}
public async postTx(tx: Uint8Array): Promise<PostTxResult> {
public async postTx(tx: StdTx): Promise<PostTxResult> {
const result = await this.restClient.postTx(tx);
if (result.code) {
throw new Error(`Error when posting tx. Code: ${result.code}; Raw log: ${result.raw_log}`);
@ -176,4 +200,17 @@ export class CosmWasmClient {
}
}
}
private async txsQuery(query: string): Promise<readonly TxsResponse[]> {
// TODO: we need proper pagination support
const limit = 100;
const result = await this.restClient.txsQuery(`${query}&limit=${limit}`);
const pages = parseInt(result.page_total, 10);
if (pages > 1) {
throw new Error(
`Found more results on the backend than we can process currently. Results: ${result.total_count}, supported: ${limit}`,
);
}
return result.txs;
}
}

View File

@ -14,6 +14,7 @@ export {
SearchByIdQuery,
SearchBySentFromOrToQuery,
SearchTxQuery,
SearchTxFilter,
} from "./cosmwasmclient";
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
export {

View File

@ -1,14 +1,15 @@
/* eslint-disable @typescript-eslint/camelcase */
import { Sha256 } from "@iov/crypto";
import { Encoding } from "@iov/encoding";
import { assert } from "@iov/utils";
import { assert, sleep } from "@iov/utils";
import { ReadonlyDate } from "readonly-date";
import { makeSignBytes, marshalTx } from "./encoding";
import { makeSignBytes } from "./encoding";
import { findAttribute, parseLogs } from "./logs";
import { Pen, Secp256k1Pen } from "./pen";
import { encodeBech32Pubkey } from "./pubkey";
import { PostTxsResponse, RestClient } from "./restclient";
import { PostTxsResponse, RestClient, TxsResponse } from "./restclient";
import { SigningCosmWasmClient } from "./signingcosmwasmclient";
import cosmoshub from "./testdata/cosmoshub.json";
import {
getRandomizedHackatom,
@ -87,7 +88,7 @@ async function uploadCustomContract(
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(marshalTx(signedTx));
return client.postTx(signedTx);
}
async function uploadContract(client: RestClient, pen: Pen): Promise<PostTxsResponse> {
@ -128,7 +129,7 @@ async function instantiateContract(
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(marshalTx(signedTx));
return client.postTx(signedTx);
}
async function executeContract(
@ -160,7 +161,7 @@ async function executeContract(
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(marshalTx(signedTx));
return client.postTx(signedTx);
}
describe("RestClient", () => {
@ -169,15 +170,61 @@ describe("RestClient", () => {
expect(client).toBeTruthy();
});
describe("nodeInfo", () => {
it("works", async () => {
// The /auth endpoints
describe("authAccounts", () => {
it("works for unused account without pubkey", async () => {
pendingWithoutWasmd();
const client = new RestClient(httpUrl);
const info = await client.nodeInfo();
expect(info.node_info.network).toEqual(defaultNetworkId);
const { result } = await client.authAccounts(unusedAccount.address);
expect(result).toEqual({
type: "cosmos-sdk/Account",
value: {
address: unusedAccount.address,
public_key: "", // not known to the chain
coins: [
{
amount: "1000000000",
denom: "ucosm",
},
{
amount: "1000000000",
denom: "ustake",
},
],
account_number: 5,
sequence: 0,
},
});
});
// This fails in the first test run if you forget to run `./scripts/wasmd/init.sh`
it("has correct pubkey for faucet", async () => {
pendingWithoutWasmd();
const client = new RestClient(httpUrl);
const { result } = await client.authAccounts(faucet.address);
expect(result.value).toEqual(
jasmine.objectContaining({
public_key: encodeBech32Pubkey(faucet.pubkey, "cosmospub"),
}),
);
});
// This property is used by CosmWasmClient.getAccount
it("returns empty address for non-existent account", async () => {
pendingWithoutWasmd();
const client = new RestClient(httpUrl);
const nonExistentAccount = makeRandomAddress();
const { result } = await client.authAccounts(nonExistentAccount);
expect(result).toEqual({
type: "cosmos-sdk/Account",
value: jasmine.objectContaining({ address: "" }),
});
});
});
// The /blocks endpoints
describe("blocksLatest", () => {
it("works", async () => {
pendingWithoutWasmd();
@ -245,54 +292,190 @@ describe("RestClient", () => {
});
});
describe("authAccounts", () => {
it("works for unused account without pubkey", async () => {
// The /node_info endpoint
describe("nodeInfo", () => {
it("works", async () => {
pendingWithoutWasmd();
const client = new RestClient(httpUrl);
const { result } = await client.authAccounts(unusedAccount.address);
expect(result).toEqual({
type: "cosmos-sdk/Account",
value: {
address: unusedAccount.address,
public_key: "", // not known to the chain
coins: [
{
amount: "1000000000",
denom: "ucosm",
},
{
amount: "1000000000",
denom: "ustake",
},
],
account_number: 5,
sequence: 0,
},
});
const info = await client.nodeInfo();
expect(info.node_info.network).toEqual(defaultNetworkId);
});
});
// The /txs endpoints
describe("txsQuery", () => {
let posted:
| {
readonly sender: string;
readonly recipient: string;
readonly hash: string;
readonly height: number;
readonly tx: TxsResponse;
}
| undefined;
beforeAll(async () => {
if (wasmdEnabled()) {
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const recipient = makeRandomAddress();
const transferAmount = [
{
denom: "ucosm",
amount: "1234567",
},
];
const result = await client.sendTokens(recipient, transferAmount);
await sleep(50); // wait until tx is indexed
const txDetails = await new RestClient(httpUrl).txsById(result.transactionHash);
posted = {
sender: faucet.address,
recipient: recipient,
hash: result.transactionHash,
height: Number.parseInt(txDetails.height, 10),
tx: txDetails,
};
}
});
// This fails in the first test run if you forget to run `./scripts/wasmd/init.sh`
it("has correct pubkey for faucet", async () => {
it("can query transactions by height", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(httpUrl);
const { result } = await client.authAccounts(faucet.address);
expect(result.value).toEqual(
jasmine.objectContaining({
public_key: encodeBech32Pubkey(faucet.pubkey, "cosmospub"),
}),
);
const result = await client.txsQuery(`tx.height=${posted.height}&limit=26`);
expect(parseInt(result.count, 10)).toEqual(1);
expect(parseInt(result.limit, 10)).toEqual(26);
expect(parseInt(result.page_number, 10)).toEqual(1);
expect(parseInt(result.page_total, 10)).toEqual(1);
expect(parseInt(result.total_count, 10)).toEqual(1);
expect(result.txs).toEqual([posted.tx]);
});
// This property is used by CosmWasmClient.getAccount
it("returns empty address for non-existent account", async () => {
it("can query transactions by ID", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(httpUrl);
const nonExistentAccount = makeRandomAddress();
const { result } = await client.authAccounts(nonExistentAccount);
expect(result).toEqual({
type: "cosmos-sdk/Account",
value: jasmine.objectContaining({ address: "" }),
});
const result = await client.txsQuery(`tx.hash=${posted.hash}&limit=26`);
expect(parseInt(result.count, 10)).toEqual(1);
expect(parseInt(result.limit, 10)).toEqual(26);
expect(parseInt(result.page_number, 10)).toEqual(1);
expect(parseInt(result.page_total, 10)).toEqual(1);
expect(parseInt(result.total_count, 10)).toEqual(1);
expect(result.txs).toEqual([posted.tx]);
});
it("can query transactions by sender", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(httpUrl);
const result = await client.txsQuery(`message.sender=${posted.sender}&limit=200`);
expect(parseInt(result.count, 10)).toBeGreaterThanOrEqual(1);
expect(parseInt(result.limit, 10)).toEqual(200);
expect(parseInt(result.page_number, 10)).toEqual(1);
expect(parseInt(result.page_total, 10)).toEqual(1);
expect(parseInt(result.total_count, 10)).toBeGreaterThanOrEqual(1);
expect(result.txs.length).toBeGreaterThanOrEqual(1);
expect(result.txs[result.txs.length - 1]).toEqual(posted.tx);
});
it("can query transactions by recipient", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(httpUrl);
const result = await client.txsQuery(`transfer.recipient=${posted.recipient}&limit=200`);
expect(parseInt(result.count, 10)).toEqual(1);
expect(parseInt(result.limit, 10)).toEqual(200);
expect(parseInt(result.page_number, 10)).toEqual(1);
expect(parseInt(result.page_total, 10)).toEqual(1);
expect(parseInt(result.total_count, 10)).toEqual(1);
expect(result.txs.length).toBeGreaterThanOrEqual(1);
expect(result.txs[result.txs.length - 1]).toEqual(posted.tx);
});
it("can filter by tx.hash and tx.minheight", async () => {
pending("This combination is broken 🤷‍♂️. Handle client-side at higher level.");
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(httpUrl);
const hashQuery = `tx.hash=${posted.hash}`;
{
const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=0`);
expect(count).toEqual("1");
}
{
const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height - 1}`);
expect(count).toEqual("1");
}
{
const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height}`);
expect(count).toEqual("1");
}
{
const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height + 1}`);
expect(count).toEqual("0");
}
});
it("can filter by recipient and tx.minheight", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(httpUrl);
const recipientQuery = `transfer.recipient=${posted.recipient}`;
{
const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=0`);
expect(count).toEqual("1");
}
{
const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height - 1}`);
expect(count).toEqual("1");
}
{
const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height}`);
expect(count).toEqual("1");
}
{
const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height + 1}`);
expect(count).toEqual("0");
}
});
it("can filter by recipient and tx.maxheight", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(httpUrl);
const recipientQuery = `transfer.recipient=${posted.recipient}`;
{
const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=9999999999999`);
expect(count).toEqual("1");
}
{
const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height + 1}`);
expect(count).toEqual("1");
}
{
const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height}`);
expect(count).toEqual("1");
}
{
const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height - 1}`);
expect(count).toEqual("0");
}
});
});
@ -304,7 +487,7 @@ describe("RestClient", () => {
});
});
describe("post", () => {
describe("postTx", () => {
it("can send tokens", async () => {
pendingWithoutWasmd();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
@ -340,7 +523,7 @@ describe("RestClient", () => {
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
const result = await client.postTx(marshalTx(signedTx));
const result = await client.postTx(signedTx);
// console.log("Raw log:", result.raw_log);
expect(result.code).toBeFalsy();
});
@ -410,6 +593,8 @@ describe("RestClient", () => {
});
});
// The /wasm endpoints
describe("query", () => {
it("can list upload code", async () => {
pendingWithoutWasmd();

View File

@ -6,13 +6,13 @@ import {
ContractInfo,
CosmosSdkAccount,
CosmosSdkTx,
isStdTx,
Model,
parseWasmData,
StdTx,
WasmData,
} from "./types";
const { fromBase64, fromUtf8, toHex, toUtf8 } = Encoding;
const { fromBase64, toHex, toUtf8 } = Encoding;
interface NodeInfo {
readonly network: string;
@ -235,14 +235,19 @@ export class RestClient {
return data;
}
public async nodeInfo(): Promise<NodeInfoResponse> {
const responseData = await this.get("/node_info");
if (!(responseData as any).node_info) {
// The /auth endpoints
public async authAccounts(address: string): Promise<AuthAccountsResponse> {
const path = `/auth/accounts/${address}`;
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 NodeInfoResponse;
return responseData as AuthAccountsResponse;
}
// The /blocks endpoints
public async blocksLatest(): Promise<BlockResponse> {
const responseData = await this.get("/blocks/latest");
if (!(responseData as any).block) {
@ -259,25 +264,19 @@ export class RestClient {
return responseData as BlockResponse;
}
/** returns the amino-encoding of the transaction performed by the server */
public async encodeTx(tx: CosmosSdkTx): Promise<Uint8Array> {
const responseData = await this.post("/txs/encode", tx);
if (!(responseData as any).tx) {
// The /node_info endpoint
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 Encoding.fromBase64((responseData as EncodeTxResponse).tx);
return responseData as NodeInfoResponse;
}
public async authAccounts(address: string): Promise<AuthAccountsResponse> {
const path = `/auth/accounts/${address}`;
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;
}
// The /txs endpoints
public async txs(query: string): Promise<SearchTxsResponse> {
public async txsQuery(query: string): Promise<SearchTxsResponse> {
const responseData = await this.get(`/txs?${query}`);
if (!(responseData as any).txs) {
throw new Error("Unexpected response data format");
@ -293,15 +292,25 @@ export class RestClient {
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 (!isStdTx(decoded)) {
throw new Error("Must be json encoded StdTx");
/** returns the amino-encoding of the transaction performed by the server */
public async encodeTx(tx: CosmosSdkTx): Promise<Uint8Array> {
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);
}
/**
* 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: decoded,
tx: tx,
mode: this.mode,
};
const responseData = await this.post("/txs", params);
@ -311,6 +320,8 @@ export class RestClient {
return responseData as PostTxsResponse;
}
// The /wasm endpoints
// 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`;

View File

@ -3,7 +3,7 @@ import { Encoding } from "@iov/encoding";
import pako from "pako";
import { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient";
import { makeSignBytes, marshalTx } from "./encoding";
import { makeSignBytes } from "./encoding";
import { findAttribute, Log } from "./logs";
import { BroadcastMode } from "./restclient";
import {
@ -120,7 +120,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
signatures: [signature],
};
const result = await this.postTx(marshalTx(signedTx));
const result = await this.postTx(signedTx);
const codeIdAttr = findAttribute(result.logs, "message", "code_id");
return {
originalSize: wasmCode.length,
@ -162,7 +162,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
signatures: [signature],
};
const result = await this.postTx(marshalTx(signedTx));
const result = await this.postTx(signedTx);
const contractAddressAttr = findAttribute(result.logs, "message", "contract_address");
return contractAddressAttr.value;
}
@ -195,7 +195,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
signatures: [signature],
};
const result = await this.postTx(marshalTx(signedTx));
const result = await this.postTx(signedTx);
return {
logs: result.logs,
};
@ -228,6 +228,6 @@ export class SigningCosmWasmClient extends CosmWasmClient {
signatures: [signature],
};
return this.postTx(marshalTx(signedTx));
return this.postTx(signedTx);
}
}

View File

@ -1,6 +1,6 @@
import { Log } from "./logs";
import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient";
import { CosmosSdkAccount, CosmosSdkTx } from "./types";
import { CosmosSdkAccount, CosmosSdkTx, StdTx } from "./types";
export interface GetNonceResult {
readonly accountNumber: number;
readonly sequence: number;
@ -21,6 +21,10 @@ export interface SearchBySentFromOrToQuery {
readonly sentFromOrTo: string;
}
export declare type SearchTxQuery = SearchByIdQuery | SearchByHeightQuery | SearchBySentFromOrToQuery;
export interface SearchTxFilter {
readonly minHeight?: number;
readonly maxHeight?: number;
}
export declare class CosmWasmClient {
protected readonly restClient: RestClient;
constructor(url: string, broadcastMode?: BroadcastMode);
@ -44,8 +48,8 @@ export declare class CosmWasmClient {
* @param height The height of the block. If undefined, the latest height is used.
*/
getBlock(height?: number): Promise<BlockResponse>;
searchTx(query: SearchTxQuery): Promise<readonly TxsResponse[]>;
postTx(tx: Uint8Array): Promise<PostTxResult>;
searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise<readonly TxsResponse[]>;
postTx(tx: StdTx): Promise<PostTxResult>;
/**
* Returns the data at the key if present (raw contract dependent storage data)
* or null if no data at this key.
@ -60,4 +64,5 @@ export declare class CosmWasmClient {
* Promise is rejected for invalid query format.
*/
queryContractSmart(address: string, queryMsg: object): Promise<Uint8Array>;
private txsQuery;
}

View File

@ -13,6 +13,7 @@ export {
SearchByIdQuery,
SearchBySentFromOrToQuery,
SearchTxQuery,
SearchTxFilter,
} from "./cosmwasmclient";
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
export {

View File

@ -1,4 +1,4 @@
import { CodeInfo, ContractInfo, CosmosSdkAccount, CosmosSdkTx, Model } from "./types";
import { CodeInfo, ContractInfo, CosmosSdkAccount, CosmosSdkTx, Model, StdTx } from "./types";
interface NodeInfo {
readonly network: string;
}
@ -124,15 +124,22 @@ export declare class RestClient {
constructor(url: string, mode?: BroadcastMode);
get(path: string): Promise<RestClientResponse>;
post(path: string, params: PostTxsParams): Promise<RestClientResponse>;
nodeInfo(): Promise<NodeInfoResponse>;
authAccounts(address: string): Promise<AuthAccountsResponse>;
blocksLatest(): Promise<BlockResponse>;
blocks(height: number): Promise<BlockResponse>;
nodeInfo(): Promise<NodeInfoResponse>;
txsQuery(query: string): Promise<SearchTxsResponse>;
txsById(id: string): Promise<TxsResponse>;
/** returns the amino-encoding of the transaction performed by the server */
encodeTx(tx: CosmosSdkTx): Promise<Uint8Array>;
authAccounts(address: string): Promise<AuthAccountsResponse>;
txs(query: string): Promise<SearchTxsResponse>;
txsById(id: string): Promise<TxsResponse>;
postTx(tx: Uint8Array): Promise<PostTxsResponse>;
/**
* 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)
*/
postTx(tx: StdTx): Promise<PostTxsResponse>;
listCodeInfo(): Promise<readonly CodeInfo[]>;
getCode(id: number): Promise<Uint8Array>;
listContractAddresses(): Promise<readonly string[]>;