Merge pull request #52 from confio/add-wasm-queries

Add some basic wasm queries
This commit is contained in:
Ethan Frey 2020-02-05 18:25:15 +01:00 committed by GitHub
commit 7320bcec3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 467 additions and 95 deletions

View File

@ -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 {
@ -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";
@ -96,6 +96,112 @@ function findAttribute(logs: readonly Log[], eventType: "message" | "transfer",
return out;
}
async function uploadContract(
client: RestClient,
wallet: Secp256k1HdWallet,
signer: Identity,
): Promise<PostTxsResponse> {
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<PostTxsResponse> {
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<PostTxsResponse> {
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);
@ -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,42 +316,21 @@ 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);
const amountAttr = findAttribute(logs, "transfer", "amount");
expect(amountAttr.value).toEqual("1234ucosm,321ustake");
const contractAddressAttr = findAttribute(logs, "message", "contract_address");
contractAddress = contractAddressAttr.value;
const amountAttr = findAttribute(logs, "transfer", "amount");
expect(amountAttr.value).toEqual("1234ucosm,321ustake");
const balance = (await client.authAccounts(contractAddress)).result.value.coins;
expect(balance).toEqual(transferAmount);
@ -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);
@ -315,6 +350,178 @@ describe("RestClient", () => {
const contractBalance = (await client.authAccounts(contractAddress)).result.value.coins;
expect(contractBalance).toEqual([]);
}
}, 30_000);
});
});
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];
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
});
it("can list contracts and get info", 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);
expect(uploaded.code).toBeFalsy();
const uploadLogs = parseSuccess(uploaded.raw_log);
const codeIdAttr = findAttribute(uploadLogs, "message", "code_id");
codeId = Number.parseInt(codeIdAttr.value, 10);
}
// create new instance and compare before and after
const existingContracts = await client.listContractAddresses();
const result = await instantiateContract(
client,
wallet,
signer,
codeId,
beneficiaryAddress,
transferAmount,
);
expect(result.code).toBeFalsy();
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();
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
await client
.getContractInfo(beneficiaryAddress)
.then(() => fail("this shouldn't succeed"))
.catch(error => expect(error).toMatch(`No contract with address ${beneficiaryAddress}`));
});
describe("contract state", () => {
const client = new RestClient(httpUrl);
const noContract = makeRandomAddress();
const expectedKey = toAscii("config");
// find an existing contract (created above)
// we assume all contracts on this chain are the same (created by these tests)
const getContractAddress = async (): Promise<string> => {
const contractInfos = await client.listContractAddresses();
expect(contractInfos.length).toBeGreaterThan(0);
return contractInfos[0];
};
it("can get all state", async () => {
pendingWithoutCosmos();
const contractAddress = await getContractAddress();
// 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));
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);
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 as any).verifier).toBeDefined();
expect((model as any).beneficiary).toBeDefined();
// 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
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"),
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:'
});
});
});
});

View File

@ -1,9 +1,9 @@
import { Encoding } from "@iov/encoding";
import axios, { AxiosInstance } from "axios";
import { AminoTx, BaseAccount, 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;
@ -41,6 +41,20 @@ 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;
interface WasmSuccess {
readonly height: string;
readonly result: string;
}
interface WasmError {
readonly error: string;
}
export interface TxsResponse {
readonly height: string;
readonly txhash: string;
@ -59,7 +73,7 @@ interface SearchTxsResponse {
interface PostTxsParams {}
interface PostTxsResponse {
export interface PostTxsResponse {
readonly height: string;
readonly txhash: string;
readonly code?: number;
@ -84,10 +98,22 @@ type RestClientResponse =
| TxsResponse
| SearchTxsResponse
| PostTxsResponse
| EncodeTxResponse;
| EncodeTxResponse
| WasmResponse;
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
@ -198,4 +224,73 @@ 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<readonly CodeInfo[]> {
const path = `/wasm/code`;
const responseData = await this.get(path);
// answer may be null (empty array)
return parseWasmResponse(responseData as WasmResponse) || [];
}
// this will download the original wasm bytecode by code id
// throws error if no code with this id
public async getCode(id: number): Promise<Uint8Array> {
// TODO: broken currently
const path = `/wasm/code/${id}`;
const responseData = await this.get(path);
const { code } = parseWasmResponse(responseData as WasmResponse);
return fromBase64(code);
}
public async listContractAddresses(): Promise<readonly string[]> {
const path = `/wasm/contract`;
const responseData = await this.get(path);
// 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<ContractInfo> {
const path = `/wasm/contract/${address}`;
const responseData = await this.get(path);
// 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}`);
}
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<readonly WasmData[]> {
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 queryContractRaw(address: string, key: Uint8Array): Promise<unknown | null> {
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 queryContractSmart(address: string, query: object): Promise<unknown> {
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;
}
}

View File

@ -162,3 +162,34 @@ export interface BaseAccount {
/** The data we need from BaseAccount to create a nonce */
export type NonceInfo = Pick<BaseAccount, "account_number" | "sequence">;
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 CodeDetails {
// TODO: this should be base64 encoded string with content - not in current stack
readonly code: string;
}
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 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;
}

View File

@ -1,4 +1,4 @@
import { AminoTx, BaseAccount, StdTx } from "./types";
import { AminoTx, BaseAccount, CodeInfo, ContractInfo, StdTx, WasmData } from "./types";
interface NodeInfo {
readonly network: string;
}
@ -29,6 +29,14 @@ interface AuthAccountsResponse {
readonly value: BaseAccount;
};
}
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;
@ -44,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;
@ -66,7 +74,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 +92,12 @@ export declare class RestClient {
txs(query: string): Promise<SearchTxsResponse>;
txsById(id: string): Promise<TxsResponse>;
postTx(tx: Uint8Array): Promise<PostTxsResponse>;
listCodeInfo(): Promise<readonly CodeInfo[]>;
getCode(id: number): Promise<Uint8Array>;
listContractAddresses(): Promise<readonly string[]>;
getContractInfo(address: string): Promise<ContractInfo>;
getAllContractState(address: string): Promise<readonly WasmData[]>;
queryContractRaw(address: string, key: Uint8Array): Promise<unknown | null>;
queryContractSmart(address: string, query: object): Promise<unknown>;
}
export {};

View File

@ -120,4 +120,27 @@ export interface BaseAccount {
}
/** The data we need from BaseAccount to create a nonce */
export declare type NonceInfo = Pick<BaseAccount, "account_number" | "sequence">;
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;
readonly source?: string;
readonly builder?: string;
}
export interface CodeDetails {
readonly code: string;
}
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 interface WasmData {
readonly key: string;
readonly val: unknown;
}
export {};

View File

@ -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"