Merge pull request #380 from CosmWasm/272-stargate-search-tx

Add searchTx method to StargateClient
This commit is contained in:
mergify[bot] 2020-08-13 13:06:54 +00:00 committed by GitHub
commit 54357fa75a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 372 additions and 17 deletions

View File

@ -91,19 +91,19 @@ export type SearchTxQuery =
| SearchBySentFromOrToQuery
| SearchByTagsQuery;
function isSearchByIdQuery(query: SearchTxQuery): query is SearchByIdQuery {
export function isSearchByIdQuery(query: SearchTxQuery): query is SearchByIdQuery {
return (query as SearchByIdQuery).id !== undefined;
}
function isSearchByHeightQuery(query: SearchTxQuery): query is SearchByHeightQuery {
export function isSearchByHeightQuery(query: SearchTxQuery): query is SearchByHeightQuery {
return (query as SearchByHeightQuery).height !== undefined;
}
function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySentFromOrToQuery {
export function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySentFromOrToQuery {
return (query as SearchBySentFromOrToQuery).sentFromOrTo !== undefined;
}
function isSearchByTagsQuery(query: SearchTxQuery): query is SearchByTagsQuery {
export function isSearchByTagsQuery(query: SearchTxQuery): query is SearchByTagsQuery {
return (query as SearchByTagsQuery).tags !== undefined;
}

View File

@ -23,6 +23,10 @@ export {
SearchByTagsQuery,
SearchTxQuery,
SearchTxFilter,
isSearchByHeightQuery,
isSearchByIdQuery,
isSearchBySentFromOrToQuery,
isSearchByTagsQuery,
} from "./cosmosclient";
export { makeSignBytes } from "./encoding";
export {

View File

@ -61,6 +61,10 @@ export declare type SearchTxQuery =
| SearchByHeightQuery
| SearchBySentFromOrToQuery
| SearchByTagsQuery;
export declare function isSearchByIdQuery(query: SearchTxQuery): query is SearchByIdQuery;
export declare function isSearchByHeightQuery(query: SearchTxQuery): query is SearchByHeightQuery;
export declare function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySentFromOrToQuery;
export declare function isSearchByTagsQuery(query: SearchTxQuery): query is SearchByTagsQuery;
export interface SearchTxFilter {
readonly minHeight?: number;
readonly maxHeight?: number;

View File

@ -21,6 +21,10 @@ export {
SearchByTagsQuery,
SearchTxQuery,
SearchTxFilter,
isSearchByHeightQuery,
isSearchByIdQuery,
isSearchBySentFromOrToQuery,
isSearchByTagsQuery,
} from "./cosmosclient";
export { makeSignBytes } from "./encoding";
export {

View File

@ -0,0 +1,254 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { Bech32, fromBase64 } from "@cosmjs/encoding";
import { Coin, coins, Secp256k1Wallet } from "@cosmjs/launchpad";
import { makeSignBytes, omitDefaults, Registry } from "@cosmjs/proto-signing";
import { assert } from "@cosmjs/utils";
import { cosmos } from "./generated/codecimpl";
import {
BroadcastTxResponse,
isBroadcastTxFailure,
isBroadcastTxSuccess,
StargateClient,
} from "./stargateclient";
import { faucet, makeRandomAddress, pendingWithoutSimapp, simapp, simappEnabled } from "./testutils.spec";
const { AuthInfo, SignDoc, Tx, TxBody } = cosmos.tx;
const { PublicKey } = cosmos.crypto;
interface TestTxSend {
readonly sender: string;
readonly recipient: string;
readonly hash: string;
readonly height: number;
readonly tx: Uint8Array;
}
async function sendTokens(
client: StargateClient,
registry: Registry,
wallet: Secp256k1Wallet,
recipient: string,
amount: readonly Coin[],
memo: string,
): Promise<{
readonly broadcastResponse: BroadcastTxResponse;
readonly tx: Uint8Array;
}> {
const [{ address: walletAddress, pubkey: pubkeyBytes }] = await wallet.getAccounts();
const publicKey = PublicKey.create({ secp256k1: pubkeyBytes });
const txBodyFields = {
typeUrl: "/cosmos.tx.TxBody",
value: {
messages: [
{
typeUrl: "/cosmos.bank.MsgSend",
value: {
fromAddress: Bech32.decode(walletAddress).data,
toAddress: Bech32.decode(recipient).data,
amount: amount,
},
},
],
memo: memo,
},
};
const txBodyBytes = registry.encode(txBodyFields);
const txBody = TxBody.decode(txBodyBytes);
const authInfo = {
signerInfos: [
{
publicKey: publicKey,
modeInfo: {
single: {
mode: cosmos.tx.signing.SignMode.SIGN_MODE_DIRECT,
},
},
},
],
fee: {
gasLimit: 200000,
},
};
const authInfoBytes = Uint8Array.from(AuthInfo.encode(authInfo).finish());
const { accountNumber, sequence } = (await client.getSequence(walletAddress))!;
const chainId = await client.getChainId();
const signDoc = SignDoc.create(
omitDefaults({
bodyBytes: txBodyBytes,
authInfoBytes: authInfoBytes,
chainId: chainId,
accountNumber: accountNumber,
accountSequence: sequence,
}),
);
const signDocBytes = makeSignBytes(signDoc);
const signature = await wallet.sign(walletAddress, signDocBytes);
const txRaw = Tx.create({
body: txBody,
authInfo: authInfo,
signatures: [fromBase64(signature.signature)],
});
const txRawBytes = Uint8Array.from(Tx.encode(txRaw).finish());
const broadcastResponse = await client.broadcastTx(txRawBytes);
return {
broadcastResponse: broadcastResponse,
tx: txRawBytes,
};
}
describe("StargateClient.searchTx", () => {
const registry = new Registry();
let sendUnsuccessful: TestTxSend | undefined;
let sendSuccessful: TestTxSend | undefined;
beforeAll(async () => {
if (simappEnabled()) {
const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic);
const client = await StargateClient.connect(simapp.tendermintUrl);
const unsuccessfulRecipient = makeRandomAddress();
const successfulRecipient = makeRandomAddress();
const unsuccessfulResult = await sendTokens(
client,
registry,
wallet,
unsuccessfulRecipient,
coins(123456700000000, "ucosm"),
"Sending more than I can afford",
);
if (isBroadcastTxFailure(unsuccessfulResult.broadcastResponse)) {
sendUnsuccessful = {
sender: faucet.address0,
recipient: unsuccessfulRecipient,
hash: unsuccessfulResult.broadcastResponse.transactionHash,
height: unsuccessfulResult.broadcastResponse.height,
tx: unsuccessfulResult.tx,
};
}
const successfulResult = await sendTokens(
client,
registry,
wallet,
successfulRecipient,
coins(1234567, "ucosm"),
"Something I can afford",
);
if (isBroadcastTxSuccess(successfulResult.broadcastResponse)) {
sendSuccessful = {
sender: faucet.address0,
recipient: successfulRecipient,
hash: successfulResult.broadcastResponse.transactionHash,
height: successfulResult.broadcastResponse.height,
tx: successfulResult.tx,
};
}
}
});
describe("with SearchByIdQuery", () => {
it("can search successful tx by ID", async () => {
pendingWithoutSimapp();
assert(sendSuccessful, "value must be set in beforeAll()");
const client = await StargateClient.connect(simapp.tendermintUrl);
const result = await client.searchTx({ id: sendSuccessful.hash });
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
height: sendSuccessful.height,
hash: sendSuccessful.hash,
code: 0,
tx: sendSuccessful.tx,
}),
);
});
it("can search unsuccessful tx by ID", async () => {
pendingWithoutSimapp();
assert(sendUnsuccessful, "value must be set in beforeAll()");
const client = await StargateClient.connect(simapp.tendermintUrl);
const result = await client.searchTx({ id: sendUnsuccessful.hash });
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
height: sendUnsuccessful.height,
hash: sendUnsuccessful.hash,
code: 5,
tx: sendUnsuccessful.tx,
}),
);
});
it("can search by ID (non existent)", async () => {
pendingWithoutSimapp();
const client = await StargateClient.connect(simapp.tendermintUrl);
const nonExistentId = "0000000000000000000000000000000000000000000000000000000000000000";
const result = await client.searchTx({ id: nonExistentId });
expect(result.length).toEqual(0);
});
it("can search by ID and filter by minHeight", async () => {
pendingWithoutSimapp();
assert(sendSuccessful, "value must be set in beforeAll()");
const client = await StargateClient.connect(simapp.tendermintUrl);
const query = { id: sendSuccessful.hash };
{
const result = await client.searchTx(query, { minHeight: 0 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: sendSuccessful.height - 1 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: sendSuccessful.height });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: sendSuccessful.height + 1 });
expect(result.length).toEqual(0);
}
});
});
describe("with SearchByHeightQuery", () => {
it("can search successful tx by height", async () => {
pendingWithoutSimapp();
assert(sendSuccessful, "value must be set in beforeAll()");
const client = await StargateClient.connect(simapp.tendermintUrl);
const result = await client.searchTx({ height: sendSuccessful.height });
expect(result.length).toBeGreaterThanOrEqual(1);
expect(result).toContain(
jasmine.objectContaining({
height: sendSuccessful.height,
hash: sendSuccessful.hash,
code: 0,
tx: sendSuccessful.tx,
}),
);
});
it("can search unsuccessful tx by height", async () => {
pendingWithoutSimapp();
assert(sendUnsuccessful, "value must be set in beforeAll()");
const client = await StargateClient.connect(simapp.tendermintUrl);
const result = await client.searchTx({ height: sendUnsuccessful.height });
expect(result.length).toBeGreaterThanOrEqual(1);
expect(result).toContain(
jasmine.objectContaining({
height: sendUnsuccessful.height,
hash: sendUnsuccessful.hash,
code: 5,
tx: sendUnsuccessful.tx,
}),
);
});
});
});

View File

@ -147,7 +147,7 @@ describe("StargateClient", () => {
header: jasmine.objectContaining({
chainId: await client.getChainId(),
}),
txs: [],
txs: jasmine.arrayContaining([]),
}),
);
@ -171,7 +171,7 @@ describe("StargateClient", () => {
height: height - 1,
chainId: await client.getChainId(),
}),
txs: [],
txs: jasmine.arrayContaining([]),
}),
);

View File

@ -1,14 +1,34 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { Bech32, toAscii, toHex } from "@cosmjs/encoding";
import { Block, Coin, decodeAminoPubkey, PubKey } from "@cosmjs/launchpad";
import {
Block,
Coin,
decodeAminoPubkey,
isSearchByHeightQuery,
isSearchByIdQuery,
PubKey,
SearchTxFilter,
SearchTxQuery,
} from "@cosmjs/launchpad";
import { Uint53, Uint64 } from "@cosmjs/math";
import { decodeAny } from "@cosmjs/proto-signing";
import { broadcastTxCommitSuccess, Client as TendermintClient } from "@cosmjs/tendermint-rpc";
import { broadcastTxCommitSuccess, Client as TendermintClient, QueryString } from "@cosmjs/tendermint-rpc";
import { arrayContentEquals, assert, assertDefined } from "@cosmjs/utils";
import Long from "long";
import { cosmos } from "./generated/codecimpl";
/** A transaction that is indexed as part of the transaction history */
export interface IndexedTx {
readonly height: number;
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
readonly hash: string;
/** Transaction execution error code. 0 on success. */
readonly code: number;
readonly rawLog: string;
readonly tx: Uint8Array;
}
export interface Account {
/** Bech32 account address */
readonly address: string;
@ -206,25 +226,57 @@ export class StargateClient {
return response.balances.map(coinFromProto);
}
public async searchTx(query: SearchTxQuery, filter: SearchTxFilter = {}): Promise<readonly IndexedTx[]> {
const minHeight = filter.minHeight || 0;
const maxHeight = filter.maxHeight || Number.MAX_SAFE_INTEGER;
if (maxHeight < minHeight) return []; // optional optimization
let txs: readonly IndexedTx[];
if (isSearchByIdQuery(query)) {
txs = await this.txsQuery(`tx.hash='${query.id}'`);
} else if (isSearchByHeightQuery(query)) {
txs =
query.height >= minHeight && query.height <= maxHeight
? await this.txsQuery(`tx.height=${query.height}`)
: [];
} else {
throw new Error("Unknown query type");
}
const filtered = txs.filter((tx) => tx.height >= minHeight && tx.height <= maxHeight);
return filtered;
}
public disconnect(): void {
this.tmClient.disconnect();
}
public async broadcastTx(tx: Uint8Array): Promise<BroadcastTxResponse> {
const response = await this.tmClient.broadcastTxCommit({ tx });
return broadcastTxCommitSuccess(response)
if (broadcastTxCommitSuccess(response)) {
return {
height: response.height,
transactionHash: toHex(response.hash).toUpperCase(),
rawLog: response.deliverTx?.log,
data: response.deliverTx?.data,
};
}
return response.checkTx.code !== 0
? {
height: response.height,
transactionHash: toHex(response.hash).toUpperCase(),
rawLog: response.deliverTx?.log,
data: response.deliverTx?.data,
}
: {
height: response.height,
code: response.checkTx.code,
transactionHash: toHex(response.hash).toUpperCase(),
rawLog: response.checkTx.log,
data: response.checkTx.data,
}
: {
height: response.height,
code: response.deliverTx?.code,
transactionHash: toHex(response.hash).toUpperCase(),
rawLog: response.deliverTx?.log,
data: response.deliverTx?.data,
};
}
@ -264,4 +316,20 @@ export class StargateClient {
return response.value;
}
private async txsQuery(query: string): Promise<readonly IndexedTx[]> {
const params = {
query: query as QueryString,
};
const results = await this.tmClient.txSearchAll(params);
return results.txs.map((tx) => {
return {
height: tx.height,
hash: toHex(tx.hash).toUpperCase(),
code: tx.result.code,
rawLog: tx.result.log || "",
tx: tx.tx,
};
});
}
}

View File

@ -1,7 +1,12 @@
import { Random } from "@cosmjs/crypto";
import { Bech32 } from "@cosmjs/encoding";
export function simappEnabled(): boolean {
return !!process.env.SIMAPP_ENABLED;
}
export function pendingWithoutSimapp(): void {
if (!process.env.SIMAPP_ENABLED) {
if (!simappEnabled()) {
return pending("Set SIMAPP_ENABLED to enable Simapp based tests");
}
}
@ -10,6 +15,10 @@ export function makeRandomAddressBytes(): Uint8Array {
return Random.getBytes(20);
}
export function makeRandomAddress(): string {
return Bech32.encode("cosmos", makeRandomAddressBytes());
}
export const simapp = {
tendermintUrl: "localhost:26657",
chainId: "simd-testing",

View File

@ -1,5 +1,15 @@
import { Block, Coin, PubKey } from "@cosmjs/launchpad";
import { Block, Coin, PubKey, SearchTxFilter, SearchTxQuery } from "@cosmjs/launchpad";
import { Client as TendermintClient } from "@cosmjs/tendermint-rpc";
/** A transaction that is indexed as part of the transaction history */
export interface IndexedTx {
readonly height: number;
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
readonly hash: string;
/** Transaction execution error code. 0 on success. */
readonly code: number;
readonly rawLog: string;
readonly tx: Uint8Array;
}
export interface Account {
/** Bech32 account address */
readonly address: string;
@ -55,8 +65,10 @@ export declare class StargateClient {
* proofs from such a method.
*/
getAllBalancesUnverified(address: string): Promise<readonly Coin[]>;
searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise<readonly IndexedTx[]>;
disconnect(): void;
broadcastTx(tx: Uint8Array): Promise<BroadcastTxResponse>;
private queryVerified;
private queryUnverified;
private txsQuery;
}