Merge pull request #119 from confio/rest-cleanup
RestClient cleanups; Pass min/max height filters to backend
This commit is contained in:
commit
41db029f88
@ -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(
|
||||
|
||||
4
packages/bcp/types/cosmwasmconnection.d.ts
vendored
4
packages/bcp/types/cosmwasmconnection.d.ts
vendored
@ -73,8 +73,8 @@ export declare class CosmWasmConnection implements BlockchainConnection {
|
||||
searchTx({
|
||||
height,
|
||||
id,
|
||||
maxHeight: maxHeightOptional,
|
||||
minHeight: minHeightOptional,
|
||||
maxHeight,
|
||||
minHeight,
|
||||
sentFromOrTo,
|
||||
signedBy,
|
||||
tags,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}`)
|
||||
}
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ export {
|
||||
SearchByIdQuery,
|
||||
SearchBySentFromOrToQuery,
|
||||
SearchTxQuery,
|
||||
SearchTxFilter,
|
||||
} from "./cosmwasmclient";
|
||||
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
|
||||
export {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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`;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
11
packages/sdk/types/cosmwasmclient.d.ts
vendored
11
packages/sdk/types/cosmwasmclient.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
1
packages/sdk/types/index.d.ts
vendored
1
packages/sdk/types/index.d.ts
vendored
@ -13,6 +13,7 @@ export {
|
||||
SearchByIdQuery,
|
||||
SearchBySentFromOrToQuery,
|
||||
SearchTxQuery,
|
||||
SearchTxFilter,
|
||||
} from "./cosmwasmclient";
|
||||
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
|
||||
export {
|
||||
|
||||
19
packages/sdk/types/restclient.d.ts
vendored
19
packages/sdk/types/restclient.d.ts
vendored
@ -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[]>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user