diff --git a/packages/bcp/src/cosmwasmconnection.spec.ts b/packages/bcp/src/cosmwasmconnection.spec.ts index 4355fd57..864f41e5 100644 --- a/packages/bcp/src/cosmwasmconnection.spec.ts +++ b/packages/bcp/src/cosmwasmconnection.spec.ts @@ -589,6 +589,124 @@ describe("CosmWasmConnection", () => { connection.disconnect(); }); + it("can post an ERC20 transfer and search for the transaction", async () => { + pendingWithoutWasmd(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); + const profile = new UserProfile(); + const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic)); + const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path); + const senderAddress = connection.codec.identityToAddress(sender); + + const recipient = makeRandomAddress(); + const unsigned = await connection.withDefaultFee({ + kind: "bcp/send", + chainId: defaultChainId, + sender: senderAddress, + recipient: recipient, + memo: "My first payment", + amount: { + quantity: "75", + fractionalDigits: 0, + tokenTicker: "ISA" as TokenTicker, + }, + }); + const nonce = await connection.getNonce({ address: senderAddress }); + const signed = await profile.signTransaction(sender, unsigned, connection.codec, nonce); + const postableBytes = connection.codec.bytesToPost(signed); + const response = await connection.postTx(postableBytes); + const { transactionId } = response; + const blockInfo = await response.blockInfo.waitFor(info => !isBlockInfoPending(info)); + expect(blockInfo.state).toEqual(TransactionState.Succeeded); + + // search by id + const byIdResults = await connection.searchTx({ id: transactionId }); + expect(byIdResults.length).toEqual(1); + const byIdResult = byIdResults[0]; + expect(byIdResult.transactionId).toEqual(transactionId); + assert(isConfirmedTransaction(byIdResult), "Expected transaction to succeed"); + assert(byIdResult.log, "Log must be available"); + const [firstByIdlog] = JSON.parse(byIdResult.log); + expect(firstByIdlog.events.length).toEqual(2); + expect(firstByIdlog.events[0].type).toEqual("message"); + expect(firstByIdlog.events[1].type).toEqual("wasm"); + // wasm event attributes added by contract + expect(firstByIdlog.events[1].attributes).toContain({ key: "action", value: "transfer" }); + expect(firstByIdlog.events[1].attributes).toContain({ key: "sender", value: senderAddress }); + expect(firstByIdlog.events[1].attributes).toContain({ key: "recipient", value: recipient }); + // wasm event attributes added wasmd + expect(firstByIdlog.events[1].attributes).toContain({ + key: "contract_address", + value: defaultConfig.erc20Tokens![1].contractAddress, + }); + const byIdTransaction = byIdResult.transaction; + assert(isSendTransaction(byIdTransaction), "Expected send transaction"); + expect(byIdTransaction).toEqual(unsigned); + + // search by sender address + const bySenderResults = await connection.searchTx({ sentFromOrTo: senderAddress }); + expect(bySenderResults).toBeTruthy(); + expect(bySenderResults.length).toBeGreaterThanOrEqual(1); + const bySenderResult = bySenderResults[bySenderResults.length - 1]; + expect(bySenderResult.transactionId).toEqual(transactionId); + assert(isConfirmedTransaction(bySenderResult), "Expected transaction to succeed"); + assert(bySenderResult.log, "Log must be available"); + const [firstBySenderLog] = JSON.parse(bySenderResult.log); + expect(firstBySenderLog.events.length).toEqual(2); + expect(firstBySenderLog.events[0].type).toEqual("message"); + expect(firstBySenderLog.events[1].type).toEqual("wasm"); + // wasm event attributes added by contract + expect(firstBySenderLog.events[1].attributes).toContain({ key: "action", value: "transfer" }); + expect(firstBySenderLog.events[1].attributes).toContain({ key: "sender", value: senderAddress }); + expect(firstBySenderLog.events[1].attributes).toContain({ key: "recipient", value: recipient }); + // wasm event attributes added wasmd + expect(firstBySenderLog.events[1].attributes).toContain({ + key: "contract_address", + value: defaultConfig.erc20Tokens![1].contractAddress, + }); + const bySenderTransaction = bySenderResult.transaction; + assert(isSendTransaction(bySenderTransaction), "Expected send transaction"); + expect(bySenderTransaction).toEqual(unsigned); + + // search by recipient address + const byRecipientResults = await connection.searchTx({ sentFromOrTo: recipient }); + expect(byRecipientResults.length).toBeGreaterThanOrEqual(1); + const byRecipientResult = byRecipientResults[byRecipientResults.length - 1]; + expect(byRecipientResult.transactionId).toEqual(transactionId); + assert(isConfirmedTransaction(byRecipientResult), "Expected transaction to succeed"); + assert(byRecipientResult.log, "Log must be available"); + const [firstByRecipientLog] = JSON.parse(bySenderResult.log); + expect(firstByRecipientLog.events.length).toEqual(2); + expect(firstByRecipientLog.events[0].type).toEqual("message"); + expect(firstByRecipientLog.events[1].type).toEqual("wasm"); + // wasm event attributes added by contract + expect(firstByRecipientLog.events[1].attributes).toContain({ key: "action", value: "transfer" }); + expect(firstByRecipientLog.events[1].attributes).toContain({ key: "sender", value: senderAddress }); + expect(firstByRecipientLog.events[1].attributes).toContain({ key: "recipient", value: recipient }); + // wasm event attributes added wasmd + expect(firstByRecipientLog.events[1].attributes).toContain({ + key: "contract_address", + value: defaultConfig.erc20Tokens![1].contractAddress, + }); + const byRecipeintTransaction = byRecipientResult.transaction; + assert(isSendTransaction(byRecipeintTransaction), "Expected send transaction"); + expect(byRecipeintTransaction).toEqual(unsigned); + + // search by height + const heightResults = await connection.searchTx({ height: byIdResult.height }); + expect(heightResults.length).toEqual(1); + const heightResult = heightResults[0]; + expect(heightResult.transactionId).toEqual(transactionId); + assert(isConfirmedTransaction(heightResult), "Expected transaction to succeed"); + assert(heightResult.log, "Log must be available"); + const [firstHeightLog] = JSON.parse(heightResult.log); + expect(firstHeightLog.events.length).toEqual(2); + const heightTransaction = heightResult.transaction; + assert(isSendTransaction(heightTransaction), "Expected send transaction"); + expect(heightTransaction).toEqual(unsigned); + + connection.disconnect(); + }); + it("can search by minHeight and maxHeight", async () => { pendingWithoutWasmd(); const connection = await CosmWasmConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); @@ -980,47 +1098,4 @@ describe("CosmWasmConnection", () => { })().catch(done.fail); }); }); - - describe("integration tests", () => { - it("can send ERC20 tokens", async () => { - pendingWithoutWasmd(); - const connection = await CosmWasmConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); - const profile = new UserProfile(); - const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic)); - const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path); - const senderAddress = connection.codec.identityToAddress(sender); - const recipient = makeRandomAddress(); - - const unsigned = await connection.withDefaultFee({ - kind: "bcp/send", - chainId: defaultChainId, - sender: senderAddress, - recipient: recipient, - memo: "My first payment", - amount: { - quantity: "75", - fractionalDigits: 0, - tokenTicker: "ISA" as TokenTicker, - }, - }); - const nonce = await connection.getNonce({ address: senderAddress }); - const signed = await profile.signTransaction(sender, unsigned, connection.codec, nonce); - const postableBytes = connection.codec.bytesToPost(signed); - const response = await connection.postTx(postableBytes); - const blockInfo = await response.blockInfo.waitFor(info => !isBlockInfoPending(info)); - expect(blockInfo.state).toEqual(TransactionState.Succeeded); - - const recipientAccount = await connection.getAccount({ address: recipient }); - assert(recipientAccount, "Recipient account must have ISA tokens"); - expect(recipientAccount.balance).toEqual([ - { - tokenTicker: "ISA" as TokenTicker, - quantity: "75", - fractionalDigits: 0, - }, - ]); - - connection.disconnect(); - }); - }); }); diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index f26fa724..648ea7a0 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -60,6 +60,25 @@ function isDefined(value: X | undefined): value is X { return value !== undefined; } +function deduplicate(input: ReadonlyArray, comparator: (a: T, b: T) => number): Array { + const out = new Array(); + for (const element of input) { + if (!out.find(o => comparator(o, element) === 0)) { + out.push(element); + } + } + return out; +} + +/** Compares transaxtion by height. If the height is equal, compare by hash to ensure deterministic order */ +function compareByHeightAndHash(a: TxsResponse, b: TxsResponse): number { + if (a.height === b.height) { + return a.txhash.localeCompare(b.txhash); + } else { + return parseInt(a.height, 10) - parseInt(b.height, 10); + } +} + /** Account and undefined are valid events. The third option means no event fired yet */ type LastWatchAccountEvent = Account | undefined | "no_event_fired_yet"; @@ -324,7 +343,35 @@ export class CosmWasmConnection implements BlockchainConnection { } else if (height) { txs = await this.cosmWasmClient.searchTx({ height: height }, filter); } else if (sentFromOrTo) { - txs = await this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo }, filter); + const pendingRequests = new Array>(); + pendingRequests.push(this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo }, filter)); + for (const contract of this.erc20Tokens.map(token => token.contractAddress)) { + const searchBySender = [ + { + key: "wasm.contract_address", + value: contract, + }, + { + key: "wasm.sender", + value: sentFromOrTo, + }, + ]; + const searchByRecipient = [ + { + key: "wasm.contract_address", + value: contract, + }, + { + key: "wasm.recipient", + value: sentFromOrTo, + }, + ]; + pendingRequests.push(this.cosmWasmClient.searchTx({ tags: searchBySender }, filter)); + pendingRequests.push(this.cosmWasmClient.searchTx({ tags: searchByRecipient }, filter)); + } + const responses = await Promise.all(pendingRequests); + const allResults = responses.reduce((accumulator, results) => accumulator.concat(results), []); + txs = deduplicate(allResults, (a, b) => a.txhash.localeCompare(b.txhash)).sort(compareByHeightAndHash); } else { throw new Error("Unsupported query"); } diff --git a/packages/sdk/src/cosmwasmclient.searchtx.spec.ts b/packages/sdk/src/cosmwasmclient.searchtx.spec.ts new file mode 100644 index 00000000..5079a6bd --- /dev/null +++ b/packages/sdk/src/cosmwasmclient.searchtx.spec.ts @@ -0,0 +1,361 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { assert, sleep } from "@iov/utils"; + +import { CosmWasmClient } from "./cosmwasmclient"; +import { Secp256k1Pen } from "./pen"; +import { RestClient } from "./restclient"; +import { SigningCosmWasmClient } from "./signingcosmwasmclient"; +import { + deployedErc20, + faucet, + fromOneElementArray, + makeRandomAddress, + pendingWithoutWasmd, + wasmdEnabled, + wasmdEndpoint, +} from "./testutils.spec"; +import { Coin, CosmosSdkTx, isMsgExecuteContract, isMsgInstantiateContract, isMsgSend } from "./types"; + +describe("CosmWasmClient.searchTx", () => { + let postedSend: + | { + readonly sender: string; + readonly recipient: string; + readonly hash: string; + readonly height: number; + 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(wasmdEndpoint, faucet.address, signBytes => + pen.sign(signBytes), + ); + + { + const recipient = makeRandomAddress(); + const transferAmount: Coin = { + denom: "ucosm", + amount: "1234567", + }; + const result = await client.sendTokens(recipient, [transferAmount]); + await sleep(50); // wait until tx is indexed + const txDetails = await new RestClient(wasmdEndpoint).txsById(result.transactionHash); + postedSend = { + 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(wasmdEndpoint).txsById(result.transactionHash); + postedExecute = { + sender: faucet.address, + contract: hashInstance, + hash: result.transactionHash, + height: Number.parseInt(txDetails.height, 10), + tx: txDetails.tx, + }; + } + } + }); + + describe("with SearchByIdQuery", () => { + it("can search by ID", async () => { + pendingWithoutWasmd(); + assert(postedSend, "value must be set in beforeAll()"); + const client = new CosmWasmClient(wasmdEndpoint); + const result = await client.searchTx({ id: postedSend.hash }); + expect(result.length).toEqual(1); + expect(result[0]).toEqual( + jasmine.objectContaining({ + height: postedSend.height.toString(), + txhash: postedSend.hash, + tx: postedSend.tx, + }), + ); + }); + + it("can search by ID (non existent)", async () => { + pendingWithoutWasmd(); + const client = new CosmWasmClient(wasmdEndpoint); + 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 () => { + pendingWithoutWasmd(); + assert(postedSend); + const client = new CosmWasmClient(wasmdEndpoint); + const query = { id: postedSend.hash }; + + { + const result = await client.searchTx(query, { minHeight: 0 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: postedSend.height - 1 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: postedSend.height }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: postedSend.height + 1 }); + expect(result.length).toEqual(0); + } + }); + }); + + describe("with SearchByHeightQuery", () => { + it("can search by height", async () => { + pendingWithoutWasmd(); + assert(postedSend, "value must be set in beforeAll()"); + const client = new CosmWasmClient(wasmdEndpoint); + const result = await client.searchTx({ height: postedSend.height }); + expect(result.length).toEqual(1); + expect(result[0]).toEqual( + jasmine.objectContaining({ + height: postedSend.height.toString(), + txhash: postedSend.hash, + tx: postedSend.tx, + }), + ); + }); + }); + + describe("with SearchBySentFromOrToQuery", () => { + it("can search by sender", async () => { + pendingWithoutWasmd(); + assert(postedSend, "value must be set in beforeAll()"); + const client = new CosmWasmClient(wasmdEndpoint); + const results = await client.searchTx({ sentFromOrTo: postedSend.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 === 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: 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(wasmdEndpoint); + 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 recipient and filter by minHeight", async () => { + pendingWithoutWasmd(); + assert(postedSend); + const client = new CosmWasmClient(wasmdEndpoint); + const query = { sentFromOrTo: postedSend.recipient }; + + { + const result = await client.searchTx(query, { minHeight: 0 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: postedSend.height - 1 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: postedSend.height }); + expect(result.length).toEqual(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(postedSend); + const client = new CosmWasmClient(wasmdEndpoint); + const query = { sentFromOrTo: postedSend.recipient }; + + { + const result = await client.searchTx(query, { maxHeight: 9999999999999 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { maxHeight: postedSend.height + 1 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { maxHeight: postedSend.height }); + expect(result.length).toEqual(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(wasmdEndpoint); + 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(wasmdEndpoint); + 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(wasmdEndpoint); + 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, + }), + ); + }); + }); +}); diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 5f130e65..773816a3 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -1,39 +1,29 @@ /* eslint-disable @typescript-eslint/camelcase */ import { Sha256 } from "@iov/crypto"; import { Bech32, Encoding } from "@iov/encoding"; -import { assert, sleep } from "@iov/utils"; +import { assert } from "@iov/utils"; import { ReadonlyDate } from "readonly-date"; import { Code, CosmWasmClient } from "./cosmwasmclient"; import { makeSignBytes } from "./encoding"; import { findAttribute } from "./logs"; import { Secp256k1Pen } from "./pen"; -import { RestClient } from "./restclient"; import { SigningCosmWasmClient } from "./signingcosmwasmclient"; import cosmoshub from "./testdata/cosmoshub.json"; import { + deployedErc20, + faucet, getRandomizedHackatom, makeRandomAddress, pendingWithoutWasmd, tendermintIdMatcher, wasmdEnabled, + wasmdEndpoint, } from "./testutils.spec"; -import { CosmosSdkTx, MsgSend, StdFee } from "./types"; +import { MsgSend, StdFee } from "./types"; const { fromAscii, fromHex, fromUtf8, toAscii, toBase64 } = Encoding; -const httpUrl = "http://localhost:1317"; - -const faucet = { - mnemonic: - "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone", - pubkey: { - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }, - address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", -}; - const unused = { address: "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u", }; @@ -53,7 +43,7 @@ interface HackatomInstance { describe("CosmWasmClient", () => { describe("makeReadOnly", () => { it("can be constructed", () => { - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); expect(client).toBeTruthy(); }); }); @@ -61,7 +51,7 @@ describe("CosmWasmClient", () => { describe("chainId", () => { it("works", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); expect(await client.chainId()).toEqual("testing"); }); }); @@ -69,7 +59,7 @@ describe("CosmWasmClient", () => { describe("getNonce", () => { it("works", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); expect(await client.getNonce(unused.address)).toEqual({ accountNumber: 5, sequence: 0, @@ -78,7 +68,7 @@ describe("CosmWasmClient", () => { it("throws for missing accounts", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const missing = makeRandomAddress(); await client.getNonce(missing).then( () => fail("this must not succeed"), @@ -90,7 +80,7 @@ describe("CosmWasmClient", () => { describe("getAccount", () => { it("works", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); expect(await client.getAccount(unused.address)).toEqual({ address: unused.address, account_number: 5, @@ -105,7 +95,7 @@ describe("CosmWasmClient", () => { it("returns undefined for missing accounts", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const missing = makeRandomAddress(); expect(await client.getAccount(missing)).toBeUndefined(); }); @@ -114,7 +104,7 @@ describe("CosmWasmClient", () => { describe("getBlock", () => { it("works for latest block", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const response = await client.getBlock(); // id @@ -134,7 +124,7 @@ describe("CosmWasmClient", () => { it("works for block by height", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const height = parseInt((await client.getBlock()).block.header.height, 10); const response = await client.getBlock(height - 1); @@ -157,7 +147,7 @@ describe("CosmWasmClient", () => { describe("getIdentifier", () => { it("works", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); expect(await client.getIdentifier(cosmoshub.tx)).toEqual(cosmoshub.id); }); }); @@ -166,7 +156,7 @@ describe("CosmWasmClient", () => { it("works", async () => { pendingWithoutWasmd(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const memo = "My first contract on chain"; const sendMsg: MsgSend = { @@ -210,205 +200,18 @@ describe("CosmWasmClient", () => { }); }); - describe("searchTx", () => { - let posted: - | { - readonly sender: string; - readonly recipient: 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 = [ - { - 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.tx, - }; - } - }); - - it("can search by ID", async () => { - pendingWithoutWasmd(); - assert(posted, "value must be set in beforeAll()"); - const client = new CosmWasmClient(httpUrl); - const result = await client.searchTx({ id: posted.hash }); - expect(result.length).toEqual(1); - expect(result[0]).toEqual( - jasmine.objectContaining({ - height: posted.height.toString(), - txhash: posted.hash, - tx: posted.tx, - }), - ); - }); - - it("can search by ID (non existent)", async () => { - pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); - const nonExistentId = "0000000000000000000000000000000000000000000000000000000000000000"; - const result = await client.searchTx({ id: nonExistentId }); - expect(result.length).toEqual(0); - }); - - it("can search by height", async () => { - pendingWithoutWasmd(); - assert(posted, "value must be set in beforeAll()"); - const client = new CosmWasmClient(httpUrl); - const result = await client.searchTx({ height: posted.height }); - expect(result.length).toEqual(1); - expect(result[0]).toEqual( - jasmine.objectContaining({ - height: posted.height.toString(), - txhash: posted.hash, - tx: posted.tx, - }), - ); - }); - - it("can search by sender", async () => { - pendingWithoutWasmd(); - assert(posted, "value must be set in beforeAll()"); - const client = new CosmWasmClient(httpUrl); - const result = await client.searchTx({ sentFromOrTo: posted.sender }); - expect(result.length).toBeGreaterThanOrEqual(1); - expect(result[result.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 result = await client.searchTx({ sentFromOrTo: posted.recipient }); - expect(result.length).toBeGreaterThanOrEqual(1); - expect(result[result.length - 1]).toEqual( - jasmine.objectContaining({ - height: posted.height.toString(), - txhash: posted.hash, - tx: posted.tx, - }), - ); - }); - - 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("getCodes", () => { it("works", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const result = await client.getCodes(); expect(result.length).toBeGreaterThanOrEqual(1); const [first] = result; expect(first).toEqual({ - id: 1, - checksum: "aff8c8873d79d2153a8b9066a0683fec3c903669267eb806ffa831dcd4b3daae", - source: undefined, - builder: undefined, + id: deployedErc20.codeId, + source: deployedErc20.source, + builder: deployedErc20.builder, + checksum: deployedErc20.checksum, creator: faucet.address, }); }); @@ -417,14 +220,14 @@ describe("CosmWasmClient", () => { describe("getCodeDetails", () => { it("works", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const result = await client.getCodeDetails(1); const expectedInfo: Code = { - id: 1, - checksum: "aff8c8873d79d2153a8b9066a0683fec3c903669267eb806ffa831dcd4b3daae", - source: undefined, - builder: undefined, + id: deployedErc20.codeId, + source: deployedErc20.source, + builder: deployedErc20.builder, + checksum: deployedErc20.checksum, creator: faucet.address, }; @@ -438,7 +241,7 @@ describe("CosmWasmClient", () => { describe("getContracts", () => { it("works", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const result = await client.getContracts(1); expect(result.length).toBeGreaterThanOrEqual(3); const [hash, isa, jade] = result; @@ -466,7 +269,7 @@ describe("CosmWasmClient", () => { describe("getContract", () => { it("works for HASH instance", async () => { pendingWithoutWasmd(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const hash = await client.getContract("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5"); expect(hash).toEqual({ address: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", @@ -505,7 +308,9 @@ describe("CosmWasmClient", () => { if (wasmdEnabled()) { pendingWithoutWasmd(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); + const client = new SigningCosmWasmClient(wasmdEndpoint, faucet.address, signBytes => + pen.sign(signBytes), + ); const { codeId } = await client.upload(getRandomizedHackatom()); const initMsg = { verifier: makeRandomAddress(), beneficiary: makeRandomAddress() }; const contractAddress = await client.instantiate(codeId, initMsg, "random hackatom"); @@ -517,7 +322,7 @@ describe("CosmWasmClient", () => { pendingWithoutWasmd(); assert(contract); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const raw = await client.queryContractRaw(contract.address, configKey); assert(raw, "must get result"); expect(JSON.parse(fromUtf8(raw))).toEqual({ @@ -531,7 +336,7 @@ describe("CosmWasmClient", () => { pendingWithoutWasmd(); assert(contract); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const raw = await client.queryContractRaw(contract.address, otherKey); expect(raw).toBeNull(); }); @@ -541,7 +346,7 @@ describe("CosmWasmClient", () => { assert(contract); const nonExistentAddress = makeRandomAddress(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); await client.queryContractRaw(nonExistentAddress, configKey).then( () => fail("must not succeed"), error => expect(error).toMatch(`No contract found at address "${nonExistentAddress}"`), @@ -556,7 +361,9 @@ describe("CosmWasmClient", () => { if (wasmdEnabled()) { pendingWithoutWasmd(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); + const client = new SigningCosmWasmClient(wasmdEndpoint, faucet.address, signBytes => + pen.sign(signBytes), + ); const { codeId } = await client.upload(getRandomizedHackatom()); const initMsg = { verifier: makeRandomAddress(), beneficiary: makeRandomAddress() }; const contractAddress = await client.instantiate(codeId, initMsg, "a different hackatom"); @@ -568,7 +375,7 @@ describe("CosmWasmClient", () => { pendingWithoutWasmd(); assert(contract); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); const verifier = await client.queryContractSmart(contract.address, { verifier: {} }); expect(fromAscii(verifier)).toEqual(contract.initMsg.verifier); }); @@ -577,7 +384,7 @@ describe("CosmWasmClient", () => { pendingWithoutWasmd(); assert(contract); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); await client.queryContractSmart(contract.address, { broken: {} }).then( () => fail("must not succeed"), error => expect(error).toMatch(/Error parsing QueryMsg/i), @@ -588,7 +395,7 @@ describe("CosmWasmClient", () => { pendingWithoutWasmd(); const nonExistentAddress = makeRandomAddress(); - const client = new CosmWasmClient(httpUrl); + const client = new CosmWasmClient(wasmdEndpoint); await client.queryContractSmart(nonExistentAddress, { verifier: {} }).then( () => fail("must not succeed"), error => expect(error).toMatch(`No contract found at address "${nonExistentAddress}"`), diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index 0ea8ef77..e07ac6dc 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -13,7 +13,7 @@ export interface GetNonceResult { export interface PostTxResult { readonly logs: readonly Log[]; readonly rawLog: string; - /** Transaction hash (might be used as transaction ID). Guaranteed to be non-exmpty upper-case hex */ + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ readonly transactionHash: string; } @@ -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; @@ -159,11 +175,16 @@ export class CosmWasmClient { } } else if (isSearchBySentFromOrToQuery(query)) { // We cannot get both in one request (see https://github.com/cosmos/gaia/issues/75) - const sent = await this.txsQuery(withFilters(`message.sender=${query.sentFromOrTo}`)); - const received = await this.txsQuery(withFilters(`transfer.recipient=${query.sentFromOrTo}`)); + const sentQuery = withFilters(`message.module=bank&message.sender=${query.sentFromOrTo}`); + const receivedQuery = withFilters(`message.module=bank&transfer.recipient=${query.sentFromOrTo}`); + const sent = await this.txsQuery(sentQuery); + const received = await this.txsQuery(receivedQuery); 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/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 82fa3d66..28e95ce1 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -13,6 +13,9 @@ import { SigningCosmWasmClient } from "./signingcosmwasmclient"; import cosmoshub from "./testdata/cosmoshub.json"; import { bech32AddressMatcher, + deployedErc20, + faucet, + fromOneElementArray, getRandomizedHackatom, makeRandomAddress, pendingWithoutWasmd, @@ -20,9 +23,12 @@ import { tendermintIdMatcher, tendermintOptionalIdMatcher, wasmdEnabled, + wasmdEndpoint, } from "./testutils.spec"; import { Coin, + isMsgInstantiateContract, + isMsgStoreCode, Msg, MsgExecuteContract, MsgInstantiateContract, @@ -35,17 +41,7 @@ import { const { fromAscii, fromBase64, fromHex, toAscii, toBase64, toHex } = Encoding; -const httpUrl = "http://localhost:1317"; const defaultNetworkId = "testing"; -const faucet = { - mnemonic: - "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone", - pubkey: { - type: "tendermint/PubKeySecp256k1", - value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", - }, - address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", -}; const emptyAddress = "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k"; const unusedAccount = { address: "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u", @@ -168,7 +164,7 @@ async function executeContract( describe("RestClient", () => { it("can be constructed", () => { - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); expect(client).toBeTruthy(); }); @@ -177,7 +173,7 @@ describe("RestClient", () => { describe("authAccounts", () => { it("works for unused account without pubkey", async () => { pendingWithoutWasmd(); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const { result } = await client.authAccounts(unusedAccount.address); expect(result).toEqual({ type: "cosmos-sdk/Account", @@ -203,7 +199,7 @@ describe("RestClient", () => { // 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 client = new RestClient(wasmdEndpoint); const { result } = await client.authAccounts(faucet.address); expect(result.value).toEqual( jasmine.objectContaining({ @@ -215,7 +211,7 @@ describe("RestClient", () => { // This property is used by CosmWasmClient.getAccount it("returns empty address for non-existent account", async () => { pendingWithoutWasmd(); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const nonExistentAccount = makeRandomAddress(); const { result } = await client.authAccounts(nonExistentAccount); expect(result).toEqual({ @@ -230,7 +226,7 @@ describe("RestClient", () => { describe("blocksLatest", () => { it("works", async () => { pendingWithoutWasmd(); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const response = await client.blocksLatest(); // id @@ -263,7 +259,7 @@ describe("RestClient", () => { describe("blocks", () => { it("works for block by height", async () => { pendingWithoutWasmd(); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const height = parseInt((await client.blocksLatest()).block.header.height, 10); const response = await client.blocks(height - 1); @@ -299,7 +295,7 @@ describe("RestClient", () => { describe("nodeInfo", () => { it("works", async () => { pendingWithoutWasmd(); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const info = await client.nodeInfo(); expect(info.node_info.network).toEqual(defaultNetworkId); }); @@ -321,7 +317,9 @@ describe("RestClient", () => { beforeAll(async () => { if (wasmdEnabled()) { const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); + const client = new SigningCosmWasmClient(wasmdEndpoint, faucet.address, signBytes => + pen.sign(signBytes), + ); const recipient = makeRandomAddress(); const transferAmount = [ @@ -333,7 +331,7 @@ describe("RestClient", () => { const result = await client.sendTokens(recipient, transferAmount); await sleep(50); // wait until tx is indexed - const txDetails = await new RestClient(httpUrl).txsById(result.transactionHash); + const txDetails = await new RestClient(wasmdEndpoint).txsById(result.transactionHash); posted = { sender: faucet.address, recipient: recipient, @@ -347,7 +345,7 @@ describe("RestClient", () => { it("can query transactions by height", async () => { pendingWithoutWasmd(); assert(posted); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); 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); @@ -360,7 +358,7 @@ describe("RestClient", () => { it("can query transactions by ID", async () => { pendingWithoutWasmd(); assert(posted); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); 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); @@ -373,7 +371,7 @@ describe("RestClient", () => { it("can query transactions by sender", async () => { pendingWithoutWasmd(); assert(posted); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); 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); @@ -387,7 +385,7 @@ describe("RestClient", () => { it("can query transactions by recipient", async () => { pendingWithoutWasmd(); assert(posted); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); 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); @@ -402,7 +400,7 @@ describe("RestClient", () => { pending("This combination is broken 🤷‍♂️. Handle client-side at higher level."); pendingWithoutWasmd(); assert(posted); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const hashQuery = `tx.hash=${posted.hash}`; { @@ -429,7 +427,7 @@ describe("RestClient", () => { it("can filter by recipient and tx.minheight", async () => { pendingWithoutWasmd(); assert(posted); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const recipientQuery = `transfer.recipient=${posted.recipient}`; { @@ -456,7 +454,7 @@ describe("RestClient", () => { it("can filter by recipient and tx.maxheight", async () => { pendingWithoutWasmd(); assert(posted); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const recipientQuery = `transfer.recipient=${posted.recipient}`; { @@ -479,12 +477,114 @@ describe("RestClient", () => { expect(count).toEqual("0"); } }); + + it("can query by tags (module + code_id)", async () => { + pendingWithoutWasmd(); + assert(posted); + const client = new RestClient(wasmdEndpoint); + const result = await client.txsQuery(`message.module=wasm&message.code_id=${deployedErc20.codeId}`); + expect(parseInt(result.count, 10)).toBeGreaterThanOrEqual(4); + + // Check first 4 results + const [store, hash, isa, jade] = result.txs.map(tx => fromOneElementArray(tx.tx.value.msg)); + assert(isMsgStoreCode(store)); + assert(isMsgInstantiateContract(hash)); + assert(isMsgInstantiateContract(isa)); + assert(isMsgInstantiateContract(jade)); + expect(store.value).toEqual( + jasmine.objectContaining({ + sender: faucet.address, + source: deployedErc20.source, + builder: deployedErc20.builder, + }), + ); + expect(hash.value).toEqual({ + code_id: deployedErc20.codeId.toString(), + init_funds: [], + init_msg: jasmine.objectContaining({ + symbol: "HASH", + }), + label: "HASH", + sender: faucet.address, + }); + expect(isa.value).toEqual({ + code_id: deployedErc20.codeId.toString(), + init_funds: [], + init_msg: jasmine.objectContaining({ symbol: "ISA" }), + label: "ISA", + sender: faucet.address, + }); + expect(jade.value).toEqual({ + code_id: deployedErc20.codeId.toString(), + init_funds: [], + init_msg: jasmine.objectContaining({ symbol: "JADE" }), + label: "JADE", + sender: faucet.address, + }); + }); + + // Like previous test but filtered by message.action=store-code and message.action=instantiate + it("can query by tags (module + code_id + action)", async () => { + pendingWithoutWasmd(); + assert(posted); + const client = new RestClient(wasmdEndpoint); + + { + const uploads = await client.txsQuery( + `message.module=wasm&message.code_id=${deployedErc20.codeId}&message.action=store-code`, + ); + expect(parseInt(uploads.count, 10)).toEqual(1); + const store = fromOneElementArray(uploads.txs[0].tx.value.msg); + assert(isMsgStoreCode(store)); + expect(store.value).toEqual( + jasmine.objectContaining({ + sender: faucet.address, + source: deployedErc20.source, + builder: deployedErc20.builder, + }), + ); + } + + { + const instantiations = await client.txsQuery( + `message.module=wasm&message.code_id=${deployedErc20.codeId}&message.action=instantiate`, + ); + expect(parseInt(instantiations.count, 10)).toBeGreaterThanOrEqual(3); + const [hash, isa, jade] = instantiations.txs.map(tx => fromOneElementArray(tx.tx.value.msg)); + assert(isMsgInstantiateContract(hash)); + assert(isMsgInstantiateContract(isa)); + assert(isMsgInstantiateContract(jade)); + expect(hash.value).toEqual({ + code_id: deployedErc20.codeId.toString(), + init_funds: [], + init_msg: jasmine.objectContaining({ + symbol: "HASH", + }), + label: "HASH", + sender: faucet.address, + }); + expect(isa.value).toEqual({ + code_id: deployedErc20.codeId.toString(), + init_funds: [], + init_msg: jasmine.objectContaining({ symbol: "ISA" }), + label: "ISA", + sender: faucet.address, + }); + expect(jade.value).toEqual({ + code_id: deployedErc20.codeId.toString(), + init_funds: [], + init_msg: jasmine.objectContaining({ symbol: "JADE" }), + label: "JADE", + sender: faucet.address, + }); + } + }); }); describe("encodeTx", () => { it("works for cosmoshub example", async () => { pendingWithoutWasmd(); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); expect(await client.encodeTx(cosmoshub.tx)).toEqual(fromBase64(cosmoshub.tx_data)); }); }); @@ -519,7 +619,7 @@ describe("RestClient", () => { gas: "890000", }; - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value; const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); @@ -533,7 +633,7 @@ describe("RestClient", () => { it("can upload, instantiate and execute wasm", async () => { pendingWithoutWasmd(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const transferAmount: readonly Coin[] = [ { @@ -607,7 +707,7 @@ describe("RestClient", () => { it("can list upload code", async () => { pendingWithoutWasmd(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); // check with contracts were here first to compare const existingInfos = await client.listCodeInfo(); @@ -647,7 +747,7 @@ describe("RestClient", () => { it("can list contracts and get info", async () => { pendingWithoutWasmd(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const beneficiaryAddress = makeRandomAddress(); const transferAmount: readonly Coin[] = [ { @@ -708,7 +808,7 @@ describe("RestClient", () => { }); describe("contract state", () => { - const client = new RestClient(httpUrl); + const client = new RestClient(wasmdEndpoint); const noContract = makeRandomAddress(); const expectedKey = toAscii("config"); let contractAddress: string | undefined; diff --git a/packages/sdk/src/signingcosmwasmclient.ts b/packages/sdk/src/signingcosmwasmclient.ts index aa89a894..8a2616a5 100644 --- a/packages/sdk/src/signingcosmwasmclient.ts +++ b/packages/sdk/src/signingcosmwasmclient.ts @@ -83,6 +83,8 @@ export interface UploadReceipt { export interface ExecuteResult { readonly logs: readonly Log[]; + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ + readonly transactionHash: string; } export class SigningCosmWasmClient extends CosmWasmClient { @@ -220,6 +222,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { const result = await this.postTx(signedTx); return { logs: result.logs, + transactionHash: result.transactionHash, }; } diff --git a/packages/sdk/src/testutils.spec.ts b/packages/sdk/src/testutils.spec.ts index f72d4636..5eba32fa 100644 --- a/packages/sdk/src/testutils.spec.ts +++ b/packages/sdk/src/testutils.spec.ts @@ -63,6 +63,31 @@ export const tendermintAddressMatcher = /^[0-9A-F]{40}$/; // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 export const bech32AddressMatcher = /^[\x21-\x7e]{1,83}1[02-9ac-hj-np-z]{38}$/; +/** Deployed as part of scripts/wasmd/init.sh */ +export const deployedErc20 = { + codeId: 1, + 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 const wasmdEndpoint = "http://localhost:1317"; + +export const faucet = { + mnemonic: + "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone", + pubkey: { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }, + address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", +}; + export function wasmdEnabled(): boolean { return !!process.env.WASMD_ENABLED; } @@ -73,6 +98,12 @@ export function pendingWithoutWasmd(): void { } } +/** Returns first element. Throws if array has a different length than 1. */ +export function fromOneElementArray(elements: ArrayLike): T { + if (elements.length !== 1) throw new Error(`Expected exactly one element but got ${elements.length}`); + return elements[0]; +} + describe("leb128", () => { describe("leb128Encode", () => { it("works for single byte values", () => { diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts index d001e6df..f544e060 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -8,7 +8,7 @@ export interface GetNonceResult { export interface PostTxResult { readonly logs: readonly Log[]; readonly rawLog: string; - /** Transaction hash (might be used as transaction ID). Guaranteed to be non-exmpty upper-case hex */ + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ readonly transactionHash: string; } export interface SearchByIdQuery { @@ -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; diff --git a/packages/sdk/types/signingcosmwasmclient.d.ts b/packages/sdk/types/signingcosmwasmclient.d.ts index 9510b2fa..318a387a 100644 --- a/packages/sdk/types/signingcosmwasmclient.d.ts +++ b/packages/sdk/types/signingcosmwasmclient.d.ts @@ -31,6 +31,8 @@ export interface UploadReceipt { } export interface ExecuteResult { readonly logs: readonly Log[]; + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ + readonly transactionHash: string; } export declare class SigningCosmWasmClient extends CosmWasmClient { readonly senderAddress: string; diff --git a/scripts/wasmd/deploy_erc20.js b/scripts/wasmd/deploy_erc20.js index 4401d53d..63af5032 100755 --- a/scripts/wasmd/deploy_erc20.js +++ b/scripts/wasmd/deploy_erc20.js @@ -17,6 +17,11 @@ const guest = { address: "cosmos17d0jcz59jf68g52vq38tuuncmwwjk42u6mcxej", }; +const codeMeta = { + source: "https://crates.io/api/v1/crates/cw-erc20/0.2.0/download", + builder: "confio/cosmwasm-opt:0.7.0", +}; + const initMsgHash = { decimals: 5, name: "Hash token", @@ -72,7 +77,7 @@ async function main() { const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); const wasm = fs.readFileSync(__dirname + "/contracts/cw-erc20.wasm"); - const uploadReceipt = await client.upload(wasm, {}, "Upload ERC20 contract"); + const uploadReceipt = await client.upload(wasm, codeMeta, "Upload ERC20 contract"); console.info(`Upload succeeded. Receipt: ${JSON.stringify(uploadReceipt)}`); for (const initMsg of [initMsgHash, initMsgIsa, initMsgJade]) {