From 69e2e6b43ac51b600edfab2ff3c3bb49169d57ae Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 2 Mar 2020 10:28:54 +0100 Subject: [PATCH] Implement and test CosmWasmClient.searchTx --- packages/sdk/src/cosmwasmclient.spec.ts | 283 +++++++++++++++++------- packages/sdk/src/cosmwasmclient.ts | 21 +- packages/sdk/src/testutils.spec.ts | 5 + packages/sdk/types/cosmwasmclient.d.ts | 16 +- 4 files changed, 249 insertions(+), 76 deletions(-) diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 96b7cd44..b5c26c60 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -20,7 +20,15 @@ import { tendermintIdMatcher, wasmdEnabled, } from "./testutils.spec"; -import { CosmosSdkTx, isMsgSend, MsgSend, StdFee } from "./types"; +import { + Coin, + CosmosSdkTx, + isMsgExecuteContract, + isMsgInstantiateContract, + isMsgSend, + MsgSend, + StdFee, +} from "./types"; const { fromAscii, fromHex, fromUtf8, toAscii, toBase64 } = Encoding; @@ -213,7 +221,7 @@ describe("CosmWasmClient", () => { }); describe("searchTx", () => { - let posted: + let postedSend: | { readonly sender: string; readonly recipient: string; @@ -222,44 +230,72 @@ describe("CosmWasmClient", () => { readonly tx: CosmosSdkTx; } | undefined; + let postedExecute: + | { + readonly sender: string; + readonly contract: string; + readonly hash: string; + readonly height: number; + readonly tx: CosmosSdkTx; + } + | 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 = [ - { + { + const recipient = makeRandomAddress(); + const transferAmount: Coin = { denom: "ucosm", amount: "1234567", - }, - ]; - const result = await client.sendTokens(recipient, transferAmount); + }; + const result = await client.sendTokens(recipient, [transferAmount]); + await sleep(50); // wait until tx is indexed + const txDetails = await new RestClient(httpUrl).txsById(result.transactionHash); + postedSend = { + sender: faucet.address, + recipient: recipient, + hash: result.transactionHash, + height: Number.parseInt(txDetails.height, 10), + tx: txDetails.tx, + }; + } - 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.tx, - }; + { + const hashInstance = deployedErc20.instances[0]; + const msg = { + approve: { + spender: makeRandomAddress(), + amount: "12", + }, + }; + const result = await client.execute(hashInstance, msg); + await sleep(50); // wait until tx is indexed + const txDetails = await new RestClient(httpUrl).txsById(result.transactionHash); + postedExecute = { + sender: faucet.address, + contract: hashInstance, + hash: result.transactionHash, + height: Number.parseInt(txDetails.height, 10), + tx: txDetails.tx, + }; + } } }); it("can search by ID", async () => { pendingWithoutWasmd(); - assert(posted, "value must be set in beforeAll()"); + assert(postedSend, "value must be set in beforeAll()"); const client = new CosmWasmClient(httpUrl); - const result = await client.searchTx({ id: posted.hash }); + const result = await client.searchTx({ id: postedSend.hash }); expect(result.length).toEqual(1); expect(result[0]).toEqual( jasmine.objectContaining({ - height: posted.height.toString(), - txhash: posted.hash, - tx: posted.tx, + height: postedSend.height.toString(), + txhash: postedSend.hash, + tx: postedSend.tx, }), ); }); @@ -274,50 +310,24 @@ describe("CosmWasmClient", () => { it("can search by height", async () => { pendingWithoutWasmd(); - assert(posted, "value must be set in beforeAll()"); + assert(postedSend, "value must be set in beforeAll()"); const client = new CosmWasmClient(httpUrl); - const result = await client.searchTx({ height: posted.height }); + const result = await client.searchTx({ height: postedSend.height }); expect(result.length).toEqual(1); expect(result[0]).toEqual( jasmine.objectContaining({ - height: posted.height.toString(), - txhash: posted.hash, - tx: posted.tx, + height: postedSend.height.toString(), + txhash: postedSend.hash, + tx: postedSend.tx, }), ); }); it("can search by sender", async () => { pendingWithoutWasmd(); - assert(posted, "value must be set in beforeAll()"); + assert(postedSend, "value must be set in beforeAll()"); const client = new CosmWasmClient(httpUrl); - const results = await client.searchTx({ sentFromOrTo: posted.sender }); - expect(results.length).toBeGreaterThanOrEqual(1); - - // Check basic structure of all results - for (const result of results) { - const msg = fromOneElementArray(result.tx.value.msg); - assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`); - expect(msg.value.to_address === posted.sender || msg.value.from_address == posted.sender).toEqual( - true, - ); - } - - // Check details of most recent result - expect(results[results.length - 1]).toEqual( - jasmine.objectContaining({ - height: posted.height.toString(), - txhash: posted.hash, - tx: posted.tx, - }), - ); - }); - - it("can search by recipient", async () => { - pendingWithoutWasmd(); - assert(posted, "value must be set in beforeAll()"); - const client = new CosmWasmClient(httpUrl); - const results = await client.searchTx({ sentFromOrTo: posted.recipient }); + const results = await client.searchTx({ sentFromOrTo: postedSend.sender }); expect(results.length).toBeGreaterThanOrEqual(1); // Check basic structure of all results @@ -325,25 +335,51 @@ describe("CosmWasmClient", () => { const msg = fromOneElementArray(result.tx.value.msg); assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`); expect( - msg.value.to_address === posted.recipient || msg.value.from_address == posted.recipient, + msg.value.to_address === postedSend.sender || msg.value.from_address == postedSend.sender, ).toEqual(true); } // Check details of most recent result expect(results[results.length - 1]).toEqual( jasmine.objectContaining({ - height: posted.height.toString(), - txhash: posted.hash, - tx: posted.tx, + height: postedSend.height.toString(), + txhash: postedSend.hash, + tx: postedSend.tx, + }), + ); + }); + + it("can search by recipient", async () => { + pendingWithoutWasmd(); + assert(postedSend, "value must be set in beforeAll()"); + const client = new CosmWasmClient(httpUrl); + const results = await client.searchTx({ sentFromOrTo: postedSend.recipient }); + expect(results.length).toBeGreaterThanOrEqual(1); + + // Check basic structure of all results + for (const result of results) { + const msg = fromOneElementArray(result.tx.value.msg); + assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`); + expect( + msg.value.to_address === postedSend.recipient || msg.value.from_address == postedSend.recipient, + ).toEqual(true); + } + + // Check details of most recent result + expect(results[results.length - 1]).toEqual( + jasmine.objectContaining({ + height: postedSend.height.toString(), + txhash: postedSend.hash, + tx: postedSend.tx, }), ); }); it("can search by ID and filter by minHeight", async () => { pendingWithoutWasmd(); - assert(posted); + assert(postedSend); const client = new CosmWasmClient(httpUrl); - const query = { id: posted.hash }; + const query = { id: postedSend.hash }; { const result = await client.searchTx(query, { minHeight: 0 }); @@ -351,26 +387,26 @@ describe("CosmWasmClient", () => { } { - const result = await client.searchTx(query, { minHeight: posted.height - 1 }); + const result = await client.searchTx(query, { minHeight: postedSend.height - 1 }); expect(result.length).toEqual(1); } { - const result = await client.searchTx(query, { minHeight: posted.height }); + const result = await client.searchTx(query, { minHeight: postedSend.height }); expect(result.length).toEqual(1); } { - const result = await client.searchTx(query, { minHeight: posted.height + 1 }); + const result = await client.searchTx(query, { minHeight: postedSend.height + 1 }); expect(result.length).toEqual(0); } }); it("can search by recipient and filter by minHeight", async () => { pendingWithoutWasmd(); - assert(posted); + assert(postedSend); const client = new CosmWasmClient(httpUrl); - const query = { sentFromOrTo: posted.recipient }; + const query = { sentFromOrTo: postedSend.recipient }; { const result = await client.searchTx(query, { minHeight: 0 }); @@ -378,26 +414,26 @@ describe("CosmWasmClient", () => { } { - const result = await client.searchTx(query, { minHeight: posted.height - 1 }); + const result = await client.searchTx(query, { minHeight: postedSend.height - 1 }); expect(result.length).toEqual(1); } { - const result = await client.searchTx(query, { minHeight: posted.height }); + const result = await client.searchTx(query, { minHeight: postedSend.height }); expect(result.length).toEqual(1); } { - const result = await client.searchTx(query, { minHeight: posted.height + 1 }); + const result = await client.searchTx(query, { minHeight: postedSend.height + 1 }); expect(result.length).toEqual(0); } }); it("can search by recipient and filter by maxHeight", async () => { pendingWithoutWasmd(); - assert(posted); + assert(postedSend); const client = new CosmWasmClient(httpUrl); - const query = { sentFromOrTo: posted.recipient }; + const query = { sentFromOrTo: postedSend.recipient }; { const result = await client.searchTx(query, { maxHeight: 9999999999999 }); @@ -405,20 +441,119 @@ describe("CosmWasmClient", () => { } { - const result = await client.searchTx(query, { maxHeight: posted.height + 1 }); + const result = await client.searchTx(query, { maxHeight: postedSend.height + 1 }); expect(result.length).toEqual(1); } { - const result = await client.searchTx(query, { maxHeight: posted.height }); + const result = await client.searchTx(query, { maxHeight: postedSend.height }); expect(result.length).toEqual(1); } { - const result = await client.searchTx(query, { maxHeight: posted.height - 1 }); + const result = await client.searchTx(query, { maxHeight: postedSend.height - 1 }); expect(result.length).toEqual(0); } }); + + describe("with SearchByTagsQuery", () => { + it("can search by transfer.recipient", async () => { + pendingWithoutWasmd(); + assert(postedSend, "value must be set in beforeAll()"); + const client = new CosmWasmClient(httpUrl); + const results = await client.searchTx({ + tags: [{ key: "transfer.recipient", value: postedSend.recipient }], + }); + expect(results.length).toBeGreaterThanOrEqual(1); + + // Check basic structure of all results + for (const result of results) { + const msg = fromOneElementArray(result.tx.value.msg); + assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`); + expect(msg.value.to_address).toEqual(postedSend.recipient); + } + + // Check details of most recent result + expect(results[results.length - 1]).toEqual( + jasmine.objectContaining({ + height: postedSend.height.toString(), + txhash: postedSend.hash, + tx: postedSend.tx, + }), + ); + }); + + it("can search by message.contract_address", async () => { + pendingWithoutWasmd(); + assert(postedExecute, "value must be set in beforeAll()"); + const client = new CosmWasmClient(httpUrl); + const results = await client.searchTx({ + tags: [{ key: "message.contract_address", value: postedExecute.contract }], + }); + expect(results.length).toBeGreaterThanOrEqual(1); + + // Check basic structure of all results + for (const result of results) { + const msg = fromOneElementArray(result.tx.value.msg); + assert( + isMsgExecuteContract(msg) || isMsgInstantiateContract(msg), + `${result.txhash} (at ${result.height}) not an execute or instantiate msg`, + ); + } + + // Check that the first result is the instantiation + const first = fromOneElementArray(results[0].tx.value.msg); + assert(isMsgInstantiateContract(first), "First contract search result must be an instantiation"); + expect(first).toEqual({ + type: "wasm/instantiate", + value: { + sender: faucet.address, + code_id: deployedErc20.codeId.toString(), + label: "HASH", + init_msg: jasmine.objectContaining({ symbol: "HASH" }), + init_funds: [], + }, + }); + + // Check details of most recent result + expect(results[results.length - 1]).toEqual( + jasmine.objectContaining({ + height: postedExecute.height.toString(), + txhash: postedExecute.hash, + tx: postedExecute.tx, + }), + ); + }); + + it("can search by message.contract_address + message.action", async () => { + pendingWithoutWasmd(); + assert(postedExecute, "value must be set in beforeAll()"); + const client = new CosmWasmClient(httpUrl); + const results = await client.searchTx({ + tags: [ + { key: "message.contract_address", value: postedExecute.contract }, + { key: "message.action", value: "execute" }, + ], + }); + expect(results.length).toBeGreaterThanOrEqual(1); + + // Check basic structure of all results + for (const result of results) { + const msg = fromOneElementArray(result.tx.value.msg); + assert(isMsgExecuteContract(msg), `${result.txhash} (at ${result.height}) not an execute msg`); + expect(msg.value.contract).toEqual(postedExecute.contract); + } + + // Check details of most recent result + expect(results[results.length - 1]).toEqual( + jasmine.objectContaining({ + height: postedExecute.height.toString(), + txhash: postedExecute.hash, + tx: postedExecute.tx, + }), + ); + }); + }); }); describe("getCodes", () => { diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index c173f5d6..5ce22c5f 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -29,7 +29,19 @@ export interface SearchBySentFromOrToQuery { readonly sentFromOrTo: string; } -export type SearchTxQuery = SearchByIdQuery | SearchByHeightQuery | SearchBySentFromOrToQuery; +/** + * This query type allows you to pass arbitrary key/value pairs to the backend. It is + * more powerful and slightly lower level than the other search options. + */ +export interface SearchByTagsQuery { + readonly tags: readonly { readonly key: string; readonly value: string }[]; +} + +export type SearchTxQuery = + | SearchByIdQuery + | SearchByHeightQuery + | SearchBySentFromOrToQuery + | SearchByTagsQuery; function isSearchByIdQuery(query: SearchTxQuery): query is SearchByIdQuery { return (query as SearchByIdQuery).id !== undefined; @@ -43,6 +55,10 @@ function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySen return (query as SearchBySentFromOrToQuery).sentFromOrTo !== undefined; } +function isSearchByTagsQuery(query: SearchTxQuery): query is SearchByTagsQuery { + return (query as SearchByTagsQuery).tags !== undefined; +} + export interface SearchTxFilter { readonly minHeight?: number; readonly maxHeight?: number; @@ -166,6 +182,9 @@ export class CosmWasmClient { const sentHashes = sent.map(t => t.txhash); txs = [...sent, ...received.filter(t => !sentHashes.includes(t.txhash))]; + } else if (isSearchByTagsQuery(query)) { + const rawQuery = withFilters(query.tags.map(t => `${t.key}=${t.value}`).join("&")); + txs = await this.txsQuery(rawQuery); } else { throw new Error("Unknown query type"); } diff --git a/packages/sdk/src/testutils.spec.ts b/packages/sdk/src/testutils.spec.ts index d072842f..78f3c090 100644 --- a/packages/sdk/src/testutils.spec.ts +++ b/packages/sdk/src/testutils.spec.ts @@ -69,6 +69,11 @@ export const deployedErc20 = { source: "https://crates.io/api/v1/crates/cw-erc20/0.2.0/download", builder: "confio/cosmwasm-opt:0.7.0", checksum: "aff8c8873d79d2153a8b9066a0683fec3c903669267eb806ffa831dcd4b3daae", + instances: [ + "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", // HASH + "cosmos1hqrdl6wstt8qzshwc6mrumpjk9338k0lr4dqxd", // ISA + "cosmos18r5szma8hm93pvx6lwpjwyxruw27e0k5uw835c", // JADE + ], }; export function wasmdEnabled(): boolean { diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts index 5b8ada44..f544e060 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -20,7 +20,21 @@ export interface SearchByHeightQuery { export interface SearchBySentFromOrToQuery { readonly sentFromOrTo: string; } -export declare type SearchTxQuery = SearchByIdQuery | SearchByHeightQuery | SearchBySentFromOrToQuery; +/** + * This query type allows you to pass arbitrary key/value pairs to the backend. It is + * more powerful and slightly lower level than the other search options. + */ +export interface SearchByTagsQuery { + readonly tags: readonly { + readonly key: string; + readonly value: string; + }[]; +} +export declare type SearchTxQuery = + | SearchByIdQuery + | SearchByHeightQuery + | SearchBySentFromOrToQuery + | SearchByTagsQuery; export interface SearchTxFilter { readonly minHeight?: number; readonly maxHeight?: number;