Merge pull request #126 from confio/test-0.7-simon2

A lot of 0.7.0 adaptions
This commit is contained in:
Simon Warta 2020-02-28 14:40:07 +01:00 committed by GitHub
commit f8c71cf471
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 169 additions and 81 deletions

View File

@ -1,5 +1,5 @@
{
"version": "0.0.8",
"version": "0.7.0-alpha.2",
"useWorkspaces": true,
"npmClient": "yarn"
}

View File

@ -1,6 +1,6 @@
{
"name": "@cosmwasm/bcp",
"version": "0.0.8",
"version": "0.7.0-alpha.2",
"description": "Transaction codec and client to communicate with any wasmd blockchain",
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
"license": "Apache-2.0",
@ -38,7 +38,7 @@
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
},
"dependencies": {
"@cosmwasm/sdk": "^0.0.8",
"@cosmwasm/sdk": "^0.7.0-alpha.2",
"@iov/bcp": "^2.1.0",
"@iov/crypto": "^2.1.0",
"@iov/encoding": "^2.1.0",

View File

@ -425,7 +425,7 @@ describe("CosmWasmConnection", () => {
assert(isConfirmedTransaction(getResponse), "Expected transaction to succeed");
assert(getResponse.log, "Log must be available");
const [firstLog] = JSON.parse(getResponse.log);
expect(firstLog.events.length).toEqual(1);
expect(firstLog.events.length).toEqual(2);
const { transaction, signatures } = getResponse;
assert(isSendTransaction(transaction), "Expected send transaction");

View File

@ -1,6 +1,6 @@
{
"name": "@cosmwasm/cli",
"version": "0.0.8",
"version": "0.7.0-alpha.2",
"description": "Command line interface",
"contributors": [
"IOV SAS <admin@iov.one>",
@ -36,7 +36,7 @@
"!**/testdata/"
],
"dependencies": {
"@cosmwasm/sdk": "^0.0.8",
"@cosmwasm/sdk": "^0.7.0-alpha.2",
"@iov/crypto": "^2.1.0",
"@iov/encoding": "^2.1.0",
"@iov/utils": "^2.0.2",

View File

@ -1,6 +1,6 @@
{
"name": "@cosmwasm/faucet",
"version": "0.0.8",
"version": "0.7.0-alpha.2",
"description": "The faucet",
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
"license": "Apache-2.0",
@ -35,10 +35,10 @@
"test": "yarn build-or-skip && yarn test-node"
},
"dependencies": {
"@cosmwasm/bcp": "^0.0.8",
"@cosmwasm/bcp": "^0.7.0-alpha.2",
"@iov/bcp": "^2.1.0",
"@iov/crypto": "^2.1.0",
"@iov/encoding":"^2.1.0",
"@iov/encoding": "^2.1.0",
"@iov/keycontrol": "^2.1.0",
"@iov/utils": "^2.0.2",
"@koa/cors": "^3.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "@cosmwasm/sdk",
"version": "0.0.8",
"version": "0.7.0-alpha.2",
"description": "CosmWasm SDK",
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
"license": "Apache-2.0",

View File

@ -4,7 +4,7 @@ import { Bech32, Encoding } from "@iov/encoding";
import { assert, sleep } from "@iov/utils";
import { ReadonlyDate } from "readonly-date";
import { CosmWasmClient } from "./cosmwasmclient";
import { Code, CosmWasmClient } from "./cosmwasmclient";
import { makeSignBytes } from "./encoding";
import { findAttribute } from "./logs";
import { Secp256k1Pen } from "./pen";
@ -20,7 +20,7 @@ import {
} from "./testutils.spec";
import { CosmosSdkTx, MsgSend, StdFee } from "./types";
const { fromAscii, fromHex, fromUtf8, toAscii } = Encoding;
const { fromAscii, fromHex, fromUtf8, toAscii, toBase64 } = Encoding;
const httpUrl = "http://localhost:1317";
@ -419,8 +419,19 @@ describe("CosmWasmClient", () => {
pendingWithoutWasmd();
const client = new CosmWasmClient(httpUrl);
const result = await client.getCodeDetails(1);
const checksum = new Sha256(result.wasm).digest();
expect(checksum).toEqual(fromHex("aff8c8873d79d2153a8b9066a0683fec3c903669267eb806ffa831dcd4b3daae"));
const expectedInfo: Code = {
id: 1,
checksum: "aff8c8873d79d2153a8b9066a0683fec3c903669267eb806ffa831dcd4b3daae",
source: undefined,
builder: undefined,
creator: faucet.address,
};
// check info
expect(result).toEqual(jasmine.objectContaining(expectedInfo));
// check data
expect(new Sha256(result.data).digest()).toEqual(fromHex(expectedInfo.checksum));
});
});
@ -430,21 +441,24 @@ describe("CosmWasmClient", () => {
const client = new CosmWasmClient(httpUrl);
const result = await client.getContracts(1);
expect(result.length).toBeGreaterThanOrEqual(3);
const [jade, hash, isa] = result;
const [hash, isa, jade] = result;
expect(hash).toEqual({
address: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5",
codeId: 1,
creator: faucet.address,
label: "HASH",
});
expect(isa).toEqual({
address: "cosmos1hqrdl6wstt8qzshwc6mrumpjk9338k0lr4dqxd",
codeId: 1,
creator: faucet.address,
label: "ISA",
});
expect(jade).toEqual({
address: "cosmos18r5szma8hm93pvx6lwpjwyxruw27e0k5uw835c",
codeId: 1,
creator: faucet.address,
label: "JADE",
});
});
});
@ -458,6 +472,7 @@ describe("CosmWasmClient", () => {
address: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5",
codeId: 1,
creator: faucet.address,
label: "HASH",
initMsg: {
decimals: 5,
name: "Hash token",
@ -506,9 +521,9 @@ describe("CosmWasmClient", () => {
const raw = await client.queryContractRaw(contract.address, configKey);
assert(raw, "must get result");
expect(JSON.parse(fromUtf8(raw))).toEqual({
verifier: Array.from(Bech32.decode(contract.initMsg.verifier).data),
beneficiary: Array.from(Bech32.decode(contract.initMsg.beneficiary).data),
funder: Array.from(Bech32.decode(faucet.address).data),
verifier: toBase64(Bech32.decode(contract.initMsg.verifier).data),
beneficiary: toBase64(Bech32.decode(contract.initMsg.beneficiary).data),
funder: toBase64(Bech32.decode(faucet.address).data),
});
});

View File

@ -58,9 +58,9 @@ export interface Code {
readonly builder?: string;
}
export interface CodeDetails {
export interface CodeDetails extends Code {
/** The original wasm bytes */
readonly wasm: Uint8Array;
readonly data: Uint8Array;
}
export interface Contract {
@ -68,6 +68,7 @@ export interface Contract {
readonly codeId: number;
/** Bech32 account address */
readonly creator: string;
readonly label: string;
}
export interface ContractDetails extends Contract {
@ -195,37 +196,53 @@ export class CosmWasmClient {
public async getCodes(): Promise<readonly Code[]> {
const result = await this.restClient.listCodeInfo();
return result.map(r => ({
id: r.id,
creator: r.creator,
checksum: Encoding.toHex(Encoding.fromHex(r.code_hash)),
source: r.source || undefined,
builder: r.builder || undefined,
}));
return result.map(
(entry): Code => ({
id: entry.id,
creator: entry.creator,
checksum: Encoding.toHex(Encoding.fromHex(entry.code_hash)),
source: entry.source || undefined,
builder: entry.builder || undefined,
}),
);
}
public async getCodeDetails(codeId: number): Promise<CodeDetails> {
const result = await this.restClient.getCode(codeId);
// TODO: implement as one request when https://github.com/cosmwasm/wasmd/issues/90 is done
const [codeInfos, getCodeResult] = await Promise.all([this.getCodes(), this.restClient.getCode(codeId)]);
const codeInfo = codeInfos.find(code => code.id === codeId);
if (!codeInfo) throw new Error("No code info found");
return {
wasm: result,
...codeInfo,
data: getCodeResult,
};
}
public async getContracts(codeId: number): Promise<readonly Contract[]> {
const result = await this.restClient.listContractsByCodeId(codeId);
return result.map(entry => ({
address: entry.address,
codeId: entry.code_id,
creator: entry.creator,
}));
return result.map(
(entry): Contract => ({
address: entry.address,
codeId: entry.code_id,
creator: entry.creator,
label: entry.label,
}),
);
}
/**
* Throws an error if no contract was found at the address
*/
public async getContract(address: string): Promise<ContractDetails> {
const result = await this.restClient.getContractInfo(address);
if (!result) throw new Error(`No contract found at address "${address}"`);
return {
address: result.address,
codeId: result.code_id,
creator: result.creator,
label: result.label,
initMsg: result.init_msg,
};
}
@ -238,7 +255,7 @@ export class CosmWasmClient {
*/
public async queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null> {
// just test contract existence
const _info = await this.restClient.getContractInfo(address);
const _info = await this.getContract(address);
return this.restClient.queryContractRaw(address, key);
}
@ -254,7 +271,7 @@ export class CosmWasmClient {
return await this.restClient.queryContractSmart(address, queryMsg);
} catch (error) {
if (error instanceof Error) {
if (error.message === "not found: contract") {
if (error.message.startsWith("not found: contract")) {
throw new Error(`No contract found at address "${address}"`);
} else {
throw error;

View File

@ -1,13 +1,22 @@
/* eslint-disable @typescript-eslint/camelcase */
import { isNonNullObject } from "@iov/encoding";
const supportedEventTypes: readonly string[] = ["message", "transfer", "wasm"];
export type SupportedEventType = "message" | "transfer" | "wasm";
export function isSupportedEventType(data: any): data is SupportedEventType {
if (typeof data !== "string") return false;
return supportedEventTypes.includes(data);
}
export interface Attribute {
readonly key: string;
readonly value: string;
}
export interface Event {
readonly type: "message" | "transfer";
readonly type: SupportedEventType;
readonly attributes: readonly Attribute[];
}
@ -34,7 +43,9 @@ export function parseAttribute(input: unknown): Attribute {
export function parseEvent(input: unknown): Event {
if (!isNonNullObject(input)) throw new Error("Event must be a non-null object");
const { type, attributes } = input as any;
if (type !== "message" && type !== "transfer") throw new Error("Event must be of type message or transfer");
if (!isSupportedEventType(type)) {
throw new Error(`Event type must be one of ${supportedEventTypes.join(", ")}; got ${type}`);
}
if (!Array.isArray(attributes)) throw new Error("Event's attributes must be an array");
return {
type: type,

View File

@ -12,6 +12,7 @@ import { PostTxsResponse, RestClient, TxsResponse } from "./restclient";
import { SigningCosmWasmClient } from "./signingcosmwasmclient";
import cosmoshub from "./testdata/cosmoshub.json";
import {
bech32AddressMatcher,
getRandomizedHackatom,
makeRandomAddress,
pendingWithoutWasmd,
@ -144,7 +145,7 @@ async function executeContract(
value: {
sender: faucet.address,
contract: contractAddress,
msg: {},
msg: { release: {} },
sent_funds: [],
},
};
@ -581,9 +582,15 @@ describe("RestClient", () => {
{
const result = await executeContract(client, pen, contractAddress);
expect(result.code).toBeFalsy();
// console.log("Raw log:", result.raw_log);
const [firstLog] = parseLogs(result.logs);
expect(firstLog.log).toEqual(`released funds to ${beneficiaryAddress}`);
// console.log("Raw log:", result.logs);
const logs = parseLogs(result.logs);
const wasmEvent = logs.find(() => true)?.events.find(e => e.type === "wasm");
assert(wasmEvent, "Event of type wasm expected");
expect(wasmEvent.attributes).toContain({ key: "action", value: "release" });
expect(wasmEvent.attributes).toContain({
key: "destination",
value: beneficiaryAddress,
});
// Verify token transfer from contract to beneficiary
const beneficiaryBalance = (await client.authAccounts(beneficiaryAddress)).result.value.coins;
@ -664,7 +671,12 @@ describe("RestClient", () => {
// create new instance and compare before and after
const existingContractsByCode = await client.listContractsByCodeId(codeId);
existingContractsByCode.forEach(ctc => expect(ctc.code_id).toEqual(codeId));
for (const contract of existingContractsByCode) {
expect(contract.address).toMatch(bech32AddressMatcher);
expect(contract.code_id).toEqual(codeId);
expect(contract.creator).toMatch(bech32AddressMatcher);
expect(contract.label).toMatch(/^.+$/);
}
const result = await instantiateContract(client, pen, codeId, beneficiaryAddress, transferAmount);
expect(result.code).toBeFalsy();
@ -673,21 +685,26 @@ describe("RestClient", () => {
const myAddress = contractAddressAttr.value;
const newContractsByCode = await client.listContractsByCodeId(codeId);
newContractsByCode.forEach(ctc => expect(ctc.code_id).toEqual(codeId));
expect(newContractsByCode.length).toEqual(existingContractsByCode.length + 1);
const newContract = newContractsByCode[newContractsByCode.length - 1];
expect(newContract).toEqual(
jasmine.objectContaining({
code_id: codeId,
creator: faucet.address,
label: "my escrow",
}),
);
// check out info
const myInfo = await client.getContractInfo(myAddress);
assert(myInfo);
expect(myInfo.code_id).toEqual(codeId);
expect(myInfo.creator).toEqual(faucet.address);
expect((myInfo.init_msg as any).beneficiary).toEqual(beneficiaryAddress);
// make sure random addresses don't give useful info
const nonExistentAddress = makeRandomAddress();
await client
.getContractInfo(nonExistentAddress)
.then(() => fail("this shouldn't succeed"))
.catch(error => expect(error).toMatch(`No contract found at address "${nonExistentAddress}"`));
expect(await client.getContractInfo(nonExistentAddress)).toBeNull();
});
describe("contract state", () => {

View File

@ -139,6 +139,7 @@ export interface ContractInfo {
readonly code_id: number;
/** Bech32 account address */
readonly creator: string;
readonly label: string;
}
export interface ContractDetails extends ContractInfo {
@ -195,28 +196,23 @@ function unwrapWasmResponse<T>(response: WasmResponse<T>): T {
return response.result;
}
function parseWasmResponse(response: WasmResponse<string>): any {
if (isWasmError(response)) {
throw new Error(response.error);
}
return JSON.parse(response.result);
}
// We want to get message data from 500 errors
// https://stackoverflow.com/questions/56577124/how-to-handle-500-error-message-with-axios
// this should be chained to catch one error and throw a more informative one
function parseAxios500error(err: AxiosError): never {
function parseAxiosError(err: AxiosError): never {
// use the error message sent from server, not default 500 msg
if (err.response?.data) {
let errorText: string;
const data = err.response.data;
// expect { error: string }, but otherwise dump
if (data.error) {
throw new Error(data.error);
if (data.error && typeof data.error === "string") {
errorText = data.error;
} else if (typeof data === "string") {
throw new Error(data);
errorText = data;
} else {
throw new Error(JSON.stringify(data));
errorText = JSON.stringify(data);
}
throw new Error(`${errorText} (HTTP ${err.response.status})`);
} else {
throw err;
}
@ -238,7 +234,7 @@ export class RestClient {
}
public async get(path: string): Promise<RestClientResponse> {
const { data } = await this.client.get(path).catch(parseAxios500error);
const { data } = await this.client.get(path).catch(parseAxiosError);
if (data === null) {
throw new Error("Received null response from server");
}
@ -246,7 +242,7 @@ export class RestClient {
}
public async post(path: string, params: PostTxsParams): Promise<RestClientResponse> {
const { data } = await this.client.post(path, params).catch(parseAxios500error);
const { data } = await this.client.post(path, params).catch(parseAxiosError);
if (data === null) {
throw new Error("Received null response from server");
}
@ -362,11 +358,26 @@ export class RestClient {
return unwrapWasmResponse(responseData) || [];
}
// throws error if no contract at this address
public async getContractInfo(address: string): Promise<ContractDetails> {
/**
* Returns null when contract was not found at this address.
*/
public async getContractInfo(address: string): Promise<ContractDetails | null> {
const path = `/wasm/contract/${address}`;
const responseData = (await this.get(path)) as WasmResponse<ContractDetails>;
return unwrapWasmResponse(responseData);
try {
const response = (await this.get(path)) as WasmResponse<ContractDetails>;
return unwrapWasmResponse(response);
} catch (error) {
if (error instanceof Error) {
if (error.message.startsWith("unknown address:")) {
return null;
} else {
throw error;
}
} else {
throw error;
}
}
}
// Returns all contract state.

View File

@ -143,9 +143,14 @@ describe("SigningCosmWasmClient", () => {
);
// execute
const result = await client.execute(contractAddress, {release:{}}, undefined);
const [firstLog] = result.logs;
expect(firstLog.log).toEqual(`released funds to ${beneficiaryAddress}`);
const result = await client.execute(contractAddress, { release: {} }, undefined);
const wasmEvent = result.logs.find(() => true)?.events.find(e => e.type === "wasm");
assert(wasmEvent, "Event of type wasm expected");
expect(wasmEvent.attributes).toContain({ key: "action", value: "release" });
expect(wasmEvent.attributes).toContain({
key: "destination",
value: beneficiaryAddress,
});
// Verify token transfer from contract to beneficiary
const rest = new RestClient(httpUrl);

View File

@ -27,19 +27,18 @@ export function leb128Encode(uint: number): Uint8Array {
export function getRandomizedHackatom(): Uint8Array {
const data = Encoding.fromBase64(hackatom.data);
// TODO: this needs to be redone!
// The return value of the export function cosmwasm_api_0_6 is unused and
// can be randomized for testing.
//
// Find position of mutable bytes as follows:
// $ wasm-objdump -d contract.wasm | grep -F "cosmwasm_api_0_6" -A 1
// 00e67c func[149] <cosmwasm_api_0_6>:
// 00e67d: 41 83 0c | i32.const 1539
// 0136d2 func[198] <cosmwasm_api_0_6>:
// 0136d3: 41 83 0c | i32.const 1539
//
// In the last line, the addresses 00e67d-00e67f hold a one byte instruction
// (https://github.com/WebAssembly/design/blob/master/BinaryEncoding.md#constants-described-here)
// and a two byte value (leb128 encoded 1539)
// In the last line, the addresses [0136d3, 0136d3+1, 0136d3+2] hold a one byte instruction
// and a two byte value (leb128 encoded 1539). See also
// https://github.com/WebAssembly/design/blob/master/BinaryEncoding.md#constants-described-here.
// Any unsigned integer from 128 to 16383 is encoded to two leb128 bytes
const min = 128;
@ -47,8 +46,8 @@ export function getRandomizedHackatom(): Uint8Array {
const random = Math.floor(Math.random() * (max - min)) + min;
const bytes = leb128Encode(random);
data[0x00e67d + 1] = bytes[0];
data[0x00e67d + 2] = bytes[1];
data[0x0136d3 + 1] = bytes[0];
data[0x0136d3 + 2] = bytes[1];
return data;
}
@ -61,6 +60,9 @@ export const tendermintIdMatcher = /^[0-9A-F]{64}$/;
export const tendermintOptionalIdMatcher = /^([0-9A-F]{64}|)$/;
export const tendermintAddressMatcher = /^[0-9A-F]{40}$/;
// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32
export const bech32AddressMatcher = /^[\x21-\x7e]{1,83}1[02-9ac-hj-np-z]{38}$/;
export function wasmdEnabled(): boolean {
return !!process.env.WASMD_ENABLED;
}

View File

@ -34,15 +34,16 @@ export interface Code {
readonly source?: string;
readonly builder?: string;
}
export interface CodeDetails {
export interface CodeDetails extends Code {
/** The original wasm bytes */
readonly wasm: Uint8Array;
readonly data: Uint8Array;
}
export interface Contract {
readonly address: string;
readonly codeId: number;
/** Bech32 account address */
readonly creator: string;
readonly label: string;
}
export interface ContractDetails extends Contract {
/** Argument passed on initialization of the contract */
@ -76,6 +77,9 @@ export declare class CosmWasmClient {
getCodes(): Promise<readonly Code[]>;
getCodeDetails(codeId: number): Promise<CodeDetails>;
getContracts(codeId: number): Promise<readonly Contract[]>;
/**
* Throws an error if no contract was found at the address
*/
getContract(address: string): Promise<ContractDetails>;
/**
* Returns the data at the key if present (raw contract dependent storage data)

View File

@ -1,9 +1,11 @@
export declare type SupportedEventType = "message" | "transfer" | "wasm";
export declare function isSupportedEventType(data: any): data is SupportedEventType;
export interface Attribute {
readonly key: string;
readonly value: string;
}
export interface Event {
readonly type: "message" | "transfer";
readonly type: SupportedEventType;
readonly attributes: readonly Attribute[];
}
export interface Log {

View File

@ -106,6 +106,7 @@ export interface ContractInfo {
readonly code_id: number;
/** Bech32 account address */
readonly creator: string;
readonly label: string;
}
export interface ContractDetails extends ContractInfo {
/** Argument passed on initialization of the contract */
@ -165,7 +166,10 @@ export declare class RestClient {
listCodeInfo(): Promise<readonly CodeInfo[]>;
getCode(id: number): Promise<Uint8Array>;
listContractsByCodeId(id: number): Promise<readonly ContractInfo[]>;
getContractInfo(address: string): Promise<ContractDetails>;
/**
* Returns null when contract was not found at this address.
*/
getContractInfo(address: string): Promise<ContractDetails | null>;
getAllContractState(address: string): Promise<readonly Model[]>;
queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null>;
queryContractSmart(address: string, query: object): Promise<Uint8Array>;