From e0b3cb006c47acbafc489829d41b37d56dc8755b Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 11:23:18 +0100 Subject: [PATCH 01/13] Add some basic wasm queries (manually tested with curl) --- packages/sdk/src/restclient.ts | 62 +++++++++++++++++++++++++++++- packages/sdk/src/types.ts | 21 ++++++++++ packages/sdk/types/restclient.d.ts | 12 +++++- packages/sdk/types/types.d.ts | 18 +++++++++ 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index 2e56a4a4..9782f198 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -1,7 +1,7 @@ import { Encoding } from "@iov/encoding"; import axios, { AxiosInstance } from "axios"; -import { AminoTx, BaseAccount, isAminoStdTx, StdTx } from "./types"; +import { AminoTx, BaseAccount, CodeInfo, CodeInfoWithId, ContractInfo, isAminoStdTx, StdTx } from "./types"; const { fromUtf8 } = Encoding; @@ -41,6 +41,34 @@ interface AuthAccountsResponse { }; } +// Currently all wasm query responses return json-encoded strings... +// later deprecate this and use the specific types below +interface WasmResponse { + readonly result: string; +} + +interface ListCodeResponse { + // TODO: this is returning json.encoded string now, parse on client or server? + readonly result: readonly CodeInfoWithId[]; +} + +interface GetCodeResponse { + // TODO: this is returning json.encoded string now, parse on client or server? + readonly result: CodeInfo; +} + +interface ListContractResponse { + // TODO: this is returning json.encoded string now, parse on client or server? + // contains list of all contract addresses + readonly result: readonly string[]; +} + +interface GetContractResponse { + // TODO: this is returning json.encoded string now, parse on client or server? + // contains list of all contract addresses + readonly result: ContractInfo; +} + export interface TxsResponse { readonly height: string; readonly txhash: string; @@ -84,7 +112,8 @@ type RestClientResponse = | TxsResponse | SearchTxsResponse | PostTxsResponse - | EncodeTxResponse; + | EncodeTxResponse + | WasmResponse; type BroadcastMode = "block" | "sync" | "async"; @@ -198,4 +227,33 @@ export class RestClient { } return responseData as PostTxsResponse; } + + // 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`; + const responseData = await this.get(path); + const codes = JSON.parse((responseData as WasmResponse).result); + return codes as CodeInfoWithId[]; + } + + public async getCodeInfo(id: number): Promise { + const path = `/wasm/code/${id}`; + const responseData = await this.get(path); + const code = JSON.parse((responseData as WasmResponse).result); + return code as CodeInfo; + } + + public async listContractAddresses(): Promise { + const path = `/wasm/contract`; + const responseData = await this.get(path); + const codes = JSON.parse((responseData as WasmResponse).result); + return codes as string[]; + } + + public async getContractInfo(address: string): Promise { + const path = `/wasm/contract/${address}`; + const responseData = await this.get(path); + const code = JSON.parse((responseData as WasmResponse).result); + return code as ContractInfo; + } } diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 78f21208..62fc2b48 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -162,3 +162,24 @@ export interface BaseAccount { /** The data we need from BaseAccount to create a nonce */ export type NonceInfo = Pick; + +export interface CodeInfo { + /** Bech32 account address */ + readonly creator: string; + /** Hex-encoded sha256 hash of the code stored here */ + readonly code_hash: string; + readonly source?: string; + readonly builder?: string; +} + +export interface CodeInfoWithId extends CodeInfo { + readonly id: number; +} + +export interface ContractInfo { + readonly code_id: number; + /** Bech32 account address */ + readonly creator: string; + /** Argument passed on initialization of the contract */ + readonly init_msg: object; +} diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index e235d842..59934755 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -1,4 +1,4 @@ -import { AminoTx, BaseAccount, StdTx } from "./types"; +import { AminoTx, BaseAccount, CodeInfo, CodeInfoWithId, ContractInfo, StdTx } from "./types"; interface NodeInfo { readonly network: string; } @@ -29,6 +29,9 @@ interface AuthAccountsResponse { readonly value: BaseAccount; }; } +interface WasmResponse { + readonly result: string; +} export interface TxsResponse { readonly height: string; readonly txhash: string; @@ -66,7 +69,8 @@ declare type RestClientResponse = | TxsResponse | SearchTxsResponse | PostTxsResponse - | EncodeTxResponse; + | EncodeTxResponse + | WasmResponse; declare type BroadcastMode = "block" | "sync" | "async"; export declare class RestClient { private readonly client; @@ -83,5 +87,9 @@ export declare class RestClient { txs(query: string): Promise; txsById(id: string): Promise; postTx(tx: Uint8Array): Promise; + listCodeInfo(): Promise; + getCodeInfo(id: number): Promise; + listContractAddresses(): Promise; + getContractInfo(address: string): Promise; } export {}; diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts index 83f5ace6..fd4e8605 100644 --- a/packages/sdk/types/types.d.ts +++ b/packages/sdk/types/types.d.ts @@ -120,4 +120,22 @@ export interface BaseAccount { } /** The data we need from BaseAccount to create a nonce */ export declare type NonceInfo = Pick; +export interface CodeInfo { + /** Bech32 account address */ + readonly creator: string; + /** Hex-encoded sha256 hash of the code stored here */ + readonly code_hash: string; + readonly source?: string; + readonly builder?: string; +} +export interface CodeInfoWithId extends CodeInfo { + readonly id: number; +} +export interface ContractInfo { + readonly code_id: number; + /** Bech32 account address */ + readonly creator: string; + /** Argument passed on initialization of the contract */ + readonly init_msg: object; +} export {}; From 477511f1b32554f6adb8a2d0e74f2936d41d562a Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 16:11:46 +0100 Subject: [PATCH 02/13] Complete wasm calls via manual testing with curl --- packages/sdk/src/restclient.ts | 102 ++++++++++++++++++++++----------- packages/sdk/src/types.ts | 14 ++++- 2 files changed, 80 insertions(+), 36 deletions(-) diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index 9782f198..8f121628 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -1,9 +1,9 @@ import { Encoding } from "@iov/encoding"; import axios, { AxiosInstance } from "axios"; -import { AminoTx, BaseAccount, CodeInfo, CodeInfoWithId, ContractInfo, isAminoStdTx, StdTx } from "./types"; +import { AminoTx, BaseAccount, CodeInfo, ContractInfo, isAminoStdTx, StdTx, WasmData } from "./types"; -const { fromUtf8 } = Encoding; +const { fromBase64, fromUtf8, toHex, toUtf8 } = Encoding; interface NodeInfo { readonly network: string; @@ -42,31 +42,17 @@ interface AuthAccountsResponse { } // Currently all wasm query responses return json-encoded strings... -// later deprecate this and use the specific types below -interface WasmResponse { +// later deprecate this and use the specific types for result +// (assuming it is inlined, no second parse needed) +type WasmResponse = WasmSuccess | WasmError; + +interface WasmSuccess { + readonly height: string; readonly result: string; } -interface ListCodeResponse { - // TODO: this is returning json.encoded string now, parse on client or server? - readonly result: readonly CodeInfoWithId[]; -} - -interface GetCodeResponse { - // TODO: this is returning json.encoded string now, parse on client or server? - readonly result: CodeInfo; -} - -interface ListContractResponse { - // TODO: this is returning json.encoded string now, parse on client or server? - // contains list of all contract addresses - readonly result: readonly string[]; -} - -interface GetContractResponse { - // TODO: this is returning json.encoded string now, parse on client or server? - // contains list of all contract addresses - readonly result: ContractInfo; +interface WasmError { + readonly error: string; } export interface TxsResponse { @@ -117,6 +103,17 @@ type RestClientResponse = type BroadcastMode = "block" | "sync" | "async"; +function isWasmError(resp: WasmResponse): resp is WasmError { + return (resp as WasmError).error !== undefined; +} + +function parseWasmResponse(response: WasmResponse): any { + if (isWasmError(response)) { + throw new Error(response.error); + } + return JSON.parse(response.result); +} + export class RestClient { private readonly client: AxiosInstance; // From https://cosmos.network/rpc/#/ICS0/post_txs @@ -229,31 +226,68 @@ export class RestClient { } // 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 { + public async listCodeInfo(): Promise { const path = `/wasm/code`; const responseData = await this.get(path); - const codes = JSON.parse((responseData as WasmResponse).result); - return codes as CodeInfoWithId[]; + // answer may be null (empty array) + return parseWasmResponse(responseData as WasmResponse) || []; } - public async getCodeInfo(id: number): Promise { + // this will download the original wasm bytecode by code id + // throws error if no code with this id + public async getCode(id: number): Promise { const path = `/wasm/code/${id}`; const responseData = await this.get(path); - const code = JSON.parse((responseData as WasmResponse).result); - return code as CodeInfo; + const { code } = parseWasmResponse(responseData as WasmResponse); + return fromBase64(code); } public async listContractAddresses(): Promise { const path = `/wasm/contract`; const responseData = await this.get(path); - const codes = JSON.parse((responseData as WasmResponse).result); - return codes as string[]; + // answer may be null (empty array) + return parseWasmResponse(responseData as WasmResponse) || []; } + // throws error if no contract at this address public async getContractInfo(address: string): Promise { const path = `/wasm/contract/${address}`; const responseData = await this.get(path); - const code = JSON.parse((responseData as WasmResponse).result); - return code as ContractInfo; + const info = parseWasmResponse(responseData as WasmResponse); + if (!info) { + throw new Error(`No contract with address ${address}`); + } + return info; + } + + // Returns all contract state. + // This is an empty array if no such contract, or contract has no data. + public async getAllContractState(address: string): Promise { + const path = `/wasm/contract/${address}/state`; + const responseData = await this.get(path); + return parseWasmResponse(responseData as WasmResponse); + } + + // Returns the data at the key if present (unknown decoded json), + // or null if no data at this (contract address, key) pair + public async getContractKey(address: string, key: Uint8Array): Promise { + const hexKey = toHex(key); + const path = `/wasm/contract/${address}/raw/${hexKey}?encoding=hex`; + const responseData = await this.get(path); + const data: readonly WasmData[] = parseWasmResponse(responseData as WasmResponse); + return data.length === 0 ? null : data[0].val; + } + + // Makes a "smart query" on the contract, returns response verbatim (json.RawMessage) + // Throws error if no such contract or invalid query format + public async queryContract(address: string, query: object): Promise { + const encoded = toHex(toUtf8(JSON.stringify(query))); + const path = `/wasm/contract/${address}/smart/${encoded}?encoding=hex`; + const responseData = (await this.get(path)) as WasmResponse; + if (isWasmError(responseData)) { + throw new Error(responseData.error); + } + // no extra parse here + return responseData.result; } } diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 62fc2b48..9cf9cdfb 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -164,16 +164,19 @@ export interface BaseAccount { export type NonceInfo = Pick; export interface CodeInfo { + readonly id: number; /** Bech32 account address */ readonly creator: string; /** Hex-encoded sha256 hash of the code stored here */ readonly code_hash: string; + // TODO: these are not supported in current wasmd readonly source?: string; readonly builder?: string; } -export interface CodeInfoWithId extends CodeInfo { - readonly id: number; +export interface CodeDetails { + // TODO: this should be base64 encoded string with content - not in current stack + readonly code: string; } export interface ContractInfo { @@ -183,3 +186,10 @@ export interface ContractInfo { /** Argument passed on initialization of the contract */ readonly init_msg: object; } + +export interface WasmData { + // key is hex-encoded + readonly key: string; + // value can be any decoded json, often an object but can be anything + readonly val: unknown; +} From da9eff547eb0e2b68a0ecfb3d68541fb85e99178 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 16:26:37 +0100 Subject: [PATCH 03/13] Pull out wasm contract actions as test helper functions --- packages/sdk/src/restclient.spec.ts | 201 ++++++++++++++++------------ packages/sdk/src/restclient.ts | 3 +- packages/sdk/types/restclient.d.ts | 18 ++- packages/sdk/types/types.d.ts | 9 +- 4 files changed, 140 insertions(+), 91 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 5c12db17..1ae9bb2f 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 { ChainId, PrehashType, SignableBytes } from "@iov/bcp"; +import { ChainId, Identity, PrehashType, SignableBytes } from "@iov/bcp"; import { Random } from "@iov/crypto"; import { Bech32, Encoding } from "@iov/encoding"; import { HdPaths, Secp256k1HdWallet } from "@iov/keycontrol"; @@ -7,7 +7,7 @@ import { HdPaths, Secp256k1HdWallet } from "@iov/keycontrol"; import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; import { leb128Encode } from "./leb128.spec"; import { Attribute, Log, parseLogs } from "./logs"; -import { RestClient } from "./restclient"; +import { PostTxsResponse, RestClient } from "./restclient"; import contract from "./testdata/contract.json"; import cosmoshub from "./testdata/cosmoshub.json"; import { @@ -174,6 +174,112 @@ describe("RestClient", () => { expect(result.code).toBeFalsy(); }); + async function uploadContract( + client: RestClient, + wallet: Secp256k1HdWallet, + signer: Identity, + ): Promise { + const memo = "My first contract on chain"; + const theMsg: MsgStoreCode = { + type: "wasm/store-code", + value: { + sender: faucetAddress, + wasm_byte_code: toBase64(getRandomizedContract()), + source: "https://github.com/confio/cosmwasm/raw/0.7/lib/vm/testdata/contract_0.6.wasm", + builder: "cosmwasm-opt:0.6.2", + }, + }; + const fee: StdFee = { + amount: [ + { + amount: "5000000", + denom: "ucosm", + }, + ], + gas: "89000000", + }; + + const account = (await client.authAccounts(faucetAddress)).result.value; + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; + const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); + const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); + const signedTx = makeSignedTx(theMsg, fee, memo, signature); + return client.postTx(marshalTx(signedTx)); + } + + async function instantiateContract( + client: RestClient, + wallet: Secp256k1HdWallet, + signer: Identity, + codeId: number, + beneficiaryAddress: string, + transferAmount: readonly Coin[], + ): Promise { + const memo = "Create an escrow instance"; + const theMsg: MsgInstantiateContract = { + type: "wasm/instantiate", + value: { + sender: faucetAddress, + code_id: codeId.toString(), + init_msg: { + verifier: faucetAddress, + beneficiary: beneficiaryAddress, + }, + init_funds: transferAmount, + }, + }; + const fee: StdFee = { + amount: [ + { + amount: "5000000", + denom: "ucosm", + }, + ], + gas: "89000000", + }; + + const account = (await client.authAccounts(faucetAddress)).result.value; + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; + const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); + const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); + const signedTx = makeSignedTx(theMsg, fee, memo, signature); + return client.postTx(marshalTx(signedTx)); + } + + async function executeContract( + client: RestClient, + wallet: Secp256k1HdWallet, + signer: Identity, + contractAddress: string, + ): Promise { + const memo = "Time for action"; + const theMsg: MsgExecuteContract = { + type: "wasm/execute", + value: { + sender: faucetAddress, + contract: contractAddress, + msg: {}, + sent_funds: [], + }, + }; + const fee: StdFee = { + amount: [ + { + amount: "5000000", + denom: "ucosm", + }, + ], + gas: "89000000", + }; + + const account = (await client.authAccounts(faucetAddress)).result.value; + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; + const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); + const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); + const signedTx = makeSignedTx(theMsg, fee, memo, signature); + return client.postTx(marshalTx(signedTx)); + } + it("can upload, instantiate and execute wasm", async () => { pendingWithoutCosmos(); const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic); @@ -196,33 +302,8 @@ describe("RestClient", () => { // upload { - const memo = "My first contract on chain"; - const theMsg: MsgStoreCode = { - type: "wasm/store-code", - value: { - sender: faucetAddress, - wasm_byte_code: toBase64(getRandomizedContract()), - source: "https://github.com/confio/cosmwasm/raw/0.7/lib/vm/testdata/contract_0.6.wasm", - builder: "cosmwasm-opt:0.6.2", - }, - }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - - const account = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; - const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); - const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); - const signedTx = makeSignedTx(theMsg, fee, memo, signature); - const result = await client.postTx(marshalTx(signedTx)); // console.log("Raw log:", result.raw_log); + const result = await uploadContract(client, wallet, signer); expect(result.code).toBeFalsy(); const logs = parseSuccess(result.raw_log); const codeIdAttr = findAttribute(logs, "message", "code_id"); @@ -235,35 +316,14 @@ describe("RestClient", () => { // instantiate { - const memo = "Create an escrow instance"; - const theMsg: MsgInstantiateContract = { - type: "wasm/instantiate", - value: { - sender: faucetAddress, - code_id: codeId.toString(), - init_msg: { - verifier: faucetAddress, - beneficiary: beneficiaryAddress, - }, - init_funds: transferAmount, - }, - }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - - const account = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; - const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); - const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); - const signedTx = makeSignedTx(theMsg, fee, memo, signature); - const result = await client.postTx(marshalTx(signedTx)); + const result = await instantiateContract( + client, + wallet, + signer, + codeId, + beneficiaryAddress, + transferAmount, + ); expect(result.code).toBeFalsy(); // console.log("Raw log:", result.raw_log); const logs = parseSuccess(result.raw_log); @@ -278,32 +338,7 @@ describe("RestClient", () => { // execute { - const memo = "Time for action"; - const theMsg: MsgExecuteContract = { - type: "wasm/execute", - value: { - sender: faucetAddress, - contract: contractAddress, - msg: {}, - sent_funds: [], - }, - }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - - const account = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; - const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); - const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); - const signedTx = makeSignedTx(theMsg, fee, memo, signature); - const result = await client.postTx(marshalTx(signedTx)); + const result = await executeContract(client, wallet, signer, contractAddress); expect(result.code).toBeFalsy(); // console.log("Raw log:", result.raw_log); const [firstLog] = parseSuccess(result.raw_log); diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index 8f121628..a2a71e27 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -73,7 +73,7 @@ interface SearchTxsResponse { interface PostTxsParams {} -interface PostTxsResponse { +export interface PostTxsResponse { readonly height: string; readonly txhash: string; readonly code?: number; @@ -236,6 +236,7 @@ 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); diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index 59934755..e9feca7c 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -1,4 +1,4 @@ -import { AminoTx, BaseAccount, CodeInfo, CodeInfoWithId, ContractInfo, StdTx } from "./types"; +import { AminoTx, BaseAccount, CodeInfo, ContractInfo, StdTx, WasmData } from "./types"; interface NodeInfo { readonly network: string; } @@ -29,9 +29,14 @@ interface AuthAccountsResponse { readonly value: BaseAccount; }; } -interface WasmResponse { +declare type WasmResponse = WasmSuccess | WasmError; +interface WasmSuccess { + readonly height: string; readonly result: string; } +interface WasmError { + readonly error: string; +} export interface TxsResponse { readonly height: string; readonly txhash: string; @@ -47,7 +52,7 @@ interface SearchTxsResponse { readonly txs: readonly TxsResponse[]; } interface PostTxsParams {} -interface PostTxsResponse { +export interface PostTxsResponse { readonly height: string; readonly txhash: string; readonly code?: number; @@ -87,9 +92,12 @@ export declare class RestClient { txs(query: string): Promise; txsById(id: string): Promise; postTx(tx: Uint8Array): Promise; - listCodeInfo(): Promise; - getCodeInfo(id: number): Promise; + listCodeInfo(): Promise; + getCode(id: number): Promise; listContractAddresses(): Promise; getContractInfo(address: string): Promise; + getAllContractState(address: string): Promise; + getContractKey(address: string, key: Uint8Array): Promise; + queryContract(address: string, query: object): Promise; } export {}; diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts index fd4e8605..2d26a1b3 100644 --- a/packages/sdk/types/types.d.ts +++ b/packages/sdk/types/types.d.ts @@ -121,6 +121,7 @@ export interface BaseAccount { /** The data we need from BaseAccount to create a nonce */ export declare type NonceInfo = Pick; export interface CodeInfo { + readonly id: number; /** Bech32 account address */ readonly creator: string; /** Hex-encoded sha256 hash of the code stored here */ @@ -128,8 +129,8 @@ export interface CodeInfo { readonly source?: string; readonly builder?: string; } -export interface CodeInfoWithId extends CodeInfo { - readonly id: number; +export interface CodeDetails { + readonly code: string; } export interface ContractInfo { readonly code_id: number; @@ -138,4 +139,8 @@ export interface ContractInfo { /** Argument passed on initialization of the contract */ readonly init_msg: object; } +export interface WasmData { + readonly key: string; + readonly val: unknown; +} export {}; From 03be05964a8d4f58bd616d8487e791e8d90f6ee7 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 16:31:26 +0100 Subject: [PATCH 04/13] More test helper refactoring --- packages/sdk/src/restclient.spec.ts | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 1ae9bb2f..2241fc34 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -207,6 +207,14 @@ describe("RestClient", () => { return client.postTx(marshalTx(signedTx)); } + // getCodeId can be used with the result of uploadContract + function getCodeId(result: PostTxsResponse): number { + expect(result.code).toBeFalsy(); + const logs = parseSuccess(result.raw_log); + const codeIdAttr = findAttribute(logs, "message", "code_id"); + return Number.parseInt(codeIdAttr.value, 10); + } + async function instantiateContract( client: RestClient, wallet: Secp256k1HdWallet, @@ -246,6 +254,15 @@ describe("RestClient", () => { return client.postTx(marshalTx(signedTx)); } + // getContractAddress can be used with the result of instantiateContract + function getContractAddress(result: PostTxsResponse): string { + expect(result.code).toBeFalsy(); + // console.log("Raw log:", result.raw_log); + const logs = parseSuccess(result.raw_log); + const contractAddressAttr = findAttribute(logs, "message", "contract_address"); + return contractAddressAttr.value; + } + async function executeContract( client: RestClient, wallet: Secp256k1HdWallet, @@ -304,10 +321,7 @@ describe("RestClient", () => { { // console.log("Raw log:", result.raw_log); const result = await uploadContract(client, wallet, signer); - expect(result.code).toBeFalsy(); - const logs = parseSuccess(result.raw_log); - const codeIdAttr = findAttribute(logs, "message", "code_id"); - codeId = Number.parseInt(codeIdAttr.value, 10); + codeId = getCodeId(result); expect(codeId).toBeGreaterThanOrEqual(1); expect(codeId).toBeLessThanOrEqual(200); } @@ -324,13 +338,11 @@ describe("RestClient", () => { beneficiaryAddress, transferAmount, ); - expect(result.code).toBeFalsy(); - // console.log("Raw log:", result.raw_log); + contractAddress = getContractAddress(result); + const logs = parseSuccess(result.raw_log); const amountAttr = findAttribute(logs, "transfer", "amount"); expect(amountAttr.value).toEqual("1234ucosm,321ustake"); - const contractAddressAttr = findAttribute(logs, "message", "contract_address"); - contractAddress = contractAddressAttr.value; const balance = (await client.authAccounts(contractAddress)).result.value.coins; expect(balance).toEqual(transferAmount); @@ -350,6 +362,6 @@ describe("RestClient", () => { const contractBalance = (await client.authAccounts(contractAddress)).result.value.coins; expect(contractBalance).toEqual([]); } - }, 30_000); + }, 10_000); }); }); From 08461624340f4e6005341dcf65d780b0efb43a70 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 16:39:51 +0100 Subject: [PATCH 05/13] Test list codes --- packages/sdk/src/restclient.spec.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 2241fc34..016d9433 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -363,5 +363,30 @@ describe("RestClient", () => { expect(contractBalance).toEqual([]); } }, 10_000); + + it("can list code and query details", async () => { + pendingWithoutCosmos(); + const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic); + const signer = await wallet.createIdentity("abc" as ChainId, faucetPath); + const client = new RestClient(httpUrl); + + const existingInfos = await client.listCodeInfo(); + existingInfos.forEach((val, idx) => expect(val.id).toEqual(idx + 1)); + const numExisting = existingInfos.length; + + const result = await uploadContract(client, wallet, signer); + const codeId = getCodeId(result); + + const newInfos = await client.listCodeInfo(); + expect(newInfos.length).toEqual(numExisting + 1); + const lastInfo = newInfos[newInfos.length - 1]; + expect(lastInfo.id).toEqual(codeId); + expect(lastInfo.creator).toEqual(faucetAddress); + + // TODO: check code hash matches expectation + // expect(lastInfo.code_hash).toEqual(faucetAddress); + + // TODO: download code and check against auto-gen + }); }); }); From 4bacba83404b3b58b681aeea48db570a722e3047 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 16:55:51 +0100 Subject: [PATCH 06/13] Add some basic contract queries --- packages/sdk/src/restclient.spec.ts | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 016d9433..d8468bef 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -388,5 +388,68 @@ describe("RestClient", () => { // TODO: download code and check against auto-gen }); + + it("can list contracts and query details", async () => { + pendingWithoutCosmos(); + const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic); + const signer = await wallet.createIdentity("abc" as ChainId, faucetPath); + const client = new RestClient(httpUrl); + const beneficiaryAddress = makeRandomAddress(); + const transferAmount: readonly Coin[] = [ + { + amount: "707707", + denom: "ucosm", + }, + ]; + + // reuse an existing contract, or upload if needed + let codeId: number; + const existingInfos = await client.listCodeInfo(); + if (existingInfos.length > 0) { + codeId = existingInfos[existingInfos.length - 1].id; + } else { + const uploaded = await uploadContract(client, wallet, signer); + codeId = getCodeId(uploaded); + } + + // create new instance and compare before and after + const existingContracts = await client.listContractAddresses(); + + const result = await instantiateContract( + client, + wallet, + signer, + codeId, + beneficiaryAddress, + transferAmount, + ); + const myAddress = getContractAddress(result); + + // ensure we were added to the list + const newContracts = await client.listContractAddresses(); + expect(newContracts.length).toEqual(existingContracts.length + 1); + // note: we are NOT guaranteed to be added to the end + const diff = newContracts.filter(x => !existingContracts.includes(x)); + expect(diff.length).toEqual(1); + const lastContract = diff[0]; + expect(lastContract).toEqual(myAddress); + + // check out info + const myInfo = await client.getContractInfo(myAddress); + expect(myInfo.code_id).toEqual(codeId); + expect(myInfo.creator).toEqual(faucetAddress); + expect((myInfo.init_msg as any).beneficiary).toEqual(beneficiaryAddress); + + // make sure random addresses don't give useful info + client + .getContractInfo(beneficiaryAddress) + .then(() => fail("this shouldn't succeed")) + .catch(() => {}); + + // TODO: check code hash matches expectation + // expect(lastInfo.code_hash).toEqual(faucetAddress); + + // TODO: download code and check against auto-gen + }); }); }); From 620767b5c65f3f9db4d94ec372111eefe79764e7 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 16:59:26 +0100 Subject: [PATCH 07/13] Inline getCodeId and getContractAddress helpers --- packages/sdk/src/restclient.spec.ts | 44 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index d8468bef..dd34e7dc 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -207,14 +207,6 @@ describe("RestClient", () => { return client.postTx(marshalTx(signedTx)); } - // getCodeId can be used with the result of uploadContract - function getCodeId(result: PostTxsResponse): number { - expect(result.code).toBeFalsy(); - const logs = parseSuccess(result.raw_log); - const codeIdAttr = findAttribute(logs, "message", "code_id"); - return Number.parseInt(codeIdAttr.value, 10); - } - async function instantiateContract( client: RestClient, wallet: Secp256k1HdWallet, @@ -254,15 +246,6 @@ describe("RestClient", () => { return client.postTx(marshalTx(signedTx)); } - // getContractAddress can be used with the result of instantiateContract - function getContractAddress(result: PostTxsResponse): string { - expect(result.code).toBeFalsy(); - // console.log("Raw log:", result.raw_log); - const logs = parseSuccess(result.raw_log); - const contractAddressAttr = findAttribute(logs, "message", "contract_address"); - return contractAddressAttr.value; - } - async function executeContract( client: RestClient, wallet: Secp256k1HdWallet, @@ -321,7 +304,10 @@ describe("RestClient", () => { { // console.log("Raw log:", result.raw_log); const result = await uploadContract(client, wallet, signer); - codeId = getCodeId(result); + expect(result.code).toBeFalsy(); + const logs = parseSuccess(result.raw_log); + const codeIdAttr = findAttribute(logs, "message", "code_id"); + codeId = Number.parseInt(codeIdAttr.value, 10); expect(codeId).toBeGreaterThanOrEqual(1); expect(codeId).toBeLessThanOrEqual(200); } @@ -338,9 +324,11 @@ describe("RestClient", () => { beneficiaryAddress, transferAmount, ); - contractAddress = getContractAddress(result); - + expect(result.code).toBeFalsy(); + // console.log("Raw log:", result.raw_log); const logs = parseSuccess(result.raw_log); + const contractAddressAttr = findAttribute(logs, "message", "contract_address"); + contractAddress = contractAddressAttr.value; const amountAttr = findAttribute(logs, "transfer", "amount"); expect(amountAttr.value).toEqual("1234ucosm,321ustake"); @@ -375,7 +363,10 @@ describe("RestClient", () => { const numExisting = existingInfos.length; const result = await uploadContract(client, wallet, signer); - const codeId = getCodeId(result); + expect(result.code).toBeFalsy(); + const logs = parseSuccess(result.raw_log); + const codeIdAttr = findAttribute(logs, "message", "code_id"); + const codeId = Number.parseInt(codeIdAttr.value, 10); const newInfos = await client.listCodeInfo(); expect(newInfos.length).toEqual(numExisting + 1); @@ -409,7 +400,10 @@ describe("RestClient", () => { codeId = existingInfos[existingInfos.length - 1].id; } else { const uploaded = await uploadContract(client, wallet, signer); - codeId = getCodeId(uploaded); + expect(uploaded.code).toBeFalsy(); + const logs = parseSuccess(uploaded.raw_log); + const codeIdAttr = findAttribute(logs, "message", "code_id"); + codeId = Number.parseInt(codeIdAttr.value, 10); } // create new instance and compare before and after @@ -423,7 +417,11 @@ describe("RestClient", () => { beneficiaryAddress, transferAmount, ); - const myAddress = getContractAddress(result); + expect(result.code).toBeFalsy(); + // console.log("Raw log:", result.raw_log); + const logs = parseSuccess(result.raw_log); + const contractAddressAttr = findAttribute(logs, "message", "contract_address"); + const myAddress = contractAddressAttr.value; // ensure we were added to the list const newContracts = await client.listContractAddresses(); From 90ac9f1387e380ff43ab181bb714b580cbd1162c Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 17:14:08 +0100 Subject: [PATCH 08/13] All contract queries except smart --- packages/sdk/src/restclient.spec.ts | 46 ++++++++++++++++++++++++----- packages/sdk/src/restclient.ts | 4 +-- packages/sdk/types/restclient.d.ts | 4 +-- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index dd34e7dc..47049f46 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -22,7 +22,7 @@ import { StdTx, } from "./types"; -const { fromBase64, toBase64 } = Encoding; +const { fromBase64, fromHex, toAscii, toBase64, toHex } = Encoding; const httpUrl = "http://localhost:1317"; const defaultNetworkId = "testing"; @@ -380,7 +380,7 @@ describe("RestClient", () => { // TODO: download code and check against auto-gen }); - it("can list contracts and query details", async () => { + it("can list contracts and get info", async () => { pendingWithoutCosmos(); const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic); const signer = await wallet.createIdentity("abc" as ChainId, faucetPath); @@ -401,8 +401,8 @@ describe("RestClient", () => { } else { const uploaded = await uploadContract(client, wallet, signer); expect(uploaded.code).toBeFalsy(); - const logs = parseSuccess(uploaded.raw_log); - const codeIdAttr = findAttribute(logs, "message", "code_id"); + const uploadLogs = parseSuccess(uploaded.raw_log); + const codeIdAttr = findAttribute(uploadLogs, "message", "code_id"); codeId = Number.parseInt(codeIdAttr.value, 10); } @@ -418,7 +418,6 @@ describe("RestClient", () => { transferAmount, ); expect(result.code).toBeFalsy(); - // console.log("Raw log:", result.raw_log); const logs = parseSuccess(result.raw_log); const contractAddressAttr = findAttribute(logs, "message", "contract_address"); const myAddress = contractAddressAttr.value; @@ -443,11 +442,42 @@ describe("RestClient", () => { .getContractInfo(beneficiaryAddress) .then(() => fail("this shouldn't succeed")) .catch(() => {}); + }); - // TODO: check code hash matches expectation - // expect(lastInfo.code_hash).toEqual(faucetAddress); + it("can list query contract state", async () => { + pendingWithoutCosmos(); + const client = new RestClient(httpUrl); + const noContract = makeRandomAddress(); - // TODO: download code and check against auto-gen + // find an existing contract (created above) + // we assume all contracts on this chain are the same (created by these tests) + const contractInfos = await client.listContractAddresses(); + expect(contractInfos.length).toBeGreaterThan(0); + const contractAddress = contractInfos[0]; + + // get contract state + const expectedKey = toAscii("config"); + const state = await client.getAllContractState(contractAddress); + expect(state.length).toEqual(1); + const data = state[0]; + expect(data.key.toLowerCase()).toEqual(toHex(expectedKey)); + + // bad address is empty array + const noContractState = await client.getAllContractState(noContract); + expect(noContractState).toEqual([]); + + // query by one key + const model = await client.queryContractRaw(contractAddress, expectedKey); + expect(model).not.toBeNull(); + expect(model).toEqual(data.val); + + // missing key is null + const missing = await client.queryContractRaw(contractAddress, fromHex("cafe0dad")); + expect(missing).toBeNull(); + + // bad address is null + const noContractModel = await client.queryContractRaw(noContract, expectedKey); + expect(noContractModel).toBeNull(); }); }); }); diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index a2a71e27..22513647 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -271,7 +271,7 @@ export class RestClient { // Returns the data at the key if present (unknown decoded json), // or null if no data at this (contract address, key) pair - public async getContractKey(address: string, key: Uint8Array): Promise { + public async queryContractRaw(address: string, key: Uint8Array): Promise { const hexKey = toHex(key); const path = `/wasm/contract/${address}/raw/${hexKey}?encoding=hex`; const responseData = await this.get(path); @@ -281,7 +281,7 @@ export class RestClient { // Makes a "smart query" on the contract, returns response verbatim (json.RawMessage) // Throws error if no such contract or invalid query format - public async queryContract(address: string, query: object): Promise { + public async queryContractSmart(address: string, query: object): Promise { const encoded = toHex(toUtf8(JSON.stringify(query))); const path = `/wasm/contract/${address}/smart/${encoded}?encoding=hex`; const responseData = (await this.get(path)) as WasmResponse; diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index e9feca7c..d0c8f9d3 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -97,7 +97,7 @@ export declare class RestClient { listContractAddresses(): Promise; getContractInfo(address: string): Promise; getAllContractState(address: string): Promise; - getContractKey(address: string, key: Uint8Array): Promise; - queryContract(address: string, query: object): Promise; + queryContractRaw(address: string, key: Uint8Array): Promise; + queryContractSmart(address: string, query: object): Promise; } export {}; From c7313275c3fb90a85dfae008e3bae9c72a9c52ba Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 17:18:11 +0100 Subject: [PATCH 09/13] Test smart queries --- packages/sdk/src/restclient.spec.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 47049f46..07c0f565 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -479,5 +479,33 @@ describe("RestClient", () => { const noContractModel = await client.queryContractRaw(noContract, expectedKey); expect(noContractModel).toBeNull(); }); + + it("can make smart queries", async () => { + pendingWithoutCosmos(); + const client = new RestClient(httpUrl); + const noContract = makeRandomAddress(); + + // find an existing contract (created above) + // we assume all contracts on this chain are the same (created by these tests) + const contractInfos = await client.listContractAddresses(); + expect(contractInfos.length).toBeGreaterThan(0); + const contractAddress = contractInfos[0]; + + // we can query the verifier properly + const verifier = await client.queryContractSmart(contractAddress, { verifier: {} }); + expect(verifier).toEqual(faucetAddress); + + // invalid query syntax throws an error + client + .queryContractSmart(contractAddress, { no_such_key: {} }) + .then(() => fail("shouldn't succeed")) + .catch(() => {}); + + // invalid address throws an error + client + .queryContractSmart(noContract, { verifier: {} }) + .then(() => fail("shouldn't succeed")) + .catch(() => {}); + }); }); }); From 213d59c75cebede05cb6432ded61957b859c282b Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 17:30:07 +0100 Subject: [PATCH 10/13] Clean up test naming --- packages/sdk/src/restclient.spec.ts | 332 ++++++++++++++-------------- 1 file changed, 172 insertions(+), 160 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 07c0f565..d29cfe68 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -96,6 +96,112 @@ function findAttribute(logs: readonly Log[], eventType: "message" | "transfer", return out; } +async function uploadContract( + client: RestClient, + wallet: Secp256k1HdWallet, + signer: Identity, +): Promise { + const memo = "My first contract on chain"; + const theMsg: MsgStoreCode = { + type: "wasm/store-code", + value: { + sender: faucetAddress, + wasm_byte_code: toBase64(getRandomizedContract()), + source: "https://github.com/confio/cosmwasm/raw/0.7/lib/vm/testdata/contract_0.6.wasm", + builder: "cosmwasm-opt:0.6.2", + }, + }; + const fee: StdFee = { + amount: [ + { + amount: "5000000", + denom: "ucosm", + }, + ], + gas: "89000000", + }; + + const account = (await client.authAccounts(faucetAddress)).result.value; + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; + const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); + const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); + const signedTx = makeSignedTx(theMsg, fee, memo, signature); + return client.postTx(marshalTx(signedTx)); +} + +async function instantiateContract( + client: RestClient, + wallet: Secp256k1HdWallet, + signer: Identity, + codeId: number, + beneficiaryAddress: string, + transferAmount: readonly Coin[], +): Promise { + const memo = "Create an escrow instance"; + const theMsg: MsgInstantiateContract = { + type: "wasm/instantiate", + value: { + sender: faucetAddress, + code_id: codeId.toString(), + init_msg: { + verifier: faucetAddress, + beneficiary: beneficiaryAddress, + }, + init_funds: transferAmount, + }, + }; + const fee: StdFee = { + amount: [ + { + amount: "5000000", + denom: "ucosm", + }, + ], + gas: "89000000", + }; + + const account = (await client.authAccounts(faucetAddress)).result.value; + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; + const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); + const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); + const signedTx = makeSignedTx(theMsg, fee, memo, signature); + return client.postTx(marshalTx(signedTx)); +} + +async function executeContract( + client: RestClient, + wallet: Secp256k1HdWallet, + signer: Identity, + contractAddress: string, +): Promise { + const memo = "Time for action"; + const theMsg: MsgExecuteContract = { + type: "wasm/execute", + value: { + sender: faucetAddress, + contract: contractAddress, + msg: {}, + sent_funds: [], + }, + }; + const fee: StdFee = { + amount: [ + { + amount: "5000000", + denom: "ucosm", + }, + ], + gas: "89000000", + }; + + const account = (await client.authAccounts(faucetAddress)).result.value; + const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; + const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); + const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); + const signedTx = makeSignedTx(theMsg, fee, memo, signature); + return client.postTx(marshalTx(signedTx)); +} + describe("RestClient", () => { it("can be constructed", () => { const client = new RestClient(httpUrl); @@ -174,112 +280,6 @@ describe("RestClient", () => { expect(result.code).toBeFalsy(); }); - async function uploadContract( - client: RestClient, - wallet: Secp256k1HdWallet, - signer: Identity, - ): Promise { - const memo = "My first contract on chain"; - const theMsg: MsgStoreCode = { - type: "wasm/store-code", - value: { - sender: faucetAddress, - wasm_byte_code: toBase64(getRandomizedContract()), - source: "https://github.com/confio/cosmwasm/raw/0.7/lib/vm/testdata/contract_0.6.wasm", - builder: "cosmwasm-opt:0.6.2", - }, - }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - - const account = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; - const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); - const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); - const signedTx = makeSignedTx(theMsg, fee, memo, signature); - return client.postTx(marshalTx(signedTx)); - } - - async function instantiateContract( - client: RestClient, - wallet: Secp256k1HdWallet, - signer: Identity, - codeId: number, - beneficiaryAddress: string, - transferAmount: readonly Coin[], - ): Promise { - const memo = "Create an escrow instance"; - const theMsg: MsgInstantiateContract = { - type: "wasm/instantiate", - value: { - sender: faucetAddress, - code_id: codeId.toString(), - init_msg: { - verifier: faucetAddress, - beneficiary: beneficiaryAddress, - }, - init_funds: transferAmount, - }, - }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - - const account = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; - const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); - const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); - const signedTx = makeSignedTx(theMsg, fee, memo, signature); - return client.postTx(marshalTx(signedTx)); - } - - async function executeContract( - client: RestClient, - wallet: Secp256k1HdWallet, - signer: Identity, - contractAddress: string, - ): Promise { - const memo = "Time for action"; - const theMsg: MsgExecuteContract = { - type: "wasm/execute", - value: { - sender: faucetAddress, - contract: contractAddress, - msg: {}, - sent_funds: [], - }, - }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - - const account = (await client.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes; - const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256); - const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature); - const signedTx = makeSignedTx(theMsg, fee, memo, signature); - return client.postTx(marshalTx(signedTx)); - } - it("can upload, instantiate and execute wasm", async () => { pendingWithoutCosmos(); const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic); @@ -351,23 +351,28 @@ describe("RestClient", () => { expect(contractBalance).toEqual([]); } }, 10_000); + }); - it("can list code and query details", async () => { + describe("query", () => { + it("can list upload code", async () => { pendingWithoutCosmos(); const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic); const signer = await wallet.createIdentity("abc" as ChainId, faucetPath); const client = new RestClient(httpUrl); + // check with contracts were here first to compare const existingInfos = await client.listCodeInfo(); existingInfos.forEach((val, idx) => expect(val.id).toEqual(idx + 1)); const numExisting = existingInfos.length; + // upload data const result = await uploadContract(client, wallet, signer); expect(result.code).toBeFalsy(); const logs = parseSuccess(result.raw_log); const codeIdAttr = findAttribute(logs, "message", "code_id"); const codeId = Number.parseInt(codeIdAttr.value, 10); + // ensure we were added to the end of the list const newInfos = await client.listCodeInfo(); expect(newInfos.length).toEqual(numExisting + 1); const lastInfo = newInfos[newInfos.length - 1]; @@ -444,68 +449,75 @@ describe("RestClient", () => { .catch(() => {}); }); - it("can list query contract state", async () => { - pendingWithoutCosmos(); + describe("contract state", () => { const client = new RestClient(httpUrl); const noContract = makeRandomAddress(); - - // find an existing contract (created above) - // we assume all contracts on this chain are the same (created by these tests) - const contractInfos = await client.listContractAddresses(); - expect(contractInfos.length).toBeGreaterThan(0); - const contractAddress = contractInfos[0]; - - // get contract state const expectedKey = toAscii("config"); - const state = await client.getAllContractState(contractAddress); - expect(state.length).toEqual(1); - const data = state[0]; - expect(data.key.toLowerCase()).toEqual(toHex(expectedKey)); - - // bad address is empty array - const noContractState = await client.getAllContractState(noContract); - expect(noContractState).toEqual([]); - - // query by one key - const model = await client.queryContractRaw(contractAddress, expectedKey); - expect(model).not.toBeNull(); - expect(model).toEqual(data.val); - - // missing key is null - const missing = await client.queryContractRaw(contractAddress, fromHex("cafe0dad")); - expect(missing).toBeNull(); - - // bad address is null - const noContractModel = await client.queryContractRaw(noContract, expectedKey); - expect(noContractModel).toBeNull(); - }); - - it("can make smart queries", async () => { - pendingWithoutCosmos(); - const client = new RestClient(httpUrl); - const noContract = makeRandomAddress(); // find an existing contract (created above) // we assume all contracts on this chain are the same (created by these tests) - const contractInfos = await client.listContractAddresses(); - expect(contractInfos.length).toBeGreaterThan(0); - const contractAddress = contractInfos[0]; + const getContractAddress = async (): Promise => { + const contractInfos = await client.listContractAddresses(); + expect(contractInfos.length).toBeGreaterThan(0); + return contractInfos[0]; + }; - // we can query the verifier properly - const verifier = await client.queryContractSmart(contractAddress, { verifier: {} }); - expect(verifier).toEqual(faucetAddress); + let dataByState: unknown; - // invalid query syntax throws an error - client - .queryContractSmart(contractAddress, { no_such_key: {} }) - .then(() => fail("shouldn't succeed")) - .catch(() => {}); + it("can get all state", async () => { + pendingWithoutCosmos(); + const contractAddress = await getContractAddress(); - // invalid address throws an error - client - .queryContractSmart(noContract, { verifier: {} }) - .then(() => fail("shouldn't succeed")) - .catch(() => {}); + // get contract state + const state = await client.getAllContractState(contractAddress); + expect(state.length).toEqual(1); + const data = state[0]; + expect(data.key.toLowerCase()).toEqual(toHex(expectedKey)); + dataByState = data.val; + + // bad address is empty array + const noContractState = await client.getAllContractState(noContract); + expect(noContractState).toEqual([]); + }); + + it("can query by key", async () => { + pendingWithoutCosmos(); + const contractAddress = await getContractAddress(); + + // query by one key + const model = await client.queryContractRaw(contractAddress, expectedKey); + expect(model).not.toBeNull(); + expect(model).toEqual(dataByState); + + // missing key is null + const missing = await client.queryContractRaw(contractAddress, fromHex("cafe0dad")); + expect(missing).toBeNull(); + + // bad address is null + const noContractModel = await client.queryContractRaw(noContract, expectedKey); + expect(noContractModel).toBeNull(); + }); + + it("can make smart queries", async () => { + pendingWithoutCosmos(); + const contractAddress = await getContractAddress(); + + // we can query the verifier properly + const verifier = await client.queryContractSmart(contractAddress, { verifier: {} }); + expect(verifier).toEqual(faucetAddress); + + // invalid query syntax throws an error + client + .queryContractSmart(contractAddress, { no_such_key: {} }) + .then(() => fail("shouldn't succeed")) + .catch(() => {}); + + // invalid address throws an error + client + .queryContractSmart(noContract, { verifier: {} }) + .then(() => fail("shouldn't succeed")) + .catch(() => {}); + }); }); }); }); From 3e48ee1213d539c8bfac3fa9a51cac94fbe8f958 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 18:02:41 +0100 Subject: [PATCH 11/13] Pin to docker image v0.0.2 --- scripts/cosm/env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cosm/env b/scripts/cosm/env index f2eb21ba..e759ee1e 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="latest" +VERSION="v0.0.2" CONTAINER_NAME="wasmd" From 8699e2e8f76ed1b9b2fd849235bcdd06e4bf9e0f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 18:05:19 +0100 Subject: [PATCH 12/13] Cleanup from PR review --- packages/sdk/src/restclient.spec.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index d29cfe68..5406181f 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -350,7 +350,7 @@ describe("RestClient", () => { const contractBalance = (await client.authAccounts(contractAddress)).result.value.coins; expect(contractBalance).toEqual([]); } - }, 10_000); + }); }); describe("query", () => { @@ -446,7 +446,7 @@ describe("RestClient", () => { client .getContractInfo(beneficiaryAddress) .then(() => fail("this shouldn't succeed")) - .catch(() => {}); + .catch(error => expect(error).toMatch(`No contract with address ${beneficiaryAddress}`)); }); describe("contract state", () => { @@ -462,8 +462,6 @@ describe("RestClient", () => { return contractInfos[0]; }; - let dataByState: unknown; - it("can get all state", async () => { pendingWithoutCosmos(); const contractAddress = await getContractAddress(); @@ -473,7 +471,8 @@ describe("RestClient", () => { expect(state.length).toEqual(1); const data = state[0]; expect(data.key.toLowerCase()).toEqual(toHex(expectedKey)); - dataByState = data.val; + expect((data.val as any).verifier).toBeDefined(); + expect((data.val as any).beneficiary).toBeDefined(); // bad address is empty array const noContractState = await client.getAllContractState(noContract); @@ -487,7 +486,8 @@ describe("RestClient", () => { // query by one key const model = await client.queryContractRaw(contractAddress, expectedKey); expect(model).not.toBeNull(); - expect(model).toEqual(dataByState); + expect((model as any).verifier).toBeDefined(); + expect((model as any).beneficiary).toBeDefined(); // missing key is null const missing = await client.queryContractRaw(contractAddress, fromHex("cafe0dad")); @@ -507,16 +507,22 @@ describe("RestClient", () => { expect(verifier).toEqual(faucetAddress); // invalid query syntax throws an error - client - .queryContractSmart(contractAddress, { no_such_key: {} }) + await client + .queryContractSmart(contractAddress, { nosuchkey: {} }) .then(() => fail("shouldn't succeed")) .catch(() => {}); + // TODO: debug rest server. Here I get: + // Expected Error: Request failed with status code 500 to match 'Parse Error:' + // .catch(error => expect(error).toMatch(`not found: contract`)); // invalid address throws an error - client + await client .queryContractSmart(noContract, { verifier: {} }) .then(() => fail("shouldn't succeed")) .catch(() => {}); + // TODO: debug rest server. Here I get: + // Expected Error: Request failed with status code 500 to match 'Parse Error:' + // .catch(error => expect(error).toMatch(`not found: contract`)); }); }); }); From 49070f668f60efb63452849c39009d764bfebc4d Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Wed, 5 Feb 2020 18:22:20 +0100 Subject: [PATCH 13/13] A bit more cleanup --- packages/sdk/src/restclient.spec.ts | 28 +++++++++++++--------------- packages/sdk/src/restclient.ts | 8 +++++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 5406181f..405b780e 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -443,7 +443,7 @@ describe("RestClient", () => { expect((myInfo.init_msg as any).beneficiary).toEqual(beneficiaryAddress); // make sure random addresses don't give useful info - client + await client .getContractInfo(beneficiaryAddress) .then(() => fail("this shouldn't succeed")) .catch(error => expect(error).toMatch(`No contract with address ${beneficiaryAddress}`)); @@ -507,22 +507,20 @@ describe("RestClient", () => { expect(verifier).toEqual(faucetAddress); // invalid query syntax throws an error - await client - .queryContractSmart(contractAddress, { nosuchkey: {} }) - .then(() => fail("shouldn't succeed")) - .catch(() => {}); - // TODO: debug rest server. Here I get: - // Expected Error: Request failed with status code 500 to match 'Parse Error:' - // .catch(error => expect(error).toMatch(`not found: contract`)); + await client.queryContractSmart(contractAddress, { nosuchkey: {} }).then( + () => fail("shouldn't succeed"), + error => expect(error).toBeTruthy(), + ); + // 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")) - .catch(() => {}); - // TODO: debug rest server. Here I get: - // Expected Error: Request failed with status code 500 to match 'Parse Error:' - // .catch(error => expect(error).toMatch(`not found: contract`)); + await client.queryContractSmart(noContract, { verifier: {} }).then( + () => fail("shouldn't succeed"), + error => expect(error).toBeTruthy(), + ); + // 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 22513647..29e17cc8 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -246,15 +246,17 @@ export class RestClient { public async listContractAddresses(): Promise { const path = `/wasm/contract`; const responseData = await this.get(path); - // answer may be null (empty array) - return parseWasmResponse(responseData as WasmResponse) || []; + // answer may be null (go's encoding of empty array) + const addresses: string[] | null = parseWasmResponse(responseData as WasmResponse); + return addresses || []; } // throws error if no contract at this address public async getContractInfo(address: string): Promise { const path = `/wasm/contract/${address}`; const responseData = await this.get(path); - const info = parseWasmResponse(responseData as WasmResponse); + // 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}`); }