diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index af0e42a0..c822ac0a 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { Random } from "@iov/crypto"; +import { Random, Sha256 } from "@iov/crypto"; import { Bech32, Encoding } from "@iov/encoding"; import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; @@ -85,13 +85,17 @@ function makeRandomAddress(): string { return Bech32.encode("cosmos", Random.getBytes(20)); } -async function uploadContract(client: RestClient, pen: Pen): Promise { +async function uploadCustomContract( + client: RestClient, + pen: Pen, + wasmCode: Uint8Array, +): Promise { const memo = "My first contract on chain"; const theMsg: MsgStoreCode = { type: "wasm/store-code", value: { sender: faucet.address, - wasm_byte_code: toBase64(getRandomizedContract()), + wasm_byte_code: toBase64(wasmCode), source: "https://github.com/confio/cosmwasm/raw/0.7/lib/vm/testdata/contract_0.6.wasm", builder: "cosmwasm-opt:0.6.2", }, @@ -113,6 +117,10 @@ async function uploadContract(client: RestClient, pen: Pen): Promise { + return uploadCustomContract(client, pen, getRandomizedContract()); +} + async function instantiateContract( client: RestClient, pen: Pen, @@ -223,6 +231,7 @@ describe("RestClient", () => { }); }); + // this is failing for me on first run (faucet has not signed anything) it("has correct pubkey for faucet", async () => { pendingWithoutCosmos(); const client = new RestClient(httpUrl); @@ -362,7 +371,8 @@ describe("RestClient", () => { const numExisting = existingInfos.length; // upload data - const result = await uploadContract(client, pen); + const wasmCode = getRandomizedContract(); + const result = await uploadCustomContract(client, pen, wasmCode); expect(result.code).toBeFalsy(); const logs = parseLogs(result.logs); const codeIdAttr = findAttribute(logs, "message", "code_id"); @@ -375,10 +385,19 @@ describe("RestClient", () => { expect(lastInfo.id).toEqual(codeId); expect(lastInfo.creator).toEqual(faucet.address); - // TODO: check code hash matches expectation - // expect(lastInfo.code_hash).toEqual(faucet.address); + // ensure metadata is present + expect(lastInfo.source).toEqual( + "https://github.com/confio/cosmwasm/raw/0.7/lib/vm/testdata/contract_0.6.wasm", + ); + expect(lastInfo.builder).toEqual("cosmwasm-opt:0.6.2"); - // TODO: download code and check against auto-gen + // check code hash matches expectation + const wasmHash = new Sha256(wasmCode).digest(); + expect(lastInfo.code_hash.toLowerCase()).toEqual(toHex(wasmHash)); + + // download code and check against auto-gen + const download = await client.getCode(codeId); + expect(download).toEqual(wasmCode); }); it("can list contracts and get info", async () => { @@ -408,6 +427,8 @@ describe("RestClient", () => { // create new instance and compare before and after const existingContracts = await client.listContractAddresses(); + const existingContractsByCode = await client.listContractsByCodeId(codeId); + existingContractsByCode.forEach(ctc => expect(ctc.code_id).toEqual(codeId)); const result = await instantiateContract(client, pen, codeId, beneficiaryAddress, transferAmount); expect(result.code).toBeFalsy(); @@ -424,6 +445,11 @@ describe("RestClient", () => { const lastContract = diff[0]; expect(lastContract).toEqual(myAddress); + // also by codeID list + const newContractsByCode = await client.listContractsByCodeId(codeId); + newContractsByCode.forEach(ctc => expect(ctc.code_id).toEqual(codeId)); + expect(newContractsByCode.length).toEqual(existingContractsByCode.length + 1); + // check out info const myInfo = await client.getContractInfo(myAddress); expect(myInfo.code_id).toEqual(codeId); @@ -501,18 +527,14 @@ describe("RestClient", () => { // invalid query syntax throws an error await client.queryContractSmart(contractAddress, { nosuchkey: {} }).then( () => fail("shouldn't succeed"), - error => expect(error).toBeTruthy(), + error => expect(error).toMatch("Error parsing QueryMsg"), ); - // TODO: debug rest server. I expect a 'Parse Error', but get - // Request failed with status code 500 to match 'Parse Error:' // invalid address throws an error await client.queryContractSmart(noContract, { verifier: {} }).then( () => fail("shouldn't succeed"), - error => expect(error).toBeTruthy(), + error => expect(error).toMatch("not found"), ); - // TODO: debug rest server. I expect a 'not found', but get - // Request failed with status code 500 to match 'Parse Error:' }); }); }); diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index 632f0b5b..cd128366 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -1,5 +1,5 @@ import { Encoding } from "@iov/encoding"; -import axios, { AxiosInstance } from "axios"; +import axios, { AxiosError, AxiosInstance } from "axios"; import { AminoTx, CodeInfo, ContractInfo, CosmosSdkAccount, isAminoStdTx, StdTx, WasmData } from "./types"; @@ -45,11 +45,11 @@ interface AuthAccountsResponse { // Currently all wasm query responses return json-encoded strings... // later deprecate this and use the specific types for result // (assuming it is inlined, no second parse needed) -type WasmResponse = WasmSuccess | WasmError; +type WasmResponse = WasmSuccess | WasmError; -interface WasmSuccess { +interface WasmSuccess { readonly height: string; - readonly result: string; + readonly result: T; } interface WasmError { @@ -92,6 +92,11 @@ interface EncodeTxResponse { readonly tx: string; } +interface GetCodeResult { + // base64 encoded wasm + readonly code: string; +} + type RestClientResponse = | NodeInfoResponse | BlocksResponse @@ -100,21 +105,49 @@ type RestClientResponse = | SearchTxsResponse | PostTxsResponse | EncodeTxResponse - | WasmResponse; + | WasmResponse + | WasmResponse; type BroadcastMode = "block" | "sync" | "async"; -function isWasmError(resp: WasmResponse): resp is WasmError { +function isWasmError(resp: WasmResponse): resp is WasmError { return (resp as WasmError).error !== undefined; } -function parseWasmResponse(response: WasmResponse): any { +function unwrapWasmResponse(response: WasmResponse): T { + if (isWasmError(response)) { + throw new Error(response.error); + } + return response.result; +} + +function parseWasmResponse(response: WasmResponse): any { if (isWasmError(response)) { throw new Error(response.error); } return JSON.parse(response.result); } +// We want to get message data from 500 errors +// https://stackoverflow.com/questions/56577124/how-to-handle-500-error-message-with-axios +// this should be chained to catch one error and throw a more informative one +function parseAxios500error(err: AxiosError): never { + // use the error message sent from server, not default 500 msg + if (err.response?.data) { + const data = err.response.data; + // expect { error: string }, but otherwise dump + if (data.error) { + throw new Error(data.error); + } else if (typeof data === "string") { + throw new Error(data); + } else { + throw new Error(JSON.stringify(data)); + } + } else { + throw err; + } +} + export class RestClient { private readonly client: AxiosInstance; // From https://cosmos.network/rpc/#/ICS0/post_txs @@ -133,7 +166,7 @@ export class RestClient { } public async get(path: string): Promise { - const { data } = await this.client.get(path); + const { data } = await this.client.get(path).catch(parseAxios500error); if (data === null) { throw new Error("Received null response from server"); } @@ -141,7 +174,7 @@ export class RestClient { } public async post(path: string, params: PostTxsParams): Promise { - const { data } = await this.client.post(path, params); + const { data } = await this.client.post(path, params).catch(parseAxios500error); if (data === null) { throw new Error("Received null response from server"); } @@ -237,10 +270,9 @@ export class RestClient { // this will download the original wasm bytecode by code id // throws error if no code with this id public async getCode(id: number): Promise { - // TODO: broken currently const path = `/wasm/code/${id}`; - const responseData = await this.get(path); - const { code } = parseWasmResponse(responseData as WasmResponse); + const responseData = (await this.get(path)) as WasmResponse; + const { code } = unwrapWasmResponse(responseData); return fromBase64(code); } @@ -252,6 +284,14 @@ export class RestClient { return addresses || []; } + public async listContractsByCodeId(id: number): Promise { + const path = `/wasm/code/${id}/contracts`; + const responseData = await this.get(path); + // answer may be null (go's encoding of empty array) + const contracts: ContractInfo[] | null = parseWasmResponse(responseData as WasmResponse); + return contracts || []; + } + // throws error if no contract at this address public async getContractInfo(address: string): Promise { const path = `/wasm/contract/${address}`; diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index a7fbd87f..d8a23d42 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -30,10 +30,10 @@ interface AuthAccountsResponse { readonly value: CosmosSdkAccount; }; } -declare type WasmResponse = WasmSuccess | WasmError; -interface WasmSuccess { +declare type WasmResponse = WasmSuccess | WasmError; +interface WasmSuccess { readonly height: string; - readonly result: string; + readonly result: T; } interface WasmError { readonly error: string; @@ -68,6 +68,9 @@ export interface PostTxsResponse { interface EncodeTxResponse { readonly tx: string; } +interface GetCodeResult { + readonly code: string; +} declare type RestClientResponse = | NodeInfoResponse | BlocksResponse @@ -76,7 +79,8 @@ declare type RestClientResponse = | SearchTxsResponse | PostTxsResponse | EncodeTxResponse - | WasmResponse; + | WasmResponse + | WasmResponse; declare type BroadcastMode = "block" | "sync" | "async"; export declare class RestClient { private readonly client; @@ -96,6 +100,7 @@ export declare class RestClient { listCodeInfo(): Promise; getCode(id: number): Promise; listContractAddresses(): Promise; + listContractsByCodeId(id: number): Promise; getContractInfo(address: string): Promise; getAllContractState(address: string): Promise; queryContractRaw(address: string, key: Uint8Array): Promise; diff --git a/scripts/cosm/env b/scripts/cosm/env index e759ee1e..d178946e 100644 --- a/scripts/cosm/env +++ b/scripts/cosm/env @@ -1,5 +1,5 @@ # Choose from https://hub.docker.com/r/cosmwasm/wasmd-demo/tags REPOSITORY="cosmwasm/wasmd-demo" -VERSION="v0.0.2" +VERSION="v0.0.4" CONTAINER_NAME="wasmd"