diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index 1a4ca439..f26fa724 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { CosmWasmClient, findSequenceForSignedTx, TxsResponse, types } from "@cosmwasm/sdk"; +import { CosmWasmClient, findSequenceForSignedTx, SearchTxFilter, TxsResponse, types } from "@cosmwasm/sdk"; import { Account, AccountQuery, @@ -257,7 +257,9 @@ export class CosmWasmConnection implements BlockchainConnection { } public async postTx(tx: PostableBytes): Promise { - const { transactionHash, rawLog } = await this.cosmWasmClient.postTx(tx); + const txAsJson = JSON.parse(Encoding.fromUtf8(tx)); + if (!types.isStdTx(txAsJson)) throw new Error("Postable bytes must contain a JSON encoded StdTx"); + const { transactionHash, rawLog } = await this.cosmWasmClient.postTx(txAsJson); const transactionId = transactionHash as TransactionId; const firstEvent: BlockInfo = { state: TransactionState.Pending }; let blockInfoInterval: NodeJS.Timeout; @@ -298,8 +300,8 @@ export class CosmWasmConnection implements BlockchainConnection { public async searchTx({ height, id, - maxHeight: maxHeightOptional, - minHeight: minHeightOptional, + maxHeight, + minHeight, sentFromOrTo, signedBy, tags, @@ -314,32 +316,20 @@ export class CosmWasmConnection implements BlockchainConnection { ); } - const minHeight = minHeightOptional || 0; - const maxHeight = maxHeightOptional || Number.MAX_SAFE_INTEGER; - - if (maxHeight < minHeight) return []; // optional optimization + const filter: SearchTxFilter = { minHeight: minHeight, maxHeight: maxHeight }; let txs: readonly TxsResponse[]; if (id) { - txs = await this.cosmWasmClient.searchTx({ id: id }); + txs = await this.cosmWasmClient.searchTx({ id: id }, filter); } else if (height) { - if (height < minHeight) return []; // optional optimization - if (height > maxHeight) return []; // optional optimization - txs = await this.cosmWasmClient.searchTx({ height: height }); + txs = await this.cosmWasmClient.searchTx({ height: height }, filter); } else if (sentFromOrTo) { - // TODO: pass minHeight/maxHeight to server once we have - // https://github.com/cosmwasm/wasmd/issues/73 - txs = await this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo }); + txs = await this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo }, filter); } else { throw new Error("Unsupported query"); } - const filtered = txs.filter(tx => { - const txHeight = parseInt(tx.height, 10); - return txHeight >= minHeight && txHeight <= maxHeight; - }); - - return filtered.map(tx => this.parseAndPopulateTxResponseUnsigned(tx)); + return txs.map(tx => this.parseAndPopulateTxResponseUnsigned(tx)); } public listenTx( diff --git a/packages/bcp/types/cosmwasmconnection.d.ts b/packages/bcp/types/cosmwasmconnection.d.ts index b1161cf6..b4059376 100644 --- a/packages/bcp/types/cosmwasmconnection.d.ts +++ b/packages/bcp/types/cosmwasmconnection.d.ts @@ -73,8 +73,8 @@ export declare class CosmWasmConnection implements BlockchainConnection { searchTx({ height, id, - maxHeight: maxHeightOptional, - minHeight: minHeightOptional, + maxHeight, + minHeight, sentFromOrTo, signedBy, tags, diff --git a/packages/cli/README.md b/packages/cli/README.md index 6a271994..2277eaa1 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -72,7 +72,7 @@ const signedTx: types.StdTx = { memo: memo, signatures: [signature], } -const postResult = await client.postTx(marshalTx(signedTx)); +const postResult = await client.postTx(signedTx); ``` ## Extended helpers diff --git a/packages/cli/examples/helpers.ts b/packages/cli/examples/helpers.ts index 5e26fc4d..6b414455 100644 --- a/packages/cli/examples/helpers.ts +++ b/packages/cli/examples/helpers.ts @@ -47,7 +47,7 @@ const instantiateContract = async (initClient: RestClient, initPen: Secp256k1Pen memo: memo, signatures: [signature], }; - const result = await initClient.postTx(marshalTx(signedTx)); + const result = await initClient.postTx(signedTx); if (result.code) { throw new Error(`Failed tx: (${result.code}): ${result.raw_log}`) } @@ -78,7 +78,7 @@ const executeContract = async (execClient: RestClient, execPen: Secp256k1Pen, co memo: memo, signatures: [signature], }; - const result = await execClient.postTx(marshalTx(signedTx)); + const result = await execClient.postTx(signedTx); if (result.code) { throw new Error(`Failed tx: (${result.code}): ${result.raw_log}`) } diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 0f68f264..34e0143f 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -4,7 +4,7 @@ import { assert, sleep } from "@iov/utils"; import { ReadonlyDate } from "readonly-date"; import { CosmWasmClient } from "./cosmwasmclient"; -import { makeSignBytes, marshalTx } from "./encoding"; +import { makeSignBytes } from "./encoding"; import { findAttribute } from "./logs"; import { Secp256k1Pen } from "./pen"; import { RestClient } from "./restclient"; @@ -198,7 +198,7 @@ describe("CosmWasmClient", () => { memo: memo, signatures: [signature], }; - const { logs, transactionHash } = await client.postTx(marshalTx(signedTx)); + const { logs, transactionHash } = await client.postTx(signedTx); const amountAttr = findAttribute(logs, "transfer", "amount"); expect(amountAttr.value).toEqual("1234567ucosm"); expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); @@ -309,6 +309,87 @@ describe("CosmWasmClient", () => { }), ); }); + + it("can search by ID and filter by minHeight", async () => { + pendingWithoutWasmd(); + assert(posted); + const client = new CosmWasmClient(httpUrl); + const query = { id: posted.hash }; + + { + const result = await client.searchTx(query, { minHeight: 0 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: posted.height - 1 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: posted.height }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: posted.height + 1 }); + expect(result.length).toEqual(0); + } + }); + + it("can search by recipient and filter by minHeight", async () => { + pendingWithoutWasmd(); + assert(posted); + const client = new CosmWasmClient(httpUrl); + const query = { sentFromOrTo: posted.recipient }; + + { + const result = await client.searchTx(query, { minHeight: 0 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: posted.height - 1 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: posted.height }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: posted.height + 1 }); + expect(result.length).toEqual(0); + } + }); + + it("can search by recipient and filter by maxHeight", async () => { + pendingWithoutWasmd(); + assert(posted); + const client = new CosmWasmClient(httpUrl); + const query = { sentFromOrTo: posted.recipient }; + + { + const result = await client.searchTx(query, { maxHeight: 9999999999999 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { maxHeight: posted.height + 1 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { maxHeight: posted.height }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { maxHeight: posted.height - 1 }); + expect(result.length).toEqual(0); + } + }); }); describe("queryContractRaw", () => { diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/sdk/src/cosmwasmclient.ts index 97de555a..313bbbb2 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -3,7 +3,7 @@ import { Encoding } from "@iov/encoding"; import { Log, parseLogs } from "./logs"; import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient"; -import { CosmosSdkAccount, CosmosSdkTx } from "./types"; +import { CosmosSdkAccount, CosmosSdkTx, StdTx } from "./types"; export interface GetNonceResult { readonly accountNumber: number; @@ -43,6 +43,11 @@ function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySen return (query as SearchBySentFromOrToQuery).sentFromOrTo !== undefined; } +export interface SearchTxFilter { + readonly minHeight?: number; + readonly maxHeight?: number; +} + export class CosmWasmClient { protected readonly restClient: RestClient; @@ -104,28 +109,47 @@ export class CosmWasmClient { } } - public async searchTx(query: SearchTxQuery): Promise { - // TODO: we need proper pagination support - function limited(originalQuery: string): string { - return `${originalQuery}&limit=75`; + public async searchTx(query: SearchTxQuery, filter: SearchTxFilter = {}): Promise { + const minHeight = filter.minHeight || 0; + const maxHeight = filter.maxHeight || Number.MAX_SAFE_INTEGER; + + if (maxHeight < minHeight) return []; // optional optimization + + function withFilters(originalQuery: string): string { + return `${originalQuery}&tx.minheight=${minHeight}&tx.maxheight=${maxHeight}`; } + let txs: readonly TxsResponse[]; if (isSearchByIdQuery(query)) { - return (await this.restClient.txs(`tx.hash=${query.id}`)).txs; + txs = await this.txsQuery(`tx.hash=${query.id}`); } else if (isSearchByHeightQuery(query)) { - return (await this.restClient.txs(`tx.height=${query.height}`)).txs; + // optional optimization to avoid network request + if (query.height < minHeight || query.height > maxHeight) { + txs = []; + } else { + txs = await this.txsQuery(`tx.height=${query.height}`); + } } else if (isSearchBySentFromOrToQuery(query)) { // We cannot get both in one request (see https://github.com/cosmos/gaia/issues/75) - const sent = (await this.restClient.txs(limited(`message.sender=${query.sentFromOrTo}`))).txs; - const received = (await this.restClient.txs(limited(`transfer.recipient=${query.sentFromOrTo}`))).txs; + const sent = await this.txsQuery(withFilters(`message.sender=${query.sentFromOrTo}`)); + const received = await this.txsQuery(withFilters(`transfer.recipient=${query.sentFromOrTo}`)); + const sentHashes = sent.map(t => t.txhash); - return [...sent, ...received.filter(t => !sentHashes.includes(t.txhash))]; + txs = [...sent, ...received.filter(t => !sentHashes.includes(t.txhash))]; } else { throw new Error("Unknown query type"); } + + // backend sometimes messes up with min/max height filtering + const filtered = txs.filter(tx => { + const txHeight = parseInt(tx.height, 10); + return txHeight >= minHeight && txHeight <= maxHeight; + }); + + return filtered; } - public async postTx(tx: Uint8Array): Promise { + public async postTx(tx: StdTx): Promise { const result = await this.restClient.postTx(tx); if (result.code) { throw new Error(`Error when posting tx. Code: ${result.code}; Raw log: ${result.raw_log}`); @@ -176,4 +200,17 @@ export class CosmWasmClient { } } } + + private async txsQuery(query: string): Promise { + // TODO: we need proper pagination support + const limit = 100; + const result = await this.restClient.txsQuery(`${query}&limit=${limit}`); + const pages = parseInt(result.page_total, 10); + if (pages > 1) { + throw new Error( + `Found more results on the backend than we can process currently. Results: ${result.total_count}, supported: ${limit}`, + ); + } + return result.txs; + } } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index e24b852b..4611ed78 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -14,6 +14,7 @@ export { SearchByIdQuery, SearchBySentFromOrToQuery, SearchTxQuery, + SearchTxFilter, } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index d66c34de..27171dbf 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -1,14 +1,15 @@ /* eslint-disable @typescript-eslint/camelcase */ import { Sha256 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; -import { assert } from "@iov/utils"; +import { assert, sleep } from "@iov/utils"; import { ReadonlyDate } from "readonly-date"; -import { makeSignBytes, marshalTx } from "./encoding"; +import { makeSignBytes } from "./encoding"; import { findAttribute, parseLogs } from "./logs"; import { Pen, Secp256k1Pen } from "./pen"; import { encodeBech32Pubkey } from "./pubkey"; -import { PostTxsResponse, RestClient } from "./restclient"; +import { PostTxsResponse, RestClient, TxsResponse } from "./restclient"; +import { SigningCosmWasmClient } from "./signingcosmwasmclient"; import cosmoshub from "./testdata/cosmoshub.json"; import { getRandomizedHackatom, @@ -87,7 +88,7 @@ async function uploadCustomContract( const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); const signature = await pen.sign(signBytes); const signedTx = makeSignedTx(theMsg, fee, memo, signature); - return client.postTx(marshalTx(signedTx)); + return client.postTx(signedTx); } async function uploadContract(client: RestClient, pen: Pen): Promise { @@ -128,7 +129,7 @@ async function instantiateContract( const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); const signature = await pen.sign(signBytes); const signedTx = makeSignedTx(theMsg, fee, memo, signature); - return client.postTx(marshalTx(signedTx)); + return client.postTx(signedTx); } async function executeContract( @@ -160,7 +161,7 @@ async function executeContract( const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); const signature = await pen.sign(signBytes); const signedTx = makeSignedTx(theMsg, fee, memo, signature); - return client.postTx(marshalTx(signedTx)); + return client.postTx(signedTx); } describe("RestClient", () => { @@ -169,15 +170,61 @@ describe("RestClient", () => { expect(client).toBeTruthy(); }); - describe("nodeInfo", () => { - it("works", async () => { + // The /auth endpoints + + describe("authAccounts", () => { + it("works for unused account without pubkey", async () => { pendingWithoutWasmd(); const client = new RestClient(httpUrl); - const info = await client.nodeInfo(); - expect(info.node_info.network).toEqual(defaultNetworkId); + const { result } = await client.authAccounts(unusedAccount.address); + expect(result).toEqual({ + type: "cosmos-sdk/Account", + value: { + address: unusedAccount.address, + public_key: "", // not known to the chain + coins: [ + { + amount: "1000000000", + denom: "ucosm", + }, + { + amount: "1000000000", + denom: "ustake", + }, + ], + account_number: 5, + sequence: 0, + }, + }); + }); + + // This fails in the first test run if you forget to run `./scripts/wasmd/init.sh` + it("has correct pubkey for faucet", async () => { + pendingWithoutWasmd(); + const client = new RestClient(httpUrl); + const { result } = await client.authAccounts(faucet.address); + expect(result.value).toEqual( + jasmine.objectContaining({ + public_key: encodeBech32Pubkey(faucet.pubkey, "cosmospub"), + }), + ); + }); + + // This property is used by CosmWasmClient.getAccount + it("returns empty address for non-existent account", async () => { + pendingWithoutWasmd(); + const client = new RestClient(httpUrl); + const nonExistentAccount = makeRandomAddress(); + const { result } = await client.authAccounts(nonExistentAccount); + expect(result).toEqual({ + type: "cosmos-sdk/Account", + value: jasmine.objectContaining({ address: "" }), + }); }); }); + // The /blocks endpoints + describe("blocksLatest", () => { it("works", async () => { pendingWithoutWasmd(); @@ -245,54 +292,190 @@ describe("RestClient", () => { }); }); - describe("authAccounts", () => { - it("works for unused account without pubkey", async () => { + // The /node_info endpoint + + describe("nodeInfo", () => { + it("works", async () => { pendingWithoutWasmd(); const client = new RestClient(httpUrl); - const { result } = await client.authAccounts(unusedAccount.address); - expect(result).toEqual({ - type: "cosmos-sdk/Account", - value: { - address: unusedAccount.address, - public_key: "", // not known to the chain - coins: [ - { - amount: "1000000000", - denom: "ucosm", - }, - { - amount: "1000000000", - denom: "ustake", - }, - ], - account_number: 5, - sequence: 0, - }, - }); + const info = await client.nodeInfo(); + expect(info.node_info.network).toEqual(defaultNetworkId); + }); + }); + + // The /txs endpoints + + describe("txsQuery", () => { + let posted: + | { + readonly sender: string; + readonly recipient: string; + readonly hash: string; + readonly height: number; + readonly tx: TxsResponse; + } + | undefined; + + beforeAll(async () => { + if (wasmdEnabled()) { + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes)); + + const recipient = makeRandomAddress(); + const transferAmount = [ + { + denom: "ucosm", + amount: "1234567", + }, + ]; + const result = await client.sendTokens(recipient, transferAmount); + + await sleep(50); // wait until tx is indexed + const txDetails = await new RestClient(httpUrl).txsById(result.transactionHash); + posted = { + sender: faucet.address, + recipient: recipient, + hash: result.transactionHash, + height: Number.parseInt(txDetails.height, 10), + tx: txDetails, + }; + } }); - // This fails in the first test run if you forget to run `./scripts/wasmd/init.sh` - it("has correct pubkey for faucet", async () => { + it("can query transactions by height", async () => { pendingWithoutWasmd(); + assert(posted); const client = new RestClient(httpUrl); - const { result } = await client.authAccounts(faucet.address); - expect(result.value).toEqual( - jasmine.objectContaining({ - public_key: encodeBech32Pubkey(faucet.pubkey, "cosmospub"), - }), - ); + const result = await client.txsQuery(`tx.height=${posted.height}&limit=26`); + expect(parseInt(result.count, 10)).toEqual(1); + expect(parseInt(result.limit, 10)).toEqual(26); + expect(parseInt(result.page_number, 10)).toEqual(1); + expect(parseInt(result.page_total, 10)).toEqual(1); + expect(parseInt(result.total_count, 10)).toEqual(1); + expect(result.txs).toEqual([posted.tx]); }); - // This property is used by CosmWasmClient.getAccount - it("returns empty address for non-existent account", async () => { + it("can query transactions by ID", async () => { pendingWithoutWasmd(); + assert(posted); const client = new RestClient(httpUrl); - const nonExistentAccount = makeRandomAddress(); - const { result } = await client.authAccounts(nonExistentAccount); - expect(result).toEqual({ - type: "cosmos-sdk/Account", - value: jasmine.objectContaining({ address: "" }), - }); + const result = await client.txsQuery(`tx.hash=${posted.hash}&limit=26`); + expect(parseInt(result.count, 10)).toEqual(1); + expect(parseInt(result.limit, 10)).toEqual(26); + expect(parseInt(result.page_number, 10)).toEqual(1); + expect(parseInt(result.page_total, 10)).toEqual(1); + expect(parseInt(result.total_count, 10)).toEqual(1); + expect(result.txs).toEqual([posted.tx]); + }); + + it("can query transactions by sender", async () => { + pendingWithoutWasmd(); + assert(posted); + const client = new RestClient(httpUrl); + const result = await client.txsQuery(`message.sender=${posted.sender}&limit=200`); + expect(parseInt(result.count, 10)).toBeGreaterThanOrEqual(1); + expect(parseInt(result.limit, 10)).toEqual(200); + expect(parseInt(result.page_number, 10)).toEqual(1); + expect(parseInt(result.page_total, 10)).toEqual(1); + expect(parseInt(result.total_count, 10)).toBeGreaterThanOrEqual(1); + expect(result.txs.length).toBeGreaterThanOrEqual(1); + expect(result.txs[result.txs.length - 1]).toEqual(posted.tx); + }); + + it("can query transactions by recipient", async () => { + pendingWithoutWasmd(); + assert(posted); + const client = new RestClient(httpUrl); + const result = await client.txsQuery(`transfer.recipient=${posted.recipient}&limit=200`); + expect(parseInt(result.count, 10)).toEqual(1); + expect(parseInt(result.limit, 10)).toEqual(200); + expect(parseInt(result.page_number, 10)).toEqual(1); + expect(parseInt(result.page_total, 10)).toEqual(1); + expect(parseInt(result.total_count, 10)).toEqual(1); + expect(result.txs.length).toBeGreaterThanOrEqual(1); + expect(result.txs[result.txs.length - 1]).toEqual(posted.tx); + }); + + it("can filter by tx.hash and tx.minheight", async () => { + pending("This combination is broken 🤷‍♂️. Handle client-side at higher level."); + pendingWithoutWasmd(); + assert(posted); + const client = new RestClient(httpUrl); + const hashQuery = `tx.hash=${posted.hash}`; + + { + const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=0`); + expect(count).toEqual("1"); + } + + { + const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height - 1}`); + expect(count).toEqual("1"); + } + + { + const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height}`); + expect(count).toEqual("1"); + } + + { + const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height + 1}`); + expect(count).toEqual("0"); + } + }); + + it("can filter by recipient and tx.minheight", async () => { + pendingWithoutWasmd(); + assert(posted); + const client = new RestClient(httpUrl); + const recipientQuery = `transfer.recipient=${posted.recipient}`; + + { + const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=0`); + expect(count).toEqual("1"); + } + + { + const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height - 1}`); + expect(count).toEqual("1"); + } + + { + const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height}`); + expect(count).toEqual("1"); + } + + { + const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height + 1}`); + expect(count).toEqual("0"); + } + }); + + it("can filter by recipient and tx.maxheight", async () => { + pendingWithoutWasmd(); + assert(posted); + const client = new RestClient(httpUrl); + const recipientQuery = `transfer.recipient=${posted.recipient}`; + + { + const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=9999999999999`); + expect(count).toEqual("1"); + } + + { + const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height + 1}`); + expect(count).toEqual("1"); + } + + { + const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height}`); + expect(count).toEqual("1"); + } + + { + const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height - 1}`); + expect(count).toEqual("0"); + } }); }); @@ -304,7 +487,7 @@ describe("RestClient", () => { }); }); - describe("post", () => { + describe("postTx", () => { it("can send tokens", async () => { pendingWithoutWasmd(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); @@ -340,7 +523,7 @@ describe("RestClient", () => { const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence); const signature = await pen.sign(signBytes); const signedTx = makeSignedTx(theMsg, fee, memo, signature); - const result = await client.postTx(marshalTx(signedTx)); + const result = await client.postTx(signedTx); // console.log("Raw log:", result.raw_log); expect(result.code).toBeFalsy(); }); @@ -410,6 +593,8 @@ describe("RestClient", () => { }); }); + // The /wasm endpoints + describe("query", () => { it("can list upload code", async () => { pendingWithoutWasmd(); diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index 476dc85a..2387692f 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -6,13 +6,13 @@ import { ContractInfo, CosmosSdkAccount, CosmosSdkTx, - isStdTx, Model, parseWasmData, + StdTx, WasmData, } from "./types"; -const { fromBase64, fromUtf8, toHex, toUtf8 } = Encoding; +const { fromBase64, toHex, toUtf8 } = Encoding; interface NodeInfo { readonly network: string; @@ -235,14 +235,19 @@ export class RestClient { return data; } - public async nodeInfo(): Promise { - const responseData = await this.get("/node_info"); - if (!(responseData as any).node_info) { + // The /auth endpoints + + public async authAccounts(address: string): Promise { + const path = `/auth/accounts/${address}`; + const responseData = await this.get(path); + if ((responseData as any).result.type !== "cosmos-sdk/Account") { throw new Error("Unexpected response data format"); } - return responseData as NodeInfoResponse; + return responseData as AuthAccountsResponse; } + // The /blocks endpoints + public async blocksLatest(): Promise { const responseData = await this.get("/blocks/latest"); if (!(responseData as any).block) { @@ -259,25 +264,19 @@ export class RestClient { return responseData as BlockResponse; } - /** returns the amino-encoding of the transaction performed by the server */ - public async encodeTx(tx: CosmosSdkTx): Promise { - const responseData = await this.post("/txs/encode", tx); - if (!(responseData as any).tx) { + // The /node_info endpoint + + public async nodeInfo(): Promise { + const responseData = await this.get("/node_info"); + if (!(responseData as any).node_info) { throw new Error("Unexpected response data format"); } - return Encoding.fromBase64((responseData as EncodeTxResponse).tx); + return responseData as NodeInfoResponse; } - public async authAccounts(address: string): Promise { - const path = `/auth/accounts/${address}`; - const responseData = await this.get(path); - if ((responseData as any).result.type !== "cosmos-sdk/Account") { - throw new Error("Unexpected response data format"); - } - return responseData as AuthAccountsResponse; - } + // The /txs endpoints - public async txs(query: string): Promise { + public async txsQuery(query: string): Promise { const responseData = await this.get(`/txs?${query}`); if (!(responseData as any).txs) { throw new Error("Unexpected response data format"); @@ -293,15 +292,25 @@ export class RestClient { return responseData as TxsResponse; } - // tx must be JSON encoded StdTx (no wrapper) - public async postTx(tx: Uint8Array): Promise { - // TODO: check this is StdTx - const decoded = JSON.parse(fromUtf8(tx)); - if (!isStdTx(decoded)) { - throw new Error("Must be json encoded StdTx"); + /** returns the amino-encoding of the transaction performed by the server */ + public async encodeTx(tx: CosmosSdkTx): Promise { + const responseData = await this.post("/txs/encode", tx); + if (!(responseData as any).tx) { + throw new Error("Unexpected response data format"); } + return Encoding.fromBase64((responseData as EncodeTxResponse).tx); + } + + /** + * Broadcasts a signed transaction to into the transaction pool. + * Depending on the RestClient's broadcast mode, this might or might + * wait for checkTx or deliverTx to be executed before returning. + * + * @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container) + */ + public async postTx(tx: StdTx): Promise { const params = { - tx: decoded, + tx: tx, mode: this.mode, }; const responseData = await this.post("/txs", params); @@ -311,6 +320,8 @@ export class RestClient { return responseData as PostTxsResponse; } + // The /wasm endpoints + // wasm rest queries are listed here: https://github.com/cosmwasm/wasmd/blob/master/x/wasm/client/rest/query.go#L19-L27 public async listCodeInfo(): Promise { const path = `/wasm/code`; diff --git a/packages/sdk/src/signingcosmwasmclient.ts b/packages/sdk/src/signingcosmwasmclient.ts index c8a6999e..285487e7 100644 --- a/packages/sdk/src/signingcosmwasmclient.ts +++ b/packages/sdk/src/signingcosmwasmclient.ts @@ -3,7 +3,7 @@ import { Encoding } from "@iov/encoding"; import pako from "pako"; import { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient"; -import { makeSignBytes, marshalTx } from "./encoding"; +import { makeSignBytes } from "./encoding"; import { findAttribute, Log } from "./logs"; import { BroadcastMode } from "./restclient"; import { @@ -120,7 +120,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { signatures: [signature], }; - const result = await this.postTx(marshalTx(signedTx)); + const result = await this.postTx(signedTx); const codeIdAttr = findAttribute(result.logs, "message", "code_id"); return { originalSize: wasmCode.length, @@ -162,7 +162,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { signatures: [signature], }; - const result = await this.postTx(marshalTx(signedTx)); + const result = await this.postTx(signedTx); const contractAddressAttr = findAttribute(result.logs, "message", "contract_address"); return contractAddressAttr.value; } @@ -195,7 +195,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { signatures: [signature], }; - const result = await this.postTx(marshalTx(signedTx)); + const result = await this.postTx(signedTx); return { logs: result.logs, }; @@ -228,6 +228,6 @@ export class SigningCosmWasmClient extends CosmWasmClient { signatures: [signature], }; - return this.postTx(marshalTx(signedTx)); + return this.postTx(signedTx); } } diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts index b7d0f3bf..5cdc1c6f 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -1,6 +1,6 @@ import { Log } from "./logs"; import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient"; -import { CosmosSdkAccount, CosmosSdkTx } from "./types"; +import { CosmosSdkAccount, CosmosSdkTx, StdTx } from "./types"; export interface GetNonceResult { readonly accountNumber: number; readonly sequence: number; @@ -21,6 +21,10 @@ export interface SearchBySentFromOrToQuery { readonly sentFromOrTo: string; } export declare type SearchTxQuery = SearchByIdQuery | SearchByHeightQuery | SearchBySentFromOrToQuery; +export interface SearchTxFilter { + readonly minHeight?: number; + readonly maxHeight?: number; +} export declare class CosmWasmClient { protected readonly restClient: RestClient; constructor(url: string, broadcastMode?: BroadcastMode); @@ -44,8 +48,8 @@ export declare class CosmWasmClient { * @param height The height of the block. If undefined, the latest height is used. */ getBlock(height?: number): Promise; - searchTx(query: SearchTxQuery): Promise; - postTx(tx: Uint8Array): Promise; + searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise; + postTx(tx: StdTx): Promise; /** * Returns the data at the key if present (raw contract dependent storage data) * or null if no data at this key. @@ -60,4 +64,5 @@ export declare class CosmWasmClient { * Promise is rejected for invalid query format. */ queryContractSmart(address: string, queryMsg: object): Promise; + private txsQuery; } diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index ddc5eb94..b74b8428 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -13,6 +13,7 @@ export { SearchByIdQuery, SearchBySentFromOrToQuery, SearchTxQuery, + SearchTxFilter, } from "./cosmwasmclient"; export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; export { diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index d629c7bc..e5f96309 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -1,4 +1,4 @@ -import { CodeInfo, ContractInfo, CosmosSdkAccount, CosmosSdkTx, Model } from "./types"; +import { CodeInfo, ContractInfo, CosmosSdkAccount, CosmosSdkTx, Model, StdTx } from "./types"; interface NodeInfo { readonly network: string; } @@ -124,15 +124,22 @@ export declare class RestClient { constructor(url: string, mode?: BroadcastMode); get(path: string): Promise; post(path: string, params: PostTxsParams): Promise; - nodeInfo(): Promise; + authAccounts(address: string): Promise; blocksLatest(): Promise; blocks(height: number): Promise; + nodeInfo(): Promise; + txsQuery(query: string): Promise; + txsById(id: string): Promise; /** returns the amino-encoding of the transaction performed by the server */ encodeTx(tx: CosmosSdkTx): Promise; - authAccounts(address: string): Promise; - txs(query: string): Promise; - txsById(id: string): Promise; - postTx(tx: Uint8Array): Promise; + /** + * Broadcasts a signed transaction to into the transaction pool. + * Depending on the RestClient's broadcast mode, this might or might + * wait for checkTx or deliverTx to be executed before returning. + * + * @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container) + */ + postTx(tx: StdTx): Promise; listCodeInfo(): Promise; getCode(id: number): Promise; listContractAddresses(): Promise;