Merge pull request #380 from CosmWasm/272-stargate-search-tx
Add searchTx method to StargateClient
This commit is contained in:
commit
54357fa75a
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,10 @@ export {
|
||||
SearchByTagsQuery,
|
||||
SearchTxQuery,
|
||||
SearchTxFilter,
|
||||
isSearchByHeightQuery,
|
||||
isSearchByIdQuery,
|
||||
isSearchBySentFromOrToQuery,
|
||||
isSearchByTagsQuery,
|
||||
} from "./cosmosclient";
|
||||
export { makeSignBytes } from "./encoding";
|
||||
export {
|
||||
|
||||
4
packages/launchpad/types/cosmosclient.d.ts
vendored
4
packages/launchpad/types/cosmosclient.d.ts
vendored
@ -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;
|
||||
|
||||
4
packages/launchpad/types/index.d.ts
vendored
4
packages/launchpad/types/index.d.ts
vendored
@ -21,6 +21,10 @@ export {
|
||||
SearchByTagsQuery,
|
||||
SearchTxQuery,
|
||||
SearchTxFilter,
|
||||
isSearchByHeightQuery,
|
||||
isSearchByIdQuery,
|
||||
isSearchBySentFromOrToQuery,
|
||||
isSearchByTagsQuery,
|
||||
} from "./cosmosclient";
|
||||
export { makeSignBytes } from "./encoding";
|
||||
export {
|
||||
|
||||
254
packages/stargate/src/stargateclient.searchtx.spec.ts
Normal file
254
packages/stargate/src/stargateclient.searchtx.spec.ts
Normal 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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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([]),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
14
packages/stargate/types/stargateclient.d.ts
vendored
14
packages/stargate/types/stargateclient.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user