diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index d8be0c4c..2dcf49a4 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -160,8 +160,7 @@ export class CosmWasmConnection implements BlockchainConnection { this.erc20Tokens.map( async (erc20): Promise => { const queryMsg = { balance: { address: address } }; - // tslint:disable-next-line: deprecation - const smart = await this.restClient.queryContractSmart(erc20.contractAddress, queryMsg); + const smart = await this.cosmWasmClient.queryContractSmart(erc20.contractAddress, queryMsg); const response = JSON.parse(fromAscii(smart)); const normalizedBalance = new BN(response.balance).toString(); return { diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 5862ae17..cab410d3 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -1,3 +1,4 @@ +import { Bech32, Encoding } from "@iov/encoding"; import { assert } from "@iov/utils"; import { CosmWasmClient } from "./cosmwasmclient"; @@ -9,6 +10,8 @@ import cosmoshub from "./testdata/cosmoshub.json"; import { getRandomizedHackatom, makeRandomAddress } from "./testutils.spec"; import { Coin, CosmosSdkTx, MsgSend, StdFee } from "./types"; +const { fromAscii, fromUtf8, toAscii } = Encoding; + const httpUrl = "http://localhost:1317"; function cosmosEnabled(): boolean { @@ -35,6 +38,14 @@ const unusedAccount = { address: "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u", }; +interface HackatomInstance { + readonly initMsg: { + readonly verifier: string; + readonly beneficiary: string; + }; + readonly address: string; +} + describe("CosmWasmClient", () => { describe("makeReadOnly", () => { it("can be constructed", () => { @@ -359,4 +370,104 @@ describe("CosmWasmClient", () => { expect(contractBalance).toEqual([]); }); }); + + describe("queryContractRaw", () => { + const configKey = toAscii("config"); + const otherKey = toAscii("this_does_not_exist"); + let contract: HackatomInstance | undefined; + + beforeAll(async () => { + if (cosmosEnabled()) { + pendingWithoutCosmos(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = CosmWasmClient.makeWritable(httpUrl, 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); + contract = { initMsg: initMsg, address: contractAddress }; + } + }); + + it("can query existing key", async () => { + pendingWithoutCosmos(); + assert(contract); + + const client = CosmWasmClient.makeReadOnly(httpUrl); + const raw = await client.queryContractRaw(contract.address, configKey); + assert(raw, "must get result"); + expect(JSON.parse(fromUtf8(raw))).toEqual({ + verifier: Array.from(Bech32.decode(contract.initMsg.verifier).data), + beneficiary: Array.from(Bech32.decode(contract.initMsg.beneficiary).data), + funder: Array.from(Bech32.decode(faucet.address).data), + }); + }); + + it("can query non-existent key", async () => { + pendingWithoutCosmos(); + assert(contract); + + const client = CosmWasmClient.makeReadOnly(httpUrl); + const raw = await client.queryContractRaw(contract.address, otherKey); + expect(raw).toBeNull(); + }); + + it("errors for non-existent contract", async () => { + pendingWithoutCosmos(); + assert(contract); + + const nonExistentAddress = makeRandomAddress(); + const client = CosmWasmClient.makeReadOnly(httpUrl); + await client.queryContractRaw(nonExistentAddress, configKey).then( + () => fail("must not succeed"), + error => expect(error).toMatch(`No contract found at address "${nonExistentAddress}"`), + ); + }); + }); + + describe("queryContractSmart", () => { + let contract: HackatomInstance | undefined; + + beforeAll(async () => { + if (cosmosEnabled()) { + pendingWithoutCosmos(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = CosmWasmClient.makeWritable(httpUrl, 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); + contract = { initMsg: initMsg, address: contractAddress }; + } + }); + + it("works", async () => { + pendingWithoutCosmos(); + assert(contract); + + const client = CosmWasmClient.makeReadOnly(httpUrl); + const verifier = await client.queryContractSmart(contract.address, { verifier: {} }); + expect(fromAscii(verifier)).toEqual(contract.initMsg.verifier); + }); + + it("errors for malformed query message", async () => { + pendingWithoutCosmos(); + assert(contract); + + const client = CosmWasmClient.makeReadOnly(httpUrl); + await client.queryContractSmart(contract.address, { broken: {} }).then( + () => fail("must not succeed"), + error => expect(error).toMatch(/Error parsing QueryMsg/i), + ); + }); + + it("errors for non-existent contract", async () => { + pendingWithoutCosmos(); + + const nonExistentAddress = makeRandomAddress(); + const client = CosmWasmClient.makeReadOnly(httpUrl); + 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 c94232fd..1079dd85 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/sdk/src/cosmwasmclient.ts @@ -294,4 +294,39 @@ export class CosmWasmClient { logs: result.logs, }; } + + /** + * Returns the data at the key if present (raw contract dependent storage data) + * or null if no data at this key. + * + * Promise is rejected when contract does not exist. + */ + public async queryContractRaw(address: string, key: Uint8Array): Promise { + // just test contract existence + const _info = await this.restClient.getContractInfo(address); + + return this.restClient.queryContractRaw(address, key); + } + + /** + * Makes a "smart query" on the contract, returns raw data + * + * Promise is rejected when contract does not exist. + * Promise is rejected for invalid query format. + */ + public async queryContractSmart(address: string, queryMsg: object): Promise { + try { + return await this.restClient.queryContractSmart(address, queryMsg); + } catch (error) { + if (error instanceof Error) { + if (error.message === "not found: contract") { + throw new Error(`No contract found at address "${address}"`); + } else { + throw error; + } + } else { + throw error; + } + } + } } diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 276de57b..74f53a22 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -430,10 +430,11 @@ describe("RestClient", () => { expect((myInfo.init_msg as any).beneficiary).toEqual(beneficiaryAddress); // make sure random addresses don't give useful info + const nonExistentAddress = makeRandomAddress(); await client - .getContractInfo(beneficiaryAddress) + .getContractInfo(nonExistentAddress) .then(() => fail("this shouldn't succeed")) - .catch(error => expect(error).toMatch(`No contract with address ${beneficiaryAddress}`)); + .catch(error => expect(error).toMatch(`No contract found at address "${nonExistentAddress}"`)); }); describe("contract state", () => { diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index a8d4b1a5..3fd3c847 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -311,7 +311,7 @@ export class RestClient { // rest server returns null if no data for the address const info: ContractInfo | null = parseWasmResponse(responseData as WasmResponse); if (!info) { - throw new Error(`No contract with address ${address}`); + throw new Error(`No contract found at address "${address}"`); } return info; } diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/sdk/types/cosmwasmclient.d.ts index 14be80a8..88eebd4c 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/sdk/types/cosmwasmclient.d.ts @@ -62,4 +62,18 @@ export declare class CosmWasmClient { memo?: string, transferAmount?: readonly Coin[], ): Promise; + /** + * Returns the data at the key if present (raw contract dependent storage data) + * or null if no data at this key. + * + * Promise is rejected when contract does not exist. + */ + queryContractRaw(address: string, key: Uint8Array): Promise; + /** + * Makes a "smart query" on the contract, returns raw data + * + * Promise is rejected when contract does not exist. + * Promise is rejected for invalid query format. + */ + queryContractSmart(address: string, queryMsg: object): Promise; }