From 6083c3f7ea9c577cce1b0e3083bcf4a189baacec Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 5 Feb 2020 09:10:17 +0100 Subject: [PATCH 1/5] Serach codeIdAttr in first message event --- packages/sdk/src/restclient.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index aa4cd3f9..348e1fb5 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -193,7 +193,9 @@ describe("RestClient", () => { // console.log("Raw log:", result.raw_log); expect(result.code).toBeFalsy(); const [firstLog] = parseSuccess(result.raw_log); - const codeIdAttr = firstLog.events[0].attributes.find(attr => attr.key === "code_id"); + const codeIdAttr = firstLog.events + .find(event => event.type === "message") + ?.attributes.find(attr => attr.key === "code_id"); if (!codeIdAttr) throw new Error("Could not find code_id attribute"); codeId = Number.parseInt(codeIdAttr.value, 10); expect(codeId).toBeGreaterThanOrEqual(1); From 68670fc8a1ca976151cfba1175899abaf4800141 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 5 Feb 2020 09:22:27 +0100 Subject: [PATCH 2/5] Avoid innecessary Value* types; Add docs --- packages/sdk/src/types.ts | 69 +++++++++++++++++++---------------- packages/sdk/types/types.d.ts | 66 ++++++++++++++++++--------------- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 5c3f1d60..23fdca3b 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -27,48 +27,53 @@ interface MsgTemplate { readonly value: object; } -export interface ValueSend { - /** Bech32 account address */ - readonly from_address: string; - /** Bech32 account address */ - readonly to_address: string; - readonly amount: ReadonlyArray; -} - +/** A Cosmos SDK token transfer message */ export interface MsgSend extends MsgTemplate { readonly type: "cosmos-sdk/MsgSend"; - readonly value: ValueSend; -} - -export interface ValueStoreCode { - /** Bech32 account address */ - readonly sender: string; - /** Base64 encoded Wasm */ - readonly wasm_byte_code: string; - /** A valid URI reference to the contract's source code, optional */ - readonly source?: string; - /** A docker tag, optional */ - readonly builder?: string; + readonly value: { + /** Bech32 account address */ + readonly from_address: string; + /** Bech32 account address */ + readonly to_address: string; + readonly amount: ReadonlyArray; + }; } +/** + * Uploads Wam code to the chain + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L17 + */ export interface MsgStoreCode extends MsgTemplate { readonly type: "wasm/store-code"; - readonly value: ValueStoreCode; -} - -export interface ValueInstantiateContract { - /** Bech32 account address */ - readonly sender: string; - /** ID of the Wasm code that was uploaded before */ - readonly code_id: string; - /** Init message as JavaScript object */ - readonly init_msg: object; - readonly init_funds: ReadonlyArray; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** Base64 encoded Wasm */ + readonly wasm_byte_code: string; + /** A valid URI reference to the contract's source code, optional */ + readonly source?: string; + /** A docker tag, optional */ + readonly builder?: string; + }; } +/** + * Creates an instance of contract that was uploaded before. + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L73 + */ export interface MsgInstantiateContract extends MsgTemplate { readonly type: "wasm/instantiate"; - readonly value: ValueInstantiateContract; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** ID of the Wasm code that was uploaded before */ + readonly code_id: string; + /** Init message as JavaScript object */ + readonly init_msg: object; + readonly init_funds: ReadonlyArray; + }; } export type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | MsgTemplate; diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts index 81880b03..cbb86dec 100644 --- a/packages/sdk/types/types.d.ts +++ b/packages/sdk/types/types.d.ts @@ -16,43 +16,51 @@ interface MsgTemplate { readonly type: string; readonly value: object; } -export interface ValueSend { - /** Bech32 account address */ - readonly from_address: string; - /** Bech32 account address */ - readonly to_address: string; - readonly amount: ReadonlyArray; -} +/** A Cosmos SDK token transfer message */ export interface MsgSend extends MsgTemplate { readonly type: "cosmos-sdk/MsgSend"; - readonly value: ValueSend; -} -export interface ValueStoreCode { - /** Bech32 account address */ - readonly sender: string; - /** Base64 encoded Wasm */ - readonly wasm_byte_code: string; - /** A valid URI reference to the contract's source code, optional */ - readonly source?: string; - /** A docker tag, optional */ - readonly builder?: string; + readonly value: { + /** Bech32 account address */ + readonly from_address: string; + /** Bech32 account address */ + readonly to_address: string; + readonly amount: ReadonlyArray; + }; } +/** + * Uploads Wam code to the chain + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L17 + */ export interface MsgStoreCode extends MsgTemplate { readonly type: "wasm/store-code"; - readonly value: ValueStoreCode; -} -export interface ValueInstantiateContract { - /** Bech32 account address */ - readonly sender: string; - /** ID of the Wasm code that was uploaded before */ - readonly code_id: string; - /** Init message as JavaScript object */ - readonly init_msg: object; - readonly init_funds: ReadonlyArray; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** Base64 encoded Wasm */ + readonly wasm_byte_code: string; + /** A valid URI reference to the contract's source code, optional */ + readonly source?: string; + /** A docker tag, optional */ + readonly builder?: string; + }; } +/** + * Creates an instance of contract that was uploaded before. + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L73 + */ export interface MsgInstantiateContract extends MsgTemplate { readonly type: "wasm/instantiate"; - readonly value: ValueInstantiateContract; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** ID of the Wasm code that was uploaded before */ + readonly code_id: string; + /** Init message as JavaScript object */ + readonly init_msg: object; + readonly init_funds: ReadonlyArray; + }; } export declare type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | MsgTemplate; export declare function isMsgSend(msg: Msg): msg is MsgSend; From ce18a5296df64b85baf52c63570e30a454975912 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 5 Feb 2020 09:45:15 +0100 Subject: [PATCH 3/5] Add some fields to PostTxsResponse --- packages/sdk/src/restclient.ts | 6 ++++++ packages/sdk/types/restclient.d.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index b210c873..2e56a4a4 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -64,6 +64,12 @@ interface PostTxsResponse { readonly txhash: string; readonly code?: number; readonly raw_log?: string; + /** The same as `raw_log` but deserialized? */ + readonly logs?: object; + /** The gas limit as set by the user */ + readonly gas_wanted?: string; + /** The gas used by the execution */ + readonly gas_used?: string; } interface EncodeTxResponse { diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index 08f16827..e235d842 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -49,6 +49,12 @@ interface PostTxsResponse { readonly txhash: string; readonly code?: number; readonly raw_log?: string; + /** The same as `raw_log` but deserialized? */ + readonly logs?: object; + /** The gas limit as set by the user */ + readonly gas_wanted?: string; + /** The gas used by the execution */ + readonly gas_used?: string; } interface EncodeTxResponse { readonly tx: string; From e3c294ed2e2c0868624677eb66f04f3d457c5650 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 5 Feb 2020 09:45:45 +0100 Subject: [PATCH 4/5] Implement contract execution --- packages/bcp/src/cosmwasmconnection.ts | 6 +- packages/sdk/src/restclient.spec.ts | 78 +++++++++++++++++++++----- packages/sdk/src/types.ts | 24 +++++++- packages/sdk/types/types.d.ts | 20 ++++++- 4 files changed, 110 insertions(+), 18 deletions(-) diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index 8e02310b..fe4c9f2d 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -311,7 +311,11 @@ export class CosmWasmConnection implements BlockchainConnection { let senderAddress: string; if (types.isMsgSend(firstMsg)) { senderAddress = firstMsg.value.from_address; - } else if (types.isMsgStoreCode(firstMsg) || types.isMsgInstantiateContract(firstMsg)) { + } else if ( + types.isMsgStoreCode(firstMsg) || + types.isMsgInstantiateContract(firstMsg) || + types.isMsgExecuteContract(firstMsg) + ) { senderAddress = firstMsg.value.sender; } else { throw new Error(`Got unsupported type of message: ${firstMsg.type}`); diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 348e1fb5..bfd71c9a 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/camelcase */ import { ChainId, PrehashType, SignableBytes } from "@iov/bcp"; -import { Encoding } from "@iov/encoding"; +import { Random } from "@iov/crypto"; +import { Bech32, Encoding } from "@iov/encoding"; import { HdPaths, Secp256k1HdWallet } from "@iov/keycontrol"; import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; @@ -12,6 +13,7 @@ import cosmoshub from "./testdata/cosmoshub.json"; import { Coin, Msg, + MsgExecuteContract, MsgInstantiateContract, MsgSend, MsgStoreCode, @@ -76,6 +78,10 @@ function getRandomizedContract(): Uint8Array { return data; } +function makeRandomAddress(): string { + return Bech32.encode("cosmos", Random.getBytes(20)); +} + describe("RestClient", () => { it("can be constructed", () => { const client = new RestClient(httpUrl); @@ -154,12 +160,24 @@ describe("RestClient", () => { expect(result.code).toBeFalsy(); }); - it("can upload and instantiate wasm", async () => { + it("can upload, instantiate and execute wasm", async () => { pendingWithoutCosmos(); const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic); const signer = await wallet.createIdentity("abc" as ChainId, faucetPath); const client = new RestClient(httpUrl); + const transferAmount: readonly Coin[] = [ + { + amount: "1234", + denom: "ucosm", + }, + { + amount: "321", + denom: "ustake", + }, + ]; + const beneficiaryAddress = makeRandomAddress(); + let codeId: number; // upload @@ -207,24 +225,14 @@ describe("RestClient", () => { // instantiate { const memo = "Create an escrow instance"; - const transferAmount: readonly Coin[] = [ - { - amount: "1234", - denom: "ucosm", - }, - { - amount: "321", - denom: "ustake", - }, - ]; const theMsg: MsgInstantiateContract = { type: "wasm/instantiate", value: { sender: faucetAddress, code_id: codeId.toString(), init_msg: { - verifier: "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k", - beneficiary: "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k", + verifier: faucetAddress, + beneficiary: beneficiaryAddress, }, init_funds: transferAmount, }, @@ -264,6 +272,46 @@ describe("RestClient", () => { const balance = (await client.authAccounts(contractAddress)).result.value.coins; expect(balance).toEqual(transferAmount); } - }); + + // 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)); + expect(result.code).toBeFalsy(); + // console.log("Raw log:", result.raw_log); + const [firstLog] = parseSuccess(result.raw_log); + expect(firstLog.log).toEqual(`released funds to ${beneficiaryAddress}`); + + // Verify token transfer from contract to beneficiary + const beneficiaryBalance = (await client.authAccounts(beneficiaryAddress)).result.value.coins; + expect(beneficiaryBalance).toEqual(transferAmount); + const contractBalance = (await client.authAccounts(contractAddress)).result.value.coins; + expect(contractBalance).toEqual([]); + } + }, 30_000); }); }); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 23fdca3b..560a2fdd 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -76,7 +76,25 @@ export interface MsgInstantiateContract extends MsgTemplate { }; } -export type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | MsgTemplate; +/** + * Creates an instance of contract that was uploaded before. + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L103 + */ +export interface MsgExecuteContract extends MsgTemplate { + readonly type: "wasm/execute"; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** Bech32 account address */ + readonly contract: string; + /** Handle message as JavaScript object */ + readonly msg: object; + readonly sent_funds: ReadonlyArray; + }; +} + +export type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | MsgExecuteContract | MsgTemplate; export function isMsgSend(msg: Msg): msg is MsgSend { return (msg as MsgSend).type === "cosmos-sdk/MsgSend"; @@ -90,6 +108,10 @@ export function isMsgInstantiateContract(msg: Msg): msg is MsgInstantiateContrac return (msg as MsgInstantiateContract).type === "wasm/instantiate"; } +export function isMsgExecuteContract(msg: Msg): msg is MsgExecuteContract { + return (msg as MsgExecuteContract).type === "wasm/execute"; +} + export interface StdFee { readonly amount: ReadonlyArray; readonly gas: string; diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts index cbb86dec..723db8f6 100644 --- a/packages/sdk/types/types.d.ts +++ b/packages/sdk/types/types.d.ts @@ -62,10 +62,28 @@ export interface MsgInstantiateContract extends MsgTemplate { readonly init_funds: ReadonlyArray; }; } -export declare type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | MsgTemplate; +/** + * Creates an instance of contract that was uploaded before. + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L103 + */ +export interface MsgExecuteContract extends MsgTemplate { + readonly type: "wasm/execute"; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** Bech32 account address */ + readonly contract: string; + /** Handle message as JavaScript object */ + readonly msg: object; + readonly sent_funds: ReadonlyArray; + }; +} +export declare type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | MsgExecuteContract | MsgTemplate; export declare function isMsgSend(msg: Msg): msg is MsgSend; export declare function isMsgStoreCode(msg: Msg): msg is MsgStoreCode; export declare function isMsgInstantiateContract(msg: Msg): msg is MsgInstantiateContract; +export declare function isMsgExecuteContract(msg: Msg): msg is MsgExecuteContract; export interface StdFee { readonly amount: ReadonlyArray; readonly gas: string; From f1a354eb0cbee2f45ec3b54667899f39c9b09c0f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 5 Feb 2020 11:41:54 +0100 Subject: [PATCH 5/5] Extract findAttribute in test code --- packages/sdk/src/restclient.spec.ts | 37 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index bfd71c9a..5c12db17 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -6,7 +6,7 @@ import { HdPaths, Secp256k1HdWallet } from "@iov/keycontrol"; import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; import { leb128Encode } from "./leb128.spec"; -import { Log, parseLogs } from "./logs"; +import { Attribute, Log, parseLogs } from "./logs"; import { RestClient } from "./restclient"; import contract from "./testdata/contract.json"; import cosmoshub from "./testdata/cosmoshub.json"; @@ -82,6 +82,20 @@ function makeRandomAddress(): string { return Bech32.encode("cosmos", Random.getBytes(20)); } +/** Throws if the attribute was not found */ +function findAttribute(logs: readonly Log[], eventType: "message" | "transfer", attrKey: string): Attribute { + const firstLogs = logs.find(() => true); + const out = firstLogs?.events + .find(event => event.type === eventType) + ?.attributes.find(attr => attr.key === attrKey); + if (!out) { + throw new Error( + `Could not find attribute '${attrKey}' in first event of type '${eventType}' in first log.`, + ); + } + return out; +} + describe("RestClient", () => { it("can be constructed", () => { const client = new RestClient(httpUrl); @@ -210,11 +224,8 @@ describe("RestClient", () => { const result = await client.postTx(marshalTx(signedTx)); // console.log("Raw log:", result.raw_log); expect(result.code).toBeFalsy(); - const [firstLog] = parseSuccess(result.raw_log); - const codeIdAttr = firstLog.events - .find(event => event.type === "message") - ?.attributes.find(attr => attr.key === "code_id"); - if (!codeIdAttr) throw new Error("Could not find code_id attribute"); + 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); @@ -255,18 +266,10 @@ describe("RestClient", () => { const result = await client.postTx(marshalTx(signedTx)); expect(result.code).toBeFalsy(); // console.log("Raw log:", result.raw_log); - const [firstLog] = parseSuccess(result.raw_log); - - const amountAttr = firstLog.events - .find(event => event.type === "transfer") - ?.attributes.find(attr => attr.key === "amount"); - if (!amountAttr) throw new Error("Could not find amount attribute"); + const logs = parseSuccess(result.raw_log); + const amountAttr = findAttribute(logs, "transfer", "amount"); expect(amountAttr.value).toEqual("1234ucosm,321ustake"); - - const contractAddressAttr = firstLog.events - .find(event => event.type === "message") - ?.attributes.find(attr => attr.key === "contract_address"); - if (!contractAddressAttr) throw new Error("Could not find contract_address attribute"); + const contractAddressAttr = findAttribute(logs, "message", "contract_address"); contractAddress = contractAddressAttr.value; const balance = (await client.authAccounts(contractAddress)).result.value.coins;