From f1e1c76baa29862edc8506a46d809fc6fb751106 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 10 Feb 2020 12:01:21 +0100 Subject: [PATCH 1/5] Update rest server, pass tests --- packages/sdk/src/restclient.spec.ts | 4 +++- scripts/cosm/env | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index af0e42a0..abf79c5f 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -223,7 +223,9 @@ describe("RestClient", () => { }); }); - it("has correct pubkey for faucet", async () => { + // TODO: re-enable when stable + // this is failing for me on first run (faucet has not signed anything) + xit("has correct pubkey for faucet", async () => { pendingWithoutCosmos(); const client = new RestClient(httpUrl); const { result } = await client.authAccounts(faucet.address); diff --git a/scripts/cosm/env b/scripts/cosm/env index e759ee1e..7a6a9a2b 100644 --- a/scripts/cosm/env +++ b/scripts/cosm/env @@ -1,5 +1,6 @@ # Choose from https://hub.docker.com/r/cosmwasm/wasmd-demo/tags REPOSITORY="cosmwasm/wasmd-demo" -VERSION="v0.0.2" +#VERSION="v0.0.4" +VERSION="latest" CONTAINER_NAME="wasmd" From efcca8a7698be7c9c4cc50d771b152125e3a1727 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 10 Feb 2020 12:18:19 +0100 Subject: [PATCH 2/5] Download wasm code by id works --- packages/sdk/src/restclient.spec.ts | 29 +++++++++++++++++++--------- packages/sdk/src/restclient.ts | 30 ++++++++++++++++++++--------- packages/sdk/types/restclient.d.ts | 12 ++++++++---- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index abf79c5f..7e2cecbf 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,9 +231,8 @@ describe("RestClient", () => { }); }); - // TODO: re-enable when stable // this is failing for me on first run (faucet has not signed anything) - xit("has correct pubkey for faucet", async () => { + it("has correct pubkey for faucet", async () => { pendingWithoutCosmos(); const client = new RestClient(httpUrl); const { result } = await client.authAccounts(faucet.address); @@ -364,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"); @@ -377,10 +385,13 @@ 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); + // check code hash matches expectation + const wasmHash = new Sha256(wasmCode).digest(); + expect(lastInfo.code_hash.toLowerCase()).toEqual(toHex(wasmHash)); - // TODO: download code and check against auto-gen + // 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 () => { diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index 632f0b5b..91b4647e 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -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,15 +105,23 @@ 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); } @@ -237,10 +250,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); } diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index a7fbd87f..d560856f 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; From abe8eeeac3baa1295a204838d9a9e9e48ce1e800 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 10 Feb 2020 12:41:47 +0100 Subject: [PATCH 3/5] Return full info on 500 errors --- packages/sdk/src/restclient.spec.ts | 15 ++++++++----- packages/sdk/src/restclient.ts | 34 ++++++++++++++++++++++++++--- packages/sdk/types/restclient.d.ts | 1 + 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 7e2cecbf..e9ca24be 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -421,6 +421,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(); @@ -437,6 +439,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); @@ -514,18 +521,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 91b4647e..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"; @@ -128,6 +128,26 @@ function parseWasmResponse(response: WasmResponse): any { 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 @@ -146,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"); } @@ -154,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"); } @@ -264,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 d560856f..d8a23d42 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -100,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; From 77294979bc5e7314f7359d6c47d32011180ca7ea Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 10 Feb 2020 12:46:01 +0100 Subject: [PATCH 4/5] Ensure metadata is set --- packages/sdk/src/restclient.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index e9ca24be..c822ac0a 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -385,6 +385,12 @@ describe("RestClient", () => { expect(lastInfo.id).toEqual(codeId); expect(lastInfo.creator).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"); + // check code hash matches expectation const wasmHash = new Sha256(wasmCode).digest(); expect(lastInfo.code_hash.toLowerCase()).toEqual(toHex(wasmHash)); From 8be57612c4533d2e2c8d1d3ea3c1b776fcb483d0 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 10 Feb 2020 12:57:44 +0100 Subject: [PATCH 5/5] Use new v0.0.4 docker image --- scripts/cosm/env | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/cosm/env b/scripts/cosm/env index 7a6a9a2b..d178946e 100644 --- a/scripts/cosm/env +++ b/scripts/cosm/env @@ -1,6 +1,5 @@ # Choose from https://hub.docker.com/r/cosmwasm/wasmd-demo/tags REPOSITORY="cosmwasm/wasmd-demo" -#VERSION="v0.0.4" -VERSION="latest" +VERSION="v0.0.4" CONTAINER_NAME="wasmd"