diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index a1f1d56d..8e02310b 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -311,7 +311,7 @@ export class CosmWasmConnection implements BlockchainConnection { let senderAddress: string; if (types.isMsgSend(firstMsg)) { senderAddress = firstMsg.value.from_address; - } else if (types.isMsgStoreCode(firstMsg)) { + } else if (types.isMsgStoreCode(firstMsg) || types.isMsgInstantiateContract(firstMsg)) { senderAddress = firstMsg.value.sender; } else { throw new Error(`Got unsupported type of message: ${firstMsg.type}`); diff --git a/packages/sdk/src/logs.spec.ts b/packages/sdk/src/logs.spec.ts index a34efd66..213d36b7 100644 --- a/packages/sdk/src/logs.spec.ts +++ b/packages/sdk/src/logs.spec.ts @@ -7,6 +7,11 @@ describe("logs", () => { const attr = parseAttribute({ key: "a", value: "b" }); expect(attr).toEqual({ key: "a", value: "b" }); }); + + it("works for unset value", () => { + const attr = parseAttribute({ key: "amount" }); + expect(attr).toEqual({ key: "amount", value: undefined }); + }); }); describe("parseEvent", () => { @@ -40,6 +45,37 @@ describe("logs", () => { const event = parseEvent(original); expect(event).toEqual(original); }); + + it("works for transfer event", () => { + const original = { + type: "transfer", + attributes: [ + { + key: "recipient", + value: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", + }, + { + key: "amount", + }, + ], + } as const; + const expected = { + type: "transfer", + attributes: [ + { + key: "recipient", + value: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", + }, + { + key: "amount", + value: undefined, + }, + ], + } as const; + + const event = parseEvent(original); + expect(event).toEqual(expected); + }); }); describe("parseLog", () => { diff --git a/packages/sdk/src/logs.ts b/packages/sdk/src/logs.ts index 4c822dc4..baf62b9d 100644 --- a/packages/sdk/src/logs.ts +++ b/packages/sdk/src/logs.ts @@ -3,11 +3,11 @@ import { isNonNullObject } from "@iov/encoding"; export interface Attribute { readonly key: string; - readonly value: string; + readonly value: string | undefined; } export interface Event { - readonly type: "message"; + readonly type: "message" | "transfer"; readonly attributes: readonly Attribute[]; } @@ -20,9 +20,11 @@ export interface Log { export function parseAttribute(input: unknown): Attribute { if (!isNonNullObject(input)) throw new Error("Attribute must be a non-null object"); const { key, value } = input as any; - if (typeof key !== "string" || typeof value !== "string") { - throw new Error("Attribute is not a key/value pair"); + if (typeof key !== "string" || !key) throw new Error("Attribute's key must be a non-empty string"); + if (typeof value !== "string" && typeof value !== "undefined") { + throw new Error("Attribute's value must be a string or unset"); } + return { key: key, value: value, @@ -32,7 +34,7 @@ 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") throw new Error("Event must be of type message"); + if (type !== "message" && type !== "transfer") throw new Error("Event must be of type message or transfer"); if (!Array.isArray(attributes)) throw new Error("Event's attributes must be an array"); return { type: type, diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index 5b516e7c..ea257513 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -8,7 +8,7 @@ import { Log, parseLogs } from "./logs"; import { RestClient } from "./restclient"; import contract from "./testdata/contract.json"; import data from "./testdata/cosmoshub.json"; -import { MsgSend, MsgStoreCode, StdFee, StdTx } from "./types"; +import { MsgInstantiateContract, MsgSend, MsgStoreCode, StdFee, StdTx } from "./types"; const { fromBase64 } = Encoding; @@ -117,51 +117,126 @@ describe("RestClient", () => { expect(result.code).toBeFalsy(); }); - it("can upload wasm", async () => { + it("can upload and instantiate wasm", async () => { pendingWithoutCosmos(); const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic); const signer = await wallet.createIdentity("abc" as ChainId, faucetPath); + const client = new RestClient(httpUrl); - const memo = "My first contract on chain"; - const theMsg: MsgStoreCode = { - type: "wasm/store-code", - value: { - sender: faucetAddress, - wasm_byte_code: contract.data, - 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: [ + // upload + { + const memo = "My first contract on chain"; + const theMsg: MsgStoreCode = { + type: "wasm/store-code", + value: { + sender: faucetAddress, + wasm_byte_code: contract.data, + 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: StdTx = { + msg: [theMsg], + fee: fee, + memo: memo, + signatures: [signature], + }; + + const postableBytes = marshalTx(signedTx); + const result = await client.postTx(postableBytes); + // 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"); + expect(codeIdAttr).toEqual({ key: "code_id", value: "1" }); + } + + let contractAddress: string; + + // instantiate + { + const memo = "Create an escrow instance"; + const theMsg: MsgInstantiateContract = { + type: "wasm/instantiate", + value: { + sender: faucetAddress, + code_id: "1", + init_msg: { + verifier: "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k", + beneficiary: "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k", + }, + init_funds: [ + { + amount: "1234", + denom: "ucosm", + }, + { + amount: "321", + denom: "ustake", + }, + ], + }, + }; + 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: StdTx = { + msg: [theMsg], + fee: fee, + memo: memo, + signatures: [signature], + }; + + const postableBytes = marshalTx(signedTx); + const result = await client.postTx(postableBytes); + expect(result.code).toBeFalsy(); + // console.log("Raw log:", result.raw_log); + const [firstLog] = parseSuccess(result.raw_log); + const contractAddressAttr = firstLog.events[0].attributes.find( + attr => attr.key === "contract_address", + ); + if (!contractAddressAttr) throw new Error("Could not find contract_address attribute"); + contractAddress = contractAddressAttr.value || ""; + + const balance = (await client.authAccounts(contractAddress)).result.value.coins; + expect(balance).toEqual([ { - amount: "5000000", + amount: "1234", denom: "ucosm", }, - ], - gas: "89000000", - }; - - const client = new RestClient(httpUrl); - 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: StdTx = { - msg: [theMsg], - fee: fee, - memo: memo, - signatures: [signature], - }; - - const postableBytes = marshalTx(signedTx); - const result = await client.postTx(postableBytes); - // 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"); - expect(codeIdAttr).toEqual({ key: "code_id", value: "1" }); + { + amount: "321", + denom: "ustake", + }, + ]); + } }); }); }); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 243088b4..1404e90d 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -56,7 +56,22 @@ export interface MsgStoreCode extends MsgTemplate { readonly value: ValueStoreCode; } -export type Msg = MsgSend | MsgStoreCode | MsgTemplate; +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; +} + +export interface MsgInstantiateContract extends MsgTemplate { + readonly type: "wasm/instantiate"; + readonly value: ValueInstantiateContract; +} + +export type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | MsgTemplate; export function isMsgSend(msg: Msg): msg is MsgSend { return (msg as MsgSend).type === "cosmos-sdk/MsgSend"; @@ -66,6 +81,10 @@ export function isMsgStoreCode(msg: Msg): msg is MsgStoreCode { return (msg as MsgStoreCode).type === "wasm/store-code"; } +export function isMsgInstantiateContract(msg: Msg): msg is MsgInstantiateContract { + return (msg as MsgInstantiateContract).type === "wasm/instantiate"; +} + export interface StdFee { readonly amount: ReadonlyArray; readonly gas: string; diff --git a/packages/sdk/types/logs.d.ts b/packages/sdk/types/logs.d.ts index 159ce322..cbdeef7f 100644 --- a/packages/sdk/types/logs.d.ts +++ b/packages/sdk/types/logs.d.ts @@ -1,9 +1,9 @@ export interface Attribute { readonly key: string; - readonly value: string; + readonly value: string | undefined; } export interface Event { - readonly type: "message"; + readonly type: "message" | "transfer"; readonly attributes: readonly Attribute[]; } export interface Log { diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts index 3eaa73c3..962c80c0 100644 --- a/packages/sdk/types/types.d.ts +++ b/packages/sdk/types/types.d.ts @@ -41,9 +41,23 @@ export interface MsgStoreCode extends MsgTemplate { readonly type: "wasm/store-code"; readonly value: ValueStoreCode; } -export declare type Msg = MsgSend | MsgStoreCode | MsgTemplate; +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; +} +export interface MsgInstantiateContract extends MsgTemplate { + readonly type: "wasm/instantiate"; + readonly value: ValueInstantiateContract; +} +export declare type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | 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 interface StdFee { readonly amount: ReadonlyArray; readonly gas: string;